SoftTraderFrontend/app/symbol-details/page.tsx
2026-03-21 09:26:43 -04:00

214 lines
8.8 KiB
TypeScript

'use client'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { ArrowLeft, ChevronDown, RefreshCw, BellPlus } from 'lucide-react'
import { Suspense, useState, useEffect } from 'react'
import { SentimentalityBar } from '@/components/widgets/SentimentalityBar'
import { getLiveQuote, formatMarketValue, type MappedQuote } from '@/lib/stockApi'
import { StockChart } from '@/components/stock-chart/stock-chart'
import { NewsHeadlines } from '@/components/widgets/NewsHeadlines'
import { AlertsModal } from '@/components/widgets/AlertsModal'
import type { TimeRange } from '@/components/stock-chart/types'
function SymbolDetailsContent() {
const searchParams = useSearchParams()
const symbol = (searchParams.get('symbol') || 'AAPL').toUpperCase()
const [chartExpanded, setChartExpanded] = useState(true)
const [headlinesExpanded, setHeadlinesExpanded] = useState(true)
const [selectedRange, setSelectedRange] = useState<TimeRange>('1D')
const [quote, setQuote] = useState<MappedQuote | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refreshTick, setRefreshTick] = useState(0)
const [avgSentiment, setAvgSentiment] = useState<number | null>(null)
const [alertsModalOpen, setAlertsModalOpen] = useState(false)
useEffect(() => {
setLoading(true)
setError(null)
getLiveQuote(symbol)
.then(setQuote)
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
}, [symbol, refreshTick])
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="bg-background border-b border-border px-6 py-4 flex items-center gap-4">
<Link href="/interests" className="p-2 hover:bg-secondary transition-colors">
<ArrowLeft size={20} />
</Link>
<h1 className="text-2xl font-bold">{symbol}</h1>
</div>
{/* Loading / Error states */}
{loading && (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
Loading {symbol}...
</div>
)}
{!loading && error && (
<div className="flex-1 flex items-center justify-center text-negative">
{error}
</div>
)}
{!loading && !error && quote && (
<div className="flex-1 overflow-y-auto">
<div className="p-6 space-y-6 max-w-5xl">
{/* Stock Summary Card */}
<div className="border border-border bg-card p-6 space-y-6">
{/* Card header row with refresh button */}
<div className="flex items-start justify-between pb-4 border-b border-border space-y-3">
<div className="space-y-3 flex-1">
<div className="text-lg font-semibold">{quote.symbol}</div>
<div className="flex items-baseline gap-4">
<div className="text-4xl font-bold">${quote.lastPrice.toFixed(2)}</div>
<div className={
quote.priceChange > 0 ? 'text-positive'
: quote.priceChange < 0 ? 'text-negative'
: 'text-foreground'
}>
<div className="font-semibold">
{quote.priceChange > 0 ? '+' : ''}${quote.priceChange.toFixed(2)}
</div>
<div className="text-sm">
({quote.priceChange > 0 ? '+' : ''}{quote.percentChange.toFixed(2)}%)
</div>
</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setAlertsModalOpen(true)}
className="p-2 hover:bg-secondary transition-colors text-muted-foreground"
title="Configure alerts"
>
<BellPlus size={16} />
</button>
<button
onClick={() => setRefreshTick((t) => t + 1)}
disabled={loading}
className="p-2 hover:bg-secondary transition-colors disabled:opacity-40 text-muted-foreground"
title="Refresh quote"
>
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
</button>
</div>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-2 gap-4 pb-4 border-b border-border">
<div className="space-y-2">
<div className="text-xs text-muted-foreground">Open</div>
<div className="font-semibold">
{formatMarketValue(quote.openPrice, quote.marketOpen, (v) => `$${v.toFixed(2)}`)}
</div>
</div>
<div className="space-y-2">
<div className="text-xs text-muted-foreground">Prev Close</div>
<div className="font-semibold">${quote.previousClose.toFixed(2)}</div>
</div>
<div className="space-y-2 col-span-2">
<div className="text-xs text-muted-foreground">Volume</div>
<div className="font-semibold">
{formatMarketValue(quote.volume, quote.marketOpen, (v) => v.toLocaleString())}
</div>
</div>
</div>
{/* Sentimentality Bar - average of headline sentiments */}
<SentimentalityBar sentiment={avgSentiment ?? 50} />
</div>
{/* Chart Section - Collapsible */}
<div className="border border-border">
<button
onClick={() => setChartExpanded(!chartExpanded)}
className="w-full flex items-center justify-between bg-card p-4 hover:bg-secondary transition-colors"
>
<span className="font-semibold">Chart</span>
<ChevronDown size={20} className={`transition-transform ${chartExpanded ? 'rotate-180' : ''}`} />
</button>
{chartExpanded && (
<div className="bg-card border-t border-border">
{/* Range selector */}
<div className="flex border-b border-border">
{(['1D', '5D', '1M', '3M', '6M', 'YTD', '1Y', '3Y'] as TimeRange[]).map((r) => (
<button
key={r}
onClick={() => setSelectedRange(r)}
className={`flex-1 py-2 text-xs font-medium transition-colors
${selectedRange === r
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-secondary'
}`}
>
{r}
</button>
))}
</div>
{/* Chart will be embedded here */}
<div className="p-4">
<StockChart symbol={symbol} range={selectedRange} />
</div>
<div className="px-4 pb-4">
<Link
href={`/complex-chart?symbol=${symbol}`}
className="block w-full text-center bg-primary text-primary-foreground p-3 hover:bg-blue-700 transition-colors font-medium"
>
Show more
</Link>
</div>
</div>
)}
</div>
{/* Headlines Section - Collapsible */}
<div className="border border-border">
<button
onClick={() => setHeadlinesExpanded(!headlinesExpanded)}
className="w-full flex items-center justify-between bg-card p-4 hover:bg-secondary transition-colors"
>
<span className="font-semibold">Headlines</span>
<ChevronDown size={20} className={`transition-transform ${headlinesExpanded ? 'rotate-180' : ''}`} />
</button>
{headlinesExpanded && (
<div className="bg-card border-t border-border">
<div className="p-4">
<NewsHeadlines keyword={symbol} onAverageSentiment={setAvgSentiment} />
</div>
</div>
)}
</div>
</div>
</div>
)}
<AlertsModal
open={alertsModalOpen}
onOpenChange={setAlertsModalOpen}
symbol={symbol}
/>
</div>
)
}
export default function SymbolDetails() {
return (
<Suspense fallback={
<div className="h-full flex items-center justify-center bg-background">
<div className="text-muted-foreground">Loading...</div>
</div>
}>
<SymbolDetailsContent />
</Suspense>
)
}