import { useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import type { GeoEvent } from '../../types'; interface Props { currentTime: number; startTime: number; endTime: number; events: GeoEvent[]; onSeek: (time: number) => void; onEventFlyTo?: (event: GeoEvent) => void; } const KST_OFFSET = 9 * 3600_000; const TYPE_COLORS: Record = { airstrike: '#ef4444', explosion: '#f97316', missile_launch: '#eab308', intercept: '#3b82f6', alert: '#a855f7', impact: '#ff0000', osint: '#06b6d4', }; const TYPE_I18N_KEYS: Record = { airstrike: 'event.airstrike', explosion: 'event.explosion', missile_launch: 'event.missileLaunch', intercept: 'event.intercept', alert: 'event.alert', impact: 'event.impact', osint: 'event.osint', }; const SOURCE_I18N_KEYS: Record = { US: 'source.US', IL: 'source.IL', IR: 'source.IR', proxy: 'source.proxy', }; export function TimelineSlider({ currentTime, startTime, endTime, events, onSeek, onEventFlyTo }: Props) { const { t } = useTranslation(); const [selectedId, setSelectedId] = useState(null); const progress = ((currentTime - startTime) / (endTime - startTime)) * 100; const eventMarkers = useMemo(() => { return events.map(e => ({ id: e.id, position: ((e.timestamp - startTime) / (endTime - startTime)) * 100, type: e.type, label: e.label, })); }, [events, startTime, endTime]); const formatTime = (ts: number) => { const d = new Date(ts + KST_OFFSET); return d.toISOString().slice(0, 16).replace('T', ' ') + ' KST'; }; const formatTimeShort = (ts: number) => { const d = new Date(ts + KST_OFFSET); return `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`; }; const handleTrackClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const pct = (e.clientX - rect.left) / rect.width; onSeek(startTime + pct * (endTime - startTime)); }; // When a marker is clicked: select it + seek to its time const handleMarkerClick = useCallback((e: React.MouseEvent, ev: GeoEvent) => { e.stopPropagation(); // don't trigger track seek setSelectedId(prev => prev === ev.id ? null : ev.id); onSeek(ev.timestamp); }, [onSeek]); // Find events near the selected event (within 30 min) const selectedCluster = useMemo(() => { if (!selectedId) return []; const sel = events.find(e => e.id === selectedId); if (!sel) return []; const WINDOW = 30 * 60_000; // 30 min return events .filter(e => Math.abs(e.timestamp - sel.timestamp) <= WINDOW) .sort((a, b) => a.timestamp - b.timestamp); }, [selectedId, events]); const handleEventCardClick = useCallback((ev: GeoEvent) => { onSeek(ev.timestamp); onEventFlyTo?.(ev); }, [onSeek, onEventFlyTo]); return (
{formatTime(startTime)} {formatTime(currentTime)} {formatTime(endTime)}
{eventMarkers.map(m => { const ev = events.find(e => e.id === m.id)!; const isSelected = selectedId === m.id; const isInCluster = selectedCluster.some(c => c.id === m.id); return (
handleMarkerClick(e, ev)} /> ); })}
{/* Event detail strip — shown when a marker is selected */} {selectedCluster.length > 0 && (
{selectedCluster.map(ev => { const color = TYPE_COLORS[ev.type] || '#888'; const isPast = ev.timestamp <= currentTime; const isActive = ev.id === selectedId; const sourceKey = ev.source ? SOURCE_I18N_KEYS[ev.source] : ''; const source = sourceKey ? t(sourceKey) : ''; const typeKey = TYPE_I18N_KEYS[ev.type]; const typeLabel = typeKey ? t(typeKey) : ev.type; return ( ); })}
)}
); }