From 479c0b55401129aeafd3541754e65ad8699c79db Mon Sep 17 00:00:00 2001 From: Ryana May Que Date: Fri, 6 Sep 2024 17:40:05 +0800 Subject: [PATCH] feat: supoprt news section --- src/components/NewsColumn.tsx | 128 ++++++++++++++++++++++++++++++++++ src/components/StockPrice.tsx | 97 ++++++++++++++++++-------- src/components/Ticker.tsx | 117 +++++++++++++++---------------- src/pages/api/news.ts | 20 ++++++ src/pages/api/quote.ts | 29 ++++---- src/pages/index.tsx | 10 ++- src/utils/sparkle.ts | 11 ++- 7 files changed, 306 insertions(+), 106 deletions(-) create mode 100644 src/components/NewsColumn.tsx create mode 100644 src/pages/api/news.ts diff --git a/src/components/NewsColumn.tsx b/src/components/NewsColumn.tsx new file mode 100644 index 0000000..380703e --- /dev/null +++ b/src/components/NewsColumn.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from 'react'; + +interface NewsArticle { + category: string; + datetime: number; + headline: string; + id: number; + image: string; + related: string; + source: string; + summary: string; + url: string; +} + +const NewsColumn = () => { + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchNews = async () => { + try { + const res = await fetch('/api/news'); + const data = await res.json(); + const filteredNews = data.result.filter((article: any) => article.source !== 'MarketWatch'); + setNews(filteredNews); + } catch (err) { + setError('Failed to fetch news'); + } finally { + setLoading(false); + } + }; + + fetchNews(); + }, []); + + if (loading) { + return

Loading...

; + } + + if (error) { + return

{error}

; + } + + + const featuredArticle = news[0]; + const otherArticles = news.slice(1, 7); + const latestArticles = news.slice(7); + return ( +
+
+ {/* Main featured article */} +
+ {featuredArticle && ( + + {featuredArticle.headline} +
+

{featuredArticle.headline}

+

+ {new Date(featuredArticle.datetime * 1000).toLocaleString()} | {featuredArticle.source} +

+

{featuredArticle.summary}

+
+
+ )} +
+ + {/* Other articles */} +
+ {otherArticles.map(article => ( + + {article.headline} +
+

{article.headline}

+

+ {new Date(article.datetime * 1000).toLocaleString()} | {article.source} +

+
+
+ ))} +
+
+ + {/* Latest news sidebar */} +
+

Latest

+ +
+
+ ); +}; + +export default NewsColumn; \ No newline at end of file diff --git a/src/components/StockPrice.tsx b/src/components/StockPrice.tsx index b3b95f7..2922c53 100644 --- a/src/components/StockPrice.tsx +++ b/src/components/StockPrice.tsx @@ -1,49 +1,91 @@ -import { useState, useEffect } from 'react' -import Ticker from './Ticker' -import SearchBar from './SearchBar' +import { useState, useEffect } from 'react'; +import Ticker from './Ticker'; interface StockPriceProps { - symbol: string + symbol: string; +} + +interface StockData { + c: number; // Current price + d: number; // Change + dp: number; // Percent change + h: number; // High price of the day + l: number; // Low price of the day + o: number; // Open price of the day + pc: number; // Previous close price +} + +interface StockDescription { + description: string; } const StockPrice = ({ symbol }: StockPriceProps) => { - const [price, setPrice] = useState(null) - const [error, setError] = useState('') + const [stockData, setStockData] = useState(null); + const [stockDescription, setStockDescription] = useState(''); + const [error, setError] = useState(''); const fetchStockPrice = async (selectedSymbol: string) => { - setError('') + setError(''); try { - const res = await fetch(`/api/quote?symbol=${selectedSymbol}`) - const data = await res.json() + const res = await fetch(`/api/quote?symbol=${selectedSymbol}`); + const data = await res.json(); if (data.error) { - setError(data.error) - setPrice(null) + setError(data.error); + setStockData(null); } else { - setPrice(data.c) + setStockData(data); } } catch (err) { - setError('Failed to fetch stock price') - setPrice(null) + setError('Failed to fetch stock price'); + setStockData(null); } - } + }; + + const fetchStockDescription = async (selectedSymbol: string) => { + setError(''); + try { + const res = await fetch(`/api/search?query=${selectedSymbol}`); + const data = await res.json(); + if (data.result && data.result.length > 0) { + setStockDescription(data.result[0].description); // Assume the first result matches + } else { + setError('Description not found'); + setStockDescription(''); + } + } catch (err) { + setError('Failed to fetch stock description'); + setStockDescription(''); + } + }; useEffect(() => { - let intervalId: NodeJS.Timeout + let intervalId: NodeJS.Timeout; if (symbol) { - fetchStockPrice(symbol) - intervalId = setInterval(() => fetchStockPrice(symbol), 300000) // 300000 ms = 5 minutes + fetchStockPrice(symbol); + fetchStockDescription(symbol); + intervalId = setInterval(() => fetchStockPrice(symbol), 300000); // 300000 ms = 5 minutes } - return () => clearInterval(intervalId) - }, [symbol]) + return () => clearInterval(intervalId); + }, [symbol]); return ( -
+
{symbol && (
-

Symbol: {symbol}

- {price !== null && ( +

+

{symbol}

+ + {stockDescription &&

{stockDescription}

} + + {stockData !== null && (
-

Current Price: ${price}

+

Current Price: ${stockData.c}

+

Change: ${stockData.d}

+

Percent Change: {stockData.dp}%

+

High Price of the Day: ${stockData.h}

+

Low Price of the Day: ${stockData.l}

+

Open Price of the Day: ${stockData.o}

+

Previous Close Price: ${stockData.pc}

)} {error && ( @@ -51,11 +93,10 @@ const StockPrice = ({ symbol }: StockPriceProps) => {

{error}

)} -
)}
- ) -} + ); +}; -export default StockPrice \ No newline at end of file +export default StockPrice; \ No newline at end of file diff --git a/src/components/Ticker.tsx b/src/components/Ticker.tsx index 598510d..a3b1d12 100644 --- a/src/components/Ticker.tsx +++ b/src/components/Ticker.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react' -import { fetchQuote } from '../utils/sparkle' -import { tradeConditions } from '../utils/tradeConditions' +import { useEffect, useState } from 'react'; +import { fetchQuote } from '../utils/sparkle'; +import { tradeConditions } from '../utils/tradeConditions'; interface Trade { p: number; // Price @@ -11,102 +11,97 @@ interface Trade { } const Ticker = ({ symbol }: { symbol: string }) => { - const [trades, setTrades] = useState([]) - const [bid, setBid] = useState(null) - const [ask, setAsk] = useState(null) - const [webSocketInitialized, setWebSocketInitialized] = useState(false) + const [latestTrade, setLatestTrade] = useState(null); + const [bid, setBid] = useState(null); + const [ask, setAsk] = useState(null); + const [webSocketInitialized, setWebSocketInitialized] = useState(false); useEffect(() => { - // Fetch initial bid and ask prices + const initializePrices = async () => { try { - const quote = await fetchQuote(symbol) - setBid(quote.b) - setAsk(quote.a) + const quote = await fetchQuote(symbol); + setBid(quote.b); + setAsk(quote.a); } catch (error) { - console.error('Failed to fetch initial bid/ask prices:', error) + console.error('Failed to fetch initial bid/ask prices:', error); } - } - - initializePrices() - }, [symbol]) + }; + initializePrices(); + }, [symbol]); useEffect(() => { const initializeWebSocket = async () => { try { - const response = await fetch('/api/ws/start') - const data = await response.json() + const response = await fetch('/api/ws/start'); + const data = await response.json(); if (data.status === 'WebSocket server is running') { - setWebSocketInitialized(true) + setWebSocketInitialized(true); } } catch (error) { - console.error('Failed to initialize WebSocket:', error) + console.error('Failed to initialize WebSocket:', error); } - } - - initializeWebSocket() - }, []) + }; + initializeWebSocket(); + }, []); useEffect(() => { - if (!webSocketInitialized) return - const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`) + if (!webSocketInitialized) return; + + const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`); socket.onopen = () => { - console.log('WebSocket connection established') - socket.send(JSON.stringify({ type: 'subscribe', symbol })) - } + console.log('WebSocket connection established'); + socket.send(JSON.stringify({ type: 'subscribe', symbol })); + }; socket.onmessage = (event) => { - const data = JSON.parse(event.data) + const data = JSON.parse(event.data); if (data.type === 'trade') { - setTrades((prevTrades) => [...data.data, ...prevTrades]) + setLatestTrade(data.data[0]); } - } + }; socket.onclose = () => { - console.log('WebSocket connection closed') - } + console.log('WebSocket connection closed'); + }; return () => { - socket.send(JSON.stringify({ type: 'unsubscribe', symbol })) - socket.close() - } - }, [symbol, webSocketInitialized]) + socket.send(JSON.stringify({ type: 'unsubscribe', symbol })); + socket.close(); + }; + }, [symbol, webSocketInitialized]); const identifyTradeType = (trade: Trade) => { if (bid !== null && ask !== null) { if (trade.p >= ask) { - return 'Buy' + return 'Buy'; } else if (trade.p <= bid) { - return 'Sell' + return 'Sell'; } } - return 'Unknown' - } + return 'Unknown'; + }; const getTradeConditions = (trade: Trade) => { if (trade.c && trade.c.length > 0) { - return trade.c.map(code => tradeConditions[code] || `Unknown Condition: ${code}`).join(', ') + return trade.c.map(code => tradeConditions[code] || `Unknown Condition: ${code}`).join(', '); } - return 'No conditions' - } + return 'No conditions'; + }; return ( -
-

