mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
89085173f9
* feat: remove admin mode * feat: bugfix * feat(crypto): 添加RSA-OAEP + AES-GCM混合加密服务 - 实现CryptoService加密服务,支持RSA-OAEP-2048 + AES-256-GCM混合加密 - 集成数据库层加密,自动加密存储敏感字段(API密钥、私钥等) - 支持环境变量DATA_ENCRYPTION_KEY配置数据加密密钥 - 适配SQLite数据库加密存储(从PostgreSQL移植) - 保持Hyperliquid代理钱包处理兼容性 - 更新.gitignore以正确处理crypto模块代码 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(scripts): 添加加密环境一键设置脚本 - setup_encryption.sh: 一键生成RSA密钥对+数据加密密钥+JWT密钥 - generate_rsa_keys.sh: 专业的RSA-2048密钥对生成工具 - generate_data_key.sh: 生成AES-256数据加密密钥和JWT认证密钥 - ENCRYPTION_README.md: 详细的加密系统说明文档 - 支持自动检测现有密钥并只生成缺失的密钥 - 完善的权限管理和安全验证 - 兼容macOS和Linux的跨平台支持 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(api): 添加加密API端点和Gin框架集成 - 新增CryptoHandler处理加密相关API请求 - 提供/api/crypto/public-key端点获取RSA公钥 - 提供/api/crypto/decrypt端点解密敏感数据 - 适配Gin框架的HTTP处理器格式 - 集成CryptoService到API服务器 - 支持前端加密数据传输和解密 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(web): 添加前端加密服务和两阶段密钥输入组件 - CryptoService: Web Crypto API集成,支持RSA-OAEP加密 - TwoStageKeyModal: 安全的两阶段私钥输入组件,支持剪贴板混淆 - 完善国际化翻译支持加密相关UI文本 - 修复TypeScript类型错误和编译问题 - 支持前端敏感数据加密传输到后端 - 增强用户隐私保护和数据安全 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(auth): 增强JWT认证安全性 - 优先使用环境变量JWT_SECRET而不是数据库配置 - 支持通过.env文件安全配置JWT认证密钥 - 保留数据库配置作为回退机制 - 改进JWT密钥来源日志显示 - 增强系统启动时的安全配置检查 - 支持运行时动态JWT密钥切换 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(docker): 集成加密环境变量到Docker部署 - 添加DATA_ENCRYPTION_KEY环境变量传递到容器 - 添加JWT_SECRET环境变量支持 - 挂载secrets目录使容器可访问RSA密钥文件 - 确保容器内加密服务正常工作 - 解决容器启动失败和加密初始化问题 - 完善Docker Compose加密环境配置 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(start): 集成自动加密环境检测和设置 - 增强check_encryption()函数检测JWT_SECRET和DATA_ENCRYPTION_KEY - 自动运行setup_encryption.sh当检测到缺失密钥时 - 改进加密状态显示,包含RSA+AES+JWT全套加密信息 - 优化用户体验,提供清晰的加密配置反馈 - 支持一键设置完整加密环境 - 确保容器启动前加密环境就绪 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat: format fix * fix(security): 修复前端模型和交易所配置敏感数据明文传输 - 在handleSaveModelConfig中对API密钥进行RSA-OAEP加密 - 在handleSaveExchangeConfig中对API密钥、Secret密钥和Aster私钥进行加密 - 只有非空敏感数据才进行加密处理 - 添加加密失败错误处理和用户友好提示 - 增加encryptionFailed翻译键的中英文支持 - 使用用户ID和会话ID作为加密上下文增强安全性 这修复了之前敏感数据在网络传输中以明文形式发送的安全漏洞。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(crypto): 修复后端加密服务集成和缺失的加密端点 - 添加Server结构体缺少的cryptoService字段 - 实现handleUpdateModelConfigsEncrypted处理器用于模型配置加密传输 - 修复handleUpdateExchangeConfigsEncrypted中的函数调用 - 在前端API中添加updateModelConfigsEncrypted方法 - 统一RSA密钥路径从secrets/rsa_key改为keys/rsa_private.key - 确保前端可以使用加密端点安全传输敏感数据 - 兼容原有加密通信模式和二段输入私钥功能 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> --------- Co-authored-by: icy <icyoung520@gmail.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
933 lines
35 KiB
TypeScript
933 lines
35 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import { Menu, X, ChevronDown } from 'lucide-react'
|
|
import { t, type Language } from '../../i18n/translations'
|
|
|
|
interface HeaderBarProps {
|
|
onLoginClick?: () => void
|
|
isLoggedIn?: boolean
|
|
isHomePage?: boolean
|
|
currentPage?: string
|
|
language?: Language
|
|
onLanguageChange?: (lang: Language) => void
|
|
user?: { email: string } | null
|
|
onLogout?: () => void
|
|
onPageChange?: (page: string) => void
|
|
}
|
|
|
|
export default function HeaderBar({
|
|
isLoggedIn = false,
|
|
isHomePage = false,
|
|
currentPage,
|
|
language = 'zh' as Language,
|
|
onLanguageChange,
|
|
user,
|
|
onLogout,
|
|
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 */}
|
|
<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>
|
|
</a>
|
|
|
|
{/* Desktop Menu */}
|
|
<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>
|
|
|
|
<button
|
|
onClick={() => {
|
|
console.log(
|
|
'FAQ button clicked, onPageChange:',
|
|
onPageChange
|
|
)
|
|
onPageChange?.('faq')
|
|
}}
|
|
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
|
style={{
|
|
color:
|
|
currentPage === 'faq'
|
|
? 'var(--brand-yellow)'
|
|
: 'var(--brand-light-gray)',
|
|
padding: '8px 16px',
|
|
borderRadius: '8px',
|
|
position: 'relative',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (currentPage !== 'faq') {
|
|
e.currentTarget.style.color = 'var(--brand-yellow)'
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (currentPage !== 'faq') {
|
|
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
|
}
|
|
}}
|
|
>
|
|
{/* Background for selected state */}
|
|
{currentPage === 'faq' && (
|
|
<span
|
|
className="absolute inset-0 rounded-lg"
|
|
style={{
|
|
background: 'rgba(240, 185, 11, 0.15)',
|
|
zIndex: -1,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{t('faqNav', 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>
|
|
|
|
<a
|
|
href="/faq"
|
|
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
|
style={{
|
|
color:
|
|
currentPage === 'faq'
|
|
? 'var(--brand-yellow)'
|
|
: 'var(--brand-light-gray)',
|
|
padding: '8px 16px',
|
|
borderRadius: '8px',
|
|
position: 'relative',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (currentPage !== 'faq') {
|
|
e.currentTarget.style.color = 'var(--brand-yellow)'
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (currentPage !== 'faq') {
|
|
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
|
}
|
|
}}
|
|
>
|
|
{/* Background for selected state */}
|
|
{currentPage === 'faq' && (
|
|
<span
|
|
className="absolute inset-0 rounded-lg"
|
|
style={{
|
|
background: 'rgba(240, 185, 11, 0.15)',
|
|
zIndex: -1,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{t('faqNav', 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>
|
|
{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 */}
|
|
<motion.button
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
className="md:hidden"
|
|
style={{ color: 'var(--brand-light-gray)' }}
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
{mobileMenuOpen ? (
|
|
<X className="w-6 h-6" />
|
|
) : (
|
|
<Menu className="w-6 h-6" />
|
|
)}
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Menu */}
|
|
<motion.div
|
|
initial={false}
|
|
animate={
|
|
mobileMenuOpen
|
|
? { height: 'auto', opacity: 1 }
|
|
: { height: 0, opacity: 0 }
|
|
}
|
|
transition={{ duration: 0.3 }}
|
|
className="md:hidden overflow-hidden"
|
|
style={{
|
|
background: 'var(--brand-dark-gray)',
|
|
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
|
|
}}
|
|
>
|
|
<div className="px-4 py-4 space-y-3">
|
|
{/* 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>
|
|
<button
|
|
onClick={() => {
|
|
console.log(
|
|
'移动端 FAQ button clicked, onPageChange:',
|
|
onPageChange
|
|
)
|
|
onPageChange?.('faq')
|
|
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 === 'faq'
|
|
? 'var(--brand-yellow)'
|
|
: 'var(--brand-light-gray)',
|
|
padding: '12px 16px',
|
|
borderRadius: '8px',
|
|
position: 'relative',
|
|
width: '100%',
|
|
textAlign: 'left',
|
|
}}
|
|
>
|
|
{/* Background for selected state */}
|
|
{currentPage === 'faq' && (
|
|
<span
|
|
className="absolute inset-0 rounded-lg"
|
|
style={{
|
|
background: 'rgba(240, 185, 11, 0.15)',
|
|
zIndex: -1,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{t('faqNav', 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>
|
|
))}
|
|
|
|
{/* 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>
|
|
{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>
|
|
)
|
|
}
|