diff --git a/src/App.tsx b/src/App.tsx index e28278e..9e0d424 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { SuccessPage } from '@/components/SuccessPage'; import { VerifyEmail } from '@/components/VerifyEmail'; import { AuthCallback } from '@/components/AuthCallback'; import { DashboardPage } from '@/features/dashboard/DashboardPage'; +import { LiveMarketDashboard } from '@/pages/LiveMarketDashboard'; import { AuthGuard } from '@/features/auth/AuthGuard'; import { useUserStore } from '@/stores/userStore'; import { ROUTES } from '@/constants/routes.constants'; @@ -97,7 +98,7 @@ function AppContent() { path={ROUTES.LOGIN} element={ navigate(ROUTES.DASHBOARD)} + onLoginSuccess={() => navigate(ROUTES.EXPLORE)} onNavigateToSignup={() => navigate(ROUTES.REGISTER)} onNavigateToForgot={() => navigate(ROUTES.FORGOT_PASSWORD)} /> @@ -131,6 +132,7 @@ function AppContent() { path={ROUTES.RESET_PASSWORD} element={ navigate(ROUTES.LOGIN)} />} /> + } /> {/* Protected Routes */} { useEffect(() => { if (isAuthenticated) { // Clean up fragment from url and navigate to dashboard - window.history.replaceState(null, '', '/#/dashboard'); - navigate(ROUTES.DASHBOARD); + window.history.replaceState(null, '', '/#/explore'); + navigate(ROUTES.EXPLORE); } else { navigate(ROUTES.LOGIN); } diff --git a/src/constants/routes.constants.ts b/src/constants/routes.constants.ts index ff0a582..481b88f 100644 --- a/src/constants/routes.constants.ts +++ b/src/constants/routes.constants.ts @@ -7,6 +7,7 @@ export const ROUTES = { VERIFY_EMAIL: '/verify-email', AUTH_CALLBACK: '/auth/callback', SUCCESS: '/success', + EXPLORE: '/explore', DASHBOARD: '/dashboard', } as const; diff --git a/src/features/globe/GlobeView.tsx b/src/features/globe/GlobeView.tsx index b6c02fc..984e64b 100644 --- a/src/features/globe/GlobeView.tsx +++ b/src/features/globe/GlobeView.tsx @@ -1,6 +1,8 @@ import React, { useRef } from 'react'; import { useGlobe } from './useGlobe'; import { NATIONS } from './nations.constants'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '@/constants/routes.constants'; import '@/styles/components/overlay.css'; import '@/styles/components/country-list.css'; @@ -16,6 +18,7 @@ interface GlobeViewProps { */ export const GlobeView: React.FC = ({ onEnterApp }) => { const containerRef = useRef(null); + const navigate = useNavigate(); useGlobe(containerRef); return ( @@ -28,7 +31,7 @@ export const GlobeView: React.FC = ({ onEnterApp }) => {
- + {/* Violet Comets */}
@@ -47,7 +50,7 @@ export const GlobeView: React.FC = ({ onEnterApp }) => {
- + {/* Big stars */}
@@ -70,7 +73,7 @@ export const GlobeView: React.FC = ({ onEnterApp }) => { x Discipline Co-pilot - +
🔍 @@ -92,7 +95,7 @@ export const GlobeView: React.FC = ({ onEnterApp }) => { {/* Hero Content */}

Look First Then Leap

- +
Save up to 17% 🤑
+ +
{/* ── Section 2: Globe (100vh) ─────────────────────────────────── */}
- +
{ + const [marketData, setMarketData] = useState({ gainers: [], losers: [] }); + const [activeTab, setActiveTab] = useState('Stocks'); + + useEffect(() => { + // 1. Connect to the FastAPI WebSocket Bridge + const ws = new WebSocket("ws://localhost:8000/dashboard/ws/market"); + + // 2. Listen for live updates being pushed from FastAPI + ws.onmessage = (event) => { + try { + const liveData = JSON.parse(event.data); + console.log("Live Update Received:", liveData); + // Instantly update the React state + setMarketData(liveData); + } catch (err) { + console.error("Error parsing websocket message", err); + } + }; + + // 3. Clean up the connection if the user leaves the page + return () => ws.close(); + }, []); + + const formatPrice = (price: number) => { + if (!price) return "0.00"; + if (price < 0.01) { + return price.toExponential(5); + } + return price.toFixed(2); + }; + + const getInitials = (symbol: string) => { + return symbol.substring(0, 2).toUpperCase(); + }; + + return ( +
+
+
+ {['Stocks', 'Crypto', 'Futures', 'Forex', 'Economy', 'Brokers'].map(tab => ( + + ))} +
+
+ +
+ {/* Gainers */} +
+
+

+ {activeTab === 'Stocks' ? 'Stock' : activeTab} gainers + > +

+
+ +
+ {marketData.gainers && marketData.gainers.length > 0 ? ( + marketData.gainers.map((stock: any) => ( +
+
+
{getInitials(stock.symbol)}
+
+
{stock.symbol.split('.')[0]}
+
{stock.symbol}
+
+
+
+ {formatPrice(stock.price)} + {stock.currency || 'USD'} +
+
+
+{Math.abs(stock.percent_change).toFixed(2)}%
+
+
+ )) + ) : ( +
Waiting for data...
+ )} +
+
+ + {/* Losers */} +
+
+

+ {activeTab === 'Stocks' ? 'Stock' : activeTab} losers + > +

+
+ +
+ {marketData.losers && marketData.losers.length > 0 ? ( + marketData.losers.map((stock: any) => ( +
+
+
{getInitials(stock.symbol)}
+
+
{stock.symbol.split('.')[0]}
+
{stock.symbol}
+
+
+
+ {formatPrice(stock.price)} + {stock.currency || 'USD'} +
+
+
+ -{Math.abs(stock.percent_change).toFixed(2)}% +
+
+
+ )) + ) : ( +
Waiting for data...
+ )} +
+
+
+
+ ); +}; diff --git a/src/styles/components/live-market.css b/src/styles/components/live-market.css new file mode 100644 index 0000000..ad31869 --- /dev/null +++ b/src/styles/components/live-market.css @@ -0,0 +1,211 @@ +/* Ultra-Precise Match to the User's Screenshot */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +.market-dashboard { + background-color: #ffffff; + min-height: 100vh; + padding: 30px 40px; + color: #111111; + font-family: 'Inter', sans-serif; + box-sizing: border-box; +} + +.top-nav { + display: flex; + justify-content: center; + margin-bottom: 40px; +} + +.nav-pills { + display: flex; + background: #ffffff; + border: 1px solid #eaeaea; + border-radius: 40px; + padding: 4px; + gap: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.nav-pill { + background: transparent; + border: none; + padding: 8px 16px; + border-radius: 40px; + font-size: 0.9rem; + font-weight: 500; + color: #555555; + cursor: pointer; +} + +.nav-pill:hover { + color: #111111; +} + +.nav-pill.active { + background: #2a2a2a; + color: #ffffff; +} + +.market-sections { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + max-width: 1300px; + margin: 0 auto; +} + +.market-list-section { + background: #ffffff; +} + +.section-header { + margin-bottom: 20px; +} + +.section-header h2 { + font-size: 1.5rem; + font-weight: 700; + margin: 0; + color: #111111; + display: flex; + align-items: center; +} + +.heading-arrow { + margin-left: 2px; + font-size: 1.4rem; + font-weight: 600; + color: #111111; +} + +.stock-list { + display: flex; + flex-direction: column; +} + +.stock-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 0; + border-bottom: 1px solid #f6f6f6; +} + +.stock-row:last-child { + border-bottom: none; +} + +.stock-left { + display: flex; + align-items: center; + gap: 12px; + width: 220px; +} + +.stock-icon { + width: 38px; + height: 38px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 0.9rem; + font-weight: 700; +} + +.icon-blue { + background-color: #0d6de0; +} + +.icon-purple { + background-color: #6a309e; +} + +.stock-names { + display: flex; + flex-direction: column; + gap: 4px; +} + +.stock-name { + font-weight: 500; + font-size: 0.95rem; + color: #111111; +} + +.stock-ticker { + background-color: #f7f7f7; + color: #333333; + font-size: 0.65rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + display: inline-block; + width: fit-content; + text-transform: uppercase; + letter-spacing: 0.2px; +} + +.stock-middle { + display: flex; + align-items: baseline; + gap: 2px; + flex: 1; + justify-content: flex-end; + padding-right: 40px; +} + +.price { + font-weight: 500; + font-size: 0.95rem; + color: #111111; +} + +.currency { + font-size: 0.6rem; + color: #555555; + font-weight: 600; +} + +.stock-right { + display: flex; + justify-content: flex-end; + width: 90px; +} + +.pill-gain { + background-color: #1d9c73; + color: #ffffff; + font-weight: 600; + font-size: 0.8rem; + padding: 4px 8px; + border-radius: 4px; + min-width: 60px; + text-align: center; +} + +.pill-loss { + background-color: #cc3a4a; + color: #ffffff; + font-weight: 600; + font-size: 0.8rem; + padding: 4px 8px; + border-radius: 4px; + min-width: 60px; + text-align: center; +} + +.loading-state { + text-align: center; + padding: 40px; + color: #888888; +} + +@media (max-width: 1200px) { + .market-sections { + grid-template-columns: 1fr; + gap: 40px; + } +} \ No newline at end of file