refactor: replace window.location with useNavigate for routing in auth components (#1470)

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-04-13 23:44:14 +08:00
committed by GitHub
parent eef78b7987
commit e1b5a5d833
22 changed files with 1930 additions and 1273 deletions

View File

@@ -1,21 +1,19 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import {
ArrowRight,
Copy,
RefreshCw,
Shield,
Wallet,
X,
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { ArrowRight, Copy, RefreshCw, Shield, Wallet, X } from 'lucide-react'
import { QRCodeSVG } from 'qrcode.react'
import { toast } from 'sonner'
import { useLanguage } from '../contexts/LanguageContext'
import { api } from '../lib/api'
import type { BeginnerOnboardingResponse } from '../types'
import { setBeginnerWalletAddress, markBeginnerOnboardingCompleted } from '../lib/onboarding'
import {
setBeginnerWalletAddress,
markBeginnerOnboardingCompleted,
} from '../lib/onboarding'
export function BeginnerOnboardingPage() {
const { language } = useLanguage()
const navigate = useNavigate()
const [data, setData] = useState<BeginnerOnboardingResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
@@ -79,8 +77,7 @@ export function BeginnerOnboardingPage() {
const handleContinue = () => {
markBeginnerOnboardingCompleted()
window.history.pushState({}, '', '/traders')
window.dispatchEvent(new PopStateEvent('popstate'))
navigate('/traders')
}
return (
@@ -104,7 +101,9 @@ export function BeginnerOnboardingPage() {
<div>
<div
className={`font-semibold uppercase text-nofx-gold/80 ${
isZh ? 'text-[11px] tracking-[0.34em]' : 'text-[10px] tracking-[0.2em]'
isZh
? 'text-[11px] tracking-[0.34em]'
: 'text-[10px] tracking-[0.2em]'
}`}
>
{isZh ? '新手保护' : 'Beginner Guard'}
@@ -136,7 +135,9 @@ export function BeginnerOnboardingPage() {
<div className="overflow-hidden rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(8,11,16,0.94),rgba(5,7,10,0.88))] shadow-[0_24px_120px_rgba(0,0,0,0.58)] backdrop-blur-2xl">
{loading ? (
<div className="flex min-h-[390px] items-center justify-center px-6 text-sm text-zinc-400">
{isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'}
{isZh
? '正在准备你的 Base 钱包...'
: 'Preparing your Base wallet...'}
</div>
) : data ? (
<div className="grid lg:grid-cols-[0.82fr_1.18fr]">
@@ -147,13 +148,17 @@ export function BeginnerOnboardingPage() {
</div>
<div className="mt-4 text-[15px] font-medium text-zinc-300">
{isZh ? '充值地址Base USDC' : 'Deposit address (Base USDC)'}
{isZh
? '充值地址Base USDC'
: 'Deposit address (Base USDC)'}
</div>
<div className="mt-4 flex items-center justify-between gap-3 rounded-[24px] border border-emerald-400/20 bg-emerald-500/7 px-5 py-3.5 shadow-[0_0_0_1px_rgba(16,185,129,0.08)]">
<div className="text-left">
<div className="flex items-baseline gap-3 font-mono font-bold tracking-tight text-emerald-300">
<span className="text-[22px]">{data.balance_usdc}</span>
<span className="text-[22px]">
{data.balance_usdc}
</span>
<span className="text-[20px]">USDC</span>
</div>
</div>
@@ -164,12 +169,16 @@ export function BeginnerOnboardingPage() {
className="inline-flex h-12 w-12 items-center justify-center rounded-2xl border border-emerald-300/20 bg-black/20 text-emerald-300 transition hover:bg-emerald-500/10 disabled:cursor-not-allowed disabled:opacity-60"
aria-label={isZh ? '刷新余额' : 'Refresh balance'}
>
<RefreshCw className={`h-4 w-4 ${refreshingBalance ? 'animate-spin' : ''}`} />
<RefreshCw
className={`h-4 w-4 ${refreshingBalance ? 'animate-spin' : ''}`}
/>
</button>
</div>
<div className="mt-4 text-sm text-zinc-500">
{isZh ? '$5-$10 可以用很久' : '$5-$10 usually lasts a long time'}
{isZh
? '$5-$10 可以用很久'
: '$5-$10 usually lasts a long time'}
</div>
</div>
</section>
@@ -187,7 +196,9 @@ export function BeginnerOnboardingPage() {
</div>
<button
type="button"
onClick={() => copyText(data.address, isZh ? '地址' : 'Address')}
onClick={() =>
copyText(data.address, isZh ? '地址' : 'Address')
}
className="inline-flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/5 text-zinc-300 transition hover:border-white/20 hover:bg-white/10 hover:text-white"
aria-label={isZh ? '复制地址' : 'Copy address'}
>
@@ -199,16 +210,27 @@ export function BeginnerOnboardingPage() {
<div className="pt-1">
<div className="mb-3 flex items-center gap-2 text-sm font-medium text-nofx-gold">
<Shield className="h-4 w-4" />
<span>{isZh ? '私钥,请立即备份' : 'Private key, back it up now'}</span>
<span>
{isZh
? '私钥,请立即备份'
: 'Private key, back it up now'}
</span>
</div>
<div className="flex items-stretch gap-3">
<div className="min-w-0 flex-1 rounded-[24px] border border-nofx-gold/20 bg-[linear-gradient(180deg,rgba(32,25,7,0.44),rgba(14,10,3,0.28))] px-5 py-3 font-mono text-[13px] leading-6 text-amber-100 shadow-[0_0_0_1px_rgba(240,185,11,0.05)]">
<div className="overflow-x-auto whitespace-nowrap">{data.private_key}</div>
<div className="overflow-x-auto whitespace-nowrap">
{data.private_key}
</div>
</div>
<div className="flex shrink-0 flex-col justify-end">
<button
type="button"
onClick={() => copyText(data.private_key, isZh ? '私钥' : 'Private key')}
onClick={() =>
copyText(
data.private_key,
isZh ? '私钥' : 'Private key'
)
}
className="inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-nofx-gold/20 bg-nofx-gold/10 text-nofx-gold transition hover:bg-nofx-gold/15"
aria-label={isZh ? '复制私钥' : 'Copy private key'}
>
@@ -220,7 +242,9 @@ export function BeginnerOnboardingPage() {
<div
className={`rounded-[24px] border border-white/15 bg-black/18 px-5 py-3.5 text-zinc-500 ${
isZh ? 'text-xs lg:whitespace-nowrap' : 'text-[11px] leading-6'
isZh
? 'text-xs lg:whitespace-nowrap'
: 'text-[11px] leading-6'
}`}
>
<span className="mr-2 text-zinc-600"></span>
@@ -246,7 +270,9 @@ export function BeginnerOnboardingPage() {
isZh ? 'text-[20px]' : 'text-[16px] sm:text-[18px]'
}`}
>
<span>{isZh ? '我已保存,进入下一步' : 'I saved it, continue'}</span>
<span>
{isZh ? '我已保存,进入下一步' : 'I saved it, continue'}
</span>
<ArrowRight className="h-5 w-5" />
</button>

View File

@@ -34,24 +34,8 @@ export function LandingPage() {
user={user}
onLogout={logout}
onLoginRequired={handleLoginRequired}
onPageChange={(page) => {
const pathMap: Record<string, string> = {
'data': '/data',
'competition': '/competition',
'strategy-market': '/strategy-market',
'traders': '/traders',
'trader': '/dashboard',
'strategy': '/strategy',
'faq': '/faq',
}
const path = pathMap[page]
if (path) {
window.location.href = path
}
}}
/>
<div className="min-h-screen bg-nofx-bg text-nofx-text font-sans selection:bg-nofx-gold selection:text-black">
<TerminalHero />
<LiveFeed />

View File

@@ -1,6 +1,17 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { User, Cpu, Building2, MessageCircle, Eye, EyeOff, ChevronRight, Plus, Pencil } from 'lucide-react'
import {
User,
Cpu,
Building2,
MessageCircle,
Eye,
EyeOff,
ChevronRight,
Plus,
Pencil,
} from 'lucide-react'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
import { api } from '../lib/api'
@@ -20,8 +31,11 @@ type Tab = 'account' | 'models' | 'exchanges' | 'telegram'
export function SettingsPage() {
const { user } = useAuth()
const { language } = useLanguage()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<Tab>('account')
const [userMode, setUserModeState] = useState<UserMode>(() => getUserMode() ?? 'advanced')
const [userMode, setUserModeState] = useState<UserMode>(
() => getUserMode() ?? 'advanced'
)
// Account state
const [newPassword, setNewPassword] = useState('')
@@ -53,7 +67,8 @@ export function SettingsPage() {
.catch(() => toast.error('Failed to load AI models'))
}
if (activeTab === 'exchanges') {
api.getExchangeConfigs()
api
.getExchangeConfigs()
.then(setExchanges)
.catch(() => toast.error('Failed to load exchanges'))
}
@@ -82,7 +97,9 @@ export function SettingsPage() {
toast.success('Password updated successfully')
setNewPassword('')
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to update password')
toast.error(
err instanceof Error ? err.message : 'Failed to update password'
)
} finally {
setChangingPassword(false)
}
@@ -104,8 +121,7 @@ export function SettingsPage() {
)
const nextPath = getPostAuthPath(nextMode)
window.history.pushState({}, '', nextPath)
window.dispatchEvent(new PopStateEvent('popstate'))
navigate(nextPath)
}
const handleSaveModel = async (
@@ -118,33 +134,48 @@ export function SettingsPage() {
const existingModel = configuredModels.find((m) => m.id === modelId)
const modelTemplate = supportedModels.find((m) => m.id === modelId)
const modelToUpdate = existingModel || modelTemplate
if (!modelToUpdate) { toast.error('Model not found'); return }
if (!modelToUpdate) {
toast.error('Model not found')
return
}
let updatedModels: AIModel[]
if (existingModel) {
updatedModels = configuredModels.map((m) =>
m.id === modelId
? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true }
? {
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
: m
)
} else {
updatedModels = [...configuredModels, {
...modelToUpdate,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}]
updatedModels = [
...configuredModels,
{
...modelToUpdate,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
},
]
}
const request = {
models: Object.fromEntries(
updatedModels.map((m) => [m.provider, {
enabled: m.enabled,
api_key: m.apiKey || '',
custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
}])
updatedModels.map((m) => [
m.provider,
{
enabled: m.enabled,
api_key: m.apiKey || '',
custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
},
])
),
}
await api.updateModelConfigs(request)
@@ -161,16 +192,27 @@ export function SettingsPage() {
const handleDeleteModel = async (modelId: string) => {
try {
const updatedModels = configuredModels.map((m) =>
m.id === modelId ? { ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false } : m
m.id === modelId
? {
...m,
apiKey: '',
customApiUrl: '',
customModelName: '',
enabled: false,
}
: m
)
const request = {
models: Object.fromEntries(
updatedModels.map((m) => [m.provider, {
enabled: m.enabled,
api_key: m.apiKey || '',
custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
}])
updatedModels.map((m) => [
m.provider,
{
enabled: m.enabled,
api_key: m.apiKey || '',
custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
},
])
),
}
await api.updateModelConfigs(request)
@@ -223,7 +265,7 @@ export function SettingsPage() {
},
}
await api.updateExchangeConfigsEncrypted(request)
toast.success('Exchange config updated')
toast.success('Exchange config updated')
} else {
const createRequest = {
exchange_type: exchangeType,
@@ -243,7 +285,7 @@ export function SettingsPage() {
lighter_api_key_index: lighterApiKeyIndex || 0,
}
await api.createExchangeEncrypted(createRequest)
toast.success('Exchange account created')
toast.success('Exchange account created')
}
const refreshed = await api.getExchangeConfigs()
setExchanges(refreshed)
@@ -275,7 +317,10 @@ export function SettingsPage() {
]
return (
<div className="min-h-screen pt-20 pb-12 px-4" style={{ background: '#0B0E11' }}>
<div
className="min-h-screen pt-20 pb-12 px-4"
style={{ background: '#0B0E11' }}
>
<div className="max-w-2xl mx-auto">
<h1 className="text-xl font-bold text-white mb-6">Settings</h1>
@@ -286,9 +331,10 @@ export function SettingsPage() {
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
${activeTab === tab.key
? 'bg-nofx-gold text-black'
: 'text-zinc-400 hover:text-white'
${
activeTab === tab.key
? 'bg-nofx-gold text-black'
: 'text-zinc-400 hover:text-white'
}`}
>
{tab.icon}
@@ -299,7 +345,6 @@ export function SettingsPage() {
{/* Tab Content */}
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-6">
{/* Account Tab */}
{activeTab === 'account' && (
<div className="space-y-6">
@@ -322,8 +367,12 @@ export function SettingsPage() {
</div>
<span className="rounded-full border border-nofx-gold/20 bg-nofx-gold/10 px-3 py-1 text-xs font-semibold text-nofx-gold">
{userMode === 'beginner'
? language === 'zh' ? '当前:新手模式' : 'Current: Beginner'
: language === 'zh' ? '当前:手模式' : 'Current: Advanced'}
? language === 'zh'
? '当前:手模式'
: 'Current: Beginner'
: language === 'zh'
? '当前:老手模式'
: 'Current: Advanced'}
</span>
</div>
@@ -369,10 +418,14 @@ export function SettingsPage() {
</div>
<div className="border-t border-zinc-800 pt-6">
<h3 className="text-sm font-semibold text-white mb-4">Change Password</h3>
<h3 className="text-sm font-semibold text-white mb-4">
Change Password
</h3>
<form onSubmit={handleChangePassword} className="space-y-4">
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">New Password</label>
<label className="block text-xs font-medium text-zinc-400 mb-2">
New Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
@@ -387,7 +440,11 @@ export function SettingsPage() {
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
{showPassword ? (
<EyeOff size={16} />
) : (
<Eye size={16} />
)}
</button>
</div>
</div>
@@ -408,10 +465,14 @@ export function SettingsPage() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-400">
{configuredModels.length} model{configuredModels.length !== 1 ? 's' : ''} configured
{configuredModels.length} model
{configuredModels.length !== 1 ? 's' : ''} configured
</p>
<button
onClick={() => { setEditingModel(null); setShowModelModal(true) }}
onClick={() => {
setEditingModel(null)
setShowModelModal(true)
}}
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
>
<Plus size={14} />
@@ -428,7 +489,10 @@ export function SettingsPage() {
{configuredModels.map((model) => (
<button
key={model.id}
onClick={() => { setEditingModel(model.id); setShowModelModal(true) }}
onClick={() => {
setEditingModel(model.id)
setShowModelModal(true)
}}
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-800/50 hover:bg-zinc-800 border border-zinc-700/50 transition-colors group"
>
<div className="flex items-center gap-3">
@@ -436,15 +500,24 @@ export function SettingsPage() {
<Cpu size={14} className="text-zinc-300" />
</div>
<div className="text-left">
<p className="text-sm font-medium text-white">{model.name}</p>
<p className="text-xs text-zinc-500">{model.provider}</p>
<p className="text-sm font-medium text-white">
{model.name}
</p>
<p className="text-xs text-zinc-500">
{model.provider}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${model.enabled ? 'bg-emerald-500/10 text-emerald-400' : 'bg-zinc-700 text-zinc-500'}`}>
<span
className={`text-xs px-2 py-0.5 rounded-full ${model.enabled ? 'bg-emerald-500/10 text-emerald-400' : 'bg-zinc-700 text-zinc-500'}`}
>
{model.enabled ? 'Active' : 'Inactive'}
</span>
<Pencil size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
<Pencil
size={14}
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
/>
</div>
</button>
))}
@@ -458,10 +531,14 @@ export function SettingsPage() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-zinc-400">
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''} connected
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''}{' '}
connected
</p>
<button
onClick={() => { setEditingExchange(null); setShowExchangeModal(true) }}
onClick={() => {
setEditingExchange(null)
setShowExchangeModal(true)
}}
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
>
<Plus size={14} />
@@ -478,7 +555,10 @@ export function SettingsPage() {
{exchanges.map((exchange) => (
<button
key={exchange.id}
onClick={() => { setEditingExchange(exchange.id); setShowExchangeModal(true) }}
onClick={() => {
setEditingExchange(exchange.id)
setShowExchangeModal(true)
}}
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-800/50 hover:bg-zinc-800 border border-zinc-700/50 transition-colors group"
>
<div className="flex items-center gap-3">
@@ -486,11 +566,18 @@ export function SettingsPage() {
<Building2 size={14} className="text-zinc-300" />
</div>
<div className="text-left">
<p className="text-sm font-medium text-white">{exchange.account_name || exchange.name}</p>
<p className="text-xs text-zinc-500 capitalize">{exchange.exchange_type || exchange.type}</p>
<p className="text-sm font-medium text-white">
{exchange.account_name || exchange.name}
</p>
<p className="text-xs text-zinc-500 capitalize">
{exchange.exchange_type || exchange.type}
</p>
</div>
</div>
<ChevronRight size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
<ChevronRight
size={14}
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
/>
</button>
))}
</div>
@@ -502,7 +589,8 @@ export function SettingsPage() {
{activeTab === 'telegram' && (
<div className="space-y-4">
<p className="text-sm text-zinc-400">
Connect a Telegram bot to receive trading notifications and interact with your traders.
Connect a Telegram bot to receive trading notifications and
interact with your traders.
</p>
<button
onClick={() => setShowTelegramModal(true)}
@@ -512,9 +600,14 @@ export function SettingsPage() {
<div className="w-8 h-8 rounded-lg bg-[#0088cc]/20 flex items-center justify-center">
<MessageCircle size={14} className="text-[#0088cc]" />
</div>
<span className="text-sm font-medium text-white">Configure Telegram Bot</span>
<span className="text-sm font-medium text-white">
Configure Telegram Bot
</span>
</div>
<ChevronRight size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
<ChevronRight
size={14}
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
/>
</button>
</div>
)}
@@ -530,7 +623,10 @@ export function SettingsPage() {
editingModelId={editingModel}
onSave={handleSaveModel}
onDelete={handleDeleteModel}
onClose={() => { setShowModelModal(false); setEditingModel(null) }}
onClose={() => {
setShowModelModal(false)
setEditingModel(null)
}}
language={language}
/>
</div>
@@ -544,7 +640,10 @@ export function SettingsPage() {
editingExchangeId={editingExchange}
onSave={handleSaveExchange}
onDelete={handleDeleteExchange}
onClose={() => { setShowExchangeModal(false); setEditingExchange(null) }}
onClose={() => {
setShowExchangeModal(false)
setEditingExchange(null)
}}
language={language}
/>
</div>

View File

@@ -1,5 +1,6 @@
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useNavigate } from 'react-router-dom'
import useSWR from 'swr'
import {
TrendingUp,
@@ -15,7 +16,7 @@ import {
Activity,
Terminal,
Cpu,
Database
Database,
} from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
@@ -39,14 +40,24 @@ interface PublicStrategy {
updated_at: string
}
const strategyStyles: Record<string, { color: string; border: string; glow: string; shadow: string; icon: any; bg: string }> = {
const strategyStyles: Record<
string,
{
color: string
border: string
glow: string
shadow: string
icon: any
bg: string
}
> = {
scalper: {
color: 'text-[#F0B90B]',
border: 'border-[#F0B90B]/30',
glow: 'shadow-[0_0_20px_rgba(240,185,11,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(240,185,11,0.25)]',
bg: 'bg-[#F0B90B]/5',
icon: Zap
icon: Zap,
},
swing: {
color: 'text-cyan-400',
@@ -54,7 +65,7 @@ const strategyStyles: Record<string, { color: string; border: string; glow: stri
glow: 'shadow-[0_0_20px_rgba(34,211,238,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(34,211,238,0.25)]',
bg: 'bg-cyan-400/5',
icon: TrendingUp
icon: TrendingUp,
},
arbitrage: {
color: 'text-purple-400',
@@ -62,7 +73,7 @@ const strategyStyles: Record<string, { color: string; border: string; glow: stri
glow: 'shadow-[0_0_20px_rgba(192,132,252,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(192,132,252,0.25)]',
bg: 'bg-purple-400/5',
icon: Layers
icon: Layers,
},
conservative: {
color: 'text-emerald-400',
@@ -70,7 +81,7 @@ const strategyStyles: Record<string, { color: string; border: string; glow: stri
glow: 'shadow-[0_0_20px_rgba(52,211,153,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(52,211,153,0.25)]',
bg: 'bg-emerald-400/5',
icon: Shield
icon: Shield,
},
aggressive: {
color: 'text-red-500',
@@ -78,7 +89,7 @@ const strategyStyles: Record<string, { color: string; border: string; glow: stri
glow: 'shadow-[0_0_20px_rgba(239,68,68,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(239,68,68,0.25)]',
bg: 'bg-red-500/5',
icon: Target
icon: Target,
},
default: {
color: 'text-zinc-400',
@@ -86,8 +97,8 @@ const strategyStyles: Record<string, { color: string; border: string; glow: stri
glow: '',
shadow: 'hover:shadow-[0_0_20px_rgba(255,255,255,0.05)]',
bg: 'bg-zinc-800/20',
icon: Activity
}
icon: Activity,
},
}
function getStrategyStyle(name: string) {
@@ -95,12 +106,15 @@ function getStrategyStyle(name: string) {
if (lowerName.includes('scalp')) return strategyStyles.scalper
if (lowerName.includes('swing')) return strategyStyles.swing
if (lowerName.includes('arb')) return strategyStyles.arbitrage
if (lowerName.includes('safe') || lowerName.includes('conserv')) return strategyStyles.conservative
if (lowerName.includes('aggress') || lowerName.includes('high')) return strategyStyles.aggressive
if (lowerName.includes('safe') || lowerName.includes('conserv'))
return strategyStyles.conservative
if (lowerName.includes('aggress') || lowerName.includes('high'))
return strategyStyles.aggressive
return strategyStyles.default
}
export function StrategyMarketPage() {
const navigate = useNavigate()
const { language } = useLanguage()
const { token, user } = useAuth()
const [searchQuery, setSearchQuery] = useState('')
@@ -120,23 +134,28 @@ export function StrategyMarketPage() {
},
{
refreshInterval: 60000,
revalidateOnFocus: false
revalidateOnFocus: false,
}
)
const filteredStrategies = strategies?.filter(s => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
return s.name.toLowerCase().includes(query) ||
s.description?.toLowerCase().includes(query)
}
return true
}) || []
const filteredStrategies =
strategies?.filter((s) => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
return (
s.name.toLowerCase().includes(query) ||
s.description?.toLowerCase().includes(query)
)
}
return true
}) || []
const handleCopyConfig = async (strategy: PublicStrategy) => {
if (!strategy.config) return
try {
await navigator.clipboard.writeText(JSON.stringify(strategy.config, null, 2))
await navigator.clipboard.writeText(
JSON.stringify(strategy.config, null, 2)
)
setCopiedId(strategy.id)
toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000)
@@ -147,14 +166,16 @@ export function StrategyMarketPage() {
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).replace(',', '')
return date
.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
.replace(',', '')
}
const getIndicatorList = (config: any) => {
@@ -174,15 +195,15 @@ export function StrategyMarketPage() {
return (
<DeepVoidBackground className="min-h-screen text-white font-mono py-12">
<div className="w-full px-4 md:px-8 space-y-8">
<div className="w-full relative z-10">
{/* Header Section */}
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
SYSTEM_STATUS: <span className="text-emerald-500 animate-pulse">ONLINE</span>
SYSTEM_STATUS:{' '}
<span className="text-emerald-500 animate-pulse">ONLINE</span>
<br />
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
MARKET_UPLINK:{' '}
<span className="text-emerald-500">ESTABLISHED</span>
</div>
<div className="flex items-center gap-4 mb-4">
@@ -191,11 +212,15 @@ export function StrategyMarketPage() {
<Database className="w-8 h-8 text-nofx-gold relative z-10" />
</div>
<div>
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={tr('title')}>
<h1
className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text"
data-text={tr('title')}
>
{tr('title')}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
// {tr('subtitle')}
{'// '}
{tr('subtitle')}
</p>
</div>
</div>
@@ -232,16 +257,21 @@ export function StrategyMarketPage() {
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${
selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
>
{selectedCategory === cat && (
<motion.div
layoutId="filter-highlight"
className="absolute inset-0 bg-nofx-gold"
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
transition={{
type: 'spring',
bounce: 0.2,
duration: 0.6,
}}
/>
)}
<span className="relative z-10">{tr(cat)}</span>
@@ -260,11 +290,22 @@ export function StrategyMarketPage() {
<Cpu size={24} className="text-nofx-gold/50" />
</div>
</div>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{tr('loading')}</p>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">
{tr('loading')}
</p>
<div className="flex gap-1">
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.4s' }}
></div>
</div>
</div>
)}
@@ -279,7 +320,9 @@ export function StrategyMarketPage() {
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{tr('noStrategies')}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">{tr('noStrategiesDesc')}</p>
<p className="text-zinc-600 text-xs tracking-wide uppercase">
{tr('noStrategiesDesc')}
</p>
</div>
)}
@@ -290,9 +333,10 @@ export function StrategyMarketPage() {
{filteredStrategies.map((strategy, i) => {
const style = getStrategyStyle(strategy.name)
const Icon = style.icon
const indicators = strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
const indicators =
strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
return (
<motion.div
@@ -304,16 +348,24 @@ export function StrategyMarketPage() {
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
>
{/* Holographic Border Highlight */}
<div className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
<div className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}></div>
<div
className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
<div
className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
{/* Category Side Strip */}
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
<div
className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}
></div>
<div className="p-6 relative">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div className={`p-2 rounded-none border ${style.border} ${style.bg}`}>
<div
className={`p-2 rounded-none border ${style.border} ${style.bg}`}
>
<Icon className={`w-5 h-5 ${style.color}`} />
</div>
<div className="text-[10px] font-mono">
@@ -332,7 +384,9 @@ export function StrategyMarketPage() {
</div>
{/* Name and Description */}
<h3 className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}>
<h3
className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}
>
{strategy.name}
<span className="absolute -bottom-1 left-0 w-8 h-[2px] bg-zinc-800 group-hover:bg-nofx-gold transition-colors"></span>
</h3>
@@ -343,12 +397,22 @@ export function StrategyMarketPage() {
{/* Meta Data */}
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
<div className="flex flex-col">
<span className="text-zinc-700 uppercase">{tr('author')}</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">@{strategy.author_email?.split('@')[0] || 'UNKNOWN'}</span>
<span className="text-zinc-700 uppercase">
{tr('author')}
</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">
@
{strategy.author_email?.split('@')[0] ||
'UNKNOWN'}
</span>
</div>
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">{tr('createdAt')}</span>
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
<span className="text-zinc-700 uppercase">
{tr('createdAt')}
</span>
<span className="text-zinc-400">
{formatDate(strategy.created_at)}
</span>
</div>
</div>
@@ -358,14 +422,20 @@ export function StrategyMarketPage() {
<div className="space-y-3">
{/* Indicators */}
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide pb-1">
{indicators.length > 0 ? indicators.map((ind) => (
<span
key={ind}
className="px-1.5 py-0.5 border border-zinc-700 bg-zinc-800 text-[9px] text-zinc-300 font-mono whitespace-nowrap"
>
{ind}
{indicators.length > 0 ? (
indicators.map((ind) => (
<span
key={ind}
className="px-1.5 py-0.5 border border-zinc-700 bg-zinc-800 text-[9px] text-zinc-300 font-mono whitespace-nowrap"
>
{ind}
</span>
))
) : (
<span className="text-[9px] text-zinc-600">
NO_INDICATORS
</span>
)) : <span className="text-[9px] text-zinc-600">NO_INDICATORS</span>}
)}
</div>
{/* Risk Control */}
@@ -373,22 +443,38 @@ export function StrategyMarketPage() {
<div className="flex justify-between items-center text-[10px]">
<div className="flex gap-3">
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">LEV</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.btc_eth_max_leverage || '-'}x</span>
<span className="text-zinc-600 scale-90 origin-left">
LEV
</span>
<span className="text-zinc-300 font-bold">
{strategy.config.risk_control
.btc_eth_max_leverage || '-'}
x
</span>
</div>
<div className="flex flex-col">
<span className="text-zinc-600 scale-90 origin-left">POS</span>
<span className="text-zinc-300 font-bold">{strategy.config.risk_control.max_positions || '-'}</span>
<span className="text-zinc-600 scale-90 origin-left">
POS
</span>
<span className="text-zinc-300 font-bold">
{strategy.config.risk_control
.max_positions || '-'}
</span>
</div>
</div>
<Activity size={12} className="text-zinc-700" />
<Activity
size={12}
className="text-zinc-700"
/>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
<EyeOff size={16} className="mb-1 opacity-50" />
<span className="text-[9px] uppercase tracking-widest">{tr('configHiddenDesc')}</span>
<span className="text-[9px] uppercase tracking-widest">
{tr('configHiddenDesc')}
</span>
</div>
)}
</div>
@@ -403,7 +489,9 @@ export function StrategyMarketPage() {
{copiedId === strategy.id ? (
<>
<Check className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-500">{tr('copied')}</span>
<span className="text-emerald-500">
{tr('copied')}
</span>
</>
) : (
<>
@@ -413,13 +501,15 @@ export function StrategyMarketPage() {
)}
</button>
) : (
<button disabled className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2">
<button
disabled
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-800 bg-black text-zinc-700 cursor-not-allowed flex items-center justify-center gap-2"
>
<Shield size={12} />
{tr('hideConfig')}
</button>
)}
</div>
</div>
</motion.div>
)
@@ -436,13 +526,23 @@ export function StrategyMarketPage() {
transition={{ delay: 0.3 }}
className="mt-16 mb-20 flex justify-center"
>
<div className="relative group cursor-pointer" onClick={() => window.location.href = '/strategy'}>
<div
className="relative group cursor-pointer"
onClick={() => navigate('/strategy')}
>
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold to-yellow-600 rounded blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon className="text-nofx-gold animate-spin-slow" size={24} />
<Hexagon
className="text-nofx-gold animate-spin-slow"
size={24}
/>
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{tr('shareYours')}</div>
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">
{tr('shareYours')}
</div>
<div className="text-[10px] text-zinc-500 font-mono">
CONTRIBUTE TO THE GLOBAL DATABASE
</div>
</div>
<div className="w-[1px] h-8 bg-zinc-800 mx-2"></div>
<div className="text-xs font-mono text-zinc-400 group-hover:translate-x-1 transition-transform">
@@ -452,7 +552,6 @@ export function StrategyMarketPage() {
</div>
</motion.div>
)}
</div>
</div>
</DeepVoidBackground>