From 1e7e234026595fb5640572ba5b747d6b4b66fdea Mon Sep 17 00:00:00 2001 From: akashrane Date: Sun, 14 Dec 2025 13:57:16 -0500 Subject: [PATCH 1/5] Add columns view mode to CustomDashboard --- .../pages/CustomDashboard/CustomDashboard.css | 33 ++++++++++++++++++- .../pages/CustomDashboard/CustomDashboard.tsx | 12 +++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/CustomDashboard/CustomDashboard.css b/frontend/src/pages/CustomDashboard/CustomDashboard.css index f922eec..5ddd8ad 100644 --- a/frontend/src/pages/CustomDashboard/CustomDashboard.css +++ b/frontend/src/pages/CustomDashboard/CustomDashboard.css @@ -317,6 +317,36 @@ gap: 1rem; } +.saved-stocks-container.columns { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +/* Columns specific adjustments to prevent overflow */ +.saved-stocks-container.columns .stock-item-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.saved-stocks-container.columns .stock-info { + min-width: 180px; +} + +.saved-stocks-container.columns .stock-actions { + flex-wrap: wrap; + gap: 0.25rem; +} + +.saved-stocks-container.columns .saved-stock-item button { + padding: 4px 8px; + font-size: 0.75rem; +} + +.saved-stocks-container.columns .saved-stock-item { + overflow-x: hidden; +} + .saved-stocks-container.dragging-active { background: rgba(0, 212, 170, 0.05); border-radius: 8px; @@ -637,7 +667,8 @@ justify-content: center; } - .saved-stocks-container.grid { + .saved-stocks-container.grid, + .saved-stocks-container.columns { grid-template-columns: 1fr; } diff --git a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx index 821dbb8..d6ec446 100644 --- a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx +++ b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx @@ -178,7 +178,7 @@ const CustomDashboard: React.FC = () => { const [isLoadingIndices, setIsLoadingIndices] = useState(true); const [error, setError] = useState(null); const [isRealtimeConnected, setIsRealtimeConnected] = useState(false); - const [viewMode, setViewMode] = useState<'list' | 'grid'>('list'); + const [viewMode, setViewMode] = useState<'list' | 'columns' | 'grid'>('list'); const [isDragging, setIsDragging] = useState(false); const [isMarketOpen, setIsMarketOpen] = useState(null); const [marketStatus, setMarketStatus] = useState(''); @@ -669,6 +669,14 @@ const CustomDashboard: React.FC = () => { > List + + + +
+

+ Receive a daily morning digest of breaking news for your {symbols.length} portfolio holdings: +
+ {symbols.slice(0, 5).join(', ')}{symbols.length > 5 ? '...' : ''} +

+ +
+
+ setEmail(e.target.value)} + required + className="email-input" + /> +
+ + {status === 'error' &&

{message}

} + {status === 'success' &&

{message}

} + + +
+ +
+ +
+ +

+ We'll only send one email per day at 8:00 AM. Unsubscribe anytime. +

+
+ + + ); +}; + +export default EmailSubscriptionModal; diff --git a/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx b/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx index b461812..c683ba7 100644 --- a/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx +++ b/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom'; +import { Link, NavLink } from 'react-router-dom'; import React, { useState, useEffect, useCallback } from 'react'; import Button from '../../ui/Button'; import { useKeycloak } from '@react-keycloak/web'; @@ -52,8 +52,9 @@ const SecondaryNav: React.FC = () => { { id: 'my-profile', label: 'Profile', href: '/profile' }, { id: 'my-portfolio', label: 'Portfolio', href: '/portfolio' }, { id: 'dashboard', label: 'Custom Dashboard', href: '/dashboard' }, + { id: 'news', label: 'News', href: '/news' }, { id: 'markets', label: 'Markets', href: '/markets' }, - { id: 'research', label: 'Live Market Data', href: '/research' }, + { id: 'research', label: 'Live Market Data', href: '/live-markets' }, // Corrected href from /research if needed, keeping context { id: 'historical-data', label: 'Historical Data', href: '/historical-data' }, ]; @@ -103,9 +104,14 @@ const SecondaryNav: React.FC = () => { return true; }).map((item) => (
  • - + + isActive ? 'secondary-nav-link active' : 'secondary-nav-link' + } + > {item.label} - +
  • ))} diff --git a/frontend/src/components/ui/ChartModal/ChartModal.tsx b/frontend/src/components/ui/ChartModal/ChartModal.tsx index e166747..3772162 100644 --- a/frontend/src/components/ui/ChartModal/ChartModal.tsx +++ b/frontend/src/components/ui/ChartModal/ChartModal.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { createChart, ColorType, IChartApi, CandlestickSeries, HistogramSeries, LineSeries, Time } from 'lightweight-charts'; import { stockDataService } from '../../../services/stockDataService'; +import { yahooFinanceService } from '../../../services/yahooFinance'; import './ChartModal.css'; interface ChartModalProps { @@ -37,7 +38,10 @@ export const ChartModal: React.FC = ({ symbol, onClose }) => { const fetchData = async () => { try { setLoading(true); - const data = await stockDataService.getEODData(symbol); + const [data, quote] = await Promise.all([ + stockDataService.getEODData(symbol), + yahooFinanceService.getStockQuote(symbol) + ]); if (!data || !data.data || data.data.length === 0) { throw new Error('No data available'); @@ -55,18 +59,30 @@ export const ChartModal: React.FC = ({ symbol, onClose }) => { })).sort((a: any, b: any) => new Date(a.time).getTime() - new Date(b.time).getTime()); // Stats calculation - const latest = candles[candles.length - 1]; - const previous = candles[candles.length - 2]; - const change = latest.close - previous.close; - const changePercent = (change / previous.close) * 100; + let currentPrice = 0; + let change = 0; + let changePercent = 0; + + if (quote) { + currentPrice = quote.price; + change = quote.price - quote.previousClose; + changePercent = (change / quote.previousClose) * 100; + } else { + // Fallback to last candle if quote fails + const latest = candles[candles.length - 1]; + const previous = candles[candles.length - 2]; + currentPrice = latest.close; + change = latest.close - previous.close; + changePercent = (change / previous.close) * 100; + } setStats({ - current: latest.close, + current: currentPrice, change: change, changePercent: changePercent, high: Math.max(...candles.map((c: any) => c.high)), low: Math.min(...candles.map((c: any) => c.low)), - volume: latest.volume + volume: candles[candles.length - 1].volume }); if (chartContainerRef.current) { diff --git a/frontend/src/pages/CustomDashboard/CustomDashboard.css b/frontend/src/pages/CustomDashboard/CustomDashboard.css index 5ddd8ad..574ceea 100644 --- a/frontend/src/pages/CustomDashboard/CustomDashboard.css +++ b/frontend/src/pages/CustomDashboard/CustomDashboard.css @@ -295,6 +295,24 @@ font-style: italic; } +/* Section Headers */ +.section-title { + font-size: 1.5rem; + font-weight: 600; + color: #fff; + margin: 0; + display: flex; + align-items: baseline; + gap: 12px; +} + +.drag-hint { + font-size: 0.85rem; + font-weight: 400; + color: #888; + font-style: italic; +} + /* Saved Stocks Section */ .saved-stocks-section { background: #1a1a1a; diff --git a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx index d6ec446..b5c404b 100644 --- a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx +++ b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx @@ -657,7 +657,7 @@ const CustomDashboard: React.FC = () => {

    - Saved Stocks ({savedStocks.length}) + Saved Stocks Drag to rearrange

    @@ -759,7 +759,7 @@ const CustomDashboard: React.FC = () => { style={{ cursor: 'pointer' }} title="Click to edit title" > - {watchlistTitle} ({watchlistStocks.length}) + {watchlistTitle} {/* ✏️ */} )} diff --git a/frontend/src/pages/News/NewsPage.css b/frontend/src/pages/News/NewsPage.css index 8668f39..3ef440c 100644 --- a/frontend/src/pages/News/NewsPage.css +++ b/frontend/src/pages/News/NewsPage.css @@ -5,16 +5,25 @@ padding: 20px; } -.news-header { - text-align: center; +.news-header-container { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 40px; - padding: 40px 0; + padding: 20px 0; + border-bottom: 1px solid #333; +} + +.news-header { + text-align: left; + margin-bottom: 0; + padding: 0; } .news-title { font-size: 3rem; font-weight: 700; - margin: 0 0 16px 0; + margin: 0 0 8px 0; background: linear-gradient(135deg, #00d4aa, #0099cc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; @@ -22,11 +31,19 @@ } .news-subtitle { - font-size: 1.2rem; + font-size: 1.1rem; color: #a0a0a0; margin: 0; - max-width: 600px; - margin: 0 auto; +} + +.news-actions { + display: flex; + align-items: center; +} + +.subscribe-button-header { + font-weight: 600; + box-shadow: 0 4px 12px rgba(0, 212, 170, 0.3); } .news-controls { @@ -37,6 +54,13 @@ gap: 24px; } +.category-tabs { + display: flex; + justify-content: center; + gap: 16px; + margin-bottom: 16px; +} + .search-section { display: flex; justify-content: center; @@ -48,18 +72,8 @@ } .category-filters { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 12px; -} - -.category-button { - transition: all 0.2s ease; -} - -.category-button:hover { - transform: translateY(-2px); + display: none; + /* Hide old filters if not used, or repurpose */ } .news-content { diff --git a/frontend/src/pages/News/NewsPage.tsx b/frontend/src/pages/News/NewsPage.tsx index a7dfc74..a297170 100644 --- a/frontend/src/pages/News/NewsPage.tsx +++ b/frontend/src/pages/News/NewsPage.tsx @@ -1,118 +1,55 @@ import React, { useState, useEffect } from 'react'; -import { polygonService } from '../../services/polygonService'; import ArticleCard from '../../components/ui/ArticleCard'; import SearchBar from '../../components/ui/SearchBar'; import Button from '../../components/ui/Button'; +import { newsService, NewsArticle } from '../../services/newsService'; +import { usePortfolio } from '../../context/PortfolioContext'; +import EmailSubscriptionModal from '../../components/features/News/EmailSubscriptionModal'; import './NewsPage.css'; -interface NewsArticle { - id: string; - title: string; - description: string; - source: string; - timestamp: string; - imageUrl?: string; - stockTicker?: string; - stockChange?: number; - stockChangePercent?: number; - isPremium?: boolean; -} - const NewsPage: React.FC = () => { const [articles, setArticles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); - const [filteredArticles, setFilteredArticles] = useState([]); - const [selectedCategory, setSelectedCategory] = useState('all'); + const [activeTab, setActiveTab] = useState<'market' | 'portfolio'>('market'); + const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false); - const categories = [ - { id: 'all', label: 'All News' }, - { id: 'market', label: 'Market News' }, - { id: 'earnings', label: 'Earnings' }, - { id: 'ipo', label: 'IPO' }, - { id: 'mergers', label: 'M&A' } - ]; + const { holdings } = usePortfolio(); useEffect(() => { loadNews(); - }, []); + }, [activeTab]); // Reload when tab changes - useEffect(() => { - filterArticles(); - }, [articles, searchQuery, selectedCategory]); + const handleSearch = (query: string) => { + setSearchQuery(query); + // If query is empty, reload default news. If has value, search. + loadNews(query); + }; - const loadNews = async () => { + // Modify loadNews to accept optional query override + const loadNews = async (queryOverride?: string) => { try { setIsLoading(true); - - // Mock news data with some real stock tickers - const mockNews: NewsArticle[] = [ - { - id: '1', - title: 'Apple Reports Strong Q4 Earnings, Stock Surges 5%', - description: 'Apple Inc. reported better-than-expected quarterly earnings, driven by strong iPhone sales and services revenue growth.', - source: 'Reuters', - timestamp: '2h ago', - imageUrl: '/api/placeholder/400/250', - stockTicker: 'AAPL', - stockChange: 8.45, - stockChangePercent: 4.8 - }, - { - id: '2', - title: 'Tesla Announces New Gigafactory in Texas', - description: 'Tesla plans to build a new manufacturing facility in Austin, Texas, creating thousands of jobs.', - source: 'Bloomberg', - timestamp: '4h ago', - imageUrl: '/api/placeholder/400/250', - stockTicker: 'TSLA', - stockChange: 12.30, - stockChangePercent: 2.1 - }, - { - id: '3', - title: 'Microsoft Cloud Revenue Exceeds Expectations', - description: 'Microsoft Azure and Office 365 continue to drive strong growth in the cloud computing sector.', - source: 'CNBC', - timestamp: '6h ago', - imageUrl: '/api/placeholder/400/250', - stockTicker: 'MSFT', - stockChange: -2.15, - stockChangePercent: -0.6 - }, - { - id: '4', - title: 'Federal Reserve Signals Potential Rate Cut', - description: 'The Fed hints at possible interest rate adjustments in response to economic indicators.', - source: 'Wall Street Journal', - timestamp: '8h ago', - imageUrl: '/api/placeholder/400/250' - }, - { - id: '5', - title: 'Google Parent Alphabet Reports Record Revenue', - description: 'Alphabet Inc. posts record quarterly revenue driven by strong advertising growth.', - source: 'Financial Times', - timestamp: '10h ago', - imageUrl: '/api/placeholder/400/250', - stockTicker: 'GOOGL', - stockChange: 15.75, - stockChangePercent: 1.2 - }, - { - id: '6', - title: 'Amazon Expands Prime Delivery Network', - description: 'Amazon announces expansion of its same-day delivery service to 50 new cities.', - source: 'MarketWatch', - timestamp: '12h ago', - imageUrl: '/api/placeholder/400/250', - stockTicker: 'AMZN', - stockChange: 22.40, - stockChangePercent: 1.8 + let news: NewsArticle[] = []; + + // If explicit search query (from search bar) + if (queryOverride) { + news = await newsService.getNews(queryOverride); + } + // Else use tabs + else if (activeTab === 'portfolio') { + if (holdings.length > 0) { + const symbols = holdings.map(h => h.symbol); + news = await newsService.getNews(symbols); + } else { + news = []; } - ]; + } else { + // General market news + news = await newsService.getNews([]); + } - setArticles(mockNews); + setArticles(news); } catch (error) { console.error('Error loading news:', error); } finally { @@ -120,95 +57,86 @@ const NewsPage: React.FC = () => { } }; - const filterArticles = () => { - let filtered = articles; - - // Filter by category - if (selectedCategory !== 'all') { - filtered = filtered.filter(article => - article.title.toLowerCase().includes(selectedCategory) || - article.description.toLowerCase().includes(selectedCategory) - ); - } - - // Filter by search query - if (searchQuery.trim()) { - filtered = filtered.filter(article => - article.title.toLowerCase().includes(searchQuery.toLowerCase()) || - article.description.toLowerCase().includes(searchQuery.toLowerCase()) || - article.source.toLowerCase().includes(searchQuery.toLowerCase()) - ); - } - - setFilteredArticles(filtered); - }; - - const handleSearch = (query: string) => { - setSearchQuery(query); - }; - return (
    -
    -

    Financial News

    -

    Stay updated with the latest market news and analysis

    +
    +
    +

    Financial News

    +

    Live market updates and portfolio insights

    +
    + +
    + +
    +
    + + +
    +
    - -
    - {categories.map(category => ( - - ))} -
    {isLoading ? (
    -

    Loading latest news...

    +

    Loading {activeTab} news...

    ) : (
    - {filteredArticles.map((article, index) => ( -
    - + {articles.length > 0 ? ( + articles.map((article, index) => ( +
    + +
    + )) + ) : ( +
    + {activeTab === 'portfolio' && holdings.length === 0 ? ( +

    Add stocks to your portfolio to see personalized news.

    + ) : ( +

    No articles found.

    + )}
    - ))} -
    - )} - - {!isLoading && filteredArticles.length === 0 && ( -
    -

    No articles found matching your criteria.

    - + )}
    )}
    + + {isSubscriptionModalOpen && ( + setIsSubscriptionModalOpen(false)} /> + )}
    ); }; diff --git a/frontend/src/services/newsService.ts b/frontend/src/services/newsService.ts new file mode 100644 index 0000000..b62401a --- /dev/null +++ b/frontend/src/services/newsService.ts @@ -0,0 +1,61 @@ + +import axios from 'axios'; + +const LIVE_API_URL = 'http://localhost:3001/api'; + +export interface NewsArticle { + id: string; + title: string; + description: string; + source: string; + timestamp: number; // Unix timestamp + url: string; + imageUrl?: string; + stockTicker?: string; +} + +export const newsService = { + // Fetch general, symbol-specific, or search query news + getNews: async (input: string[] | string): Promise => { + try { + let url = `${LIVE_API_URL}/yahoo/news`; + + if (Array.isArray(input)) { + // It's a list of symbols + const query = input.length > 0 ? input.join(',') : 'market'; + url += `?symbols=${query}`; + } else { + // It's a search term + url += `?q=${encodeURIComponent(input)}`; + } + + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error("News Service Error:", error); + return []; + } + }, + + // Subscribe to email alerts + subscribe: async (email: string, symbols: string[]) => { + try { + const response = await axios.post(`${LIVE_API_URL}/subscribe`, { email, symbols }); + return response.data; + } catch (error) { + console.error("Subscription Error:", error); + throw error; + } + }, + + // Unsubscribe + unsubscribe: async (email: string) => { + try { + const response = await axios.post(`${LIVE_API_URL}/unsubscribe`, { email }); + return response.data; + } catch (error) { + console.error("Unsubscribe Error:", error); + throw error; + } + } +}; From 329d3c18cb35084d3868e91c47e7710893314edc Mon Sep 17 00:00:00 2001 From: akashrane Date: Sun, 14 Dec 2025 19:57:22 -0500 Subject: [PATCH 4/5] Added layout toggles and historicaldata for the live market data --- .../src/components/ui/LiveChart/LiveChart.tsx | 51 +++++++++++-- .../src/pages/LiveMarkets/LiveMarketsPage.css | 59 ++++++++++++++- .../src/pages/LiveMarkets/LiveMarketsPage.tsx | 73 ++++++++++++++++--- 3 files changed, 165 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/ui/LiveChart/LiveChart.tsx b/frontend/src/components/ui/LiveChart/LiveChart.tsx index 7b4b354..24ebb5e 100644 --- a/frontend/src/components/ui/LiveChart/LiveChart.tsx +++ b/frontend/src/components/ui/LiveChart/LiveChart.tsx @@ -47,8 +47,41 @@ const LiveChart: React.FC = ({ symbol, onRemove }) => { const [data, setData] = useState([]); const [stats, setStats] = useState({ high: 0, low: Infinity, vol: 0 }); - // Connect to WebSocket + // Connect to WebSocket & Fetch History useEffect(() => { + let isMounted = true; + + // 1. Fetch Historical Data first (so we have something to show immediately) + const fetchHistory = async () => { + try { + // Fetch last 1 day of 5-minute intervals + const response = await fetch(`http://localhost:3001/api/yahoo/history?symbol=${symbol}&range=1d&interval=5m`); + const history = await response.json(); + + if (isMounted && Array.isArray(history) && history.length > 0) { + const formattedHistory = history.map((h: any) => ({ + x: h.timestamp * 1000, + y: h.close, + v: h.volume + })); + setData(formattedHistory); + + // Initialize stats from history + const prices = formattedHistory.map((d: any) => d.y); + setStats({ + high: Math.max(...prices), + low: Math.min(...prices), + vol: formattedHistory.reduce((acc: number, curr: any) => acc + curr.v, 0) + }); + } + } catch (err) { + console.error("Failed to load history for", symbol, err); + } + }; + + fetchHistory(); + + // 2. Subscribe to Live Updates const ws = connectLiveFinnhub(symbol, (trade) => { if (trade.s === symbol) { const price = trade.p; @@ -56,14 +89,15 @@ const LiveChart: React.FC = ({ symbol, onRemove }) => { const time = trade.t; setData((prev) => { - // Filter out any potential duplicates or invalid timestamps first if needed - const newData = [...prev, { x: time, y: price, v: vol }]; + // Avoid duplicates if WS sends old data overlapping with history + const filteredPrev = prev.filter(d => d.x < time); + const newData = [...filteredPrev, { x: time, y: price, v: vol }]; - // Sort by timestamp to ensure line draws left-to-right correctly + // Sort by timestamp newData.sort((a, b) => a.x - b.x); - // Keep last 100 points - return newData.slice(-100); + // Keep reasonable history length (e.g. 500 points) to avoid memory leaks + return newData.slice(-500); }); setStats((prev) => ({ @@ -74,7 +108,10 @@ const LiveChart: React.FC = ({ symbol, onRemove }) => { } }); - return () => ws.close(); + return () => { + isMounted = false; + ws.close(); + }; }, [symbol]); // Chart Configuration diff --git a/frontend/src/pages/LiveMarkets/LiveMarketsPage.css b/frontend/src/pages/LiveMarkets/LiveMarketsPage.css index 677f1a5..48b7097 100644 --- a/frontend/src/pages/LiveMarkets/LiveMarketsPage.css +++ b/frontend/src/pages/LiveMarkets/LiveMarketsPage.css @@ -5,7 +5,64 @@ .charts-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 20px; margin-top: 20px; } + +/* Default Responsive Grid */ +.charts-grid.layout-grid { + grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); +} + +/* Compact 1x2 Grid (Fixed 2 Columns) */ +.charts-grid.layout-compact { + grid-template-columns: 1fr 1fr; +} + +/* Vertical List (1 Column) */ +.charts-grid.layout-vertical { + grid-template-columns: 1fr; + max-width: 100%; +} + +.charts-grid.layout-vertical .chart-wrapper { + height: 450px; +} + +/* Header Controls */ +.live-header-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.layout-toggles { + display: flex; + gap: 10px; +} + +.layout-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #ccc; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; +} + +.layout-btn:hover, +.layout-btn.active { + background: #00ffbf; + color: #000; + border-color: #00ffbf; +} + +/* Search Container override */ +.search-container-live { + width: 300px; +} \ No newline at end of file diff --git a/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx b/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx index 63e76f2..1f52610 100644 --- a/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx +++ b/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx @@ -1,19 +1,72 @@ -import React from "react"; +import React, { useState } from "react"; import LiveChart from "../../components/ui/LiveChart/LiveChart"; +import SearchBar from "../../components/ui/SearchBar/SearchBar"; import "./LiveMarketsPage.css"; const LiveMarketsPage = () => { + // Default symbols + const [symbols, setSymbols] = useState(["AAPL", "TSLA", "MSFT", "NVDA", "GOOGL"]); + const [layout, setLayout] = useState<'grid' | 'compact' | 'vertical'>('grid'); + + const handleAddSymbol = (query: string) => { + const symbol = query.toUpperCase().trim(); + if (symbol && !symbols.includes(symbol)) { + setSymbols(prev => [symbol, ...prev]); + } + }; + + const handleRemoveSymbol = (symbolToRemove: string) => { + setSymbols(prev => prev.filter(s => s !== symbolToRemove)); + }; + return (
    -

    Live Markets

    -

    Streaming real-time data from Finnhub

    - -
    - - - - - +
    +
    +

    Live Markets

    +

    Streaming real-time data from Finnhub

    +
    + +
    + + + +
    + +
    + +
    +
    + +
    + {symbols.map(symbol => ( + + ))}
    ); From 361b731ab1536e357aaf766302fcdc79f6cf3827 Mon Sep 17 00:00:00 2001 From: akashrane Date: Sun, 14 Dec 2025 22:40:39 -0500 Subject: [PATCH 5/5] Remove Markets page and add StockSearch component --- frontend/src/App.tsx | 2 - .../layout/SecondaryNav/SecondaryNav.tsx | 2 +- .../components/ui/ArticleCard/ArticleCard.css | 2 +- .../components/ui/ArticleCard/ArticleCard.tsx | 124 +++---- .../src/components/ui/LiveChart/LiveChart.css | 1 - .../src/components/ui/LiveChart/LiveChart.tsx | 2 +- .../src/components/ui/SearchBar/SearchBar.tsx | 25 +- .../components/ui/StockSearch/StockSearch.css | 157 ++++++++ .../components/ui/StockSearch/StockSearch.tsx | 127 +++++++ .../pages/CustomDashboard/CustomDashboard.tsx | 2 +- .../src/pages/LiveMarkets/LiveMarketsPage.tsx | 26 +- frontend/src/pages/Markets/MarketsPage.css | 300 --------------- frontend/src/pages/Markets/MarketsPage.tsx | 351 ------------------ frontend/src/pages/Markets/index.ts | 1 - frontend/src/pages/News/NewsPage.css | 17 +- frontend/src/pages/News/NewsPage.tsx | 6 +- frontend/src/types/index.ts | 2 + 17 files changed, 390 insertions(+), 757 deletions(-) create mode 100644 frontend/src/components/ui/StockSearch/StockSearch.css create mode 100644 frontend/src/components/ui/StockSearch/StockSearch.tsx delete mode 100644 frontend/src/pages/Markets/MarketsPage.css delete mode 100644 frontend/src/pages/Markets/MarketsPage.tsx delete mode 100644 frontend/src/pages/Markets/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 54b2347..7e97480 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,6 @@ import HomePage from './pages/HomePage'; import ProfilePage from './pages/Profile/ProfilePage'; import CustomDashboard from './pages/CustomDashboard'; import NewsPage from './pages/News'; -import MarketsPage from './pages/Markets'; import ResearchPage from './pages/Research'; import PersonalFinancePage from './pages/PersonalFinance'; import VideosPage from './pages/Videos'; @@ -56,7 +55,6 @@ function App() { } /> - } /> diff --git a/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx b/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx index c683ba7..b89ec52 100644 --- a/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx +++ b/frontend/src/components/layout/SecondaryNav/SecondaryNav.tsx @@ -53,7 +53,7 @@ const SecondaryNav: React.FC = () => { { id: 'my-portfolio', label: 'Portfolio', href: '/portfolio' }, { id: 'dashboard', label: 'Custom Dashboard', href: '/dashboard' }, { id: 'news', label: 'News', href: '/news' }, - { id: 'markets', label: 'Markets', href: '/markets' }, + { id: 'research', label: 'Live Market Data', href: '/live-markets' }, // Corrected href from /research if needed, keeping context { id: 'historical-data', label: 'Historical Data', href: '/historical-data' }, ]; diff --git a/frontend/src/components/ui/ArticleCard/ArticleCard.css b/frontend/src/components/ui/ArticleCard/ArticleCard.css index f7e3130..f9f7635 100644 --- a/frontend/src/components/ui/ArticleCard/ArticleCard.css +++ b/frontend/src/components/ui/ArticleCard/ArticleCard.css @@ -144,4 +144,4 @@ .article-timestamp { color: #888; font-size: 11px; -} +} \ No newline at end of file diff --git a/frontend/src/components/ui/ArticleCard/ArticleCard.tsx b/frontend/src/components/ui/ArticleCard/ArticleCard.tsx index 7e58e61..b0256f7 100644 --- a/frontend/src/components/ui/ArticleCard/ArticleCard.tsx +++ b/frontend/src/components/ui/ArticleCard/ArticleCard.tsx @@ -3,74 +3,74 @@ import { ArticleCardProps } from '../../../types'; import StockTicker from '../StockTicker'; import './ArticleCard.css'; -const ArticleCard: React.FC = -({ - article, - variant = 'standard', - onClick -}) => { - const { - title, - description, - urlToImage: imageUrl, - stockTicker, - stockChange, - stockChangePercent, - } = (article as any) || {}; +const ArticleCard: React.FC = + ({ + article, + variant = 'standard', + onClick + }) => { + const { + title, + description, + urlToImage: imageUrl, + stockTicker, + stockChange, + stockChangePercent, + } = (article as any) || {}; - const handleClick = () => { - if (onClick) { - onClick(); - } - }; + const handleClick = () => { + if (onClick) { + onClick(); + } + }; - const renderStockTicker = () => { - if (stockTicker && stockChange !== undefined && stockChangePercent !== undefined) { - return ( - - ); - } - return null; - }; - - if (!article) return null; // Safety check + const renderStockTicker = () => { + if (stockTicker && stockChange !== undefined && stockChangePercent !== undefined) { + return ( + + ); + } + return null; + }; - return ( -
    -
    -

    {title}

    - - {/* 4. FIX: Image Block - Only render if imageUrl exists AND the variant is "featured" */} - {imageUrl && variant === 'featured' && ( -
    - {title -
    - )} - - {description && variant !== 'compact' && ( -

    {description}

    - )} -
    -
    - {(article as any)?.source?.name} + if (!article) return null; // Safety check + + return ( +
    +
    +

    {title}

    + + {/* 4. FIX: Image Block - Only render if imageUrl exists AND the variant is "featured" */} + {imageUrl && variant === 'featured' && ( +
    + {title +
    + )} + + {description && variant !== 'compact' && ( +

    {description}

    + )} +
    +
    + {(article as any)?.source?.name} +
    + {renderStockTicker()}
    - {renderStockTicker()}
    -
    -
    - ); -}; + + ); + }; export default ArticleCard; \ No newline at end of file diff --git a/frontend/src/components/ui/LiveChart/LiveChart.css b/frontend/src/components/ui/LiveChart/LiveChart.css index a8db6f7..4c77089 100644 --- a/frontend/src/components/ui/LiveChart/LiveChart.css +++ b/frontend/src/components/ui/LiveChart/LiveChart.css @@ -6,7 +6,6 @@ /* More subtle border */ padding: 20px 24px 20px; width: 100%; - transition: 0.25s ease; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); } diff --git a/frontend/src/components/ui/LiveChart/LiveChart.tsx b/frontend/src/components/ui/LiveChart/LiveChart.tsx index 24ebb5e..7858e7d 100644 --- a/frontend/src/components/ui/LiveChart/LiveChart.tsx +++ b/frontend/src/components/ui/LiveChart/LiveChart.tsx @@ -293,4 +293,4 @@ const LiveChart: React.FC = ({ symbol, onRemove }) => { ); }; -export default LiveChart; +export default React.memo(LiveChart); diff --git a/frontend/src/components/ui/SearchBar/SearchBar.tsx b/frontend/src/components/ui/SearchBar/SearchBar.tsx index 7cce3cd..9048225 100644 --- a/frontend/src/components/ui/SearchBar/SearchBar.tsx +++ b/frontend/src/components/ui/SearchBar/SearchBar.tsx @@ -2,16 +2,17 @@ import React, { useState } from 'react'; import { SearchBarProps } from '../../../types'; import './SearchBar.css'; -const SearchBar: React.FC = ({ - placeholder, - onSearch, - className = '' +const SearchBar: React.FC = ({ + placeholder, + onSearch, + className = '' }) => { const [query, setQuery] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSearch(query); + setQuery(''); // Clear input after search }; const handleInputChange = (e: React.ChangeEvent) => { @@ -29,16 +30,16 @@ const SearchBar: React.FC = ({ className="search-input" />
    diff --git a/frontend/src/components/ui/StockSearch/StockSearch.css b/frontend/src/components/ui/StockSearch/StockSearch.css new file mode 100644 index 0000000..17d58d5 --- /dev/null +++ b/frontend/src/components/ui/StockSearch/StockSearch.css @@ -0,0 +1,157 @@ +.stock-search-container { + position: relative; + width: 100%; +} + +.search-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: #1a1a1a; + border: 1px solid #444; + border-top: none; + border-radius: 0 0 12px 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + z-index: 1000; + max-height: 400px; + overflow: hidden; + margin-top: 4px; +} + +.search-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1.5rem; + color: #a0a0a0; + font-size: 0.9rem; +} + +.loading-spinner { + width: 20px; + height: 20px; + border: 2px solid #333; + border-top: 2px solid #00d4aa; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.search-results { + max-height: 400px; + overflow-y: auto; +} + +.results-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid #333; + background: #1a1a1a; + position: sticky; + top: 0; + z-index: 10; +} + +.results-count { + color: #a0a0a0; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.results-list { + display: flex; + flex-direction: column; +} + +.search-result-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: transparent; + border-bottom: 1px solid #2a2a2a; + transition: all 0.2s ease; + cursor: pointer; +} + +.search-result-item:last-child { + border-bottom: none; +} + +.search-result-item:hover { + background: #2a2a2a; + border-left: 3px solid #00d4aa; + padding-left: calc(1rem - 3px); +} + +.result-info { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; +} + +.result-header { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.result-symbol { + font-weight: 700; + color: #00d4aa; + font-size: 0.95rem; + background: rgba(0, 212, 170, 0.1); + padding: 2px 6px; + border-radius: 4px; +} + +.result-name { + color: #e0e0e0; + font-size: 0.9rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +.result-details { + display: flex; + gap: 0.75rem; + font-size: 0.75rem; + color: #888; +} + +.add-button { + min-width: 60px; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; +} + +.no-results { + text-align: center; + color: #a0a0a0; + padding: 2rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.no-results-icon { + font-size: 1.5rem; + opacity: 0.6; +} \ No newline at end of file diff --git a/frontend/src/components/ui/StockSearch/StockSearch.tsx b/frontend/src/components/ui/StockSearch/StockSearch.tsx new file mode 100644 index 0000000..6a93d21 --- /dev/null +++ b/frontend/src/components/ui/StockSearch/StockSearch.tsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { yahooService } from '../../../services/yahooService'; +import { Stock } from '../../../types'; +import SearchBar from '../SearchBar'; +import Button from '../Button'; +import './StockSearch.css'; + +interface StockSearchProps { + onSelect: (stock: Stock) => void; + placeholder?: string; +} + +const StockSearch: React.FC = ({ onSelect, placeholder = "Search for stocks..." }) => { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const searchContainerRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) { + setSearchQuery(''); + setSearchResults([]); + } + }; + + if (searchQuery) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [searchQuery]); + + const handleSearch = async (query: string) => { + setSearchQuery(query); + if (query.trim().length > 0) { + setIsSearching(true); + + try { + // Use debouncing in real app, but for now direct call + const searchResults = await yahooService.searchStocks(query); + setSearchResults(searchResults); + } catch (err) { + console.error('Error searching stocks:', err); + setSearchResults([]); + } finally { + setIsSearching(false); + } + } else { + setSearchResults([]); + } + }; + + const handleSelectStock = (stock: Stock) => { + onSelect(stock); + setSearchQuery(''); + setSearchResults([]); + }; + + return ( +
    + + + {searchQuery && ( +
    + {isSearching ? ( +
    +
    + Searching... +
    + ) : searchResults.length > 0 ? ( +
    +
    + {searchResults.length} results found +
    +
    + {searchResults.map((stock) => ( +
    handleSelectStock(stock)} + > +
    +
    + {stock.symbol} + {stock.name} +
    +
    + {stock.exch} + {stock.type} +
    +
    + +
    + ))} +
    +
    + ) : ( +
    +
    🔍
    + No stocks found matching "{searchQuery}" +
    + )} +
    + )} +
    + ); +}; + +export default StockSearch; diff --git a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx index b5c404b..65a4380 100644 --- a/frontend/src/pages/CustomDashboard/CustomDashboard.tsx +++ b/frontend/src/pages/CustomDashboard/CustomDashboard.tsx @@ -574,7 +574,7 @@ const CustomDashboard: React.FC = () => { return (
    -

    Portfolio Analytics Dashboard

    +

    Analytics Dashboard

    {/* Search Section Moved Here */}
    diff --git a/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx b/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx index 1f52610..8300ebe 100644 --- a/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx +++ b/frontend/src/pages/LiveMarkets/LiveMarketsPage.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import LiveChart from "../../components/ui/LiveChart/LiveChart"; -import SearchBar from "../../components/ui/SearchBar/SearchBar"; +import StockSearch from "../../components/ui/StockSearch/StockSearch"; +import { Stock } from "../../types"; import "./LiveMarketsPage.css"; const LiveMarketsPage = () => { @@ -8,16 +9,19 @@ const LiveMarketsPage = () => { const [symbols, setSymbols] = useState(["AAPL", "TSLA", "MSFT", "NVDA", "GOOGL"]); const [layout, setLayout] = useState<'grid' | 'compact' | 'vertical'>('grid'); - const handleAddSymbol = (query: string) => { - const symbol = query.toUpperCase().trim(); - if (symbol && !symbols.includes(symbol)) { - setSymbols(prev => [symbol, ...prev]); - } - }; + const handleAddSymbol = React.useCallback((stock: Stock) => { + const symbol = stock.symbol; + setSymbols(prev => { + if (symbol && !prev.includes(symbol)) { + return [symbol, ...prev]; + } + return prev; + }); + }, []); - const handleRemoveSymbol = (symbolToRemove: string) => { + const handleRemoveSymbol = React.useCallback((symbolToRemove: string) => { setSymbols(prev => prev.filter(s => s !== symbolToRemove)); - }; + }, []); return (
    @@ -52,9 +56,9 @@ const LiveMarketsPage = () => {
    -
    diff --git a/frontend/src/pages/Markets/MarketsPage.css b/frontend/src/pages/Markets/MarketsPage.css deleted file mode 100644 index cb33de6..0000000 --- a/frontend/src/pages/Markets/MarketsPage.css +++ /dev/null @@ -1,300 +0,0 @@ -.markets-page { - min-height: 100vh; - background: #000; - color: #fff; - padding: 20px; -} - -.markets-header { - text-align: center; - margin-bottom: 40px; - padding: 40px 0; -} - -.markets-title { - font-size: 3rem; - font-weight: 700; - margin: 0 0 16px 0; - background: linear-gradient(135deg, #00d4aa, #0099cc); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.markets-subtitle { - font-size: 1.2rem; - color: #a0a0a0; - margin: 0; - max-width: 600px; - margin: 0 auto; -} - -.markets-controls { - width: 100%; - margin: 0 auto 40px; - display: flex; - flex-direction: column; - gap: 24px; -} - -.search-section { - display: flex; - justify-content: center; -} - -.markets-search { - max-width: 500px; - width: 100%; -} - -.tabs-container { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 12px; -} - -.tab-button { - transition: all 0.2s ease; -} - -.tab-button:hover { - transform: translateY(-2px); -} - -.markets-content { - width: 100%; - margin: 0 auto; -} - -.loading-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 80px 20px; - text-align: center; -} - -.loading-spinner { - width: 40px; - height: 40px; - border: 3px solid #333; - border-top: 3px solid #00d4aa; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 16px; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -.search-results { - margin-bottom: 40px; -} - -.search-results h2 { - font-size: 1.5rem; - margin-bottom: 24px; - color: #00d4aa; -} - -.market-section { - margin-bottom: 40px; -} - -.section-title { - font-size: 1.8rem; - margin-bottom: 24px; - color: #fff; - text-align: center; -} - -.market-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 20px; -} - -.market-card { - background: #1a1a1a; - border: 1px solid #333; - border-radius: 12px; - padding: 20px; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.market-card:hover { - transform: translateY(-4px); - border-color: #00d4aa; - box-shadow: 0 8px 25px rgba(0, 212, 170, 0.15); -} - -.market-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; -} - -.stock-info { - flex: 1; -} - -.stock-symbol { - font-size: 1.4rem; - font-weight: 700; - margin: 0 0 4px 0; - color: #fff; -} - -.stock-name { - font-size: 0.9rem; - color: #a0a0a0; - margin: 0; - line-height: 1.3; -} - -.price-info { - text-align: right; -} - -.stock-price { - font-size: 1.5rem; - font-weight: 700; - color: #fff; - display: block; - margin-bottom: 4px; -} - -.price-change { - font-size: 0.9rem; - font-weight: 600; - display: flex; - flex-direction: column; - align-items: flex-end; -} - -.price-change.positive { - color: #00d4aa; -} - -.price-change.negative { - color: #ff4757; -} - -.sparkline-container { - height: 60px; - margin: 16px 0; - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - padding: 8px; -} - -.market-card-footer { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.8rem; - color: #a0a0a0; - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #333; -} - -.volume-info, -.market-cap { - display: flex; - align-items: center; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .markets-page { - padding: 16px; - } - - .markets-title { - font-size: 2rem; - } - - .markets-subtitle { - font-size: 1rem; - } - - .markets-controls { - margin-bottom: 32px; - } - - .tabs-container { - gap: 8px; - } - - .market-grid { - grid-template-columns: 1fr; - gap: 16px; - } - - .market-card { - padding: 16px; - } - - .market-card-header { - flex-direction: column; - gap: 12px; - } - - .price-info { - text-align: left; - width: 100%; - } - - .price-change { - flex-direction: row; - align-items: center; - gap: 8px; - } - - .market-card-footer { - flex-direction: column; - gap: 8px; - align-items: flex-start; - } -} - -@media (max-width: 480px) { - .markets-header { - padding: 20px 0; - margin-bottom: 24px; - } - - .markets-title { - font-size: 1.8rem; - } - - .tabs-container { - flex-direction: column; - align-items: center; - } - - .tab-button { - width: 200px; - } - - .stock-symbol { - font-size: 1.2rem; - } - - .stock-price { - font-size: 1.3rem; - } -} \ No newline at end of file diff --git a/frontend/src/pages/Markets/MarketsPage.tsx b/frontend/src/pages/Markets/MarketsPage.tsx deleted file mode 100644 index 9bed4c6..0000000 --- a/frontend/src/pages/Markets/MarketsPage.tsx +++ /dev/null @@ -1,351 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { polygonService } from '../../services/polygonService'; -import MarketIndex from '../../components/ui/MarketIndex'; -import StockTicker from '../../components/ui/StockTicker'; -import SparklineChart from '../../components/ui/SparklineChart'; -import SearchBar from '../../components/ui/SearchBar'; -import Button from '../../components/ui/Button'; -import './MarketsPage.css'; - - - -interface MarketData { - symbol: string; - name: string; - price: number; - change: number; - changePercent: number; - volume: number; - marketCap?: number; - high52Week?: number; - low52Week?: number; - sparklineData?: number[]; -} - -interface MarketIndexData { - symbol: string; - name: string; - price: number; - change: number; - changePercent: number; - volume: number; -} - -const MarketsPage: React.FC = () => { - const [marketIndices, setMarketIndices] = useState([]); - const [topGainers, setTopGainers] = useState([]); - const [topLosers, setTopLosers] = useState([]); - const [mostActive, setMostActive] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [selectedTab, setSelectedTab] = useState('indices'); - - const tabs = [ - { id: 'indices', label: 'Market Indices' }, - { id: 'gainers', label: 'Top Gainers' }, - { id: 'losers', label: 'Top Losers' }, - { id: 'active', label: 'Most Active' } - ]; - - useEffect(() => { - loadMarketData(); - }, []); - - const loadMarketData = async () => { - try { - setIsLoading(true); - - // Mock market indices data - const mockIndices: MarketIndexData[] = [ - { - symbol: 'SPX', - name: 'S&P 500', - price: 4567.89, - change: 23.45, - changePercent: 0.52, - volume: 2500000000 - }, - { - symbol: 'DJI', - name: 'Dow Jones', - price: 34567.89, - change: -123.45, - changePercent: -0.36, - volume: 1800000000 - }, - { - symbol: 'IXIC', - name: 'NASDAQ', - price: 14567.89, - change: 89.12, - changePercent: 0.62, - volume: 3200000000 - }, - { - symbol: 'RUT', - name: 'Russell 2000', - price: 1987.65, - change: 12.34, - changePercent: 0.62, - volume: 450000000 - } - ]; - - // Mock top gainers data - const mockGainers: MarketData[] = [ - { - symbol: 'NVDA', - name: 'NVIDIA Corporation', - price: 485.67, - change: 45.23, - changePercent: 10.28, - volume: 45000000, - marketCap: 1200000000000, - high52Week: 502.30, - low52Week: 180.50, - sparklineData: [440, 445, 450, 460, 470, 475, 480, 485] - }, - { - symbol: 'AMD', - name: 'Advanced Micro Devices', - price: 145.89, - change: 12.45, - changePercent: 9.33, - volume: 32000000, - marketCap: 235000000000, - high52Week: 164.46, - low52Week: 78.20, - sparklineData: [133, 135, 138, 140, 142, 144, 145, 146] - }, - { - symbol: 'TSLA', - name: 'Tesla, Inc.', - price: 267.45, - change: 18.90, - changePercent: 7.60, - volume: 85000000, - marketCap: 850000000000, - high52Week: 299.29, - low52Week: 138.80, - sparklineData: [248, 250, 255, 260, 262, 265, 266, 267] - } - ]; - - // Mock top losers data - const mockLosers: MarketData[] = [ - { - symbol: 'META', - name: 'Meta Platforms, Inc.', - price: 312.45, - change: -25.67, - changePercent: -7.59, - volume: 28000000, - marketCap: 790000000000, - high52Week: 384.33, - low52Week: 88.09, - sparklineData: [338, 335, 330, 325, 320, 315, 313, 312] - }, - { - symbol: 'NFLX', - name: 'Netflix, Inc.', - price: 445.67, - change: -28.90, - changePercent: -6.09, - volume: 15000000, - marketCap: 195000000000, - high52Week: 485.00, - low52Week: 285.00, - sparklineData: [474, 470, 465, 460, 455, 450, 447, 446] - } - ]; - - // Mock most active data - const mockActive: MarketData[] = [ - { - symbol: 'AAPL', - name: 'Apple Inc.', - price: 189.45, - change: 2.34, - changePercent: 1.25, - volume: 65000000, - marketCap: 2950000000000, - high52Week: 198.23, - low52Week: 124.17, - sparklineData: [187, 188, 189, 190, 191, 190, 189, 189] - }, - { - symbol: 'MSFT', - name: 'Microsoft Corporation', - price: 378.90, - change: -1.23, - changePercent: -0.32, - volume: 42000000, - marketCap: 2810000000000, - high52Week: 384.30, - low52Week: 309.45, - sparklineData: [380, 379, 378, 377, 378, 379, 378, 379] - } - ]; - - setMarketIndices(mockIndices); - setTopGainers(mockGainers); - setTopLosers(mockLosers); - setMostActive(mockActive); - - } catch (error) { - console.error('Error loading market data:', error); - } finally { - setIsLoading(false); - } - }; - - const handleSearch = async (query: string) => { - setSearchQuery(query); - if (query.trim()) { - setIsSearching(true); - try { - // Mock search results - const mockResults: MarketData[] = [ - { - symbol: query.toUpperCase(), - name: `${query.toUpperCase()} Corporation`, - price: Math.random() * 500 + 50, - change: (Math.random() - 0.5) * 20, - changePercent: (Math.random() - 0.5) * 10, - volume: Math.floor(Math.random() * 10000000), - marketCap: Math.floor(Math.random() * 1000000000000), - sparklineData: Array.from({ length: 8 }, () => Math.random() * 100 + 200) - } - ]; - setSearchResults(mockResults); - } catch (error) { - console.error('Error searching stocks:', error); - setSearchResults([]); - } finally { - setIsSearching(false); - } - } else { - setSearchResults([]); - } - }; - - const getCurrentData = () => { - switch (selectedTab) { - case 'indices': - return marketIndices; - case 'gainers': - return topGainers; - case 'losers': - return topLosers; - case 'active': - return mostActive; - default: - return []; - } - }; - - const renderMarketCard = (item: MarketData | MarketIndexData, index: number) => ( -
    -
    -
    -

    {item.symbol}

    -

    {item.name}

    -
    -
    - ${item.price.toFixed(2)} -
    = 0 ? 'positive' : 'negative'}`}> - {item.change >= 0 ? '+' : ''}{item.change.toFixed(2)} - ({item.change >= 0 ? '+' : ''}{item.changePercent.toFixed(2)}%) -
    -
    -
    - - {('sparklineData' in item && item.sparklineData) && ( -
    - -
    - )} - -
    -
    - Volume: {item.volume.toLocaleString()} -
    - {('marketCap' in item && item.marketCap) && ( -
    - Market Cap: ${(item.marketCap / 1000000000).toFixed(1)}B -
    - )} -
    -
    - ); - - return ( -
    -
    -

    Markets

    -

    Real-time market data and analysis

    -
    - -
    -
    - -
    - -
    - {tabs.map(tab => ( - - ))} -
    -
    - -
    - {isLoading ? ( -
    -
    -

    Loading market data...

    -
    - ) : ( - <> - {searchQuery && ( -
    -

    Search Results for "{searchQuery}"

    -
    - {isSearching ? ( -
    - ) : ( - searchResults.map((result, index) => renderMarketCard(result, index)) - )} -
    -
    - )} - -
    -

    - {tabs.find(tab => tab.id === selectedTab)?.label} -

    -
    - {getCurrentData().map((item, index) => renderMarketCard(item, index))} -
    -
    - - )} -
    -
    - ); -}; - -export default MarketsPage; diff --git a/frontend/src/pages/Markets/index.ts b/frontend/src/pages/Markets/index.ts deleted file mode 100644 index 8be32a9..0000000 --- a/frontend/src/pages/Markets/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './MarketsPage'; diff --git a/frontend/src/pages/News/NewsPage.css b/frontend/src/pages/News/NewsPage.css index 3ef440c..2c3a8d1 100644 --- a/frontend/src/pages/News/NewsPage.css +++ b/frontend/src/pages/News/NewsPage.css @@ -9,8 +9,8 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 40px; - padding: 20px 0; + margin-bottom: 20px; + padding: 10px 0; border-bottom: 1px solid #333; } @@ -21,17 +21,14 @@ } .news-title { - font-size: 3rem; + font-size: 1.8rem; font-weight: 700; - margin: 0 0 8px 0; - background: linear-gradient(135deg, #00d4aa, #0099cc); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + margin: 0; + color: #00d4aa; } .news-subtitle { - font-size: 1.1rem; + font-size: 1rem; color: #a0a0a0; margin: 0; } @@ -48,7 +45,7 @@ .news-controls { width: 100%; - margin: 0 auto 40px; + margin: 0 auto 20px; display: flex; flex-direction: column; gap: 24px; diff --git a/frontend/src/pages/News/NewsPage.tsx b/frontend/src/pages/News/NewsPage.tsx index a297170..a91c98d 100644 --- a/frontend/src/pages/News/NewsPage.tsx +++ b/frontend/src/pages/News/NewsPage.tsx @@ -71,7 +71,7 @@ const NewsPage: React.FC = () => { onClick={() => setIsSubscriptionModalOpen(true)} className="subscribe-button-header" > - 🔔 Get Daily Alerts + Get Daily Alerts
    @@ -111,13 +111,13 @@ const NewsPage: React.FC = () => {
    {articles.length > 0 ? ( articles.map((article, index) => ( -
    +
    )) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index c9c5656..89f780c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -29,6 +29,8 @@ export interface Stock { price: number; change: number; changePercent: number; + exch?: string; + type?: string; } export interface StockTicker {