mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 07:20:34 +00:00
Compare commits
2 commits
eff9a06d57
...
57ea40937d
Author | SHA1 | Date | |
---|---|---|---|
57ea40937d | |||
7869362aeb |
13 changed files with 366 additions and 93 deletions
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import PeersWidget from './PeersWidget';
|
||||||
|
|
||||||
interface CompanyProfileProps {
|
interface CompanyProfileProps {
|
||||||
ticker: string;
|
ticker: string;
|
||||||
|
@ -51,29 +52,30 @@ const CompanyProfileCard = ({ ticker }: CompanyProfileProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 border bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-4 flex-col">
|
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md">
|
||||||
<h1 className="mb-4 font-bold">About this company</h1>
|
<h1 className="text-xl font-bold text-text dark:text-text">About this company</h1>
|
||||||
<div className="flex items-center space-x-4 mb-4">
|
<div className="flex items-center gap-4 mt-4">
|
||||||
<img src={profile.logo} alt={`${profile.name} logo`} className="w-16 h-16 object-cover rounded-lg" />
|
<img src={profile.logo} alt={`${profile.name} logo`} className="w-16 h-16 object-cover rounded-lg" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold">{profile.name}</h2>
|
<h2 className="text-2xl font-bold text-text dark:text-text">{profile.name}</h2>
|
||||||
<p className="text-sm text-gray-500">{profile.ticker} | {profile.finnhubIndustry}</p>
|
<p className="text-sm text-text dark:text-subtext1">{profile.ticker} | {profile.finnhubIndustry}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||||
<div>
|
<div>
|
||||||
<p><span className="font-semibold">Country:</span> {profile.country}</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Country:</span> {profile.country}</p>
|
||||||
<p><span className="font-semibold">Currency:</span> {profile.currency}</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Currency:</span> {profile.currency}</p>
|
||||||
<p><span className="font-semibold">Exchange:</span> {profile.exchange}</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Exchange:</span> {profile.exchange}</p>
|
||||||
<p><span className="font-semibold">Market Cap:</span> ${profile.marketCapitalization.toFixed(2)}B</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Market Cap:</span> ${profile.marketCapitalization.toFixed(2)}B</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p><span className="font-semibold">IPO Date:</span> {new Date(profile.ipo).toLocaleDateString()}</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">IPO Date:</span> {new Date(profile.ipo).toLocaleDateString()}</p>
|
||||||
<p><span className="font-semibold">Outstanding Shares:</span> {profile.shareOutstanding.toFixed(2)}M</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Outstanding Shares:</span> {profile.shareOutstanding.toFixed(2)}M</p>
|
||||||
<p><span className="font-semibold">Phone:</span> {profile.phone}</p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Phone:</span> {profile.phone}</p>
|
||||||
<p><span className="font-semibold">Website:</span> <a href={profile.weburl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">{profile.weburl}</a></p>
|
<p className="text-text dark:text-subtext1"><span className="font-semibold text-text dark:text-subtext1">Website:</span> <a href={profile.weburl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">{profile.weburl}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PeersWidget symbol={ticker} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,8 +24,8 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-crust transition-all text-text border-b-2 border-surface0 px-4">
|
<nav className="w-full g-crust transition-all text-text border-b-2 border-surface0 px-4">
|
||||||
<div className="container mx-auto max-w-7xl flex items-center justify-between py-1">
|
<div className="container mx-auto max-w-7xl w-full flex items-center justify-between py-1">
|
||||||
<Link className="text-lg font-bold flex flex-row align-middle items-center gap-2" href="/"><IoSparkles /> <span className="md:block hidden">twinkle</span>
|
<Link className="text-lg font-bold flex flex-row align-middle items-center gap-2" href="/"><IoSparkles /> <span className="md:block hidden">twinkle</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
|
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
|
||||||
|
|
61
src/components/PeersWidget.tsx
Normal file
61
src/components/PeersWidget.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface PeersWidgetProps {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PeersWidget = ({ symbol }: PeersWidgetProps) => {
|
||||||
|
const [peers, setPeers] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPeers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/peers?symbol=${symbol}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
setPeers(data);
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to fetch data');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (symbol) {
|
||||||
|
fetchPeers();
|
||||||
|
}
|
||||||
|
}, [symbol]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="text-red-500">{error}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4">
|
||||||
|
<h2 className="text-xl font-bold text-text dark:text-text mb-2">Peers for {symbol}</h2>
|
||||||
|
<ul className="list-none">
|
||||||
|
{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"
|
||||||
|
>
|
||||||
|
{peer}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PeersWidget;
|
106
src/components/RecommendationTrends.tsx
Normal file
106
src/components/RecommendationTrends.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
interface RecommendationTrendsWidgetProps {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecommendationTrend {
|
||||||
|
period: string;
|
||||||
|
strongBuy: number;
|
||||||
|
buy: number;
|
||||||
|
hold: number;
|
||||||
|
sell: number;
|
||||||
|
strongSell: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecommendationTrendsWidget = ({ symbol }: RecommendationTrendsWidgetProps) => {
|
||||||
|
const [recommendationTrends, setRecommendationTrends] = useState<RecommendationTrend[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRecommendationTrends = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/recommendation-trends?symbol=${symbol}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
setRecommendationTrends(data);
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to fetch data');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (symbol) {
|
||||||
|
fetchRecommendationTrends();
|
||||||
|
}
|
||||||
|
}, [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">Recommendation Trends for {symbol}</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart data={recommendationTrends} margin={{ top: 5, right: 20, left: 20, bottom: 5 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="colorStrongBuy" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#32a852" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#32a852" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="colorBuy" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#7dc968" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#7dc968" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="colorHold" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#d3a637" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#d3a637" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="colorSell" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#e57373" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#e57373" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="colorStrongSell" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="#c62828" stopOpacity={0.8} />
|
||||||
|
<stop offset="95%" stopColor="#c62828" stopOpacity={0} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--color-overlay0)" />
|
||||||
|
<XAxis dataKey="period" tick={{ fill: "var(--color-text)" }} />
|
||||||
|
<YAxis tick={{ fill: "var(--color-text)" }} />
|
||||||
|
<Tooltip contentStyle={{ backgroundColor: "var(--color-surface0)", border: "none" }} />
|
||||||
|
<Legend wrapperStyle={{ color: "var(--color-text)" }}/>
|
||||||
|
<Bar dataKey="strongBuy" fill="url(#colorStrongBuy)" />
|
||||||
|
<Bar dataKey="buy" fill="url(#colorBuy)" />
|
||||||
|
<Bar dataKey="hold" fill="url(#colorHold)" />
|
||||||
|
<Bar dataKey="sell" fill="url(#colorSell)" />
|
||||||
|
<Bar dataKey="strongSell" fill="url(#colorStrongSell)" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecommendationTrendsWidget;
|
|
@ -177,7 +177,7 @@ const SearchBar = ({ onSelectSymbol }: SearchBarProps) => {
|
||||||
? "Searching..."
|
? "Searching..."
|
||||||
: "Start searching"
|
: "Start searching"
|
||||||
: `Searching...`
|
: `Searching...`
|
||||||
: `Showing top hits for '${query}'. (Matched ${totalCount} result${totalCount > 1 ? "s" : ""})`}
|
: `Showing top hits for '${query}'. (Matched ${totalCount} result${totalCount > 1 ? "s" : ""}).`}
|
||||||
</span>
|
</span>
|
||||||
{loading && (
|
{loading && (
|
||||||
<span
|
<span
|
||||||
|
|
28
src/components/StockGraph.tsx
Normal file
28
src/components/StockGraph.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { LineChart, Line, CartesianGrid, Tooltip, ResponsiveContainer, defs, linearGradient, stop } from 'recharts';
|
||||||
|
import { generateMockHistoricalData } from '../utils/mockHistoricalData';
|
||||||
|
|
||||||
|
interface StockGraphProps {
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockGraph = ({ symbol }: StockGraphProps) => {
|
||||||
|
const data = generateMockHistoricalData(symbol);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockGraph;
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import StockGraph from './StockGraph';
|
||||||
|
|
||||||
interface StockPriceProps {
|
interface StockPriceProps {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
@ -63,42 +64,49 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [symbol]);
|
}, [symbol]);
|
||||||
|
|
||||||
return (
|
const PriceBadge = ({ label, value, isPositive = true }: { label: string, value: number | string, isPositive?: boolean }) => (
|
||||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md mt-6">
|
<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`}>
|
||||||
{symbol && (
|
{label} {value}
|
||||||
<div>
|
</span>
|
||||||
<h1 className="text-3xl font-bold mb-2">{symbol}</h1>
|
);
|
||||||
{stockDescription && <p className="text-lg mb-4 text-gray-600">{stockDescription}</p>}
|
|
||||||
|
|
||||||
{stockData !== null && (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
<div className="relative bg-surface0 dark:bg-surface0 p-6 rounded-t-lg shadow-md">
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
<div className="absolute inset-0 opacity-20">
|
||||||
<p className="text-xl font-semibold">Current Price:</p>
|
<StockGraph symbol={symbol} />
|
||||||
<p className="text-2xl">${stockData.c}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
{symbol && (
|
||||||
<p className="text-xl font-semibold">Change:</p>
|
<div className="relative z-10">
|
||||||
<p className="text-2xl">${stockData.d}</p>
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-3xl font-bold text-text dark:text-text">{symbol}</h1>
|
||||||
|
{stockData && (
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<PriceBadge label="Change:" value={stockData.d} isPositive={stockData.d >= 0} />
|
||||||
|
<PriceBadge label="Percent Change:" value={`${stockData.dp}%`} isPositive={stockData.dp >= 0} />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
)}
|
||||||
<p className="text-xl font-semibold">Percent Change:</p>
|
|
||||||
<p className="text-2xl">{stockData.dp}%</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
{stockDescription && <p className="text-lg mb-4 text-text dark:text-subtext1">{stockDescription}</p>}
|
||||||
<p className="text-xl font-semibold">High Price of the Day:</p>
|
{stockData && (
|
||||||
<p className="text-2xl">${stockData.h}</p>
|
<div className="mt-4">
|
||||||
|
<div className="text-5xl font-bold mb-2 text-subtext0 dark:text-subtext0">${stockData.c}</div>
|
||||||
|
<div className="grid grid-cols-4 gap-4 mt-4">
|
||||||
|
<div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
|
||||||
|
<p className="text-sm font-semibold text-text dark:text-subtext1">High</p>
|
||||||
|
<p className="text-xl text-text dark:text-text">${stockData.h}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
<div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
|
||||||
<p className="text-xl font-semibold">Low Price of the Day:</p>
|
<p className="text-sm font-semibold text-text dark:text-subtext1">Low</p>
|
||||||
<p className="text-2xl">${stockData.l}</p>
|
<p className="text-xl text-text dark:text-text">${stockData.l}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
<div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
|
||||||
<p className="text-xl font-semibold">Open Price of the Day:</p>
|
<p className="text-sm font-semibold text-text dark:text-subtext1">Open</p>
|
||||||
<p className="text-2xl">${stockData.o}</p>
|
<p className="text-xl text-text dark:text-text">${stockData.o}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
|
||||||
|
<p className="text-sm font-semibold text-text dark:text-subtext1">Previous Close</p>
|
||||||
|
<p className="text-xl text-text dark:text-text">${stockData.pc}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md">
|
|
||||||
<p className="text-xl font-semibold">Previous Close Price:</p>
|
|
||||||
<p className="text-2xl">${stockData.pc}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -17,7 +17,6 @@ const Ticker = ({ symbol }: { symbol: string }) => {
|
||||||
const [webSocketInitialized, setWebSocketInitialized] = useState(false);
|
const [webSocketInitialized, setWebSocketInitialized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const initializePrices = async () => {
|
const initializePrices = async () => {
|
||||||
try {
|
try {
|
||||||
const quote = await fetchQuote(symbol);
|
const quote = await fetchQuote(symbol);
|
||||||
|
@ -57,8 +56,9 @@ const Ticker = ({ symbol }: { symbol: string }) => {
|
||||||
|
|
||||||
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' && data.data.some((trade: Trade) => trade.s === symbol)) {
|
||||||
setLatestTrade(data.data[0]);
|
const tradeForSymbol = data.data.find((trade: Trade) => trade.s === symbol);
|
||||||
|
setLatestTrade(tradeForSymbol);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,10 +93,12 @@ const Ticker = ({ symbol }: { symbol: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
{symbol && latestTrade && (
|
{symbol && latestTrade && (
|
||||||
<div className="border-b dark:bg-overlay0 bg-overlay0 p-2 px-4 rounded-lg">
|
<div className="border-b dark:bg-overlay0 bg-overlay0 p-2 px-4 rounded-b-lg">
|
||||||
<div className="flex flex-row space-x-4 ed-lg text-xs">
|
<div className="flex flex-row space-x-4 ed-lg text-xs">
|
||||||
<strong>LIVE ${latestTrade.s}</strong>
|
<strong>LIVE ${latestTrade.s}</strong>
|
||||||
<div>Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}</div>
|
<div>Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}</div>
|
||||||
|
<div>Type: {identifyTradeType(latestTrade)}</div>
|
||||||
|
<div>Conditions: {getTradeConditions(latestTrade)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
18
src/pages/api/peers.ts
Normal file
18
src/pages/api/peers.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { fetchPeers } 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 fetchPeers(symbol);
|
||||||
|
res.status(200).json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: "Error fetching peers" });
|
||||||
|
}
|
||||||
|
}
|
18
src/pages/api/recommendation-trends.ts
Normal file
18
src/pages/api/recommendation-trends.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { fetchRecommendationTrends } 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 fetchRecommendationTrends(symbol);
|
||||||
|
res.status(200).json(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,37 @@
|
||||||
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 Ticker from '../components/Ticker';
|
||||||
import NewsColumn from '@/components/NewsColumn'
|
import NewsColumn from '../components/NewsColumn';
|
||||||
import CompanyProfileCard from '@/components/CompanyProfileCard'
|
import CompanyProfileCard from '../components/CompanyProfileCard';
|
||||||
|
import PeersWidget from '../components/PeersWidget';
|
||||||
|
import RecommendationTrendsWidget from '@/components/RecommendationTrends';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [symbol, setSymbol] = useState('')
|
const [symbol, setSymbol] = useState('');
|
||||||
|
|
||||||
const handleSelectSymbol = (selectedSymbol: string) => {
|
const handleSelectSymbol = (selectedSymbol: string) => {
|
||||||
setSymbol(selectedSymbol)
|
setSymbol(selectedSymbol);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col items-center w-full">
|
||||||
<NavigationBar onSelectSymbol={handleSelectSymbol} />
|
<NavigationBar onSelectSymbol={handleSelectSymbol} />
|
||||||
<div className="container flex flex-row mx-auto max-w-7xl">
|
<div className="gap-8 flex-row flex max-w-7xl w-full p-4">
|
||||||
<div>
|
{symbol && (
|
||||||
<Ticker symbol={symbol} />
|
<>
|
||||||
<CompanyProfileCard ticker={symbol} />
|
<div className="flex flex-col w-full">
|
||||||
<StockPrice symbol={symbol} />
|
<StockPrice symbol={symbol} />
|
||||||
<NewsColumn />
|
<Ticker symbol={symbol} />
|
||||||
|
<RecommendationTrendsWidget symbol={symbol} /> \
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col max-w-md">
|
||||||
|
<CompanyProfileCard ticker={symbol} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* <NewsColumn /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
16
src/utils/mockhistoricalData.ts
Normal file
16
src/utils/mockhistoricalData.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export const generateMockHistoricalData = (symbol: string) => {
|
||||||
|
// Mock data for the past 10 days
|
||||||
|
const mockData = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (let i = 10; i >= 0; i--) {
|
||||||
|
const date = new Date(now);
|
||||||
|
date.setDate(now.getDate() - i);
|
||||||
|
mockData.push({
|
||||||
|
date: date.toISOString().split('T')[0],
|
||||||
|
close: (Math.random() * 100 + 100).toFixed(2), // Generate a random closing price
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockData;
|
||||||
|
};
|
|
@ -1,21 +1,23 @@
|
||||||
const SPARKLE_BASE_URL = process.env.NEXT_PUBLIC_SPARKLE_BASE_URL;
|
const SPARKLE_BASE_URL = process.env.NEXT_PUBLIC_SPARKLE_BASE_URL;
|
||||||
|
|
||||||
|
const handleResponse = async (res: Response) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.message || 'An error occurred');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchQuote = async (symbol: string) => {
|
export const fetchQuote = async (symbol: string) => {
|
||||||
const url = `${SPARKLE_BASE_URL}/api/v1/quote?symbol=${symbol}`;
|
const url = `${SPARKLE_BASE_URL}/api/v1/quote?symbol=${symbol}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
return handleResponse(res);
|
||||||
throw new Error('Error fetching quote');
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchSymbols = async (symbol: string) => {
|
export const fetchSymbols = async (symbol: string) => {
|
||||||
const url = `${SPARKLE_BASE_URL}/api/v1/search?query=${symbol}`;
|
const url = `${SPARKLE_BASE_URL}/api/v1/search?query=${symbol}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
const data = await handleResponse(res);
|
||||||
throw new Error('Error fetching symbols');
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
return {
|
return {
|
||||||
symbols: data.result,
|
symbols: data.result,
|
||||||
totalCount: data.totalCount,
|
totalCount: data.totalCount,
|
||||||
|
@ -25,26 +27,29 @@ export const fetchSymbols = async (symbol: string) => {
|
||||||
export const startWebSocket = async () => {
|
export const startWebSocket = async () => {
|
||||||
const url = `${SPARKLE_BASE_URL}/ws/start-websocket`;
|
const url = `${SPARKLE_BASE_URL}/ws/start-websocket`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
return handleResponse(res);
|
||||||
throw new Error('Error starting WebSocket');
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchNews = async () => {
|
export const fetchNews = async () => {
|
||||||
const url = `${SPARKLE_BASE_URL}/api/v1/marketnews`;
|
const url = `${SPARKLE_BASE_URL}/api/v1/marketnews`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
return handleResponse(res);
|
||||||
throw new Error('Error fetching news');
|
};
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchProfile = async (symbol: string) => {
|
export const fetchProfile = async (symbol: string) => {
|
||||||
const url = `${SPARKLE_BASE_URL}/api/v1/profile?symbol=${symbol}`;
|
const url = `${SPARKLE_BASE_URL}/api/v1/profile?symbol=${symbol}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
return handleResponse(res);
|
||||||
throw new Error('Error fetching profile');
|
};
|
||||||
}
|
|
||||||
return res.json();
|
export const fetchPeers = async (symbol: string) => {
|
||||||
|
const url = `${SPARKLE_BASE_URL}/api/v1/peers?symbol=${symbol}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
return handleResponse(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
};
|
};
|
Loading…
Reference in a new issue