feat: supoprt news section

This commit is contained in:
ryana mittens 2024-09-06 17:40:05 +08:00
parent 2bf8d16250
commit 479c0b5540
7 changed files with 306 additions and 106 deletions

View file

@ -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<NewsArticle[]>([]);
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 <p>Loading...</p>;
}
if (error) {
return <p className="text-red-500">{error}</p>;
}
const featuredArticle = news[0];
const otherArticles = news.slice(1, 7);
const latestArticles = news.slice(7);
return (
<div className="lg:flex lg:space-x-4">
<div className="lg:flex-1 lg:space-y-4">
{/* Main featured article */}
<div className="mb-4 lg:mb-0">
{featuredArticle && (
<a
href={featuredArticle.url}
target="_blank"
rel="noopener noreferrer"
className="block overflow-hidden rounded-lg shadow-lg hover:underline"
>
<img
src={featuredArticle.image}
alt={featuredArticle.headline}
className="w-full h-64 object-cover"
/>
<div className="p-4 bg-white dark:bg-gray-800">
<h3 className="text-xl font-bold">{featuredArticle.headline}</h3>
<p className="text-sm text-gray-500">
{new Date(featuredArticle.datetime * 1000).toLocaleString()} | {featuredArticle.source}
</p>
<p className="text-sm mt-2">{featuredArticle.summary}</p>
</div>
</a>
)}
</div>
{/* Other articles */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{otherArticles.map(article => (
<a
key={article.id}
href={article.url}
target="_blank"
rel="noopener noreferrer"
className="block overflow-hidden rounded-lg shadow-lg hover:underline"
>
<img
src={article.image}
alt={article.headline}
className="w-full h-32 object-cover rounded-t-lg"
/>
<div className="p-2 bg-white h-full dark:bg-gray-800">
<h3 className="text-sm font-semibold">{article.headline}</h3>
<p className="text-xs text-gray-500">
{new Date(article.datetime * 1000).toLocaleString()} | {article.source}
</p>
</div>
</a>
))}
</div>
</div>
{/* Latest news sidebar */}
<div className="mt-4 lg:mt-0 lg:w-1/4 lg:order-last border p-4 rounded-lg bg-white dark:bg-gray-800 lg:overflow-y-auto lg:h-screen">
<h2 className="text-xl font-bold mb-4">Latest</h2>
<ul>
{latestArticles.map(article => (
<li key={article.id} className="mb-4">
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
className="hover:underline block"
>
<p className="text-sm font-semibold">{article.headline}</p>
<p className="text-xs text-gray-500">
{new Date(article.datetime * 1000).toLocaleString()}
</p>
</a>
</li>
))}
</ul>
</div>
</div>
);
};
export default NewsColumn;

View file

