feat(web): redesign login page and proxy strategy market to vergex.trade

- LoginPage: two-column desktop layout with brand panel (status pill,
  gradient headline, stats strip) and form panel; single-column mobile
  layout with centered brand mark. Self-contained grid centering so
  layout no longer depends on parent flex behavior. Drop the dead
  OnboardingModeSelector (it belongs to SetupPage, not login) and add
  loader spinner, animated submit arrow, and clearer error banner.
- StrategyMarketPage: replace the 560-line bespoke marketplace with a
  branded handoff to vergex.trade/explore. Direct iframe embedding is
  currently blocked by vergex's X-Frame-Options: SAMEORIGIN and
  frame-ancestors 'self', and there is no way to reliably detect the
  block from JavaScript (load event fires for the browser error page,
  contentWindow.location throws SecurityError in both success and
  failure). The component now renders a centered card with the
  POWERED BY VERGEX.TRADE pill, headline, description, gold CTA, and
  a stats row, with all three supported languages.
- .gitignore: exclude .gstack/ (local security audit reports).
This commit is contained in:
tinkle-community
2026-05-29 16:14:46 +08:00
parent 99361cb085
commit 75832f9eb2
3 changed files with 287 additions and 592 deletions

1
.gitignore vendored
View File

@@ -133,3 +133,4 @@ PR_DESCRIPTION.md
# Go build artifacts
/nofx-server
.gstack/

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Eye, EyeOff, Loader2, ArrowRight } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner'
import { useAuth } from '../../contexts/AuthContext'
@@ -7,8 +7,6 @@ import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { DeepVoidBackground } from '../common/DeepVoidBackground'
import { LanguageSwitcher } from '../common/LanguageSwitcher'
import { OnboardingModeSelector } from './OnboardingModeSelector'
import type { UserMode } from '../../lib/onboarding'
import { invalidateSystemConfig } from '../../lib/config'
export function LoginPage() {
@@ -23,16 +21,13 @@ export function LoginPage() {
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(
null
)
const [mode, setMode] = useState<UserMode>('beginner')
// Clean up stale auth state once on mount
useEffect(() => {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
localStorage.removeItem('user_id')
}, [])
// Show session-expired toast (re-runs on language change to update text)
useEffect(() => {
if (sessionStorage.getItem('from401') === 'true') {
const id = toast.warning(t('sessionExpired', language), {
@@ -49,9 +44,9 @@ export function LoginPage() {
const res = await fetch('/api/reset-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Server-side guard against accidental/drive-by triggers.
// Phrase must match handler_user.go resetAccountConfirmPhrase.
body: JSON.stringify({ confirm: 'I_UNDERSTAND_THIS_DELETES_EVERYTHING' }),
body: JSON.stringify({
confirm: 'I_UNDERSTAND_THIS_DELETES_EVERYTHING',
}),
})
if (res.ok) {
localStorage.removeItem('auth_token')
@@ -60,9 +55,7 @@ export function LoginPage() {
sessionStorage.removeItem('from401')
invalidateSystemConfig()
toast.success(t('forgotAccountSuccess', language))
setTimeout(() => {
navigate('/setup')
}, 1500)
setTimeout(() => navigate('/setup'), 1500)
} else {
const data = await res.json()
toast.error(data.error || 'Reset failed')
@@ -76,7 +69,7 @@ export function LoginPage() {
e.preventDefault()
setError('')
setLoading(true)
const result = await login(email, password, mode)
const result = await login(email, password)
setLoading(false)
if (result.success) {
if (expiredToastId) toast.dismiss(expiredToastId)
@@ -91,49 +84,146 @@ export function LoginPage() {
<DeepVoidBackground disableAnimation>
<LanguageSwitcher />
<div className="flex-1 flex items-center justify-center px-4 py-16">
<div className="w-full max-w-sm">
{/* Logo + Title */}
<div className="text-center mb-10">
<div className="flex justify-center mb-5">
<div className="relative">
<div className="absolute -inset-3 bg-nofx-gold/15 rounded-full blur-2xl" />
<img
src="/icons/nofx.svg"
alt="NOFX"
className="w-14 h-14 relative z-10"
/>
</div>
{/* Self-contained centering grid — works regardless of parent flex setup */}
<main className="flex-1 grid lg:grid-cols-2">
{/* ───────── LEFT: brand panel (desktop only) ───────── */}
<section className="hidden lg:flex flex-col justify-between p-12 xl:p-16 relative overflow-hidden">
{/* Ambient gold halo */}
<div className="absolute -left-32 top-1/3 w-[28rem] h-[28rem] bg-nofx-gold/[0.06] rounded-full blur-3xl pointer-events-none" />
<div className="absolute -right-16 bottom-0 w-72 h-72 bg-nofx-accent/[0.04] rounded-full blur-3xl pointer-events-none" />
{/* Brand mark */}
<div className="flex items-center gap-3 relative">
<img src="/icons/nofx.svg" alt="NOFX" className="w-9 h-9" />
<div className="font-mono font-bold text-xl tracking-tight text-white">
NOFX<span className="text-nofx-gold">.</span>
</div>
<h1 className="text-2xl font-bold text-white mb-1.5">
Welcome back
</h1>
<p className="text-zinc-500 text-sm">Sign in to your account</p>
</div>
{/* Card */}
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-8 shadow-2xl">
<form onSubmit={handleLogin} className="space-y-5">
{/* Headline */}
<div className="relative max-w-lg">
<div className="inline-flex items-center gap-2 mb-7 px-3 py-1 rounded-full border border-nofx-success/25 bg-nofx-success/[0.06]">
<div className="w-1.5 h-1.5 rounded-full bg-nofx-success animate-pulse" />
<span className="text-[10.5px] font-mono tracking-[0.18em] text-nofx-success uppercase">
Terminal Online
</span>
</div>
<h2 className="text-4xl xl:text-5xl font-bold tracking-tight text-white leading-[1.05]">
{language === 'zh' ? (
<>
AI <br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
</span>
</>
) : language === 'id' ? (
<>
Terminal Trading<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
Multi-Pasar AI
</span>
</>
) : (
<>
AI-Powered<br />
<span className="bg-gradient-to-r from-nofx-gold to-yellow-300 bg-clip-text text-transparent">
Trading Terminal
</span>
</>
)}
</h2>
<p className="mt-5 text-zinc-400 text-base leading-relaxed max-w-md">
{language === 'zh'
? '一键接入 Hyperliquid、OKX、Aster 等 10+ 交易所与 7 个 LLM 模型, 用自然语言部署 24/7 自动化策略.'
: language === 'id'
? 'Hubungkan ke 10+ bursa termasuk Hyperliquid, OKX, Aster dan 7 model LLM. Terapkan strategi otomatis 24/7 dengan bahasa alami.'
: 'Plug into 10+ exchanges including Hyperliquid, OKX, Aster, and 7 LLM models. Deploy 24/7 automated strategies with natural language.'}
</p>
</div>
{/* Stats strip */}
<div className="relative grid grid-cols-3 gap-8 max-w-md">
<Stat
value="10+"
label={
language === 'zh'
? '交易所'
: language === 'id'
? 'Bursa'
: 'Exchanges'
}
/>
<Stat
value="7"
label={
language === 'zh'
? 'AI 模型'
: language === 'id'
? 'Model AI'
: 'AI Models'
}
/>
<Stat
value="24/7"
label={
language === 'zh'
? '全天候'
: language === 'id'
? 'Sepanjang Waktu'
: 'Always On'
}
/>
</div>
</section>
{/* ───────── RIGHT: form panel ───────── */}
<section className="flex items-center justify-center p-6 sm:p-12 relative">
<div className="w-full max-w-sm">
{/* Mobile brand */}
<div className="lg:hidden flex flex-col items-center gap-3 mb-10">
<img src="/icons/nofx.svg" alt="NOFX" className="w-12 h-12" />
<div className="font-mono font-bold text-lg tracking-tight text-white">
NOFX<span className="text-nofx-gold">.</span>
</div>
</div>
{/* Form header */}
<div className="mb-7">
<h1 className="text-[26px] sm:text-3xl font-bold tracking-tight text-white">
{t('signIn', language)}
</h1>
<p className="mt-1.5 text-sm text-zinc-500">
{language === 'zh'
? '使用您的邮箱继续'
: language === 'id'
? 'Lanjutkan dengan email Anda'
: 'Continue with your email'}
</p>
</div>
{/* Form */}
<form onSubmit={handleLogin} className="space-y-4">
{/* Email */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">
<label className="block text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500 mb-2">
{t('email', language)}
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="you@example.com"
required
autoFocus
autoComplete="email"
/>
</div>
{/* Password */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs font-medium text-zinc-400">
<label className="text-[10.5px] font-medium uppercase tracking-[0.14em] text-zinc-500">
{t('password', language)}
</label>
<button
@@ -149,57 +239,80 @@ export function LoginPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-zinc-900/60 border border-white/[0.08] rounded-lg px-4 py-[11px] pr-11 text-[14px] text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/50 focus:bg-zinc-900 focus:ring-2 focus:ring-nofx-gold/20 transition-all"
placeholder="••••••••"
required
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<OnboardingModeSelector
language={language}
mode={mode}
onChange={setMode}
/>
{/* Error */}
{/* Error banner */}
{error && (
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
{error}
</p>
<div className="flex items-start gap-2 rounded-lg border border-red-500/25 bg-red-500/[0.08] px-3 py-2.5 text-xs text-red-300">
<span className="text-red-400 font-bold mt-px">!</span>
<span className="leading-relaxed">{error}</span>
</div>
)}
{/* Submit */}
<button
type="submit"
disabled={loading}
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
className="group mt-2 flex w-full items-center justify-center gap-2 rounded-lg bg-nofx-gold py-[11px] text-sm font-semibold text-black shadow-lg shadow-nofx-gold/10 transition-all hover:bg-yellow-400 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
>
{loading
? t('loggingIn', language) || 'Signing in...'
: t('signIn', language) || 'Sign In'}
{loading ? (
<>
<Loader2 size={16} className="animate-spin" />
{t('loggingIn', language) || 'Signing in...'}
</>
) : (
<>
{t('signIn', language)}
<ArrowRight
size={16}
className="transition-transform group-hover:translate-x-0.5"
/>
</>
)}
</button>
</form>
<div className="mt-4 text-center">
{/* Footer */}
<div className="mt-8 pt-5 border-t border-white/[0.06] flex items-center justify-between text-[11px]">
<span className="font-mono text-zinc-600">v1.0</span>
<button
type="button"
onClick={handleResetAccount}
className="text-xs text-zinc-600 hover:text-red-400 transition-colors"
className="text-zinc-600 transition-colors hover:text-red-400"
>
{t('forgotAccount', language)}
</button>
</div>
</div>
</div>
</div>
</section>
</main>
</DeepVoidBackground>
)
}
function Stat({ value, label }: { value: string; label: string }) {
return (
<div>
<div className="font-mono text-2xl xl:text-3xl font-bold text-white">
{value}
</div>
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-zinc-500">
{label}
</div>
</div>
)
}

View File

@@ -1,560 +1,141 @@
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useNavigate } from 'react-router-dom'
import useSWR from 'swr'
import {
TrendingUp,
Shield,
Zap,
Eye,
EyeOff,
Copy,
Check,
Hexagon,
Layers,
Target,
Activity,
Terminal,
Cpu,
Database,
} from 'lucide-react'
import { ExternalLink } from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext'
import { useAuth } from '../contexts/AuthContext'
import { toast } from 'sonner'
import { t } from '../i18n/translations'
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
interface PublicStrategy {
id: string
name: string
description: string
author_email?: string
is_public: boolean
config_visible: boolean
config?: any
stats?: {
used_by: number
rating: number
}
created_at: string
updated_at: string
}
const strategyStyles: Record<
string,
{
color: string
border: string
glow: string
shadow: string
icon: any
bg: string
}
> = {
scalper: {
color: 'text-[#F0B90B]',
border: 'border-[#F0B90B]/30',
glow: 'shadow-[0_0_20px_rgba(240,185,11,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(240,185,11,0.25)]',
bg: 'bg-[#F0B90B]/5',
icon: Zap,
},
swing: {
color: 'text-cyan-400',
border: 'border-cyan-400/30',
glow: 'shadow-[0_0_20px_rgba(34,211,238,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(34,211,238,0.25)]',
bg: 'bg-cyan-400/5',
icon: TrendingUp,
},
arbitrage: {
color: 'text-purple-400',
border: 'border-purple-400/30',
glow: 'shadow-[0_0_20px_rgba(192,132,252,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(192,132,252,0.25)]',
bg: 'bg-purple-400/5',
icon: Layers,
},
conservative: {
color: 'text-emerald-400',
border: 'border-emerald-400/30',
glow: 'shadow-[0_0_20px_rgba(52,211,153,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(52,211,153,0.25)]',
bg: 'bg-emerald-400/5',
icon: Shield,
},
aggressive: {
color: 'text-red-500',
border: 'border-red-500/30',
glow: 'shadow-[0_0_20px_rgba(239,68,68,0.15)]',
shadow: 'hover:shadow-[0_0_30px_rgba(239,68,68,0.25)]',
bg: 'bg-red-500/5',
icon: Target,
},
default: {
color: 'text-zinc-400',
border: 'border-zinc-700',
glow: '',
shadow: 'hover:shadow-[0_0_20px_rgba(255,255,255,0.05)]',
bg: 'bg-zinc-800/20',
icon: Activity,
},
}
function getStrategyStyle(name: string) {
const lowerName = name.toLowerCase()
if (lowerName.includes('scalp')) return strategyStyles.scalper
if (lowerName.includes('swing')) return strategyStyles.swing
if (lowerName.includes('arb')) return strategyStyles.arbitrage
if (lowerName.includes('safe') || lowerName.includes('conserv'))
return strategyStyles.conservative
if (lowerName.includes('aggress') || lowerName.includes('high'))
return strategyStyles.aggressive
return strategyStyles.default
}
const VERGEX_EXPLORE_URL = 'https://vergex.trade/explore'
// Strategy Market — proxied to vergex.trade/explore.
//
// vergex.trade currently sets `X-Frame-Options: SAMEORIGIN` on /explore which
// makes browsers refuse cross-origin embedding and render their own "refused
// to connect" page inside the iframe. There is no reliable way to detect
// this from JavaScript (the iframe's `load` event fires for the browser
// error page, and `contentWindow.location` always throws cross-origin
// regardless of success or failure), so we don't try to be clever — we
// surface a clean external-launch CTA instead.
//
// TO RE-ENABLE INLINE EMBEDDING:
// 1. Ask the vergex.trade team to add NOFX origins to the /explore
// CSP `frame-ancestors` (same as /trending: 'self' https://nofxos.ai
// https://www.nofxos.ai http://127.0.0.1:3000 http://localhost:3000)
// AND drop the `X-Frame-Options: SAMEORIGIN` header on that path.
// 2. Replace this component with the same iframe pattern used by
// DataPage.tsx (which already embeds vergex.trade/trending successfully).
export function StrategyMarketPage() {
const navigate = useNavigate()
const { language } = useLanguage()
const { token, user } = useAuth()
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [copiedId, setCopiedId] = useState<string | null>(null)
const tr = (key: string) => t(`strategyMarket.${key}`, language)
const heading =
language === 'zh'
? 'Vergex 策略市场'
: language === 'id'
? 'Pasar Strategi Vergex'
: 'Vergex Strategy Market'
// Fetch public strategies
const { data: strategies, isLoading } = useSWR<PublicStrategy[]>(
'public-strategies',
async () => {
const response = await fetch('/api/strategies/public')
if (!response.ok) throw new Error('Failed to fetch strategies')
const data = await response.json()
return data.strategies || []
},
{
refreshInterval: 60000,
revalidateOnFocus: false,
}
)
const description =
language === 'zh'
? '在 Vergex 上探索由社区创建的交易策略,一键复制到您的 NOFX 账户。当前需要在新窗口打开。'
: language === 'id'
? 'Jelajahi strategi trading komunitas di Vergex dan salin ke akun NOFX Anda. Saat ini terbuka di tab baru.'
: 'Explore community-built trading strategies on Vergex and copy them to your NOFX account. Currently opens in a new tab.'
const filteredStrategies =
strategies?.filter((s) => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
return (
s.name.toLowerCase().includes(query) ||
s.description?.toLowerCase().includes(query)
)
}
return true
}) || []
const ctaLabel =
language === 'zh'
? '在 Vergex 打开策略市场'
: language === 'id'
? 'Buka Pasar Strategi di Vergex'
: 'Open Strategy Market on Vergex'
const handleCopyConfig = async (strategy: PublicStrategy) => {
if (!strategy.config) return
try {
await navigator.clipboard.writeText(
JSON.stringify(strategy.config, null, 2)
)
setCopiedId(strategy.id)
toast.success(tr('copied'))
setTimeout(() => setCopiedId(null), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
const subtitle =
language === 'zh'
? 'POWERED BY VERGEX.TRADE'
: language === 'id'
? 'DITENAGAI OLEH VERGEX.TRADE'
: 'POWERED BY VERGEX.TRADE'
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date
.toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
.replace(',', '')
}
const getIndicatorList = (config: any) => {
const indicatorsConfig = config?.ai_config?.indicators || config?.indicators
if (!indicatorsConfig) return []
const indicators = []
if (indicatorsConfig.enable_ema) indicators.push('EMA')
if (indicatorsConfig.enable_macd) indicators.push('MACD')
if (indicatorsConfig.enable_rsi) indicators.push('RSI')
if (indicatorsConfig.enable_atr) indicators.push('ATR')
if (indicatorsConfig.enable_boll) indicators.push('BOLL')
if (indicatorsConfig.enable_volume) indicators.push('VOL')
if (indicatorsConfig.enable_oi) indicators.push('OI')
if (indicatorsConfig.enable_funding_rate) indicators.push('FR')
return indicators
}
const features =
language === 'zh'
? [
{ label: '策略数量', value: '100+' },
{ label: '覆盖市场', value: 'CEX & DEX' },
{ label: '实时数据', value: '24/7' },
]
: language === 'id'
? [
{ label: 'Total Strategi', value: '100+' },
{ label: 'Cakupan Pasar', value: 'CEX & DEX' },
{ label: 'Data Real-time', value: '24/7' },
]
: [
{ label: 'Strategies', value: '100+' },
{ label: 'Markets', value: 'CEX & DEX' },
{ label: 'Live Data', value: '24/7' },
]
return (
<DeepVoidBackground className="min-h-screen text-white font-mono py-12">
<div className="w-full px-4 md:px-8 space-y-8">
<div className="w-full relative z-10">
{/* Header Section */}
<div className="mb-12 border-b border-zinc-800 pb-8 relative">
<div className="absolute top-0 right-0 p-2 border border-zinc-800 rounded bg-black/50 text-xs text-zinc-500 font-mono hidden md:block">
SYSTEM_STATUS:{' '}
<span className="text-emerald-500 animate-pulse">ONLINE</span>
<br />
MARKET_UPLINK:{' '}
<span className="text-emerald-500">ESTABLISHED</span>
<div className="relative flex h-[calc(100vh-64px)] w-full items-center justify-center overflow-hidden bg-nofx-bg px-6">
{/* Ambient halos */}
<div className="pointer-events-none absolute -left-20 top-1/4 h-96 w-96 rounded-full bg-nofx-gold/[0.06] blur-3xl" />
<div className="pointer-events-none absolute -right-20 bottom-1/4 h-80 w-80 rounded-full bg-nofx-accent/[0.04] blur-3xl" />
{/* Main card */}
<div className="relative w-full max-w-xl">
<div className="overflow-hidden rounded-2xl border border-white/[0.08] bg-zinc-950/60 shadow-2xl shadow-black/40 backdrop-blur-xl">
{/* Top gold edge */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-nofx-gold/40 to-transparent" />
<div className="p-8 sm:p-10 text-center">
{/* Subtitle */}
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-nofx-gold/25 bg-nofx-gold/[0.06] px-3 py-1">
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-nofx-gold" />
<span className="text-[10.5px] font-mono uppercase tracking-[0.18em] text-nofx-gold">
{subtitle}
</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>
<h1
className="text-4xl font-bold tracking-tighter text-white uppercase glitch-text"
data-text={tr('title')}
>
{tr('title')}
</h1>
<p className="text-xs text-nofx-gold tracking-[0.3em] font-bold mt-1">
{'// '}
{tr('subtitle')}
</p>
</div>
</div>
<p className="text-sm text-zinc-500 max-w-2xl border-l-2 border-zinc-800 pl-4">
{tr('description')}
{/* Heading */}
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-white leading-tight">
{heading}
</h1>
{/* Description */}
<p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-zinc-400">
{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={tr('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>
{/* CTA */}
<a
href={VERGEX_EXPLORE_URL}
target="_blank"
rel="noopener noreferrer"
className="group mt-8 inline-flex items-center gap-2 rounded-lg bg-nofx-gold px-6 py-3 text-sm font-semibold text-black shadow-lg shadow-nofx-gold/20 transition-all hover:bg-yellow-400 active:scale-[0.98]"
>
{ctaLabel}
<ExternalLink
size={15}
className="transition-transform group-hover:translate-x-0.5"
/>
</a>
{/* Category Filter */}
<div className="flex gap-2 bg-zinc-900/50 p-1 border border-zinc-800">
{['all', 'popular', 'recent'].map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 text-xs font-mono uppercase tracking-wider transition-all relative overflow-hidden ${
selectedCategory === cat
? 'text-black font-bold'
: 'text-zinc-500 hover:text-white'
}`}
>
{selectedCategory === cat && (
<motion.div
layoutId="filter-highlight"
className="absolute inset-0 bg-nofx-gold"
transition={{
type: 'spring',
bounce: 0.2,
duration: 0.6,
}}
/>
)}
<span className="relative z-10">{tr(cat)}</span>
</button>
{/* Stats row */}
<div className="mt-10 grid grid-cols-3 gap-6 border-t border-white/[0.05] pt-8">
{features.map((f) => (
<div key={f.label}>
<div className="font-mono text-xl font-bold text-white">
{f.value}
</div>
<div className="mt-1 text-[10.5px] uppercase tracking-[0.14em] text-zinc-500">
{f.label}
</div>
</div>
))}
</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>
<p className="text-nofx-gold text-xs tracking-widest animate-pulse">
{tr('loading')}
</p>
<div className="flex gap-1">
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
></div>
<div
className="w-1 h-1 bg-nofx-gold rounded-full animate-bounce"
style={{ animationDelay: '0.4s' }}
></div>
</div>
</div>
)}
{/* Empty State */}
{!isLoading && filteredStrategies.length === 0 && (
<div className="flex flex-col items-center justify-center py-32 border border-zinc-800 border-dashed bg-zinc-900/20 rounded">
<div className="relative mb-6">
<div className="absolute -inset-4 bg-red-500/10 rounded-full blur-xl animate-pulse"></div>
<Activity className="w-16 h-16 text-zinc-700 relative z-10" />
</div>
<h3 className="text-xl font-bold text-zinc-300 font-mono tracking-tight mb-2">
[{tr('noStrategies')}]
</h3>
<p className="text-zinc-600 text-xs tracking-wide uppercase">
{tr('noStrategiesDesc')}
</p>
</div>
)}
{/* Strategy Grid */}
{!isLoading && filteredStrategies.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredStrategies.map((strategy, i) => {
const style = getStrategyStyle(strategy.name)
const Icon = style.icon
const indicators =
strategy.config_visible && strategy.config
? getIndicatorList(strategy.config)
: []
return (
<motion.div
key={strategy.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: i * 0.05 }}
className={`group relative bg-black border border-zinc-800 hover:border-zinc-600 transition-all duration-300 ${style.shadow}`}
>
{/* Holographic Border Highlight */}
<div
className={`absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
<div
className={`absolute bottom-0 right-0 w-full h-[1px] bg-gradient-to-r from-transparent via-${style.color.split('-')[1]}-500 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500`}
></div>
{/* Category Side Strip */}
<div
className={`absolute left-0 top-0 bottom-0 w-[2px] ${style.bg.replace('/5', '/50')}`}
></div>
<div className="p-6 relative">
{/* Header */}
<div className="flex justify-between items-start mb-6">
<div
className={`p-2 rounded-none border ${style.border} ${style.bg}`}
>
<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 */}
<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">
{tr('author')}
</span>
<span className="text-zinc-400 group-hover:text-white transition-colors">
@
{strategy.author_email?.split('@')[0] ||
'UNKNOWN'}
</span>
</div>
<div className="flex flex-col text-right">
<span className="text-zinc-700 uppercase">
{tr('createdAt')}
</span>
<span className="text-zinc-400">
{formatDate(strategy.created_at)}
</span>
</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.ai_config?.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.ai_config?.risk_control || 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.ai_config?.risk_control || 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">
{tr('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">
{tr('copied')}
</span>
</>
) : (
<>
<Copy className="w-3 h-3 group-hover/btn:scale-110 transition-transform" />
{tr('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} />
{tr('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={() => navigate('/strategy')}
>
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold to-yellow-600 rounded blur opacity-25 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
<div className="relative px-8 py-4 bg-black border border-zinc-800 hover:border-nofx-gold/50 flex items-center gap-4 transition-all">
<Hexagon
className="text-nofx-gold animate-spin-slow"
size={24}
/>
<div className="text-left">
<div className="text-sm font-bold text-white uppercase tracking-wider group-hover:text-nofx-gold transition-colors">
{tr('shareYours')}
</div>
<div className="text-[10px] text-zinc-500 font-mono">
CONTRIBUTE TO THE GLOBAL DATABASE
</div>
</div>
<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 -&gt;
</div>
</div>
</div>
</motion.div>
)}
</div>
{/* Footer hint */}
<p className="mt-4 text-center text-[11px] text-zinc-600">
vergex.trade · {VERGEX_EXPLORE_URL}
</p>
</div>
</DeepVoidBackground>
</div>
)
}