Latest Trades for {symbol}

-
    - {trades.slice(0, 5).map((trade, index) => ( -
  • -
    Price: ${trade.p}
    -
    Volume: {trade.v}
    -
    Time: {new Date(trade.t).toLocaleTimeString()}
    -
    Type: {identifyTradeType(trade)}
    -
    Conditions: {getTradeConditions(trade)}
    -
  • - ))} -
+
+ {symbol && latestTrade && ( +
+
+ LIVE ${latestTrade.s} +
Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}
+
+
+ )}
- ) -} + ); +}; -export default Ticker \ No newline at end of file +export default Ticker; \ No newline at end of file diff --git a/src/pages/api/news.ts b/src/pages/api/news.ts new file mode 100644 index 0000000..e4b5067 --- /dev/null +++ b/src/pages/api/news.ts @@ -0,0 +1,20 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { fetchNews } from "../../utils/sparkle"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== "GET") { + res.setHeader("Allow", ["GET"]); + res.status(405).json({ error: `Method ${req.method} Not Allowed` }); + return; + } + + try { + const result = await fetchNews(); + res.status(200).json({ result: result }); + } catch (error) { + res.status(500).json({ error: "Error fetching symbols" }); + } +} \ No newline at end of file diff --git a/src/pages/api/quote.ts b/src/pages/api/quote.ts index 8c14c8b..66ec98a 100644 --- a/src/pages/api/quote.ts +++ b/src/pages/api/quote.ts @@ -1,20 +1,21 @@ + import type { NextApiRequest, NextApiResponse } from "next"; import { fetchQuote } from "../../utils/sparkle"; export default async function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - const { symbol } = req.query; - if (!symbol || typeof symbol !== "string") { - res.status(400).json({ error: "Invalid symbol" }); - return; - } + const { symbol } = req.query; + if (!symbol || typeof symbol !== "string") { + res.status(400).json({ error: "Invalid symbol" }); + return; + } - try { - const quote = await fetchQuote(symbol); - res.status(200).json(quote); - } catch (error) { - res.status(500).json({ error: "Error fetching quote" }); - } -} + try { + const quote = await fetchQuote(symbol as string); + res.status(200).json(quote); + } catch (error) { + res.status(500).json({ error: "Error fetching quote" }); + } +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ee929bb..bab5d6a 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,8 @@ import { useState } from 'react' import NavigationBar from '../components/NavigationBar' import StockPrice from '../components/StockPrice' +import Ticker from '@/components/Ticker' +import NewsColumn from '@/components/NewsColumn' export default function Home() { const [symbol, setSymbol] = useState('') @@ -12,8 +14,12 @@ export default function Home() { return (
-
- +
+
+ + + +
) diff --git a/src/utils/sparkle.ts b/src/utils/sparkle.ts index e176d2b..298b14c 100644 --- a/src/utils/sparkle.ts +++ b/src/utils/sparkle.ts @@ -29,4 +29,13 @@ export const startWebSocket = async () => { throw new Error('Error starting WebSocket'); } return res.json(); -}; \ No newline at end of file +}; + +export const fetchNews = async () => { + const url = `${SPARKLE_BASE_URL}/api/v1/marketnews`; + const res = await fetch(url); + if (!res.ok) { + throw new Error('Error fetching news'); + } + return res.json(); +} \ No newline at end of file