@ -1,49 +1,91 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react';
import Ticker from './Ticker' import Ticker from './Ticker';
import SearchBar from './SearchBar'
interface StockPriceProps { 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 StockPrice = ({ symbol }: StockPriceProps) => {
const [price, setPrice] = useState<number | null>(null) const [stockData, setStockData] = useState<StockData | null>(null);
const [error, setError] = useState('') const [stockDescription, setStockDescription] = useState<string>('');
const [error, setError] = useState('');
const fetchStockPrice = async (selectedSymbol: string) => { const fetchStockPrice = async (selectedSymbol: string) => {
setError('') setError('');
try { try {
const res = await fetch(`/api/quote?symbol=${selectedSymbol}`) const res = await fetch(`/api/quote?symbol=${selectedSymbol}`);
const data = await res.json() const data = await res.json();
if (data.error) { if (data.error) {
setError(data.error) setError(data.error);
setPrice(null) setStockData(null);
} else { } else {
setPrice(data.c) setStockData(data);
} }
} catch (err) { } catch (err) {
setError('Failed to fetch stock price') setError('Failed to fetch stock price');
setPrice(null) 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(() => { useEffect(() => {
let intervalId: NodeJS.Timeout let intervalId: NodeJS.Timeout;
if (symbol) { if (symbol) {
fetchStockPrice(symbol) fetchStockPrice(symbol);
intervalId = setInterval(() => fetchStockPrice(symbol), 300000) // 300000 ms = 5 minutes fetchStockDescription(symbol);
intervalId = setInterval(() => fetchStockPrice(symbol), 300000); // 300000 ms = 5 minutes
} }
return () => clearInterval(intervalId) return () => clearInterval(intervalId);
}, [symbol]) }, [symbol]);
return ( return (
<div className="my-4"> <div className="">
{symbol && ( {symbol && (
<div> <div>
<h2 className="text-2xl font-bold mt-4">Symbol: {symbol}</h2> <h2 className="text-xl font-bold"></h2>
{price !== null && ( <h1 className="text-2xl font-bold mt-4">{symbol}</h1>
{stockDescription && <p className="text-lg mb-4">{stockDescription}</p>}
{stockData !== null && (
<div className="mt-2"> <div className="mt-2">
<p>Current Price: ${price}</p> <p>Current Price: ${stockData.c}</p>
<p>Change: ${stockData.d}</p>
<p>Percent Change: {stockData.dp}%</p>
<p>High Price of the Day: ${stockData.h}</p>
<p>Low Price of the Day: ${stockData.l}</p>
<p>Open Price of the Day: ${stockData.o}</p>
<p>Previous Close Price: ${stockData.pc}</p>
</div> </div>
)} )}
{error && ( {error && (
@ -51,11 +93,10 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
<p>{error}</p> <p>{error}</p>
</div> </div>
)} )}
<Ticker symbol={symbol} />
</div> </div>
)} )}
</div> </div>
) );
} };
export default StockPrice export default StockPrice;

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react';
import { fetchQuote } from '../utils/sparkle' import { fetchQuote } from '../utils/sparkle';
import { tradeConditions } from '../utils/tradeConditions' import { tradeConditions } from '../utils/tradeConditions';
interface Trade { interface Trade {
p: number; // Price p: number; // Price
@ -11,102 +11,97 @@ interface Trade {
} }
const Ticker = ({ symbol }: { symbol: string }) => { const Ticker = ({ symbol }: { symbol: string }) => {
const [trades, setTrades] = useState<Trade[]>([]) const [latestTrade, setLatestTrade] = useState<Trade | null>(null);
const [bid, setBid] = useState<number | null>(null) const [bid, setBid] = useState<number | null>(null);
const [ask, setAsk] = useState<number | null>(null) const [ask, setAsk] = useState<number | null>(null);
const [webSocketInitialized, setWebSocketInitialized] = useState(false) const [webSocketInitialized, setWebSocketInitialized] = useState(false);
useEffect(() => { useEffect(() => {
// Fetch initial bid and ask prices
const initializePrices = async () => { const initializePrices = async () => {
try { try {
const quote = await fetchQuote(symbol) const quote = await fetchQuote(symbol);
setBid(quote.b) setBid(quote.b);
setAsk(quote.a) setAsk(quote.a);
} catch (error) { } catch (error) {
console.error('Failed to fetch initial bid/ask prices:', error) console.error('Failed to fetch initial bid/ask prices:', error);
} }
} };
initializePrices();
initializePrices() }, [symbol]);
}, [symbol])
useEffect(() => { useEffect(() => {
const initializeWebSocket = async () => { const initializeWebSocket = async () => {
try { try {
const response = await fetch('/api/ws/start') const response = await fetch('/api/ws/start');
const data = await response.json() const data = await response.json();
if (data.status === 'WebSocket server is running') { if (data.status === 'WebSocket server is running') {
setWebSocketInitialized(true) setWebSocketInitialized(true);
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize WebSocket:', error) console.error('Failed to initialize WebSocket:', error);
} }
} };
initializeWebSocket();
initializeWebSocket() }, []);
}, [])
useEffect(() => { useEffect(() => {
if (!webSocketInitialized) return if (!webSocketInitialized) return;
const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`)
const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`);
socket.onopen = () => { socket.onopen = () => {
console.log('WebSocket connection established') console.log('WebSocket connection established');
socket.send(JSON.stringify({ type: 'subscribe', symbol })) socket.send(JSON.stringify({ type: 'subscribe', symbol }));
} };
socket.onmessage = (event) => { socket.onmessage = (event) => {
const data = JSON.parse(event.data) const data = JSON.parse(event.data);
if (data.type === 'trade') { if (data.type === 'trade') {
setTrades((prevTrades) => [...data.data, ...prevTrades]) setLatestTrade(data.data[0]);
}
} }
};
socket.onclose = () => { socket.onclose = () => {
console.log('WebSocket connection closed') console.log('WebSocket connection closed');
} };
return () => { return () => {
socket.send(JSON.stringify({ type: 'unsubscribe', symbol })) socket.send(JSON.stringify({ type: 'unsubscribe', symbol }));
socket.close() socket.close();
} };
}, [symbol, webSocketInitialized]) }, [symbol, webSocketInitialized]);
const identifyTradeType = (trade: Trade) => { const identifyTradeType = (trade: Trade) => {
if (bid !== null && ask !== null) { if (bid !== null && ask !== null) {
if (trade.p >= ask) { if (trade.p >= ask) {
return 'Buy' return 'Buy';
} else if (trade.p <= bid) { } else if (trade.p <= bid) {
return 'Sell' return 'Sell';
} }
} }
return 'Unknown' return 'Unknown';
} };
const getTradeConditions = (trade: Trade) => { const getTradeConditions = (trade: Trade) => {
if (trade.c && trade.c.length > 0) { 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 ( return (
<div className="mt-4"> <div className="">
<h2 className="text-2xl font-bold">Latest Trades for {symbol}</h2> {symbol && latestTrade && (
<ul className="mt-2"> <div className="border-b dark:bg-overlay0 bg-overlay0 p-2 px-4 rounded-lg">
{trades.slice(0, 5).map((trade, index) => ( <div className="flex flex-row space-x-4 ed-lg text-xs">
<li key={index} className="p-2 border-b"> <strong>LIVE ${latestTrade.s}</strong>
<div>Price: ${trade.p}</div> <div>Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}</div>
<div>Volume: {trade.v}</div>
<div>Time: {new Date(trade.t).toLocaleTimeString()}</div>
<div>Type: {identifyTradeType(trade)}</div>
<div>Conditions: {getTradeConditions(trade)}</div>
</li>
))}
</ul>
</div> </div>
) </div>
} )}
</div>
);
};
export default Ticker export default Ticker;

20
src/pages/api/news.ts Normal file
View file

@ -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" });
}
}

View file

@ -1,3 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { fetchQuote } from "../../utils/sparkle"; import { fetchQuote } from "../../utils/sparkle";
@ -12,7 +13,7 @@ export default async function handler(
} }
try { try {
const quote = await fetchQuote(symbol); const quote = await fetchQuote(symbol as string);
res.status(200).json(quote); res.status(200).json(quote);
} catch (error) { } catch (error) {
res.status(500).json({ error: "Error fetching quote" }); res.status(500).json({ error: "Error fetching quote" });

View file

@ -1,6 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import NavigationBar from '../components/NavigationBar' import NavigationBar from '../components/NavigationBar'
import StockPrice from '../components/StockPrice' import StockPrice from '../components/StockPrice'
import Ticker from '@/components/Ticker'
import NewsColumn from '@/components/NewsColumn'
export default function Home() { export default function Home() {
const [symbol, setSymbol] = useState('') const [symbol, setSymbol] = useState('')
@ -12,8 +14,12 @@ export default function Home() {
return ( return (
<div> <div>
<NavigationBar onSelectSymbol={handleSelectSymbol} /> <NavigationBar onSelectSymbol={handleSelectSymbol} />
<div className="container mx-auto p-4"> <div className="container flex flex-row mx-auto gap-4 p-4">
<div>
<Ticker symbol={symbol} />
<StockPrice symbol={symbol} /> <StockPrice symbol={symbol} />
<NewsColumn />
</div>
</div> </div>
</div> </div>
) )

View file

@ -30,3 +30,12 @@ export const startWebSocket = async () => {
} }
return res.json(); return res.json();
}; };
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();
}