Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState, useMemo } from 'react'; | |
| import { X, TrendingUp, TrendingDown, Star } from 'lucide-react'; | |
| import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine } from 'recharts'; | |
| import { getHistoricalData, fetchRealData } from '../data/historicalData'; | |
| import { Loader2 } from 'lucide-react'; | |
| import InvestmentCommittee from './InvestmentCommittee'; | |
| const TIMEFRAME_DAYS = { | |
| '1W': 7, | |
| '1M': 30, | |
| '3M': 90, | |
| '1Y': 365, | |
| 'ALL': 365 | |
| }; | |
| export default function StockPopup({ ticker, assetName, onClose, allHoldings = [], prices = {} }) { | |
| const [data, setData] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const [isDebating, setIsDebating] = useState(false); | |
| const [timeframe, setTimeframe] = useState('1M'); | |
| useEffect(() => { | |
| async function loadData() { | |
| if (ticker) { | |
| setLoading(true); | |
| const realData = await fetchRealData(ticker); | |
| setData(realData); | |
| setLoading(false); | |
| setIsDebating(false); | |
| } | |
| } | |
| loadData(); | |
| }, [ticker]); | |
| // Calculate Top Performers | |
| const topPerformers = useMemo(() => { | |
| if (!allHoldings.length || !prices) return []; | |
| return allHoldings | |
| .map(asset => ({ | |
| ...asset, | |
| perf: prices[asset.ticker]?.percent || 0, | |
| currentPrice: prices[asset.ticker]?.price || 0 | |
| })) | |
| .sort((a, b) => b.perf - a.perf) | |
| .slice(0, 5); | |
| }, [allHoldings, prices]); | |
| const viewData = useMemo(() => { | |
| if (!data) return null; | |
| // Slice history based on timeframe | |
| const daysToShow = TIMEFRAME_DAYS[timeframe] || 30; | |
| const startIndex = Math.max(0, data.history.length - daysToShow); | |
| const slicedHistory = data.history.slice(startIndex); | |
| // Recalculate metrics for the specific timeframe | |
| if (slicedHistory.length === 0) return null; | |
| const currentPrice = slicedHistory[slicedHistory.length - 1].price; | |
| const oldPrice = slicedHistory[0].price; | |
| const change = Number((currentPrice - oldPrice).toFixed(2)); | |
| const percentChange = Number(((change / oldPrice) * 100).toFixed(2)); | |
| const isPositive = change >= 0; | |
| return { | |
| ...data, | |
| history: slicedHistory, | |
| change, | |
| percentChange, | |
| isPositive | |
| }; | |
| }, [data, timeframe]); | |
| if (!ticker) return null; | |
| if (loading || !viewData) { | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"> | |
| <div className="text-center"> | |
| <Loader2 className="animate-spin text-gs-gold mb-4 mx-auto" size={48} /> | |
| <p className="text-white text-lg font-light">Connecting to Yahoo Finance...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const color = viewData.isPositive ? '#00C805' : '#FF5000'; | |
| const bgColor = '#111111'; // Dark theme background | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"> | |
| <div | |
| className="w-full max-w-5xl max-h-[90vh] overflow-y-auto rounded-xl shadow-2xl flex flex-col animate-in zoom-in-95 duration-200" | |
| style={{ backgroundColor: bgColor }} | |
| > | |
| {/* Header */} | |
| <div className="p-6 pb-4 flex justify-between items-start sticky top-0 bg-[#111111] z-10 border-b border-gray-800"> | |
| <div> | |
| <h2 className="text-2xl font-bold text-white flex items-center"> | |
| {assetName} <Star size={18} className="ml-3 text-gray-500 hover:text-yellow-400 cursor-pointer" /> | |
| </h2> | |
| <div className="flex items-end mt-2 space-x-3"> | |
| <span className="text-4xl font-bold text-white">${viewData.currentPrice}</span> | |
| <div className={`flex items-center text-lg font-medium pb-1 ${viewData.isPositive ? 'text-[#00C805]' : 'text-[#FF5000]'}`}> | |
| {viewData.isPositive ? '+' : ''}{viewData.change} ({viewData.isPositive ? '+' : ''}{viewData.percentChange}%) | |
| </div> | |
| </div> | |
| <p className="text-gray-400 text-xs mt-1">At close: {viewData.history[viewData.history.length-1].date}</p> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-2 rounded-full bg-gray-800 text-gray-400 hover:text-white hover:bg-gray-700 transition-colors" | |
| > | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div className="p-8 grid grid-cols-1 lg:grid-cols-12 gap-10"> | |
| {/* Main Chart Area */} | |
| <div className="lg:col-span-8"> | |
| <div className="h-80 w-full mt-4"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <AreaChart data={viewData.history} margin={{ top: 10, right: 0, left: 0, bottom: 0 }}> | |
| <defs> | |
| <linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="5%" stopColor={color} stopOpacity={0.3}/> | |
| <stop offset="95%" stopColor={color} stopOpacity={0}/> | |
| </linearGradient> | |
| </defs> | |
| <XAxis dataKey="date" hide /> | |
| <YAxis domain={['dataMin', 'dataMax']} hide /> | |
| <Tooltip | |
| contentStyle={{ backgroundColor: '#222', border: 'none', borderRadius: '8px', color: '#fff' }} | |
| itemStyle={{ color: '#fff', fontWeight: 'bold' }} | |
| labelStyle={{ color: '#888' }} | |
| /> | |
| <ReferenceLine y={viewData.history[0].price} stroke="#333" strokeDasharray="3 3" /> | |
| <Area | |
| type="monotone" | |
| dataKey="price" | |
| stroke={color} | |
| strokeWidth={2} | |
| fillOpacity={1} | |
| fill="url(#colorPrice)" | |
| /> | |
| </AreaChart> | |
| </ResponsiveContainer> | |
| </div> | |
| {/* Time Controls */} | |
| <div className="flex space-x-6 mt-6 border-b border-gray-800 pb-2"> | |
| {['1W', '1M', '3M', '1Y', 'ALL'].map(t => ( | |
| <button | |
| key={t} | |
| onClick={() => setTimeframe(t)} | |
| className={`text-sm font-bold pb-2 border-b-2 transition-colors ${ | |
| t === timeframe | |
| ? (viewData.isPositive ? 'text-[#00C805] border-[#00C805]' : 'text-[#FF5000] border-[#FF5000]') | |
| : 'text-gray-600 border-transparent hover:text-gray-300' | |
| }`} | |
| > | |
| {t} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="mt-8"> | |
| <InvestmentCommittee | |
| ticker={ticker} | |
| isDebating={isDebating} | |
| setIsDebating={setIsDebating} | |
| /> | |
| </div> | |
| </div> | |
| {/* Right Sidebar: Top Performers */} | |
| <div className="lg:col-span-4 space-y-8"> | |
| <div> | |
| <h3 className="text-white text-xs font-black uppercase tracking-[0.2em] mb-8 flex items-center gap-3"> | |
| <span className="text-lg">π</span> Top Performers | |
| </h3> | |
| <div className="space-y-6"> | |
| {topPerformers.map((asset, idx) => ( | |
| <div key={asset.ticker} className="flex items-center justify-between group"> | |
| <div className="flex items-center gap-4"> | |
| <div className="w-6 flex justify-center"> | |
| {idx === 0 ? <span className="text-lg">π₯</span> : | |
| idx === 1 ? <span className="text-lg">π₯</span> : | |
| idx === 2 ? <span className="text-lg">π₯</span> : | |
| <div className="p-1.5 bg-gs-navy rounded-md text-gs-gold/50"><TrendingUp size={12} /></div>} | |
| </div> | |
| <div | |
| className="w-12 h-12 rounded-xl flex items-center justify-center text-[10px] font-black text-gs-navy" | |
| style={{ backgroundColor: asset.color }} | |
| > | |
| {asset.ticker} | |
| </div> | |
| <div> | |
| <p className="text-sm font-bold text-white tracking-tight">{asset.ticker}</p> | |
| <p className="text-[10px] text-gray-500 font-medium tracking-wide">${asset.currentPrice.toLocaleString()}</p> | |
| </div> | |
| </div> | |
| <div className="text-right"> | |
| <span className={`text-sm font-black ${asset.perf >= 0 ? 'text-[#00C805]' : 'text-[#FF5000]'}`}> | |
| {asset.perf >= 0 ? '+' : ''}{asset.perf.toFixed(2)}% | |
| </span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="pt-8 border-t border-gray-800"> | |
| <div className="bg-gs-navy/30 rounded-2xl p-5 border border-white/5"> | |
| <h4 className="text-[10px] font-bold text-gs-gold uppercase tracking-widest mb-3">Portfolio Insight</h4> | |
| <p className="text-xs text-gray-400 leading-relaxed"> | |
| {assetName} currently ranks #{topPerformers.findIndex(a => a.ticker === ticker) + 1} among your top-tier performers. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |