mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 04:20:59 +08:00
fix: resolve merge conflicts in RegisterPage.tsx
- Remove merge conflict markers - Keep HeaderBar import instead of ArrowLeft - Remove duplicate betaMode state declaration - Remove duplicate useEffect hook - Update translation keys to use authStep* variants - Ensure proper beta mode functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
261
web/src/App.tsx
261
web/src/App.tsx
@@ -7,12 +7,12 @@ import { LoginPage } from './components/LoginPage';
|
||||
import { RegisterPage } from './components/RegisterPage';
|
||||
import { CompetitionPage } from './components/CompetitionPage';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import HeaderBar from './components/landing/HeaderBar';
|
||||
import AILearning from './components/AILearning';
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { t, type Language } from './i18n/translations';
|
||||
import { useSystemConfig } from './hooks/useSystemConfig';
|
||||
import { Zap } from 'lucide-react';
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -44,29 +44,42 @@ function App() {
|
||||
const { config: systemConfig, loading: configLoading } = useSystemConfig();
|
||||
const [route, setRoute] = useState(window.location.pathname);
|
||||
|
||||
// 从URL hash读取初始页面状态(支持刷新保持页面)
|
||||
// 从URL路径读取初始页面状态(支持刷新保持页面)
|
||||
const getInitialPage = (): Page => {
|
||||
const path = window.location.pathname;
|
||||
const hash = window.location.hash.slice(1); // 去掉 #
|
||||
return hash === 'trader' || hash === 'details' ? 'trader' : 'competition';
|
||||
|
||||
if (path === '/traders' || hash === 'traders') return 'traders';
|
||||
if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader';
|
||||
return 'competition'; // 默认为竞赛页面
|
||||
};
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<Page>(getInitialPage());
|
||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||
|
||||
// 监听URL hash变化,同步页面状态
|
||||
// 监听URL变化,同步页面状态
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const handleRouteChange = () => {
|
||||
const path = window.location.pathname;
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash === 'trader' || hash === 'details') {
|
||||
|
||||
if (path === '/traders' || hash === 'traders') {
|
||||
setCurrentPage('traders');
|
||||
} else if (path === '/dashboard' || hash === 'trader' || hash === 'details') {
|
||||
setCurrentPage('trader');
|
||||
} else if (hash === 'competition' || hash === '') {
|
||||
} else if (path === '/competition' || hash === 'competition' || hash === '') {
|
||||
setCurrentPage('competition');
|
||||
}
|
||||
setRoute(path);
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
window.addEventListener('hashchange', handleRouteChange);
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleRouteChange);
|
||||
window.removeEventListener('popstate', handleRouteChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展)
|
||||
@@ -166,153 +179,137 @@ function App() {
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, []);
|
||||
|
||||
// Set current page based on route for consistent navigation state
|
||||
useEffect(() => {
|
||||
if (route === '/competition') {
|
||||
setCurrentPage('competition');
|
||||
} else if (route === '/traders') {
|
||||
setCurrentPage('traders');
|
||||
} else if (route === '/dashboard') {
|
||||
setCurrentPage('trader');
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
// Show loading spinner while checking auth or config
|
||||
if (isLoading || configLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||
<div className="text-center">
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
||||
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
||||
<p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show landing page for root route when not authenticated
|
||||
// Handle specific routes regardless of authentication
|
||||
if (route === '/login') {
|
||||
return <LoginPage />;
|
||||
}
|
||||
if (route === '/register') {
|
||||
return <RegisterPage />;
|
||||
}
|
||||
if (route === '/competition') {
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: '#000000', color: '#EAECEF' }}>
|
||||
<HeaderBar
|
||||
|
||||
isLoggedIn={!!user}
|
||||
currentPage="competition"
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
isAdminMode={systemConfig?.admin_mode}
|
||||
onPageChange={(page) => {
|
||||
console.log('Competition page onPageChange called with:', page);
|
||||
console.log('Current route:', route, 'Current page:', currentPage);
|
||||
|
||||
if (page === 'competition') {
|
||||
console.log('Navigating to competition');
|
||||
window.history.pushState({}, '', '/competition');
|
||||
setRoute('/competition');
|
||||
setCurrentPage('competition');
|
||||
} else if (page === 'traders') {
|
||||
console.log('Navigating to traders');
|
||||
window.history.pushState({}, '', '/traders');
|
||||
setRoute('/traders');
|
||||
setCurrentPage('traders');
|
||||
} else if (page === 'trader') {
|
||||
console.log('Navigating to trader/dashboard');
|
||||
window.history.pushState({}, '', '/dashboard');
|
||||
setRoute('/dashboard');
|
||||
setCurrentPage('trader');
|
||||
}
|
||||
|
||||
console.log('After navigation - route:', route, 'currentPage:', currentPage);
|
||||
}}
|
||||
/>
|
||||
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
|
||||
<CompetitionPage />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show landing page for root route
|
||||
if (route === '/' || route === '') {
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
// Show main app for authenticated users on other routes
|
||||
if (!systemConfig?.admin_mode && (!user || !token)) {
|
||||
if (route === '/login') {
|
||||
return <LoginPage />;
|
||||
}
|
||||
if (route === '/register') {
|
||||
return <RegisterPage />;
|
||||
}
|
||||
// Default to landing page when not authenticated
|
||||
// Default to landing page when not authenticated and no specific route
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
||||
{/* Header - Binance Style */}
|
||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-4">
|
||||
<div className="relative flex items-center">
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
</h1>
|
||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
{t('subtitle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center - Page Toggle (absolutely positioned) */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('competition')}
|
||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'competition'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('aiCompetition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'traders'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('aiTraders', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('tradingPanel', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right - Actions */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
|
||||
{/* User Info - Only show if not in admin mode */}
|
||||
{!systemConfig?.admin_mode && user && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span className="text-sm" style={{ color: '#EAECEF' }}>{user.email}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Admin Mode Indicator */}
|
||||
{systemConfig?.admin_mode && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setLanguage('zh')}
|
||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
||||
style={language === 'zh'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
中文
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLanguage('en')}
|
||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
||||
style={language === 'en'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout Button - Only show if not in admin mode */}
|
||||
{!systemConfig?.admin_mode && (
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
||||
>
|
||||
{t('logout', language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="min-h-screen" style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
|
||||
<HeaderBar
|
||||
|
||||
isLoggedIn={!!user}
|
||||
isHomePage={false}
|
||||
currentPage={currentPage}
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
isAdminMode={systemConfig?.admin_mode}
|
||||
onPageChange={(page) => {
|
||||
console.log('App.tsx onPageChange called with:', page);
|
||||
console.log('Current route:', route, 'Current page:', currentPage);
|
||||
|
||||
if (page === 'competition') {
|
||||
console.log('Navigating to competition');
|
||||
window.history.pushState({}, '', '/competition');
|
||||
setRoute('/competition');
|
||||
setCurrentPage('competition');
|
||||
} else if (page === 'traders') {
|
||||
console.log('Navigating to traders');
|
||||
window.history.pushState({}, '', '/traders');
|
||||
setRoute('/traders');
|
||||
setCurrentPage('traders');
|
||||
} else if (page === 'trader') {
|
||||
console.log('Navigating to trader/dashboard');
|
||||
window.history.pushState({}, '', '/dashboard');
|
||||
setRoute('/dashboard');
|
||||
setCurrentPage('trader');
|
||||
}
|
||||
|
||||
console.log('After navigation - route:', route, 'currentPage:', currentPage);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-[1920px] mx-auto px-6 py-6">
|
||||
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
|
||||
{currentPage === 'competition' ? (
|
||||
<CompetitionPage />
|
||||
) : currentPage === 'traders' ? (
|
||||
<AITradersPage
|
||||
onTraderSelect={(traderId) => {
|
||||
setSelectedTraderId(traderId);
|
||||
window.history.pushState({}, '', '/dashboard');
|
||||
setRoute('/dashboard');
|
||||
setCurrentPage('trader');
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -31,6 +31,8 @@ export function CompetitionPage() {
|
||||
setIsModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch trader config:', error);
|
||||
// 对于未登录用户,不显示详细配置,这是正常行为
|
||||
// 竞赛页面主要用于查看排行榜和基本信息
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl",
|
||||
"bg-gradient-to-br from-[#0C0E12] to-[#1E2329]",
|
||||
"border-[#2B3139] hover:border-[#F0B90B]/50",
|
||||
"bg-gradient-to-br from-[#000000] to-[#0A0A0A]",
|
||||
"border-[#1A1A1A] hover:border-[#F0B90B]/50",
|
||||
isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]",
|
||||
className
|
||||
)}
|
||||
@@ -61,11 +61,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
||||
<div className="relative z-10 p-8 flex flex-col h-full">
|
||||
{/* Icon container */}
|
||||
<motion.div
|
||||
className={cn(
|
||||
"mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl",
|
||||
"bg-gradient-to-br from-[#F0B90B]/20 to-[#F0B90B]/5",
|
||||
"border border-[#F0B90B]/30"
|
||||
)}
|
||||
className="mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.3)'
|
||||
}}
|
||||
animate={{
|
||||
scale: isHovered ? 1.1 : 1,
|
||||
boxShadow: isHovered
|
||||
@@ -74,14 +74,14 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="text-[#F0B90B]">{icon}</div>
|
||||
<div style={{ color: 'var(--brand-yellow)' }}>{icon}</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-2xl font-bold text-[#EAECEF] mb-3">{title}</h3>
|
||||
<h3 className="text-2xl font-bold mb-3" style={{ color: 'var(--brand-light-gray)' }}>{title}</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-[#848E9C] mb-6 flex-grow leading-relaxed">{description}</p>
|
||||
<p className="mb-6 flex-grow leading-relaxed" style={{ color: 'var(--text-secondary)' }}>{description}</p>
|
||||
|
||||
{/* Features list */}
|
||||
<div className="space-y-3 mb-6">
|
||||
@@ -95,11 +95,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<div className="w-5 h-5 rounded-full bg-[#F0B90B]/20 flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-[#F0B90B]" />
|
||||
<div className="w-5 h-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.2)' }}>
|
||||
<Check className="w-3 h-3" style={{ color: 'var(--brand-yellow)' }} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-[#EAECEF]">{feature}</span>
|
||||
<span className="text-sm" style={{ color: 'var(--brand-light-gray)' }}>{feature}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Header({ simple = false }: HeaderProps) {
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center">
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" />
|
||||
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
|
||||
@@ -2,8 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { Header } from './Header';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import HeaderBar from './landing/HeaderBar';
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -51,43 +50,44 @@ export function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
|
||||
<Header simple />
|
||||
<div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
|
||||
<HeaderBar
|
||||
onLoginClick={() => {}}
|
||||
isLoggedIn={false}
|
||||
isHomePage={false}
|
||||
currentPage="login"
|
||||
language={language}
|
||||
onLanguageChange={() => {}}
|
||||
onPageChange={(page) => {
|
||||
console.log('LoginPage onPageChange called with:', page);
|
||||
if (page === 'competition') {
|
||||
window.location.href = '/competition';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||
<div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Back to Home */}
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回首页
|
||||
</button>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('loginTitle', language)}
|
||||
<h1 className="text-2xl font-bold" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
登录 NOFX
|
||||
</h1>
|
||||
<p className="text-sm mt-2" style={{ color: '#848E9C' }}>
|
||||
{step === 'login' ? t('loginTitle', language) : t('enterOTPCode', language)}
|
||||
<p className="text-sm mt-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
{step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="rounded-lg p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
|
||||
{step === 'login' ? (
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('email', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -95,14 +95,14 @@ export function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('emailPlaceholder', language)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('password', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -110,14 +110,14 @@ export function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('passwordPlaceholder', language)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -126,7 +126,7 @@ export function LoginPage() {
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
{loading ? t('loading', language) : t('loginButton', language)}
|
||||
</button>
|
||||
@@ -142,7 +142,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('otpCode', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -150,7 +150,7 @@ export function LoginPage() {
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('otpPlaceholder', language)}
|
||||
maxLength={6}
|
||||
required
|
||||
@@ -158,7 +158,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -168,7 +168,7 @@ export function LoginPage() {
|
||||
type="button"
|
||||
onClick={() => setStep('login')}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('back', language)}
|
||||
</button>
|
||||
@@ -187,17 +187,17 @@ export function LoginPage() {
|
||||
|
||||
{/* Register Link */}
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('noAccount', language)}{' '}
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
还没有账户?{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/register');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="font-semibold hover:underline"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="font-semibold hover:underline transition-colors"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
{t('registerNow', language)}
|
||||
立即注册
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { getSystemConfig } from '../lib/config';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import HeaderBar from './landing/HeaderBar';
|
||||
|
||||
export function RegisterPage() {
|
||||
const { language } = useLanguage();
|
||||
@@ -13,13 +13,13 @@ export function RegisterPage() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [betaCode, setBetaCode] = useState('');
|
||||
const [betaMode, setBetaMode] = useState(false);
|
||||
const [otpCode, setOtpCode] = useState('');
|
||||
const [userID, setUserID] = useState('');
|
||||
const [otpSecret, setOtpSecret] = useState('');
|
||||
const [qrCodeURL, setQrCodeURL] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [betaMode, setBetaMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取系统配置,检查是否开启内测模式
|
||||
@@ -89,27 +89,28 @@ export function RegisterPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||
<div className="w-full max-w-md">
|
||||
{/* Back to Home */}
|
||||
{step === 'register' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
返回首页
|
||||
</button>
|
||||
)}
|
||||
<div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
|
||||
<HeaderBar
|
||||
isLoggedIn={false}
|
||||
isHomePage={false}
|
||||
currentPage="register"
|
||||
language={language}
|
||||
onLanguageChange={() => {}}
|
||||
onPageChange={(page) => {
|
||||
console.log('RegisterPage onPageChange called with:', page);
|
||||
if (page === 'competition') {
|
||||
window.location.href = '/competition';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||
<div className="w-full max-w-md">
|
||||
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
@@ -122,11 +123,11 @@ export function RegisterPage() {
|
||||
</div>
|
||||
|
||||
{/* Registration Form */}
|
||||
<div className="rounded-lg p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
|
||||
{step === 'register' && (
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('email', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -134,14 +135,14 @@ export function RegisterPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('emailPlaceholder', language)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('password', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -149,14 +150,14 @@ export function RegisterPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('passwordPlaceholder', language)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('confirmPassword', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -164,7 +165,7 @@ export function RegisterPage() {
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('confirmPasswordPlaceholder', language)}
|
||||
required
|
||||
/>
|
||||
@@ -192,7 +193,7 @@ export function RegisterPage() {
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -201,7 +202,7 @@ export function RegisterPage() {
|
||||
type="submit"
|
||||
disabled={loading || (betaMode && !betaCode.trim())}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
{loading ? t('loading', language) : t('registerButton', language)}
|
||||
</button>
|
||||
@@ -221,21 +222,21 @@ export function RegisterPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('step1Title', language)}
|
||||
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('authStep1Title', language)}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('step1Desc', language)}
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('authStep1Desc', language)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('step2Title', language)}
|
||||
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('authStep2Title', language)}
|
||||
</p>
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('step2Desc', language)}
|
||||
{t('authStep2Desc', language)}
|
||||
</p>
|
||||
|
||||
{qrCodeURL && (
|
||||
@@ -252,13 +253,13 @@ export function RegisterPage() {
|
||||
<p className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('otpSecret', language)}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 px-2 py-1 text-xs rounded font-mono"
|
||||
style={{ background: '#2B3139', color: '#EAECEF' }}>
|
||||
style={{ background: 'var(--panel-bg-hover)', color: 'var(--brand-light-gray)' }}>
|
||||
{otpSecret}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(otpSecret)}
|
||||
className="px-2 py-1 text-xs rounded"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
{t('copy', language)}
|
||||
</button>
|
||||
@@ -266,12 +267,12 @@ export function RegisterPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('step3Title', language)}
|
||||
<div className="p-3 rounded" style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)' }}>
|
||||
<p className="text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('authStep3Title', language)}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('step3Desc', language)}
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('authStep3Desc', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,7 +298,7 @@ export function RegisterPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{t('otpCode', language)}
|
||||
</label>
|
||||
<input
|
||||
@@ -305,7 +306,7 @@ export function RegisterPage() {
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||
placeholder={t('otpPlaceholder', language)}
|
||||
maxLength={6}
|
||||
required
|
||||
@@ -313,7 +314,7 @@ export function RegisterPage() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -323,7 +324,7 @@ export function RegisterPage() {
|
||||
type="button"
|
||||
onClick={() => setStep('setup-otp')}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{t('back', language)}
|
||||
</button>
|
||||
@@ -343,21 +344,22 @@ export function RegisterPage() {
|
||||
{/* Login Link */}
|
||||
{step === 'register' && (
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
已有账户?{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/login');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
className="font-semibold hover:underline"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="font-semibold hover:underline transition-colors"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
立即登录
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,13 @@ import { motion } from 'framer-motion'
|
||||
import { Shield, Target } from 'lucide-react'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
import Typewriter from '../Typewriter'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
export default function AboutSection() {
|
||||
interface AboutSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function AboutSection({ language }: AboutSectionProps) {
|
||||
return (
|
||||
<AnimatedSection id='about' backgroundColor='var(--brand-dark-gray)'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
@@ -31,7 +36,7 @@ export default function AboutSection() {
|
||||
className='text-sm font-semibold'
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
关于 NOFX
|
||||
{t('aboutNofx', language)}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
@@ -39,23 +44,19 @@ export default function AboutSection() {
|
||||
className='text-4xl font-bold'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
什么是 NOFX?
|
||||
{t('whatIsNofx', language)}
|
||||
</h2>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
NOFX 不是另一个交易机器人,而是 AI 交易的 'Linux' ——
|
||||
一个透明、可信任的开源 OS,提供统一的 '决策-风险-执行'
|
||||
层,支持所有资产类别。
|
||||
{t('nofxNotAnotherBot', language)} {t('nofxDescription1', language)} {t('nofxDescription2', language)}
|
||||
</p>
|
||||
<p
|
||||
className='text-lg leading-relaxed'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
从加密市场起步(24/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI
|
||||
达尔文主义(多代理自竞争、策略进化)、CodeFi 飞轮(开发者 PR
|
||||
贡献获积分奖励)。
|
||||
{t('nofxDescription3', language)} {t('nofxDescription4', language)} {t('nofxDescription5', language)}
|
||||
</p>
|
||||
<motion.div
|
||||
className='flex items-center gap-3 pt-4'
|
||||
@@ -75,13 +76,13 @@ export default function AboutSection() {
|
||||
className='font-semibold'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
你 100% 掌控
|
||||
{t('youFullControl', language)}
|
||||
</div>
|
||||
<div
|
||||
className='text-sm'
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
完全掌控 AI 提示词和资金
|
||||
{t('fullControlDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -101,16 +102,16 @@ export default function AboutSection() {
|
||||
'$ cd nofx',
|
||||
'$ chmod +x start.sh',
|
||||
'$ ./start.sh start --build',
|
||||
' 启动自动交易系统...',
|
||||
' API服务器启动在端口 8080',
|
||||
' Web 控制台 http://localhost:3000',
|
||||
t('startupMessages1', language),
|
||||
t('startupMessages2', language),
|
||||
t('startupMessages3', language),
|
||||
]}
|
||||
typingSpeed={70}
|
||||
lineDelay={900}
|
||||
className='text-sm font-mono'
|
||||
style={{
|
||||
color: '#00FF41',
|
||||
textShadow: '0 0 6px rgba(0,255,65,0.6)',
|
||||
color: '#00FF88',
|
||||
textShadow: '0 0 8px rgba(0,255,136,0.4)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,13 @@ import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
import { CryptoFeatureCard } from '../CryptoFeatureCard'
|
||||
import { Code, Cpu, Lock, Rocket } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
export default function FeaturesSection() {
|
||||
interface FeaturesSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function FeaturesSection({ language }: FeaturesSectionProps) {
|
||||
return (
|
||||
<AnimatedSection id='features'>
|
||||
<div className='max-w-7xl mx-auto'>
|
||||
@@ -15,37 +20,52 @@ export default function FeaturesSection() {
|
||||
>
|
||||
<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)' }}>
|
||||
为什么选择 NOFX?
|
||||
{t('whyChooseNofx', language)}
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
开源、透明、社区驱动的 AI 交易操作系统
|
||||
{t('openCommunityDriven', language)}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<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' />}
|
||||
title='100% 开源与自托管'
|
||||
description='你的框架,你的规则。非黑箱,支持自定义提示词和多模型。'
|
||||
features={['完全开源代码', '支持自托管部署', '自定义 AI 提示词', '多模型支持(DeepSeek、Qwen)']}
|
||||
title={t('openSourceSelfHosted', language)}
|
||||
description={t('openSourceDesc', language)}
|
||||
features={[
|
||||
t('openSourceFeatures1', language),
|
||||
t('openSourceFeatures2', language),
|
||||
t('openSourceFeatures3', language),
|
||||
t('openSourceFeatures4', language)
|
||||
]}
|
||||
delay={0}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Cpu className='w-8 h-8' />}
|
||||
title='多代理智能竞争'
|
||||
description='AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。'
|
||||
features={['多 AI 代理并行运行', '策略自动优化', '沙盒安全测试', '跨市场策略移植']}
|
||||
title={t('multiAgentCompetition', language)}
|
||||
description={t('multiAgentDesc', language)}
|
||||
features={[
|
||||
t('multiAgentFeatures1', language),
|
||||
t('multiAgentFeatures2', language),
|
||||
t('multiAgentFeatures3', language),
|
||||
t('multiAgentFeatures4', language)
|
||||
]}
|
||||
delay={0.1}
|
||||
/>
|
||||
<CryptoFeatureCard
|
||||
icon={<Lock className='w-8 h-8' />}
|
||||
title='安全可靠交易'
|
||||
description='企业级安全保障,完全掌控你的资金和交易策略。'
|
||||
features={['本地私钥管理', 'API 权限精细控制', '实时风险监控', '交易日志审计']}
|
||||
title={t('secureReliableTrading', language)}
|
||||
description={t('secureDesc', language)}
|
||||
features={[
|
||||
t('secureFeatures1', language),
|
||||
t('secureFeatures2', language),
|
||||
t('secureFeatures3', language),
|
||||
t('secureFeatures4', language)
|
||||
]}
|
||||
delay={0.2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
export default function FooterSection() {
|
||||
const { language } = useLanguage()
|
||||
interface FooterSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function FooterSection({ language }: FooterSectionProps) {
|
||||
return (
|
||||
<footer style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}>
|
||||
<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='/images/logo.png' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<img src='/icons/nofx.svg' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<div>
|
||||
<div className='text-lg font-bold' style={{ color: '#EAECEF' }}>
|
||||
NOFX
|
||||
</div>
|
||||
<div className='text-xs' style={{ color: '#848E9C' }}>
|
||||
AI 交易的未来标准
|
||||
{t('futureStandardAI', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,7 +28,7 @@ export default function FooterSection() {
|
||||
className='text-sm font-semibold mb-3'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
链接
|
||||
{t('links', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
@@ -67,7 +69,7 @@ export default function FooterSection() {
|
||||
className='text-sm font-semibold mb-3'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
资源
|
||||
{t('resources', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
@@ -77,7 +79,7 @@ export default function FooterSection() {
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
文档
|
||||
{t('documentation', language)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -108,7 +110,7 @@ export default function FooterSection() {
|
||||
className='text-sm font-semibold mb-3'
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
支持方
|
||||
{t('supporters', language)}
|
||||
</h3>
|
||||
<ul className='space-y-2 text-sm' style={{ color: '#848E9C' }}>
|
||||
<li>
|
||||
@@ -148,7 +150,7 @@ export default function FooterSection() {
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
Amber.ac <span className='opacity-70'>(战略投资)</span>
|
||||
Amber.ac <span className='opacity-70'>{t('strategicInvestment', language)}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -158,7 +160,7 @@ export default function FooterSection() {
|
||||
{/* Bottom note (kept subtle) */}
|
||||
<div
|
||||
className='pt-6 mt-8 text-center text-xs'
|
||||
style={{ color: '#5E6673', borderTop: '1px solid #2B3139' }}
|
||||
style={{ color: 'var(--text-tertiary)', borderTop: '1px solid var(--panel-border)' }}
|
||||
>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className='mt-1'>{t('footerWarning', language)}</p>
|
||||
|
||||
@@ -1,56 +1,363 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Menu, X } from 'lucide-react'
|
||||
import { Menu, X, ChevronDown } from 'lucide-react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
|
||||
export default function HeaderBar({ onLoginClick }: { onLoginClick: () => void }) {
|
||||
interface HeaderBarProps {
|
||||
onLoginClick?: () => void
|
||||
isLoggedIn?: boolean
|
||||
isHomePage?: boolean
|
||||
currentPage?: string
|
||||
language?: Language
|
||||
onLanguageChange?: (lang: Language) => void
|
||||
user?: { email: string } | null
|
||||
onLogout?: () => void
|
||||
isAdminMode?: boolean
|
||||
onPageChange?: (page: string) => void
|
||||
}
|
||||
|
||||
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)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const userDropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setLanguageDropdownOpen(false)
|
||||
}
|
||||
if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) {
|
||||
setUserDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
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'>
|
||||
{/* Logo */}
|
||||
<div className='flex items-center gap-3'>
|
||||
<img src='/images/logo.png' alt='NOFX Logo' className='w-8 h-8' />
|
||||
<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)' }}>
|
||||
Agentic Trading OS
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className='hidden md:flex items-center gap-6'>
|
||||
{['功能', '如何运作', 'GitHub', '社区'].map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href={
|
||||
item === 'GitHub'
|
||||
? 'https://github.com/tinkle-community/nofx'
|
||||
: item === '社区'
|
||||
? 'https://t.me/nofx_dev_community'
|
||||
: `#${item === '功能' ? 'features' : 'how-it-works'}`
|
||||
}
|
||||
target={item === 'GitHub' || item === '社区' ? '_blank' : undefined}
|
||||
rel={item === 'GitHub' || item === '社区' ? 'noopener noreferrer' : undefined}
|
||||
className='text-sm transition-colors relative group'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{item}
|
||||
<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>
|
||||
))}
|
||||
<button
|
||||
onClick={onLoginClick}
|
||||
className='px-4 py-2 rounded font-semibold text-sm'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
登录 / 注册
|
||||
</button>
|
||||
<div className='hidden md:flex items-center justify-between flex-1 ml-8'>
|
||||
{/* Left Side - Navigation Tabs */}
|
||||
<div className='flex items-center gap-4'>
|
||||
{isLoggedIn ? (
|
||||
// Main app navigation when logged in
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'traders') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'traders') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'traders' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('configNav', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'trader') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'trader') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'trader' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Side - Original Navigation Items and Login */}
|
||||
<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>
|
||||
))}
|
||||
|
||||
{/* User Info and Actions */}
|
||||
{isLoggedIn && user ? (
|
||||
<div className='flex items-center gap-3'>
|
||||
{/* User Info with Dropdown */}
|
||||
<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)'}
|
||||
>
|
||||
<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)' }} />
|
||||
</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>
|
||||
{!isAdminMode && onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
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)' }}
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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'>
|
||||
<a
|
||||
href='/login'
|
||||
className='px-3 py-2 text-sm font-medium transition-colors rounded'
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
{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)' }}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Language Toggle - Always at the rightmost */}
|
||||
<div className='relative' ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||
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'}
|
||||
>
|
||||
<span className='text-lg'>
|
||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
||||
</span>
|
||||
<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)' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('zh')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'zh' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background: language === 'zh' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
|
||||
}}
|
||||
>
|
||||
<span className='text-base'>🇨🇳</span>
|
||||
<span className='text-sm'>中文</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('en')
|
||||
setLanguageDropdownOpen(false)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
||||
language === 'en' ? '' : 'hover:opacity-80'
|
||||
}`}
|
||||
style={{
|
||||
color: 'var(--brand-light-gray)',
|
||||
background: language === 'en' ? 'rgba(240, 185, 11, 0.1)' : 'transparent'
|
||||
}}
|
||||
>
|
||||
<span className='text-base'>🇺🇸</span>
|
||||
<span className='text-sm'>English</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -74,21 +381,232 @@ export default function HeaderBar({ onLoginClick }: { onLoginClick: () => void }
|
||||
style={{ background: 'var(--brand-dark-gray)', borderTop: '1px solid rgba(240, 185, 11, 0.1)' }}
|
||||
>
|
||||
<div className='px-4 py-4 space-y-3'>
|
||||
{['功能', '如何运作', 'GitHub', '社区'].map((item) => (
|
||||
<a key={item} href={`#${item}`} className='block text-sm py-2' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
{item}
|
||||
{/* New Navigation Tabs */}
|
||||
{isLoggedIn ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'competition' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
)}
|
||||
{/* Only show 配置 and 看板 when logged in */}
|
||||
{isLoggedIn && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'traders' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'traders' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('configNav', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
style={{
|
||||
color: currentPage === 'trader' ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'trader' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
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>
|
||||
))}
|
||||
<button
|
||||
onClick={() => {
|
||||
onLoginClick()
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className='w-full px-4 py-2 rounded font-semibold text-sm mt-2'
|
||||
style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }}
|
||||
>
|
||||
登录 / 注册
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<span className='text-lg'>🇨🇳</span>
|
||||
<span className='text-sm'>中文</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onLanguageChange?.('en')
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<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)' }}>
|
||||
{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>
|
||||
</div>
|
||||
{!isAdminMode && onLogout && (
|
||||
<button
|
||||
onClick={() => {
|
||||
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)' }}
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</nav>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
||||
import { motion, useScroll, useTransform, useAnimation } from 'framer-motion'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
export default function HeroSection() {
|
||||
interface HeroSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function HeroSection({ language }: HeroSectionProps) {
|
||||
const { scrollYProgress } = useScroll()
|
||||
const opacity = useTransform(scrollYProgress, [0, 0.2], [1, 0])
|
||||
const scale = useTransform(scrollYProgress, [0, 0.2], [1, 0.8])
|
||||
const handControls = useAnimation()
|
||||
|
||||
const fadeInUp = {
|
||||
initial: { opacity: 0, y: 60 },
|
||||
@@ -27,40 +33,39 @@ export default function HeroSection() {
|
||||
>
|
||||
<Sparkles className='w-4 h-4' style={{ color: 'var(--brand-yellow)' }} />
|
||||
<span className='text-sm font-semibold' style={{ color: 'var(--brand-yellow)' }}>
|
||||
3 天内 2.5K+ GitHub Stars
|
||||
{t('githubStarsInDays', language)}
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<h1 className='text-5xl lg:text-7xl font-bold leading-tight' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
Read the Market.
|
||||
{t('heroTitle1', language)}
|
||||
<br />
|
||||
<span style={{ color: 'var(--brand-yellow)' }}>Write the Trade.</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}>
|
||||
NOFX 是 AI 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster DEX 等交易所,
|
||||
自托管、多代理竞争,让 AI 为你自动决策、执行和优化交易。
|
||||
{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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/stars/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/forks/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
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 }}>
|
||||
<img
|
||||
src='https://img.shields.io/github/contributors/tinkle-community/nofx?style=for-the-badge&logo=github&logoColor=white&color=F0B90B&labelColor=1E2329'
|
||||
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'
|
||||
/>
|
||||
@@ -68,12 +73,62 @@ export default function HeroSection() {
|
||||
</div>
|
||||
|
||||
<motion.p className='text-xs pt-4' style={{ color: 'var(--text-tertiary)' }} variants={fadeInUp}>
|
||||
由 Aster DEX 和 Binance 提供支持,Amber.ac 战略投资。
|
||||
{t('poweredBy', language)}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Visual */}
|
||||
<motion.img src='/images/main.png' alt='NOFX Platform' className='w-full opacity-90' whileHover={{ scale: 1.05, rotate: 5 }} transition={{ type: 'spring', stiffness: 300 }} />
|
||||
{/* Right Visual - Interactive Robot */}
|
||||
<div
|
||||
className='relative w-full cursor-pointer'
|
||||
onMouseEnter={() => {
|
||||
handControls.start({
|
||||
y: [-8, 8, -8],
|
||||
rotate: [-3, 3, -3],
|
||||
x: [-2, 2, -2],
|
||||
transition: {
|
||||
duration: 2.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
times: [0, 0.5, 1]
|
||||
}
|
||||
})
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
handControls.start({
|
||||
y: 0,
|
||||
rotate: 0,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
ease: "easeOut"
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{/* Background Layer */}
|
||||
<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'
|
||||
style={{ opacity }}
|
||||
animate={handControls}
|
||||
initial={{ y: 0, rotate: 0, x: 0 }}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
transition: { type: 'spring', stiffness: 400 }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import AnimatedSection from './AnimatedSection'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
function StepCard({ number, title, description, delay }: any) {
|
||||
return (
|
||||
@@ -24,25 +25,29 @@ function StepCard({ number, title, description, delay }: any) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function HowItWorksSection() {
|
||||
interface HowItWorksSectionProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
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)' }}>
|
||||
如何开始使用 NOFX
|
||||
{t('howToStart', language)}
|
||||
</h2>
|
||||
<p className='text-lg' style={{ color: 'var(--text-secondary)' }}>
|
||||
四个简单步骤,开启 AI 自动交易之旅
|
||||
{t('fourSimpleSteps', language)}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className='space-y-8'>
|
||||
{[
|
||||
{ number: 1, title: '拉取 GitHub 仓库', description: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。' },
|
||||
{ number: 2, title: '配置环境', description: '前端设置交易所 API(如 Binance、Hyperliquid)、AI 模型和自定义提示词。' },
|
||||
{ number: 3, title: '部署与运行', description: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。' },
|
||||
{ number: 4, title: '优化与贡献', description: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。' },
|
||||
{ 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} />
|
||||
))}
|
||||
@@ -61,10 +66,10 @@ export default function HowItWorksSection() {
|
||||
</div>
|
||||
<div>
|
||||
<div className='font-semibold mb-2' style={{ color: '#F6465D' }}>
|
||||
重要风险提示
|
||||
{t('importantRiskWarning', language)}
|
||||
</div>
|
||||
<p className='text-sm' style={{ color: 'var(--text-secondary)' }}>
|
||||
dev 分支不稳定,勿用无法承受损失的资金。NOFX 非托管,无官方策略。交易有风险,投资需谨慎。
|
||||
{t('riskWarningText', language)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { X } from 'lucide-react'
|
||||
import { t, Language } from '../../i18n/translations'
|
||||
|
||||
export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||
interface LoginModalProps {
|
||||
onClose: () => void
|
||||
language: Language
|
||||
}
|
||||
|
||||
export default function LoginModal({ onClose, language }: LoginModalProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className='fixed inset-0 z-50 flex items-center justify-center p-4'
|
||||
@@ -23,10 +29,10 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||
<X className='w-6 h-6' />
|
||||
</motion.button>
|
||||
<h2 className='text-2xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }}>
|
||||
访问 NOFX 平台
|
||||
{t('accessNofxPlatform', language)}
|
||||
</h2>
|
||||
<p className='text-sm mb-6' style={{ color: 'var(--text-secondary)' }}>
|
||||
请选择登录或注册以访问完整的 AI 交易平台
|
||||
{t('loginRegisterPrompt', language)}
|
||||
</p>
|
||||
<div className='space-y-3'>
|
||||
<motion.button
|
||||
@@ -40,7 +46,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||
whileHover={{ scale: 1.05, boxShadow: '0 10px 30px rgba(240, 185, 11, 0.4)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
登录
|
||||
{t('signIn', language)}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
@@ -53,7 +59,7 @@ export default function LoginModal({ onClose }: { onClose: () => void }) {
|
||||
whileHover={{ scale: 1.05, borderColor: 'var(--brand-yellow)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
注册新账号
|
||||
{t('registerNewAccount', language)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -15,6 +15,11 @@ export const translations = {
|
||||
logout: 'Logout',
|
||||
switchTrader: 'Switch Trader:',
|
||||
view: 'View',
|
||||
|
||||
// Navigation
|
||||
realtimeNav: 'Live',
|
||||
configNav: 'Config',
|
||||
dashboardNav: 'Dashboard',
|
||||
|
||||
// Footer
|
||||
footerTitle: 'NOFX - AI Trading System',
|
||||
@@ -296,12 +301,12 @@ export const translations = {
|
||||
scanQRCodeInstructions: 'Scan this QR code with Google Authenticator or Authy',
|
||||
otpSecret: 'Or enter this secret manually:',
|
||||
qrCodeHint: 'QR code (if scanning fails, use the secret below):',
|
||||
step1Title: 'Step 1: Install Google Authenticator',
|
||||
step1Desc: 'Download and install Google Authenticator from your app store',
|
||||
step2Title: 'Step 2: Add account',
|
||||
step2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"',
|
||||
step3Title: 'Step 3: Verify setup',
|
||||
step3Desc: 'After setup, continue to enter the 6-digit code',
|
||||
authStep1Title: 'Step 1: Install Google Authenticator',
|
||||
authStep1Desc: 'Download and install Google Authenticator from your app store',
|
||||
authStep2Title: 'Step 2: Add account',
|
||||
authStep2Desc: 'Tap "+", then choose "Scan QR code" or "Enter a setup key"',
|
||||
authStep3Title: 'Step 3: Verify setup',
|
||||
authStep3Desc: 'After setup, continue to enter the 6-digit code',
|
||||
setupCompleteContinue: 'I have completed setup, continue',
|
||||
copy: 'Copy',
|
||||
completeRegistration: 'Complete Registration',
|
||||
@@ -317,6 +322,96 @@ export const translations = {
|
||||
passwordRequired: 'Password is required',
|
||||
invalidEmail: 'Invalid email format',
|
||||
passwordTooShort: 'Password must be at least 6 characters',
|
||||
|
||||
// Landing Page
|
||||
features: 'Features',
|
||||
howItWorks: 'How it Works',
|
||||
community: 'Community',
|
||||
language: 'Language',
|
||||
loggedInAs: 'Logged in as',
|
||||
exitLogin: 'Sign Out',
|
||||
signIn: 'Sign In',
|
||||
signUp: 'Sign Up',
|
||||
|
||||
// Hero Section
|
||||
githubStarsInDays: '2.5K+ GitHub Stars in 3 days',
|
||||
heroTitle1: 'Read the Market.',
|
||||
heroTitle2: 'Write the Trade.',
|
||||
heroDescription: 'NOFX is the future standard for AI trading — an open, community-driven agentic trading OS. Supporting Binance, Aster DEX and other exchanges, self-hosted, multi-agent competition, let AI automatically make decisions, execute and optimize trades for you.',
|
||||
poweredBy: 'Powered by Aster DEX and Binance, strategically invested by Amber.ac.',
|
||||
|
||||
// Landing Page CTA
|
||||
readyToDefine: 'Ready to define the future of AI trading?',
|
||||
startWithCrypto: 'Starting with crypto markets, expanding to TradFi. NOFX is the infrastructure of AgentFi.',
|
||||
getStartedNow: 'Get Started Now',
|
||||
viewSourceCode: 'View Source Code',
|
||||
|
||||
// Features Section
|
||||
coreFeatures: 'Core Features',
|
||||
whyChooseNofx: 'Why Choose NOFX?',
|
||||
openCommunityDriven: 'Open source, transparent, community-driven AI trading OS',
|
||||
openSourceSelfHosted: '100% Open Source & Self-Hosted',
|
||||
openSourceDesc: 'Your framework, your rules. Non-black box, supports custom prompts and multi-models.',
|
||||
openSourceFeatures1: 'Fully open source code',
|
||||
openSourceFeatures2: 'Self-hosting deployment support',
|
||||
openSourceFeatures3: 'Custom AI prompts',
|
||||
openSourceFeatures4: 'Multi-model support (DeepSeek, Qwen)',
|
||||
multiAgentCompetition: 'Multi-Agent Intelligent Competition',
|
||||
multiAgentDesc: 'AI strategies battle at high speed in sandbox, survival of the fittest, achieving strategy evolution.',
|
||||
multiAgentFeatures1: 'Multiple AI agents running in parallel',
|
||||
multiAgentFeatures2: 'Automatic strategy optimization',
|
||||
multiAgentFeatures3: 'Sandbox security testing',
|
||||
multiAgentFeatures4: 'Cross-market strategy porting',
|
||||
secureReliableTrading: 'Secure and Reliable Trading',
|
||||
secureDesc: 'Enterprise-grade security, complete control over your funds and trading strategies.',
|
||||
secureFeatures1: 'Local private key management',
|
||||
secureFeatures2: 'Fine-grained API permission control',
|
||||
secureFeatures3: 'Real-time risk monitoring',
|
||||
secureFeatures4: 'Trading log auditing',
|
||||
|
||||
// About Section
|
||||
aboutNofx: 'About NOFX',
|
||||
whatIsNofx: 'What is NOFX?',
|
||||
nofxNotAnotherBot: "NOFX is not another trading bot, but the 'Linux' of AI trading —",
|
||||
nofxDescription1: 'a transparent, trustworthy open source OS that provides a unified',
|
||||
nofxDescription2: "'decision-risk-execution' layer, supporting all asset classes.",
|
||||
nofxDescription3: 'Starting with crypto markets (24/7, high volatility perfect testing ground), future expansion to stocks, futures, forex. Core: open architecture, AI',
|
||||
nofxDescription4: 'Darwinism (multi-agent self-competition, strategy evolution), CodeFi',
|
||||
nofxDescription5: 'flywheel (developers get point rewards for PR contributions).',
|
||||
youFullControl: 'You 100% Control',
|
||||
fullControlDesc: 'Complete control over AI prompts and funds',
|
||||
startupMessages1: 'Starting automated trading system...',
|
||||
startupMessages2: 'API server started on port 8080',
|
||||
startupMessages3: 'Web console http://localhost:3000',
|
||||
|
||||
// How It Works Section
|
||||
howToStart: 'How to Get Started with NOFX',
|
||||
fourSimpleSteps: 'Four simple steps to start your AI automated trading journey',
|
||||
step1Title: 'Clone GitHub Repository',
|
||||
step1Desc: 'git clone https://github.com/tinkle-community/nofx and switch to dev branch to test new features.',
|
||||
step2Title: 'Configure Environment',
|
||||
step2Desc: 'Frontend setup for exchange APIs (like Binance, Hyperliquid), AI models and custom prompts.',
|
||||
step3Title: 'Deploy & Run',
|
||||
step3Desc: 'One-click Docker deployment, start AI agents. Note: High-risk market, only test with money you can afford to lose.',
|
||||
step4Title: 'Optimize & Contribute',
|
||||
step4Desc: 'Monitor trading, submit PRs to improve framework. Join Telegram to share strategies.',
|
||||
importantRiskWarning: 'Important Risk Warning',
|
||||
riskWarningText: 'Dev branch is unstable, do not use funds you cannot afford to lose. NOFX is non-custodial, no official strategies. Trading involves risks, invest carefully.',
|
||||
|
||||
// Community Section (testimonials are kept as-is since they are quotes)
|
||||
|
||||
// Footer Section
|
||||
futureStandardAI: 'The future standard of AI trading',
|
||||
links: 'Links',
|
||||
resources: 'Resources',
|
||||
documentation: 'Documentation',
|
||||
supporters: 'Supporters',
|
||||
strategicInvestment: '(Strategic Investment)',
|
||||
|
||||
// Login Modal
|
||||
accessNofxPlatform: 'Access NOFX Platform',
|
||||
loginRegisterPrompt: 'Please login or register to access the full AI trading platform',
|
||||
registerNewAccount: 'Register New Account',
|
||||
},
|
||||
zh: {
|
||||
// Header
|
||||
@@ -332,6 +427,11 @@ export const translations = {
|
||||
logout: '退出',
|
||||
switchTrader: '切换交易员:',
|
||||
view: '查看',
|
||||
|
||||
// Navigation
|
||||
realtimeNav: '实时',
|
||||
configNav: '配置',
|
||||
dashboardNav: '看板',
|
||||
|
||||
// Footer
|
||||
footerTitle: 'NOFX - AI交易系统',
|
||||
@@ -613,12 +713,12 @@ export const translations = {
|
||||
scanQRCodeInstructions: '使用Google Authenticator或Authy扫描此二维码',
|
||||
otpSecret: '或手动输入此密钥:',
|
||||
qrCodeHint: '二维码(如果无法扫描,请使用下方密钥):',
|
||||
step1Title: '步骤1:下载Google Authenticator',
|
||||
step1Desc: '在手机应用商店下载并安装Google Authenticator应用',
|
||||
step2Title: '步骤2:添加账户',
|
||||
step2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”',
|
||||
step3Title: '步骤3:验证设置',
|
||||
step3Desc: '设置完成后,点击下方按钮输入6位验证码',
|
||||
authStep1Title: '步骤1:下载Google Authenticator',
|
||||
authStep1Desc: '在手机应用商店下载并安装Google Authenticator应用',
|
||||
authStep2Title: '步骤2:添加账户',
|
||||
authStep2Desc: '在应用中点击“+”,选择“扫描二维码”或“手动输入密钥”',
|
||||
authStep3Title: '步骤3:验证设置',
|
||||
authStep3Desc: '设置完成后,点击下方按钮输入6位验证码',
|
||||
setupCompleteContinue: '我已完成设置,继续',
|
||||
copy: '复制',
|
||||
completeRegistration: '完成注册',
|
||||
@@ -634,6 +734,96 @@ export const translations = {
|
||||
passwordRequired: '请输入密码',
|
||||
invalidEmail: '邮箱格式不正确',
|
||||
passwordTooShort: '密码至少需要6个字符',
|
||||
|
||||
// Landing Page
|
||||
features: '功能',
|
||||
howItWorks: '如何运作',
|
||||
community: '社区',
|
||||
language: '语言',
|
||||
loggedInAs: '已登录为',
|
||||
exitLogin: '退出登录',
|
||||
signIn: '登录',
|
||||
signUp: '注册',
|
||||
|
||||
// Hero Section
|
||||
githubStarsInDays: '3 天内 2.5K+ GitHub Stars',
|
||||
heroTitle1: 'Read the Market.',
|
||||
heroTitle2: 'Write the Trade.',
|
||||
heroDescription: 'NOFX 是 AI 交易的未来标准——一个开放、社区驱动的代理式交易操作系统。支持 Binance、Aster DEX 等交易所,自托管、多代理竞争,让 AI 为你自动决策、执行和优化交易。',
|
||||
poweredBy: '由 Aster DEX 和 Binance 提供支持,Amber.ac 战略投资。',
|
||||
|
||||
// Landing Page CTA
|
||||
readyToDefine: '准备好定义 AI 交易的未来吗?',
|
||||
startWithCrypto: '从加密市场起步,扩展到 TradFi。NOFX 是 AgentFi 的基础架构。',
|
||||
getStartedNow: '立即开始',
|
||||
viewSourceCode: '查看源码',
|
||||
|
||||
// Features Section
|
||||
coreFeatures: '核心功能',
|
||||
whyChooseNofx: '为什么选择 NOFX?',
|
||||
openCommunityDriven: '开源、透明、社区驱动的 AI 交易操作系统',
|
||||
openSourceSelfHosted: '100% 开源与自托管',
|
||||
openSourceDesc: '你的框架,你的规则。非黑箱,支持自定义提示词和多模型。',
|
||||
openSourceFeatures1: '完全开源代码',
|
||||
openSourceFeatures2: '支持自托管部署',
|
||||
openSourceFeatures3: '自定义 AI 提示词',
|
||||
openSourceFeatures4: '多模型支持(DeepSeek、Qwen)',
|
||||
multiAgentCompetition: '多代理智能竞争',
|
||||
multiAgentDesc: 'AI 策略在沙盒中高速战斗,最优者生存,实现策略进化。',
|
||||
multiAgentFeatures1: '多 AI 代理并行运行',
|
||||
multiAgentFeatures2: '策略自动优化',
|
||||
multiAgentFeatures3: '沙盒安全测试',
|
||||
multiAgentFeatures4: '跨市场策略移植',
|
||||
secureReliableTrading: '安全可靠交易',
|
||||
secureDesc: '企业级安全保障,完全掌控你的资金和交易策略。',
|
||||
secureFeatures1: '本地私钥管理',
|
||||
secureFeatures2: 'API 权限精细控制',
|
||||
secureFeatures3: '实时风险监控',
|
||||
secureFeatures4: '交易日志审计',
|
||||
|
||||
// About Section
|
||||
aboutNofx: '关于 NOFX',
|
||||
whatIsNofx: '什么是 NOFX?',
|
||||
nofxNotAnotherBot: 'NOFX 不是另一个交易机器人,而是 AI 交易的 \'Linux\' ——',
|
||||
nofxDescription1: '一个透明、可信任的开源 OS,提供统一的 \'决策-风险-执行\'',
|
||||
nofxDescription2: '层,支持所有资产类别。',
|
||||
nofxDescription3: '从加密市场起步(24/7、高波动性完美测试场),未来扩展到股票、期货、外汇。核心:开放架构、AI',
|
||||
nofxDescription4: '达尔文主义(多代理自竞争、策略进化)、CodeFi 飞轮(开发者 PR',
|
||||
nofxDescription5: '贡献获积分奖励)。',
|
||||
youFullControl: '你 100% 掌控',
|
||||
fullControlDesc: '完全掌控 AI 提示词和资金',
|
||||
startupMessages1: ' 启动自动交易系统...',
|
||||
startupMessages2: ' API服务器启动在端口 8080',
|
||||
startupMessages3: ' Web 控制台 http://localhost:3000',
|
||||
|
||||
// How It Works Section
|
||||
howToStart: '如何开始使用 NOFX',
|
||||
fourSimpleSteps: '四个简单步骤,开启 AI 自动交易之旅',
|
||||
step1Title: '拉取 GitHub 仓库',
|
||||
step1Desc: 'git clone https://github.com/tinkle-community/nofx 并切换到 dev 分支测试新功能。',
|
||||
step2Title: '配置环境',
|
||||
step2Desc: '前端设置交易所 API(如 Binance、Hyperliquid)、AI 模型和自定义提示词。',
|
||||
step3Title: '部署与运行',
|
||||
step3Desc: '一键 Docker 部署,启动 AI 代理。注意:高风险市场,仅用闲钱测试。',
|
||||
step4Title: '优化与贡献',
|
||||
step4Desc: '监控交易,提交 PR 改进框架。加入 Telegram 分享策略。',
|
||||
importantRiskWarning: '重要风险提示',
|
||||
riskWarningText: 'dev 分支不稳定,勿用无法承受损失的资金。NOFX 非托管,无官方策略。交易有风险,投资需谨慎。',
|
||||
|
||||
// Community Section (testimonials are kept as-is since they are quotes)
|
||||
|
||||
// Footer Section
|
||||
futureStandardAI: 'AI 交易的未来标准',
|
||||
links: '链接',
|
||||
resources: '资源',
|
||||
documentation: '文档',
|
||||
supporters: '支持方',
|
||||
strategicInvestment: '(战略投资)',
|
||||
|
||||
// Login Modal
|
||||
accessNofxPlatform: '访问 NOFX 平台',
|
||||
loginRegisterPrompt: '请选择登录或注册以访问完整的 AI 交易平台',
|
||||
registerNewAccount: '注册新账号',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 强制显示滚动条以确保所有页面布局一致 */
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Binance Brand Colors */
|
||||
--brand-yellow: #F0B90B;
|
||||
--brand-black: #0C0E12;
|
||||
--brand-dark-gray: #1E2329;
|
||||
--brand-black: #000000;
|
||||
--brand-dark-gray: #0A0A0A;
|
||||
--brand-light-gray: #EAECEF;
|
||||
--brand-almost-white: #FAFAFA;
|
||||
--brand-white: #FFFFFF;
|
||||
@@ -20,14 +25,14 @@
|
||||
--binance-yellow-light: #FCD535;
|
||||
--binance-yellow-glow: rgba(240, 185, 11, 0.2);
|
||||
|
||||
--background: #181A20; /* Binance body bg */
|
||||
--header-bg: #0B0E11; /* Binance header bg */
|
||||
--background-elevated: #181A20;
|
||||
--background: #000000; /* Binance body bg */
|
||||
--header-bg: #000000; /* Binance header bg */
|
||||
--background-elevated: #000000;
|
||||
--foreground: #EAECEF;
|
||||
--panel-bg: #1E2329;
|
||||
--panel-bg-hover: #252930;
|
||||
--panel-border: #2B3139;
|
||||
--panel-border-hover: #474D57;
|
||||
--panel-bg: #0A0A0A;
|
||||
--panel-bg-hover: #111111;
|
||||
--panel-border: #1A1A1A;
|
||||
--panel-border-hover: #2A2A2A;
|
||||
|
||||
/* Binance Signature Colors */
|
||||
--binance-green: #0ECB81;
|
||||
@@ -44,7 +49,7 @@
|
||||
--text-disabled: #474D57;
|
||||
|
||||
/* Chart Colors */
|
||||
--grid-stroke: #2B3139;
|
||||
--grid-stroke: #1A1A1A;
|
||||
--axis-tick: #5E6673;
|
||||
--ref-line: #474D57;
|
||||
|
||||
|
||||
@@ -32,13 +32,20 @@ function getAuthHeaders(): Record<string, string> {
|
||||
export const api = {
|
||||
// AI交易员管理接口
|
||||
async getTraders(): Promise<TraderInfo[]> {
|
||||
const res = await fetch(`${API_BASE}/traders`, {
|
||||
const res = await fetch(`${API_BASE}/my-traders`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) throw new Error('获取trader列表失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// 获取公开的交易员列表(无需认证)
|
||||
async getPublicTraders(): Promise<any[]> {
|
||||
const res = await fetch(`${API_BASE}/traders`);
|
||||
if (!res.ok) throw new Error('获取公开trader列表失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||
const res = await fetch(`${API_BASE}/traders`, {
|
||||
method: 'POST',
|
||||
@@ -252,11 +259,9 @@ export const api = {
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// 获取竞赛数据
|
||||
// 获取竞赛数据(无需认证)
|
||||
async getCompetition(): Promise<CompetitionData> {
|
||||
const res = await fetch(`${API_BASE}/competition`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
const res = await fetch(`${API_BASE}/competition`);
|
||||
if (!res.ok) throw new Error('获取竞赛数据失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface SystemConfig {
|
||||
admin_mode: boolean;
|
||||
beta_mode: boolean;
|
||||
}
|
||||
|
||||
let configPromise: Promise<SystemConfig> | null = null;
|
||||
|
||||
@@ -10,43 +10,71 @@ import CommunitySection from '../components/landing/CommunitySection'
|
||||
import AnimatedSection from '../components/landing/AnimatedSection'
|
||||
import LoginModal from '../components/landing/LoginModal'
|
||||
import FooterSection from '../components/landing/FooterSection'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
export function LandingPage() {
|
||||
const [showLoginModal, setShowLoginModal] = useState(false)
|
||||
const { user, logout } = useAuth()
|
||||
const { language, setLanguage } = useLanguage()
|
||||
const isLoggedIn = !!user
|
||||
|
||||
console.log('LandingPage - user:', user, 'isLoggedIn:', isLoggedIn);
|
||||
return (
|
||||
<div className='min-h-screen overflow-hidden' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
|
||||
<HeaderBar onLoginClick={() => setShowLoginModal(true)} />
|
||||
<HeroSection />
|
||||
<AboutSection />
|
||||
<FeaturesSection />
|
||||
<HowItWorksSection />
|
||||
<>
|
||||
<HeaderBar
|
||||
onLoginClick={() => setShowLoginModal(true)}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isHomePage={true}
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
onPageChange={(page) => {
|
||||
console.log('LandingPage onPageChange called with:', page);
|
||||
if (page === 'competition') {
|
||||
window.location.href = '/competition';
|
||||
} else if (page === 'traders') {
|
||||
window.location.href = '/traders';
|
||||
} else if (page === 'trader') {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className='min-h-screen px-4 sm:px-6 lg:px-8' style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
|
||||
<HeroSection language={language} />
|
||||
<AboutSection language={language} />
|
||||
<FeaturesSection language={language} />
|
||||
<HowItWorksSection language={language} />
|
||||
<CommunitySection />
|
||||
|
||||
{/* CTA */}
|
||||
<AnimatedSection backgroundColor='var(--panel-bg)'>
|
||||
<div className='max-w-4xl mx-auto text-center'>
|
||||
<motion.h2 className='text-5xl font-bold mb-6' style={{ color: 'var(--brand-light-gray)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}>
|
||||
准备好定义 AI 交易的未来吗?
|
||||
{t('readyToDefine', language)}
|
||||
</motion.h2>
|
||||
<motion.p className='text-xl mb-12' style={{ color: 'var(--text-secondary)' }} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.1 }}>
|
||||
从加密市场起步,扩展到 TradFi。NOFX 是 AgentFi 的基础架构。
|
||||
{t('startWithCrypto', language)}
|
||||
</motion.p>
|
||||
<div className='flex flex-wrap justify-center gap-4'>
|
||||
<motion.button onClick={() => setShowLoginModal(true)} className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
立即开始
|
||||
{t('getStartedNow', language)}
|
||||
<motion.div animate={{ x: [0, 5, 0] }} transition={{ duration: 1.5, repeat: Infinity }}>
|
||||
<ArrowRight className='w-5 h-5' />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
<motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-dark-gray)', color: 'var(--brand-light-gray)', border: '1px solid rgba(240, 185, 11, 0.2)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
查看源码
|
||||
<motion.a href='https://github.com/tinkle-community/nofx/tree/dev' target='_blank' rel='noopener noreferrer' className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'transparent', color: 'var(--brand-light-gray)', border: '2px solid var(--brand-yellow)' }} whileHover={{ scale: 1.05, backgroundColor: 'rgba(240, 185, 11, 0.1)' }} whileTap={{ scale: 0.95 }}>
|
||||
{t('viewSourceCode', language)}
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedSection>
|
||||
|
||||
{showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} />}
|
||||
<FooterSection />
|
||||
</div>
|
||||
{showLoginModal && <LoginModal onClose={() => setShowLoginModal(false)} language={language} />}
|
||||
<FooterSection language={language} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user