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

- +
) }