From 09117bb4049fa8805906e6c34094485f2bc6caea Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 1 Jan 2026 23:05:58 +0800 Subject: [PATCH] feat: add strategy market, login overlay, and registration limit page - Add public strategy market API endpoint (/api/strategies/public) - Add is_public and config_visible fields to Strategy model - Add LoginRequiredOverlay component for unified auth prompts - Add WhitelistFullPage for registration capacity limit - Add StrategyMarketPage for browsing public strategies - Unify navigation logic across HeaderBar, LandingPage, App - Reduce klines API calls (fetch once on mount) - Fix various page transition issues --- api/server.go | 3 + api/strategy.go | 75 +- store/strategy.go | 42 +- web/src/App.tsx | 355 ++++----- web/src/components/HeaderBar.tsx | 695 ++--------------- web/src/components/LoginPage.tsx | 463 +++++------ web/src/components/LoginRequiredOverlay.tsx | 163 ++++ web/src/components/RegisterPage.tsx | 735 ++++++++---------- web/src/components/WhitelistFullPage.tsx | 124 +++ web/src/components/landing/FooterSection.tsx | 33 +- .../components/landing/core/TerminalHero.tsx | 3 +- web/src/contexts/AuthContext.tsx | 47 +- web/src/pages/LandingPage.tsx | 34 +- web/src/pages/StrategyMarketPage.tsx | 515 ++++++++++++ 14 files changed, 1747 insertions(+), 1540 deletions(-) create mode 100644 web/src/components/LoginRequiredOverlay.tsx create mode 100644 web/src/components/WhitelistFullPage.tsx create mode 100644 web/src/pages/StrategyMarketPage.tsx diff --git a/api/server.go b/api/server.go index 9914b7af..1d519ded 100644 --- a/api/server.go +++ b/api/server.go @@ -127,6 +127,9 @@ func (s *Server) setupRoutes() { api.GET("/klines", s.handleKlines) api.GET("/symbols", s.handleSymbols) + // Public strategy market (no authentication required) + api.GET("/strategies/public", s.handlePublicStrategies) + // Authentication related routes (no authentication required) api.POST("/register", s.handleRegister) api.POST("/login", s.handleLogin) diff --git a/api/strategy.go b/api/strategy.go index 6c581703..c33edeb2 100644 --- a/api/strategy.go +++ b/api/strategy.go @@ -29,6 +29,43 @@ func validateStrategyConfig(config *store.StrategyConfig) []string { return warnings } +// handlePublicStrategies Get public strategies for strategy market (no auth required) +func (s *Server) handlePublicStrategies(c *gin.Context) { + strategies, err := s.store.Strategy().ListPublic() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get public strategies: " + err.Error()}) + return + } + + // Convert to frontend format with visibility control + result := make([]gin.H, 0, len(strategies)) + for _, st := range strategies { + item := gin.H{ + "id": st.ID, + "name": st.Name, + "description": st.Description, + "author_email": "", // Will be filled if we have user info + "is_public": st.IsPublic, + "config_visible": st.ConfigVisible, + "created_at": st.CreatedAt, + "updated_at": st.UpdatedAt, + } + + // Only include config if config_visible is true + if st.ConfigVisible { + var config store.StrategyConfig + json.Unmarshal([]byte(st.Config), &config) + item["config"] = config + } + + result = append(result, item) + } + + c.JSON(http.StatusOK, gin.H{ + "strategies": result, + }) +} + // handleGetStrategies Get strategy list func (s *Server) handleGetStrategies(c *gin.Context) { userID := c.GetString("user_id") @@ -50,14 +87,16 @@ func (s *Server) handleGetStrategies(c *gin.Context) { json.Unmarshal([]byte(st.Config), &config) result = append(result, gin.H{ - "id": st.ID, - "name": st.Name, - "description": st.Description, - "is_active": st.IsActive, - "is_default": st.IsDefault, - "config": config, - "created_at": st.CreatedAt, - "updated_at": st.UpdatedAt, + "id": st.ID, + "name": st.Name, + "description": st.Description, + "is_active": st.IsActive, + "is_default": st.IsDefault, + "is_public": st.IsPublic, + "config_visible": st.ConfigVisible, + "config": config, + "created_at": st.CreatedAt, + "updated_at": st.UpdatedAt, }) } @@ -174,9 +213,11 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } var req struct { - Name string `json:"name"` - Description string `json:"description"` - Config store.StrategyConfig `json:"config"` + Name string `json:"name"` + Description string `json:"description"` + Config store.StrategyConfig `json:"config"` + IsPublic bool `json:"is_public"` + ConfigVisible bool `json:"config_visible"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -192,11 +233,13 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) { } strategy := &store.Strategy{ - ID: strategyID, - UserID: userID, - Name: req.Name, - Description: req.Description, - Config: string(configJSON), + ID: strategyID, + UserID: userID, + Name: req.Name, + Description: req.Description, + Config: string(configJSON), + IsPublic: req.IsPublic, + ConfigVisible: req.ConfigVisible, } if err := s.store.Strategy().Update(strategy); err != nil { diff --git a/store/strategy.go b/store/strategy.go index 1e4af1bd..993fb79e 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -15,15 +15,17 @@ type StrategyStore struct { // Strategy strategy configuration type Strategy struct { - ID string `gorm:"primaryKey" json:"id"` - UserID string `gorm:"column:user_id;not null;default:'';index" json:"user_id"` - Name string `gorm:"not null" json:"name"` - Description string `gorm:"default:''" json:"description"` - IsActive bool `gorm:"column:is_active;default:false;index" json:"is_active"` - IsDefault bool `gorm:"column:is_default;default:false" json:"is_default"` - Config string `gorm:"not null;default:'{}'" json:"config"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `gorm:"primaryKey" json:"id"` + UserID string `gorm:"column:user_id;not null;default:'';index" json:"user_id"` + Name string `gorm:"not null" json:"name"` + Description string `gorm:"default:''" json:"description"` + IsActive bool `gorm:"column:is_active;default:false;index" json:"is_active"` + IsDefault bool `gorm:"column:is_default;default:false" json:"is_default"` + IsPublic bool `gorm:"column:is_public;default:false;index" json:"is_public"` // whether visible in strategy market + ConfigVisible bool `gorm:"column:config_visible;default:true" json:"config_visible"` // whether config details are visible + Config string `gorm:"not null;default:'{}'" json:"config"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (Strategy) TableName() string { return "strategies" } @@ -298,10 +300,12 @@ func (s *StrategyStore) Update(strategy *Strategy) error { return s.db.Model(&Strategy{}). Where("id = ? AND user_id = ?", strategy.ID, strategy.UserID). Updates(map[string]interface{}{ - "name": strategy.Name, - "description": strategy.Description, - "config": strategy.Config, - "updated_at": time.Now(), + "name": strategy.Name, + "description": strategy.Description, + "config": strategy.Config, + "is_public": strategy.IsPublic, + "config_visible": strategy.ConfigVisible, + "updated_at": time.Now(), }).Error } @@ -328,6 +332,18 @@ func (s *StrategyStore) List(userID string) ([]*Strategy, error) { return strategies, nil } +// ListPublic get all public strategies for the strategy market +func (s *StrategyStore) ListPublic() ([]*Strategy, error) { + var strategies []*Strategy + err := s.db.Where("is_public = ?", true). + Order("created_at DESC"). + Find(&strategies).Error + if err != nil { + return nil, err + } + return strategies, nil +} + // Get get a single strategy func (s *StrategyStore) Get(userID, id string) (*Strategy, error) { var st Strategy diff --git a/web/src/App.tsx b/web/src/App.tsx index 9d090701..6b23de38 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,6 @@ import { useEffect, useState, useRef } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +// Force HMR Update import useSWR, { mutate } from 'swr' import { api } from './lib/api' import { ChartTabs } from './components/ChartTabs' @@ -11,6 +13,8 @@ import { LandingPage } from './pages/LandingPage' import { FAQPage } from './pages/FAQPage' import { StrategyStudioPage } from './pages/StrategyStudioPage' import { DebateArenaPage } from './pages/DebateArenaPage' +import { StrategyMarketPage } from './pages/StrategyMarketPage' +import { LoginRequiredOverlay } from './components/LoginRequiredOverlay' import HeaderBar from './components/HeaderBar' import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { AuthProvider, useAuth } from './contexts/AuthContext' @@ -40,6 +44,7 @@ type Page = | 'trader' | 'backtest' | 'strategy' + | 'strategy-market' | 'debate' | 'faq' | 'login' @@ -119,6 +124,11 @@ function App() { const { loading: configLoading } = useSystemConfig() const [route, setRoute] = useState(window.location.pathname) + // Debug log + useEffect(() => { + console.log('[App] Mounted. Route:', window.location.pathname); + }, []); + // 从URL路径读取初始页面状态(支持刷新保持页面) const getInitialPage = (): Page => { const path = window.location.pathname @@ -127,12 +137,44 @@ function App() { if (path === '/traders' || hash === 'traders') return 'traders' if (path === '/backtest' || hash === 'backtest') return 'backtest' if (path === '/strategy' || hash === 'strategy') return 'strategy' + if (path === '/strategy-market' || hash === 'strategy-market') return 'strategy-market' if (path === '/debate' || hash === 'debate') return 'debate' if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader' return 'competition' // 默认为竞赛页面 } + // Login required overlay state + const [loginOverlayOpen, setLoginOverlayOpen] = useState(false) + const [loginOverlayFeature, setLoginOverlayFeature] = useState('') + + const handleLoginRequired = (featureName: string) => { + setLoginOverlayFeature(featureName) + setLoginOverlayOpen(true) + } + + // Unified page navigation handler + const navigateToPage = (page: Page) => { + const pathMap: Record = { + 'competition': '/competition', + 'strategy-market': '/strategy-market', + 'traders': '/traders', + 'trader': '/dashboard', + 'backtest': '/backtest', + 'strategy': '/strategy', + 'debate': '/debate', + 'faq': '/faq', + 'login': '/login', + 'register': '/register', + } + const path = pathMap[page] + if (path) { + window.history.pushState({}, '', path) + setRoute(path) + setCurrentPage(page) + } + } + const [currentPage, setCurrentPage] = useState(getInitialPage()) // 从 URL 参数读取初始 trader 标识(格式: name-id前4位) const [selectedTraderSlug, setSelectedTraderSlug] = useState(() => { @@ -178,6 +220,8 @@ function App() { setCurrentPage('backtest') } else if (path === '/strategy' || hash === 'strategy') { setCurrentPage('strategy') + } else if (path === '/strategy-market' || hash === 'strategy-market') { + setCurrentPage('strategy-market') } else if (path === '/debate' || hash === 'debate') { setCurrentPage('debate') } else if ( @@ -381,154 +425,28 @@ function App() { onLanguageChange={setLanguage} user={user} onLogout={logout} - onPageChange={(page: Page) => { - if (page === 'competition') { - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } else if (page === 'trader') { - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - } else if (page === 'faq') { - window.history.pushState({}, '', '/faq') - setRoute('/faq') - } else if (page === 'backtest') { - window.history.pushState({}, '', '/backtest') - setRoute('/backtest') - setCurrentPage('backtest') - } else if (page === 'strategy') { - window.history.pushState({}, '', '/strategy') - setRoute('/strategy') - setCurrentPage('strategy') - } else if (page === 'debate') { - window.history.pushState({}, '', '/debate') - setRoute('/debate') - setCurrentPage('debate') - } - }} + onLoginRequired={handleLoginRequired} + onPageChange={navigateToPage} /> + setLoginOverlayOpen(false)} + featureName={loginOverlayFeature} + /> ) } if (route === '/reset-password') { return } - if (route === '/competition') { - return ( -
- { - console.log('Competition page onPageChange called with:', page) - console.log('Current route:', route, 'Current page:', currentPage) - - if (page === 'competition') { - console.log('Navigating to competition') - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - console.log('Navigating to traders') - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } else if (page === 'trader') { - console.log('Navigating to trader/dashboard') - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - } else if (page === 'faq') { - console.log('Navigating to faq') - window.history.pushState({}, '', '/faq') - setRoute('/faq') - } else if (page === 'backtest') { - console.log('Navigating to backtest') - window.history.pushState({}, '', '/backtest') - setRoute('/backtest') - setCurrentPage('backtest') - } else if (page === 'strategy') { - console.log('Navigating to strategy') - window.history.pushState({}, '', '/strategy') - setRoute('/strategy') - setCurrentPage('strategy') - } else if (page === 'debate') { - console.log('Navigating to debate') - window.history.pushState({}, '', '/debate') - setRoute('/debate') - setCurrentPage('debate') - } - - console.log( - 'After navigation - route:', - route, - 'currentPage:', - currentPage - ) - }} - /> -
- -
-
- ) - } - // Show landing page for root route if (route === '/' || route === '') { return } - // Allow unauthenticated users to open backtest page directly (others仍展示 Landing) + // Redirect unauthenticated users to landing page if (!user || !token) { - if (route === '/backtest' || currentPage === 'backtest') { - return ( -
- { - if (page === 'competition') { - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } - }} - /> -
- -
-
- ) - } - return - } - - // Show main app for authenticated users on other routes - if (!user || !token) { - // Default to landing page when not authenticated and no specific route return } @@ -544,41 +462,11 @@ function App() { onLanguageChange={setLanguage} user={user} onLogout={logout} - onPageChange={(page: Page) => { - console.log('Main app onPageChange called with:', page) - - if (page === 'competition') { - window.history.pushState({}, '', '/competition') - setRoute('/competition') - setCurrentPage('competition') - } else if (page === 'traders') { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - } else if (page === 'trader') { - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - } else if (page === 'backtest') { - window.history.pushState({}, '', '/backtest') - setRoute('/backtest') - setCurrentPage('backtest') - } else if (page === 'strategy') { - window.history.pushState({}, '', '/strategy') - setRoute('/strategy') - setCurrentPage('strategy') - } else if (page === 'faq') { - window.history.pushState({}, '', '/faq') - setRoute('/faq') - } else if (page === 'debate') { - window.history.pushState({}, '', '/debate') - setRoute('/debate') - setCurrentPage('debate') - } - }} + onLoginRequired={handleLoginRequired} + onPageChange={navigateToPage} /> - {/* Main Content */} + {/* Main Content with Page Transitions */}
- {currentPage === 'competition' ? ( - - ) : currentPage === 'traders' ? ( - { - setSelectedTraderId(traderId) - window.history.pushState({}, '', '/dashboard') - setRoute('/dashboard') - setCurrentPage('trader') - }} - /> - ) : currentPage === 'backtest' ? ( - - ) : currentPage === 'strategy' ? ( - - ) : currentPage === 'debate' ? ( - - ) : ( - { - setSelectedTraderId(traderId) - // 更新 URL 参数(使用 slug: name-id前4位) - const trader = traders?.find(t => t.trader_id === traderId) - if (trader) { - const url = new URL(window.location.href) - url.searchParams.set('trader', getTraderSlug(trader)) - window.history.replaceState({}, '', url.toString()) - } - }} - onNavigateToTraders={() => { - window.history.pushState({}, '', '/traders') - setRoute('/traders') - setCurrentPage('traders') - }} - exchanges={exchanges} - /> - )} + + + {currentPage === 'competition' ? ( + + ) : currentPage === 'strategy-market' ? ( + + ) : currentPage === 'traders' ? ( + { + setSelectedTraderId(traderId) + window.history.pushState({}, '', '/dashboard') + setRoute('/dashboard') + setCurrentPage('trader') + }} + /> + ) : currentPage === 'backtest' ? ( + + ) : currentPage === 'strategy' ? ( + + ) : currentPage === 'debate' ? ( + + ) : ( + { + setSelectedTraderId(traderId) + // 更新 URL 参数(使用 slug: name-id前4位) + const trader = traders?.find(t => t.trader_id === traderId) + if (trader) { + const url = new URL(window.location.href) + url.searchParams.set('trader', getTraderSlug(trader)) + window.history.replaceState({}, '', url.toString()) + } + }} + onNavigateToTraders={() => { + window.history.pushState({}, '', '/traders') + setRoute('/traders') + setCurrentPage('traders') + }} + exchanges={exchanges} + /> + )} + +
{/* Footer - Hidden on debate page */} @@ -751,6 +651,13 @@ function App() { )} + + {/* Login Required Overlay */} + setLoginOverlayOpen(false)} + featureName={loginOverlayFeature} + /> ) } @@ -1146,7 +1053,7 @@ function TraderDetailsPage({ > {getModelDisplayName( selectedTrader.ai_model.split('_').pop() || - selectedTrader.ai_model + selectedTrader.ai_model )} @@ -1351,13 +1258,13 @@ function TraderDetailsPage({ style={ pos.side === 'long' ? { - background: 'rgba(14, 203, 129, 0.1)', - color: '#0ECB81', - } + background: 'rgba(14, 203, 129, 0.1)', + color: '#0ECB81', + } : { - background: 'rgba(246, 70, 93, 0.1)', - color: '#F6465D', - } + background: 'rgba(246, 70, 93, 0.1)', + color: '#F6465D', + } } > {t( diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx index 6a718552..77c9b032 100644 --- a/web/src/components/HeaderBar.tsx +++ b/web/src/components/HeaderBar.tsx @@ -12,6 +12,7 @@ type Page = | 'trader' | 'backtest' | 'strategy' + | 'strategy-market' | 'debate' | 'faq' | 'login' @@ -27,6 +28,7 @@ interface HeaderBarProps { user?: { email: string } | null onLogout?: () => void onPageChange?: (page: Page) => void + onLoginRequired?: (featureName: string) => void } export default function HeaderBar({ @@ -38,6 +40,7 @@ export default function HeaderBar({ user, onLogout, onPageChange, + onLoginRequired, }: HeaderBarProps) { const navigate = useNavigate() const [mobileMenuOpen, setMobileMenuOpen] = useState(false) @@ -92,380 +95,67 @@ export default function HeaderBar({ {/* Desktop Menu */}
- {/* Left Side - Navigation Tabs */} -
- {isLoggedIn ? ( - // Main app navigation when logged in - <> + {/* Left Side - Navigation Tabs - Always show all tabs */} +
+ {/* Navigation tabs configuration */} + {(() => { + // Define all navigation tabs + const navTabs: { page: Page; path: string; label: string; requiresAuth: boolean }[] = [ + { page: 'competition', path: '/competition', label: t('realtimeNav', language), requiresAuth: true }, + { page: 'strategy-market', path: '/strategy-market', label: language === 'zh' ? '策略市场' : 'Market', requiresAuth: true }, + { page: 'traders', path: '/traders', label: t('configNav', language), requiresAuth: true }, + { page: 'trader', path: '/dashboard', label: t('dashboardNav', language), requiresAuth: true }, + { page: 'strategy', path: '/strategy', label: t('strategyNav', language), requiresAuth: true }, + { page: 'debate', path: '/debate', label: t('debateNav', language), requiresAuth: true }, + { page: 'backtest', path: '/backtest', label: 'Backtest', requiresAuth: true }, + { page: 'faq', path: '/faq', label: t('faqNav', language), requiresAuth: false }, + ] + + const handleNavClick = (tab: typeof navTabs[0]) => { + // If requires auth and not logged in, show login prompt + if (tab.requiresAuth && !isLoggedIn) { + onLoginRequired?.(tab.label) + return + } + // Navigate normally + if (onPageChange) { + onPageChange(tab.page) + } + navigate(tab.path) + } + + return navTabs.map((tab) => ( - - - - - - - - - - - - - - ) : ( - // Landing page navigation when not logged in - <> - { - if (currentPage !== 'competition') { - e.currentTarget.style.color = 'var(--brand-yellow)' - } - }} - onMouseLeave={(e) => { - if (currentPage !== 'competition') { - e.currentTarget.style.color = 'var(--brand-light-gray)' - } - }} - > - {/* Background for selected state */} - {currentPage === 'competition' && ( - - )} - - {t('realtimeNav', language)} - - - { - if (currentPage !== 'faq') { - e.currentTarget.style.color = 'var(--brand-yellow)' - } - }} - onMouseLeave={(e) => { - if (currentPage !== 'faq') { - e.currentTarget.style.color = 'var(--brand-light-gray)' - } - }} - > - {/* Background for selected state */} - {currentPage === 'faq' && ( - - )} - - {t('faqNav', language)} - - - )} + )) + })()}
{/* Right Side - Social Links and User Actions */} @@ -755,89 +445,40 @@ export default function HeaderBar({ borderTop: '1px solid rgba(240, 185, 11, 0.1)', }} > -
- {/* New Navigation Tabs */} - {isLoggedIn ? ( - - ) : ( - - {/* Background for selected state */} - {currentPage === 'competition' && ( - - )} - - {t('realtimeNav', language)} - - )} - {/* Only show 配置 and 看板 when logged in */} - {isLoggedIn && ( - <> + return navTabs.map((tab) => ( - - - - - - - )} + )) + })()} {/* Original Navigation Items - Only on home page */} {isHomePage && diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index f4307440..dad37c34 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -3,7 +3,7 @@ import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { Eye, EyeOff } from 'lucide-react' -import { Input } from './ui/input' +// import { Input } from './ui/input' // Removed unused import import { toast } from 'sonner' import { useSystemConfig } from '../hooks/useSystemConfig' @@ -102,261 +102,262 @@ export function LoginPage() { } return ( -
-
- {/* Logo */} -
-
- NoFx Logo +
+ {/* Background Effects */} +
+
+ + {/* Scanline Effect */} +
+ +
+ {/* Navigation - Top Bar (Mobile/Desktop Friendly) */} +
+ +
+ + {/* Terminal Header */} +
+
+
+
+ NoFx Logo +
-

- 登录 NOFX +

+ SYSTEM ACCESS

-

- {step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'} +

+ {step === 'login' ? 'Authentication Protocol v3.0' : 'Multi-Factor Verification'}

- {/* Login Form */} -
- {adminMode ? ( -
-
- - setAdminPassword(e.target.value)} - className="w-full px-3 py-2 rounded" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder="请输入管理员密码" - required - /> + {/* Terminal Output / Form Container */} +
+
+ + {/* Window Bar */} +
+
+
window.location.href = '/'} + title="Close / Return Home" + >
+
+
+
+
+ login.exe +
+
+ +
+ {/* Status Output */} +
+
+ + Initiating handshake...
- - {error && ( -
- {error} -
- )} - - - - ) : step === 'login' ? ( -
-
- - setEmail(e.target.value)} - placeholder={t('emailPlaceholder', language)} - required - /> +
+ + Target: NOFX CORE HUB
+
+ + Status: AWAITING CREDENTIALS +
+
-
- -
- setPassword(e.target.value)} - className="pr-10" - placeholder={t('passwordPlaceholder', language)} + {adminMode ? ( + +
+ + setAdminPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-700 text-white font-mono" + placeholder="ENTER_ROOT_PASSWORD" required /> -
-
- -
-
- {error && ( -
- {error} -
- )} + {error && ( +
+ [ERROR]: {error} +
+ )} - - - ) : ( -
-
-
📱
-

- {t('scanQRCodeInstructions', language)} -
- {t('enterOTPCode', language)} -

-
- -
- - - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} -
- )} - -
- -
-
- )} + + ) : step === 'login' ? ( +
+
+
+ + setEmail(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-700 text-white font-mono" + placeholder="user@nofx.os" + required + /> +
+ +
+
+ +
+ +
+ setPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-700 text-white font-mono pr-10" + placeholder="••••••••••••" + required + /> + +
+
+ +
+
+
+ + {error && ( +
+ {error} +
+ )} + + +
+ ) : ( +
+
+
+ 🔐 +
+

+ {t('scanQRCodeInstructions', language)}
+ {t('enterOTPCode', language)} +

+
+ +
+ + + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full bg-black border border-zinc-700 rounded px-4 py-4 text-center text-2xl tracking-[0.5em] font-mono text-white focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800" + placeholder="000000" + maxLength={6} + required + autoFocus + /> +
+ + {error && ( +
+ [ACCESS DENIED]: {error} +
+ )} + +
+ + +
+
+ )} +
+ + {/* Terminal Footer Info */} +
+
SECURE_CONNECTION: ENCRYPTED
+
{new Date().toISOString().split('T')[0]}
+
{/* Register Link */} {!adminMode && registrationEnabled && ( -
-

- 还没有账户?{' '} +

+

+ NEW_USER_DETECTED?{' '}

+
)}
diff --git a/web/src/components/LoginRequiredOverlay.tsx b/web/src/components/LoginRequiredOverlay.tsx new file mode 100644 index 00000000..3d63359c --- /dev/null +++ b/web/src/components/LoginRequiredOverlay.tsx @@ -0,0 +1,163 @@ +import { motion, AnimatePresence } from 'framer-motion' +import { LogIn, UserPlus, X, AlertTriangle, Terminal } from 'lucide-react' +import { useLanguage } from '../contexts/LanguageContext' + +interface LoginRequiredOverlayProps { + isOpen: boolean + onClose: () => void + featureName?: string +} + +export function LoginRequiredOverlay({ isOpen, onClose, featureName }: LoginRequiredOverlayProps) { + const { language } = useLanguage() + + const texts = { + zh: { + title: '系统访问受限', + subtitle: featureName ? `访问「${featureName}」需要更高权限` : '此模块需要授权访问', + description: '初始化身份验证协议以解锁完整系统功能:AI 交易员配置、策略市场数据流、回测模拟核心。', + benefits: [ + 'AI 交易员控制权', + '高频策略核心市场', + '历史数据回测引擎', + '全系统数据可视化' + ], + login: '执行登录指令', + register: '注册新用户 ID', + later: '中止操作' + }, + en: { + title: 'SYSTEM ACCESS DENIED', + subtitle: featureName ? `Module "${featureName}" requires elevated privileges` : 'Authorization required for this module', + description: 'Initialize authentication protocol to unlock full system capabilities: AI Trader configuration, Strategy Market data streams, and Backtest Simulation core.', + benefits: [ + 'AI Trader Control', + 'HFT Strategy Market', + 'Historical Backtest Engine', + 'Full System Visualization' + ], + login: 'EXECUTE LOGIN', + register: 'REGISTER NEW ID', + later: 'ABORT' + } + } + + const t = texts[language] + + return ( + + {isOpen && ( + + {/* Scanline Effect */} +
+ + e.stopPropagation()} + > + {/* Terminal Window Header */} +
+
+ + auth_protocol.exe +
+ +
+ + {/* Main Content */} +
+ {/* Background Grid */} +
+ +
+ {/* Flashing Access Denied */} +
+
+
+
+ + {language === 'zh' ? '访问被拒绝' : 'ACCESS DENIED'} +
+
+
+ + {/* Terminal Text */} +
+
+

{t.title}

+

{t.subtitle}

+
+ +
+

+ $ + {t.description} +

+
+ +
+ {t.benefits.map((benefit, i) => ( +
+ {benefit} +
+ ))} +
+
+ + {/* Action Buttons */} + + +
+ +
+ +
+
+ + {/* Corner Accents */} +
+
+ +
+
+ )} +
+ ) +} diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 8a310d61..9cfc10f6 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -6,14 +6,15 @@ import { getSystemConfig } from '../lib/config' import { toast } from 'sonner' import { copyWithToast } from '../lib/clipboard' import { Eye, EyeOff } from 'lucide-react' -import { Input } from './ui/input' +// import { Input } from './ui/input' // Removed unused import import PasswordChecklist from 'react-password-checklist' import { RegistrationDisabled } from './RegistrationDisabled' +import { WhitelistFullPage } from './WhitelistFullPage' export function RegisterPage() { const { language } = useLanguage() const { register, completeRegistration } = useAuth() - const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp'>( + const [step, setStep] = useState<'register' | 'setup-otp' | 'verify-otp' | 'whitelist-full'>( 'register' ) const [email, setEmail] = useState('') @@ -49,6 +50,11 @@ export function RegisterPage() { return } + // 如果白名单已满,显示容量已满页面 + if (step === 'whitelist-full') { + return setStep('register')} /> + } + const handleRegister = async (e: React.FormEvent) => { e.preventDefault() setError('') @@ -66,20 +72,54 @@ export function RegisterPage() { setLoading(true) - const result = await register(email, password, betaCode.trim() || undefined) + try { + const result = await register(email, password, betaCode.trim() || undefined) - if (result.success && result.userID) { - setUserID(result.userID) - setOtpSecret(result.otpSecret || '') - setQrCodeURL(result.qrCodeURL || '') - setStep('setup-otp') - } else { - // Only business errors reach here (system/network errors shown via toast) - const msg = result.message || t('registrationFailed', language) - setError(msg) + // Helper to check for whitelist errors + const isWhitelistError = (msg: string) => { + const lowerMsg = msg.toLowerCase() + return lowerMsg.includes('whitelist') || + lowerMsg.includes('capacity') || + lowerMsg.includes('limit') || + lowerMsg.includes('permission denied') || + lowerMsg.includes('not on whitelist') + } + + if (result.success && result.userID) { + setUserID(result.userID) + setOtpSecret(result.otpSecret || '') + setQrCodeURL(result.qrCodeURL || '') + setStep('setup-otp') + } else { + // Check for whitelist/capacity limit error + const msg = result.message || t('registrationFailed', language) + if (isWhitelistError(msg)) { + setStep('whitelist-full') + return + } + setError(msg) + toast.error(msg) + } + } catch (e) { + console.error('Registration error:', e) + const errorMsg = e instanceof Error ? e.message : 'Registration failed due to server error' + + // Check for whitelist error in catch block too + const lowerMsg = errorMsg.toLowerCase() + if (lowerMsg.includes('whitelist') || + lowerMsg.includes('capacity') || + lowerMsg.includes('limit') || + lowerMsg.includes('permission denied') || + lowerMsg.includes('not on whitelist')) { + setStep('whitelist-full') + return + } + + setError(errorMsg) + toast.error(errorMsg) + } finally { + setLoading(false) } - - setLoading(false) } const handleSetupComplete = () => { @@ -108,437 +148,326 @@ export function RegisterPage() { } return ( -
-
- {/* Logo */} -
-
- NoFx Logo +
+ {/* Background Effects */} +
+
+ + {/* Scanline Effect */} +
+ +
+ {/* Navigation - Top Bar (Mobile/Desktop Friendly) */} +
+ +
+ + {/* Terminal Header */} +
+
+
+
+ NoFx Logo +
-

- {t('appTitle', language)} +

+ NEW_USER ONBOARDING

-

- {step === 'register' && t('registerTitle', language)} - {step === 'setup-otp' && t('setupTwoFactor', language)} - {step === 'verify-otp' && t('verifyOTP', language)} +

+ {step === 'register' && 'Initializing Registration Sequence...'} + {step === 'setup-otp' && 'Configuring Security Protocols...'} + {step === 'verify-otp' && 'Finalizing Authentication...'}

- {/* Registration Form */} -
- {step === 'register' && ( -
-
- - setEmail(e.target.value)} - placeholder={t('emailPlaceholder', language)} - required - /> -
+ {/* Terminal Output / Form Container */} +
+
-
- -
- setPassword(e.target.value)} - className="pr-10" - placeholder={t('passwordPlaceholder', language)} - required - /> - -
-
- -
- -
- setConfirmPassword(e.target.value)} - className="pr-10" - placeholder={t('confirmPasswordPlaceholder', language)} - required - /> - -
-
- - {/* 密码规则清单(通过才允许提交) */} + {/* Window Bar */} +
+
-
- {t('passwordRequirements', language)} -
- setPasswordValid(isValid)} - /> -
+ className="w-2.5 h-2.5 rounded-full bg-red-500/50 hover:bg-red-500 cursor-pointer transition-colors" + onClick={() => window.location.href = '/'} + title="Close / Return Home" + >
+
+
+
+
+ setup_account.sh +
+
- {betaMode && ( +
+ {/* Status Output */} +
+
+ + System Check: READY +
+
+ + Mode: {betaMode ? 'CLOSED_BETA CA1' : 'PUBLIC'} +
+
+ + {step === 'register' && ( +
- + - setBetaCode( - e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase() - ) - } - className="w-full px-3 py-2 rounded font-mono" - style={{ - background: '#0B0E11', - border: '1px solid #2B3139', - color: '#EAECEF', - }} - placeholder="请输入6位内测码" - maxLength={6} - required={betaMode} + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono" + placeholder="user@nofx.os" + required /> -

- 内测码由6位字母数字组成,区分大小写 -

-
- )} - - {error && ( -
- {error} -
- )} - - - - )} - - {step === 'setup-otp' && ( -
-
-
📱
-

- {t('setupTwoFactor', language)} -

-

- {t('setupTwoFactorDesc', language)} -

-
- -
-
-

- {t('authStep1Title', language)} -

-

- {t('authStep1Desc', language)} -

-
-

- {t('authStep2Title', language)} -

-

- {t('authStep2Desc', language)} -

- - {qrCodeURL && ( -
-

- {t('qrCodeHint', language)} -

-
- QR Code -
-
- )} - -
-

- {t('otpSecret', language)} -

-
- - {otpSecret} - +
+
+ +
+ setPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" + placeholder="••••••••" + required + /> +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono pr-10" + placeholder="••••••••" + required + /> +
-
-

- {t('authStep3Title', language)} -

-

- {t('authStep3Desc', language)} -

+
+
+
+ Password Strength Protocol +
+
+ setPasswordValid(isValid)} + iconSize={10} + /> +
-
- -
- )} + {betaMode && ( +
+ + setBetaCode(e.target.value.replace(/[^a-z0-9]/gi, '').toLowerCase())} + className="w-full bg-black/50 border border-zinc-700 rounded px-4 py-3 text-sm focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800 text-white font-mono tracking-widest" + placeholder="XXXXXX" + maxLength={6} + required={betaMode} + /> +

* CASE SENSITIVE ALPHANUMERIC

+
+ )} - {step === 'verify-otp' && ( -
-
-
🔐
-

- {t('enterOTPCode', language)} -
- {t('completeRegistrationSubtitle', language)} -

-
+ {error && ( +
+ [REGISTRATION_ERROR]: {error} +
+ )} -
- - - setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) - } - className="w-full px-3 py-2 rounded text-center text-2xl font-mono" - style={{ - background: 'var(--brand-black)', - border: '1px solid var(--panel-border)', - color: 'var(--brand-light-gray)', - }} - placeholder={t('otpPlaceholder', language)} - maxLength={6} - required - /> -
- - {error && ( -
- {error} -
- )} - -
+ + )} + + {step === 'setup-otp' && ( +
+
+
SCAN_QR_CODE_SEQUENCE
+ {qrCodeURL ? ( +
+ QR Code +
+ ) : ( +
+ )} +
+

Backup Secret Key

+
+ {otpSecret} + +
+
+
+ +
+
+ 01 +

Install Google Authenticator or Authy on your mobile device.

+
+
+ 02 +

Scan the QR code above or manually enter the secret key.

+
+
+ 03 +

Proceed to verify the generated 6-digit token.

+
+
+ + +
+ )} + + {step === 'verify-otp' && ( +
+
+

+ ENTER 6-DIGIT SECURITY TOKEN TO FINALIZE ONBOARDING +

+
+ +
+ + setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6)) + } + className="w-full bg-black border border-zinc-700 rounded px-4 py-4 text-center text-3xl tracking-[0.5em] font-mono text-white focus:border-nofx-gold focus:ring-1 focus:ring-nofx-gold/50 outline-none transition-all placeholder-zinc-800" + placeholder="000000" + maxLength={6} + required + autoFocus + /> +
+ + {error && ( +
+ [VERIFICATION_FAILED]: {error} +
+ )} + -
- - )} + + )} + +
+ + {/* Terminal Footer Info */} +
+
ENCRYPTION: AES-256
+
SECURE_REGISTRY
+
{/* Login Link */} {step === 'register' && ( -
-

- 已有账户?{' '} +

+

+ EXISTING_OPERATOR?{' '}

+
)} +
) diff --git a/web/src/components/WhitelistFullPage.tsx b/web/src/components/WhitelistFullPage.tsx new file mode 100644 index 00000000..2d7f2f1d --- /dev/null +++ b/web/src/components/WhitelistFullPage.tsx @@ -0,0 +1,124 @@ +import { motion } from 'framer-motion' +import { ShieldAlert, ArrowLeft, Twitter, Send, Lock } from 'lucide-react' +import { OFFICIAL_LINKS } from '../constants/branding' + +interface WhitelistFullPageProps { + onBack?: () => void +} + +export function WhitelistFullPage({ onBack }: WhitelistFullPageProps) { + const handleBackToLogin = () => { + if (onBack) { + onBack() + } else { + window.location.href = '/login' + } + } + + return ( +
+ {/* Background Grid & Scanlines */} +
+
+
+ + +
+ + {/* Top Bar */} +
+
+
+
+
+
+
+ ACCESS_DENIED // ERROR_403 +
+
+ +
+ {/* Icon */} +
+
+
+ +
+
+ + {/* Title */} +

+ RESTRICTED ACCESS +

+ +
+ + {/* Description */} +

+ [SYSTEM_MESSAGE]: YOUR IDENTIFIER IS NOT ON THE ACTIVE WHITELIST. +

+ Platform capacity limits have been reached for the current beta phase. Prioritized access is currently reserved for authorized operators only. +

+ + {/* Info Box */} +
+
+ +
+

Authorization Protocol

+

+ Access is rolled out in batches. If you believe this is an error, please verify your credentials or contact system administrators. +

+
+
+
+ + {/* Action Buttons */} +
+ + + +
+ +
+ + {/* Footer */} +
+ ERR_CODE: WLIST_0x403 // SECURITY_LAYER_ACTIVE +
+ +
+
+
+ ) +} diff --git a/web/src/components/landing/FooterSection.tsx b/web/src/components/landing/FooterSection.tsx index bcaefe4a..d6b46bf8 100644 --- a/web/src/components/landing/FooterSection.tsx +++ b/web/src/components/landing/FooterSection.tsx @@ -30,9 +30,13 @@ export default function FooterSection({ language }: FooterSectionProps) { { name: 'Pull Requests', href: 'https://github.com/NoFxAiOS/nofx/pulls' }, ], supporters: [ + { name: 'Binance', href: 'https://www.binance.com/join?ref=NOFXENG' }, + { name: 'Bybit', href: 'https://partner.bybit.com/b/83856' }, + { name: 'OKX', href: 'https://www.okx.com/join/1865360' }, + { name: 'Bitget', href: 'https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172' }, + { name: 'Hyperliquid', href: 'https://app.hyperliquid.xyz/join/AITRADING' }, { name: 'Aster DEX', href: 'https://www.asterdex.com/en/referral/fdfc0e' }, - { name: 'Binance', href: 'https://www.maxweb.red/join?ref=NOFXAI' }, - { name: 'Hyperliquid', href: 'https://hyperliquid.xyz/' }, + { name: 'Lighter', href: 'https://app.lighter.xyz/?referral=68151432' }, ], } @@ -123,21 +127,20 @@ export default function FooterSection({ language }: FooterSectionProps) {

{t('supporters', language)}

- +
diff --git a/web/src/components/landing/core/TerminalHero.tsx b/web/src/components/landing/core/TerminalHero.tsx index 6c6d5489..64f790ee 100644 --- a/web/src/components/landing/core/TerminalHero.tsx +++ b/web/src/components/landing/core/TerminalHero.tsx @@ -54,9 +54,8 @@ export default function TerminalHero() { } } + // Only fetch once on mount, cache the result fetchPrices() - const interval = setInterval(fetchPrices, 5000) - return () => clearInterval(interval) }, []) return ( diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 0464994e..7cf07d4c 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -192,27 +192,38 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { requestBody.beta_code = betaCode } - const result = await httpClient.post<{ - user_id: string - otp_secret: string - qr_code_url: string - message: string - }>('/api/register', requestBody) + try { + const result = await httpClient.post<{ + user_id: string + otp_secret: string + qr_code_url: string + message: string + }>('/api/register', requestBody) - if (result.success && result.data) { - return { - success: true, - userID: result.data.user_id, - otpSecret: result.data.otp_secret, - qrCodeURL: result.data.qr_code_url, - message: result.message || result.data.message, + if (result.success && result.data) { + return { + success: true, + userID: result.data.user_id, + otpSecret: result.data.otp_secret, + qrCodeURL: result.data.qr_code_url, + message: result.message || result.data.message, + } } - } - // Only business errors reach here (system/network errors were intercepted) - return { - success: false, - message: result.message || 'Registration failed', + // Only business errors reach here (system/network errors were intercepted) + return { + success: false, + message: result.message || 'Registration failed', + } + } catch (error) { + console.error('Auth register error:', error); + // Re-throw if it's a critical error, or return structured error + // Since httpClient throws on 500, we should return a structured error response + // to let the UI display it gracefully without crashing. + return { + success: false, + message: error instanceof Error ? error.message : 'Detailed server error' + } } } diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 1934ad59..a37fe1ad 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import HeaderBar from '../components/HeaderBar' import LoginModal from '../components/landing/LoginModal' +import { LoginRequiredOverlay } from '../components/LoginRequiredOverlay' import FooterSection from '../components/landing/FooterSection' import TerminalHero from '../components/landing/core/TerminalHero' import LiveFeed from '../components/landing/core/LiveFeed' @@ -11,10 +12,17 @@ import { useLanguage } from '../contexts/LanguageContext' export function LandingPage() { const [showLoginModal, setShowLoginModal] = useState(false) + const [loginOverlayOpen, setLoginOverlayOpen] = useState(false) + const [loginOverlayFeature, setLoginOverlayFeature] = useState('') const { user, logout } = useAuth() const { language, setLanguage } = useLanguage() const isLoggedIn = !!user + const handleLoginRequired = (featureName: string) => { + setLoginOverlayFeature(featureName) + setLoginOverlayOpen(true) + } + return ( <> { - if (page === 'competition') { - window.location.href = '/competition' - } else if (page === 'traders') { - window.location.href = '/traders' - } else if (page === 'trader') { - window.location.href = '/dashboard' + const pathMap: Record = { + 'competition': '/competition', + 'strategy-market': '/strategy-market', + 'traders': '/traders', + 'trader': '/dashboard', + 'backtest': '/backtest', + 'strategy': '/strategy', + 'debate': '/debate', + 'faq': '/faq', + } + const path = pathMap[page] + if (path) { + window.location.href = path } }} /> @@ -53,6 +69,12 @@ export function LandingPage() { language={language} /> )} + + setLoginOverlayOpen(false)} + featureName={loginOverlayFeature} + />
) diff --git a/web/src/pages/StrategyMarketPage.tsx b/web/src/pages/StrategyMarketPage.tsx new file mode 100644 index 00000000..60af8739 --- /dev/null +++ b/web/src/pages/StrategyMarketPage.tsx @@ -0,0 +1,515 @@ +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import useSWR from 'swr' +import { + TrendingUp, + Shield, + Zap, + Eye, + EyeOff, + Copy, + Check, + Hexagon, + Layers, + Target, + Activity, + Terminal, + Cpu, + Database +} from 'lucide-react' +import { useLanguage } from '../contexts/LanguageContext' +import { useAuth } from '../contexts/AuthContext' +import { toast } from 'sonner' // Ensure sonner is installed or stick to custom toast if preferred + +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 = { + 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 +} + +export function StrategyMarketPage() { + const { language } = useLanguage() + const { token, user } = useAuth() + const [searchQuery, setSearchQuery] = useState('') + const [selectedCategory, setSelectedCategory] = useState('all') + const [copiedId, setCopiedId] = useState(null) + + const texts = { + zh: { + title: '策略市场', + subtitle: 'STRATEGY MARKETPLACE', + description: '发现、学习并复用社区精英交易员的策略配置', + search: '搜索参数...', + all: '全部协议', + popular: '热门配置', + recent: '最新提交', + myStrategies: '我的库', + noStrategies: '无信号', + noStrategiesDesc: '当前频段未检测到策略信号', + author: 'OPERATOR', + createdAt: 'TIMESTAMP', + viewConfig: 'DECRYPT CONFIG', + hideConfig: 'ENCRYPT', + copyConfig: 'CLONE CONFIG', + copied: 'COPIED', + configHidden: 'ENCRYPTED', + configHiddenDesc: '配置参数已加密', + indicators: 'INDICATORS', + maxPositions: 'POS_LIMIT', + maxLeverage: 'LEV_MAX', + shareYours: 'UPLOAD_STRATEGY', + makePublic: 'PUBLISH', + loading: 'INITIALIZING...' + }, + en: { + title: 'STRATEGY MARKET', + subtitle: 'GLOBAL STRATEGY DATABASE', + description: 'Discover, analyze, and clone high-performance trading algorithms', + search: 'SEARCH PARAMETERS...', + all: 'ALL PROTOCOLS', + popular: 'TRENDING', + recent: 'LATEST', + myStrategies: 'MY LIBRARY', + noStrategies: 'NO SIGNAL', + noStrategiesDesc: 'No strategic signals detected in this frequency', + author: 'OPERATOR', + createdAt: 'TIMESTAMP', + viewConfig: 'DECRYPT CONFIG', + hideConfig: 'ENCRYPT', + copyConfig: 'CLONE CONFIG', + copied: 'COPIED', + configHidden: 'ENCRYPTED', + configHiddenDesc: 'Configuration parameters encrypted', + indicators: 'INDICATORS', + maxPositions: 'POS_LIMIT', + maxLeverage: 'LEV_MAX', + shareYours: 'UPLOAD_STRATEGY', + makePublic: 'PUBLISH', + loading: 'INITIALIZING...' + } + } + + const t = texts[language] + + // 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 filteredStrategies = strategies?.filter(s => { + if (searchQuery) { + const query = searchQuery.toLowerCase() + return s.name.toLowerCase().includes(query) || + s.description?.toLowerCase().includes(query) + } + return true + }) || [] + + 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(t.copied) + setTimeout(() => setCopiedId(null), 2000) + } catch (err) { + console.error('Failed to copy:', err) + } + } + + 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) => { + if (!config?.indicators) return [] + const indicators = [] + if (config.indicators.enable_ema) indicators.push('EMA') + if (config.indicators.enable_macd) indicators.push('MACD') + if (config.indicators.enable_rsi) indicators.push('RSI') + if (config.indicators.enable_atr) indicators.push('ATR') + if (config.indicators.enable_boll) indicators.push('BOLL') + if (config.indicators.enable_volume) indicators.push('VOL') + if (config.indicators.enable_oi) indicators.push('OI') + if (config.indicators.enable_funding_rate) indicators.push('FR') + return indicators + } + + return ( +
+ {/* Background Grid & Scanlines */} +
+
+
+ +
+ + {/* Header Section */} +
+
+ SYSTEM_STATUS: ONLINE +
+ MARKET_UPLINK: ESTABLISHED +
+ +
+
+
+ +
+
+

