From d099694583b09ed9b67d91bcfe89f5efa20601b2 Mon Sep 17 00:00:00 2001 From: Ryana May Que Date: Fri, 6 Sep 2024 20:38:10 +0800 Subject: [PATCH] feat: massive ui improvements, proper fetching --- src/components/CompanyNewsWidget.tsx | 74 +++++++++++++++++++++ src/components/FinancialsWidget.tsx | 88 +++++++++++++++++++++++++ src/components/NavigationBar.tsx | 48 +------------- src/components/NewsColumn.tsx | 30 ++++----- src/components/PeersWidget.tsx | 2 +- src/components/PriceGraph.tsx | 52 +++++++++++++++ src/components/SearchBar.tsx | 2 +- src/components/StockGraph.tsx | 41 +++++++----- src/components/StockPrice.tsx | 5 +- src/components/StockPriceGraph.tsx | 97 ++++++++++++++++++++++++++++ src/components/Ticker.tsx | 3 +- src/pages/api/basic-financials.ts | 18 ++++++ src/pages/api/company-news.ts | 18 ++++++ src/pages/index.tsx | 15 +++-- src/utils/formatPriceData.ts | 7 ++ src/utils/sparkle.ts | 15 ++++- 16 files changed, 426 insertions(+), 89 deletions(-) create mode 100644 src/components/CompanyNewsWidget.tsx create mode 100644 src/components/FinancialsWidget.tsx create mode 100644 src/components/PriceGraph.tsx create mode 100644 src/components/StockPriceGraph.tsx create mode 100644 src/pages/api/basic-financials.ts create mode 100644 src/pages/api/company-news.ts create mode 100644 src/utils/formatPriceData.ts diff --git a/src/components/CompanyNewsWidget.tsx b/src/components/CompanyNewsWidget.tsx new file mode 100644 index 0000000..d8a315b --- /dev/null +++ b/src/components/CompanyNewsWidget.tsx @@ -0,0 +1,74 @@ +import { useState, useEffect } from 'react'; + +interface CompanyNewsWidgetProps { + symbol: string; +} + +interface NewsArticle { + category: string; + datetime: number; + headline: string; + id: number; + image: string; + related: string; + source: string; + summary: string; + url: string; +} + +const CompanyNewsWidget = ({ symbol }: CompanyNewsWidgetProps) => { + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchNews = async () => { + setLoading(true); + setError(''); + try { + const res = await fetch(`/api/company-news?symbol=${symbol}`); + const data = await res.json(); + if (res.ok) { + setNews(data); + } else { + setError(data.error || 'Failed to fetch data'); + } + } catch (err) { + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + + if (symbol) { + fetchNews(); + } + }, [symbol]); + + if (loading) { + return

Loading...

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

{error}

; + } + + return ( +
+

Company News for {symbol}

+
+ {news.map((article) => ( +
+ +

{article.headline}

+

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

+

{article.summary}

+
+
+ ))} +
+
+ ); +}; + +export default CompanyNewsWidget; \ No newline at end of file diff --git a/src/components/FinancialsWidget.tsx b/src/components/FinancialsWidget.tsx new file mode 100644 index 0000000..ee714d5 --- /dev/null +++ b/src/components/FinancialsWidget.tsx @@ -0,0 +1,88 @@ +import { useState, useEffect } from 'react'; + +interface FinancialsWidgetProps { + symbol: string; +} + +interface FinancialMetric { + name: string; + value: string | number; +} + +interface FinancialData { + metric: Record; + series: { + annual: Record; + }; +} + +const FinancialsWidget = ({ symbol }: FinancialsWidgetProps) => { + const [financialData, setFinancialData] = useState(null); + const [formattedData, setFormattedData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + const fetchFinancialData = async () => { + setLoading(true); + setError(''); + try { + const res = await fetch(`/api/basic-financials?symbol=${symbol}`); + const data = await res.json(); + if (res.ok) { + setFinancialData(data); + formatData(data); + } else { + setError(data.error || 'Failed to fetch data'); + } + } catch (err) { + setError('Failed to fetch data'); + } finally { + setLoading(false); + } + }; + + const formatData = (data: FinancialData) => { + const metrics = []; + for (const key in data.metric) { + metrics.push({ + name: key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()), + value: data.metric[key], + }); + } + setFormattedData(metrics); + }; + + if (symbol) { + fetchFinancialData(); + } + }, [symbol]); + + if (loading) { + return

Loading...

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

{error}

; + } + + if (!financialData) { + return

No financial data available.

; + } + + return ( +
+

Basic Financials for {symbol}

+
+ {formattedData.map((data, index) => ( +
+

{data.name}

+

{data.value}

+
+ ))} +
+
+ ); +}; + +export default FinancialsWidget; \ No newline at end of file diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index b71bdf4..cd6bd8c 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -33,53 +33,7 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
- - {dropdownOpen && ( -
-
- - -
-
- - -
-
- )} +
diff --git a/src/components/NewsColumn.tsx b/src/components/NewsColumn.tsx index 380703e..098c2c6 100644 --- a/src/components/NewsColumn.tsx +++ b/src/components/NewsColumn.tsx @@ -20,7 +20,7 @@ const NewsColumn = () => { useEffect(() => { const fetchNews = async () => { try { - const res = await fetch('/api/news'); + const res = await fetch('/api/news'); const data = await res.json(); const filteredNews = data.result.filter((article: any) => article.source !== 'MarketWatch'); setNews(filteredNews); @@ -30,7 +30,7 @@ const NewsColumn = () => { setLoading(false); } }; - + fetchNews(); }, []); @@ -42,10 +42,10 @@ const NewsColumn = () => { return

{error}

; } - const featuredArticle = news[0]; - const otherArticles = news.slice(1, 7); + const otherArticles = news.slice(1, 7); const latestArticles = news.slice(7); + return (
@@ -63,12 +63,12 @@ const NewsColumn = () => { alt={featuredArticle.headline} className="w-full h-64 object-cover" /> -
-

{featuredArticle.headline}

-

+

+

{featuredArticle.headline}

+

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

-

{featuredArticle.summary}

+

{featuredArticle.summary}

)} @@ -89,9 +89,9 @@ const NewsColumn = () => { alt={article.headline} className="w-full h-32 object-cover rounded-t-lg" /> -
-

{article.headline}

-

+

+

{article.headline}

+

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

@@ -101,8 +101,8 @@ const NewsColumn = () => {
{/* Latest news sidebar */} -
-

Latest

+
+

Latest

    {latestArticles.map(article => (
  • @@ -112,8 +112,8 @@ const NewsColumn = () => { rel="noopener noreferrer" className="hover:underline block" > -

    {article.headline}

    -

    +

    {article.headline}

    +

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

    diff --git a/src/components/PeersWidget.tsx b/src/components/PeersWidget.tsx index fa0d304..0a803bb 100644 --- a/src/components/PeersWidget.tsx +++ b/src/components/PeersWidget.tsx @@ -48,7 +48,7 @@ const PeersWidget = ({ symbol }: PeersWidgetProps) => { {peers.map((peer, index) => (
  • {peer}
  • diff --git a/src/components/PriceGraph.tsx b/src/components/PriceGraph.tsx new file mode 100644 index 0000000..89fb462 --- /dev/null +++ b/src/components/PriceGraph.tsx @@ -0,0 +1,52 @@ +import { + LineChart, + Line, + CartesianGrid, + ResponsiveContainer, + defs, + linearGradient, + stop, + YAxis, + } from 'recharts'; + import { formatPriceData } from '../utils/formatPriceData'; + + interface PriceGraphProps { + data: { c: number | null; h: number | null; l: number | null; o: number | null; pc: number | null; t: number | null; }[]; + } + + const PriceGraph = ({ data }: PriceGraphProps) => { + const formattedData = formatPriceData(data); + + const prices = formattedData.map((d) => d.price); + const minPrice = Math.min(...prices); + const maxPrice = Math.max(...prices); + + return ( + + + + + + + + + + + + + + ); + }; + + export default PriceGraph; \ No newline at end of file diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 8469791..395a263 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -140,7 +140,7 @@ const SearchBar = ({ onSelectSymbol }: SearchBarProps) => { {isPickerVisible && ( <> { -
      +
        {suggestions.map((item, index) => (
      • { - const data = generateMockHistoricalData(symbol); + const [historicalData, setHistoricalData] = useState([]); + + useEffect(() => { + if (symbol) { + const data = generateMockHistoricalData(symbol); + setHistoricalData(data); + } + }, [symbol]); + + if (historicalData.length === 0) { + return

        Loading data...

        ; + } return ( - - - - - - - - - - - - - +
        +

        Stock Trend for {symbol}

        + + + + + + + + + +
        ); }; diff --git a/src/components/StockPrice.tsx b/src/components/StockPrice.tsx index 583cc16..9a4dfb3 100644 --- a/src/components/StockPrice.tsx +++ b/src/components/StockPrice.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import StockGraph from './StockGraph'; +import PriceGraph from './PriceGraph'; +import StockPriceGraph from './StockPriceGraph'; interface StockPriceProps { symbol: string; @@ -73,7 +74,7 @@ const StockPrice = ({ symbol }: StockPriceProps) => { return (
        - +
        {symbol && (
        diff --git a/src/components/StockPriceGraph.tsx b/src/components/StockPriceGraph.tsx new file mode 100644 index 0000000..0a0f7db --- /dev/null +++ b/src/components/StockPriceGraph.tsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from 'react'; +import PriceGraph from './PriceGraph'; + +interface StockPriceProps { + 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 + t: number; // Timestamp +} + +const StockPriceGraph = ({ symbol }: StockPriceProps) => { + const [stockData, setStockData] = useState(null); + const [stockDescription, setStockDescription] = useState(''); + const [error, setError] = useState(''); + + const fetchStockPrice = async (selectedSymbol: string) => { + setError(''); + try { + const res = await fetch(`/api/quote?symbol=${selectedSymbol}`); + const data = await res.json(); + if (data.error) { + setError(data.error); + setStockData(null); + } else { + setStockData(data); + } + } catch (err) { + 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; + if (symbol) { + fetchStockPrice(symbol); + fetchStockDescription(symbol); + intervalId = setInterval(() => fetchStockPrice(symbol), 300000); // 300000 ms = 5 minutes + } + return () => clearInterval(intervalId); + }, [symbol]); + + const PriceBadge = ({ + label, + value, + isPositive = true, + }: { + label: string; + value: number | string; + isPositive?: boolean; + }) => ( + + {label} {value} + + ); + + return ( +
        +
        + {stockData && } +
        +
        + ); +}; + +export default StockPriceGraph; \ No newline at end of file diff --git a/src/components/Ticker.tsx b/src/components/Ticker.tsx index ee80381..f07f7ae 100644 --- a/src/components/Ticker.tsx +++ b/src/components/Ticker.tsx @@ -97,8 +97,7 @@ const Ticker = ({ symbol }: { symbol: string }) => {
        LIVE ${latestTrade.s}
        Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}
        -
        Type: {identifyTradeType(latestTrade)}
        -
        Conditions: {getTradeConditions(latestTrade)}
        +
        {getTradeConditions(latestTrade)}
        )} diff --git a/src/pages/api/basic-financials.ts b/src/pages/api/basic-financials.ts new file mode 100644 index 0000000..6dfbfc6 --- /dev/null +++ b/src/pages/api/basic-financials.ts @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { fetchBasicFinancials } from '../../utils/sparkle'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { symbol } = req.query; + + if (!symbol || typeof symbol !== 'string') { + res.status(400).json({ error: 'Invalid symbol' }); + return; + } + + try { + const data = await fetchBasicFinancials(symbol); + res.status(200).json(data); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} \ No newline at end of file diff --git a/src/pages/api/company-news.ts b/src/pages/api/company-news.ts new file mode 100644 index 0000000..73b08df --- /dev/null +++ b/src/pages/api/company-news.ts @@ -0,0 +1,18 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { fetchCompanyNews } from '../../utils/sparkle'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { symbol } = req.query; + + if (!symbol || typeof symbol !== 'string') { + res.status(400).json({ error: 'Invalid symbol' }); + return; + } + + try { + const data = await fetchCompanyNews(symbol); + res.status(200).json(data); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8a00953..7c5bafa 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -6,6 +6,9 @@ import NewsColumn from '../components/NewsColumn'; import CompanyProfileCard from '../components/CompanyProfileCard'; import PeersWidget from '../components/PeersWidget'; import RecommendationTrendsWidget from '@/components/RecommendationTrends'; +import FinancialsWidget from '@/components/FinancialsWidget'; +import CompanyNews from './api/company-news'; +import CompanyNewsWidget from '@/components/CompanyNewsWidget'; export default function Home() { const [symbol, setSymbol] = useState(''); @@ -17,20 +20,22 @@ export default function Home() { return (
        -
        - {symbol && ( +
        + {symbol ? ( <>
        - \ + +
        +
        - )} - {/* */} + ) : ()} + {/* */}
        ); diff --git a/src/utils/formatPriceData.ts b/src/utils/formatPriceData.ts new file mode 100644 index 0000000..5f6e5b6 --- /dev/null +++ b/src/utils/formatPriceData.ts @@ -0,0 +1,7 @@ +export const formatPriceData = (data) => { + return [ + { name: 'Previous Close', price: data.pc }, + { name: 'Open', price: data.o }, + { name: 'Current', price: data.c } + ]; + }; \ No newline at end of file diff --git a/src/utils/sparkle.ts b/src/utils/sparkle.ts index 6ef7232..3c6ea9f 100644 --- a/src/utils/sparkle.ts +++ b/src/utils/sparkle.ts @@ -52,4 +52,17 @@ export const fetchRecommendationTrends = async (symbol: string) => { const url = `${SPARKLE_BASE_URL}/api/v1/recommendation-trends?symbol=${symbol}`; const res = await fetch(url); return handleResponse(res); -}; \ No newline at end of file +}; + +export const fetchBasicFinancials = async (symbol: string) => { + const url = `${SPARKLE_BASE_URL}/api/v1/basic-financials?symbol=${symbol}`; + const res = await fetch(url); + return handleResponse(res); +} + +export const fetchCompanyNews = async (symbol: string) => { + const url = `${SPARKLE_BASE_URL}/api/v1/company-news?symbol=${symbol}`; + const res = await fetch(url); + return handleResponse(res); +} +