84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { getLiveQuote, formatMarketValue, type MappedQuote } from '@/lib/stockApi'
|
|
|
|
interface StockCardProps {
|
|
symbol?: string
|
|
}
|
|
|
|
export function StockCard({ symbol = 'AAPL' }: StockCardProps) {
|
|
const [quote, setQuote] = useState<MappedQuote | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
setLoading(true)
|
|
setError(null)
|
|
getLiveQuote(symbol)
|
|
.then(setQuote)
|
|
.catch((err) => setError(err.message))
|
|
.finally(() => setLoading(false))
|
|
}, [symbol])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="border border-border bg-card p-6 space-y-4">
|
|
<div className="text-muted-foreground text-sm">Loading {symbol}...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !quote) {
|
|
return (
|
|
<div className="border border-border bg-card p-6 space-y-4">
|
|
<div className="font-bold text-foreground">{symbol}</div>
|
|
<div className="text-negative text-sm">{error ?? 'No data available'}</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const changeColor =
|
|
quote.priceChange > 0
|
|
? 'text-positive'
|
|
: quote.priceChange < 0
|
|
? 'text-negative'
|
|
: 'text-foreground'
|
|
|
|
const changeSign = quote.priceChange > 0 ? '+' : ''
|
|
|
|
return (
|
|
<div className="border border-border bg-card p-6 space-y-4">
|
|
<div className="text-2xl font-bold text-foreground">{quote.symbol}</div>
|
|
|
|
<div>
|
|
<div className="text-3xl font-bold text-foreground">
|
|
${quote.lastPrice.toFixed(2)}
|
|
</div>
|
|
<div className={`text-lg font-semibold ${changeColor}`}>
|
|
{changeSign}${quote.priceChange.toFixed(2)} ({changeSign}{quote.percentChange.toFixed(2)}%)
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Open</span>
|
|
<span className="font-medium">
|
|
{formatMarketValue(quote.openPrice, quote.marketOpen, (v) => `$${v.toFixed(2)}`)}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Prev Close</span>
|
|
<span className="font-medium">${quote.previousClose.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">Volume</span>
|
|
<span className="font-medium">
|
|
{formatMarketValue(quote.volume, quote.marketOpen, (v) => v.toLocaleString())}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|