+ {t.title} +

+

+ // {t.subtitle} +

+
+
+

+ {t.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" + /> +
+
+
+
+
+ + {/* Category Filter */} +
+ {['all', 'popular', 'recent'].map((cat) => ( + + ))} +
+
+ + {/* Loading State */} + {isLoading && ( +
+
+
+
+
+ +
+
+

{t.loading}

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

+ [{t.noStrategies}] +

+

{t.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 */} +
+
+ {t.author} + @{strategy.author_email?.split('@')[0] || 'UNKNOWN'} +
+
+ {t.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.risk_control && ( +
+
+
+ LEV + {strategy.config.risk_control.btc_eth_max_leverage || '-'}x +
+
+ POS + {strategy.config.risk_control.max_positions || '-'} +
+
+ +
+ )} +
+ ) : ( +
+ + {t.configHiddenDesc} +
+ )} +
+ + {/* Action Button */} +
+ {strategy.config_visible && strategy.config ? ( + + ) : ( + + )} +
+ +
+
+ ) + })} +
+
+ )} + + {/* CTA - Share Strategy */} + {user && token && ( + +
window.location.href = '/strategy'}> +
+
+ +
+
{t.shareYours}
+
CONTRIBUTE TO THE GLOBAL DATABASE
+
+
+
+ INITIALIZE_UPLOAD -> +
+
+
+
+ )} + +
+
+ ) +}