'use client' import { useEffect, useRef } from 'react' import type { TrumpPost, Candle } from '@/types' import { useDashboardStore } from '@/store/dashboard' import Pill from '@/components/ui/Pill' import PostCards, { MOCK_POSTS } from './PostCards' import ExpandDetail from './ExpandDetail' interface ChartPanelProps { posts?: TrumpPost[] candles?: Candle[] } export default function ChartPanel({ posts = MOCK_POSTS, candles = [] }: ChartPanelProps) { const { asset, timeframe, setAsset, setTimeframe, selectedPostId, setSelectedPost } = useDashboardStore() const containerRef = useRef(null) const chartRef = useRef(null) const seriesRef = useRef(null) const fittedRef = useRef(false) // Keep latest posts/selectedPostId accessible inside chart callbacks without re-subscribing const postsRef = useRef(posts) postsRef.current = posts const selectedPostIdRef = useRef(selectedPostId) selectedPostIdRef.current = selectedPostId const assets: Array<'BTC' | 'ETH'> = ['BTC', 'ETH'] const timeframes: Array<'5m' | '15m' | '1H' | '4H' | '1D' | '1W'> = ['5m', '15m', '1H', '4H', '1D', '1W'] const selectedPost = posts.find((p) => p.id === selectedPostId) ?? null // Create chart once on mount useEffect(() => { if (!containerRef.current || typeof window === 'undefined') return let destroyed = false import('lightweight-charts').then(({ createChart, CrosshairMode }) => { if (destroyed || !containerRef.current) return const chart = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: 360, layout: { background: { color: '#050505' }, textColor: '#555555', }, grid: { vertLines: { color: '#111111' }, horzLines: { color: '#111111' }, }, crosshair: { mode: CrosshairMode.Normal }, rightPriceScale: { borderColor: '#1a1a1a' }, timeScale: { borderColor: '#1a1a1a', timeVisible: true, rightOffset: 5, barSpacing: 10, }, handleScroll: true, handleScale: true, }) // @ts-expect-error lightweight-charts type chartRef.current = chart const series = chart.addCandlestickSeries({ upColor: '#4ade80', downColor: '#ef4444', borderUpColor: '#4ade80', borderDownColor: '#ef4444', wickUpColor: '#4ade80', wickDownColor: '#ef4444', }) // @ts-expect-error lightweight-charts type seriesRef.current = series // Click on chart: find nearest post marker within 2-bar tolerance chart.subscribeClick((param: { time?: number | string }) => { if (!param.time) return const clickTime = typeof param.time === 'number' ? param.time : 0 if (!clickTime) return const allPosts = postsRef.current let closest: TrumpPost | null = null let closestDiff = Infinity for (const p of allPosts) { if (!p.published_at) continue const pt = Math.floor(new Date(p.published_at).getTime() / 1000) const diff = Math.abs(pt - clickTime) if (diff < closestDiff) { closestDiff = diff closest = p } } // Only select if click is within 2 hours of a post (7200 seconds) if (closest && closestDiff <= 7200) { const newId = closest.id === selectedPostIdRef.current ? null : closest.id setSelectedPost(newId) } else { setSelectedPost(null) } }) const ro = new ResizeObserver(() => { if (containerRef.current && !destroyed) { chart.applyOptions({ width: containerRef.current.clientWidth }) } }) ro.observe(containerRef.current) return () => { ro.disconnect() } }) return () => { destroyed = true fittedRef.current = false if (chartRef.current) { // @ts-expect-error lightweight-charts type chartRef.current.remove() chartRef.current = null seriesRef.current = null } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Update candles + markers whenever data changes useEffect(() => { const series = seriesRef.current const chart = chartRef.current if (!series || !chart || candles.length === 0) return const sorted = [...candles].sort((a, b) => a.time - b.time) // @ts-expect-error lightweight-charts type series.setData(sorted.map((c) => ({ time: c.time as number, open: c.open, high: c.high, low: c.low, close: c.close, }))) // Show markers for all posts within visible time range const minTime = sorted[0].time const maxTime = sorted[sorted.length - 1].time const visible = posts.filter((p) => { if (!p.published_at) return false const t = Math.floor(new Date(p.published_at).getTime() / 1000) return t >= minTime && t <= maxTime }) if (visible.length > 0) { const markers = [...visible] .sort((a, b) => new Date(a.published_at).getTime() - new Date(b.published_at).getTime()) .map((p) => ({ time: Math.floor(new Date(p.published_at).getTime() / 1000) as number, position: 'aboveBar' as const, color: p.id === selectedPostId ? '#fb923c' : p.sentiment === 'bearish' ? '#ef4444' : '#f97316', shape: 'circle' as const, text: '', size: p.id === selectedPostId ? 2 : 1, })) // @ts-expect-error lightweight-charts type series.setMarkers(markers) } if (!fittedRef.current) { // @ts-expect-error lightweight-charts type chart.timeScale().fitContent() fittedRef.current = true } }, [candles, posts, selectedPostId]) // Reset fit flag on timeframe/asset switch useEffect(() => { fittedRef.current = false }, [asset, timeframe]) return (
{/* Controls */}
{assets.map((a) => ( setAsset(a)}> {a} ))}
{timeframes.map((tf) => ( setTimeframe(tf as '4H' | '1D' | '1W')}> {tf} ))}
{/* Chart */}
{/* Selected post detail — shown immediately below chart */} {/* Post cards list */}
) }