Compare commits

...

2 commits

Author SHA1 Message Date
57ea40937d feat: recommendations, ui improvements 2024-09-06 19:26:15 +08:00
7869362aeb improve: code quality 2024-09-06 18:24:33 +08:00
13 changed files with 366 additions and 93 deletions

View file

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

View file

@ -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">

View 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;

View 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;

View file

@ -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

View 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;

View file

@ -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]);
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 ( return (
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md mt-6"> <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} />
</div>
{symbol && ( {symbol && (
<div> <div className="relative z-10">
<h1 className="text-3xl font-bold mb-2">{symbol}</h1> <div className="flex justify-between items-center">
{stockDescription && <p className="text-lg mb-4 text-gray-600">{stockDescription}</p>} <h1 className="text-3xl font-bold text-text dark:text-text">{symbol}</h1>
{stockData && (
{stockData !== null && ( <div className="flex space-x-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4"> <PriceBadge label="Change:" value={stockData.d} isPositive={stockData.d >= 0} />
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <PriceBadge label="Percent Change:" value={`${stockData.dp}%`} isPositive={stockData.dp >= 0} />
<p className="text-xl font-semibold">Current Price:</p>
<p className="text-2xl">${stockData.c}</p>
</div> </div>
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> )}
<p className="text-xl font-semibold">Change:</p> </div>
<p className="text-2xl">${stockData.d}</p> {stockDescription && <p className="text-lg mb-4 text-text dark:text-subtext1">{stockDescription}</p>}
</div> {stockData && (
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <div className="mt-4">
<p className="text-xl font-semibold">Percent Change:</p> <div className="text-5xl font-bold mb-2 text-subtext0 dark:text-subtext0">${stockData.c}</div>
<p className="text-2xl">{stockData.dp}%</p> <div className="grid grid-cols-4 gap-4 mt-4">
</div> <div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <p className="text-sm font-semibold text-text dark:text-subtext1">High</p>
<p className="text-xl font-semibold">High Price of the Day:</p> <p className="text-xl text-text dark:text-text">${stockData.h}</p>
<p className="text-2xl">${stockData.h}</p> </div>
</div> <div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <p className="text-sm font-semibold text-text dark:text-subtext1">Low</p>
<p className="text-xl font-semibold">Low Price of the Day:</p> <p className="text-xl text-text dark:text-text">${stockData.l}</p>
<p className="text-2xl">${stockData.l}</p> </div>
</div> <div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <p className="text-sm font-semibold text-text dark:text-subtext1">Open</p>
<p className="text-xl font-semibold">Open Price of the Day:</p> <p className="text-xl text-text dark:text-text">${stockData.o}</p>
<p className="text-2xl">${stockData.o}</p> </div>
</div> <div className="bg-overlay0 dark:bg-overlay0 p-4 rounded-lg shadow-md">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg shadow-md"> <p className="text-sm font-semibold text-text dark:text-subtext1">Previous Close</p>
<p className="text-xl font-semibold">Previous Close Price:</p> <p className="text-xl text-text dark:text-text">${stockData.pc}</p>
<p className="text-2xl">${stockData.pc}</p> </div>
</div> </div>
</div> </div>
)} )}

View file

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

View 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 });
}
}

View file

@ -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} />
</div> <RecommendationTrendsWidget symbol={symbol} /> \
</div>
<div className="flex flex-col max-w-md">
<CompanyProfileCard ticker={symbol} />
</div>
</>
)}
{/* <NewsColumn /> */}
</div> </div>
</div> </div>
) );
} }

View 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;
};

View file

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