feat: implement hybrid database architecture and frontend encryption

- Add PostgreSQL + SQLite hybrid database support with automatic switching
- Implement frontend AES-GCM + RSA-OAEP encryption for sensitive data
- Add comprehensive DatabaseInterface with all required methods
- Fix compilation issues with interface consistency
- Update all database method signatures to use DatabaseInterface
- Add missing UpdateTraderInitialBalance method to PostgreSQL implementation
- Integrate RSA public key distribution via /api/config endpoint
- Add frontend crypto service with proper error handling
- Support graceful degradation between encrypted and plaintext transmission
- Add directory creation for RSA keys and PEM parsing fixes
- Test both SQLite and PostgreSQL modes successfully
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
icy
2025-11-06 01:50:06 +08:00
parent ba0bb8c365
commit 7d58f56e49
104 changed files with 16864 additions and 4152 deletions

View File

@@ -10,18 +10,18 @@ interface AboutSectionProps {
export default function AboutSection({ language }: AboutSectionProps) {
return (
<AnimatedSection id='about' backgroundColor='var(--brand-dark-gray)'>
<div className='max-w-7xl mx-auto'>
<div className='grid lg:grid-cols-2 gap-12 items-center'>
<AnimatedSection id="about" backgroundColor="var(--brand-dark-gray)">
<div className="max-w-7xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<motion.div
className='space-y-6'
className="space-y-6"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<motion.div
className='inline-flex items-center gap-2 px-4 py-2 rounded-full'
className="inline-flex items-center gap-2 px-4 py-2 rounded-full"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
@@ -29,11 +29,11 @@ export default function AboutSection({ language }: AboutSectionProps) {
whileHover={{ scale: 1.05 }}
>
<Target
className='w-4 h-4'
className="w-4 h-4"
style={{ color: 'var(--brand-yellow)' }}
/>
<span
className='text-sm font-semibold'
className="text-sm font-semibold"
style={{ color: 'var(--brand-yellow)' }}
>
{t('aboutNofx', language)}
@@ -41,45 +41,49 @@ export default function AboutSection({ language }: AboutSectionProps) {
</motion.div>
<h2
className='text-4xl font-bold'
className="text-4xl font-bold"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('whatIsNofx', language)}
</h2>
<p
className='text-lg leading-relaxed'
className="text-lg leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
>
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)} {t('nofxDescription2', language)}
{t('nofxNotAnotherBot', language)}{' '}
{t('nofxDescription1', language)}{' '}
{t('nofxDescription2', language)}
</p>
<p
className='text-lg leading-relaxed'
className="text-lg leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
>
{t('nofxDescription3', language)} {t('nofxDescription4', language)} {t('nofxDescription5', language)}
{t('nofxDescription3', language)}{' '}
{t('nofxDescription4', language)}{' '}
{t('nofxDescription5', language)}
</p>
<motion.div
className='flex items-center gap-3 pt-4'
className="flex items-center gap-3 pt-4"
whileHover={{ x: 5 }}
>
<div
className='w-12 h-12 rounded-full flex items-center justify-center'
className="w-12 h-12 rounded-full flex items-center justify-center"
style={{ background: 'rgba(240, 185, 11, 0.1)' }}
>
<Shield
className='w-6 h-6'
className="w-6 h-6"
style={{ color: 'var(--brand-yellow)' }}
/>
</div>
<div>
<div
className='font-semibold'
className="font-semibold"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('youFullControl', language)}
</div>
<div
className='text-sm'
className="text-sm"
style={{ color: 'var(--text-secondary)' }}
>
{t('fullControlDesc', language)}
@@ -88,9 +92,9 @@ export default function AboutSection({ language }: AboutSectionProps) {
</motion.div>
</motion.div>
<div className='relative'>
<div className="relative">
<div
className='rounded-2xl p-8'
className="rounded-2xl p-8"
style={{
background: 'var(--brand-black)',
border: '1px solid var(--panel-border)',
@@ -108,7 +112,7 @@ export default function AboutSection({ language }: AboutSectionProps) {
]}
typingSpeed={70}
lineDelay={900}
className='text-sm font-mono'
className="text-sm font-mono"
style={{
color: '#00FF88',
textShadow: '0 0 8px rgba(0,255,136,0.4)',
@@ -121,4 +125,3 @@ export default function AboutSection({ language }: AboutSectionProps) {
</AnimatedSection>
)
}

View File

@@ -17,7 +17,7 @@ export default function AnimatedSection({
<motion.section
id={id}
ref={ref}
className='py-20 px-4'
className="py-20 px-4"
style={{ background: backgroundColor }}
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : { opacity: 0 }}
@@ -27,4 +27,3 @@ export default function AnimatedSection({
</motion.section>
)
}

View File

@@ -2,31 +2,40 @@ import { motion } from 'framer-motion'
import AnimatedSection from './AnimatedSection'
interface CardProps {
quote: string;
authorName: string;
handle: string;
avatarUrl: string;
tweetUrl: string;
delay: number;
quote: string
authorName: string
handle: string
avatarUrl: string
tweetUrl: string
delay: number
}
function TestimonialCard({ quote, authorName, delay }: CardProps) {
return (
<motion.div
className='p-6 rounded-xl'
style={{ background: 'var(--brand-dark-gray)', border: '1px solid rgba(240, 185, 11, 0.1)' }}
className="p-6 rounded-xl"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid rgba(240, 185, 11, 0.1)',
}}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay }}
whileHover={{ scale: 1.05 }}
>
<p className='text-lg mb-4' style={{ color: 'var(--brand-light-gray)' }}>
<p className="text-lg mb-4" style={{ color: 'var(--brand-light-gray)' }}>
"{quote}"
</p>
<div className='flex items-center gap-2'>
<div className='w-8 h-8 rounded-full' style={{ background: 'var(--binance-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--text-secondary)' }}>
<div className="flex items-center gap-2">
<div
className="w-8 h-8 rounded-full"
style={{ background: 'var(--binance-yellow)' }}
/>
<span
className="text-sm font-semibold"
style={{ color: 'var(--text-secondary)' }}
>
{authorName}
</span>
</div>
@@ -35,7 +44,9 @@ function TestimonialCard({ quote, authorName, delay }: CardProps) {
}
export default function CommunitySection() {
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } },
}
// 推特内容整合(保持原三列布局,超出自动换行)
const items: CardProps[] = [
@@ -74,12 +85,12 @@ export default function CommunitySection() {
return (
<AnimatedSection>
<div className='max-w-7xl mx-auto'>
<div className="max-w-7xl mx-auto">
<motion.div
className='grid md:grid-cols-3 gap-6'
className="grid md:grid-cols-3 gap-6"
variants={staggerContainer}
initial='initial'
whileInView='animate'
initial="initial"
whileInView="animate"
viewport={{ once: true }}
>
{items.map((item, idx) => (

View File

@@ -10,61 +10,78 @@ interface FeaturesSectionProps {
export default function FeaturesSection({ language }: FeaturesSectionProps) {
return (
<AnimatedSection id='features'>
<div className='max-w-7xl mx-auto'>
<motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<AnimatedSection id="features">
<div className="max-w-7xl mx-auto">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<motion.div
className='inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6'
style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
whileHover={{ scale: 1.05 }}
>
<Rocket className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
<Rocket
className="w-4 h-4"
style={{ color: 'var(--brand-yellow)' }}
/>
<span
className="text-sm font-semibold"
style={{ color: 'var(--brand-yellow)' }}
>
{t('coreFeatures', language)}
</span>
</motion.div>
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
<h2
className="text-4xl font-bold mb-4"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('whyChooseNofx', language)}
</h2>
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
{t('openCommunityDriven', language)}
</p>
</motion.div>
<div className='grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto'>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
<CryptoFeatureCard
icon={<Code className='w-8 h-8' />}
icon={<Code className="w-8 h-8" />}
title={t('openSourceSelfHosted', language)}
description={t('openSourceDesc', language)}
features={[
t('openSourceFeatures1', language),
t('openSourceFeatures2', language),
t('openSourceFeatures3', language),
t('openSourceFeatures4', language)
t('openSourceFeatures4', language),
]}
delay={0}
/>
<CryptoFeatureCard
icon={<Cpu className='w-8 h-8' />}
icon={<Cpu className="w-8 h-8" />}
title={t('multiAgentCompetition', language)}
description={t('multiAgentDesc', language)}
features={[
t('multiAgentFeatures1', language),
t('multiAgentFeatures2', language),
t('multiAgentFeatures3', language),
t('multiAgentFeatures4', language)
t('multiAgentFeatures4', language),
]}
delay={0.1}
/>
<CryptoFeatureCard
icon={<Lock className='w-8 h-8' />}
icon={<Lock className="w-8 h-8" />}
title={t('secureReliableTrading', language)}
description={t('secureDesc', language)}
features={[
t('secureFeatures1', language),
t('secureFeatures2', language),
t('secureFeatures3', language),
t('secureFeatures4', language)
t('secureFeatures4', language),
]}
delay={0.2}
/>
@@ -73,4 +90,3 @@ export default function FeaturesSection({ language }: FeaturesSectionProps) {
</AnimatedSection>
)
}

View File

@@ -6,57 +6,62 @@ interface FooterSectionProps {
export default function FooterSection({ language }: FooterSectionProps) {
return (
<footer style={{ borderTop: '1px solid var(--panel-border)', background: 'var(--brand-dark-gray)' }}>
<div className='max-w-[1200px] mx-auto px-6 py-10'>
<footer
style={{
borderTop: '1px solid var(--panel-border)',
background: 'var(--brand-dark-gray)',
}}
>
<div className="max-w-[1200px] mx-auto px-6 py-10">
{/* Brand */}
<div className='flex items-center gap-3 mb-8'>
<img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
<div className="flex items-center gap-3 mb-8">
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
<div>
<div className='text-lg font-bold' style={{ color: '#EAECEF' }}>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
NOFX
</div>
<div className='text-xs' style={{ color: '#848E9C' }}>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('futureStandardAI', language)}
</div>
</div>
</div>
{/* Multi-link columns */}
<div className='grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 gap-8'>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 gap-8">
<div>
<h3
className='text-sm font-semibold mb-3'
className="text-sm font-semibold mb-3"
style={{ color: '#EAECEF' }}
>
{t('links', language)}
</h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://github.com/tinkle-community/nofx'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://github.com/tinkle-community/nofx"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://t.me/nofx_dev_community'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://t.me/nofx_dev_community"
target="_blank"
rel="noopener noreferrer"
>
Telegram
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://x.com/nofx_ai'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://x.com/nofx_ai"
target="_blank"
rel="noopener noreferrer"
>
X (Twitter)
</a>
@@ -66,38 +71,38 @@ export default function FooterSection({ language }: FooterSectionProps) {
<div>
<h3
className='text-sm font-semibold mb-3'
className="text-sm font-semibold mb-3"
style={{ color: '#EAECEF' }}
>
{t('resources', language)}
</h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://github.com/tinkle-community/nofx/blob/main/README.md'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://github.com/tinkle-community/nofx/blob/main/README.md"
target="_blank"
rel="noopener noreferrer"
>
{t('documentation', language)}
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://github.com/tinkle-community/nofx/issues'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://github.com/tinkle-community/nofx/issues"
target="_blank"
rel="noopener noreferrer"
>
Issues
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://github.com/tinkle-community/nofx/pulls'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://github.com/tinkle-community/nofx/pulls"
target="_blank"
rel="noopener noreferrer"
>
Pull Requests
</a>
@@ -107,50 +112,53 @@ export default function FooterSection({ language }: FooterSectionProps) {
<div>
<h3
className='text-sm font-semibold mb-3'
className="text-sm font-semibold mb-3"
style={{ color: '#EAECEF' }}
>
{t('supporters', language)}
</h3>
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
<ul className="space-y-2 text-sm" style={{ color: '#848E9C' }}>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://asterdex.com/'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://asterdex.com/"
target="_blank"
rel="noopener noreferrer"
>
Aster DEX
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://www.binance.com/'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://www.binance.com/"
target="_blank"
rel="noopener noreferrer"
>
Binance
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://hyperliquid.xyz/'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://hyperliquid.xyz/"
target="_blank"
rel="noopener noreferrer"
>
Hyperliquid
</a>
</li>
<li>
<a
className='hover:text-[#F0B90B]'
href='https://amber.ac/'
target='_blank'
rel='noopener noreferrer'
className="hover:text-[#F0B90B]"
href="https://amber.ac/"
target="_blank"
rel="noopener noreferrer"
>
Amber.ac <span className='opacity-70'>{t('strategicInvestment', language)}</span>
Amber.ac{' '}
<span className="opacity-70">
{t('strategicInvestment', language)}
</span>
</a>
</li>
</ul>
@@ -159,11 +167,14 @@ export default function FooterSection({ language }: FooterSectionProps) {
{/* Bottom note (kept subtle) */}
<div
className='pt-6 mt-8 text-center text-xs'
style={{ color: 'var(--text-tertiary)', borderTop: '1px solid var(--panel-border)' }}
className="pt-6 mt-8 text-center text-xs"
style={{
color: 'var(--text-tertiary)',
borderTop: '1px solid var(--panel-border)',
}}
>
<p>{t('footerTitle', language)}</p>
<p className='mt-1'>{t('footerWarning', language)}</p>
<p className="mt-1">{t('footerWarning', language)}</p>
</div>
</div>
</footer>

View File

@@ -16,7 +16,17 @@ interface HeaderBarProps {
onPageChange?: (page: string) => void
}
export default function HeaderBar({ isLoggedIn = false, isHomePage = false, currentPage, language = 'zh' as Language, onLanguageChange, user, onLogout, isAdminMode = false, onPageChange }: HeaderBarProps) {
export default function HeaderBar({
isLoggedIn = false,
isHomePage = false,
currentPage,
language = 'zh' as Language,
onLanguageChange,
user,
onLogout,
isAdminMode = false,
onPageChange,
}: HeaderBarProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [languageDropdownOpen, setLanguageDropdownOpen] = useState(false)
const [userDropdownOpen, setUserDropdownOpen] = useState(false)
@@ -26,10 +36,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setLanguageDropdownOpen(false)
}
if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) {
if (
userDropdownRef.current &&
!userDropdownRef.current.contains(event.target as Node)
) {
setUserDropdownOpen(false)
}
}
@@ -41,231 +57,311 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
}, [])
return (
<nav className='fixed top-0 w-full z-50 header-bar'>
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'>
<div className='flex items-center justify-between h-16'>
<nav className="fixed top-0 w-full z-50 header-bar">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<a href='/' className='flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer'>
<img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
<span className='text-xl font-bold' style={{ color: 'var(--brand-yellow)' }}>
<a
href="/"
className="flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer"
>
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-8 h-8" />
<span
className="text-xl font-bold"
style={{ color: 'var(--brand-yellow)' }}
>
NOFX
</span>
<span className='text-sm hidden sm:block' style={{ color: 'var(--text-secondary)' }}>
<span
className="text-sm hidden sm:block"
style={{ color: 'var(--text-secondary)' }}
>
Agentic Trading OS
</span>
</a>
{/* Desktop Menu */}
<div className='hidden md:flex items-center justify-between flex-1 ml-8'>
<div className="hidden md:flex items-center justify-between flex-1 ml-8">
{/* Left Side - Navigation Tabs */}
<div className='flex items-center gap-4'>
<div className="flex items-center gap-4">
{isLoggedIn ? (
// Main app navigation when logged in
<>
<button
onClick={() => {
console.log('实时 button clicked, onPageChange:', onPageChange);
onPageChange?.('competition');
console.log(
'实时 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('competition')
}}
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'competition'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
position: 'relative',
}}
onMouseEnter={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-yellow)';
e.currentTarget.style.color = 'var(--brand-yellow)'
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
e.currentTarget.style.color = 'var(--brand-light-gray)'
}
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('realtimeNav', language)}
</button>
<button
onClick={() => {
console.log('配置 button clicked, onPageChange:', onPageChange);
onPageChange?.('traders');
console.log(
'配置 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('traders')
}}
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'traders'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
position: 'relative',
}}
onMouseEnter={(e) => {
if (currentPage !== 'traders') {
e.currentTarget.style.color = 'var(--brand-yellow)';
e.currentTarget.style.color = 'var(--brand-yellow)'
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'traders') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
e.currentTarget.style.color = 'var(--brand-light-gray)'
}
}}
>
{/* Background for selected state */}
{currentPage === 'traders' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('configNav', language)}
</button>
<button
onClick={() => {
console.log('看板 button clicked, onPageChange:', onPageChange);
onPageChange?.('trader');
console.log(
'看板 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('trader')
}}
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'trader'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
position: 'relative',
}}
onMouseEnter={(e) => {
if (currentPage !== 'trader') {
e.currentTarget.style.color = 'var(--brand-yellow)';
e.currentTarget.style.color = 'var(--brand-yellow)'
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'trader') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
e.currentTarget.style.color = 'var(--brand-light-gray)'
}
}}
>
{/* Background for selected state */}
{currentPage === 'trader' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('dashboardNav', language)}
</button>
</>
) : (
// Landing page navigation when not logged in
<a
href='/competition'
className='text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
href="/competition"
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'competition'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '8px 16px',
borderRadius: '8px',
position: 'relative'
position: 'relative',
}}
onMouseEnter={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-yellow)';
e.currentTarget.style.color = 'var(--brand-yellow)'
}
}}
onMouseLeave={(e) => {
if (currentPage !== 'competition') {
e.currentTarget.style.color = 'var(--brand-light-gray)';
e.currentTarget.style.color = 'var(--brand-light-gray)'
}
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('realtimeNav', language)}
</a>
)}
</div>
{/* Right Side - Original Navigation Items and Login */}
<div className='flex items-center gap-6'>
<div className="flex items-center gap-6">
{/* Only show original navigation items on home page */}
{isHomePage && [
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) }
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
className='text-sm transition-colors relative group'
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
<span
className='absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300'
style={{ background: 'var(--brand-yellow)' }}
/>
</a>
))}
{isHomePage &&
[
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) },
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={
item.key === 'GitHub' || item.key === 'community'
? '_blank'
: undefined
}
rel={
item.key === 'GitHub' || item.key === 'community'
? 'noopener noreferrer'
: undefined
}
className="text-sm transition-colors relative group"
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
<span
className="absolute -bottom-1 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
style={{ background: 'var(--brand-yellow)' }}
/>
</a>
))}
{/* User Info and Actions */}
{isLoggedIn && user ? (
<div className='flex items-center gap-3'>
<div className="flex items-center gap-3">
{/* User Info with Dropdown */}
<div className='relative' ref={userDropdownRef}>
<div className="relative" ref={userDropdownRef}>
<button
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--panel-bg)'}
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
style={{
background: 'var(--panel-bg)',
border: '1px solid var(--panel-border)',
}}
onMouseEnter={(e) =>
(e.currentTarget.style.background =
'rgba(255, 255, 255, 0.05)')
}
onMouseLeave={(e) =>
(e.currentTarget.style.background = 'var(--panel-bg)')
}
>
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
>
{user.email[0].toUpperCase()}
</div>
<span className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</span>
<ChevronDown className='w-4 h-4' style={{ color: 'var(--brand-light-gray)' }} />
<span
className="text-sm"
style={{ color: 'var(--brand-light-gray)' }}
>
{user.email}
</span>
<ChevronDown
className="w-4 h-4"
style={{ color: 'var(--brand-light-gray)' }}
/>
</button>
{userDropdownOpen && (
<div className='absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
<div className='px-3 py-2 border-b' style={{ borderColor: 'var(--panel-border)' }}>
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
<div className='text-sm font-medium' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
<div
className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid var(--panel-border)',
}}
>
<div
className="px-3 py-2 border-b"
style={{ borderColor: 'var(--panel-border)' }}
>
<div
className="text-xs"
style={{ color: 'var(--text-secondary)' }}
>
{t('loggedInAs', language)}
</div>
<div
className="text-sm font-medium"
style={{ color: 'var(--brand-light-gray)' }}
>
{user.email}
</div>
</div>
{!isAdminMode && onLogout && (
<button
@@ -273,10 +369,13 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
onLogout()
setUserDropdownOpen(false)
}}
className='w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center'
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
style={{
background: 'var(--binance-red-bg)',
color: 'var(--binance-red)',
}}
>
{t('exitLogin', language)}
{t('exitLogin', language)}
</button>
)}
</div>
@@ -285,43 +384,58 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
</div>
) : (
/* Show login/register buttons when not logged in and not on login/register pages */
currentPage !== 'login' && currentPage !== 'register' && (
<div className='flex items-center gap-3'>
currentPage !== 'login' &&
currentPage !== 'register' && (
<div className="flex items-center gap-3">
<a
href='/login'
className='px-3 py-2 text-sm font-medium transition-colors rounded'
href="/login"
className="px-3 py-2 text-sm font-medium transition-colors rounded"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('signIn', language)}
{t('signIn', language)}
</a>
<a
href='/register'
className='px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90'
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
href="/register"
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
>
{t('signUp', language)}
{t('signUp', language)}
</a>
</div>
)
)}
{/* Language Toggle - Always at the rightmost */}
<div className='relative' ref={dropdownRef}>
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
className='flex items-center gap-2 px-3 py-2 rounded transition-colors'
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
style={{ color: 'var(--brand-light-gray)' }}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
onMouseEnter={(e) =>
(e.currentTarget.style.background =
'rgba(255, 255, 255, 0.05)')
}
onMouseLeave={(e) =>
(e.currentTarget.style.background = 'transparent')
}
>
<span className='text-lg'>
<span className="text-lg">
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
</span>
<ChevronDown className='w-4 h-4' />
<ChevronDown className="w-4 h-4" />
</button>
{languageDropdownOpen && (
<div className='absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50' style={{ background: 'var(--brand-dark-gray)', border: '1px solid var(--panel-border)' }}>
<div
className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid var(--panel-border)',
}}
>
<button
onClick={() => {
onLanguageChange?.('zh')
@@ -330,13 +444,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
language === 'zh' ? '' : 'hover:opacity-80'
}`}
style={{
style={{
color: 'var(--brand-light-gray)',
background: language === 'zh' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
background:
language === 'zh'
? 'rgba(240, 185, 11, 0.1)'
: 'transparent',
}}
>
<span className='text-base'>🇨🇳</span>
<span className='text-sm'></span>
<span className="text-base">🇨🇳</span>
<span className="text-sm"></span>
</button>
<button
onClick={() => {
@@ -346,13 +463,16 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
language === 'en' ? '' : 'hover:opacity-80'
}`}
style={{
style={{
color: 'var(--brand-light-gray)',
background: language === 'en' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
background:
language === 'en'
? 'rgba(240, 185, 11, 0.1)'
: 'transparent',
}}
>
<span className='text-base'>🇺🇸</span>
<span className='text-sm'>English</span>
<span className="text-base">🇺🇸</span>
<span className="text-sm">English</span>
</button>
</div>
)}
@@ -363,11 +483,15 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
{/* Mobile Menu Button */}
<motion.button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className='md:hidden'
className="md:hidden"
style={{ color: 'var(--brand-light-gray)' }}
whileTap={{ scale: 0.9 }}
>
{mobileMenuOpen ? <X className='w-6 h-6' /> : <Menu className='w-6 h-6' />}
{mobileMenuOpen ? (
<X className="w-6 h-6" />
) : (
<Menu className="w-6 h-6" />
)}
</motion.button>
</div>
</div>
@@ -375,65 +499,81 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
{/* Mobile Menu */}
<motion.div
initial={false}
animate={mobileMenuOpen ? { height: 'auto', opacity: 1 } : { height: 0, opacity: 0 }}
animate={
mobileMenuOpen
? { height: 'auto', opacity: 1 }
: { height: 0, opacity: 0 }
}
transition={{ duration: 0.3 }}
className='md:hidden overflow-hidden'
style={{ background: 'var(--brand-dark-gray)', borderTop: '1px solid rgba(240, 185, 11, 0.1)' }}
className="md:hidden overflow-hidden"
style={{
background: 'var(--brand-dark-gray)',
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
}}
>
<div className='px-4 py-4 space-y-3'>
<div className="px-4 py-4 space-y-3">
{/* New Navigation Tabs */}
{isLoggedIn ? (
<button
onClick={() => {
console.log('移动端 实时 button clicked, onPageChange:', onPageChange);
console.log(
'移动端 实时 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('competition')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'competition'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
textAlign: 'left',
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('realtimeNav', language)}
</button>
) : (
<a
href='/competition'
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500'
<a
href="/competition"
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
style={{
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'competition'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative'
position: 'relative',
}}
>
{/* Background for selected state */}
{currentPage === 'competition' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('realtimeNav', language)}
</a>
)}
@@ -442,107 +582,135 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
<>
<button
onClick={() => {
console.log('移动端 配置 button clicked, onPageChange:', onPageChange);
console.log(
'移动端 配置 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('traders')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
style={{
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'traders'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
textAlign: 'left',
}}
>
{/* Background for selected state */}
{currentPage === 'traders' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('configNav', language)}
</button>
<button
onClick={() => {
console.log('移动端 看板 button clicked, onPageChange:', onPageChange);
console.log(
'移动端 看板 button clicked, onPageChange:',
onPageChange
)
onPageChange?.('trader')
setMobileMenuOpen(false)
}}
className='block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500'
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
style={{
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
color:
currentPage === 'trader'
? 'var(--brand-yellow)'
: 'var(--brand-light-gray)',
padding: '12px 16px',
borderRadius: '8px',
position: 'relative',
width: '100%',
textAlign: 'left'
textAlign: 'left',
}}
>
{/* Background for selected state */}
{currentPage === 'trader' && (
<span
<span
className="absolute inset-0 rounded-lg"
style={{
background: 'rgba(240, 185, 11, 0.15)',
zIndex: -1
zIndex: -1,
}}
/>
)}
{t('dashboardNav', language)}
</button>
</>
)}
{/* Original Navigation Items - Only on home page */}
{isHomePage && [
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) }
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={item.key === 'GitHub' || item.key === 'community' ? '_blank' : undefined}
rel={item.key === 'GitHub' || item.key === 'community' ? 'noopener noreferrer' : undefined}
className='block text-sm py-2'
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
</a>
))}
{isHomePage &&
[
{ key: 'features', label: t('features', language) },
{ key: 'howItWorks', label: t('howItWorks', language) },
{ key: 'GitHub', label: 'GitHub' },
{ key: 'community', label: t('community', language) },
].map((item) => (
<a
key={item.key}
href={
item.key === 'GitHub'
? 'https://github.com/tinkle-community/nofx'
: item.key === 'community'
? 'https://t.me/nofx_dev_community'
: `#${item.key === 'features' ? 'features' : 'how-it-works'}`
}
target={
item.key === 'GitHub' || item.key === 'community'
? '_blank'
: undefined
}
rel={
item.key === 'GitHub' || item.key === 'community'
? 'noopener noreferrer'
: undefined
}
className="block text-sm py-2"
style={{ color: 'var(--brand-light-gray)' }}
>
{item.label}
</a>
))}
{/* Language Toggle */}
<div className='py-2'>
<div className='flex items-center gap-2 mb-2'>
<span className='text-xs' style={{ color: 'var(--brand-light-gray)' }}>{t('language', language)}:</span>
<div className="py-2">
<div className="flex items-center gap-2 mb-2">
<span
className="text-xs"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('language', language)}:
</span>
</div>
<div className='space-y-1'>
<div className="space-y-1">
<button
onClick={() => {
onLanguageChange?.('zh')
setMobileMenuOpen(false)
}}
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
language === 'zh' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
language === 'zh'
? 'bg-yellow-500 text-black'
: 'text-gray-400 hover:text-white'
}`}
>
<span className='text-lg'>🇨🇳</span>
<span className='text-sm'></span>
<span className="text-lg">🇨🇳</span>
<span className="text-sm"></span>
</button>
<button
onClick={() => {
@@ -550,25 +718,49 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
setMobileMenuOpen(false)
}}
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
language === 'en' ? 'bg-yellow-500 text-black' : 'text-gray-400 hover:text-white'
language === 'en'
? 'bg-yellow-500 text-black'
: 'text-gray-400 hover:text-white'
}`}
>
<span className='text-lg'>🇺🇸</span>
<span className='text-sm'>English</span>
<span className="text-lg">🇺🇸</span>
<span className="text-sm">English</span>
</button>
</div>
</div>
{/* User info and logout for mobile when logged in */}
{isLoggedIn && user && (
<div className='mt-4 pt-4' style={{ borderTop: '1px solid var(--panel-border)' }}>
<div className='flex items-center gap-2 px-3 py-2 mb-2 rounded' style={{ background: 'var(--panel-bg)' }}>
<div className='w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}>
<div
className="mt-4 pt-4"
style={{ borderTop: '1px solid var(--panel-border)' }}
>
<div
className="flex items-center gap-2 px-3 py-2 mb-2 rounded"
style={{ background: 'var(--panel-bg)' }}
>
<div
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
>
{user.email[0].toUpperCase()}
</div>
<div>
<div className='text-xs' style={{ color: 'var(--text-secondary)' }}>{t('loggedInAs', language)}</div>
<div className='text-sm' style={{ color: 'var(--brand-light-gray)' }}>{user.email}</div>
<div
className="text-xs"
style={{ color: 'var(--text-secondary)' }}
>
{t('loggedInAs', language)}
</div>
<div
className="text-sm"
style={{ color: 'var(--brand-light-gray)' }}
>
{user.email}
</div>
</div>
</div>
{!isAdminMode && onLogout && (
@@ -577,8 +769,11 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
onLogout()
setMobileMenuOpen(false)
}}
className='w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center'
style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
style={{
background: 'var(--binance-red-bg)',
color: 'var(--binance-red)',
}}
>
{t('exitLogin', language)}
</button>
@@ -587,29 +782,36 @@ export default function HeaderBar({ isLoggedIn = false, isHomePage = false, curr
)}
{/* Show login/register buttons when not logged in and not on login/register pages */}
{!isLoggedIn && currentPage !== 'login' && currentPage !== 'register' && (
<div className='space-y-2 mt-2'>
<a
href='/login'
className='block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors'
style={{ color: 'var(--brand-light-gray)', border: '1px solid var(--brand-light-gray)' }}
onClick={() => setMobileMenuOpen(false)}
>
{t('signIn', language)}
</a>
<a
href='/register'
className='block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors'
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
onClick={() => setMobileMenuOpen(false)}
>
{t('signUp', language)}
</a>
</div>
)}
{!isLoggedIn &&
currentPage !== 'login' &&
currentPage !== 'register' && (
<div className="space-y-2 mt-2">
<a
href="/login"
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors"
style={{
color: 'var(--brand-light-gray)',
border: '1px solid var(--brand-light-gray)',
}}
onClick={() => setMobileMenuOpen(false)}
>
{t('signIn', language)}
</a>
<a
href="/register"
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
onClick={() => setMobileMenuOpen(false)}
>
{t('signUp', language)}
</a>
</div>
)}
</div>
</motion.div>
</nav>
)
}

