mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 02:40:35 +00:00
feat: massive ui improvements, proper fetching
This commit is contained in:
parent
57ea40937d
commit
d099694583
16 changed files with 426 additions and 89 deletions
74
src/components/CompanyNewsWidget.tsx
Normal file
74
src/components/CompanyNewsWidget.tsx
Normal file
|
@ -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<NewsArticle[]>([]);
|
||||
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 <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p className="text-red-500">{error}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4">
|
||||
<h2 className="text-xl font-bold text-text dark:text-text mb-4">Company News for {symbol}</h2>
|
||||
<div className="space-y-4">
|
||||
{news.map((article) => (
|
||||
<div key={article.id} className="rounded-lg bg-overlay0 dark:bg-mantle p-4">
|
||||
<a href={article.url} target="_blank" rel="noopener noreferrer" className="text-text dark:text-text hover:underline">
|
||||
<h3 className="text-lg font-semibold mb-2">{article.headline}</h3>
|
||||
<p className="text-sm text-subtext0 dark:text-subtext1 mb-2">{article.source} | {new Date(article.datetime * 1000).toLocaleDateString()}</p>
|
||||
<p className="text-sm text-text dark:text-text">{article.summary}</p>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyNewsWidget;
|
88
src/components/FinancialsWidget.tsx
Normal file
88
src/components/FinancialsWidget.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface FinancialsWidgetProps {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
interface FinancialMetric {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface FinancialData {
|
||||
metric: Record<string, number | string>;
|
||||
series: {
|
||||
annual: Record<string, { period: string; v: number }[]>;
|
||||
};
|
||||
}
|
||||
|
||||
const FinancialsWidget = ({ symbol }: FinancialsWidgetProps) => {
|
||||
const [financialData, setFinancialData] = useState<FinancialData | null>(null);
|
||||
const [formattedData, setFormattedData] = useState<FinancialMetric[]>([]);
|
||||
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 <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p className="text-red-500">{error}</p>;
|
||||
}
|
||||
|
||||
if (!financialData) {
|
||||
return <p>No financial data available.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4">
|
||||
<h2 className="text-xl font-bold text-text dark:text-text mb-4">Basic Financials for {symbol}</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{formattedData.map((data, index) => (
|
||||
<div key={index} className="bg-overlay0 dark:bg-mantle p-4 rounded-lg flex-1 min-w-[200px]">
|
||||
<p className="text-sm font-semibold text-subtext0 dark:text-subtext1">{data.name}</p>
|
||||
<p className="text-lg text-text dark:text-text">{data.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancialsWidget;
|
|
@ -33,53 +33,7 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
|||
</div>
|
||||
<div className="relative flex items-center space-x-4">
|
||||
<ThemeSwitcher />
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="bg-mantle px-4 py-2 rounded focus:outline-none focus:bg-overlay0"
|
||||
>
|
||||
<GiHamburgerMenu />
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-surface0 rounded-md shadow-lg z-20">
|
||||
<div className="py-2 px-4">
|
||||
<label
|
||||
htmlFor="currency"
|
||||
className="block text-sm font-medium text-text"
|
||||
>
|
||||
Currency:
|
||||
</label>
|
||||
<select
|
||||
id="currency"
|
||||
value={currency}
|
||||
onChange={handleCurrencyChange}
|
||||
className="mt-1 block w-full p-1 rounded border border-overlay2"
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="JPY">JPY</option>
|
||||
<option value="GBP">GBP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="py-2 px-4">
|
||||
<label
|
||||
htmlFor="watchlistView"
|
||||
className="block text-sm font-medium text-text"
|
||||
>
|
||||
View:
|
||||
</label>
|
||||
<select
|
||||
id="watchlistView"
|
||||
value={watchlistView}
|
||||
onChange={handleWatchlistViewChange}
|
||||
className="mt-1 block w-full p-1 rounded border border-overlay2"
|
||||
>
|
||||
<option value="priceChange">Price Change</option>
|
||||
<option value="percentageChange">Percentage Change</option>
|
||||
<option value="marketCap">Market Cap</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -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 <p className="text-red-500">{error}</p>;
|
||||
}
|
||||
|
||||
|
||||
const featuredArticle = news[0];
|
||||
const otherArticles = news.slice(1, 7);
|
||||
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">
|
||||
|
@ -63,12 +63,12 @@ const NewsColumn = () => {
|
|||
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">
|
||||
<div className="p-4 bg-surface0 dark:bg-surface0">
|
||||
<h3 className="text-xl font-bold text-text dark:text-text">{featuredArticle.headline}</h3>
|
||||
<p className="text-sm text-subtext1 dark:text-subtext1">
|
||||
{new Date(featuredArticle.datetime * 1000).toLocaleString()} | {featuredArticle.source}
|
||||
</p>
|
||||
<p className="text-sm mt-2">{featuredArticle.summary}</p>
|
||||
<p className="text-sm mt-2 text-text dark:text-subtext1">{featuredArticle.summary}</p>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
|
@ -89,9 +89,9 @@ const NewsColumn = () => {
|
|||
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">
|
||||
<div className="p-2 bg-surface0 dark:bg-surface0 h-full">
|
||||
<h3 className="text-sm font-semibold text-text dark:text-text">{article.headline}</h3>
|
||||
<p className="text-xs text-subtext1 dark:text-subtext1">
|
||||
{new Date(article.datetime * 1000).toLocaleString()} | {article.source}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -101,8 +101,8 @@ const NewsColumn = () => {
|
|||
</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>
|
||||
<div className="mt-4 lg:mt-0 lg:w-1/4 lg:order-last border-text dark:border-crust border p-4 rounded-lg bg-surface0 dark:bg-surface0 lg:overflow-y-auto lg:h-screen">
|
||||
<h2 className="text-xl font-bold text-text dark:text-text mb-4">Latest</h2>
|
||||
<ul>
|
||||
{latestArticles.map(article => (
|
||||
<li key={article.id} className="mb-4">
|
||||
|
@ -112,8 +112,8 @@ const NewsColumn = () => {
|
|||
rel="noopener noreferrer"
|
||||
className="hover:underline block"
|
||||
>
|
||||
<p className="text-sm font-semibold">{article.headline}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-sm font-semibold text-text dark:text-text">{article.headline}</p>
|
||||
<p className="text-xs text-subtext1 dark:text-subtext1">
|
||||
{new Date(article.datetime * 1000).toLocaleString()}
|
||||
</p>
|
||||
</a>
|
||||
|
|
|
@ -48,7 +48,7 @@ const PeersWidget = ({ symbol }: PeersWidgetProps) => {
|
|||
{peers.map((peer, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="inline-block bg-text dark:bg-text text-white text-sm font-medium py-1 px-2 m-1 rounded-md"
|
||||
className="inline-block bg-text dark:bg-crust text-white text-sm font-medium py-1 px-2 m-1 rounded-md"
|
||||
>
|
||||
{peer}
|
||||
</li>
|
||||
|
|
52
src/components/PriceGraph.tsx
Normal file
52
src/components/PriceGraph.tsx
Normal file
|
@ -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 (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={formattedData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-overlay1)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-crust)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-overlay0)" />
|
||||
<YAxis
|
||||
type="number"
|
||||
domain={[minPrice - 1, maxPrice + 1]}
|
||||
hide
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="price"
|
||||
stroke="var(--color-text)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceGraph;
|
|
@ -140,7 +140,7 @@ const SearchBar = ({ onSelectSymbol }: SearchBarProps) => {
|
|||
{isPickerVisible && (
|
||||
<>
|
||||
{
|
||||
<ul className="absolute border bg-mantle w-full mt-2 rounded-md border-surface0 overflow-hidden">
|
||||
<ul className="z-50 absolute border bg-mantle w-full mt-2 rounded-md border-surface0 overflow-hidden">
|
||||
|
||||
{suggestions.map((item, index) => (
|
||||
<li
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { LineChart, Line, CartesianGrid, Tooltip, ResponsiveContainer, defs, linearGradient, stop } from 'recharts';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { generateMockHistoricalData } from '../utils/mockHistoricalData';
|
||||
|
||||
interface StockGraphProps {
|
||||
|
@ -6,22 +7,32 @@ interface StockGraphProps {
|
|||
}
|
||||
|
||||
const StockGraph = ({ symbol }: StockGraphProps) => {
|
||||
const data = generateMockHistoricalData(symbol);
|
||||
const [historicalData, setHistoricalData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (symbol) {
|
||||
const data = generateMockHistoricalData(symbol);
|
||||
setHistoricalData(data);
|
||||
}
|
||||
}, [symbol]);
|
||||
|
||||
if (historicalData.length === 0) {
|
||||
return <p>Loading data...</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-text)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-text)" stopOpacity={1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="close" stroke="var(--color-text)" strokeWidth={2} dot={false} fill="url(#fillGradient)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md mt-4">
|
||||
<h2 className="text-xl font-bold mb-4">Stock Trend for {symbol}</h2>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<LineChart data={historicalData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line type="monotone" dataKey="close" stroke="#8884d8" activeDot={{ r: 8 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div className="relative bg-surface0 dark:bg-surface0 p-6 rounded-t-lg shadow-md">
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<StockGraph symbol={symbol} />
|
||||
<StockPriceGraph symbol={symbol} />
|
||||
</div>
|
||||
{symbol && (
|
||||
<div className="relative z-10">
|
||||
|
|
97
src/components/StockPriceGraph.tsx
Normal file
97
src/components/StockPriceGraph.tsx
Normal file
|
@ -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<StockData[] | null>(null);
|
||||
const [stockDescription, setStockDescription] = useState<string>('');
|
||||
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;
|
||||
}) => (
|
||||
<span
|
||||
className={`inline-block px-2 py-1 text-xs font-medium ${
|
||||
isPositive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
} rounded-md`}
|
||||
>
|
||||
{label} {value}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative bg-surface0 dark:bg-surface0 p-6 rounded-t-lg shadow-md overflow-hidden h-full">
|
||||
<div className="absolute inset-0 z-10">
|
||||
{stockData && <PriceGraph data={stockData} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockPriceGraph;
|
|
@ -97,8 +97,7 @@ const Ticker = ({ symbol }: { symbol: string }) => {
|
|||
<div className="flex flex-row space-x-4 ed-lg text-xs">
|
||||
<strong>LIVE ${latestTrade.s}</strong>
|
||||
<div>Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}</div>
|
||||
<div>Type: {identifyTradeType(latestTrade)}</div>
|
||||
<div>Conditions: {getTradeConditions(latestTrade)}</div>
|
||||
<div>{getTradeConditions(latestTrade)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
18
src/pages/api/basic-financials.ts
Normal file
18
src/pages/api/basic-financials.ts
Normal file
|
@ -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 });
|
||||
}
|
||||
}
|
18
src/pages/api/company-news.ts
Normal file
18
src/pages/api/company-news.ts
Normal file
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<NavigationBar onSelectSymbol={handleSelectSymbol} />
|
||||
<div className="gap-8 flex-row flex max-w-7xl w-full p-4">
|
||||
{symbol && (
|
||||
<div className="gap-8 flex-wrap lg:flex-nowrap flex-row flex max-w-7xl w-full p-4">
|
||||
{symbol ? (
|
||||
<>
|
||||
<div className="flex flex-col w-full">
|
||||
<StockPrice symbol={symbol} />
|
||||
<Ticker symbol={symbol} />
|
||||
<RecommendationTrendsWidget symbol={symbol} /> \
|
||||
<RecommendationTrendsWidget symbol={symbol} />
|
||||
<FinancialsWidget symbol={symbol} />
|
||||
</div>
|
||||
<div className="flex flex-col max-w-md">
|
||||
<CompanyProfileCard ticker={symbol} />
|
||||
<CompanyNewsWidget symbol={symbol} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* <NewsColumn /> */}
|
||||
) : (<NewsColumn />)}
|
||||
{/* */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
7
src/utils/formatPriceData.ts
Normal file
7
src/utils/formatPriceData.ts
Normal file
|
@ -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 }
|
||||
];
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue