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

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>
)
}