View File

@@ -1,6 +1,8 @@
import { motion, useScroll, useTransform, useAnimation } from 'framer-motion'
import { Sparkles } from 'lucide-react'
import { t, Language } from '../../i18n/translations'
import { useGitHubStats } from '../../hooks/useGitHubStats'
import { useCounterAnimation } from '../../hooks/useCounterAnimation'
interface HeroSectionProps {
language: Language
@@ -11,75 +13,151 @@ export default function HeroSection({ language }: HeroSectionProps) {
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
const handControls = useAnimation()
const { stars, daysOld, isLoading } = useGitHubStats('NoFxAiOS', 'nofx')
// 动画数字 - 仅对 stars 添加动画
const animatedStars = useCounterAnimation({
start: 0,
end: stars,
duration: 2000,
})
const fadeInUp = {
initial: { opacity: 0, y: 60 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.6, ease: [0.6, -0.05, 0.01, 0.99] },
}
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
const staggerContainer = {
animate: { transition: { staggerChildren: 0.1 } },
}
return (
<section className='relative pt-32 pb-20 px-4'>
<div className='max-w-7xl mx-auto'>
<div className='grid lg:grid-cols-2 gap-12 items-center'>
<section className="relative pt-32 pb-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Content */}
<motion.div className='space-y-6 relative z-10' style={{ opacity, scale }} initial='initial' animate='animate' variants={staggerContainer}>
<motion.div
className="space-y-6 relative z-10"
style={{ opacity, scale }}
initial="initial"
animate="animate"
variants={staggerContainer}
>
<motion.div variants={fadeInUp}>
<motion.div
className='inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6'
style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
whileHover={{ scale: 1.05, boxShadow: '0 0 20px rgba(240, 185, 11, 0.2)' }}
className="inline-flex items-center gap-2 px-4 py-2 rounded-full mb-6"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
whileHover={{
scale: 1.05,
boxShadow: '0 0 20px rgba(240, 185, 11, 0.2)',
}}
>
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
{t('githubStarsInDays', language)}
<Sparkles
className="w-4 h-4"
style={{ color: 'var(--brand-yellow)' }}
/>
<span
className="text-sm font-semibold"
style={{ color: 'var(--brand-yellow)' }}
>
{isLoading ? (
t('githubStarsInDays', language)
) : language === 'zh' ? (
<>
{daysOld} {' '}
<span className="inline-block tabular-nums">
{(animatedStars / 1000).toFixed(1)}
</span>
K+ GitHub Stars
</>
) : (
<>
<span className="inline-block tabular-nums">
{(animatedStars / 1000).toFixed(1)}
</span>
K+ GitHub Stars in {daysOld} days
</>
)}
</span>
</motion.div>
</motion.div>
<h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}>
<h1
className="text-5xl lg:text-7xl font-bold leading-tight"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('heroTitle1', language)}
<br />
<span style={{ color: 'var(--brand-yellow)' }}>{t('heroTitle2', language)}</span>
<span style={{ color: 'var(--brand-yellow)' }}>
{t('heroTitle2', language)}
</span>
</h1>
<motion.p className='text-xl leading-relaxed' style={{ color: 'var(--text-secondary)' }} variants={fadeInUp}>
<motion.p
className="text-xl leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
variants={fadeInUp}
>
{t('heroDescription', language)}
</motion.p>
<div className='flex items-center gap-3 flex-wrap'>
<motion.a href='https://github.com/tinkle-community/nofx' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<div className="flex items-center gap-3 flex-wrap">
<motion.a
href="https://github.com/tinkle-community/nofx"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
transition={{ type: 'spring', stiffness: 400 }}
>
<img
src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Stars'
className='h-7'
src="https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
alt="GitHub Stars"
className="h-7"
/>
</motion.a>
<motion.a href='https://github.com/tinkle-community/nofx/network/members' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<motion.a
href="https://github.com/tinkle-community/nofx/network/members"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
transition={{ type: 'spring', stiffness: 400 }}
>
<img
src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Forks'
className='h-7'
src="https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
alt="GitHub Forks"
className="h-7"
/>
</motion.a>
<motion.a href='https://github.com/tinkle-community/nofx/graphs/contributors' target='_blank' rel='noopener noreferrer' whileHover={{ scale: 1.05 }} transition={{ type: 'spring', stiffness: 400 }}>
<motion.a
href="https://github.com/tinkle-community/nofx/graphs/contributors"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
transition={{ type: 'spring', stiffness: 400 }}
>
<img
src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A'
alt='GitHub Contributors'
className='h-7'
src="https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=0A0A0A"
alt="GitHub Contributors"
className="h-7"
/>
</motion.a>
</div>
<motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}>
{t('poweredBy', language)}
<motion.p
className="text-xs pt-4"
style={{ color: 'var(--text-tertiary)' }}
variants={fadeInUp}
>
{t('poweredBy', language)}
</motion.p>
</motion.div>
{/* Right Visual - Interactive Robot */}
<div
className='relative w-full cursor-pointer'
<div
className="relative w-full cursor-pointer"
onMouseEnter={() => {
handControls.start({
y: [-8, 8, -8],
@@ -88,9 +166,9 @@ export default function HeroSection({ language }: HeroSectionProps) {
transition: {
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1]
}
ease: 'easeInOut',
times: [0, 0.5, 1],
},
})
}}
onMouseLeave={() => {
@@ -100,32 +178,32 @@ export default function HeroSection({ language }: HeroSectionProps) {
x: 0,
transition: {
duration: 0.6,
ease: "easeOut"
}
ease: 'easeOut',
},
})
}}
>
{/* Background Layer */}
<motion.img
src='/images/hand-bg.png'
alt='NOFX Platform Background'
className='w-full opacity-90'
<motion.img
src="/images/hand-bg.png"
alt="NOFX Platform Background"
className="w-full opacity-90"
style={{ opacity, scale }}
whileHover={{ scale: 1.02 }}
transition={{ type: 'spring', stiffness: 300 }}
/>
{/* Hand Layer - Animated */}
<motion.img
src='/images/hand.png'
alt='Robot Hand'
className='absolute top-0 left-0 w-full'
<motion.img
src="/images/hand.png"
alt="Robot Hand"
className="absolute top-0 left-0 w-full"
style={{ opacity }}
animate={handControls}
initial={{ y: 0, rotate: 0, x: 0 }}
whileHover={{
scale: 1.05,
transition: { type: 'spring', stiffness: 400 }
transition: { type: 'spring', stiffness: 400 },
}}
/>
</div>
@@ -134,4 +212,3 @@ export default function HeroSection({ language }: HeroSectionProps) {
</section>
)
}

