From 75832f9eb272f11f86893f8b50cceae94e722c07 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Fri, 29 May 2026 16:14:46 +0800 Subject: [PATCH] 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). --- .gitignore | 1 + web/src/components/auth/LoginPage.tsx | 225 ++++++--- web/src/pages/StrategyMarketPage.tsx | 653 +++++--------------------- 3 files changed, 287 insertions(+), 592 deletions(-) diff --git a/.gitignore b/.gitignore index a487ae58..ab4077da 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ PR_DESCRIPTION.md # Go build artifacts /nofx-server +.gstack/ diff --git a/web/src/components/auth/LoginPage.tsx b/web/src/components/auth/LoginPage.tsx index 25683063..20029847 100644 --- a/web/src/components/auth/LoginPage.tsx +++ b/web/src/components/auth/LoginPage.tsx @@ -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( null ) - const [mode, setMode] = useState('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() { -
-
- {/* Logo + Title */} -
-
-
-
- NOFX -
+ {/* Self-contained centering grid — works regardless of parent flex setup */} +
+ {/* ───────── LEFT: brand panel (desktop only) ───────── */} +
+ {/* Ambient gold halo */} +
+
+ + {/* Brand mark */} +
+ NOFX +
+ NOFX.
-

- Welcome back -

-

Sign in to your account

- {/* Card */} -
-
+ {/* Headline */} +
+
+
+ + Terminal Online + +
+

+ {language === 'zh' ? ( + <> + AI 驱动的
+ + 多市场交易终端 + + + ) : language === 'id' ? ( + <> + Terminal Trading
+ + Multi-Pasar AI + + + ) : ( + <> + AI-Powered
+ + Trading Terminal + + + )} +

+

+ {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.'} +

+
+ + {/* Stats strip */} +
+ + + +
+
+ + {/* ───────── RIGHT: form panel ───────── */} +
+
+ {/* Mobile brand */} +
+ NOFX +
+ NOFX. +
+
+ + {/* Form header */} +
+

+ {t('signIn', language)} +

+

+ {language === 'zh' + ? '使用您的邮箱继续' + : language === 'id' + ? 'Lanjutkan dengan email Anda' + : 'Continue with your email'} +

+
+ + {/* Form */} + {/* Email */}
-
{/* Password */}
-
- - - {/* Error */} + {/* Error banner */} {error && ( -

- {error} -

+
+ ! + {error} +
)} {/* Submit */} -
+ {/* Footer */} +
+ v1.0
-
-
+ + ) } + +function Stat({ value, label }: { value: string; label: string }) { + return ( +
+
+ {value} +
+
+ {label} +
+
+ ) +} diff --git a/web/src/pages/StrategyMarketPage.tsx b/web/src/pages/StrategyMarketPage.tsx index af17bd47..37115ecb 100644 --- a/web/src/pages/StrategyMarketPage.tsx +++ b/web/src/pages/StrategyMarketPage.tsx @@ -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('all') - const [copiedId, setCopiedId] = useState(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( - '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 ( - -
-
- {/* Header Section */} -
-
- SYSTEM_STATUS:{' '} - ONLINE -
- MARKET_UPLINK:{' '} - ESTABLISHED +
+ {/* Ambient halos */} +
+
+ + {/* Main card */} +
+
+ {/* Top gold edge */} +
+ +
+ {/* Subtitle */} +
+
+ + {subtitle} +
-
-
-
- -
-
-

- {tr('title')} -

-

- {'// '} - {tr('subtitle')} -

-
-
-

- {tr('description')} + {/* Heading */} +

+ {heading} +

+ + {/* Description */} +

+ {description}

-
- {/* Search and Filter Bar */} -
- {/* Search */} -
-
-
-
- -
- setSearchQuery(e.target.value)} - className="w-full bg-transparent py-3 text-sm focus:outline-none placeholder-zinc-700 text-nofx-gold font-mono" - /> -
-
-
-
-
+ {/* CTA */} + + {ctaLabel} + + - {/* Category Filter */} -
- {['all', 'popular', 'recent'].map((cat) => ( - + {/* Stats row */} +
+ {features.map((f) => ( +
+
+ {f.value} +
+
+ {f.label} +
+
))}
- - {/* Loading State */} - {isLoading && ( -
-
-
-
-
- -
-
-

- {tr('loading')} -

-
-
-
-
-
-
- )} - - {/* Empty State */} - {!isLoading && filteredStrategies.length === 0 && ( -
-
-
- -
-

- [{tr('noStrategies')}] -

-

- {tr('noStrategiesDesc')} -

-
- )} - - {/* Strategy Grid */} - {!isLoading && filteredStrategies.length > 0 && ( -
- - {filteredStrategies.map((strategy, i) => { - const style = getStrategyStyle(strategy.name) - const Icon = style.icon - const indicators = - strategy.config_visible && strategy.config - ? getIndicatorList(strategy.config) - : [] - - return ( - - {/* Holographic Border Highlight */} -
-
- - {/* Category Side Strip */} -
- -
- {/* Header */} -
-
- -
-
- {strategy.config_visible ? ( -
- - PUBLIC_ACCESS -
- ) : ( -
- - RESTRICTED -
- )} -
-
- - {/* Name and Description */} -

- {strategy.name} - -

-

- {strategy.description || 'NO_DESCRIPTION_AVAILABLE'} -

- - {/* Meta Data */} -
-
- - {tr('author')} - - - @ - {strategy.author_email?.split('@')[0] || - 'UNKNOWN'} - -
-
- - {tr('createdAt')} - - - {formatDate(strategy.created_at)} - -
-
- - {/* Config / Indicators */} -
- {strategy.config_visible && strategy.config ? ( -
- {/* Indicators */} -
- {indicators.length > 0 ? ( - indicators.map((ind) => ( - - {ind} - - )) - ) : ( - - NO_INDICATORS - - )} -
- - {/* Risk Control */} - {(strategy.config.ai_config?.risk_control || strategy.config.risk_control) && ( -
-
-
- - LEV - - - {(strategy.config.ai_config?.risk_control || strategy.config.risk_control) - .btc_eth_max_leverage || '-'} - x - -
-
- - POS - - - {(strategy.config.ai_config?.risk_control || strategy.config.risk_control) - .max_positions || '-'} - -
-
- -
- )} -
- ) : ( -
- - - {tr('configHiddenDesc')} - -
- )} -
- - {/* Action Button */} -
- {strategy.config_visible && strategy.config ? ( - - ) : ( - - )} -
-
-
- ) - })} -
-
- )} - - {/* CTA - Share Strategy */} - {user && token && ( - -
navigate('/strategy')} - > -
-
- -
-
- {tr('shareYours')} -
-
- CONTRIBUTE TO THE GLOBAL DATABASE -
-
-
-
- INITIALIZE_UPLOAD -> -
-
-
-
- )}
+ + {/* Footer hint */} +

+ vergex.trade · {VERGEX_EXPLORE_URL} +

- +
) }