feat: implement skeletons, fix css, etc

This commit is contained in:
ryana mittens 2024-09-06 21:46:34 +08:00
parent d099694583
commit 4d8f7f72e5
14 changed files with 341 additions and 69 deletions

4
next copy.config.mjs Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

1
public/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M208 512a24.84 24.84 0 0 1-23.34-16l-39.84-103.6a16.06 16.06 0 0 0-9.19-9.19L32 343.34a25 25 0 0 1 0-46.68l103.6-39.84a16.06 16.06 0 0 0 9.19-9.19L184.66 144a25 25 0 0 1 46.68 0l39.84 103.6a16.06 16.06 0 0 0 9.19 9.19l103 39.63a25.49 25.49 0 0 1 16.63 24.1 24.82 24.82 0 0 1-16 22.82l-103.6 39.84a16.06 16.06 0 0 0-9.19 9.19L231.34 496A24.84 24.84 0 0 1 208 512zm66.85-254.84zM88 176a14.67 14.67 0 0 1-13.69-9.4l-16.86-43.84a7.28 7.28 0 0 0-4.21-4.21L9.4 101.69a14.67 14.67 0 0 1 0-27.38l43.84-16.86a7.31 7.31 0 0 0 4.21-4.21L74.16 9.79A15 15 0 0 1 86.23.11a14.67 14.67 0 0 1 15.46 9.29l16.86 43.84a7.31 7.31 0 0 0 4.21 4.21l43.84 16.86a14.67 14.67 0 0 1 0 27.38l-43.84 16.86a7.28 7.28 0 0 0-4.21 4.21l-16.86 43.84A14.67 14.67 0 0 1 88 176zm312 80a16 16 0 0 1-14.93-10.26l-22.84-59.37a8 8 0 0 0-4.6-4.6l-59.37-22.84a16 16 0 0 1 0-29.86l59.37-22.84a8 8 0 0 0 4.6-4.6l22.67-58.95a16.45 16.45 0 0 1 13.17-10.57 16 16 0 0 1 16.86 10.15l22.84 59.37a8 8 0 0 0 4.6 4.6l59.37 22.84a16 16 0 0 1 0 29.86l-59.37 22.84a8 8 0 0 0-4.6 4.6l-22.84 59.37A16 16 0 0 1 400 256z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -45,8 +45,20 @@ const CompanyNewsWidget = ({ symbol }: CompanyNewsWidgetProps) => {
}
}, [symbol]);
const SkeletonLoader = () => (
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="rounded-lg bg-gray-300 dark:bg-gray-700 p-4 gap-3 animate-pulse">
<div className="h-6 bg-gray-400 dark:bg-gray-600 rounded mb-2"></div>
<div className="h-4 bg-gray-400 dark:bg-gray-600 rounded mb-2"></div>
<div className="h-4 bg-gray-400 dark:bg-gray-600 rounded w-3/4"></div>
</div>
))}
</div>
);
if (loading) {
return <p>Loading...</p>;
return <SkeletonLoader />;
}
if (error) {
@ -57,7 +69,7 @@ const CompanyNewsWidget = ({ symbol }: CompanyNewsWidgetProps) => {
<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) => (
{news.slice(0, 20).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>

View file

@ -23,10 +23,12 @@ interface CompanyProfileData {
const CompanyProfileCard = ({ ticker }: CompanyProfileProps) => {
const [profile, setProfile] = useState<CompanyProfileData | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchCompanyProfile = async (ticker: string) => {
setError('');
try {
setLoading(true);
const res = await fetch(`/api/profile?symbol=${ticker}`);
const data = await res.json();
if (data.error) {
@ -38,6 +40,8 @@ const CompanyProfileCard = ({ ticker }: CompanyProfileProps) => {
} catch (err) {
setError('Failed to fetch company profile');
setProfile(null);
} finally {
setLoading(false);
}
};
@ -47,8 +51,39 @@ const CompanyProfileCard = ({ ticker }: CompanyProfileProps) => {
}
}, [ticker]);
if (!profile) {
return error ? <p className="text-red-500">{error}</p> : <p>Loading...</p>;
const SkeletonLoader = () => (
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg gap-4 shadow-md animate-pulse">
<div className="h-6 bg-gray-300 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gray-300 dark:bg-gray-700 rounded-lg"></div>
<div className="flex-1">
<div className="h-5 bg-gray-300 dark:bg-gray-700 rounded mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-1/2"></div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
</div>
<div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
</div>
</div>
</div>
);
if (loading) {
return <SkeletonLoader />;
}
if (error) {
return <p className="text-red-500">{error}</p>;
}
return (

View file

@ -58,8 +58,19 @@ const FinancialsWidget = ({ symbol }: FinancialsWidgetProps) => {
}
}, [symbol]);
const SkeletonLoader = () => (
<div className="hidden md:block p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4 animate-pulse">
<h2 className="h-6 bg-gray-300 dark:bg-gray-700 rounded w-1/3 mb-4"></h2>
<div className="flex flex-wrap gap-4">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="bg-gray-300 dark:bg-gray-700 p-4 rounded-lg flex-1 min-w-[200px] h-16"></div>
))}
</div>
</div>
);
if (loading) {
return <p>Loading...</p>;
return <SkeletonLoader />;
}
if (error) {
@ -71,7 +82,7 @@ const FinancialsWidget = ({ symbol }: FinancialsWidgetProps) => {
}
return (
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4">
<div className="hidden md:block 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) => (

12
src/components/Footer.tsx Normal file
View file

@ -0,0 +1,12 @@
// components/Footer.tsx
const Footer = () => {
return (
<footer className="w-full bg-crust text-text py-4 border-t-2 border-surface0">
<div className="container mx-auto max-w-7xl flex flex-col text-center items-center justify-center space-y-2">
<p className="text-sm">this project is open-source and is available at github.com/ryanamay or code.lgbt/ryanamay</p>
</div>
</footer>
);
};
export default Footer;

View file

@ -0,0 +1,15 @@
// components/HeadTemplate.tsx
import Head from 'next/head';
const HeadTemplate = () => {
return (
<Head>
<title>Twinkle - Track Your Stocks</title>
<meta name="description" content="Twinkle helps you track your favorite stocks in a delightful and user-friendly way." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" />
</Head>
);
};
export default HeadTemplate;

View file

@ -0,0 +1,44 @@
import React from 'react';
import { FaArrowUp } from 'react-icons/fa';
const HeroSection = () => {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] mt-8 text-center bg-base text-text px-4">
<div className="relative mb-6">
<FaArrowUp className="text-4xl text-overlay0 animate-bounce" />
<p className="absolute top-[-2rem] left-1/2 transform -translate-x-1/2 text-xs w-[400px] text-text">Search your first stock</p>
</div>
<h1 className="text-6xl font-bold text-lavender mb-3">twinkle</h1>
<p className="text-3xl font-extrabold mb-8 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-500 to-red-500 ">
your symbol twinkling at the distance
</p>
<p className="text-lg text-overlay1-dark mb-4 max-w-2xl mx-auto px-4">
Welcome to <span className="font-bold text-lavender">twinkle</span>, your charming companion for keeping an eye on your favorite stocks.
</p>
<p className="text-lg text-overlay1-dark mb-8 max-w-2xl mx-auto px-4">
Powered by our magical <span className="font-bold text-lavender">sparkle aggregator API</span>, twinkle makes it a breeze to stay updated with the latest stock prices in a delightful and user-friendly way.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mt-6">
<a
href="https://code.lgbt/ryanamay/sparkle"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-600 hover:to-blue-600 text-white font-semibold rounded-lg shadow-md transform transition-all duration-300"
>
View Sparkle Source Code on Code.lgbt
</a>
<a
href="https://code.lgbt/ryanamay/twinkle"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 bg-gradient-to-r from-teal-400 to-green-500 hover:from-teal-500 hover:to-green-600 text-white font-semibold rounded-lg shadow-md transform transition-all duration-300"
>
View Twinkle Source Code on Code.lgbt
</a>
</div>
</div>
);
};
export default HeroSection;

View file

@ -1,43 +1,46 @@
import { useState, useEffect } from 'react'
import SearchBar from './SearchBar'
import Link from 'next/link'
import ThemeSwitcher from './ThemeSwitcher'
import { IoSparkles } from 'react-icons/io5'
import { FaHamburger } from 'react-icons/fa'
import { GiHamburgerMenu } from 'react-icons/gi'
import { useState } from 'react';
import SearchBar from './SearchBar';
import Link from 'next/link';
import ThemeSwitcher from './ThemeSwitcher';
import { IoSparkles } from 'react-icons/io5';
interface NavigationBarProps {
onSelectSymbol: (symbol: string) => void
onSelectSymbol: (symbol: string) => void;
}
const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
const [currency, setCurrency] = useState('USD')
const [watchlistView, setWatchlistView] = useState('priceChange')
const [dropdownOpen, setDropdownOpen] = useState(false)
const [currency, setCurrency] = useState('USD');
const [watchlistView, setWatchlistView] = useState('priceChange');
const [dropdownOpen, setDropdownOpen] = useState(false);
const handleCurrencyChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setCurrency(event.target.value)
}
setCurrency(event.target.value);
};
const handleWatchlistViewChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setWatchlistView(event.target.value)
}
setWatchlistView(event.target.value);
};
const resetSymbol = () => {
onSelectSymbol('');
};
return (
<nav className="w-full g-crust transition-all text-text border-b-2 border-surface0 px-4">
<nav className="w-full bg-crust transition-all text-text border-b-2 border-surface0 px-4">
<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>
<div onClick={resetSymbol} className="cursor-pointer text-lg font-bold flex flex-row align-middle items-center gap-2">
<IoSparkles />
<span className="md:block hidden">twinkle</span>
</div>
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
<SearchBar onSelectSymbol={onSelectSymbol} />
</div>
<div className="relative flex items-center space-x-4">
<ThemeSwitcher />
</div>
</div>
</nav>
)
}
);
};
export default NavigationBar
export default NavigationBar;