View File

@@ -4,20 +4,36 @@ import { t, Language } from '../../i18n/translations'
function StepCard({ number, title, description, delay }: any) {
return (
<motion.div className='flex gap-6 items-start' initial={{ opacity: 0, x: -50 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ delay }} whileHover={{ x: 10 }}>
<motion.div
className="flex gap-6 items-start"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay }}
whileHover={{ x: 10 }}
>
<motion.div
className='flex-shrink-0 w-14 h-14 rounded-full flex items-center justify-center font-bold text-2xl'
style={{ background: 'var(--binance-yellow)', color: 'var(--brand-black)' }}
className="flex-shrink-0 w-14 h-14 rounded-full flex items-center justify-center font-bold text-2xl"
style={{
background: 'var(--binance-yellow)',
color: 'var(--brand-black)',
}}
whileHover={{ scale: 1.2, rotate: 360 }}
transition={{ type: 'spring', stiffness: 260, damping: 20 }}
>
{number}
</motion.div>
<div>
<h3 className='text-2xl font-semibold mb-2' style={{ color: 'var(--brand-light-gray)' }}>
<h3
className="text-2xl font-semibold mb-2"
style={{ color: 'var(--brand-light-gray)' }}
>
{title}
</h3>
<p className='text-lg leading-relaxed' style={{ color: 'var(--text-secondary)' }}>
<p
className="text-lg leading-relaxed"
style={{ color: 'var(--text-secondary)' }}
>
{description}
</p>
</div>
@@ -29,46 +45,91 @@ interface HowItWorksSectionProps {
language: Language
}
export default function HowItWorksSection({ language }: HowItWorksSectionProps) {
export default function HowItWorksSection({
language,
}: HowItWorksSectionProps) {
return (
<AnimatedSection id='how-it-works' backgroundColor='var(--brand-dark-gray)'>
<div className='max-w-7xl mx-auto'>
<motion.div className='text-center mb-16' initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
<h2 className='text-4xl font-bold mb-4' style={{ color: 'var(--brand-light-gray)' }}>
<AnimatedSection id="how-it-works" backgroundColor="var(--brand-dark-gray)">
<div className="max-w-7xl mx-auto">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2
className="text-4xl font-bold mb-4"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('howToStart', language)}
</h2>
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
<p className="text-lg" style={{ color: 'var(--text-secondary)' }}>
{t('fourSimpleSteps', language)}
</p>
</motion.div>
<div className='space-y-8'>
<div className="space-y-8">
{[
{ number: 1, title: t('step1Title', language), description: t('step1Desc', language) },
{ number: 2, title: t('step2Title', language), description: t('step2Desc', language) },
{ number: 3, title: t('step3Title', language), description: t('step3Desc', language) },
{ number: 4, title: t('step4Title', language), description: t('step4Desc', language) },
{
number: 1,
title: t('step1Title', language),
description: t('step1Desc', language),
},
{
number: 2,
title: t('step2Title', language),
description: t('step2Desc', language),
},
{
number: 3,
title: t('step3Title', language),
description: t('step3Desc', language),
},
{
number: 4,
title: t('step4Title', language),
description: t('step4Desc', language),
},
].map((step, index) => (
<StepCard key={step.number} {...step} delay={index * 0.1} />
))}
</div>
<motion.div
className='mt-12 p-6 rounded-xl flex items-start gap-4'
style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.3)' }}
className="mt-12 p-6 rounded-xl flex items-start gap-4"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
}}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
whileHover={{ scale: 1.02 }}
>
<div className='w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0' style={{ background: 'rgba(246, 70, 93, 0.2)', color: '#F6465D' }}>
<svg xmlns='http://www.w3.org/2000/svg' className='w-6 h-6' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'><path d='M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z'/><line x1='12' x2='12' y1='9' y2='13'/><line x1='12' x2='12.01' y1='17' y2='17'/></svg>
<div
className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0"
style={{ background: 'rgba(246, 70, 93, 0.2)', color: '#F6465D' }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
<line x1="12" x2="12" y1="9" y2="13" />
<line x1="12" x2="12.01" y1="17" y2="17" />
</svg>
</div>
<div>
<div className='font-semibold mb-2' style={{ color: '#F6465D' }}>
<div className="font-semibold mb-2" style={{ color: '#F6465D' }}>
{t('importantRiskWarning', language)}
</div>
<p className='text-sm' style={{ color: 'var(--text-secondary)' }}>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
{t('riskWarningText', language)}
</p>
</div>

View File

@@ -10,7 +10,7 @@ interface LoginModalProps {
export default function LoginModal({ onClose, language }: LoginModalProps) {
return (
<motion.div
className='fixed inset-0 z-50 flex items-center justify-center p-4'
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(0, 0, 0, 0.8)' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -18,32 +18,50 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
onClick={onClose}
>
<motion.div
className='relative max-w-md w-full rounded-2xl p-8'
style={{ background: 'var(--brand-dark-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
className="relative max-w-md w-full rounded-2xl p-8"
style={{
background: 'var(--brand-dark-gray)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
initial={{ scale: 0.9, y: 50 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 50 }}
onClick={(e) => e.stopPropagation()}
>
<motion.button onClick={onClose} className='absolute top-4 right-4' style={{ color: 'var(--text-secondary)' }} whileHover={{ scale: 1.1, rotate: 90 }} whileTap={{ scale: 0.9 }}>
<X className='w-6 h-6' />
<motion.button
onClick={onClose}
className="absolute top-4 right-4"
style={{ color: 'var(--text-secondary)' }}
whileHover={{ scale: 1.1, rotate: 90 }}
whileTap={{ scale: 0.9 }}
>
<X className="w-6 h-6" />
</motion.button>
<h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}>
<h2
className="text-2xl font-bold mb-6"
style={{ color: 'var(--brand-light-gray)' }}
>
{t('accessNofxPlatform', language)}
</h2>
<p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}>
<p className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
{t('loginRegisterPrompt', language)}
</p>
<div className='space-y-3'>
<div className="space-y-3">
<motion.button
onClick={() => {
window.history.pushState({}, '', '/login')
window.dispatchEvent(new PopStateEvent('popstate'))
onClose()
}}
className='block w-full px-6 py-3 rounded-lg font-semibold text-center'
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }}
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
style={{
background: 'var(--brand-yellow)',
color: 'var(--brand-black)',
}}
whileHover={{
scale: 1.05,
boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)',
}}
whileTap={{ scale: 0.95 }}
>
{t('signIn', language)}
@@ -54,8 +72,12 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
window.dispatchEvent(new PopStateEvent('popstate'))
onClose()
}}
className='block w-full px-6 py-3 rounded-lg font-semibold text-center'
style={{ background: 'var(--brand-dark-gray)', color: 'var(--brand-light-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
className="block w-full px-6 py-3 rounded-lg font-semibold text-center"
style={{
background: 'var(--brand-dark-gray)',
color: 'var(--brand-light-gray)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
whileTap={{ scale: 0.95 }}
>
@@ -66,4 +88,3 @@ export default function LoginModal({ onClose, language }: LoginModalProps) {
</motion.div>
)
}