mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat: add DeepVoidBackground and update UI theme across pages
- Add DeepVoidBackground component with animated gradient effects - Apply nofx theme classes to StrategyStudioPage, AITradersPage, etc. - Update styling for consistent dark theme with gold accents - Add PageNotFound and TraderDashboardPage components
This commit is contained in:
1021
web/src/App.tsx
1021
web/src/App.tsx
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -145,43 +145,41 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
console.log('[ChartTabs] rendering, activeTab:', activeTab)
|
||||
|
||||
return (
|
||||
<div className="binance-card" style={{ background: '#0D1117', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<div className="nofx-glass rounded-lg border border-white/5 relative z-10 w-full h-[600px] flex flex-col">
|
||||
{/* Clean Professional Toolbar */}
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-1.5"
|
||||
style={{ borderBottom: '1px solid rgba(43, 49, 57, 0.6)', background: '#161B22' }}
|
||||
className="relative z-20 flex flex-wrap md:flex-nowrap items-center justify-between gap-y-2 px-3 py-2 shrink-0 backdrop-blur-md bg-[#0B0E11]/80 rounded-t-lg"
|
||||
style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}
|
||||
>
|
||||
{/* Left: Tab Switcher */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('equity')}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
||||
activeTab === 'equity'
|
||||
? 'bg-blue-500/15 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
}`}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'equity'
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
<BarChart3 className="w-3.5 h-3.5" />
|
||||
<span>{t('accountEquityCurve', language)}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('kline')}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
||||
activeTab === 'kline'
|
||||
? 'bg-blue-500/15 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
}`}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'kline'
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<CandlestickChart className="w-3 h-3" />
|
||||
<CandlestickChart className="w-3.5 h-3.5" />
|
||||
<span>{t('marketChart', language)}</span>
|
||||
</button>
|
||||
|
||||
{/* Market Type Pills - Only when kline active */}
|
||||
{activeTab === 'kline' && (
|
||||
<>
|
||||
<div className="w-px h-3 bg-[#30363D] mx-1" />
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="w-px h-4 bg-white/10 mx-2" />
|
||||
<div className="flex items-center gap-1">
|
||||
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
|
||||
const config = MARKET_CONFIG[type]
|
||||
const isActive = marketType === type
|
||||
@@ -189,13 +187,13 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => handleMarketTypeChange(type)}
|
||||
className={`px-2 py-0.5 text-[10px] font-medium rounded transition-all ${
|
||||
isActive
|
||||
? 'bg-[#21262D] text-white'
|
||||
: 'text-gray-500 hover:text-gray-400'
|
||||
}`}
|
||||
className={`px-2.5 py-1 text-[10px] font-medium rounded transition-all border ${isActive
|
||||
? 'bg-white/10 text-white border-white/20'
|
||||
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{config.icon} {language === 'zh' ? config.label.zh : config.label.en}
|
||||
<span className="mr-1 opacity-70">{config.icon}</span>
|
||||
{language === 'zh' ? config.label.zh : config.label.en}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -206,87 +204,89 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
|
||||
{/* Right: Symbol + Interval */}
|
||||
{activeTab === 'kline' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-3 w-full md:w-auto min-w-0">
|
||||
{/* Symbol Dropdown */}
|
||||
{marketConfig.hasDropdown ? (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-[#21262D] rounded text-[11px] font-bold text-white hover:bg-[#30363D] transition-all"
|
||||
>
|
||||
<span>{chartSymbol}</span>
|
||||
<ChevronDown className={`w-3 h-3 text-gray-400 transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<div className="absolute top-full right-0 mt-1 w-56 bg-[#161B22] border border-[#30363D] rounded-lg shadow-2xl z-50 max-h-72 overflow-hidden">
|
||||
<div className="p-2 border-b border-[#30363D]">
|
||||
<div className="flex items-center gap-2 px-2 py-1 bg-[#0D1117] rounded border border-[#30363D]">
|
||||
<Search className="w-3 h-3 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
placeholder="Search..."
|
||||
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="shrink-0 relative" ref={dropdownRef}>
|
||||
{marketConfig.hasDropdown ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main hover:border-nofx-gold/30 hover:text-nofx-gold transition-all"
|
||||
>
|
||||
<span>{chartSymbol}</span>
|
||||
<ChevronDown className={`w-3 h-3 text-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{showDropdown && (
|
||||
<div className="absolute top-full right-0 mt-2 w-64 bg-[#0B0E11] border border-white/10 rounded-lg shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)] z-50 overflow-hidden nofx-glass ring-1 ring-white/5">
|
||||
<div className="p-2 border-b border-white/5">
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 bg-black/40 rounded border border-white/10 focus-within:border-nofx-gold/50 transition-colors">
|
||||
<Search className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
placeholder="Search symbol..."
|
||||
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none font-mono"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-60 custom-scrollbar">
|
||||
{['crypto', 'stock', 'forex', 'commodity', 'index'].map(category => {
|
||||
const categorySymbols = filteredSymbols.filter(s => s.category === category)
|
||||
if (categorySymbols.length === 0) return null
|
||||
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Index' }
|
||||
return (
|
||||
<div key={category}>
|
||||
<div className="px-3 py-1.5 text-[9px] font-bold text-nofx-text-muted/60 bg-white/5 uppercase tracking-wider">{labels[category]}</div>
|
||||
{categorySymbols.map(s => (
|
||||
<button
|
||||
key={s.symbol}
|
||||
onClick={() => { setChartSymbol(s.symbol); setShowDropdown(false); setSearchFilter('') }}
|
||||
className={`w-full px-3 py-2 text-left text-[11px] font-mono hover:bg-white/5 transition-all flex items-center justify-between ${chartSymbol === s.symbol ? 'bg-nofx-gold/10 text-nofx-gold' : 'text-nofx-text-muted'}`}
|
||||
>
|
||||
<span>{s.symbol}</span>
|
||||
<span className="text-[9px] opacity-40">{s.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-y-auto max-h-52">
|
||||
{['crypto', 'stock', 'forex', 'commodity', 'index'].map(category => {
|
||||
const categorySymbols = filteredSymbols.filter(s => s.category === category)
|
||||
if (categorySymbols.length === 0) return null
|
||||
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Index' }
|
||||
return (
|
||||
<div key={category}>
|
||||
<div className="px-3 py-1 text-[9px] font-medium text-gray-500 bg-[#0D1117] uppercase tracking-wider">{labels[category]}</div>
|
||||
{categorySymbols.map(s => (
|
||||
<button
|
||||
key={s.symbol}
|
||||
onClick={() => { setChartSymbol(s.symbol); setShowDropdown(false); setSearchFilter('') }}
|
||||
className={`w-full px-3 py-1.5 text-left text-[11px] hover:bg-[#21262D] transition-all ${chartSymbol === s.symbol ? 'bg-blue-500/20 text-blue-400' : 'text-gray-300'}`}
|
||||
>
|
||||
{s.symbol}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-[#21262D] rounded text-[11px] font-bold text-white">{chartSymbol}</span>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="px-2.5 py-1 bg-black/40 border border-white/10 rounded text-[11px] font-bold text-nofx-text-main font-mono">{chartSymbol}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Interval Selector */}
|
||||
<div className="flex items-center bg-[#21262D] rounded overflow-hidden">
|
||||
{/* Interval Selector - Allow scrolling if needed */}
|
||||
<div className="flex items-center bg-black/40 rounded border border-white/10 overflow-x-auto no-scrollbar max-w-[200px] md:max-w-none">
|
||||
{INTERVALS.map((int) => (
|
||||
<button
|
||||
key={int.value}
|
||||
onClick={() => setInterval(int.value)}
|
||||
className={`px-2 py-1 text-[10px] font-medium transition-all ${
|
||||
interval === int.value
|
||||
? 'bg-blue-500/30 text-blue-400'
|
||||
: 'text-gray-500 hover:text-gray-300 hover:bg-[#30363D]'
|
||||
}`}
|
||||
className={`px-2 py-1 text-[10px] font-medium transition-all ${interval === int.value
|
||||
? 'bg-nofx-gold/20 text-nofx-gold'
|
||||
: 'text-nofx-text-muted hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{int.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Input */}
|
||||
<form onSubmit={handleSymbolSubmit} className="flex items-center">
|
||||
{/* Quick Input - Hidden on mobile, dropdown search is enough */}
|
||||
<form onSubmit={handleSymbolSubmit} className="hidden md:flex items-center shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={symbolInput}
|
||||
onChange={(e) => setSymbolInput(e.target.value)}
|
||||
placeholder="Symbol..."
|
||||
className="w-20 px-2 py-1 bg-[#0D1117] border border-[#30363D] rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-blue-500/50"
|
||||
placeholder="Sym"
|
||||
className="w-16 px-2 py-1 bg-black/40 border border-white/10 rounded-l text-[10px] text-white placeholder-gray-600 focus:outline-none focus:border-nofx-gold/50 font-mono transition-colors"
|
||||
/>
|
||||
<button type="submit" className="px-2 py-1 bg-[#21262D] border border-[#30363D] border-l-0 rounded-r text-[10px] text-gray-400 hover:text-white hover:bg-[#30363D] transition-all">
|
||||
<button type="submit" className="px-2 py-1 bg-white/5 border border-white/10 border-l-0 rounded-r text-[10px] text-nofx-text-muted hover:text-white hover:bg-white/10 transition-all">
|
||||
Go
|
||||
</button>
|
||||
</form>
|
||||
@@ -295,32 +295,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="relative overflow-hidden min-h-[400px]">
|
||||
<div className="relative flex-1 bg-[#0B0E11]/50 rounded-b-lg overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'equity' ? (
|
||||
<motion.div
|
||||
key="equity"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
<EquityChart traderId={traderId} embedded />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={`kline-${chartSymbol}-${interval}-${currentExchange}`}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="h-full"
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
<AdvancedChart
|
||||
symbol={chartSymbol}
|
||||
interval={interval}
|
||||
traderID={traderId}
|
||||
// Dynamic height to fill container
|
||||
height={550}
|
||||
exchange={currentExchange}
|
||||
onSymbolChange={setChartSymbol}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTraderColor } from '../utils/traderColors'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
|
||||
export function CompetitionPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -44,83 +45,78 @@ export function CompetitionPage() {
|
||||
|
||||
if (!competition) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="binance-card p-8 animate-pulse">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="skeleton h-8 w-64"></div>
|
||||
<div className="skeleton h-4 w-48"></div>
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="container mx-auto max-w-7xl px-4 md:px-8">
|
||||
<div className="space-y-6">
|
||||
<div className="animate-pulse bg-black/40 border border-white/10 rounded-xl p-8 backdrop-blur-md">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="h-8 w-64 bg-white/5 rounded"></div>
|
||||
<div className="h-4 w-48 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
<div className="h-12 w-32 bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md">
|
||||
<div className="h-6 w-40 mb-4 bg-white/5 rounded"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="h-20 w-full bg-white/5 rounded"></div>
|
||||
<div className="h-20 w-full bg-white/5 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skeleton h-12 w-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="binance-card p-6">
|
||||
<div className="skeleton h-6 w-40 mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
<div className="skeleton h-20 w-full rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
// 如果有数据返回但没有交易员,显示空状态
|
||||
if (!competition.traders || competition.traders.length === 0) {
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
style={{ color: '#000' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="container mx-auto max-w-7xl px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
|
||||
>
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
0 {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||
>
|
||||
0 {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400">
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
<div className="binance-card p-8 text-center">
|
||||
<Trophy
|
||||
className="w-16 h-16 mx-auto mb-4 opacity-40"
|
||||
style={{ color: '#848E9C' }}
|
||||
/>
|
||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('noTraders', language)}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('createFirstTrader', language)}
|
||||
</p>
|
||||
{/* Empty State */}
|
||||
<div className="bg-black/40 border border-white/10 rounded-xl p-16 text-center backdrop-blur-md">
|
||||
<Trophy
|
||||
className="w-16 h-16 mx-auto mb-4 text-zinc-700"
|
||||
/>
|
||||
<h3 className="text-lg font-bold mb-2 text-white">
|
||||
{t('noTraders', language)}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
{t('createFirstTrader', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,375 +129,358 @@ export function CompetitionPage() {
|
||||
const leader = sortedTraders[0]
|
||||
|
||||
return (
|
||||
<div className="space-y-5 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
style={{ color: '#000' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
<DeepVoidBackground className="py-8" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||
{/* Competition Header - 精简版 */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center bg-black/60 border border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
|
||||
>
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
<Trophy
|
||||
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left md:text-right w-full md:w-auto">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
{t('leader', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-base md:text-lg font-bold"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{leader?.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-semibold"
|
||||
style={{
|
||||
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
|
||||
}}
|
||||
>
|
||||
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Left/Right Split: Performance Chart + Leaderboard */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Left: Performance Comparison Chart */}
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('realTimePnL', language)}
|
||||
{t('aiCompetition', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||
>
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400">
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
|
||||
</div>
|
||||
|
||||
{/* Right: Leaderboard */}
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('leaderboard', language)}
|
||||
</h2>
|
||||
<div className="text-left md:text-right w-full md:w-auto">
|
||||
<div className="text-xs mb-1 text-zinc-400">
|
||||
{t('leader', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs px-2 py-1 rounded"
|
||||
className="text-base md:text-lg font-bold text-nofx-gold"
|
||||
>
|
||||
{leader?.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-semibold"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
color: '#F0B90B',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
|
||||
}}
|
||||
>
|
||||
{t('live', language)}
|
||||
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isLeader = index === 0
|
||||
const traderColor = getTraderColor(
|
||||
sortedTraders,
|
||||
trader.trader_id
|
||||
)
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
onClick={() => handleTraderClick(trader.trader_id)}
|
||||
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
|
||||
style={{
|
||||
background: isLeader
|
||||
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
|
||||
: '#0B0E11',
|
||||
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||
boxShadow: isLeader
|
||||
? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
|
||||
: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Rank & Avatar & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Rank Badge */}
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: index === 0
|
||||
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
|
||||
: index === 1
|
||||
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
|
||||
: index === 2
|
||||
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
|
||||
: '#2B3139',
|
||||
color: index < 3 ? '#000' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
{/* Punk Avatar */}
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={36}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="font-bold text-sm"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mono font-semibold"
|
||||
style={{ color: traderColor }}
|
||||
>
|
||||
{trader.ai_model.toUpperCase()} +{' '}
|
||||
{trader.exchange.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Left/Right Split: Performance Chart + Leaderboard */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left: Performance Comparison Chart */}
|
||||
<div
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in hover:border-white/20 transition-colors"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs text-zinc-400">
|
||||
{t('realTimePnL', language)}
|
||||
</div>
|
||||
</div>
|
||||
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
|
||||
{/* Total Equity */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('equity', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs md:text-sm font-bold mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
{/* Right: Leaderboard */}
|
||||
<div
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in hover:border-white/20 transition-colors"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2
|
||||
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('leaderboard', language)}
|
||||
</h2>
|
||||
<div
|
||||
className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_8px_rgba(240,185,11,0.1)]"
|
||||
>
|
||||
{t('live', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isLeader = index === 0
|
||||
const traderColor = getTraderColor(
|
||||
sortedTraders,
|
||||
trader.trader_id
|
||||
)
|
||||
|
||||
{/* P&L */}
|
||||
<div className="text-right min-w-[70px] md:min-w-[90px]">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('pnl', language)}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
onClick={() => handleTraderClick(trader.trader_id)}
|
||||
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
|
||||
style={{
|
||||
background: isLeader
|
||||
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
|
||||
: '#0B0E11',
|
||||
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
||||
boxShadow: isLeader
|
||||
? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
|
||||
: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Rank & Avatar & Name */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Rank Badge */}
|
||||
<div
|
||||
className="text-base md:text-lg font-bold mono"
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
color:
|
||||
(trader.total_pnl ?? 0) >= 0
|
||||
? '#0ECB81'
|
||||
: '#F6465D',
|
||||
background: index === 0
|
||||
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
|
||||
: index === 1
|
||||
? 'linear-gradient(135deg, #C0C0C0 0%, #E8E8E8 100%)'
|
||||
: index === 2
|
||||
? 'linear-gradient(135deg, #CD7F32 0%, #E8A64C 100%)'
|
||||
: '#2B3139',
|
||||
color: index < 3 ? '#000' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
{index + 1}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mono"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{trader.total_pnl?.toFixed(2) || '0.00'}
|
||||
{/* Punk Avatar */}
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={36}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="font-bold text-sm"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mono font-semibold"
|
||||
style={{ color: traderColor }}
|
||||
>
|
||||
{trader.ai_model.toUpperCase()} +{' '}
|
||||
{trader.exchange.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('pos', language)}
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
|
||||
{/* Total Equity */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('equity', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs md:text-sm font-bold mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs md:text-sm font-bold mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.position_count}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{trader.margin_used_pct.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs font-bold"
|
||||
style={
|
||||
trader.is_running
|
||||
? {
|
||||
{/* P&L */}
|
||||
<div className="text-right min-w-[70px] md:min-w-[90px]">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('pnl', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-base md:text-lg font-bold mono"
|
||||
style={{
|
||||
color:
|
||||
(trader.total_pnl ?? 0) >= 0
|
||||
? '#0ECB81'
|
||||
: '#F6465D',
|
||||
}}
|
||||
>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mono"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||
{trader.total_pnl?.toFixed(2) || '0.00'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('pos', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs md:text-sm font-bold mono"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.position_count}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{trader.margin_used_pct.toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs font-bold"
|
||||
style={
|
||||
trader.is_running
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
: {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
}
|
||||
>
|
||||
{trader.is_running ? '●' : '○'}
|
||||
}
|
||||
>
|
||||
{trader.is_running ? '●' : '○'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Head-to-Head Stats */}
|
||||
{competition.traders.length === 2 && (
|
||||
<div
|
||||
className="binance-card p-5 animate-slide-in"
|
||||
style={{ animationDelay: '0.3s' }}
|
||||
>
|
||||
<h2
|
||||
className="text-lg font-bold mb-4 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
{/* Head-to-Head Stats */}
|
||||
{competition.traders.length === 2 && (
|
||||
<div
|
||||
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in"
|
||||
style={{ animationDelay: '0.3s' }}
|
||||
>
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isWinning = index === 0
|
||||
const opponent = sortedTraders[1 - index]
|
||||
<h2
|
||||
className="text-lg font-bold mb-6 flex items-center gap-2 text-white"
|
||||
>
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
const isWinning = index === 0
|
||||
const opponent = sortedTraders[1 - index]
|
||||
|
||||
// Check if both values are valid numbers
|
||||
const hasValidData =
|
||||
trader.total_pnl_pct != null &&
|
||||
opponent.total_pnl_pct != null &&
|
||||
!isNaN(trader.total_pnl_pct) &&
|
||||
!isNaN(opponent.total_pnl_pct)
|
||||
// Check if both values are valid numbers
|
||||
const hasValidData =
|
||||
trader.total_pnl_pct != null &&
|
||||
opponent.total_pnl_pct != null &&
|
||||
!isNaN(trader.total_pnl_pct) &&
|
||||
!isNaN(opponent.total_pnl_pct)
|
||||
|
||||
const gap = hasValidData
|
||||
? trader.total_pnl_pct - opponent.total_pnl_pct
|
||||
: NaN
|
||||
const gap = hasValidData
|
||||
? trader.total_pnl_pct - opponent.total_pnl_pct
|
||||
: NaN
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
|
||||
style={
|
||||
isWinning
|
||||
? {
|
||||
return (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
|
||||
style={
|
||||
isWinning
|
||||
? {
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(14, 203, 129, 0.08) 0%, rgba(14, 203, 129, 0.02) 100%)',
|
||||
border: '2px solid rgba(14, 203, 129, 0.3)',
|
||||
boxShadow: '0 3px 15px rgba(14, 203, 129, 0.12)',
|
||||
}
|
||||
: {
|
||||
: {
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="text-center">
|
||||
{/* Avatar */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={56}
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-sm md:text-base font-bold mb-2"
|
||||
style={{
|
||||
color: getTraderColor(sortedTraders, trader.trader_id),
|
||||
}}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-lg md:text-2xl font-bold mono mb-1"
|
||||
style={{
|
||||
color:
|
||||
(trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
|
||||
}}
|
||||
>
|
||||
{trader.total_pnl_pct != null &&
|
||||
!isNaN(trader.total_pnl_pct)
|
||||
? `${trader.total_pnl_pct >= 0 ? '+' : ''}${trader.total_pnl_pct.toFixed(2)}%`
|
||||
: '—'}
|
||||
</div>
|
||||
{hasValidData && isWinning && gap > 0 && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#0ECB81' }}
|
||||
>
|
||||
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||
}
|
||||
>
|
||||
<div className="text-center">
|
||||
{/* Avatar */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={56}
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasValidData && !isWinning && gap < 0 && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#F6465D' }}
|
||||
className="text-sm md:text-base font-bold mb-2"
|
||||
style={{
|
||||
color: getTraderColor(sortedTraders, trader.trader_id),
|
||||
}}
|
||||
>
|
||||
{t('behindBy', language, {
|
||||
gap: Math.abs(gap).toFixed(2),
|
||||
})}
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
)}
|
||||
{!hasValidData && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="text-lg md:text-2xl font-bold mono mb-1"
|
||||
style={{
|
||||
color:
|
||||
(trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
|
||||
}}
|
||||
>
|
||||
—
|
||||
{trader.total_pnl_pct != null &&
|
||||
!isNaN(trader.total_pnl_pct)
|
||||
? `${trader.total_pnl_pct >= 0 ? '+' : ''}${trader.total_pnl_pct.toFixed(2)}%`
|
||||
: '—'}
|
||||
</div>
|
||||
)}
|
||||
{hasValidData && isWinning && gap > 0 && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#0ECB81' }}
|
||||
>
|
||||
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
{hasValidData && !isWinning && gap < 0 && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#F6465D' }}
|
||||
>
|
||||
{t('behindBy', language, {
|
||||
gap: Math.abs(gap).toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!hasValidData && (
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
—
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Trader Config View Modal */}
|
||||
<TraderConfigViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
traderData={selectedTrader}
|
||||
/>
|
||||
</div>
|
||||
{/* Trader Config View Modal */}
|
||||
<TraderConfigViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
traderData={selectedTrader}
|
||||
/>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
41
web/src/components/DeepVoidBackground.tsx
Normal file
41
web/src/components/DeepVoidBackground.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface DeepVoidBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
disableAnimation?: boolean
|
||||
}
|
||||
|
||||
export function DeepVoidBackground({ children, className = '', disableAnimation = false, ...props }: DeepVoidBackgroundProps) {
|
||||
return (
|
||||
<div className={`relative w-full min-h-screen bg-nofx-bg text-nofx-text overflow-hidden flex flex-col ${className}`} {...props}>
|
||||
{/* BACKGROUND LAYERS */}
|
||||
|
||||
{/* 1. Grain/Noise Texture */}
|
||||
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-soft-light pointer-events-none fixed z-0"></div>
|
||||
|
||||
{/* 2. Grid System */}
|
||||
<div className="absolute inset-0 pointer-events-none fixed z-0">
|
||||
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-50" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(100px) scale(2)' }}></div>
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-[0.03]"></div>
|
||||
</div>
|
||||
|
||||
{/* 3. Ambient Glow Spots */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none fixed z-0">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-nofx-gold/10 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40vw] h-[40vw] bg-nofx-accent/5 rounded-full blur-[120px] mix-blend-screen animate-pulse-slow" style={{ animationDelay: '2s' }}></div>
|
||||
</div>
|
||||
|
||||
{/* 4. CRT/Scanline Overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none fixed z-[9999] opacity-40">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-[length:100%_4px,3px_100%] pointer-events-none"></div>
|
||||
</div>
|
||||
|
||||
{/* Content Layer */}
|
||||
<div className="relative z-10 flex-1 flex flex-col h-full w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -85,10 +85,7 @@ export default function HeaderBar({
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity cursor-pointer"
|
||||
>
|
||||
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-7 h-7" />
|
||||
<span
|
||||
className="text-lg font-bold"
|
||||
style={{ color: 'var(--brand-yellow)' }}
|
||||
>
|
||||
<span className="text-lg font-bold text-nofx-gold">
|
||||
NOFX
|
||||
</span>
|
||||
</div>
|
||||
@@ -128,28 +125,12 @@ export default function HeaderBar({
|
||||
<button
|
||||
key={tab.page}
|
||||
onClick={() => handleNavClick(tab)}
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === tab.page ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== tab.page) {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== tab.page) {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
className={`text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 px-3 py-2 rounded-lg
|
||||
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-nofx-gold'}`}
|
||||
>
|
||||
{currentPage === tab.page && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
||||
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||
/>
|
||||
)}
|
||||
{tab.label}
|
||||
@@ -167,16 +148,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||
@@ -188,16 +160,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#1DA1F2'
|
||||
e.currentTarget.style.background = 'rgba(29, 161, 242, 0.1)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#1DA1F2] hover:bg-[#1DA1F2]/10"
|
||||
title="Twitter"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -209,16 +172,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
||||
style={{ color: '#848E9C' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#0088cc'
|
||||
e.currentTarget.style.background = 'rgba(0, 136, 204, 0.1)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#0088cc] hover:bg-[#0088cc]/10"
|
||||
title="Telegram"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -237,62 +191,24 @@ export default function HeaderBar({
|
||||
<div className="relative" ref={userDropdownRef}>
|
||||
<button
|
||||
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.background =
|
||||
'rgba(255, 255, 255, 0.05)')
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.background = 'var(--panel-bg)')
|
||||
}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-white/5"
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{user.email}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className="w-4 h-4"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
/>
|
||||
<ChevronDown className="w-4 h-4 text-nofx-text-muted" />
|
||||
</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)' }}
|
||||
>
|
||||
<div className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50 bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<div className="px-3 py-2 border-b border-nofx-gold/20">
|
||||
<div className="text-xs text-nofx-text-muted">
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm font-medium"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<div className="text-sm font-medium text-nofx-text-muted">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,11 +218,7 @@ export default function HeaderBar({
|
||||
onLogout()
|
||||
setUserDropdownOpen(false)
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center bg-nofx-danger/20 text-nofx-danger"
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
@@ -322,19 +234,14 @@ export default function HeaderBar({
|
||||
<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)' }}
|
||||
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-white"
|
||||
>
|
||||
{t('signIn', language)}
|
||||
</a>
|
||||
{registrationEnabled && (
|
||||
<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)',
|
||||
}}
|
||||
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90 bg-nofx-gold text-black"
|
||||
>
|
||||
{t('signUp', language)}
|
||||
</a>
|
||||
@@ -347,15 +254,7 @@ export default function HeaderBar({
|
||||
<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')
|
||||
}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
||||
>
|
||||
<span className="text-lg">
|
||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
||||
@@ -364,28 +263,14 @@ export default function HeaderBar({
|
||||
</button>
|
||||
|
||||
{languageDropdownOpen && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
border: '1px solid var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50 bg-nofx-bg-lighter border border-nofx-gold/20">
|
||||
<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',
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||
>
|
||||
<span className="text-base">🇨🇳</span>
|
||||
<span className="text-sm">中文</span>
|
||||
@@ -395,16 +280,8 @@ export default function HeaderBar({
|
||||
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',
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||
>
|
||||
<span className="text-base">🇺🇸</span>
|
||||
<span className="text-sm">English</span>
|
||||
@@ -418,8 +295,7 @@ export default function HeaderBar({
|
||||
{/* Mobile Menu Button */}
|
||||
<motion.button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
className="md:hidden text-nofx-text-muted hover:text-white"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
@@ -439,11 +315,7 @@ export default function HeaderBar({
|
||||
: { height: 0, opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="md:hidden overflow-hidden"
|
||||
style={{
|
||||
background: 'var(--brand-dark-gray)',
|
||||
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
|
||||
}}
|
||||
className="md:hidden overflow-hidden bg-nofx-bg-lighter border-t border-nofx-gold/10"
|
||||
>
|
||||
<div className="px-4 py-4 space-y-2">
|
||||
{/* Mobile Navigation Tabs - Show all tabs */}
|
||||
@@ -476,25 +348,17 @@ export default function HeaderBar({
|
||||
<button
|
||||
key={tab.page}
|
||||
onClick={() => handleMobileNavClick(tab)}
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color: currentPage === tab.page ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
className={`block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 w-full text-left px-4 py-3 rounded-lg
|
||||
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-white hover:bg-white/5'}`}
|
||||
>
|
||||
{currentPage === tab.page && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
||||
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||
/>
|
||||
)}
|
||||
{tab.label}
|
||||
{tab.requiresAuth && !isLoggedIn && (
|
||||
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded" style={{ background: 'rgba(240, 185, 11, 0.2)', color: '#F0B90B' }}>
|
||||
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-nofx-gold/20 text-nofx-gold">
|
||||
{language === 'zh' ? '需登录' : 'Login'}
|
||||
</span>
|
||||
)}
|
||||
@@ -511,21 +375,19 @@ export default function HeaderBar({
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#${item.key === 'features' ? 'features' : 'how-it-works'}`}
|
||||
className="block text-sm py-2"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
className="block text-sm py-2 text-nofx-text-muted hover:text-white"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Social Links - Mobile */}
|
||||
<div className="py-3 flex items-center gap-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="py-3 flex items-center gap-3 border-t border-nofx-gold/20">
|
||||
<a
|
||||
href={OFFICIAL_LINKS.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-white"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
@@ -535,8 +397,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#1DA1F2]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
@@ -546,8 +407,7 @@ export default function HeaderBar({
|
||||
href={OFFICIAL_LINKS.telegram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg"
|
||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
||||
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#0088cc]"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
@@ -558,10 +418,7 @@ export default function HeaderBar({
|
||||
{/* 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)' }}
|
||||
>
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{t('language', language)}:
|
||||
</span>
|
||||
</div>
|
||||
@@ -571,11 +428,10 @@ export default function HeaderBar({
|
||||
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'
|
||||
}`}
|
||||
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>
|
||||
@@ -585,11 +441,10 @@ export default function HeaderBar({
|
||||
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'
|
||||
}`}
|
||||
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>
|
||||
@@ -600,33 +455,17 @@ export default function HeaderBar({
|
||||
{/* User info and logout for mobile when logged in */}
|
||||
{isLoggedIn && user && (
|
||||
<div
|
||||
className="mt-4 pt-4"
|
||||
style={{ borderTop: '1px solid var(--panel-border)' }}
|
||||
className="mt-4 pt-4 border-t border-nofx-gold/20"
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 mb-2 rounded"
|
||||
style={{ background: 'var(--panel-bg)' }}
|
||||
>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
background: 'var(--brand-yellow)',
|
||||
color: 'var(--brand-black)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-3 py-2 mb-2 rounded bg-nofx-bg-lighter">
|
||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||
{user.email[0].toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
<div className="text-xs text-nofx-text-muted">
|
||||
{t('loggedInAs', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm"
|
||||
style={{ color: 'var(--brand-light-gray)' }}
|
||||
>
|
||||
<div className="text-sm text-nofx-text-muted">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
@@ -637,11 +476,7 @@ export default function HeaderBar({
|
||||
onLogout()
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
|
||||
style={{
|
||||
background: 'var(--binance-red-bg)',
|
||||
color: 'var(--binance-red)',
|
||||
}}
|
||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center bg-nofx-danger/20 text-nofx-danger"
|
||||
>
|
||||
{t('exitLogin', language)}
|
||||
</button>
|
||||
@@ -656,11 +491,7 @@ export default function HeaderBar({
|
||||
<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)',
|
||||
}}
|
||||
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors text-nofx-text-muted border border-nofx-text-muted hover:text-white hover:border-white"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signIn', language)}
|
||||
@@ -668,11 +499,7 @@ export default function HeaderBar({
|
||||
{registrationEnabled && (
|
||||
<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)',
|
||||
}}
|
||||
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors bg-nofx-gold text-black hover:opacity-90"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{t('signUp', language)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { t } from '../i18n/translations'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
// import { Input } from './ui/input' // Removed unused import
|
||||
import { toast } from 'sonner'
|
||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||
@@ -102,13 +103,7 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
||||
{/* Background Effects */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
|
||||
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||
|
||||
<div className="w-full max-w-md relative z-10 px-6">
|
||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||
@@ -361,6 +356,6 @@ export function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
|
||||
interface LoginRequiredOverlayProps {
|
||||
@@ -51,111 +52,114 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 z-50"
|
||||
>
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
transition={{ type: 'spring', damping: 20, stiffness: 300 }}
|
||||
className="relative max-w-md w-full overflow-hidden bg-black border border-nofx-gold/30 shadow-[0_0_50px_rgba(240,185,11,0.1)] rounded-sm group font-mono"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<DeepVoidBackground
|
||||
className="w-full h-full bg-nofx-bg/95 backdrop-blur-md flex items-center justify-center p-4 text-nofx-text"
|
||||
disableAnimation
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* Terminal Window Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-zinc-900 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={12} className="text-nofx-gold" />
|
||||
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">auth_protocol.exe</span>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
transition={{ type: 'spring', damping: 20, stiffness: 300 }}
|
||||
className="relative max-w-md w-full overflow-hidden bg-nofx-bg border border-nofx-gold/30 shadow-neon rounded-sm group font-mono"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Terminal Window Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-nofx-bg-lighter border-b border-nofx-gold/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal size={12} className="text-nofx-gold" />
|
||||
<span className="text-[10px] text-nofx-text-muted uppercase tracking-wider">auth_protocol.exe</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-nofx-text-muted hover:text-nofx-danger transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-zinc-600 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="p-8 relative">
|
||||
{/* Background Grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:14px_14px] pointer-events-none"></div>
|
||||
{/* Main Content */}
|
||||
<div className="p-8 relative">
|
||||
{/* Background Grid */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:14px_14px] pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Flashing Access Denied */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||
<div className="bg-black border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||
<AlertTriangle size={18} className="animate-pulse" />
|
||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
||||
<div className="relative z-10">
|
||||
{/* Flashing Access Denied */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||
<div className="bg-nofx-bg border border-red-500/50 text-red-500 px-4 py-2 flex items-center gap-3 shadow-[0_0_15px_rgba(239,68,68,0.2)]">
|
||||
<AlertTriangle size={18} className="animate-pulse" />
|
||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal Text */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
|
||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
|
||||
{/* Terminal Text */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
|
||||
<p className="text-nofx-gold text-xs uppercase tracking-widest border-b border-nofx-gold/20 pb-4 inline-block">{t.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-nofx-bg-lighter border-l-2 border-nofx-gold/20 p-3 my-4">
|
||||
<p className="text-xs text-nofx-text-muted leading-relaxed font-mono">
|
||||
<span className="text-green-500 mr-2">$</span>
|
||||
{t.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{t.benefits.map((benefit, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[10px] text-nofx-text-muted uppercase tracking-wide">
|
||||
<span className="text-nofx-gold">✓</span> {benefit}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900/50 border-l-2 border-zinc-700 p-3 my-4">
|
||||
<p className="text-xs text-zinc-400 leading-relaxed font-mono">
|
||||
<span className="text-green-500 mr-2">$</span>
|
||||
{t.description}
|
||||
</p>
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||
>
|
||||
<LogIn size={14} />
|
||||
<span>{t.login}</span>
|
||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/register"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-nofx-gold/20 text-nofx-text-muted hover:text-white hover:border-nofx-gold font-bold text-xs uppercase tracking-widest transition-all hover:bg-nofx-gold/10"
|
||||
>
|
||||
<UserPlus size={14} />
|
||||
<span>{t.register}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{t.benefits.map((benefit, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-[10px] text-zinc-500 uppercase tracking-wide">
|
||||
<span className="text-nofx-gold">✓</span> {benefit}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||
>
|
||||
[ {t.later} ]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-nofx-gold text-black font-bold text-xs uppercase tracking-widest hover:bg-yellow-400 transition-all shadow-[0_0_15px_rgba(240,185,11,0.2)] hover:shadow-[0_0_25px_rgba(240,185,11,0.4)] group"
|
||||
>
|
||||
<LogIn size={14} />
|
||||
<span>{t.login}</span>
|
||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/register"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-transparent border border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-500 font-bold text-xs uppercase tracking-widest transition-all hover:bg-zinc-900"
|
||||
>
|
||||
<UserPlus size={14} />
|
||||
<span>{t.register}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-[10px] text-zinc-600 hover:text-red-500 uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||
>
|
||||
[ {t.later} ]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corner Accents */}
|
||||
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
|
||||
{/* Corner Accents */}
|
||||
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-nofx-gold"></div>
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</DeepVoidBackground>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getSystemConfig } from '../lib/config'
|
||||
import { toast } from 'sonner'
|
||||
import { copyWithToast } from '../lib/clipboard'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||
// import { Input } from './ui/input' // Removed unused import
|
||||
import PasswordChecklist from 'react-password-checklist'
|
||||
import { RegistrationDisabled } from './RegistrationDisabled'
|
||||
@@ -148,13 +149,7 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
||||
{/* Background Effects */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
|
||||
{/* Scanline Effect */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
|
||||
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||
|
||||
<div className="w-full max-w-lg relative z-10 px-6">
|
||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||
@@ -469,6 +464,6 @@ export function RegisterPage() {
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white font-mono relative overflow-hidden flex items-center justify-center px-4">
|
||||
<div className="min-h-screen bg-nofx-bg-deeper text-white font-mono relative overflow-hidden flex items-center justify-center px-4">
|
||||
{/* Background Grid & Scanlines */}
|
||||
<div className="fixed inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
|
||||
<div className="fixed inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
|
||||
@@ -56,14 +56,11 @@ export function FAQContent({
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-white/5">
|
||||
{/* Category Header */}
|
||||
<div
|
||||
className="flex items-center gap-3 mb-6 pb-3"
|
||||
style={{ borderBottom: '2px solid #2B3139' }}
|
||||
>
|
||||
<category.icon className="w-7 h-7" style={{ color: '#F0B90B' }} />
|
||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-white/10">
|
||||
<category.icon className="w-7 h-7 text-nofx-gold" />
|
||||
<h2 className="text-2xl font-bold text-nofx-text-main">
|
||||
{t(category.titleKey, language)}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -79,21 +76,12 @@ export function FAQContent({
|
||||
className="scroll-mt-24"
|
||||
>
|
||||
{/* Question */}
|
||||
<h3
|
||||
className="text-xl font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-3 text-nofx-text-main">
|
||||
{t(item.questionKey, language)}
|
||||
</h3>
|
||||
|
||||
{/* Answer */}
|
||||
<div
|
||||
className="prose prose-invert max-w-none"
|
||||
style={{
|
||||
color: '#B7BDC6',
|
||||
lineHeight: '1.7',
|
||||
}}
|
||||
>
|
||||
<div className="prose prose-invert max-w-none text-nofx-text-muted leading-relaxed">
|
||||
{item.id === 'github-projects-tasks' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
@@ -295,7 +283,7 @@ export function FAQContent({
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
CONTRIBUTING.md
|
||||
</a>
|
||||
@@ -304,7 +292,7 @@ export function FAQContent({
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="text-nofx-gold hover:underline"
|
||||
>
|
||||
PR_TITLE_GUIDE.md
|
||||
</a>
|
||||
@@ -383,16 +371,10 @@ export function FAQContent({
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div
|
||||
className="rounded p-3 mt-3"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.08)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.25)',
|
||||
}}
|
||||
>
|
||||
<div className="rounded p-3 mt-3 bg-nofx-gold/10 border border-nofx-gold/25">
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>提示:</strong>{' '}
|
||||
<strong className="text-nofx-gold">Note:</strong>{' '}
|
||||
我们为高质量贡献提供激励(Bounty/奖金、荣誉徽章与鸣谢、优先
|
||||
Review/合并与内测资格 等)。 详情可关注带
|
||||
<a
|
||||
@@ -448,7 +430,7 @@ export function FAQContent({
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mt-6 h-px" style={{ background: '#2B3139' }} />
|
||||
<div className="mt-6 h-px bg-white/5" />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import { Container } from '../Container'
|
||||
import { DeepVoidBackground } from '../DeepVoidBackground'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { FAQSearchBar } from './FAQSearchBar'
|
||||
import { FAQSidebar } from './FAQSidebar'
|
||||
@@ -58,125 +58,121 @@ export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-6 pt-24">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 8px 24px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<HelpCircle className="w-8 h-8" style={{ color: '#0B0E11' }} />
|
||||
<DeepVoidBackground className="py-6 pt-24" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="w-16 h-16 rounded-full flex items-center justify-center bg-gradient-to-br from-nofx-gold to-[#FCD535] shadow-[0_8px_24px_rgba(240,185,11,0.4)]">
|
||||
<HelpCircle className="w-8 h-8 text-[#0B0E11]" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4 text-nofx-text-main">
|
||||
{t('faqTitle', language)}
|
||||
</h1>
|
||||
<p className="text-lg mb-8 text-nofx-text-muted">
|
||||
{t('faqSubtitle', language)}
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<FAQSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
placeholder={
|
||||
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
{t('faqTitle', language)}
|
||||
</h1>
|
||||
<p className="text-lg mb-8" style={{ color: '#848E9C' }}>
|
||||
{t('faqSubtitle', language)}
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<FAQSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
placeholder={
|
||||
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex gap-8">
|
||||
{/* Sidebar - Hidden on mobile, visible on desktop */}
|
||||
<aside className="hidden lg:block w-64 flex-shrink-0">
|
||||
<FAQSidebar
|
||||
categories={filteredCategories}
|
||||
activeItemId={activeItemId}
|
||||
language={language}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 min-w-0">
|
||||
{filteredCategories.length > 0 ? (
|
||||
<FAQContent
|
||||
{/* Main Content */}
|
||||
<div className="flex gap-8">
|
||||
{/* Sidebar - Hidden on mobile, visible on desktop */}
|
||||
<aside className="hidden lg:block w-64 flex-shrink-0">
|
||||
<FAQSidebar
|
||||
categories={filteredCategories}
|
||||
activeItemId={activeItemId}
|
||||
language={language}
|
||||
onActiveItemChange={setActiveItemId}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-lg" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '没有找到匹配的问题'
|
||||
: 'No matching questions found'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '清除搜索' : 'Clear Search'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Contact Section */}
|
||||
<div
|
||||
className="mt-16 p-8 rounded-lg text-center"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
||||
{t('faqStillHaveQuestions', language)}
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: '#848E9C' }}>
|
||||
{t('faqContactUs', language)}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me/nofx_dev_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{t('community', language)}
|
||||
</a>
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 min-w-0">
|
||||
{filteredCategories.length > 0 ? (
|
||||
<FAQContent
|
||||
categories={filteredCategories}
|
||||
language={language}
|
||||
onActiveItemChange={setActiveItemId}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-lg" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '没有找到匹配的问题'
|
||||
: 'No matching questions found'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '清除搜索' : 'Clear Search'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<div
|
||||
className="mt-16 p-8 rounded-lg text-center"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
||||
{t('faqStillHaveQuestions', language)}
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: '#848E9C' }}>
|
||||
{t('faqContactUs', language)}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me/nofx_dev_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{t('community', language)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,36 +12,21 @@ export function FAQSearchBar({
|
||||
placeholder = 'Search FAQ...',
|
||||
}: FAQSearchBarProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative group">
|
||||
<Search
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-nofx-text-muted group-focus-within:text-nofx-gold transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none focus:ring-2"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#F0B90B'
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(240, 185, 11, 0.1)'
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#2B3139'
|
||||
e.target.style.boxShadow = 'none'
|
||||
}}
|
||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none bg-black/40 border border-white/10 text-nofx-text-main placeholder-nofx-text-muted/50 focus:border-nofx-gold/50 focus:ring-1 focus:ring-nofx-gold/20 hover:border-nofx-gold/30 font-mono"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 hover:opacity-70 transition-opacity"
|
||||
style={{ color: '#848E9C' }}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@@ -24,14 +24,11 @@ export function FAQSidebar({
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-white/5">
|
||||
{/* Category Title */}
|
||||
<div className="flex items-center gap-2 mb-3 px-3">
|
||||
<category.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3
|
||||
className="text-sm font-bold uppercase tracking-wide"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<category.icon className="w-5 h-5 text-nofx-gold" />
|
||||
<h3 className="text-sm font-bold uppercase tracking-wide text-nofx-gold">
|
||||
{t(category.titleKey, language)}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -44,30 +41,10 @@ export function FAQSidebar({
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => onItemClick(category.id, item.id)}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-sm transition-all"
|
||||
style={{
|
||||
background: isActive
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
color: isActive ? '#F0B90B' : '#848E9C',
|
||||
borderLeft: isActive
|
||||
? '3px solid #F0B90B'
|
||||
: '3px solid transparent',
|
||||
paddingLeft: isActive ? '9px' : '12px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background =
|
||||
'rgba(240, 185, 11, 0.05)'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all border-l-[3px] ${isActive
|
||||
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold pl-[9px]'
|
||||
: 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text-main'
|
||||
}`}
|
||||
>
|
||||
{t(item.questionKey, language)}
|
||||
</button>
|
||||
|
||||
@@ -143,12 +143,7 @@ export function CoinSourceEditor({
|
||||
// NofxOS badge component
|
||||
const NofxOSBadge = () => (
|
||||
<span
|
||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2))',
|
||||
color: '#a855f7',
|
||||
border: '1px solid rgba(139, 92, 246, 0.3)'
|
||||
}}
|
||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30"
|
||||
>
|
||||
NofxOS
|
||||
</span>
|
||||
@@ -158,7 +153,7 @@ export function CoinSourceEditor({
|
||||
<div className="space-y-6">
|
||||
{/* Source Type Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('sourceType')}
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
@@ -170,24 +165,16 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, source_type: value as CoinSourceConfig['source_type'] })
|
||||
}
|
||||
disabled={disabled}
|
||||
className={`p-4 rounded-lg border transition-all ${
|
||||
config.source_type === value
|
||||
? 'ring-2 ring-yellow-500'
|
||||
: 'hover:bg-white/5'
|
||||
}`}
|
||||
style={{
|
||||
background:
|
||||
config.source_type === value
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: '#0B0E11',
|
||||
borderColor: '#2B3139',
|
||||
}}
|
||||
className={`p-4 rounded-lg border transition-all ${config.source_type === value
|
||||
? 'ring-2 ring-nofx-gold bg-nofx-gold/10'
|
||||
: 'hover:bg-white/5 bg-nofx-bg'
|
||||
} border-nofx-gold/20`}
|
||||
>
|
||||
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
||||
<div className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<div className="text-sm font-medium text-nofx-text">
|
||||
{t(value)}
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
<div className="text-xs mt-1 text-nofx-text-muted">
|
||||
{t(`${value}Desc`)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -198,15 +185,14 @@ export function CoinSourceEditor({
|
||||
{/* Static Coins */}
|
||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3" style={{ color: '#EAECEF' }}>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('staticCoins')}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.static_coins || []).map((coin) => (
|
||||
<span
|
||||
key={coin}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
||||
style={{ background: '#2B3139', color: '#EAECEF' }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-bg-lighter text-nofx-text"
|
||||
>
|
||||
{coin}
|
||||
{!disabled && (
|
||||
@@ -228,17 +214,11 @@ export function CoinSourceEditor({
|
||||
onChange={(e) => setNewCoin(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddCoin()}
|
||||
placeholder="BTC, ETH, SOL..."
|
||||
className="flex-1 px-4 py-2 rounded-lg"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="flex-1 px-4 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddCoin}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('addCoin')}
|
||||
@@ -251,20 +231,19 @@ export function CoinSourceEditor({
|
||||
{/* Excluded Coins */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Ban className="w-4 h-4" style={{ color: '#F6465D' }} />
|
||||
<label className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<Ban className="w-4 h-4 text-nofx-danger" />
|
||||
<label className="text-sm font-medium text-nofx-text">
|
||||
{t('excludedCoins')}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs mb-3 text-nofx-text-muted">
|
||||
{t('excludedCoinsDesc')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(config.excluded_coins || []).map((coin) => (
|
||||
<span
|
||||
key={coin}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-danger/15 text-nofx-danger"
|
||||
>
|
||||
{coin}
|
||||
{!disabled && (
|
||||
@@ -278,7 +257,7 @@ export function CoinSourceEditor({
|
||||
</span>
|
||||
))}
|
||||
{(config.excluded_coins || []).length === 0 && (
|
||||
<span className="text-xs italic" style={{ color: '#5E6673' }}>
|
||||
<span className="text-xs italic text-nofx-text-muted">
|
||||
{language === 'zh' ? '无' : 'None'}
|
||||
</span>
|
||||
)}
|
||||
@@ -291,17 +270,11 @@ export function CoinSourceEditor({
|
||||
onChange={(e) => setNewExcludedCoin(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
|
||||
placeholder="BTC, ETH, DOGE..."
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="flex-1 px-4 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddExcludedCoin}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||||
style={{ background: '#F6465D', color: '#EAECEF' }}
|
||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm bg-nofx-danger text-white hover:bg-red-600"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
{t('addExcludedCoin')}
|
||||
@@ -313,16 +286,12 @@ export function CoinSourceEditor({
|
||||
{/* AI500 Options */}
|
||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.05)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<Zap className="w-4 h-4 text-nofx-gold" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
AI500 {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
@@ -338,14 +307,14 @@ export function CoinSourceEditor({
|
||||
!disabled && onChange({ ...config, use_ai500: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-yellow-500"
|
||||
className="w-5 h-5 rounded accent-nofx-gold"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useAI500')}</span>
|
||||
<span className="text-nofx-text">{t('useAI500')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_ai500 && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('ai500Limit')}:
|
||||
</span>
|
||||
<select
|
||||
@@ -355,12 +324,7 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
@@ -369,7 +333,7 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -379,16 +343,12 @@ export function CoinSourceEditor({
|
||||
{/* OI Top Options */}
|
||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(14, 203, 129, 0.05)',
|
||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
||||
}}
|
||||
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
OI Top {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
@@ -404,14 +364,14 @@ export function CoinSourceEditor({
|
||||
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-green-500"
|
||||
className="w-5 h-5 rounded accent-nofx-success"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useOITop')}</span>
|
||||
<span className="text-nofx-text">{t('useOITop')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_oi_top && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<span className="text-sm text-nofx-text-muted">
|
||||
{t('oiTopLimit')}:
|
||||
</span>
|
||||
<select
|
||||
@@ -421,12 +381,7 @@ export function CoinSourceEditor({
|
||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
@@ -435,7 +390,7 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -439,11 +439,15 @@ button:disabled {
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Premium Input Styles */
|
||||
/* Premium Input Styles */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--panel-bg);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
@@ -452,6 +456,35 @@ textarea:focus {
|
||||
border-color: var(--binance-yellow);
|
||||
box-shadow: 0 0 0 2px rgba(252, 213, 53, 0.15);
|
||||
outline: none;
|
||||
background-color: var(--panel-bg-hover);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Specific Premium Input Utility */
|
||||
.premium-input {
|
||||
background: rgba(11, 14, 17, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.premium-input:hover {
|
||||
border-color: rgba(240, 185, 11, 0.3);
|
||||
background: rgba(11, 14, 17, 0.8);
|
||||
}
|
||||
|
||||
.premium-input:focus {
|
||||
border-color: var(--binance-yellow);
|
||||
box-shadow: 0 0 0 1px rgba(240, 185, 11, 0.2), 0 0 15px rgba(240, 185, 11, 0.1);
|
||||
background: rgba(11, 14, 17, 0.9);
|
||||
}
|
||||
|
||||
/* Binance Card - Premium Polish */
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react'
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
|
||||
// Translations
|
||||
const T: Record<string, Record<string, string>> = {
|
||||
@@ -144,7 +145,7 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-3 rounded-lg hover:bg-white/5 transition-all border border-white/5"
|
||||
className="p-3 rounded-lg hover:bg-nofx-bg-lighter/60 transition-all border border-nofx-gold/20 backdrop-blur-sm bg-nofx-bg-lighter/20"
|
||||
style={{ borderLeft: `3px solid ${p.color}` }}
|
||||
>
|
||||
{/* Header - Always visible */}
|
||||
@@ -153,16 +154,16 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<AIAvatar name={msg.ai_model_name} size={24} />
|
||||
<span className="text-sm text-white font-medium">{msg.ai_model_name}</span>
|
||||
<span className="text-xs text-gray-500">{p.nameEn}</span>
|
||||
<span className="text-sm text-nofx-text font-medium">{msg.ai_model_name}</span>
|
||||
<span className="text-xs text-nofx-text-muted">{p.nameEn}</span>
|
||||
<div className="flex-1" />
|
||||
{msg.decision && (
|
||||
<span className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded ${a.bg} ${a.color}`}>
|
||||
{a.icon} {msg.decision.symbol || ''} {a.label}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-yellow-400 font-medium">{msg.decision?.confidence || msg.confidence}%</span>
|
||||
{open ? <ChevronUp size={14} className="text-gray-500" /> : <ChevronDown size={14} className="text-gray-500" />}
|
||||
<span className="text-xs text-nofx-gold font-medium">{msg.decision?.confidence || msg.confidence}%</span>
|
||||
{open ? <ChevronUp size={14} className="text-nofx-text-muted" /> : <ChevronDown size={14} className="text-nofx-text-muted" />}
|
||||
</div>
|
||||
|
||||
{/* Preview when collapsed */}
|
||||
@@ -277,13 +278,13 @@ function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; sym
|
||||
const a = ACT[vote.action] || ACT.wait
|
||||
const confColor = vote.confidence >= 70 ? 'bg-green-500' : vote.confidence >= 50 ? 'bg-yellow-500' : 'bg-gray-500'
|
||||
return (
|
||||
<div className="bg-[#1a1f2e] rounded-xl p-4 border border-white/10 hover:border-white/20 transition-all">
|
||||
<div className="bg-nofx-bg-lighter/40 backdrop-blur-md rounded-xl p-4 border border-nofx-gold/20 hover:border-nofx-gold/50 transition-all shadow-lg hover:shadow-[0_0_20px_rgba(240,185,11,0.1)]">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AIAvatar name={vote.ai_model_name} size={28} />
|
||||
<div>
|
||||
<span className="text-white font-semibold block">{vote.ai_model_name}</span>
|
||||
{vote.symbol && <span className="text-xs text-gray-400">{vote.symbol}</span>}
|
||||
<span className="text-nofx-text font-semibold block">{vote.ai_model_name}</span>
|
||||
{vote.symbol && <span className="text-xs text-nofx-text-muted">{vote.symbol}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-bold ${a.bg} ${a.color}`}>
|
||||
@@ -300,13 +301,13 @@ function VoteCard({ vote }: { vote: { ai_model_name: string; action: string; sym
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
||||
<div className="flex justify-between"><span className="text-gray-500">Leverage</span><span className="text-white font-semibold">{vote.leverage || '-'}x</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500">Position</span><span className="text-white font-semibold">{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500">SL</span><span className="text-red-400 font-semibold">{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-nofx-text-muted">Leverage</span><span className="text-nofx-text font-semibold">{vote.leverage || '-'}x</span></div>
|
||||
<div className="flex justify-between"><span className="text-nofx-text-muted">Position</span><span className="text-nofx-text font-semibold">{vote.position_pct ? `${(vote.position_pct * 100).toFixed(0)}%` : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-nofx-text-muted">SL</span><span className="text-red-400 font-semibold">{vote.stop_loss_pct ? `${(vote.stop_loss_pct * 100).toFixed(1)}%` : '-'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-nofx-text-muted">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
|
||||
</div>
|
||||
{vote.reasoning && (
|
||||
<p className="mt-3 text-xs text-gray-400 leading-relaxed line-clamp-2 border-t border-white/5 pt-2">{vote.reasoning}</p>
|
||||
<p className="mt-3 text-xs text-nofx-text-muted leading-relaxed line-clamp-2 border-t border-nofx-gold/10 pt-2">{vote.reasoning}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -386,22 +387,22 @@ function CreateModal({
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||
<div className="bg-[#1a1d24] rounded-xl w-full max-w-md p-4 border border-white/10">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-md p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-bold text-white">{t('createDebate', language)}</h3>
|
||||
<button onClick={onClose}><X size={20} className="text-gray-400" /></button>
|
||||
<h3 className="text-lg font-bold text-nofx-text">{t('createDebate', language)}</h3>
|
||||
<button onClick={onClose}><X size={20} className="text-nofx-text-muted" /></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
value={name} onChange={e => setName(e.target.value)}
|
||||
placeholder={t('debateName', language)} className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm"
|
||||
placeholder={t('debateName', language)} className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold"
|
||||
/>
|
||||
|
||||
{/* Strategy selector - moved up */}
|
||||
<select value={strategyId} onChange={e => setStrategyId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
|
||||
className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold">
|
||||
{strategies.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
|
||||
@@ -409,17 +410,17 @@ function CreateModal({
|
||||
{/* Show dropdown only for static type with coins defined */}
|
||||
{isStaticWithCoins ? (
|
||||
<select value={symbol} onChange={e => setSymbol(e.target.value)}
|
||||
className="flex-1 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
|
||||
className="flex-1 px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold">
|
||||
{staticCoins.map(coin => <option key={coin} value={coin}>{coin}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<div className="flex-1 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-gray-400 text-sm">
|
||||
<div className="flex-1 px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text-muted text-sm">
|
||||
{language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'}
|
||||
</div>
|
||||
)}
|
||||
<select value={maxRounds} onChange={e => setMaxRounds(+e.target.value)}
|
||||
className="px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm">
|
||||
{[2,3,4,5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
|
||||
className="px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm outline-none focus:border-nofx-gold">
|
||||
{[2, 3, 4, 5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -431,7 +432,7 @@ function CreateModal({
|
||||
{/* Personality selector */}
|
||||
<select value={p.personality} onChange={e => {
|
||||
const up = [...participants]; up[i].personality = e.target.value as DebatePersonality; setParticipants(up)
|
||||
}} className="bg-transparent text-white text-xs border-0 outline-none cursor-pointer">
|
||||
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none cursor-pointer">
|
||||
{Object.entries(PERS).map(([k, v]) => (
|
||||
<option key={k} value={k}>{v.emoji} {language === 'zh' ? v.name : v.nameEn}</option>
|
||||
))}
|
||||
@@ -439,23 +440,23 @@ function CreateModal({
|
||||
{/* AI model selector */}
|
||||
<select value={p.ai_model_id} onChange={e => {
|
||||
const up = [...participants]; up[i].ai_model_id = e.target.value; setParticipants(up)
|
||||
}} className="bg-transparent text-white text-xs border-0 outline-none">
|
||||
}} className="bg-transparent text-nofx-text text-xs border-0 outline-none">
|
||||
{aiModels.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||
</select>
|
||||
<button onClick={() => setParticipants(participants.filter((_, j) => j !== i))}
|
||||
className="text-red-400 hover:text-red-300"><X size={12} /></button>
|
||||
className="text-nofx-danger hover:text-red-300"><X size={12} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addP} className="px-2 py-1 text-xs text-yellow-400 hover:bg-yellow-500/10 rounded">
|
||||
<button onClick={addP} className="px-2 py-1 text-xs text-nofx-gold hover:bg-nofx-gold/10 rounded">
|
||||
+ {t('addAI', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button onClick={onClose} className="flex-1 py-2 rounded-lg bg-white/5 text-white text-sm">{t('cancel', language)}</button>
|
||||
<button onClick={onClose} className="flex-1 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm hover:bg-nofx-bg-lighter transition-colors">{t('cancel', language)}</button>
|
||||
<button onClick={submit} disabled={creating}
|
||||
className="flex-1 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm disabled:opacity-50">
|
||||
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
|
||||
{creating ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('create', language)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -536,26 +537,27 @@ export function DebateArenaPage() {
|
||||
const voteSum = votes.reduce((a, v) => { a[v.action] = (a[v.action] || 0) + 1; return a }, {} as Record<string, number>)
|
||||
|
||||
return (
|
||||
<div className="h-full bg-[#0a0c10] flex overflow-hidden">
|
||||
<DeepVoidBackground className="h-full flex overflow-hidden relative" disableAnimation>
|
||||
|
||||
{/* Left - Debate List + Online Traders */}
|
||||
<div className="w-56 flex-shrink-0 bg-[#0d1017] border-r border-white/5 flex flex-col">
|
||||
<div className="w-56 flex-shrink-0 bg-nofx-bg/80 backdrop-blur-md border-r border-nofx-gold/20 flex flex-col z-10">
|
||||
{/* New Debate Button */}
|
||||
<button onClick={() => setShowCreate(true)}
|
||||
className="m-2 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm flex items-center justify-center gap-1">
|
||||
className="m-2 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center justify-center gap-1 hover:bg-yellow-500 transition-colors">
|
||||
<Plus size={16} /> {t('newDebate', language)}
|
||||
</button>
|
||||
|
||||
{/* Debate List */}
|
||||
<div className="px-2 py-1 text-xs text-gray-500 font-semibold">{t('debateSessions', language)}</div>
|
||||
<div className="px-2 py-1 text-xs text-nofx-text-muted font-semibold">{t('debateSessions', language)}</div>
|
||||
<div className="overflow-y-auto" style={{ maxHeight: '30%' }}>
|
||||
{debates?.map(d => (
|
||||
<div key={d.id} onClick={() => setSelectedId(d.id)}
|
||||
className={`p-2 cursor-pointer border-l-2 ${selectedId === d.id ? 'bg-yellow-500/10 border-yellow-500' : 'border-transparent hover:bg-white/5'}`}>
|
||||
className={`p-2 cursor-pointer border-l-2 transition-all ${selectedId === d.id ? 'bg-nofx-gold/10 border-nofx-gold shadow-[inset_10px_0_20px_-10px_rgba(240,185,11,0.2)]' : 'border-transparent hover:bg-nofx-bg-lighter/50'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${STATUS_COLOR[d.status]}`} />
|
||||
<span className="text-sm text-white truncate flex-1">{d.name}</span>
|
||||
<span className="text-sm text-nofx-text truncate flex-1">{d.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{d.symbol} · R{d.current_round}/{d.max_rounds}</div>
|
||||
<div className="text-xs text-nofx-text-muted mt-1">{d.symbol} · R{d.current_round}/{d.max_rounds}</div>
|
||||
{d.status === 'pending' && selectedId === d.id && (
|
||||
<div className="flex gap-1 mt-1">
|
||||
<button onClick={e => { e.stopPropagation(); onStart(d.id) }}
|
||||
@@ -569,41 +571,41 @@ export function DebateArenaPage() {
|
||||
</div>
|
||||
|
||||
{/* Online Traders Section */}
|
||||
<div className="flex-1 border-t border-white/5 mt-2 overflow-hidden flex flex-col">
|
||||
<div className="px-2 py-2 text-xs text-gray-500 font-semibold flex items-center gap-1">
|
||||
<Zap size={12} className="text-green-400" />
|
||||
<div className="flex-1 border-t border-nofx-gold/20 mt-2 overflow-hidden flex flex-col">
|
||||
<div className="px-2 py-2 text-xs text-nofx-text-muted font-semibold flex items-center gap-1">
|
||||
<Zap size={12} className="text-nofx-success" />
|
||||
{t('onlineTraders', language)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-2 space-y-2">
|
||||
{traders?.filter(tr => tr.is_running).map(tr => (
|
||||
<div key={tr.trader_id}
|
||||
onClick={() => { setTraderId(tr.trader_id); if (decision && !decision.executed) setExecId(detail?.id || null) }}
|
||||
className={`p-2 rounded-lg cursor-pointer transition-all ${traderId === tr.trader_id ? 'bg-green-500/20 ring-1 ring-green-500' : 'bg-white/5 hover:bg-white/10'}`}>
|
||||
className={`p-2 rounded-lg cursor-pointer transition-all ${traderId === tr.trader_id ? 'bg-nofx-success/20 ring-1 ring-nofx-success' : 'bg-nofx-bg-lighter hover:bg-nofx-bg-light'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
|
||||
<div className="text-xs text-gray-500 truncate">{tr.ai_model}</div>
|
||||
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
|
||||
<div className="text-xs text-nofx-text-muted truncate">{tr.ai_model}</div>
|
||||
</div>
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span className="w-2 h-2 rounded-full bg-nofx-success animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => (
|
||||
<div key={tr.trader_id} className="p-2 rounded-lg bg-white/5 opacity-50">
|
||||
<div key={tr.trader_id} className="p-2 rounded-lg bg-nofx-bg-lighter opacity-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grayscale">
|
||||
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
|
||||
<div className="text-xs text-gray-500">{t('offline', language)}</div>
|
||||
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
|
||||
<div className="text-xs text-nofx-text-muted">{t('offline', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(!traders || traders.length === 0) && (
|
||||
<div className="text-xs text-gray-500 text-center py-4">{t('noTraders', language)}</div>
|
||||
<div className="text-xs text-nofx-text-muted text-center py-4">{t('noTraders', language)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -614,12 +616,12 @@ export function DebateArenaPage() {
|
||||
{detail ? (
|
||||
<>
|
||||
{/* Header Bar - Compact */}
|
||||
<div className="px-3 py-2 border-b border-white/5 bg-[#0d1017]/50 flex items-center gap-3 flex-shrink-0">
|
||||
<div className="px-3 py-2 border-b border-nofx-gold/20 bg-nofx-bg/60 backdrop-blur-md flex items-center gap-3 flex-shrink-0 shadow-sm">
|
||||
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${STATUS_COLOR[detail.status]}`} />
|
||||
<span className="font-bold text-white truncate">{detail.name}</span>
|
||||
<span className="text-yellow-400 font-semibold">{detail.symbol}</span>
|
||||
<span className="font-bold text-nofx-text truncate">{detail.name}</span>
|
||||
<span className="text-nofx-gold font-semibold">{detail.symbol}</span>
|
||||
{strategyName && <span className="text-xs px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded">{strategyName}</span>}
|
||||
<span className="text-xs text-gray-500">R{detail.current_round}/{detail.max_rounds}</span>
|
||||
<span className="text-xs text-nofx-text-muted">R{detail.current_round}/{detail.max_rounds}</span>
|
||||
|
||||
{/* Participants */}
|
||||
<div className="flex gap-1 ml-2">
|
||||
@@ -627,7 +629,7 @@ export function DebateArenaPage() {
|
||||
const vote = votes.find(v => v.ai_model_id === p.ai_model_id)
|
||||
const act = vote ? (ACT[vote.action] || ACT.wait) : null
|
||||
return (
|
||||
<div key={p.id} className="flex items-center gap-1 px-1 py-0.5 rounded bg-white/5 text-xs">
|
||||
<div key={p.id} className="flex items-center gap-1 px-1 py-0.5 rounded bg-nofx-bg-lighter text-xs">
|
||||
<AIAvatar name={p.ai_model_name} size={14} />
|
||||
{act && <span className={`${act.color}`}>{act.icon}</span>}
|
||||
</div>
|
||||
@@ -662,8 +664,8 @@ export function DebateArenaPage() {
|
||||
) : (
|
||||
<>
|
||||
{/* Left - Rounds */}
|
||||
<div className="flex-1 overflow-y-auto p-4 border-r border-white/5">
|
||||
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
|
||||
<div className="flex-1 overflow-y-auto p-4 border-r border-nofx-gold/20">
|
||||
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
|
||||
{t('discussionRecords', language)}
|
||||
</div>
|
||||
@@ -681,9 +683,9 @@ export function DebateArenaPage() {
|
||||
|
||||
{/* Right - Votes */}
|
||||
{votes.length > 0 && (
|
||||
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-[#0d1017]/50">
|
||||
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
|
||||
<Trophy size={16} className="text-yellow-400" />
|
||||
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-nofx-bg/30 backdrop-blur-sm">
|
||||
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
|
||||
<Trophy size={16} className="text-nofx-gold" />
|
||||
{t('finalVotes', language)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
@@ -709,37 +711,37 @@ export function DebateArenaPage() {
|
||||
|
||||
{/* Consensus Bar - Show when votes exist */}
|
||||
{(decision || votes.length > 0) && (
|
||||
<div className="p-3 border-t border-white/5 bg-gradient-to-r from-yellow-500/10 to-orange-500/10 flex items-center gap-4 flex-shrink-0">
|
||||
<div className="p-3 border-t border-nofx-gold/20 bg-gradient-to-r from-nofx-gold/10 via-nofx-bg-lighter/50 to-orange-500/10 backdrop-blur-md flex items-center gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy size={20} className="text-yellow-400" />
|
||||
<span className="text-sm text-gray-400">{t('consensus', language)}:</span>
|
||||
<Trophy size={20} className="text-nofx-gold" />
|
||||
<span className="text-sm text-nofx-text-muted">{t('consensus', language)}:</span>
|
||||
{decision ? (
|
||||
<>
|
||||
{decision.symbol && <span className="text-yellow-400 font-bold mr-1">{decision.symbol}</span>}
|
||||
{decision.symbol && <span className="text-nofx-gold font-bold mr-1">{decision.symbol}</span>}
|
||||
<span className={`flex items-center gap-1 px-2 py-1 rounded font-bold ${(ACT[decision.action] || ACT.wait).bg} ${(ACT[decision.action] || ACT.wait).color}`}>
|
||||
{(ACT[decision.action] || ACT.wait).icon}
|
||||
{decision.action.replace('_', ' ').toUpperCase()}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 px-2 py-1 rounded font-bold bg-gray-500/20 text-gray-400">
|
||||
<span className="flex items-center gap-1 px-2 py-1 rounded font-bold bg-nofx-text-muted/20 text-nofx-text-muted">
|
||||
<Clock size={14} /> VOTING...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{decision && (
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span><span className="text-gray-500">{t('confidence', language)}</span> <span className="text-yellow-400 font-bold">{decision.confidence || 0}%</span></span>
|
||||
{(decision.leverage ?? 0) > 0 && <span><span className="text-gray-500">{t('leverage', language)}</span> <span className="text-white font-bold">{decision.leverage}x</span></span>}
|
||||
{(decision.position_pct ?? 0) > 0 && <span><span className="text-gray-500">{t('position', language)}</span> <span className="text-white font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</span></span>}
|
||||
{(decision.stop_loss ?? 0) > 0 && <span><span className="text-gray-500">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</span></span>}
|
||||
{(decision.take_profit ?? 0) > 0 && <span><span className="text-gray-500">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 0) * 100).toFixed(1)}%</span></span>}
|
||||
<span><span className="text-nofx-text-muted">{t('confidence', language)}</span> <span className="text-nofx-gold font-bold">{decision.confidence || 0}%</span></span>
|
||||
{(decision.leverage ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('leverage', language)}</span> <span className="text-nofx-text font-bold">{decision.leverage}x</span></span>}
|
||||
{(decision.position_pct ?? 0) > 0 && <span><span className="text-nofx-text-muted">{t('position', language)}</span> <span className="text-nofx-text font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</span></span>}
|
||||
{(decision.stop_loss ?? 0) > 0 && <span><span className="text-nofx-text-muted">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</span></span>}
|
||||
{(decision.take_profit ?? 0) > 0 && <span><span className="text-nofx-text-muted">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 0) * 100).toFixed(1)}%</span></span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && (
|
||||
<button onClick={() => setExecId(detail.id)}
|
||||
className="px-4 py-1.5 rounded-lg bg-yellow-500 text-black font-semibold text-sm flex items-center gap-1">
|
||||
className="px-4 py-1.5 rounded-lg bg-nofx-gold text-black font-semibold text-sm flex items-center gap-1 hover:bg-yellow-500 transition-colors">
|
||||
<Zap size={14} /> {t('execute', language)}
|
||||
</button>
|
||||
)}
|
||||
@@ -748,7 +750,7 @@ export function DebateArenaPage() {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-gray-500">
|
||||
<div className="flex-1 flex items-center justify-center text-nofx-text-muted">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">🗳️</div>
|
||||
<div>{t('selectOrCreate', language)}</div>
|
||||
@@ -763,13 +765,13 @@ export function DebateArenaPage() {
|
||||
|
||||
{/* Execute Modal */}
|
||||
{execId && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||
<div className="bg-[#1a1d24] rounded-xl w-full max-w-sm p-4 border border-white/10">
|
||||
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Zap className="text-yellow-400" /> {t('executeTitle', language)}
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div className="bg-nofx-bg-lighter/90 backdrop-blur-xl rounded-xl w-full max-w-sm p-6 border border-nofx-gold/30 shadow-2xl shadow-nofx-gold/10">
|
||||
<h3 className="text-lg font-bold text-nofx-text mb-4 flex items-center gap-2">
|
||||
<Zap className="text-nofx-gold" /> {t('executeTitle', language)}
|
||||
</h3>
|
||||
<select value={traderId} onChange={e => setTraderId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white text-sm mb-3">
|
||||
className="w-full px-3 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text text-sm mb-3">
|
||||
<option value="">{t('selectTrader', language)}...</option>
|
||||
{traders?.filter(tr => tr.is_running).map(tr => (
|
||||
<option key={tr.trader_id} value={tr.trader_id}>✅ {tr.trader_name}</option>
|
||||
@@ -778,20 +780,20 @@ export function DebateArenaPage() {
|
||||
<option key={tr.trader_id} value={tr.trader_id} disabled>⏹ {tr.trader_name} ({t('offline', language)})</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="text-xs text-yellow-300 bg-yellow-500/10 p-2 rounded mb-3">
|
||||
<div className="text-xs text-yellow-300 bg-nofx-gold/10 p-2 rounded mb-3">
|
||||
⚠️ {language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => { setExecId(null); setTraderId('') }}
|
||||
className="flex-1 py-2 rounded-lg bg-white/5 text-white text-sm">{t('cancel', language)}</button>
|
||||
className="flex-1 py-2 rounded-lg bg-nofx-bg text-nofx-text text-sm hover:bg-nofx-bg-light transition-colors">{t('cancel', language)}</button>
|
||||
<button onClick={onExecute} disabled={!traderId || executing || !traders?.find(tr => tr.trader_id === traderId)?.is_running}
|
||||
className="flex-1 py-2 rounded-lg bg-yellow-500 text-black font-semibold text-sm disabled:opacity-50">
|
||||
className="flex-1 py-2 rounded-lg bg-nofx-gold text-black font-semibold text-sm disabled:opacity-50 hover:bg-yellow-500 transition-colors">
|
||||
{executing ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('execute', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
43
web/src/pages/PageNotFound.tsx
Normal file
43
web/src/pages/PageNotFound.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
import { AlertCircle, Home } from 'lucide-react'
|
||||
|
||||
export function PageNotFound() {
|
||||
return (
|
||||
<DeepVoidBackground className="flex items-center justify-center text-center p-4">
|
||||
<div className="bg-nofx-bg border border-nofx-gold/20 p-8 rounded-lg max-w-md w-full relative overflow-hidden group">
|
||||
|
||||
{/* Background Grid inside Card */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:16px_16px] pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
||||
<AlertCircle size={64} className="text-nofx-danger relative z-10" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-4xl font-bold font-mono tracking-tighter text-white">
|
||||
404
|
||||
</h1>
|
||||
<div className="text-xs uppercase tracking-[0.3em] text-nofx-danger font-mono border-b border-nofx-danger/30 pb-2 inline-block">
|
||||
SIGNAL_LOST
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-nofx-text-muted font-mono leading-relaxed">
|
||||
The requested coordinates do not exist in the current sector. The page may have been moved, deleted, or never existed in this timeline.
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-6 py-3 bg-nofx-gold text-black font-bold text-sm uppercase tracking-widest rounded hover:bg-yellow-400 transition-all shadow-neon hover:shadow-[0_0_20px_rgba(240,185,11,0.4)] group mt-4"
|
||||
>
|
||||
<Home size={16} />
|
||||
<span>RETURN_BASE</span>
|
||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity -ml-2 group-hover:ml-0">-></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { toast } from 'sonner' // Ensure sonner is installed or stick to custom toast if preferred
|
||||
import { toast } from 'sonner'
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
|
||||
interface PublicStrategy {
|
||||
id: string
|
||||
@@ -225,291 +226,289 @@ export function StrategyMarketPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white font-mono relative overflow-hidden flex flex-col items-center py-12">
|
||||
{/* Background Grid & Scanlines */}
|
||||
<div className="fixed inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none"></div>
|
||||
<div className="fixed inset-0 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||
<div className="fixed inset-0 pointer-events-none opacity-[0.03] bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.5)_50%)] bg-[length:100%_4px]"></div>
|
||||
<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 max-w-7xl relative z-10 px-6">
|
||||
<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>
|
||||
<br />
|
||||
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
|
||||
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<Database className="w-8 h-8 text-nofx-gold 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>
|
||||
<br />
|
||||
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
|
||||
{t.title}
|
||||
</h1>
|
||||
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
||||
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
|
||||
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<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={t.title}>
|
||||
{t.title}
|
||||
</h1>
|
||||
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
||||
// {t.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
||||
{t.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter Bar */}
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 group">
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-zinc-800/20 rounded opacity-0 group-hover:opacity-100 transition duration-500 blur"></div>
|
||||
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
|
||||
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
|
||||
<Terminal size={16} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
||||
/>
|
||||
<div className="pr-4">
|
||||
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
||||
{t.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
|
||||
{['all', 'popular', 'recent'].map((cat) => (
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
{selectedCategory === cat && (
|
||||
<motion.div
|
||||
layoutId="filter-highlight"
|
||||
className="absolute inset-0 bg-nofx-gold"
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-32 space-y-4">
|
||||
<div className="relative w-16 h-16">
|
||||
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||
<div className="absolute inset-0 border-2 border-nofx-gold rounded-full border-t-transparent animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Cpu size={24} className="text-nofx-gold/50" />
|
||||
{/* Search and Filter Bar */}
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 group">
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-nofx-gold/20 to-zinc-800/20 rounded opacity-0 group-hover:opacity-100 transition duration-500 blur"></div>
|
||||
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
|
||||
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
|
||||
<Terminal size={16} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
||||
/>
|
||||
<div className="pr-4">
|
||||
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.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>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
|
||||
{['all', 'popular', 'recent'].map((cat) => (
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
{selectedCategory === cat && (
|
||||
<motion.div
|
||||
layoutId="filter-highlight"
|
||||
className="absolute inset-0 bg-nofx-gold"
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && filteredStrategies.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
|
||||
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
||||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-32 space-y-4">
|
||||
<div className="relative w-16 h-16">
|
||||
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||
<div className="absolute inset-0 border-2 border-nofx-gold rounded-full border-t-transparent animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Cpu size={24} className="text-nofx-gold/50" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.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>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
||||
[{t.noStrategies}]
|
||||
</h3>
|
||||
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Strategy Grid */}
|
||||
{!isLoading && filteredStrategies.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<AnimatePresence>
|
||||
{filteredStrategies.map((strategy, i) => {
|
||||
const style = getStrategyStyle(strategy.name)
|
||||
const Icon = style.icon
|
||||
const indicators = strategy.config_visible && strategy.config
|
||||
? getIndicatorList(strategy.config)
|
||||
: []
|
||||
{/* Empty State */}
|
||||
{!isLoading && filteredStrategies.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
|
||||
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
||||
[{t.noStrategies}]
|
||||
</h3>
|
||||
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={strategy.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
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>
|
||||
{/* Strategy Grid */}
|
||||
{!isLoading && filteredStrategies.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<AnimatePresence>
|
||||
{filteredStrategies.map((strategy, i) => {
|
||||
const style = getStrategyStyle(strategy.name)
|
||||
const Icon = style.icon
|
||||
const indicators = strategy.config_visible && strategy.config
|
||||
? getIndicatorList(strategy.config)
|
||||
: []
|
||||
|
||||
{/* Category Side Strip */}
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
|
||||
return (
|
||||
<motion.div
|
||||
key={strategy.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
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="p-6 relative">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<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">
|
||||
{strategy.config_visible ? (
|
||||
<div className="flex items-center gap-1.5 text-emerald-500 border border-emerald-500/20 bg-emerald-500/10 px-2 py-1">
|
||||
<Eye size={10} />
|
||||
PUBLIC_ACCESS
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
|
||||
<EyeOff size={10} />
|
||||
RESTRICTED
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Category Side Strip */}
|
||||
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
|
||||
|
||||
{/* Name and Description */}
|
||||
<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>
|
||||
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
|
||||
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
|
||||
</p>
|
||||
|
||||
{/* 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">{t.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">{t.createdAt}</span>
|
||||
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Config / Indicators */}
|
||||
<div className="bg-zinc-900/30 border border-zinc-800/50 p-3 mb-4 backdrop-blur-sm min-h-[90px]">
|
||||
{strategy.config_visible && strategy.config ? (
|
||||
<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}
|
||||
</span>
|
||||
)) : <span className="text-[9px] text-zinc-600">NO_INDICATORS</span>}
|
||||
</div>
|
||||
|
||||
{/* Risk Control */}
|
||||
{strategy.config.risk_control && (
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<Activity size={12} className="text-zinc-700" />
|
||||
<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}`}>
|
||||
<Icon className={`w-5 h-5 ${style.color}`} />
|
||||
</div>
|
||||
<div className="text-[10px] font-mono">
|
||||
{strategy.config_visible ? (
|
||||
<div className="flex items-center gap-1.5 text-emerald-500 border border-emerald-500/20 bg-emerald-500/10 px-2 py-1">
|
||||
<Eye size={10} />
|
||||
PUBLIC_ACCESS
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
|
||||
<EyeOff size={10} />
|
||||
RESTRICTED
|
||||
</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">{t.configHiddenDesc}</span>
|
||||
</div>
|
||||
|
||||
{/* Name and Description */}
|
||||
<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>
|
||||
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
|
||||
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
|
||||
</p>
|
||||
|
||||
{/* 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">{t.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">{t.createdAt}</span>
|
||||
<span className="text-zinc-400">{formatDate(strategy.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Config / Indicators */}
|
||||
<div className="bg-zinc-900/30 border border-zinc-800/50 p-3 mb-4 backdrop-blur-sm min-h-[90px]">
|
||||
{strategy.config_visible && strategy.config ? (
|
||||
<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}
|
||||
</span>
|
||||
)) : <span className="text-[9px] text-zinc-600">NO_INDICATORS</span>}
|
||||
</div>
|
||||
|
||||
{/* Risk Control */}
|
||||
{strategy.config.risk_control && (
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<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">{t.configHiddenDesc}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div>
|
||||
{strategy.config_visible && strategy.config ? (
|
||||
<button
|
||||
onClick={() => handleCopyConfig(strategy)}
|
||||
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-700 bg-black hover:bg-zinc-900 text-zinc-300 hover:text-nofx-gold hover:border-nofx-gold transition-all flex items-center justify-center gap-2 group/btn"
|
||||
>
|
||||
{copiedId === strategy.id ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 text-emerald-500" />
|
||||
<span className="text-emerald-500">{t.copied}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
||||
{t.copyConfig}
|
||||
</>
|
||||
)}
|
||||
</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">
|
||||
<Shield size={12} />
|
||||
{t.hideConfig}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
<div>
|
||||
{strategy.config_visible && strategy.config ? (
|
||||
<button
|
||||
onClick={() => handleCopyConfig(strategy)}
|
||||
className="w-full py-2.5 text-[10px] font-bold font-mono uppercase tracking-widest border border-zinc-700 bg-black hover:bg-zinc-900 text-zinc-300 hover:text-nofx-gold hover:border-nofx-gold transition-all flex items-center justify-center gap-2 group/btn"
|
||||
>
|
||||
{copiedId === strategy.id ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 text-emerald-500" />
|
||||
<span className="text-emerald-500">{t.copied}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
||||
{t.copyConfig}
|
||||
</>
|
||||
)}
|
||||
</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">
|
||||
<Shield size={12} />
|
||||
{t.hideConfig}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA - Share Strategy */}
|
||||
{user && token && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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="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} />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.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">
|
||||
INITIALIZE_UPLOAD ->
|
||||
{/* CTA - Share Strategy */}
|
||||
{user && token && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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="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} />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">{t.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">
|
||||
INITIALIZE_UPLOAD ->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
||||
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
||||
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
||||
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||
|
||||
@@ -635,21 +636,23 @@ export function StrategyStudioPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-64px)] flex flex-col" style={{ background: '#0B0E11' }}>
|
||||
<DeepVoidBackground className="h-[calc(100vh-64px)] flex flex-col bg-nofx-bg relative overflow-hidden">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 px-4 py-3 border-b" style={{ borderColor: '#2B3139' }}>
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 px-4 py-3 border-b border-nofx-gold/20 bg-nofx-bg/60 backdrop-blur-md z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
<div className="p-2 rounded-lg bg-gradient-to-br from-nofx-gold to-yellow-500">
|
||||
<Sparkles className="w-5 h-5 text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('strategyStudio')}</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>{t('subtitle')}</p>
|
||||
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
|
||||
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs bg-nofx-danger/10 text-nofx-danger">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="hover:underline">×</button>
|
||||
</div>
|
||||
@@ -660,13 +663,13 @@ export function StrategyStudioPage() {
|
||||
{/* Main Content - Three Columns */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Column - Strategy List */}
|
||||
<div className="w-48 flex-shrink-0 border-r overflow-y-auto" style={{ borderColor: '#2B3139' }}>
|
||||
<div className="w-48 flex-shrink-0 border-r border-nofx-gold/20 overflow-y-auto bg-nofx-bg/30 backdrop-blur-sm z-10">
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-between mb-2 px-2">
|
||||
<span className="text-xs font-medium" style={{ color: '#848E9C' }}>{t('strategies')}</span>
|
||||
<span className="text-xs font-medium text-nofx-text-muted">{t('strategies')}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Import button with hidden file input */}
|
||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer" style={{ color: '#848E9C' }} title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
||||
<label className="p-1 rounded hover:bg-white/10 transition-colors cursor-pointer text-nofx-text-muted hover:text-white" title={language === 'zh' ? '导入策略' : 'Import Strategy'}>
|
||||
<Upload className="w-4 h-4" />
|
||||
<input
|
||||
type="file"
|
||||
@@ -677,8 +680,7 @@ export function StrategyStudioPage() {
|
||||
</label>
|
||||
<button
|
||||
onClick={handleCreateStrategy}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
|
||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
@@ -696,38 +698,36 @@ export function StrategyStudioPage() {
|
||||
setPromptPreview(null)
|
||||
setAiTestResult(null)
|
||||
}}
|
||||
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${
|
||||
selectedStrategy?.id === strategy.id ? 'ring-1 ring-yellow-500/50' : 'hover:bg-white/5'
|
||||
}`}
|
||||
style={{
|
||||
background: selectedStrategy?.id === strategy.id ? 'rgba(240, 185, 11, 0.1)' : 'transparent',
|
||||
}}
|
||||
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${selectedStrategy?.id === strategy.id
|
||||
? 'ring-1 ring-nofx-gold/50 bg-nofx-gold/10 shadow-[0_0_15px_rgba(240,185,11,0.1)]'
|
||||
: 'hover:bg-nofx-bg-lighter/60 hover:ring-1 hover:ring-nofx-gold/20 bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm truncate" style={{ color: '#EAECEF' }}>{strategy.name}</span>
|
||||
<span className="text-sm truncate text-nofx-text">{strategy.name}</span>
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||
title={language === 'zh' ? '导出' : 'Export'}
|
||||
>
|
||||
<Download className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
<Download className="w-3 h-3" />
|
||||
</button>
|
||||
{!strategy.is_default && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-white/10"
|
||||
className="p-1 rounded hover:bg-white/10 text-nofx-text-muted hover:text-white"
|
||||
title={language === 'zh' ? '复制' : 'Duplicate'}
|
||||
>
|
||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
<Copy className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
||||
className="p-1 rounded hover:bg-red-500/20"
|
||||
className="p-1 rounded hover:bg-nofx-danger/20 text-nofx-danger"
|
||||
title={language === 'zh' ? '删除' : 'Delete'}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -735,17 +735,17 @@ export function StrategyStudioPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||
{strategy.is_active && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-success/15 text-nofx-success">
|
||||
{t('active')}
|
||||
</span>
|
||||
)}
|
||||
{strategy.is_default && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded bg-nofx-gold/15 text-nofx-gold">
|
||||
{t('default')}
|
||||
</span>
|
||||
)}
|
||||
{strategy.is_public && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5" style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}>
|
||||
<span className="px-1.5 py-0.5 text-[10px] rounded flex items-center gap-0.5 bg-blue-400/15 text-blue-400">
|
||||
<Globe className="w-2.5 h-2.5" />
|
||||
{language === 'zh' ? '公开' : 'Public'}
|
||||
</span>
|
||||
@@ -758,7 +758,7 @@ export function StrategyStudioPage() {
|
||||
</div>
|
||||
|
||||
{/* Middle Column - Config Editor */}
|
||||
<div className="flex-1 min-w-0 overflow-y-auto border-r" style={{ borderColor: '#2B3139' }}>
|
||||
<div className="flex-1 min-w-0 overflow-y-auto border-r border-nofx-gold/20">
|
||||
{selectedStrategy && editingConfig ? (
|
||||
<div className="p-4">
|
||||
{/* Strategy Name & Actions */}
|
||||
@@ -772,19 +772,17 @@ export function StrategyStudioPage() {
|
||||
setHasChanges(true)
|
||||
}}
|
||||
disabled={selectedStrategy.is_default}
|
||||
className="text-lg font-bold bg-transparent border-none outline-none w-full"
|
||||
style={{ color: '#EAECEF' }}
|
||||
className="text-lg font-bold bg-transparent border-none outline-none w-full text-nofx-text placeholder-nofx-text-muted"
|
||||
/>
|
||||
{hasChanges && (
|
||||
<span className="text-xs" style={{ color: '#F0B90B' }}>● 未保存</span>
|
||||
<span className="text-xs text-nofx-gold">● 未保存</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{!selectedStrategy.is_active && (
|
||||
<button
|
||||
onClick={() => handleActivateStrategy(selectedStrategy.id)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors"
|
||||
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)', color: '#0ECB81' }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors bg-nofx-success/10 border border-nofx-success/30 text-nofx-success hover:bg-nofx-success/20"
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
{t('activate')}
|
||||
@@ -794,11 +792,8 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={handleSaveStrategy}
|
||||
disabled={isSaving || !hasChanges}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50"
|
||||
style={{
|
||||
background: hasChanges ? '#F0B90B' : '#2B3139',
|
||||
color: hasChanges ? '#0B0E11' : '#848E9C',
|
||||
}}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50
|
||||
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
|
||||
>
|
||||
<Save className="w-3 h-3" />
|
||||
{isSaving ? t('saving') : t('save')}
|
||||
@@ -812,8 +807,7 @@ export function StrategyStudioPage() {
|
||||
{configSections.map(({ key, icon: Icon, color, title, content }) => (
|
||||
<div
|
||||
key={key}
|
||||
className="rounded-lg overflow-hidden"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
|
||||
className="rounded-lg overflow-hidden bg-nofx-bg-lighter border border-nofx-gold/20"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleSection(key)}
|
||||
@@ -821,12 +815,12 @@ export function StrategyStudioPage() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4" style={{ color }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{title}</span>
|
||||
<span className="text-sm font-medium text-nofx-text">{title}</span>
|
||||
</div>
|
||||
{expandedSections[key] ? (
|
||||
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
|
||||
<ChevronDown className="w-4 h-4 text-nofx-text-muted" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" style={{ color: '#848E9C' }} />
|
||||
<ChevronRight className="w-4 h-4 text-nofx-text-muted" />
|
||||
)}
|
||||
</button>
|
||||
{expandedSections[key] && (
|
||||
@@ -841,8 +835,8 @@ export function StrategyStudioPage() {
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30" style={{ color: '#848E9C' }} />
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
|
||||
<p className="text-sm text-nofx-text-muted">
|
||||
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -853,29 +847,19 @@ export function StrategyStudioPage() {
|
||||
{/* Right Column - Prompt Preview & AI Test */}
|
||||
<div className="w-[420px] flex-shrink-0 flex flex-col overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="flex-shrink-0 flex border-b" style={{ borderColor: '#2B3139' }}>
|
||||
<div className="flex-shrink-0 flex border-b border-nofx-gold/20">
|
||||
<button
|
||||
onClick={() => setActiveRightTab('prompt')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
|
||||
activeRightTab === 'prompt' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeRightTab === 'prompt' ? '#a855f7' : 'transparent',
|
||||
color: activeRightTab === 'prompt' ? '#a855f7' : '#848E9C',
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${activeRightTab === 'prompt' ? 'border-b-2 border-purple-500 text-purple-500' : 'opacity-60 hover:opacity-100 text-nofx-text-muted'
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{t('promptPreview')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveRightTab('test')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
|
||||
activeRightTab === 'test' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: activeRightTab === 'test' ? '#22c55e' : 'transparent',
|
||||
color: activeRightTab === 'test' ? '#22c55e' : '#848E9C',
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${activeRightTab === 'test' ? 'border-b-2 border-green-500 text-green-500' : 'opacity-60 hover:opacity-100 text-nofx-text-muted'
|
||||
}`}
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
{t('aiTestRun')}
|
||||
@@ -892,8 +876,7 @@ export function StrategyStudioPage() {
|
||||
<select
|
||||
value={selectedVariant}
|
||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||
className="px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text outline-none focus:border-nofx-gold"
|
||||
>
|
||||
<option value="balanced">{t('balanced')}</option>
|
||||
<option value="aggressive">{t('aggressive')}</option>
|
||||
@@ -902,8 +885,7 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={fetchPromptPreview}
|
||||
disabled={isLoadingPrompt || !editingConfig}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50"
|
||||
style={{ background: '#a855f7', color: '#fff' }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 bg-purple-600 hover:bg-purple-700 text-white"
|
||||
>
|
||||
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
||||
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
|
||||
@@ -913,16 +895,16 @@ export function StrategyStudioPage() {
|
||||
{promptPreview ? (
|
||||
<>
|
||||
{/* Config Summary */}
|
||||
<div className="p-2 rounded-lg" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="p-2 rounded-lg bg-nofx-bg border border-nofx-gold/20">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<Code className="w-3 h-3" style={{ color: '#a855f7' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#a855f7' }}>Config</span>
|
||||
<Code className="w-3 h-3 text-purple-500" />
|
||||
<span className="text-xs font-medium text-purple-500">Config</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
{Object.entries(promptPreview.config_summary || {}).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<div style={{ color: '#848E9C' }}>{key.replace(/_/g, ' ')}</div>
|
||||
<div style={{ color: '#EAECEF' }}>{String(value)}</div>
|
||||
<div className="text-nofx-text-muted">{key.replace(/_/g, ' ')}</div>
|
||||
<div className="text-nofx-text">{String(value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -932,23 +914,23 @@ export function StrategyStudioPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<FileText className="w-3 h-3" style={{ color: '#a855f7' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('systemPrompt')}</span>
|
||||
<FileText className="w-3 h-3 text-purple-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
|
||||
</div>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ background: '#2B3139', color: '#848E9C' }}>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-nofx-bg-lighter text-nofx-text-muted">
|
||||
{promptPreview.system_prompt.length.toLocaleString()} chars
|
||||
</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[11px] font-mono overflow-auto"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '400px' }}
|
||||
className="p-2 rounded-lg text-[11px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
style={{ maxHeight: '400px' }}
|
||||
>
|
||||
{promptPreview.system_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12" style={{ color: '#848E9C' }}>
|
||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||
<Eye className="w-10 h-10 mb-2 opacity-30" />
|
||||
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
|
||||
</div>
|
||||
@@ -960,15 +942,14 @@ export function StrategyStudioPage() {
|
||||
{/* Controls */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('selectModel')}</span>
|
||||
<Bot className="w-4 h-4 text-green-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
|
||||
</div>
|
||||
{aiModels.length > 0 ? (
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg text-sm"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
className="w-full px-3 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
{aiModels.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
@@ -977,7 +958,7 @@ export function StrategyStudioPage() {
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="px-3 py-2 rounded-lg text-sm" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
<div className="px-3 py-2 rounded-lg text-sm bg-nofx-danger/10 text-nofx-danger">
|
||||
{t('noModel')}
|
||||
</div>
|
||||
)}
|
||||
@@ -986,8 +967,7 @@ export function StrategyStudioPage() {
|
||||
<select
|
||||
value={selectedVariant}
|
||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||
className="px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
>
|
||||
<option value="balanced">{t('balanced')}</option>
|
||||
<option value="aggressive">{t('aggressive')}</option>
|
||||
@@ -996,12 +976,7 @@ export function StrategyStudioPage() {
|
||||
<button
|
||||
onClick={runAiTest}
|
||||
disabled={isRunningAiTest || !editingConfig || !selectedModelId}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:opacity-50"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #22c55e 0%, #4ade80 100%)',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)',
|
||||
}}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:opacity-50 text-white shadow-lg shadow-green-500/20 bg-gradient-to-br from-green-500 to-green-600"
|
||||
>
|
||||
{isRunningAiTest ? (
|
||||
<>
|
||||
@@ -1016,22 +991,22 @@ export function StrategyStudioPage() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('testNote')}</p>
|
||||
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
|
||||
</div>
|
||||
|
||||
{/* Test Results */}
|
||||
{aiTestResult ? (
|
||||
<div className="space-y-3">
|
||||
{aiTestResult.error ? (
|
||||
<div className="p-3 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.3)' }}>
|
||||
<p className="text-sm" style={{ color: '#F6465D' }}>{aiTestResult.error}</p>
|
||||
<div className="p-3 rounded-lg bg-nofx-danger/10 border border-nofx-danger/30">
|
||||
<p className="text-sm text-nofx-danger">{aiTestResult.error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{aiTestResult.duration_ms && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
<Clock className="w-3 h-3 text-nofx-text-muted" />
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
||||
</span>
|
||||
</div>
|
||||
@@ -1041,12 +1016,12 @@ export function StrategyStudioPage() {
|
||||
{aiTestResult.user_prompt && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Terminal className="w-3 h-3" style={{ color: '#60a5fa' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('userPrompt')} (Input)</span>
|
||||
<Terminal className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '200px' }}
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
style={{ maxHeight: '200px' }}
|
||||
>
|
||||
{aiTestResult.user_prompt}
|
||||
</pre>
|
||||
@@ -1057,12 +1032,12 @@ export function StrategyStudioPage() {
|
||||
{aiTestResult.reasoning && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Sparkles className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('reasoning')}</span>
|
||||
<Sparkles className="w-3 h-3 text-nofx-gold" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
|
||||
style={{ background: '#0B0E11', border: '1px solid rgba(240, 185, 11, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/30 text-nofx-text"
|
||||
style={{ maxHeight: '200px' }}
|
||||
>
|
||||
{aiTestResult.reasoning}
|
||||
</pre>
|
||||
@@ -1073,12 +1048,12 @@ export function StrategyStudioPage() {
|
||||
{aiTestResult.decisions && aiTestResult.decisions.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<Activity className="w-3 h-3" style={{ color: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('decisions')}</span>
|
||||
<Activity className="w-3 h-3 text-green-500" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
|
||||
style={{ background: '#0B0E11', border: '1px solid rgba(34, 197, 94, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
|
||||
style={{ maxHeight: '200px' }}
|
||||
>
|
||||
{JSON.stringify(aiTestResult.decisions, null, 2)}
|
||||
</pre>
|
||||
@@ -1089,12 +1064,12 @@ export function StrategyStudioPage() {
|
||||
{aiTestResult.ai_response && (
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<FileText className="w-3 h-3" style={{ color: '#848E9C' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('aiOutput')} (Raw)</span>
|
||||
<FileText className="w-3 h-3 text-nofx-text-muted" />
|
||||
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
|
||||
</div>
|
||||
<pre
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '300px' }}
|
||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||
style={{ maxHeight: '300px' }}
|
||||
>
|
||||
{aiTestResult.ai_response}
|
||||
</pre>
|
||||
@@ -1104,7 +1079,7 @@ export function StrategyStudioPage() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12" style={{ color: '#848E9C' }}>
|
||||
<div className="flex flex-col items-center justify-center py-12 text-nofx-text-muted">
|
||||
<Play className="w-10 h-10 mb-2 opacity-30" />
|
||||
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
|
||||
</div>
|
||||
@@ -1114,7 +1089,7 @@ export function StrategyStudioPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
857
web/src/pages/TraderDashboardPage.tsx
Normal file
857
web/src/pages/TraderDashboardPage.tsx
Normal file
@@ -0,0 +1,857 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { mutate } from 'swr'
|
||||
import { api } from '../lib/api'
|
||||
import { ChartTabs } from '../components/ChartTabs'
|
||||
import { DecisionCard } from '../components/DecisionCard'
|
||||
import { PositionHistory } from '../components/PositionHistory'
|
||||
import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar'
|
||||
import { confirmToast, notify } from '../lib/notify'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react'
|
||||
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
Position,
|
||||
DecisionRecord,
|
||||
Statistics,
|
||||
TraderInfo,
|
||||
Exchange,
|
||||
} from '../types'
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// 获取友好的AI模型名称
|
||||
function getModelDisplayName(modelId: string): string {
|
||||
switch (modelId.toLowerCase()) {
|
||||
case 'deepseek':
|
||||
return 'DeepSeek'
|
||||
case 'qwen':
|
||||
return 'Qwen'
|
||||
case 'claude':
|
||||
return 'Claude'
|
||||
default:
|
||||
return modelId.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get exchange display name from exchange ID (UUID)
|
||||
function getExchangeDisplayNameFromList(
|
||||
exchangeId: string | undefined,
|
||||
exchanges: Exchange[] | undefined
|
||||
): string {
|
||||
if (!exchangeId) return 'Unknown'
|
||||
const exchange = exchanges?.find((e) => e.id === exchangeId)
|
||||
if (!exchange) return exchangeId.substring(0, 8).toUpperCase() + '...'
|
||||
const typeName = exchange.exchange_type?.toUpperCase() || exchange.name
|
||||
return exchange.account_name
|
||||
? `${typeName} - ${exchange.account_name}`
|
||||
: typeName
|
||||
}
|
||||
|
||||
// Helper function to get exchange type from exchange ID (UUID) - for kline charts
|
||||
function getExchangeTypeFromList(
|
||||
exchangeId: string | undefined,
|
||||
exchanges: Exchange[] | undefined
|
||||
): string {
|
||||
if (!exchangeId) return 'binance'
|
||||
const exchange = exchanges?.find((e) => e.id === exchangeId)
|
||||
if (!exchange) return 'binance' // Default to binance for charts
|
||||
return exchange.exchange_type?.toLowerCase() || 'binance'
|
||||
}
|
||||
|
||||
// Helper function to check if exchange is a perp-dex type (wallet-based)
|
||||
function isPerpDexExchange(exchangeType: string | undefined): boolean {
|
||||
if (!exchangeType) return false
|
||||
const perpDexTypes = ['hyperliquid', 'lighter', 'aster']
|
||||
return perpDexTypes.includes(exchangeType.toLowerCase())
|
||||
}
|
||||
|
||||
// Helper function to get wallet address for perp-dex exchanges
|
||||
function getWalletAddress(exchange: Exchange | undefined): string | undefined {
|
||||
if (!exchange) return undefined
|
||||
const type = exchange.exchange_type?.toLowerCase()
|
||||
switch (type) {
|
||||
case 'hyperliquid':
|
||||
return exchange.hyperliquidWalletAddr
|
||||
case 'lighter':
|
||||
return exchange.lighterWalletAddr
|
||||
case 'aster':
|
||||
return exchange.asterSigner
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to truncate wallet address for display
|
||||
function truncateAddress(address: string, startLen = 6, endLen = 4): string {
|
||||
if (address.length <= startLen + endLen + 3) return address
|
||||
return `${address.slice(0, startLen)}...${address.slice(-endLen)}`
|
||||
}
|
||||
|
||||
// --- Components ---
|
||||
|
||||
interface TraderDashboardPageProps {
|
||||
selectedTrader?: TraderInfo
|
||||
traders?: TraderInfo[]
|
||||
tradersError?: Error
|
||||
selectedTraderId?: string
|
||||
onTraderSelect: (traderId: string) => void
|
||||
onNavigateToTraders: () => void
|
||||
status?: SystemStatus
|
||||
account?: AccountInfo
|
||||
positions?: Position[]
|
||||
decisions?: DecisionRecord[]
|
||||
decisionsLimit: number
|
||||
onDecisionsLimitChange: (limit: number) => void
|
||||
stats?: Statistics
|
||||
lastUpdate: string
|
||||
language: Language
|
||||
exchanges?: Exchange[]
|
||||
}
|
||||
|
||||
export function TraderDashboardPage({
|
||||
selectedTrader,
|
||||
status,
|
||||
account,
|
||||
positions,
|
||||
decisions,
|
||||
decisionsLimit,
|
||||
onDecisionsLimitChange,
|
||||
lastUpdate,
|
||||
language,
|
||||
traders,
|
||||
tradersError,
|
||||
selectedTraderId,
|
||||
onTraderSelect,
|
||||
onNavigateToTraders,
|
||||
exchanges,
|
||||
}: TraderDashboardPageProps) {
|
||||
const [closingPosition, setClosingPosition] = useState<string | null>(null)
|
||||
const [selectedChartSymbol, setSelectedChartSymbol] = useState<string | undefined>(undefined)
|
||||
const [chartUpdateKey, setChartUpdateKey] = useState<number>(0)
|
||||
const chartSectionRef = useRef<HTMLDivElement>(null)
|
||||
const [showWalletAddress, setShowWalletAddress] = useState<boolean>(false)
|
||||
const [copiedAddress, setCopiedAddress] = useState<boolean>(false)
|
||||
|
||||
// Current positions pagination
|
||||
const [positionsPageSize, setPositionsPageSize] = useState<number>(20)
|
||||
const [positionsCurrentPage, setPositionsCurrentPage] = useState<number>(1)
|
||||
|
||||
// Calculate paginated positions
|
||||
const totalPositions = positions?.length || 0
|
||||
const totalPositionPages = Math.ceil(totalPositions / positionsPageSize)
|
||||
const paginatedPositions = positions?.slice(
|
||||
(positionsCurrentPage - 1) * positionsPageSize,
|
||||
positionsCurrentPage * positionsPageSize
|
||||
) || []
|
||||
|
||||
// Reset page when positions change
|
||||
useEffect(() => {
|
||||
setPositionsCurrentPage(1)
|
||||
}, [selectedTraderId, positionsPageSize])
|
||||
|
||||
// Get current exchange info for perp-dex wallet display
|
||||
const currentExchange = exchanges?.find(
|
||||
(e) => e.id === selectedTrader?.exchange_id
|
||||
)
|
||||
const walletAddress = getWalletAddress(currentExchange)
|
||||
const isPerpDex = isPerpDexExchange(currentExchange?.exchange_type)
|
||||
|
||||
// Copy wallet address to clipboard
|
||||
const handleCopyAddress = async () => {
|
||||
if (!walletAddress) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(walletAddress)
|
||||
setCopiedAddress(true)
|
||||
setTimeout(() => setCopiedAddress(false), 2000)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy address:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle symbol click from Decision Card
|
||||
const handleSymbolClick = (symbol: string) => {
|
||||
// Set the selected symbol
|
||||
setSelectedChartSymbol(symbol)
|
||||
// Scroll to chart section
|
||||
setTimeout(() => {
|
||||
chartSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 平仓操作
|
||||
const handleClosePosition = async (symbol: string, side: string) => {
|
||||
if (!selectedTraderId) return
|
||||
|
||||
const confirmMsg =
|
||||
language === 'zh'
|
||||
? `确定要平仓 ${symbol} ${side === 'LONG' ? '多仓' : '空仓'} 吗?`
|
||||
: `Are you sure you want to close ${symbol} ${side === 'LONG' ? 'LONG' : 'SHORT'} position?`
|
||||
|
||||
const confirmed = await confirmToast(confirmMsg, {
|
||||
title: language === 'zh' ? '确认平仓' : 'Confirm Close',
|
||||
okText: language === 'zh' ? '确认' : 'Confirm',
|
||||
cancelText: language === 'zh' ? '取消' : 'Cancel',
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
setClosingPosition(symbol)
|
||||
try {
|
||||
await api.closePosition(selectedTraderId, symbol, side)
|
||||
notify.success(
|
||||
language === 'zh' ? '平仓成功' : 'Position closed successfully'
|
||||
)
|
||||
// 使用 SWR mutate 刷新数据而非重新加载页面
|
||||
await Promise.all([
|
||||
mutate(`positions-${selectedTraderId}`),
|
||||
mutate(`account-${selectedTraderId}`),
|
||||
])
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: language === 'zh'
|
||||
? '平仓失败'
|
||||
: 'Failed to close position'
|
||||
notify.error(errorMsg)
|
||||
} finally {
|
||||
setClosingPosition(null)
|
||||
}
|
||||
}
|
||||
|
||||
// If API failed with error, show empty state (likely backend not running)
|
||||
if (tradersError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh] relative z-10">
|
||||
<div className="text-center max-w-md mx-auto px-6">
|
||||
<div
|
||||
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center nofx-glass"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
borderColor: 'rgba(240, 185, 11, 0.3)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="w-12 h-12 text-nofx-gold"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
|
||||
{language === 'zh' ? '无法连接到服务器' : 'Connection Failed'}
|
||||
</h2>
|
||||
<p className="text-base mb-6 text-nofx-text-muted">
|
||||
{language === 'zh'
|
||||
? '请确认后端服务已启动。'
|
||||
: 'Please check if the backend service is running.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
|
||||
>
|
||||
{language === 'zh' ? '重试' : 'Retry'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If traders is loaded and empty, show empty state
|
||||
if (traders && traders.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh] relative z-10">
|
||||
<div className="text-center max-w-md mx-auto px-6">
|
||||
<div
|
||||
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center nofx-glass"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
borderColor: 'rgba(240, 185, 11, 0.3)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="w-12 h-12 text-nofx-gold"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-3 text-nofx-text-main">
|
||||
{t('dashboardEmptyTitle', language)}
|
||||
</h2>
|
||||
<p className="text-base mb-6 text-nofx-text-muted">
|
||||
{t('dashboardEmptyDescription', language)}
|
||||
</p>
|
||||
<button
|
||||
onClick={onNavigateToTraders}
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95 nofx-glass border border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/10"
|
||||
>
|
||||
{t('goToTradersPage', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If traders is still loading or selectedTrader is not ready, show skeleton
|
||||
if (!selectedTrader) {
|
||||
return (
|
||||
<div className="space-y-6 relative z-10">
|
||||
<div className="nofx-glass p-6 animate-pulse">
|
||||
<div className="h-8 w-48 mb-3 bg-nofx-bg/50 rounded"></div>
|
||||
<div className="flex gap-4">
|
||||
<div className="h-4 w-32 bg-nofx-bg/50 rounded"></div>
|
||||
<div className="h-4 w-24 bg-nofx-bg/50 rounded"></div>
|
||||
<div className="h-4 w-28 bg-nofx-bg/50 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="nofx-glass p-5 animate-pulse">
|
||||
<div className="h-4 w-24 mb-3 bg-nofx-bg/50 rounded"></div>
|
||||
<div className="h-8 w-32 bg-nofx-bg/50 rounded"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="nofx-glass p-6 animate-pulse">
|
||||
<div className="h-6 w-40 mb-4 bg-nofx-bg/50 rounded"></div>
|
||||
<div className="h-64 w-full bg-nofx-bg/50 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DeepVoidBackground className="min-h-screen pb-12" disableAnimation>
|
||||
<div className="w-full px-4 md:px-8 relative z-10 pt-6">
|
||||
{/* Trader Header */}
|
||||
<div
|
||||
className="mb-6 rounded-lg p-6 animate-scale-in nofx-glass group"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.6) 0%, rgba(15, 23, 42, 0.4) 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h2 className="text-2xl font-bold flex items-center gap-4 text-nofx-text-main">
|
||||
<div className="relative">
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(
|
||||
selectedTrader.trader_id,
|
||||
selectedTrader.trader_name
|
||||
)}
|
||||
size={56}
|
||||
className="rounded-xl border-2 border-nofx-gold/30 shadow-[0_0_15px_rgba(240,185,11,0.2)]"
|
||||
/>
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-nofx-green rounded-full border-2 border-[#0B0E11] shadow-[0_0_8px_rgba(14,203,129,0.8)] animate-pulse" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-3xl tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-nofx-text-main to-nofx-text-muted">
|
||||
{selectedTrader.trader_name}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-nofx-text-muted opacity-60 flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-nofx-gold rounded-full" />
|
||||
ID: {selectedTrader.trader_id.slice(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Trader Selector */}
|
||||
{traders && traders.length > 0 && (
|
||||
<div className="flex items-center gap-2 nofx-glass px-1 py-1 rounded-lg border border-white/5">
|
||||
<select
|
||||
value={selectedTraderId}
|
||||
onChange={(e) => onTraderSelect(e.target.value)}
|
||||
className="bg-transparent text-sm font-medium cursor-pointer transition-colors text-nofx-text-main focus:outline-none px-2 py-1"
|
||||
>
|
||||
{traders.map((trader) => (
|
||||
<option key={trader.trader_id} value={trader.trader_id} className="bg-[#0B0E11]">
|
||||
{trader.trader_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wallet Address Display for Perp-DEX */}
|
||||
{exchanges && isPerpDex && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg nofx-glass border border-nofx-gold/20">
|
||||
{walletAddress ? (
|
||||
<>
|
||||
<span className="text-xs font-mono text-nofx-gold">
|
||||
{showWalletAddress
|
||||
? walletAddress
|
||||
: truncateAddress(walletAddress)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWalletAddress(!showWalletAddress)}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
title={
|
||||
showWalletAddress
|
||||
? language === 'zh'
|
||||
? '隐藏地址'
|
||||
: 'Hide address'
|
||||
: language === 'zh'
|
||||
? '显示完整地址'
|
||||
: 'Show full address'
|
||||
}
|
||||
>
|
||||
{showWalletAddress ? (
|
||||
<EyeOff className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||
) : (
|
||||
<Eye className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyAddress}
|
||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
||||
title={language === 'zh' ? '复制地址' : 'Copy address'}
|
||||
>
|
||||
{copiedAddress ? (
|
||||
<Check className="w-3.5 h-3.5 text-nofx-green" />
|
||||
) : (
|
||||
<Copy className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-nofx-text-muted">
|
||||
{language === 'zh' ? '未配置地址' : 'No address configured'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-sm flex-wrap text-nofx-text-muted font-mono pl-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="opacity-60">AI Model:</span>
|
||||
<span
|
||||
className="font-bold px-2 py-0.5 rounded text-xs tracking-wide"
|
||||
style={{
|
||||
background: selectedTrader.ai_model.includes('qwen') ? 'rgba(192, 132, 252, 0.15)' : 'rgba(96, 165, 250, 0.15)',
|
||||
color: selectedTrader.ai_model.includes('qwen') ? '#c084fc' : '#60a5fa',
|
||||
border: `1px solid ${selectedTrader.ai_model.includes('qwen') ? '#c084fc' : '#60a5fa'}40`
|
||||
}}
|
||||
>
|
||||
{getModelDisplayName(
|
||||
selectedTrader.ai_model.split('_').pop() ||
|
||||
selectedTrader.ai_model
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="w-px h-3 bg-white/10" />
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="opacity-60">Exchange:</span>
|
||||
<span className="text-nofx-text-main font-semibold">
|
||||
{getExchangeDisplayNameFromList(
|
||||
selectedTrader.exchange_id,
|
||||
exchanges
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="w-px h-3 bg-white/10" />
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="opacity-60">Strategy:</span>
|
||||
<span className="text-nofx-gold font-semibold tracking-wide">
|
||||
{selectedTrader.strategy_name || 'No Strategy'}
|
||||
</span>
|
||||
</span>
|
||||
{status && (
|
||||
<>
|
||||
<span className="w-px h-3 bg-white/10" />
|
||||
<span>Cycles: <span className="text-nofx-text-main">{status.call_count}</span></span>
|
||||
<span className="w-px h-3 bg-white/10" />
|
||||
<span>Runtime: <span className="text-nofx-text-main">{status.runtime_minutes} min</span></span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Debug Info */}
|
||||
{account && (
|
||||
<div className="mb-4 px-3 py-1.5 rounded bg-black/40 border border-white/5 text-[10px] font-mono text-nofx-text-muted flex justify-between items-center opacity-60 hover:opacity-100 transition-opacity">
|
||||
<span>SYSTEM_STATUS::ONLINE</span>
|
||||
<div className="flex gap-4">
|
||||
<span>LAST_UPDATE::{lastUpdate}</span>
|
||||
<span>EQ::{account?.total_equity?.toFixed(2)}</span>
|
||||
<span>PNL::{account?.total_pnl?.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Account Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
title={t('totalEquity', language)}
|
||||
value={`${account?.total_equity?.toFixed(2) || '0.00'}`}
|
||||
unit="USDT"
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={(account?.total_pnl ?? 0) > 0}
|
||||
icon="💰"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('availableBalance', language)}
|
||||
value={`${account?.available_balance?.toFixed(2) || '0.00'}`}
|
||||
unit="USDT"
|
||||
subtitle={`${account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0'}% ${t('free', language)}`}
|
||||
icon="💳"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('totalPnL', language)}
|
||||
value={`${account?.total_pnl !== undefined && account.total_pnl >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'}`}
|
||||
unit="USDT"
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={(account?.total_pnl ?? 0) >= 0}
|
||||
icon="📈"
|
||||
/>
|
||||
<StatCard
|
||||
title={t('positions', language)}
|
||||
value={`${account?.position_count || 0}`}
|
||||
unit="ACTIVE"
|
||||
subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
|
||||
icon="📊"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Left Column: Charts + Positions */}
|
||||
<div className="space-y-6">
|
||||
{/* Chart Tabs (Equity / K-line) */}
|
||||
<div
|
||||
ref={chartSectionRef}
|
||||
className="chart-container animate-slide-in scroll-mt-32 backdrop-blur-sm"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
>
|
||||
<ChartTabs
|
||||
traderId={selectedTrader.trader_id}
|
||||
selectedSymbol={selectedChartSymbol}
|
||||
updateKey={chartUpdateKey}
|
||||
exchangeId={getExchangeTypeFromList(
|
||||
selectedTrader.exchange_id,
|
||||
exchanges
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Positions */}
|
||||
<div
|
||||
className="nofx-glass p-6 animate-slide-in relative overflow-hidden group"
|
||||
style={{ animationDelay: '0.15s' }}
|
||||
>
|
||||
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<div className="w-24 h-24 rounded-full bg-blue-500 blur-3xl" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-5 relative z-10">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2 text-nofx-text-main uppercase tracking-wide">
|
||||
<span className="text-blue-500">◈</span> {t('currentPositions', language)}
|
||||
</h2>
|
||||
{positions && positions.length > 0 && (
|
||||
<div className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 font-mono shadow-[0_0_10px_rgba(240,185,11,0.1)]">
|
||||
{positions.length} {t('active', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{positions && positions.length > 0 ? (
|
||||
<div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-left border-b border-white/5">
|
||||
<tr>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{language === 'zh' ? '操作' : 'Action'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('entryPrice', language)}>{language === 'zh' ? '入场价' : 'Entry'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('markPrice', language)}>{language === 'zh' ? '标记价' : 'Mark'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('quantity', language)}>{language === 'zh' ? '数量' : 'Qty'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('positionValue', language)}>{language === 'zh' ? '价值' : 'Value'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center" title={t('leverage', language)}>{language === 'zh' ? '杠杆' : 'Lev.'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('unrealizedPnL', language)}>{language === 'zh' ? '未实现盈亏' : 'uPnL'}</th>
|
||||
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right" title={t('liqPrice', language)}>{language === 'zh' ? '强平价' : 'Liq.'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedPositions.map((pos, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-b border-white/5 last:border-0 transition-all hover:bg-white/5 cursor-pointer group/row"
|
||||
onClick={() => {
|
||||
setSelectedChartSymbol(pos.symbol)
|
||||
setChartUpdateKey(Date.now())
|
||||
if (chartSectionRef.current) {
|
||||
chartSectionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left text-nofx-text-main group-hover/row:text-white transition-colors">
|
||||
{pos.symbol}
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<span
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${pos.side === 'long' ? 'bg-nofx-green/10 text-nofx-green shadow-[0_0_8px_rgba(14,203,129,0.2)]' : 'bg-nofx-red/10 text-nofx-red shadow-[0_0_8px_rgba(246,70,93,0.2)]'}`}
|
||||
>
|
||||
{t(pos.side === 'long' ? 'long' : 'short', language)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleClosePosition(pos.symbol, pos.side.toUpperCase())
|
||||
}}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto bg-nofx-red/10 text-nofx-red border border-nofx-red/30 hover:bg-nofx-red/20"
|
||||
title={language === 'zh' ? '平仓' : 'Close Position'}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.entry_price.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.mark_price.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.quantity.toFixed(4)}</td>
|
||||
<td className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right text-nofx-text-main">{(pos.quantity * pos.mark_price).toFixed(2)}</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-center text-nofx-gold">{pos.leverage}x</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
|
||||
<span
|
||||
className={`font-bold ${pos.unrealized_pnl >= 0 ? 'text-nofx-green shadow-nofx-green' : 'text-nofx-red shadow-nofx-red'}`}
|
||||
style={{ textShadow: pos.unrealized_pnl >= 0 ? '0 0 10px rgba(14,203,129,0.3)' : '0 0 10px rgba(246,70,93,0.3)' }}
|
||||
>
|
||||
{pos.unrealized_pnl >= 0 ? '+' : ''}
|
||||
{pos.unrealized_pnl.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted">{pos.liquidation_price.toFixed(4)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Pagination footer */}
|
||||
{totalPositions > 10 && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs border-t border-white/5 text-nofx-text-muted">
|
||||
<span>
|
||||
{language === 'zh'
|
||||
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
|
||||
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{language === 'zh' ? '每页' : 'Per page'}:</span>
|
||||
<select
|
||||
value={positionsPageSize}
|
||||
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
|
||||
className="bg-black/40 border border-white/10 rounded px-2 py-1 text-xs text-nofx-text-main focus:outline-none focus:border-nofx-gold/50 transition-colors"
|
||||
>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
{totalPositionPages > 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
{['«', '‹', `${positionsCurrentPage} / ${totalPositionPages}`, '›', '»'].map((label, idx) => {
|
||||
const isText = idx === 2;
|
||||
const isFirst = idx === 0;
|
||||
const isPrev = idx === 1;
|
||||
const isNext = idx === 3;
|
||||
const isLast = idx === 4;
|
||||
if (isText) return <span key={idx} className="px-3 text-nofx-text-main">{label}</span>;
|
||||
|
||||
let onClick = () => { };
|
||||
let disabled = false;
|
||||
|
||||
if (isFirst) { onClick = () => setPositionsCurrentPage(1); disabled = positionsCurrentPage === 1; }
|
||||
if (isPrev) { onClick = () => setPositionsCurrentPage(p => Math.max(1, p - 1)); disabled = positionsCurrentPage === 1; }
|
||||
if (isNext) { onClick = () => setPositionsCurrentPage(p => Math.min(totalPositionPages, p + 1)); disabled = positionsCurrentPage === totalPositionPages; }
|
||||
if (isLast) { onClick = () => setPositionsCurrentPage(totalPositionPages); disabled = positionsCurrentPage === totalPositionPages; }
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`px-2 py-1 rounded transition-colors ${disabled ? 'opacity-30 cursor-not-allowed' : 'hover:bg-white/10 text-nofx-text-main bg-white/5'}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 text-nofx-text-muted opacity-60">
|
||||
<div className="text-6xl mb-4 opacity-50 grayscale">📊</div>
|
||||
<div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
|
||||
<div className="text-sm">{t('noActivePositions', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Recent Decisions */}
|
||||
<div
|
||||
className="nofx-glass p-6 animate-slide-in h-fit lg:sticky lg:top-24 lg:max-h-[calc(100vh-120px)] flex flex-col"
|
||||
style={{ animationDelay: '0.2s' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-5 pb-4 border-b border-white/5 shrink-0">
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center text-xl shadow-[0_4px_14px_rgba(99,102,241,0.4)]"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)',
|
||||
}}
|
||||
>
|
||||
🧠
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold text-nofx-text-main">
|
||||
{t('recentDecisions', language)}
|
||||
</h2>
|
||||
{decisions && decisions.length > 0 && (
|
||||
<div className="text-xs text-nofx-text-muted">
|
||||
{t('lastCycles', language, { count: decisions.length })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Limit Selector */}
|
||||
<select
|
||||
value={decisionsLimit}
|
||||
onChange={(e) => onDecisionsLimitChange(Number(e.target.value))}
|
||||
className="px-3 py-1.5 rounded-lg text-sm font-medium cursor-pointer transition-all bg-black/40 text-nofx-text-main border border-white/10 hover:border-nofx-accent focus:outline-none"
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Decisions List - Scrollable */}
|
||||
<div
|
||||
className="space-y-4 overflow-y-auto pr-2 custom-scrollbar"
|
||||
style={{ maxHeight: 'calc(100vh - 280px)' }}
|
||||
>
|
||||
{decisions && decisions.length > 0 ? (
|
||||
decisions.map((decision, i) => (
|
||||
<DecisionCard key={i} decision={decision} language={language} onSymbolClick={handleSymbolClick} />
|
||||
))
|
||||
) : (
|
||||
<div className="py-16 text-center text-nofx-text-muted opacity-60">
|
||||
<div className="text-6xl mb-4 opacity-30 grayscale">🧠</div>
|
||||
<div className="text-lg font-semibold mb-2 text-nofx-text-main">
|
||||
{t('noDecisionsYet', language)}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{t('aiDecisionsWillAppear', language)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position History Section */}
|
||||
{selectedTraderId && (
|
||||
<div
|
||||
className="nofx-glass p-6 animate-slide-in"
|
||||
style={{ animationDelay: '0.25s' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2 text-nofx-text-main">
|
||||
<span className="text-2xl">📜</span>
|
||||
{t('positionHistory.title', language)}
|
||||
</h2>
|
||||
</div>
|
||||
<PositionHistory traderId={selectedTraderId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
}
|
||||
|
||||
// Stat Card Component - Deep Void Style
|
||||
function StatCard({
|
||||
title,
|
||||
value,
|
||||
unit,
|
||||
change,
|
||||
positive,
|
||||
subtitle,
|
||||
icon,
|
||||
}: {
|
||||
title: string
|
||||
value: string
|
||||
unit?: string
|
||||
change?: number
|
||||
positive?: boolean
|
||||
subtitle?: string
|
||||
icon?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="group nofx-glass p-5 rounded-lg transition-all duration-300 hover:bg-white/5 hover:translate-y-[-2px] border border-white/5 hover:border-nofx-gold/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity text-4xl grayscale group-hover:grayscale-0">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-xs mb-2 font-mono uppercase tracking-wider text-nofx-text-muted flex items-center gap-2">
|
||||
{title}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1 mb-1">
|
||||
<div className="text-2xl font-bold font-mono text-nofx-text-main tracking-tight group-hover:text-white transition-colors">
|
||||
{value}
|
||||
</div>
|
||||
{unit && <span className="text-xs font-mono text-nofx-text-muted opacity-60">{unit}</span>}
|
||||
</div>
|
||||
|
||||
{change !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className={`text-sm mono font-bold flex items-center gap-1 ${positive ? 'text-nofx-green' : 'text-nofx-red'}`}
|
||||
>
|
||||
<span>{positive ? '▲' : '▼'}</span>
|
||||
<span>{positive ? '+' : ''}{change.toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{subtitle && (
|
||||
<div className="text-xs mt-2 mono text-nofx-text-muted opacity-80">
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user