View file

@ -12,6 +12,11 @@ interface NewsArticle {
url: string;
}
// Skeleton components for loading state
const Skeleton = ({ className }: { className: string }) => (
<div className={`animate-pulse bg-overlay1 ${className}`}></div>
);
const NewsColumn = () => {
const [news, setNews] = useState<NewsArticle[]>([]);
const [loading, setLoading] = useState(true);
@ -22,7 +27,9 @@ const NewsColumn = () => {
try {
const res = await fetch('/api/news');
const data = await res.json();
const filteredNews = data.result.filter((article: any) => article.source !== 'MarketWatch');
const filteredNews = data.result.filter(
(article: any) => article.source !== 'MarketWatch'
);
setNews(filteredNews);
} catch (err) {
setError('Failed to fetch news');
@ -35,7 +42,54 @@ const NewsColumn = () => {
}, []);
if (loading) {
return <p>Loading...</p>;
return (
<div className="lg:flex lg:space-x-4 w-full">
<div className="lg:flex-1 lg:space-y-4">
{/* Main featured article skeleton */}
<div className="mb-4 lg:mb-0">
<div className="block overflow-hidden rounded-lg shadow-lg">
<Skeleton className="w-full h-64" />
<div className="p-4 bg-surface0 dark:bg-surface0">
<Skeleton className="h-6 mb-2" />
<Skeleton className="h-4 mb-1" />
<Skeleton className="h-4" />
</div>
</div>
</div>
{/* Other articles skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, index) => (
<div
key={index}
className="block overflow-hidden rounded-lg shadow-lg"
>
<Skeleton className="w-full h-32 rounded-t-lg" />
<div className="p-2 bg-surface0 dark:bg-surface0 h-full">
<Skeleton className="h-4 mb-1" />
<Skeleton className="h-4" />
</div>
</div>
))}
</div>
</div>
{/* Latest news sidebar skeleton */}
<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>
{Array.from({ length: 5 }).map((_, index) => (
<li key={index} className="mb-4">
<Skeleton className="h-4 mb-1" />
<Skeleton className="h-4" />
</li>
))}
</ul>
</div>
</div>
);
}
if (error) {
@ -64,11 +118,16 @@ const NewsColumn = () => {
className="w-full h-64 object-cover"
/>
<div className="p-4 bg-surface0 dark:bg-surface0">
<h3 className="text-xl font-bold text-text dark:text-text">{featuredArticle.headline}</h3>
<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}
{new Date(featuredArticle.datetime * 1000).toLocaleString()} |{' '}
{featuredArticle.source}
</p>
<p className="text-sm mt-2 text-text dark:text-subtext1">
{featuredArticle.summary}
</p>
<p className="text-sm mt-2 text-text dark:text-subtext1">{featuredArticle.summary}</p>
</div>
</a>
)}
@ -76,7 +135,7 @@ const NewsColumn = () => {
{/* Other articles */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{otherArticles.map(article => (
{otherArticles.map((article) => (
<a
key={article.id}
href={article.url}
@ -90,9 +149,12 @@ const NewsColumn = () => {
className="w-full h-32 object-cover rounded-t-lg"
/>
<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>
<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}
{new Date(article.datetime * 1000).toLocaleString()} |{' '}
{article.source}
</p>
</div>
</a>
@ -101,10 +163,12 @@ const NewsColumn = () => {
</div>
{/* Latest news sidebar */}
<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>
<div className="mt-4 lg:mt-0 lg:w-1/4 lg:order-last border- dark:border-crust border p-4 lg:overflow-y-auto lg:h-screen bg-surface0 dark:bg-surface0 rounded-lg shadow-md">
<h2 className="text-xl font-bold text-text dark:text-text mb-4">
Latest
</h2>
<ul>
{latestArticles.map(article => (
{latestArticles.map((article) => (
<li key={article.id} className="mb-4">
<a
href={article.url}
@ -112,7 +176,9 @@ const NewsColumn = () => {
rel="noopener noreferrer"
className="hover:underline block"
>
<p className="text-sm font-semibold text-text dark:text-text">{article.headline}</p>
<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>

View file

@ -52,8 +52,15 @@ const RecommendationTrendsWidget = ({ symbol }: RecommendationTrendsWidgetProps)
}
}, [symbol]);
const SkeletonLoader = () => (
<div className="p-6 bg-surface0 dark:bg-surface0 rounded-lg shadow-md mt-4 animate-pulse">
<div className="h-6 bg-gray-300 dark:bg-gray-700 rounded w-1/3 mb-4"></div>
<div className="h-80 bg-gray-300 dark:bg-gray-700 rounded"></div>
</div>
);
if (loading) {
return <p>Loading...</p>;
return <SkeletonLoader />;
}
if (error) {

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import PriceGraph from './PriceGraph';
import StockPriceGraph from './StockPriceGraph';
@ -20,9 +20,11 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
const [stockData, setStockData] = useState<StockData | null>(null);
const [stockDescription, setStockDescription] = useState<string>('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const fetchStockPrice = async (selectedSymbol: string) => {
setError('');
setLoading(true);
try {
const res = await fetch(`/api/quote?symbol=${selectedSymbol}`);
const data = await res.json();
@ -35,6 +37,8 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
} catch (err) {
setError('Failed to fetch stock price');
setStockData(null);
} finally {
setLoading(false);
}
};
@ -71,6 +75,10 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
</span>
);
const Skeleton = ({ width, height }: { width: string, height: string }) => (
<div className={`bg-gray-300 dark:bg-gray-700 rounded-md`} style={{ width, height }}></div>
);
return (
<div className="relative bg-surface0 dark:bg-surface0 p-6 rounded-t-lg shadow-md">
<div className="absolute inset-0 opacity-20">
@ -79,16 +87,36 @@ const StockPrice = ({ symbol }: StockPriceProps) => {
{symbol && (
<div className="relative z-10">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-text dark:text-text">{symbol}</h1>
{stockData && (
{loading ? (
<Skeleton width="150px" height="2rem" />
) : (
<h1 className="text-3xl font-bold text-text dark:text-text">{symbol}</h1>
)}
{loading ? (
<div className="flex space-x-2 my-4">
<Skeleton width="100px" height="2rem" />
<Skeleton width="100px" height="2rem" />
</div>
) : 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>
)}
) : null}
</div>
{stockDescription && <p className="text-lg mb-4 text-text dark:text-subtext1">{stockDescription}</p>}
{stockData && (
{loading ? (
<Skeleton width="100%" height="1.5rem" className="my-4" />
) : stockDescription && (
<p className="text-lg mb-4 text-text dark:text-subtext1">{stockDescription}</p>
)}
{loading ? (
<div className="grid grid-cols-4 gap-4 mt-4">
<Skeleton width="100%" height="100px" />
<Skeleton width="100%" height="100px" />
<Skeleton width="100%" height="100px" />
<Skeleton width="100%" height="100px" />
</div>
) : stockData && (
<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">

View file

@ -15,6 +15,7 @@ const Ticker = ({ symbol }: { symbol: string }) => {
const [bid, setBid] = useState<number | null>(null);
const [ask, setAsk] = useState<number | null>(null);
const [webSocketInitialized, setWebSocketInitialized] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const initializePrices = async () => {
@ -47,27 +48,38 @@ const Ticker = ({ symbol }: { symbol: string }) => {
useEffect(() => {
if (!webSocketInitialized) return;
setLoading(true); // Set loading to true when the symbol changes
const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`);
// Define a function to handle incoming WebSocket messages
const handleWebSocketMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.type === 'trade' && data.data.some((trade: Trade) => trade.s === symbol)) {
const tradeForSymbol = data.data.find((trade: Trade) => trade.s === symbol);
setLatestTrade(tradeForSymbol);
setLoading(false);
}
};
// Subscribe to the symbol when the socket opens
socket.onopen = () => {
console.log('WebSocket connection established');
socket.send(JSON.stringify({ type: 'subscribe', symbol }));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'trade' && data.data.some((trade: Trade) => trade.s === symbol)) {
const tradeForSymbol = data.data.find((trade: Trade) => trade.s === symbol);
setLatestTrade(tradeForSymbol);
}
};
// Handle incoming messages
socket.onmessage = handleWebSocketMessage;
socket.onclose = () => {
console.log('WebSocket connection closed');
};
// Clean up by unsubscribing from the symbol and closing the socket on component unmount or symbol change
return () => {
socket.send(JSON.stringify({ type: 'unsubscribe', symbol }));
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'unsubscribe', symbol }));
}
socket.close();
};
}, [symbol, webSocketInitialized]);
@ -90,15 +102,28 @@ const Ticker = ({ symbol }: { symbol: string }) => {
return 'No conditions';
};
const SkeletonPlaceholder = () => (
<div className="flex flex-row space-x-4 ed-lg text-xs">
<strong>LIVE: </strong>
<div>Waiting for a trade to happen...</div>
</div>
);
return (
<div className="">
{symbol && latestTrade && (
{symbol && (
<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">
<strong>LIVE ${latestTrade.s}</strong>
<div>Traded {latestTrade.v} @ ${latestTrade.p} on {new Date(latestTrade.t).toLocaleTimeString()}</div>
<div>{getTradeConditions(latestTrade)}</div>
</div>
{loading ? (
<SkeletonPlaceholder />
) : (
latestTrade && (
<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>{getTradeConditions(latestTrade)}</div>
</div>
)
)}
</div>
)}
</div>

View file

@ -1,4 +1,5 @@
import { useState } from 'react';
import HeadTemplate from '@/components/HeadTemplate';
import NavigationBar from '../components/NavigationBar';
import StockPrice from '../components/StockPrice';
import Ticker from '../components/Ticker';
@ -9,6 +10,8 @@ import RecommendationTrendsWidget from '@/components/RecommendationTrends';
import FinancialsWidget from '@/components/FinancialsWidget';
import CompanyNews from './api/company-news';
import CompanyNewsWidget from '@/components/CompanyNewsWidget';
import HeroSection from '@/components/HeroSection';
import Footer from '@/components/Footer';
export default function Home() {
const [symbol, setSymbol] = useState('');
@ -18,25 +21,31 @@ export default function Home() {
};
return (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col min-h-screen items-center w-full">
<HeadTemplate />
<NavigationBar onSelectSymbol={handleSelectSymbol} />
<div className="gap-8 flex-wrap lg:flex-nowrap flex-row flex max-w-7xl w-full p-4">
<main className="gap-8 flex-wrap lg:flex-nowrap flex-row flex max-w-7xl w-full p-4 flex-1">
{symbol ? (
<>
<div className="flex flex-col w-full">
<StockPrice symbol={symbol} />
<Ticker symbol={symbol} />
<RecommendationTrendsWidget symbol={symbol} />
<FinancialsWidget symbol={symbol} />
<RecommendationTrendsWidget symbol={symbol} />
<FinancialsWidget symbol={symbol} />
</div>
<div className="flex flex-col max-w-md">
<div className="flex flex-col w-full md:max-w-md gap-2">
<CompanyProfileCard ticker={symbol} />
<CompanyNewsWidget symbol={symbol} />
</div>
</>
) : (<NewsColumn />)}
{/* */}
</div>
) : (
<div className="w-full flex flex-col items-center">
<HeroSection />
<NewsColumn />
</div>
)}
</main>
<Footer />
</div>
);
}