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)
|
console.log('[ChartTabs] rendering, activeTab:', activeTab)
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Clean Professional Toolbar */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-3 py-1.5"
|
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(43, 49, 57, 0.6)', background: '#161B22' }}
|
style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}
|
||||||
>
|
>
|
||||||
{/* Left: Tab Switcher */}
|
{/* Left: Tab Switcher */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('equity')}
|
onClick={() => setActiveTab('equity')}
|
||||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'equity'
|
||||||
activeTab === 'equity'
|
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||||
? 'bg-blue-500/15 text-blue-400'
|
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||||
: 'text-gray-500 hover:text-gray-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<BarChart3 className="w-3 h-3" />
|
<BarChart3 className="w-3.5 h-3.5" />
|
||||||
<span>{t('accountEquityCurve', language)}</span>
|
<span>{t('accountEquityCurve', language)}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('kline')}
|
onClick={() => setActiveTab('kline')}
|
||||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded text-[11px] font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-[11px] font-medium transition-all ${activeTab === 'kline'
|
||||||
activeTab === 'kline'
|
? 'bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 shadow-[0_0_10px_rgba(240,185,11,0.1)]'
|
||||||
? 'bg-blue-500/15 text-blue-400'
|
: 'text-nofx-text-muted hover:text-nofx-text-main hover:bg-white/5'
|
||||||
: 'text-gray-500 hover:text-gray-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<CandlestickChart className="w-3 h-3" />
|
<CandlestickChart className="w-3.5 h-3.5" />
|
||||||
<span>{t('marketChart', language)}</span>
|
<span>{t('marketChart', language)}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Market Type Pills - Only when kline active */}
|
{/* Market Type Pills - Only when kline active */}
|
||||||
{activeTab === 'kline' && (
|
{activeTab === 'kline' && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-3 bg-[#30363D] mx-1" />
|
<div className="w-px h-4 bg-white/10 mx-2" />
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-1">
|
||||||
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
|
{(Object.keys(MARKET_CONFIG) as MarketType[]).map((type) => {
|
||||||
const config = MARKET_CONFIG[type]
|
const config = MARKET_CONFIG[type]
|
||||||
const isActive = marketType === type
|
const isActive = marketType === type
|
||||||
@@ -189,13 +187,13 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => handleMarketTypeChange(type)}
|
onClick={() => handleMarketTypeChange(type)}
|
||||||
className={`px-2 py-0.5 text-[10px] font-medium rounded transition-all ${
|
className={`px-2.5 py-1 text-[10px] font-medium rounded transition-all border ${isActive
|
||||||
isActive
|
? 'bg-white/10 text-white border-white/20'
|
||||||
? 'bg-[#21262D] text-white'
|
: 'text-nofx-text-muted border-transparent hover:text-nofx-text-main hover:bg-white/5'
|
||||||
: 'text-gray-500 hover:text-gray-400'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -206,87 +204,89 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
|
|
||||||
{/* Right: Symbol + Interval */}
|
{/* Right: Symbol + Interval */}
|
||||||
{activeTab === 'kline' && (
|
{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 */}
|
{/* Symbol Dropdown */}
|
||||||
{marketConfig.hasDropdown ? (
|
<div className="shrink-0 relative" ref={dropdownRef}>
|
||||||
<div className="relative" ref={dropdownRef}>
|
{marketConfig.hasDropdown ? (
|
||||||
<button
|
<>
|
||||||
onClick={() => setShowDropdown(!showDropdown)}
|
<button
|
||||||
className="flex items-center gap-1 px-2 py-1 bg-[#21262D] rounded text-[11px] font-bold text-white hover:bg-[#30363D] transition-all"
|
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-gray-400 transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
<span>{chartSymbol}</span>
|
||||||
</button>
|
<ChevronDown className={`w-3 h-3 text-nofx-text-muted transition-transform ${showDropdown ? 'rotate-180' : ''}`} />
|
||||||
{showDropdown && (
|
</button>
|
||||||
<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">
|
{showDropdown && (
|
||||||
<div className="p-2 border-b border-[#30363D]">
|
<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="flex items-center gap-2 px-2 py-1 bg-[#0D1117] rounded border border-[#30363D]">
|
<div className="p-2 border-b border-white/5">
|
||||||
<Search className="w-3 h-3 text-gray-500" />
|
<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">
|
||||||
<input
|
<Search className="w-3.5 h-3.5 text-nofx-text-muted" />
|
||||||
type="text"
|
<input
|
||||||
value={searchFilter}
|
type="text"
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
value={searchFilter}
|
||||||
placeholder="Search..."
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
className="flex-1 bg-transparent text-[11px] text-white placeholder-gray-600 focus:outline-none"
|
placeholder="Search symbol..."
|
||||||
autoFocus
|
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>
|
</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
|
<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>
|
||||||
const labels: Record<string, string> = { crypto: 'Crypto', stock: 'Stocks', forex: 'Forex', commodity: 'Commodities', index: 'Index' }
|
)}
|
||||||
return (
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Interval Selector */}
|
{/* Interval Selector - Allow scrolling if needed */}
|
||||||
<div className="flex items-center bg-[#21262D] rounded overflow-hidden">
|
<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) => (
|
{INTERVALS.map((int) => (
|
||||||
<button
|
<button
|
||||||
key={int.value}
|
key={int.value}
|
||||||
onClick={() => setInterval(int.value)}
|
onClick={() => setInterval(int.value)}
|
||||||
className={`px-2 py-1 text-[10px] font-medium transition-all ${
|
className={`px-2 py-1 text-[10px] font-medium transition-all ${interval === int.value
|
||||||
interval === int.value
|
? 'bg-nofx-gold/20 text-nofx-gold'
|
||||||
? 'bg-blue-500/30 text-blue-400'
|
: 'text-nofx-text-muted hover:text-white hover:bg-white/5'
|
||||||
: 'text-gray-500 hover:text-gray-300 hover:bg-[#30363D]'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{int.label}
|
{int.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Input */}
|
{/* Quick Input - Hidden on mobile, dropdown search is enough */}
|
||||||
<form onSubmit={handleSymbolSubmit} className="flex items-center">
|
<form onSubmit={handleSymbolSubmit} className="hidden md:flex items-center shrink-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={symbolInput}
|
value={symbolInput}
|
||||||
onChange={(e) => setSymbolInput(e.target.value)}
|
onChange={(e) => setSymbolInput(e.target.value)}
|
||||||
placeholder="Symbol..."
|
placeholder="Sym"
|
||||||
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"
|
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
|
Go
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -295,32 +295,33 @@ export function ChartTabs({ traderId, selectedSymbol, updateKey, exchangeId }: C
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* 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">
|
<AnimatePresence mode="wait">
|
||||||
{activeTab === 'equity' ? (
|
{activeTab === 'equity' ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="equity"
|
key="equity"
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, x: 20 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="h-full"
|
className="h-full w-full absolute inset-0"
|
||||||
>
|
>
|
||||||
<EquityChart traderId={traderId} embedded />
|
<EquityChart traderId={traderId} embedded />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={`kline-${chartSymbol}-${interval}-${currentExchange}`}
|
key={`kline-${chartSymbol}-${interval}-${currentExchange}`}
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, x: -20 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="h-full"
|
className="h-full w-full absolute inset-0"
|
||||||
>
|
>
|
||||||
<AdvancedChart
|
<AdvancedChart
|
||||||
symbol={chartSymbol}
|
symbol={chartSymbol}
|
||||||
interval={interval}
|
interval={interval}
|
||||||
traderID={traderId}
|
traderID={traderId}
|
||||||
|
// Dynamic height to fill container
|
||||||
height={550}
|
height={550}
|
||||||
exchange={currentExchange}
|
exchange={currentExchange}
|
||||||
onSymbolChange={setChartSymbol}
|
onSymbolChange={setChartSymbol}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getTraderColor } from '../utils/traderColors'
|
|||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { t } from '../i18n/translations'
|
import { t } from '../i18n/translations'
|
||||||
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
|
||||||
|
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||||
|
|
||||||
export function CompetitionPage() {
|
export function CompetitionPage() {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
@@ -44,83 +45,78 @@ export function CompetitionPage() {
|
|||||||
|
|
||||||
if (!competition) {
|
if (!competition) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<DeepVoidBackground className="py-8" disableAnimation>
|
||||||
<div className="binance-card p-8 animate-pulse">
|
<div className="container mx-auto max-w-7xl px-4 md:px-8">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-3 flex-1">
|
<div className="animate-pulse bg-black/40 border border-white/10 rounded-xl p-8 backdrop-blur-md">
|
||||||
<div className="skeleton h-8 w-64"></div>
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="skeleton h-4 w-48"></div>
|
<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>
|
||||||
<div className="skeleton h-12 w-32"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="binance-card p-6">
|
</DeepVoidBackground>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有数据返回但没有交易员,显示空状态
|
// 如果有数据返回但没有交易员,显示空状态
|
||||||
if (!competition.traders || competition.traders.length === 0) {
|
if (!competition.traders || competition.traders.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5 animate-fade-in">
|
<DeepVoidBackground className="py-8" disableAnimation>
|
||||||
{/* Competition Header - 精简版 */}
|
<div className="container mx-auto max-w-7xl px-4 md:px-8 space-y-8 animate-fade-in">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
{/* Competition Header - 精简版 */}
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||||
<div
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
<div
|
||||||
style={{
|
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)]"
|
||||||
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' }}
|
|
||||||
>
|
>
|
||||||
{t('aiCompetition', language)}
|
<Trophy
|
||||||
<span
|
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||||
className="text-xs font-normal px-2 py-1 rounded"
|
/>
|
||||||
style={{
|
</div>
|
||||||
background: 'rgba(240, 185, 11, 0.15)',
|
<div>
|
||||||
color: '#F0B90B',
|
<h1
|
||||||
}}
|
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||||
>
|
>
|
||||||
0 {t('traders', language)}
|
{t('aiCompetition', language)}
|
||||||
</span>
|
<span
|
||||||
</h1>
|
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
>
|
||||||
{t('liveBattle', language)}
|
0 {t('traders', language)}
|
||||||
</p>
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-zinc-400">
|
||||||
|
{t('liveBattle', language)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
<div className="binance-card p-8 text-center">
|
<div className="bg-black/40 border border-white/10 rounded-xl p-16 text-center backdrop-blur-md">
|
||||||
<Trophy
|
<Trophy
|
||||||
className="w-16 h-16 mx-auto mb-4 opacity-40"
|
className="w-16 h-16 mx-auto mb-4 text-zinc-700"
|
||||||
style={{ color: '#848E9C' }}
|
/>
|
||||||
/>
|
<h3 className="text-lg font-bold mb-2 text-white">
|
||||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>
|
{t('noTraders', language)}
|
||||||
{t('noTraders', language)}
|
</h3>
|
||||||
</h3>
|
<p className="text-sm text-zinc-400">
|
||||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
{t('createFirstTrader', language)}
|
||||||
{t('createFirstTrader', language)}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DeepVoidBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,375 +129,358 @@ export function CompetitionPage() {
|
|||||||
const leader = sortedTraders[0]
|
const leader = sortedTraders[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5 animate-fade-in">
|
<DeepVoidBackground className="py-8" disableAnimation>
|
||||||
{/* Competition Header - 精简版 */}
|
<div className="w-full px-4 md:px-8 space-y-8 animate-fade-in">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
{/* Competition Header - 精简版 */}
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||||
<div
|
<div className="flex items-center gap-3 md:gap-4">
|
||||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
<div
|
||||||
style={{
|
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)]"
|
||||||
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' }}
|
|
||||||
>
|
>
|
||||||
{t('aiCompetition', language)}
|
<Trophy
|
||||||
<span
|
className="w-6 h-6 md:w-7 md:h-7 text-nofx-gold"
|
||||||
className="text-xs font-normal px-2 py-1 rounded"
|
/>
|
||||||
style={{
|
</div>
|
||||||
background: 'rgba(240, 185, 11, 0.15)',
|
<div>
|
||||||
color: '#F0B90B',
|
<h1
|
||||||
}}
|
className="text-xl md:text-2xl font-bold flex items-center gap-2 text-white"
|
||||||
>
|
>
|
||||||
{competition.count} {t('traders', language)}
|
{t('aiCompetition', language)}
|
||||||
</span>
|
<span
|
||||||
</h1>
|
className="text-xs font-normal px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20"
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
>
|
||||||
{t('liveBattle', language)}
|
{competition.count} {t('traders', language)}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
<p className="text-xs text-zinc-400">
|
||||||
<div className="text-left md:text-right w-full md:w-auto">
|
{t('liveBattle', language)}
|
||||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
</p>
|
||||||
{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)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
|
<div className="text-left md:text-right w-full md:w-auto">
|
||||||
</div>
|
<div className="text-xs mb-1 text-zinc-400">
|
||||||
|
{t('leader', language)}
|
||||||
{/* Right: Leaderboard */}
|
</div>
|
||||||
<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
|
<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={{
|
style={{
|
||||||
background: 'rgba(240, 185, 11, 0.1)',
|
color: (leader?.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D',
|
||||||
color: '#F0B90B',
|
|
||||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('live', language)}
|
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
|
||||||
|
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
{sortedTraders.map((trader, index) => {
|
|
||||||
const isLeader = index === 0
|
|
||||||
const traderColor = getTraderColor(
|
|
||||||
sortedTraders,
|
|
||||||
trader.trader_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
{/* Left/Right Split: Performance Chart + Leaderboard */}
|
||||||
<div
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
key={trader.trader_id}
|
{/* Left: Performance Comparison Chart */}
|
||||||
onClick={() => handleTraderClick(trader.trader_id)}
|
<div
|
||||||
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
|
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={{
|
style={{ animationDelay: '0.1s' }}
|
||||||
background: isLeader
|
>
|
||||||
? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
|
<div className="flex items-center justify-between mb-6">
|
||||||
: '#0B0E11',
|
<h2
|
||||||
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
|
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||||
boxShadow: isLeader
|
>
|
||||||
? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
|
{t('performanceComparison', language)}
|
||||||
: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
</h2>
|
||||||
}}
|
<div className="text-xs text-zinc-400">
|
||||||
>
|
{t('realTimePnL', language)}
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
{/* Rank & Avatar & Name */}
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<ComparisonChart traders={sortedTraders.slice(0, 10)} />
|
||||||
{/* Rank Badge */}
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Right: Leaderboard */}
|
||||||
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
|
<div
|
||||||
{/* Total Equity */}
|
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in hover:border-white/20 transition-colors"
|
||||||
<div className="text-right">
|
style={{ animationDelay: '0.1s' }}
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
>
|
||||||
{t('equity', language)}
|
<div className="flex items-center justify-between mb-6">
|
||||||
</div>
|
<h2
|
||||||
<div
|
className="text-lg font-bold flex items-center gap-2 text-white"
|
||||||
className="text-xs md:text-sm font-bold mono"
|
>
|
||||||
style={{ color: '#EAECEF' }}
|
{t('leaderboard', language)}
|
||||||
>
|
</h2>
|
||||||
{trader.total_equity?.toFixed(2) || '0.00'}
|
<div
|
||||||
</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)]"
|
||||||
</div>
|
>
|
||||||
|
{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 */}
|
return (
|
||||||
<div className="text-right min-w-[70px] md:min-w-[90px]">
|
<div
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
key={trader.trader_id}
|
||||||
{t('pnl', language)}
|
onClick={() => handleTraderClick(trader.trader_id)}
|
||||||
</div>
|
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
|
<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={{
|
style={{
|
||||||
color:
|
background: index === 0
|
||||||
(trader.total_pnl ?? 0) >= 0
|
? 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)'
|
||||||
? '#0ECB81'
|
: index === 1
|
||||||
: '#F6465D',
|
? '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 ? '+' : ''}
|
{index + 1}
|
||||||
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
{/* Punk Avatar */}
|
||||||
className="text-xs mono"
|
<PunkAvatar
|
||||||
style={{ color: '#848E9C' }}
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
>
|
size={36}
|
||||||
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
|
className="rounded-lg"
|
||||||
{trader.total_pnl?.toFixed(2) || '0.00'}
|
/>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Positions */}
|
{/* Stats */}
|
||||||
<div className="text-right">
|
<div className="flex items-center gap-2 md:gap-3 flex-wrap md:flex-nowrap">
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
{/* Total Equity */}
|
||||||
{t('pos', language)}
|
<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>
|
||||||
<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 */}
|
{/* P&L */}
|
||||||
<div>
|
<div className="text-right min-w-[70px] md:min-w-[90px]">
|
||||||
<div
|
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||||
className="px-2 py-1 rounded text-xs font-bold"
|
{t('pnl', language)}
|
||||||
style={
|
</div>
|
||||||
trader.is_running
|
<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)',
|
background: 'rgba(14, 203, 129, 0.1)',
|
||||||
color: '#0ECB81',
|
color: '#0ECB81',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
background: 'rgba(246, 70, 93, 0.1)',
|
background: 'rgba(246, 70, 93, 0.1)',
|
||||||
color: '#F6465D',
|
color: '#F6465D',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{trader.is_running ? '●' : '○'}
|
{trader.is_running ? '●' : '○'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Head-to-Head Stats */}
|
{/* Head-to-Head Stats */}
|
||||||
{competition.traders.length === 2 && (
|
{competition.traders.length === 2 && (
|
||||||
<div
|
<div
|
||||||
className="binance-card p-5 animate-slide-in"
|
className="bg-black/40 border border-white/10 rounded-xl p-6 backdrop-blur-md animate-slide-in"
|
||||||
style={{ animationDelay: '0.3s' }}
|
style={{ animationDelay: '0.3s' }}
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="text-lg font-bold mb-4 flex items-center gap-2"
|
|
||||||
style={{ color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
{t('headToHead', language)}
|
<h2
|
||||||
</h2>
|
className="text-lg font-bold mb-6 flex items-center gap-2 text-white"
|
||||||
<div className="grid grid-cols-2 gap-4">
|
>
|
||||||
{sortedTraders.map((trader, index) => {
|
{t('headToHead', language)}
|
||||||
const isWinning = index === 0
|
</h2>
|
||||||
const opponent = sortedTraders[1 - index]
|
<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
|
// Check if both values are valid numbers
|
||||||
const hasValidData =
|
const hasValidData =
|
||||||
trader.total_pnl_pct != null &&
|
trader.total_pnl_pct != null &&
|
||||||
opponent.total_pnl_pct != null &&
|
opponent.total_pnl_pct != null &&
|
||||||
!isNaN(trader.total_pnl_pct) &&
|
!isNaN(trader.total_pnl_pct) &&
|
||||||
!isNaN(opponent.total_pnl_pct)
|
!isNaN(opponent.total_pnl_pct)
|
||||||
|
|
||||||
const gap = hasValidData
|
const gap = hasValidData
|
||||||
? trader.total_pnl_pct - opponent.total_pnl_pct
|
? trader.total_pnl_pct - opponent.total_pnl_pct
|
||||||
: NaN
|
: NaN
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={trader.trader_id}
|
key={trader.trader_id}
|
||||||
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
|
className="p-4 rounded transition-all duration-300 hover:scale-[1.02]"
|
||||||
style={
|
style={
|
||||||
isWinning
|
isWinning
|
||||||
? {
|
? {
|
||||||
background:
|
background:
|
||||||
'linear-gradient(135deg, rgba(14, 203, 129, 0.08) 0%, rgba(14, 203, 129, 0.02) 100%)',
|
'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)',
|
border: '2px solid rgba(14, 203, 129, 0.3)',
|
||||||
boxShadow: '0 3px 15px rgba(14, 203, 129, 0.12)',
|
boxShadow: '0 3px 15px rgba(14, 203, 129, 0.12)',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
background: '#0B0E11',
|
background: '#0B0E11',
|
||||||
border: '1px solid #2B3139',
|
border: '1px solid #2B3139',
|
||||||
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.3)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="flex justify-center mb-3">
|
<div className="flex justify-center mb-3">
|
||||||
<PunkAvatar
|
<PunkAvatar
|
||||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||||
size={56}
|
size={56}
|
||||||
className="rounded-xl"
|
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>
|
</div>
|
||||||
)}
|
|
||||||
{hasValidData && !isWinning && gap < 0 && (
|
|
||||||
<div
|
<div
|
||||||
className="text-xs font-semibold"
|
className="text-sm md:text-base font-bold mb-2"
|
||||||
style={{ color: '#F6465D' }}
|
style={{
|
||||||
|
color: getTraderColor(sortedTraders, trader.trader_id),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('behindBy', language, {
|
{trader.trader_name}
|
||||||
gap: Math.abs(gap).toFixed(2),
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{!hasValidData && (
|
|
||||||
<div
|
<div
|
||||||
className="text-xs font-semibold"
|
className="text-lg md:text-2xl font-bold mono mb-1"
|
||||||
style={{ color: '#848E9C' }}
|
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>
|
</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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Trader Config View Modal */}
|
{/* Trader Config View Modal */}
|
||||||
<TraderConfigViewModal
|
<TraderConfigViewModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
traderData={selectedTrader}
|
traderData={selectedTrader}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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"
|
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" />
|
<img src="/icons/nofx.svg" alt="NOFX Logo" className="w-7 h-7" />
|
||||||
<span
|
<span className="text-lg font-bold text-nofx-gold">
|
||||||
className="text-lg font-bold"
|
|
||||||
style={{ color: 'var(--brand-yellow)' }}
|
|
||||||
>
|
|
||||||
NOFX
|
NOFX
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,28 +125,12 @@ export default function HeaderBar({
|
|||||||
<button
|
<button
|
||||||
key={tab.page}
|
key={tab.page}
|
||||||
onClick={() => handleNavClick(tab)}
|
onClick={() => handleNavClick(tab)}
|
||||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
className={`text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 px-3 py-2 rounded-lg
|
||||||
style={{
|
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-nofx-gold'}`}
|
||||||
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)'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentPage === tab.page && (
|
{currentPage === tab.page && (
|
||||||
<span
|
<span
|
||||||
className="absolute inset-0 rounded-lg"
|
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -167,16 +148,7 @@ export default function HeaderBar({
|
|||||||
href={OFFICIAL_LINKS.github}
|
href={OFFICIAL_LINKS.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-white hover:bg-white/5"
|
||||||
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'
|
|
||||||
}}
|
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||||
@@ -188,16 +160,7 @@ export default function HeaderBar({
|
|||||||
href={OFFICIAL_LINKS.twitter}
|
href={OFFICIAL_LINKS.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#1DA1F2] hover:bg-[#1DA1F2]/10"
|
||||||
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'
|
|
||||||
}}
|
|
||||||
title="Twitter"
|
title="Twitter"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@@ -209,16 +172,7 @@ export default function HeaderBar({
|
|||||||
href={OFFICIAL_LINKS.telegram}
|
href={OFFICIAL_LINKS.telegram}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg transition-all hover:scale-110"
|
className="p-2 rounded-lg transition-all hover:scale-110 text-nofx-text-muted hover:text-[#0088cc] hover:bg-[#0088cc]/10"
|
||||||
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'
|
|
||||||
}}
|
|
||||||
title="Telegram"
|
title="Telegram"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<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}>
|
<div className="relative" ref={userDropdownRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
|
onClick={() => setUserDropdownOpen(!userDropdownOpen)}
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
className="flex items-center gap-2 px-3 py-2 rounded transition-colors bg-nofx-bg-lighter border border-nofx-gold/20 hover:bg-white/5"
|
||||||
style={{
|
|
||||||
background: 'var(--panel-bg)',
|
|
||||||
border: '1px solid var(--panel-border)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.background =
|
|
||||||
'rgba(255, 255, 255, 0.05)')
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.background = 'var(--panel-bg)')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
|
||||||
style={{
|
|
||||||
background: 'var(--brand-yellow)',
|
|
||||||
color: 'var(--brand-black)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user.email[0].toUpperCase()}
|
{user.email[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span className="text-sm text-nofx-text-muted">
|
||||||
className="text-sm"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
|
||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown className="w-4 h-4 text-nofx-text-muted" />
|
||||||
className="w-4 h-4"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{userDropdownOpen && (
|
{userDropdownOpen && (
|
||||||
<div
|
<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">
|
||||||
className="absolute right-0 top-full mt-2 w-48 rounded-lg shadow-lg overflow-hidden z-50"
|
<div className="px-3 py-2 border-b border-nofx-gold/20">
|
||||||
style={{
|
<div className="text-xs text-nofx-text-muted">
|
||||||
background: 'var(--brand-dark-gray)',
|
|
||||||
border: '1px solid var(--panel-border)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="px-3 py-2 border-b"
|
|
||||||
style={{ borderColor: 'var(--panel-border)' }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="text-xs"
|
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
|
||||||
>
|
|
||||||
{t('loggedInAs', language)}
|
{t('loggedInAs', language)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="text-sm font-medium text-nofx-text-muted">
|
||||||
className="text-sm font-medium"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,11 +218,7 @@ export default function HeaderBar({
|
|||||||
onLogout()
|
onLogout()
|
||||||
setUserDropdownOpen(false)
|
setUserDropdownOpen(false)
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 text-sm font-semibold transition-colors hover:opacity-80 text-center"
|
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"
|
||||||
style={{
|
|
||||||
background: 'var(--binance-red-bg)',
|
|
||||||
color: 'var(--binance-red)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('exitLogin', language)}
|
{t('exitLogin', language)}
|
||||||
</button>
|
</button>
|
||||||
@@ -322,19 +234,14 @@ export default function HeaderBar({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<a
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
className="px-3 py-2 text-sm font-medium transition-colors rounded"
|
className="px-3 py-2 text-sm font-medium transition-colors rounded text-nofx-text-muted hover:text-white"
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
>
|
||||||
{t('signIn', language)}
|
{t('signIn', language)}
|
||||||
</a>
|
</a>
|
||||||
{registrationEnabled && (
|
{registrationEnabled && (
|
||||||
<a
|
<a
|
||||||
href="/register"
|
href="/register"
|
||||||
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90"
|
className="px-4 py-2 rounded font-semibold text-sm transition-colors hover:opacity-90 bg-nofx-gold text-black"
|
||||||
style={{
|
|
||||||
background: 'var(--brand-yellow)',
|
|
||||||
color: 'var(--brand-black)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('signUp', language)}
|
{t('signUp', language)}
|
||||||
</a>
|
</a>
|
||||||
@@ -347,15 +254,7 @@ export default function HeaderBar({
|
|||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
onClick={() => setLanguageDropdownOpen(!languageDropdownOpen)}
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded transition-colors"
|
className="flex items-center gap-2 px-3 py-2 rounded transition-colors text-nofx-text-muted hover:bg-white/5"
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
onMouseEnter={(e) =>
|
|
||||||
(e.currentTarget.style.background =
|
|
||||||
'rgba(255, 255, 255, 0.05)')
|
|
||||||
}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.background = 'transparent')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="text-lg">
|
<span className="text-lg">
|
||||||
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
{language === 'zh' ? '🇨🇳' : '🇺🇸'}
|
||||||
@@ -364,28 +263,14 @@ export default function HeaderBar({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{languageDropdownOpen && (
|
{languageDropdownOpen && (
|
||||||
<div
|
<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">
|
||||||
className="absolute right-0 top-full mt-2 w-32 rounded-lg shadow-lg overflow-hidden z-50"
|
|
||||||
style={{
|
|
||||||
background: 'var(--brand-dark-gray)',
|
|
||||||
border: '1px solid var(--panel-border)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onLanguageChange?.('zh')
|
onLanguageChange?.('zh')
|
||||||
setLanguageDropdownOpen(false)
|
setLanguageDropdownOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||||
language === 'zh' ? '' : 'hover:opacity-80'
|
${language === 'zh' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
color: 'var(--brand-light-gray)',
|
|
||||||
background:
|
|
||||||
language === 'zh'
|
|
||||||
? 'rgba(240, 185, 11, 0.1)'
|
|
||||||
: 'transparent',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="text-base">🇨🇳</span>
|
<span className="text-base">🇨🇳</span>
|
||||||
<span className="text-sm">中文</span>
|
<span className="text-sm">中文</span>
|
||||||
@@ -395,16 +280,8 @@ export default function HeaderBar({
|
|||||||
onLanguageChange?.('en')
|
onLanguageChange?.('en')
|
||||||
setLanguageDropdownOpen(false)
|
setLanguageDropdownOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors ${
|
className={`w-full flex items-center gap-2 px-3 py-2 transition-colors text-nofx-text-muted hover:text-white
|
||||||
language === 'en' ? '' : 'hover:opacity-80'
|
${language === 'en' ? 'bg-nofx-gold/10' : 'hover:bg-white/5'}`}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
color: 'var(--brand-light-gray)',
|
|
||||||
background:
|
|
||||||
language === 'en'
|
|
||||||
? 'rgba(240, 185, 11, 0.1)'
|
|
||||||
: 'transparent',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="text-base">🇺🇸</span>
|
<span className="text-base">🇺🇸</span>
|
||||||
<span className="text-sm">English</span>
|
<span className="text-sm">English</span>
|
||||||
@@ -418,8 +295,7 @@ export default function HeaderBar({
|
|||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
className="md:hidden"
|
className="md:hidden text-nofx-text-muted hover:text-white"
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
>
|
>
|
||||||
{mobileMenuOpen ? (
|
{mobileMenuOpen ? (
|
||||||
@@ -439,11 +315,7 @@ export default function HeaderBar({
|
|||||||
: { height: 0, opacity: 0 }
|
: { height: 0, opacity: 0 }
|
||||||
}
|
}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
className="md:hidden overflow-hidden"
|
className="md:hidden overflow-hidden bg-nofx-bg-lighter border-t border-nofx-gold/10"
|
||||||
style={{
|
|
||||||
background: 'var(--brand-dark-gray)',
|
|
||||||
borderTop: '1px solid rgba(240, 185, 11, 0.1)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="px-4 py-4 space-y-2">
|
<div className="px-4 py-4 space-y-2">
|
||||||
{/* Mobile Navigation Tabs - Show all tabs */}
|
{/* Mobile Navigation Tabs - Show all tabs */}
|
||||||
@@ -476,25 +348,17 @@ export default function HeaderBar({
|
|||||||
<button
|
<button
|
||||||
key={tab.page}
|
key={tab.page}
|
||||||
onClick={() => handleMobileNavClick(tab)}
|
onClick={() => handleMobileNavClick(tab)}
|
||||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
className={`block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 w-full text-left px-4 py-3 rounded-lg
|
||||||
style={{
|
${currentPage === tab.page ? 'text-nofx-gold' : 'text-nofx-text-muted hover:text-white hover:bg-white/5'}`}
|
||||||
color: currentPage === tab.page ? 'var(--brand-yellow)' : 'var(--brand-light-gray)',
|
|
||||||
padding: '12px 16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'left',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentPage === tab.page && (
|
{currentPage === tab.page && (
|
||||||
<span
|
<span
|
||||||
className="absolute inset-0 rounded-lg"
|
className="absolute inset-0 rounded-lg bg-nofx-gold/15 -z-10"
|
||||||
style={{ background: 'rgba(240, 185, 11, 0.15)', zIndex: -1 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab.label}
|
{tab.label}
|
||||||
{tab.requiresAuth && !isLoggedIn && (
|
{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'}
|
{language === 'zh' ? '需登录' : 'Login'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -511,21 +375,19 @@ export default function HeaderBar({
|
|||||||
<a
|
<a
|
||||||
key={item.key}
|
key={item.key}
|
||||||
href={`#${item.key === 'features' ? 'features' : 'how-it-works'}`}
|
href={`#${item.key === 'features' ? 'features' : 'how-it-works'}`}
|
||||||
className="block text-sm py-2"
|
className="block text-sm py-2 text-nofx-text-muted hover:text-white"
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Social Links - Mobile */}
|
{/* 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
|
<a
|
||||||
href={OFFICIAL_LINKS.github}
|
href={OFFICIAL_LINKS.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg"
|
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-white"
|
||||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
<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" />
|
<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}
|
href={OFFICIAL_LINKS.twitter}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg"
|
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#1DA1F2]"
|
||||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
<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" />
|
<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}
|
href={OFFICIAL_LINKS.telegram}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-2 rounded-lg"
|
className="p-2 rounded-lg text-nofx-text-muted bg-white/5 hover:text-[#0088cc]"
|
||||||
style={{ color: '#848E9C', background: 'rgba(255, 255, 255, 0.05)' }}
|
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
<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" />
|
<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 */}
|
{/* Language Toggle */}
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span
|
<span className="text-xs text-nofx-text-muted">
|
||||||
className="text-xs"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
|
||||||
{t('language', language)}:
|
{t('language', language)}:
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -571,11 +428,10 @@ export default function HeaderBar({
|
|||||||
onLanguageChange?.('zh')
|
onLanguageChange?.('zh')
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${language === 'zh'
|
||||||
language === 'zh'
|
? 'bg-yellow-500 text-black'
|
||||||
? 'bg-yellow-500 text-black'
|
: 'text-gray-400 hover:text-white'
|
||||||
: 'text-gray-400 hover:text-white'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="text-lg">🇨🇳</span>
|
<span className="text-lg">🇨🇳</span>
|
||||||
<span className="text-sm">中文</span>
|
<span className="text-sm">中文</span>
|
||||||
@@ -585,11 +441,10 @@ export default function HeaderBar({
|
|||||||
onLanguageChange?.('en')
|
onLanguageChange?.('en')
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${
|
className={`w-full flex items-center gap-3 px-3 py-2 rounded transition-colors ${language === 'en'
|
||||||
language === 'en'
|
? 'bg-yellow-500 text-black'
|
||||||
? 'bg-yellow-500 text-black'
|
: 'text-gray-400 hover:text-white'
|
||||||
: 'text-gray-400 hover:text-white'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="text-lg">🇺🇸</span>
|
<span className="text-lg">🇺🇸</span>
|
||||||
<span className="text-sm">English</span>
|
<span className="text-sm">English</span>
|
||||||
@@ -600,33 +455,17 @@ export default function HeaderBar({
|
|||||||
{/* User info and logout for mobile when logged in */}
|
{/* User info and logout for mobile when logged in */}
|
||||||
{isLoggedIn && user && (
|
{isLoggedIn && user && (
|
||||||
<div
|
<div
|
||||||
className="mt-4 pt-4"
|
className="mt-4 pt-4 border-t border-nofx-gold/20"
|
||||||
style={{ borderTop: '1px solid var(--panel-border)' }}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-2 px-3 py-2 mb-2 rounded bg-nofx-bg-lighter">
|
||||||
className="flex items-center gap-2 px-3 py-2 mb-2 rounded"
|
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold bg-nofx-gold text-black">
|
||||||
style={{ background: 'var(--panel-bg)' }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
|
||||||
style={{
|
|
||||||
background: 'var(--brand-yellow)',
|
|
||||||
color: 'var(--brand-black)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user.email[0].toUpperCase()}
|
{user.email[0].toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div className="text-xs text-nofx-text-muted">
|
||||||
className="text-xs"
|
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
|
||||||
>
|
|
||||||
{t('loggedInAs', language)}
|
{t('loggedInAs', language)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="text-sm text-nofx-text-muted">
|
||||||
className="text-sm"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
|
||||||
{user.email}
|
{user.email}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -637,11 +476,7 @@ export default function HeaderBar({
|
|||||||
onLogout()
|
onLogout()
|
||||||
setMobileMenuOpen(false)
|
setMobileMenuOpen(false)
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center"
|
className="w-full px-4 py-2 rounded text-sm font-semibold transition-colors text-center bg-nofx-danger/20 text-nofx-danger"
|
||||||
style={{
|
|
||||||
background: 'var(--binance-red-bg)',
|
|
||||||
color: 'var(--binance-red)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('exitLogin', language)}
|
{t('exitLogin', language)}
|
||||||
</button>
|
</button>
|
||||||
@@ -656,11 +491,7 @@ export default function HeaderBar({
|
|||||||
<div className="space-y-2 mt-2">
|
<div className="space-y-2 mt-2">
|
||||||
<a
|
<a
|
||||||
href="/login"
|
href="/login"
|
||||||
className="block w-full px-4 py-2 rounded text-sm font-medium text-center transition-colors"
|
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"
|
||||||
style={{
|
|
||||||
color: 'var(--brand-light-gray)',
|
|
||||||
border: '1px solid var(--brand-light-gray)',
|
|
||||||
}}
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{t('signIn', language)}
|
{t('signIn', language)}
|
||||||
@@ -668,11 +499,7 @@ export default function HeaderBar({
|
|||||||
{registrationEnabled && (
|
{registrationEnabled && (
|
||||||
<a
|
<a
|
||||||
href="/register"
|
href="/register"
|
||||||
className="block w-full px-4 py-2 rounded font-semibold text-sm text-center transition-colors"
|
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"
|
||||||
style={{
|
|
||||||
background: 'var(--brand-yellow)',
|
|
||||||
color: 'var(--brand-black)',
|
|
||||||
}}
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{t('signUp', language)}
|
{t('signUp', language)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useAuth } from '../contexts/AuthContext'
|
|||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { t } from '../i18n/translations'
|
import { t } from '../i18n/translations'
|
||||||
import { Eye, EyeOff } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||||
// import { Input } from './ui/input' // Removed unused import
|
// import { Input } from './ui/input' // Removed unused import
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||||
@@ -102,13 +103,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
<div className="w-full max-w-md relative z-10 px-6">
|
<div className="w-full max-w-md relative z-10 px-6">
|
||||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||||
@@ -361,6 +356,6 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DeepVoidBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react'
|
||||||
|
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
|
|
||||||
interface LoginRequiredOverlayProps {
|
interface LoginRequiredOverlayProps {
|
||||||
@@ -51,111 +52,114 @@ export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequ
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm"
|
className="fixed inset-0 z-50"
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
{/* Scanline Effect */}
|
<DeepVoidBackground
|
||||||
<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>
|
className="w-full h-full bg-nofx-bg/95 backdrop-blur-md flex items-center justify-center p-4 text-nofx-text"
|
||||||
|
disableAnimation
|
||||||
<motion.div
|
onClick={onClose}
|
||||||
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()}
|
|
||||||
>
|
>
|
||||||
{/* Terminal Window Header */}
|
|
||||||
<div className="flex items-center justify-between px-3 py-2 bg-zinc-900 border-b border-zinc-800">
|
<motion.div
|
||||||
<div className="flex items-center gap-2">
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||||
<Terminal size={12} className="text-nofx-gold" />
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
<span className="text-[10px] text-zinc-500 uppercase tracking-wider">auth_protocol.exe</span>
|
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>
|
</div>
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-zinc-600 hover:text-red-500 transition-colors"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="p-8 relative">
|
<div className="p-8 relative">
|
||||||
{/* Background Grid */}
|
{/* 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="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">
|
<div className="relative z-10">
|
||||||
{/* Flashing Access Denied */}
|
{/* Flashing Access Denied */}
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 bg-red-500/20 blur-xl animate-pulse"></div>
|
<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)]">
|
<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" />
|
<AlertTriangle size={18} className="animate-pulse" />
|
||||||
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
<span className="font-bold tracking-widest text-sm uppercase">{language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Terminal Text */}
|
{/* Terminal Text */}
|
||||||
<div className="space-y-4 mb-8">
|
<div className="space-y-4 mb-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-xl font-bold text-white uppercase tracking-wider mb-2">{t.title}</h2>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div className="bg-zinc-900/50 border-l-2 border-zinc-700 p-3 my-4">
|
{/* Action Buttons */}
|
||||||
<p className="text-xs text-zinc-400 leading-relaxed font-mono">
|
<div className="space-y-3">
|
||||||
<span className="text-green-500 mr-2">$</span>
|
<a
|
||||||
{t.description}
|
href="/login"
|
||||||
</p>
|
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>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="mt-4 text-center">
|
||||||
{t.benefits.map((benefit, i) => (
|
<button
|
||||||
<div key={i} className="flex items-center gap-2 text-[10px] text-zinc-500 uppercase tracking-wide">
|
onClick={onClose}
|
||||||
<span className="text-nofx-gold">✓</span> {benefit}
|
className="text-[10px] text-nofx-text-muted hover:text-nofx-danger uppercase tracking-widest hover:underline decoration-red-500/30"
|
||||||
</div>
|
>
|
||||||
))}
|
[ {t.later} ]
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Corner Accents */}
|
{/* Corner Accents */}
|
||||||
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-nofx-gold"></div>
|
<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>
|
<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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getSystemConfig } from '../lib/config'
|
|||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { copyWithToast } from '../lib/clipboard'
|
import { copyWithToast } from '../lib/clipboard'
|
||||||
import { Eye, EyeOff } from 'lucide-react'
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { DeepVoidBackground } from './DeepVoidBackground'
|
||||||
// import { Input } from './ui/input' // Removed unused import
|
// import { Input } from './ui/input' // Removed unused import
|
||||||
import PasswordChecklist from 'react-password-checklist'
|
import PasswordChecklist from 'react-password-checklist'
|
||||||
import { RegistrationDisabled } from './RegistrationDisabled'
|
import { RegistrationDisabled } from './RegistrationDisabled'
|
||||||
@@ -148,13 +149,7 @@ export function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-zinc-300 font-mono relative overflow-hidden flex items-center justify-center py-12">
|
<DeepVoidBackground className="min-h-screen flex items-center justify-center py-12 font-mono" disableAnimation>
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
<div className="w-full max-w-lg relative z-10 px-6">
|
<div className="w-full max-w-lg relative z-10 px-6">
|
||||||
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
{/* Navigation - Top Bar (Mobile/Desktop Friendly) */}
|
||||||
@@ -469,6 +464,6 @@ export function RegisterPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DeepVoidBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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-[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 bg-gradient-to-t from-black via-transparent to-transparent pointer-events-none"></div>
|
||||||
|
|||||||
@@ -56,14 +56,11 @@ export function FAQContent({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category.id}>
|
<div key={category.id} className="nofx-glass p-8 rounded-xl border border-white/5">
|
||||||
{/* Category Header */}
|
{/* Category Header */}
|
||||||
<div
|
<div className="flex items-center gap-3 mb-6 pb-3 border-b border-white/10">
|
||||||
className="flex items-center gap-3 mb-6 pb-3"
|
<category.icon className="w-7 h-7 text-nofx-gold" />
|
||||||
style={{ borderBottom: '2px solid #2B3139' }}
|
<h2 className="text-2xl font-bold text-nofx-text-main">
|
||||||
>
|
|
||||||
<category.icon className="w-7 h-7" style={{ color: '#F0B90B' }} />
|
|
||||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
|
||||||
{t(category.titleKey, language)}
|
{t(category.titleKey, language)}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,21 +76,12 @@ export function FAQContent({
|
|||||||
className="scroll-mt-24"
|
className="scroll-mt-24"
|
||||||
>
|
>
|
||||||
{/* Question */}
|
{/* Question */}
|
||||||
<h3
|
<h3 className="text-xl font-semibold mb-3 text-nofx-text-main">
|
||||||
className="text-xl font-semibold mb-3"
|
|
||||||
style={{ color: '#EAECEF' }}
|
|
||||||
>
|
|
||||||
{t(item.questionKey, language)}
|
{t(item.questionKey, language)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Answer */}
|
{/* Answer */}
|
||||||
<div
|
<div className="prose prose-invert max-w-none text-nofx-text-muted leading-relaxed">
|
||||||
className="prose prose-invert max-w-none"
|
|
||||||
style={{
|
|
||||||
color: '#B7BDC6',
|
|
||||||
lineHeight: '1.7',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.id === 'github-projects-tasks' ? (
|
{item.id === 'github-projects-tasks' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-base">
|
<div className="text-base">
|
||||||
@@ -295,7 +283,7 @@ export function FAQContent({
|
|||||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ color: '#F0B90B' }}
|
className="text-nofx-gold hover:underline"
|
||||||
>
|
>
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
</a>
|
</a>
|
||||||
@@ -304,7 +292,7 @@ export function FAQContent({
|
|||||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ color: '#F0B90B' }}
|
className="text-nofx-gold hover:underline"
|
||||||
>
|
>
|
||||||
PR_TITLE_GUIDE.md
|
PR_TITLE_GUIDE.md
|
||||||
</a>
|
</a>
|
||||||
@@ -383,16 +371,10 @@ export function FAQContent({
|
|||||||
)}
|
)}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div
|
<div className="rounded p-3 mt-3 bg-nofx-gold/10 border border-nofx-gold/25">
|
||||||
className="rounded p-3 mt-3"
|
|
||||||
style={{
|
|
||||||
background: 'rgba(240, 185, 11, 0.08)',
|
|
||||||
border: '1px solid rgba(240, 185, 11, 0.25)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language === 'zh' ? (
|
{language === 'zh' ? (
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<strong style={{ color: '#F0B90B' }}>提示:</strong>{' '}
|
<strong className="text-nofx-gold">Note:</strong>{' '}
|
||||||
我们为高质量贡献提供激励(Bounty/奖金、荣誉徽章与鸣谢、优先
|
我们为高质量贡献提供激励(Bounty/奖金、荣誉徽章与鸣谢、优先
|
||||||
Review/合并与内测资格 等)。 详情可关注带
|
Review/合并与内测资格 等)。 详情可关注带
|
||||||
<a
|
<a
|
||||||
@@ -448,7 +430,7 @@ export function FAQContent({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="mt-6 h-px" style={{ background: '#2B3139' }} />
|
<div className="mt-6 h-px bg-white/5" />
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { HelpCircle } from 'lucide-react'
|
import { HelpCircle } from 'lucide-react'
|
||||||
import { Container } from '../Container'
|
import { DeepVoidBackground } from '../DeepVoidBackground'
|
||||||
import { t, type Language } from '../../i18n/translations'
|
import { t, type Language } from '../../i18n/translations'
|
||||||
import { FAQSearchBar } from './FAQSearchBar'
|
import { FAQSearchBar } from './FAQSearchBar'
|
||||||
import { FAQSidebar } from './FAQSidebar'
|
import { FAQSidebar } from './FAQSidebar'
|
||||||
@@ -58,125 +58,121 @@ export function FAQLayout({ language }: FAQLayoutProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="py-6 pt-24">
|
<DeepVoidBackground className="py-6 pt-24" disableAnimation>
|
||||||
{/* Page Header */}
|
<div className="w-full px-4 md:px-8">
|
||||||
<div className="text-center mb-12">
|
{/* Page Header */}
|
||||||
<div className="flex items-center justify-center gap-3 mb-4">
|
<div className="text-center mb-12">
|
||||||
<div
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
className="w-16 h-16 rounded-full flex items-center justify-center"
|
<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)]">
|
||||||
style={{
|
<HelpCircle className="w-8 h-8 text-[#0B0E11]" />
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
</div>
|
||||||
boxShadow: '0 8px 24px rgba(240, 185, 11, 0.4)',
|
</div>
|
||||||
}}
|
<h1 className="text-4xl font-bold mb-4 text-nofx-text-main">
|
||||||
>
|
{t('faqTitle', language)}
|
||||||
<HelpCircle className="w-8 h-8" style={{ color: '#0B0E11' }} />
|
</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>
|
||||||
</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 */}
|
{/* Main Content */}
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="flex gap-8">
|
||||||
<FAQSearchBar
|
{/* Sidebar - Hidden on mobile, visible on desktop */}
|
||||||
searchTerm={searchTerm}
|
<aside className="hidden lg:block w-64 flex-shrink-0">
|
||||||
onSearchChange={setSearchTerm}
|
<FAQSidebar
|
||||||
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
|
|
||||||
categories={filteredCategories}
|
categories={filteredCategories}
|
||||||
|
activeItemId={activeItemId}
|
||||||
language={language}
|
language={language}
|
||||||
onActiveItemChange={setActiveItemId}
|
onItemClick={handleItemClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
</aside>
|
||||||
<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 */}
|
{/* Content Area */}
|
||||||
<div
|
<main className="flex-1 min-w-0">
|
||||||
className="mt-16 p-8 rounded-lg text-center"
|
{filteredCategories.length > 0 ? (
|
||||||
style={{
|
<FAQContent
|
||||||
background:
|
categories={filteredCategories}
|
||||||
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
|
language={language}
|
||||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
onActiveItemChange={setActiveItemId}
|
||||||
}}
|
/>
|
||||||
>
|
) : (
|
||||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
<div className="text-center py-12">
|
||||||
{t('faqStillHaveQuestions', language)}
|
<p className="text-lg" style={{ color: '#848E9C' }}>
|
||||||
</h3>
|
{language === 'zh'
|
||||||
<p className="mb-6" style={{ color: '#848E9C' }}>
|
? '没有找到匹配的问题'
|
||||||
{t('faqContactUs', language)}
|
: 'No matching questions found'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center gap-4">
|
<button
|
||||||
<a
|
onClick={() => setSearchTerm('')}
|
||||||
href="https://github.com/NoFxAiOS/nofx"
|
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
|
||||||
target="_blank"
|
style={{
|
||||||
rel="noopener noreferrer"
|
background:
|
||||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||||
style={{
|
color: '#0B0E11',
|
||||||
background: '#1E2329',
|
}}
|
||||||
color: '#EAECEF',
|
>
|
||||||
border: '1px solid #2B3139',
|
{language === 'zh' ? '清除搜索' : 'Clear Search'}
|
||||||
}}
|
</button>
|
||||||
>
|
</div>
|
||||||
GitHub
|
)}
|
||||||
</a>
|
</main>
|
||||||
<a
|
</div>
|
||||||
href="https://t.me/nofx_dev_community"
|
|
||||||
target="_blank"
|
{/* Contact Section */}
|
||||||
rel="noopener noreferrer"
|
<div
|
||||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
className="mt-16 p-8 rounded-lg text-center"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
background:
|
||||||
color: '#0B0E11',
|
'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)',
|
||||||
>
|
}}
|
||||||
{t('community', language)}
|
>
|
||||||
</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</DeepVoidBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,36 +12,21 @@ export function FAQSearchBar({
|
|||||||
placeholder = 'Search FAQ...',
|
placeholder = 'Search FAQ...',
|
||||||
}: FAQSearchBarProps) {
|
}: FAQSearchBarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative group">
|
||||||
<Search
|
<Search
|
||||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5"
|
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"
|
||||||
style={{ color: '#848E9C' }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none focus:ring-2"
|
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"
|
||||||
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'
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onSearchChange('')}
|
onClick={() => onSearchChange('')}
|
||||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 hover:opacity-70 transition-opacity"
|
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-nofx-text-muted hover:text-white transition-colors"
|
||||||
style={{ color: '#848E9C' }}
|
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -24,14 +24,11 @@ export function FAQSidebar({
|
|||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category.id}>
|
<div key={category.id} className="nofx-glass p-4 rounded-xl border border-white/5">
|
||||||
{/* Category Title */}
|
{/* Category Title */}
|
||||||
<div className="flex items-center gap-2 mb-3 px-3">
|
<div className="flex items-center gap-2 mb-3 px-3">
|
||||||
<category.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
<category.icon className="w-5 h-5 text-nofx-gold" />
|
||||||
<h3
|
<h3 className="text-sm font-bold uppercase tracking-wide text-nofx-gold">
|
||||||
className="text-sm font-bold uppercase tracking-wide"
|
|
||||||
style={{ color: '#F0B90B' }}
|
|
||||||
>
|
|
||||||
{t(category.titleKey, language)}
|
{t(category.titleKey, language)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,30 +41,10 @@ export function FAQSidebar({
|
|||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => onItemClick(category.id, item.id)}
|
onClick={() => onItemClick(category.id, item.id)}
|
||||||
className="w-full text-left px-3 py-2 rounded-lg text-sm transition-all"
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-all border-l-[3px] ${isActive
|
||||||
style={{
|
? 'bg-nofx-gold/10 text-nofx-gold border-nofx-gold pl-[9px]'
|
||||||
background: isActive
|
: 'bg-transparent text-nofx-text-muted border-transparent pl-3 hover:bg-nofx-gold/5 hover:text-nofx-text-main'
|
||||||
? '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'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t(item.questionKey, language)}
|
{t(item.questionKey, language)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -143,12 +143,7 @@ export function CoinSourceEditor({
|
|||||||
// NofxOS badge component
|
// NofxOS badge component
|
||||||
const NofxOSBadge = () => (
|
const NofxOSBadge = () => (
|
||||||
<span
|
<span
|
||||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium"
|
className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-purple-500/20 text-purple-400 border border-purple-500/30"
|
||||||
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)'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
NofxOS
|
NofxOS
|
||||||
</span>
|
</span>
|
||||||
@@ -158,7 +153,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Source Type Selector */}
|
{/* Source Type Selector */}
|
||||||
<div>
|
<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')}
|
{t('sourceType')}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-3">
|
||||||
@@ -170,24 +165,16 @@ export function CoinSourceEditor({
|
|||||||
onChange({ ...config, source_type: value as CoinSourceConfig['source_type'] })
|
onChange({ ...config, source_type: value as CoinSourceConfig['source_type'] })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`p-4 rounded-lg border transition-all ${
|
className={`p-4 rounded-lg border transition-all ${config.source_type === value
|
||||||
config.source_type === value
|
? 'ring-2 ring-nofx-gold bg-nofx-gold/10'
|
||||||
? 'ring-2 ring-yellow-500'
|
: 'hover:bg-white/5 bg-nofx-bg'
|
||||||
: 'hover:bg-white/5'
|
} border-nofx-gold/20`}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
config.source_type === value
|
|
||||||
? 'rgba(240, 185, 11, 0.1)'
|
|
||||||
: '#0B0E11',
|
|
||||||
borderColor: '#2B3139',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon className="w-6 h-6 mx-auto mb-2" style={{ color }} />
|
<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)}
|
{t(value)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs mt-1 text-nofx-text-muted">
|
||||||
{t(`${value}Desc`)}
|
{t(`${value}Desc`)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -198,15 +185,14 @@ export function CoinSourceEditor({
|
|||||||
{/* Static Coins */}
|
{/* Static Coins */}
|
||||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
||||||
<div>
|
<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')}
|
{t('staticCoins')}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
{(config.static_coins || []).map((coin) => (
|
{(config.static_coins || []).map((coin) => (
|
||||||
<span
|
<span
|
||||||
key={coin}
|
key={coin}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-bg-lighter text-nofx-text"
|
||||||
style={{ background: '#2B3139', color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
{coin}
|
{coin}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
@@ -228,17 +214,11 @@ export function CoinSourceEditor({
|
|||||||
onChange={(e) => setNewCoin(e.target.value)}
|
onChange={(e) => setNewCoin(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleAddCoin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleAddCoin()}
|
||||||
placeholder="BTC, ETH, SOL..."
|
placeholder="BTC, ETH, SOL..."
|
||||||
className="flex-1 px-4 py-2 rounded-lg"
|
className="flex-1 px-4 py-2 rounded-lg bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{
|
|
||||||
background: '#0B0E11',
|
|
||||||
border: '1px solid #2B3139',
|
|
||||||
color: '#EAECEF',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddCoin}
|
onClick={handleAddCoin}
|
||||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors bg-nofx-gold text-black hover:bg-yellow-500"
|
||||||
style={{ background: '#F0B90B', color: '#0B0E11' }}
|
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
{t('addCoin')}
|
{t('addCoin')}
|
||||||
@@ -251,20 +231,19 @@ export function CoinSourceEditor({
|
|||||||
{/* Excluded Coins */}
|
{/* Excluded Coins */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Ban className="w-4 h-4" style={{ color: '#F6465D' }} />
|
<Ban className="w-4 h-4 text-nofx-danger" />
|
||||||
<label className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<label className="text-sm font-medium text-nofx-text">
|
||||||
{t('excludedCoins')}
|
{t('excludedCoins')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>
|
<p className="text-xs mb-3 text-nofx-text-muted">
|
||||||
{t('excludedCoinsDesc')}
|
{t('excludedCoinsDesc')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
{(config.excluded_coins || []).map((coin) => (
|
{(config.excluded_coins || []).map((coin) => (
|
||||||
<span
|
<span
|
||||||
key={coin}
|
key={coin}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm bg-nofx-danger/15 text-nofx-danger"
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}
|
|
||||||
>
|
>
|
||||||
{coin}
|
{coin}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
@@ -278,7 +257,7 @@ export function CoinSourceEditor({
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{(config.excluded_coins || []).length === 0 && (
|
{(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'}
|
{language === 'zh' ? '无' : 'None'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -291,17 +270,11 @@ export function CoinSourceEditor({
|
|||||||
onChange={(e) => setNewExcludedCoin(e.target.value)}
|
onChange={(e) => setNewExcludedCoin(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
|
||||||
placeholder="BTC, ETH, DOGE..."
|
placeholder="BTC, ETH, DOGE..."
|
||||||
className="flex-1 px-4 py-2 rounded-lg text-sm"
|
className="flex-1 px-4 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{
|
|
||||||
background: '#0B0E11',
|
|
||||||
border: '1px solid #2B3139',
|
|
||||||
color: '#EAECEF',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddExcludedCoin}
|
onClick={handleAddExcludedCoin}
|
||||||
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm"
|
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"
|
||||||
style={{ background: '#F6465D', color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
<Ban className="w-4 h-4" />
|
<Ban className="w-4 h-4" />
|
||||||
{t('addExcludedCoin')}
|
{t('addExcludedCoin')}
|
||||||
@@ -313,16 +286,12 @@ export function CoinSourceEditor({
|
|||||||
{/* AI500 Options */}
|
{/* AI500 Options */}
|
||||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg"
|
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||||
style={{
|
|
||||||
background: 'rgba(240, 185, 11, 0.05)',
|
|
||||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
<Zap className="w-4 h-4 text-nofx-gold" />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
AI500 {t('dataSourceConfig')}
|
AI500 {t('dataSourceConfig')}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
@@ -338,14 +307,14 @@ export function CoinSourceEditor({
|
|||||||
!disabled && onChange({ ...config, use_ai500: e.target.checked })
|
!disabled && onChange({ ...config, use_ai500: e.target.checked })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
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>
|
</label>
|
||||||
|
|
||||||
{config.use_ai500 && (
|
{config.use_ai500 && (
|
||||||
<div className="flex items-center gap-3 pl-8">
|
<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')}:
|
{t('ai500Limit')}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
@@ -355,12 +324,7 @@ export function CoinSourceEditor({
|
|||||||
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="px-3 py-1.5 rounded"
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{
|
|
||||||
background: '#0B0E11',
|
|
||||||
border: '1px solid #2B3139',
|
|
||||||
color: '#EAECEF',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
<option key={n} value={n}>{n}</option>
|
<option key={n} value={n}>{n}</option>
|
||||||
@@ -369,7 +333,7 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
{t('nofxosNote')}
|
{t('nofxosNote')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,16 +343,12 @@ export function CoinSourceEditor({
|
|||||||
{/* OI Top Options */}
|
{/* OI Top Options */}
|
||||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg"
|
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
||||||
style={{
|
|
||||||
background: 'rgba(14, 203, 129, 0.05)',
|
|
||||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
OI Top {t('dataSourceConfig')}
|
OI Top {t('dataSourceConfig')}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
@@ -404,14 +364,14 @@ export function CoinSourceEditor({
|
|||||||
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
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>
|
</label>
|
||||||
|
|
||||||
{config.use_oi_top && (
|
{config.use_oi_top && (
|
||||||
<div className="flex items-center gap-3 pl-8">
|
<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')}:
|
{t('oiTopLimit')}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
@@ -421,12 +381,7 @@ export function CoinSourceEditor({
|
|||||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="px-3 py-1.5 rounded"
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{
|
|
||||||
background: '#0B0E11',
|
|
||||||
border: '1px solid #2B3139',
|
|
||||||
color: '#EAECEF',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
<option key={n} value={n}>{n}</option>
|
<option key={n} value={n}>{n}</option>
|
||||||
@@ -435,7 +390,7 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
{t('nofxosNote')}
|
{t('nofxosNote')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -439,11 +439,15 @@ button:disabled {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Premium Input Styles */
|
||||||
/* Premium Input Styles */
|
/* Premium Input Styles */
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
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,
|
input:focus,
|
||||||
@@ -452,6 +456,35 @@ textarea:focus {
|
|||||||
border-color: var(--binance-yellow);
|
border-color: var(--binance-yellow);
|
||||||
box-shadow: 0 0 0 2px rgba(252, 213, 53, 0.15);
|
box-shadow: 0 0 0 2px rgba(252, 213, 53, 0.15);
|
||||||
outline: none;
|
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 */
|
/* Binance Card - Premium Polish */
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
const T: Record<string, Record<string, string>> = {
|
const T: Record<string, Record<string, string>> = {
|
||||||
@@ -144,7 +145,7 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}` }}
|
style={{ borderLeft: `3px solid ${p.color}` }}
|
||||||
>
|
>
|
||||||
{/* Header - Always visible */}
|
{/* Header - Always visible */}
|
||||||
@@ -153,16 +154,16 @@ function MessageCard({ msg }: { msg: DebateMessage }) {
|
|||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
>
|
>
|
||||||
<AIAvatar name={msg.ai_model_name} size={24} />
|
<AIAvatar name={msg.ai_model_name} size={24} />
|
||||||
<span className="text-sm text-white font-medium">{msg.ai_model_name}</span>
|
<span className="text-sm text-nofx-text font-medium">{msg.ai_model_name}</span>
|
||||||
<span className="text-xs text-gray-500">{p.nameEn}</span>
|
<span className="text-xs text-nofx-text-muted">{p.nameEn}</span>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{msg.decision && (
|
{msg.decision && (
|
||||||
<span className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded ${a.bg} ${a.color}`}>
|
<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}
|
{a.icon} {msg.decision.symbol || ''} {a.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-yellow-400 font-medium">{msg.decision?.confidence || msg.confidence}%</span>
|
<span className="text-xs text-nofx-gold font-medium">{msg.decision?.confidence || msg.confidence}%</span>
|
||||||
{open ? <ChevronUp size={14} className="text-gray-500" /> : <ChevronDown size={14} className="text-gray-500" />}
|
{open ? <ChevronUp size={14} className="text-nofx-text-muted" /> : <ChevronDown size={14} className="text-nofx-text-muted" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview when collapsed */}
|
{/* 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 a = ACT[vote.action] || ACT.wait
|
||||||
const confColor = vote.confidence >= 70 ? 'bg-green-500' : vote.confidence >= 50 ? 'bg-yellow-500' : 'bg-gray-500'
|
const confColor = vote.confidence >= 70 ? 'bg-green-500' : vote.confidence >= 50 ? 'bg-yellow-500' : 'bg-gray-500'
|
||||||
return (
|
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 justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AIAvatar name={vote.ai_model_name} size={28} />
|
<AIAvatar name={vote.ai_model_name} size={28} />
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white font-semibold block">{vote.ai_model_name}</span>
|
<span className="text-nofx-text font-semibold block">{vote.ai_model_name}</span>
|
||||||
{vote.symbol && <span className="text-xs text-gray-400">{vote.symbol}</span>}
|
{vote.symbol && <span className="text-xs text-nofx-text-muted">{vote.symbol}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-bold ${a.bg} ${a.color}`}>
|
<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>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
<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-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-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-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-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-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-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">TP</span><span className="text-green-400 font-semibold">{vote.take_profit_pct ? `${(vote.take_profit_pct * 100).toFixed(1)}%` : '-'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
{vote.reasoning && (
|
{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>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -386,22 +387,22 @@ function CreateModal({
|
|||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
<div className="bg-[#1a1d24] rounded-xl w-full max-w-md p-4 border border-white/10">
|
<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">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-bold text-white">{t('createDebate', language)}</h3>
|
<h3 className="text-lg font-bold text-nofx-text">{t('createDebate', language)}</h3>
|
||||||
<button onClick={onClose}><X size={20} className="text-gray-400" /></button>
|
<button onClick={onClose}><X size={20} className="text-nofx-text-muted" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<input
|
<input
|
||||||
value={name} onChange={e => setName(e.target.value)}
|
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 */}
|
{/* Strategy selector - moved up */}
|
||||||
<select value={strategyId} onChange={e => setStrategyId(e.target.value)}
|
<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>)}
|
{strategies.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@@ -409,17 +410,17 @@ function CreateModal({
|
|||||||
{/* Show dropdown only for static type with coins defined */}
|
{/* Show dropdown only for static type with coins defined */}
|
||||||
{isStaticWithCoins ? (
|
{isStaticWithCoins ? (
|
||||||
<select value={symbol} onChange={e => setSymbol(e.target.value)}
|
<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>)}
|
{staticCoins.map(coin => <option key={coin} value={coin}>{coin}</option>)}
|
||||||
</select>
|
</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'}
|
{language === 'zh' ? '根据策略规则自动选择' : 'Auto-selected by strategy'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<select value={maxRounds} onChange={e => setMaxRounds(+e.target.value)}
|
<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">
|
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>)}
|
{[2, 3, 4, 5].map(n => <option key={n} value={n}>{n} {language === 'zh' ? '轮' : 'rounds'}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -431,7 +432,7 @@ function CreateModal({
|
|||||||
{/* Personality selector */}
|
{/* Personality selector */}
|
||||||
<select value={p.personality} onChange={e => {
|
<select value={p.personality} onChange={e => {
|
||||||
const up = [...participants]; up[i].personality = e.target.value as DebatePersonality; setParticipants(up)
|
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]) => (
|
{Object.entries(PERS).map(([k, v]) => (
|
||||||
<option key={k} value={k}>{v.emoji} {language === 'zh' ? v.name : v.nameEn}</option>
|
<option key={k} value={k}>{v.emoji} {language === 'zh' ? v.name : v.nameEn}</option>
|
||||||
))}
|
))}
|
||||||
@@ -439,23 +440,23 @@ function CreateModal({
|
|||||||
{/* AI model selector */}
|
{/* AI model selector */}
|
||||||
<select value={p.ai_model_id} onChange={e => {
|
<select value={p.ai_model_id} onChange={e => {
|
||||||
const up = [...participants]; up[i].ai_model_id = e.target.value; setParticipants(up)
|
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>)}
|
{aiModels.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button onClick={() => setParticipants(participants.filter((_, j) => j !== i))}
|
<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>
|
</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)}
|
+ {t('addAI', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-4">
|
<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}
|
<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)}
|
{creating ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('create', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>)
|
const voteSum = votes.reduce((a, v) => { a[v.action] = (a[v.action] || 0) + 1; return a }, {} as Record<string, number>)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full bg-[#0a0c10] flex overflow-hidden">
|
<DeepVoidBackground className="h-full flex overflow-hidden relative" disableAnimation>
|
||||||
|
|
||||||
{/* Left - Debate List + Online Traders */}
|
{/* 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 */}
|
{/* New Debate Button */}
|
||||||
<button onClick={() => setShowCreate(true)}
|
<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)}
|
<Plus size={16} /> {t('newDebate', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Debate List */}
|
{/* 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%' }}>
|
<div className="overflow-y-auto" style={{ maxHeight: '30%' }}>
|
||||||
{debates?.map(d => (
|
{debates?.map(d => (
|
||||||
<div key={d.id} onClick={() => setSelectedId(d.id)}
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`w-2 h-2 rounded-full ${STATUS_COLOR[d.status]}`} />
|
<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>
|
||||||
<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 && (
|
{d.status === 'pending' && selectedId === d.id && (
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<button onClick={e => { e.stopPropagation(); onStart(d.id) }}
|
<button onClick={e => { e.stopPropagation(); onStart(d.id) }}
|
||||||
@@ -569,41 +571,41 @@ export function DebateArenaPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Online Traders Section */}
|
{/* Online Traders Section */}
|
||||||
<div className="flex-1 border-t border-white/5 mt-2 overflow-hidden flex flex-col">
|
<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-gray-500 font-semibold flex items-center gap-1">
|
<div className="px-2 py-2 text-xs text-nofx-text-muted font-semibold flex items-center gap-1">
|
||||||
<Zap size={12} className="text-green-400" />
|
<Zap size={12} className="text-nofx-success" />
|
||||||
{t('onlineTraders', language)}
|
{t('onlineTraders', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-2 space-y-2">
|
<div className="flex-1 overflow-y-auto px-2 space-y-2">
|
||||||
{traders?.filter(tr => tr.is_running).map(tr => (
|
{traders?.filter(tr => tr.is_running).map(tr => (
|
||||||
<div key={tr.trader_id}
|
<div key={tr.trader_id}
|
||||||
onClick={() => { setTraderId(tr.trader_id); if (decision && !decision.executed) setExecId(detail?.id || null) }}
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
|
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
|
||||||
<div className="text-xs text-gray-500 truncate">{tr.ai_model}</div>
|
<div className="text-xs text-nofx-text-muted truncate">{tr.ai_model}</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{traders?.filter(tr => !tr.is_running).slice(0, 3).map(tr => (
|
{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="flex items-center gap-2">
|
||||||
<div className="grayscale">
|
<div className="grayscale">
|
||||||
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
<PunkAvatar seed={tr.trader_id} size={32} className="rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm text-white font-medium truncate">{tr.trader_name}</div>
|
<div className="text-sm text-nofx-text font-medium truncate">{tr.trader_name}</div>
|
||||||
<div className="text-xs text-gray-500">{t('offline', language)}</div>
|
<div className="text-xs text-nofx-text-muted">{t('offline', language)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(!traders || traders.length === 0) && (
|
{(!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>
|
||||||
</div>
|
</div>
|
||||||
@@ -614,12 +616,12 @@ export function DebateArenaPage() {
|
|||||||
{detail ? (
|
{detail ? (
|
||||||
<>
|
<>
|
||||||
{/* Header Bar - Compact */}
|
{/* 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={`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="font-bold text-nofx-text truncate">{detail.name}</span>
|
||||||
<span className="text-yellow-400 font-semibold">{detail.symbol}</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>}
|
{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 */}
|
{/* Participants */}
|
||||||
<div className="flex gap-1 ml-2">
|
<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 vote = votes.find(v => v.ai_model_id === p.ai_model_id)
|
||||||
const act = vote ? (ACT[vote.action] || ACT.wait) : null
|
const act = vote ? (ACT[vote.action] || ACT.wait) : null
|
||||||
return (
|
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} />
|
<AIAvatar name={p.ai_model_name} size={14} />
|
||||||
{act && <span className={`${act.color}`}>{act.icon}</span>}
|
{act && <span className={`${act.color}`}>{act.icon}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -662,8 +664,8 @@ export function DebateArenaPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Left - Rounds */}
|
{/* Left - Rounds */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 border-r border-white/5">
|
<div className="flex-1 overflow-y-auto p-4 border-r border-nofx-gold/20">
|
||||||
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
|
<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>
|
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
|
||||||
{t('discussionRecords', language)}
|
{t('discussionRecords', language)}
|
||||||
</div>
|
</div>
|
||||||
@@ -681,9 +683,9 @@ export function DebateArenaPage() {
|
|||||||
|
|
||||||
{/* Right - Votes */}
|
{/* Right - Votes */}
|
||||||
{votes.length > 0 && (
|
{votes.length > 0 && (
|
||||||
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-[#0d1017]/50">
|
<div className="w-[420px] flex-shrink-0 overflow-y-auto p-4 bg-nofx-bg/30 backdrop-blur-sm">
|
||||||
<div className="text-sm text-gray-400 font-semibold mb-3 flex items-center gap-2">
|
<div className="text-sm text-nofx-text-muted font-semibold mb-3 flex items-center gap-2">
|
||||||
<Trophy size={16} className="text-yellow-400" />
|
<Trophy size={16} className="text-nofx-gold" />
|
||||||
{t('finalVotes', language)}
|
{t('finalVotes', language)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -709,37 +711,37 @@ export function DebateArenaPage() {
|
|||||||
|
|
||||||
{/* Consensus Bar - Show when votes exist */}
|
{/* Consensus Bar - Show when votes exist */}
|
||||||
{(decision || votes.length > 0) && (
|
{(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">
|
<div className="flex items-center gap-2">
|
||||||
<Trophy size={20} className="text-yellow-400" />
|
<Trophy size={20} className="text-nofx-gold" />
|
||||||
<span className="text-sm text-gray-400">{t('consensus', language)}:</span>
|
<span className="text-sm text-nofx-text-muted">{t('consensus', language)}:</span>
|
||||||
{decision ? (
|
{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}`}>
|
<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}
|
{(ACT[decision.action] || ACT.wait).icon}
|
||||||
{decision.action.replace('_', ' ').toUpperCase()}
|
{decision.action.replace('_', ' ').toUpperCase()}
|
||||||
</span>
|
</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...
|
<Clock size={14} /> VOTING...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{decision && (
|
{decision && (
|
||||||
<div className="flex items-center gap-4 text-sm">
|
<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>
|
<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-gray-500">{t('leverage', language)}</span> <span className="text-white font-bold">{decision.leverage}x</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-gray-500">{t('position', language)}</span> <span className="text-white font-bold">{((decision.position_pct ?? 0) * 100).toFixed(0)}%</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-gray-500">SL</span> <span className="text-red-400 font-bold">{((decision.stop_loss ?? 0) * 100).toFixed(1)}%</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-gray-500">TP</span> <span className="text-green-400 font-bold">{((decision.take_profit ?? 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>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && (
|
{decision && !decision.executed && (decision.action === 'open_long' || decision.action === 'open_short') && (
|
||||||
<button onClick={() => setExecId(detail.id)}
|
<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)}
|
<Zap size={14} /> {t('execute', language)}
|
||||||
</button>
|
</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-center">
|
||||||
<div className="text-4xl mb-2">🗳️</div>
|
<div className="text-4xl mb-2">🗳️</div>
|
||||||
<div>{t('selectOrCreate', language)}</div>
|
<div>{t('selectOrCreate', language)}</div>
|
||||||
@@ -763,13 +765,13 @@ export function DebateArenaPage() {
|
|||||||
|
|
||||||
{/* Execute Modal */}
|
{/* Execute Modal */}
|
||||||
{execId && (
|
{execId && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
<div className="bg-[#1a1d24] rounded-xl w-full max-w-sm p-4 border border-white/10">
|
<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-white mb-4 flex items-center gap-2">
|
<h3 className="text-lg font-bold text-nofx-text mb-4 flex items-center gap-2">
|
||||||
<Zap className="text-yellow-400" /> {t('executeTitle', language)}
|
<Zap className="text-nofx-gold" /> {t('executeTitle', language)}
|
||||||
</h3>
|
</h3>
|
||||||
<select value={traderId} onChange={e => setTraderId(e.target.value)}
|
<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>
|
<option value="">{t('selectTrader', language)}...</option>
|
||||||
{traders?.filter(tr => tr.is_running).map(tr => (
|
{traders?.filter(tr => tr.is_running).map(tr => (
|
||||||
<option key={tr.trader_id} value={tr.trader_id}>✅ {tr.trader_name}</option>
|
<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>
|
<option key={tr.trader_id} value={tr.trader_id} disabled>⏹ {tr.trader_name} ({t('offline', language)})</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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'}
|
⚠️ {language === 'zh' ? '将使用账户余额执行真实交易' : 'Will execute real trade with account balance'}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button onClick={() => { setExecId(null); setTraderId('') }}
|
<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}
|
<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)}
|
{executing ? <Loader2 size={16} className="animate-spin mx-auto" /> : t('execute', language)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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'
|
} from 'lucide-react'
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
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 {
|
interface PublicStrategy {
|
||||||
id: string
|
id: string
|
||||||
@@ -225,291 +226,289 @@ export function StrategyMarketPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-white font-mono relative overflow-hidden flex flex-col items-center py-12">
|
<DeepVoidBackground className="min-h-screen text-white font-mono py-12">
|
||||||
{/* Background Grid & Scanlines */}
|
<div className="w-full px-4 md:px-8 space-y-8">
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="w-full max-w-7xl relative z-10 px-6">
|
<div className="w-full relative z-10">
|
||||||
|
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
|
<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">
|
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
|
||||||
SYSTEM_STATUS: <span className="text-emerald-500 animate-pulse">ONLINE</span>
|
SYSTEM_STATUS: <span className="text-emerald-500 animate-pulse">ONLINE</span>
|
||||||
<br />
|
<br />
|
||||||
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
|
MARKET_UPLINK: <span className="text-emerald-500">ESTABLISHED</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<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>
|
||||||
<div>
|
|
||||||
<h1 className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text" data-text={t.title}>
|
<div className="flex items-center gap-4 mb-4">
|
||||||
{t.title}
|
<div className="bg-zinc-900 border border-zinc-700 p-3 rounded-none relative group overflow-hidden">
|
||||||
</h1>
|
<div className="absolute inset-0 bg-nofx-gold/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||||
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
|
<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}
|
// {t.subtitle}
|
||||||
</p>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
|
||||||
|
{t.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category Filter */}
|
{/* Search and Filter Bar */}
|
||||||
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
|
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
||||||
{['all', 'popular', 'recent'].map((cat) => (
|
{/* Search */}
|
||||||
<button
|
<div className="relative flex-1 group">
|
||||||
key={cat}
|
<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>
|
||||||
onClick={() => setSelectedCategory(cat)}
|
<div className="relative bg-black flex items-center border border-zinc-800 group-hover:border-nofx-gold/50 transition-colors">
|
||||||
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${selectedCategory === cat
|
<div className="pl-4 pr-3 text-zinc-500 group-hover:text-nofx-gold transition-colors">
|
||||||
? 'text-black font-bold'
|
<Terminal size={16} />
|
||||||
: 'text-zinc-500 hover:text-white'
|
</div>
|
||||||
}`}
|
<input
|
||||||
>
|
type="text"
|
||||||
{selectedCategory === cat && (
|
placeholder={t.search}
|
||||||
<motion.div
|
value={searchQuery}
|
||||||
layoutId="filter-highlight"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="absolute inset-0 bg-nofx-gold"
|
className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono"
|
||||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
/>
|
||||||
/>
|
<div className="pr-4">
|
||||||
)}
|
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
|
||||||
<span className="relative z-10">{t[cat as keyof typeof t]}</span>
|
</div>
|
||||||
</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" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">{t.loading}</p>
|
|
||||||
<div className="flex gap-1">
|
{/* Category Filter */}
|
||||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0s' }}></div>
|
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
|
||||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
{['all', 'popular', 'recent'].map((cat) => (
|
||||||
<div className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Empty State */}
|
{/* Loading State */}
|
||||||
{!isLoading && filteredStrategies.length === 0 && (
|
{isLoading && (
|
||||||
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
|
<div className="flex flex-col items-center justify-center py-32 space-y-4">
|
||||||
<div className="relative mb-6">
|
<div className="relative w-16 h-16">
|
||||||
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
|
<div className="absolute inset-0 border-2 border-zinc-800 rounded-full"></div>
|
||||||
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
<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>
|
</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 */}
|
{/* Empty State */}
|
||||||
{!isLoading && filteredStrategies.length > 0 && (
|
{!isLoading && filteredStrategies.length === 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
|
||||||
<AnimatePresence>
|
<div className="relative mb-6">
|
||||||
{filteredStrategies.map((strategy, i) => {
|
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
|
||||||
const style = getStrategyStyle(strategy.name)
|
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
|
||||||
const Icon = style.icon
|
</div>
|
||||||
const indicators = strategy.config_visible && strategy.config
|
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
|
||||||
? getIndicatorList(strategy.config)
|
[{t.noStrategies}]
|
||||||
: []
|
</h3>
|
||||||
|
<p className="text-zinc-600 text-xs tracking-wide uppercase">{t.noStrategiesDesc}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
{/* Strategy Grid */}
|
||||||
<motion.div
|
{!isLoading && filteredStrategies.length > 0 && (
|
||||||
key={strategy.id}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
<AnimatePresence>
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
{filteredStrategies.map((strategy, i) => {
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
const style = getStrategyStyle(strategy.name)
|
||||||
transition={{ delay: i * 0.05 }}
|
const Icon = style.icon
|
||||||
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
|
const indicators = strategy.config_visible && strategy.config
|
||||||
>
|
? getIndicatorList(strategy.config)
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* Category Side Strip */}
|
return (
|
||||||
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
|
<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">
|
{/* Category Side Strip */}
|
||||||
{/* Header */}
|
<div className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}></div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Name and Description */}
|
<div className="p-6 relative">
|
||||||
<h3 className={`text-lg font-bold mb-2 tracking-tight group-hover:${style.color} transition-colors uppercase truncate relative`}>
|
{/* Header */}
|
||||||
{strategy.name}
|
<div className="flex justify-between items-start mb-6">
|
||||||
<span className="absolute -bottom-1 left-0 w-8 h-[2px] bg-zinc-800 group-hover:bg-nofx-gold transition-colors"></span>
|
<div className={`p-2 rounded-none border ${style.border} ${style.bg}`}>
|
||||||
</h3>
|
<Icon className={`w-5 h-5 ${style.color}`} />
|
||||||
<p className="text-xs text-zinc-500 mb-6 line-clamp-2 h-8 leading-relaxed font-sans">
|
</div>
|
||||||
{strategy.description || 'NO_DESCRIPTION_AVAILABLE'}
|
<div className="text-[10px] font-mono">
|
||||||
</p>
|
{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">
|
||||||
{/* Meta Data */}
|
<Eye size={10} />
|
||||||
<div className="grid grid-cols-2 gap-y-2 mb-6 text-[10px] font-mono text-zinc-600">
|
PUBLIC_ACCESS
|
||||||
<div className="flex flex-col">
|
</div>
|
||||||
<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 className="flex items-center gap-1.5 text-zinc-500 border border-zinc-800 bg-zinc-900 px-2 py-1">
|
||||||
</div>
|
<EyeOff size={10} />
|
||||||
<div className="flex flex-col text-right">
|
RESTRICTED
|
||||||
<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>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center h-full text-zinc-600">
|
|
||||||
<EyeOff size={16} className="mb-1 opacity-50" />
|
{/* Name and Description */}
|
||||||
<span className="text-[9px] uppercase tracking-widest">{t.configHiddenDesc}</span>
|
<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>
|
||||||
)}
|
<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>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* CTA - Share Strategy */}
|
||||||
<div>
|
{user && token && (
|
||||||
{strategy.config_visible && strategy.config ? (
|
<motion.div
|
||||||
<button
|
initial={{ opacity: 0, y: 20 }}
|
||||||
onClick={() => handleCopyConfig(strategy)}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
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"
|
transition={{ delay: 0.3 }}
|
||||||
>
|
className="mt-16 mb-20 flex justify-center"
|
||||||
{copiedId === strategy.id ? (
|
>
|
||||||
<>
|
<div className="relative group cursor-pointer" onClick={() => window.location.href = '/strategy'}>
|
||||||
<Check className="w-3 h-3 text-emerald-500" />
|
<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>
|
||||||
<span className="text-emerald-500">{t.copied}</span>
|
<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>
|
||||||
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
|
<div className="text-[10px] text-zinc-500 font-mono">CONTRIBUTE TO THE GLOBAL DATABASE</div>
|
||||||
{t.copyConfig}
|
</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">
|
||||||
</button>
|
INITIALIZE_UPLOAD ->
|
||||||
) : (
|
</div>
|
||||||
<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 ->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</motion.div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DeepVoidBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { IndicatorEditor } from '../components/strategy/IndicatorEditor'
|
|||||||
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
import { RiskControlEditor } from '../components/strategy/RiskControlEditor'
|
||||||
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor'
|
||||||
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
import { PublishSettingsEditor } from '../components/strategy/PublishSettingsEditor'
|
||||||
|
import { DeepVoidBackground } from '../components/DeepVoidBackground'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
const API_BASE = import.meta.env.VITE_API_BASE || ''
|
||||||
|
|
||||||
@@ -635,21 +636,23 @@ export function StrategyStudioPage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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 justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<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" />
|
<Sparkles className="w-5 h-5 text-black" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('strategyStudio')}</h1>
|
<h1 className="text-lg font-bold text-nofx-text">{t('strategyStudio')}</h1>
|
||||||
<p className="text-xs" style={{ color: '#848E9C' }}>{t('subtitle')}</p>
|
<p className="text-xs text-nofx-text-muted">{t('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
<button onClick={() => setError(null)} className="hover:underline">×</button>
|
<button onClick={() => setError(null)} className="hover:underline">×</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -660,13 +663,13 @@ export function StrategyStudioPage() {
|
|||||||
{/* Main Content - Three Columns */}
|
{/* Main Content - Three Columns */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* Left Column - Strategy List */}
|
{/* 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="p-2">
|
||||||
<div className="flex items-center justify-between mb-2 px-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">
|
<div className="flex items-center gap-1">
|
||||||
{/* Import button with hidden file input */}
|
{/* 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" />
|
<Upload className="w-4 h-4" />
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -677,8 +680,7 @@ export function StrategyStudioPage() {
|
|||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateStrategy}
|
onClick={handleCreateStrategy}
|
||||||
className="p-1 rounded hover:bg-white/10 transition-colors"
|
className="p-1 rounded hover:bg-white/10 transition-colors text-nofx-gold"
|
||||||
style={{ color: '#F0B90B' }}
|
|
||||||
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
title={language === 'zh' ? '新建策略' : 'New Strategy'}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@@ -696,38 +698,36 @@ export function StrategyStudioPage() {
|
|||||||
setPromptPreview(null)
|
setPromptPreview(null)
|
||||||
setAiTestResult(null)
|
setAiTestResult(null)
|
||||||
}}
|
}}
|
||||||
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${
|
className={`group px-2 py-2 rounded-lg cursor-pointer transition-all ${selectedStrategy?.id === strategy.id
|
||||||
selectedStrategy?.id === strategy.id ? 'ring-1 ring-yellow-500/50' : 'hover:bg-white/5'
|
? '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'
|
||||||
style={{
|
}`}
|
||||||
background: selectedStrategy?.id === strategy.id ? 'rgba(240, 185, 11, 0.1)' : 'transparent',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleExportStrategy(strategy) }}
|
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'}
|
title={language === 'zh' ? '导出' : 'Export'}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" style={{ color: '#848E9C' }} />
|
<Download className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
{!strategy.is_default && (
|
{!strategy.is_default && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDuplicateStrategy(strategy.id) }}
|
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'}
|
title={language === 'zh' ? '复制' : 'Duplicate'}
|
||||||
>
|
>
|
||||||
<Copy className="w-3 h-3" style={{ color: '#848E9C' }} />
|
<Copy className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteStrategy(strategy.id) }}
|
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'}
|
title={language === 'zh' ? '删除' : 'Delete'}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" style={{ color: '#F6465D' }} />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -735,17 +735,17 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||||
{strategy.is_active && (
|
{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')}
|
{t('active')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{strategy.is_default && (
|
{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')}
|
{t('default')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{strategy.is_public && (
|
{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" />
|
<Globe className="w-2.5 h-2.5" />
|
||||||
{language === 'zh' ? '公开' : 'Public'}
|
{language === 'zh' ? '公开' : 'Public'}
|
||||||
</span>
|
</span>
|
||||||
@@ -758,7 +758,7 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Column - Config Editor */}
|
{/* 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 ? (
|
{selectedStrategy && editingConfig ? (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* Strategy Name & Actions */}
|
{/* Strategy Name & Actions */}
|
||||||
@@ -772,19 +772,17 @@ export function StrategyStudioPage() {
|
|||||||
setHasChanges(true)
|
setHasChanges(true)
|
||||||
}}
|
}}
|
||||||
disabled={selectedStrategy.is_default}
|
disabled={selectedStrategy.is_default}
|
||||||
className="text-lg font-bold bg-transparent border-none outline-none w-full"
|
className="text-lg font-bold bg-transparent border-none outline-none w-full text-nofx-text placeholder-nofx-text-muted"
|
||||||
style={{ color: '#EAECEF' }}
|
|
||||||
/>
|
/>
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<span className="text-xs" style={{ color: '#F0B90B' }}>● 未保存</span>
|
<span className="text-xs text-nofx-gold">● 未保存</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{!selectedStrategy.is_active && (
|
{!selectedStrategy.is_active && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleActivateStrategy(selectedStrategy.id)}
|
onClick={() => handleActivateStrategy(selectedStrategy.id)}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs transition-colors"
|
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"
|
||||||
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)', color: '#0ECB81' }}
|
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
{t('activate')}
|
{t('activate')}
|
||||||
@@ -794,11 +792,8 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSaveStrategy}
|
onClick={handleSaveStrategy}
|
||||||
disabled={isSaving || !hasChanges}
|
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"
|
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors disabled:opacity-50
|
||||||
style={{
|
${hasChanges ? 'bg-nofx-gold text-black hover:bg-yellow-500' : 'bg-nofx-bg-lighter text-nofx-text-muted cursor-not-allowed'}`}
|
||||||
background: hasChanges ? '#F0B90B' : '#2B3139',
|
|
||||||
color: hasChanges ? '#0B0E11' : '#848E9C',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Save className="w-3 h-3" />
|
<Save className="w-3 h-3" />
|
||||||
{isSaving ? t('saving') : t('save')}
|
{isSaving ? t('saving') : t('save')}
|
||||||
@@ -812,8 +807,7 @@ export function StrategyStudioPage() {
|
|||||||
{configSections.map(({ key, icon: Icon, color, title, content }) => (
|
{configSections.map(({ key, icon: Icon, color, title, content }) => (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="rounded-lg overflow-hidden"
|
className="rounded-lg overflow-hidden bg-nofx-bg-lighter border border-nofx-gold/20"
|
||||||
style={{ background: '#1E2329', border: '1px solid #2B3139' }}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSection(key)}
|
onClick={() => toggleSection(key)}
|
||||||
@@ -821,12 +815,12 @@ export function StrategyStudioPage() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon className="w-4 h-4" style={{ color }} />
|
<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>
|
</div>
|
||||||
{expandedSections[key] ? (
|
{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>
|
</button>
|
||||||
{expandedSections[key] && (
|
{expandedSections[key] && (
|
||||||
@@ -841,8 +835,8 @@ export function StrategyStudioPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30" style={{ color: '#848E9C' }} />
|
<Activity className="w-12 h-12 mx-auto mb-2 opacity-30 text-nofx-text-muted" />
|
||||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
<p className="text-sm text-nofx-text-muted">
|
||||||
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
|
{language === 'zh' ? '选择或创建策略' : 'Select or create a strategy'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -853,29 +847,19 @@ export function StrategyStudioPage() {
|
|||||||
{/* Right Column - Prompt Preview & AI Test */}
|
{/* Right Column - Prompt Preview & AI Test */}
|
||||||
<div className="w-[420px] flex-shrink-0 flex flex-col overflow-hidden">
|
<div className="w-[420px] flex-shrink-0 flex flex-col overflow-hidden">
|
||||||
{/* Tabs */}
|
{/* 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
|
<button
|
||||||
onClick={() => setActiveRightTab('prompt')}
|
onClick={() => setActiveRightTab('prompt')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
|
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'
|
||||||
activeRightTab === 'prompt' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
|
}`}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
borderColor: activeRightTab === 'prompt' ? '#a855f7' : 'transparent',
|
|
||||||
color: activeRightTab === 'prompt' ? '#a855f7' : '#848E9C',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
{t('promptPreview')}
|
{t('promptPreview')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveRightTab('test')}
|
onClick={() => setActiveRightTab('test')}
|
||||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
|
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'
|
||||||
activeRightTab === 'test' ? 'border-b-2' : 'opacity-60 hover:opacity-100'
|
}`}
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
borderColor: activeRightTab === 'test' ? '#22c55e' : 'transparent',
|
|
||||||
color: activeRightTab === 'test' ? '#22c55e' : '#848E9C',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4" />
|
<Play className="w-4 h-4" />
|
||||||
{t('aiTestRun')}
|
{t('aiTestRun')}
|
||||||
@@ -892,8 +876,7 @@ export function StrategyStudioPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedVariant}
|
value={selectedVariant}
|
||||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||||
className="px-2 py-1.5 rounded text-xs"
|
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"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
<option value="balanced">{t('balanced')}</option>
|
<option value="balanced">{t('balanced')}</option>
|
||||||
<option value="aggressive">{t('aggressive')}</option>
|
<option value="aggressive">{t('aggressive')}</option>
|
||||||
@@ -902,8 +885,7 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={fetchPromptPreview}
|
onClick={fetchPromptPreview}
|
||||||
disabled={isLoadingPrompt || !editingConfig}
|
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"
|
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"
|
||||||
style={{ background: '#a855f7', color: '#fff' }}
|
|
||||||
>
|
>
|
||||||
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
{isLoadingPrompt ? <Loader2 className="w-3 h-3 animate-spin" /> : <RefreshCw className="w-3 h-3" />}
|
||||||
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
|
{promptPreview ? t('refreshPrompt') : t('loadPrompt')}
|
||||||
@@ -913,16 +895,16 @@ export function StrategyStudioPage() {
|
|||||||
{promptPreview ? (
|
{promptPreview ? (
|
||||||
<>
|
<>
|
||||||
{/* Config Summary */}
|
{/* 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">
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
<Code className="w-3 h-3" style={{ color: '#a855f7' }} />
|
<Code className="w-3 h-3 text-purple-500" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#a855f7' }}>Config</span>
|
<span className="text-xs font-medium text-purple-500">Config</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
{Object.entries(promptPreview.config_summary || {}).map(([key, value]) => (
|
{Object.entries(promptPreview.config_summary || {}).map(([key, value]) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<div style={{ color: '#848E9C' }}>{key.replace(/_/g, ' ')}</div>
|
<div className="text-nofx-text-muted">{key.replace(/_/g, ' ')}</div>
|
||||||
<div style={{ color: '#EAECEF' }}>{String(value)}</div>
|
<div className="text-nofx-text">{String(value)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -932,23 +914,23 @@ export function StrategyStudioPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<FileText className="w-3 h-3" style={{ color: '#a855f7' }} />
|
<FileText className="w-3 h-3 text-purple-500" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('systemPrompt')}</span>
|
<span className="text-xs font-medium text-nofx-text">{t('systemPrompt')}</span>
|
||||||
</div>
|
</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
|
{promptPreview.system_prompt.length.toLocaleString()} chars
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[11px] font-mono overflow-auto"
|
className="p-2 rounded-lg text-[11px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '400px' }}
|
style={{ maxHeight: '400px' }}
|
||||||
>
|
>
|
||||||
{promptPreview.system_prompt}
|
{promptPreview.system_prompt}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</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" />
|
<Eye className="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
|
<p className="text-sm">{language === 'zh' ? '点击生成 Prompt 预览' : 'Click to generate prompt preview'}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -960,15 +942,14 @@ export function StrategyStudioPage() {
|
|||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Bot className="w-4 h-4" style={{ color: '#22c55e' }} />
|
<Bot className="w-4 h-4 text-green-500" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('selectModel')}</span>
|
<span className="text-xs font-medium text-nofx-text">{t('selectModel')}</span>
|
||||||
</div>
|
</div>
|
||||||
{aiModels.length > 0 ? (
|
{aiModels.length > 0 ? (
|
||||||
<select
|
<select
|
||||||
value={selectedModelId}
|
value={selectedModelId}
|
||||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-lg text-sm"
|
className="w-full px-3 py-2 rounded-lg text-sm bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
{aiModels.map((model) => (
|
{aiModels.map((model) => (
|
||||||
<option key={model.id} value={model.id}>
|
<option key={model.id} value={model.id}>
|
||||||
@@ -977,7 +958,7 @@ export function StrategyStudioPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</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')}
|
{t('noModel')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -986,8 +967,7 @@ export function StrategyStudioPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedVariant}
|
value={selectedVariant}
|
||||||
onChange={(e) => setSelectedVariant(e.target.value)}
|
onChange={(e) => setSelectedVariant(e.target.value)}
|
||||||
className="px-2 py-1.5 rounded text-xs"
|
className="px-2 py-1.5 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
|
||||||
>
|
>
|
||||||
<option value="balanced">{t('balanced')}</option>
|
<option value="balanced">{t('balanced')}</option>
|
||||||
<option value="aggressive">{t('aggressive')}</option>
|
<option value="aggressive">{t('aggressive')}</option>
|
||||||
@@ -996,12 +976,7 @@ export function StrategyStudioPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={runAiTest}
|
onClick={runAiTest}
|
||||||
disabled={isRunningAiTest || !editingConfig || !selectedModelId}
|
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"
|
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"
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, #22c55e 0%, #4ade80 100%)',
|
|
||||||
color: '#fff',
|
|
||||||
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isRunningAiTest ? (
|
{isRunningAiTest ? (
|
||||||
<>
|
<>
|
||||||
@@ -1016,22 +991,22 @@ export function StrategyStudioPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px]" style={{ color: '#848E9C' }}>{t('testNote')}</p>
|
<p className="text-[10px] text-nofx-text-muted">{t('testNote')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Test Results */}
|
{/* Test Results */}
|
||||||
{aiTestResult ? (
|
{aiTestResult ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{aiTestResult.error ? (
|
{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)' }}>
|
<div className="p-3 rounded-lg bg-nofx-danger/10 border border-nofx-danger/30">
|
||||||
<p className="text-sm" style={{ color: '#F6465D' }}>{aiTestResult.error}</p>
|
<p className="text-sm text-nofx-danger">{aiTestResult.error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{aiTestResult.duration_ms && (
|
{aiTestResult.duration_ms && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-3 h-3" style={{ color: '#848E9C' }} />
|
<Clock className="w-3 h-3 text-nofx-text-muted" />
|
||||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
<span className="text-xs text-nofx-text-muted">
|
||||||
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
{t('duration')}: {(aiTestResult.duration_ms / 1000).toFixed(2)}s
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1041,12 +1016,12 @@ export function StrategyStudioPage() {
|
|||||||
{aiTestResult.user_prompt && (
|
{aiTestResult.user_prompt && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Terminal className="w-3 h-3" style={{ color: '#60a5fa' }} />
|
<Terminal className="w-3 h-3 text-blue-400" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('userPrompt')} (Input)</span>
|
<span className="text-xs font-medium text-nofx-text">{t('userPrompt')} (Input)</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '200px' }}
|
style={{ maxHeight: '200px' }}
|
||||||
>
|
>
|
||||||
{aiTestResult.user_prompt}
|
{aiTestResult.user_prompt}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -1057,12 +1032,12 @@ export function StrategyStudioPage() {
|
|||||||
{aiTestResult.reasoning && (
|
{aiTestResult.reasoning && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Sparkles className="w-3 h-3" style={{ color: '#F0B90B' }} />
|
<Sparkles className="w-3 h-3 text-nofx-gold" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('reasoning')}</span>
|
<span className="text-xs font-medium text-nofx-text">{t('reasoning')}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
|
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={{ background: '#0B0E11', border: '1px solid rgba(240, 185, 11, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
|
style={{ maxHeight: '200px' }}
|
||||||
>
|
>
|
||||||
{aiTestResult.reasoning}
|
{aiTestResult.reasoning}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -1073,12 +1048,12 @@ export function StrategyStudioPage() {
|
|||||||
{aiTestResult.decisions && aiTestResult.decisions.length > 0 && (
|
{aiTestResult.decisions && aiTestResult.decisions.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<Activity className="w-3 h-3" style={{ color: '#22c55e' }} />
|
<Activity className="w-3 h-3 text-green-500" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('decisions')}</span>
|
<span className="text-xs font-medium text-nofx-text">{t('decisions')}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto"
|
className="p-2 rounded-lg text-[10px] font-mono overflow-auto bg-nofx-bg border border-green-500/30 text-nofx-text"
|
||||||
style={{ background: '#0B0E11', border: '1px solid rgba(34, 197, 94, 0.3)', color: '#EAECEF', maxHeight: '200px' }}
|
style={{ maxHeight: '200px' }}
|
||||||
>
|
>
|
||||||
{JSON.stringify(aiTestResult.decisions, null, 2)}
|
{JSON.stringify(aiTestResult.decisions, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -1089,12 +1064,12 @@ export function StrategyStudioPage() {
|
|||||||
{aiTestResult.ai_response && (
|
{aiTestResult.ai_response && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<FileText className="w-3 h-3" style={{ color: '#848E9C' }} />
|
<FileText className="w-3 h-3 text-nofx-text-muted" />
|
||||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('aiOutput')} (Raw)</span>
|
<span className="text-xs font-medium text-nofx-text">{t('aiOutput')} (Raw)</span>
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
className="p-2 rounded-lg text-[10px] font-mono overflow-auto whitespace-pre-wrap"
|
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={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', maxHeight: '300px' }}
|
style={{ maxHeight: '300px' }}
|
||||||
>
|
>
|
||||||
{aiTestResult.ai_response}
|
{aiTestResult.ai_response}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -1104,7 +1079,7 @@ export function StrategyStudioPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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" />
|
<Play className="w-10 h-10 mb-2 opacity-30" />
|
||||||
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
|
<p className="text-sm">{language === 'zh' ? '点击运行 AI 测试' : 'Click to run AI test'}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1114,7 +1089,7 @@ export function StrategyStudioPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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