Files
Shadowbroker/frontend/src/components/NewsFeed.tsx
T
anoracleofra-code 362a6e2ceb Initial commit: ShadowBroker v0.1
Former-commit-id: 8ed321f2ba
2026-03-04 22:44:08 -07:00

1024 lines
67 KiB
TypeScript

"use client";
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, Clock, ChevronDown, ChevronUp } from 'lucide-react';
import React, { useEffect, useRef } from 'react';
import WikiImage from '@/components/WikiImage';
// Format time from pubish string "Tue, 24 Feb 2026 15:30:00 GMT" to "15:30"
function formatTime(pubDate: string) {
try {
const d = new Date(pubDate);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return "00:00";
}
}
// ICAO type designator → Wikipedia article title
const AIRCRAFT_WIKI: Record<string, string> = {
// Boeing widebodies
B741: 'Boeing 747', B742: 'Boeing 747', B743: 'Boeing 747', B744: 'Boeing 747-400', B748: 'Boeing 747-8',
B752: 'Boeing 757', B753: 'Boeing 757', B762: 'Boeing 767', B763: 'Boeing 767', B764: 'Boeing 767',
B772: 'Boeing 777', B773: 'Boeing 777', B77L: 'Boeing 777', B77W: 'Boeing 777', B778: 'Boeing 777X',
B788: 'Boeing 787 Dreamliner', B789: 'Boeing 787 Dreamliner', B78X: 'Boeing 787 Dreamliner',
// Boeing narrowbodies
B712: 'Boeing 717', B731: 'Boeing 737', B732: 'Boeing 737', B733: 'Boeing 737', B734: 'Boeing 737',
B735: 'Boeing 737', B736: 'Boeing 737', B737: 'Boeing 737', B738: 'Boeing 737 Next Generation',
B739: 'Boeing 737 Next Generation', B37M: 'Boeing 737 MAX', B38M: 'Boeing 737 MAX', B39M: 'Boeing 737 MAX',
// Airbus widebodies
A306: 'Airbus A300', A310: 'Airbus A310', A332: 'Airbus A330', A333: 'Airbus A330', A338: 'Airbus A330neo',
A339: 'Airbus A330neo', A342: 'Airbus A340', A343: 'Airbus A340', A345: 'Airbus A340', A346: 'Airbus A340',
A359: 'Airbus A350', A35K: 'Airbus A350', A388: 'Airbus A380',
// Airbus narrowbodies
A318: 'Airbus A318', A319: 'Airbus A319', A320: 'Airbus A320', A321: 'Airbus A321',
A19N: 'Airbus A319neo', A20N: 'Airbus A320neo family', A21N: 'Airbus A321neo',
// Embraer
E135: 'Embraer ERJ 145 family', E145: 'Embraer ERJ 145 family', E170: 'Embraer E-Jet family',
E175: 'Embraer E-Jet family', E190: 'Embraer E-Jet family', E195: 'Embraer E-Jet family',
E290: 'Embraer E-Jet E2 family', E295: 'Embraer E-Jet E2 family',
// Bombardier / CRJ
CRJ1: 'Bombardier CRJ100/200', CRJ2: 'Bombardier CRJ100/200', CRJ7: 'Bombardier CRJ700 series',
CRJ9: 'Bombardier CRJ700 series', CRJX: 'Bombardier CRJ700 series',
// Turboprops
DH8A: 'De Havilland Canada Dash 8', DH8B: 'De Havilland Canada Dash 8',
DH8C: 'De Havilland Canada Dash 8', DH8D: 'De Havilland Canada Dash 8',
AT45: 'ATR 42', AT46: 'ATR 42', AT72: 'ATR 72', AT76: 'ATR 72',
// Bizjets
C56X: 'Cessna Citation Excel', C680: 'Cessna Citation Sovereign', C750: 'Cessna Citation X',
CL30: 'Bombardier Challenger 300', CL35: 'Bombardier Challenger 350',
CL60: 'Bombardier Challenger 600 series', GL5T: 'Bombardier Global 5000',
GLEX: 'Bombardier Global Express', GLF4: 'Gulfstream IV', GLF5: 'Gulfstream V',
GLF6: 'Gulfstream G650', G280: 'Gulfstream G280', GA5C: 'Gulfstream G500/G600',
GA6C: 'Gulfstream G500/G600', LJ35: 'Learjet 35', LJ45: 'Learjet 45', LJ60: 'Learjet 60',
F900: 'Dassault Falcon 900', FA7X: 'Dassault Falcon 7X', FA8X: 'Dassault Falcon 8X',
// Military common
C130: 'Lockheed C-130 Hercules', C17: 'Boeing C-17 Globemaster III',
KC35: 'Boeing KC-135 Stratotanker', KC46: 'Boeing KC-46 Pegasus', K35R: 'Boeing KC-135 Stratotanker',
E3CF: 'Boeing E-3 Sentry', E6B: 'Boeing E-6 Mercury', P8: 'Boeing P-8 Poseidon',
B52H: 'Boeing B-52 Stratofortress', F16: 'General Dynamics F-16 Fighting Falcon',
F15: 'McDonnell Douglas F-15 Eagle', F18H: 'Boeing F/A-18E/F Super Hornet',
F35: 'Lockheed Martin F-35 Lightning II', F22: 'Lockheed Martin F-22 Raptor',
A10: 'Fairchild Republic A-10 Thunderbolt II', V22: 'Bell Boeing V-22 Osprey',
C5M: 'Lockheed C-5 Galaxy', C2: 'Grumman C-2 Greyhound',
EUFI: 'Eurofighter Typhoon', RFAL: 'Dassault Rafale', TORN: 'Panavia Tornado',
// GA
C172: 'Cessna 172', C182: 'Cessna 182 Skylane', C206: 'Cessna 206', C208: 'Cessna 208 Caravan',
C210: 'Cessna 210 Centurion', PA28: 'Piper PA-28 Cherokee', PA32: 'Piper PA-32',
PA46: 'Piper PA-46 Malibu', BE36: 'Beechcraft Bonanza', BE9L: 'Beechcraft King Air',
BE20: 'Beechcraft Super King Air', B350: 'Beechcraft King Air 350', PC12: 'Pilatus PC-12',
PC24: 'Pilatus PC-24', TBM7: 'Daher TBM', TBM8: 'Daher TBM', TBM9: 'Daher TBM',
// Helicopters
R44: 'Robinson R44', R22: 'Robinson R22', R66: 'Robinson R66',
B06: 'Bell 206', B407: 'Bell 407', B412: 'Bell 412',
EC35: 'Airbus Helicopters H135', EC45: 'Airbus Helicopters H145',
S76: 'Sikorsky S-76', S92: 'Sikorsky S-92',
// Russian / other
SU95: 'Sukhoi Superjet 100', AN12: 'Antonov An-12', AN26: 'Antonov An-26',
IL76: 'Ilyushin Il-76', IL96: 'Ilyushin Il-96',
A400: 'Airbus A400M Atlas', C295: 'Airbus C-295',
};
// Module-level cache for Wikipedia thumbnails (persists across re-renders)
const _wikiThumbCache: Record<string, { url: string | null; loading: boolean }> = {};
function useAircraftImage(model: string | undefined): { imgUrl: string | null; wikiUrl: string | null; loading: boolean } {
const [, forceUpdate] = useState(0);
const wikiTitle = model ? AIRCRAFT_WIKI[model] : undefined;
const wikiUrl = wikiTitle ? `https://en.wikipedia.org/wiki/${wikiTitle.replace(/ /g, '_')}` : null;
useEffect(() => {
if (!wikiTitle) return;
const key = wikiTitle;
if (_wikiThumbCache[key]) return; // Already fetched or in-flight
_wikiThumbCache[key] = { url: null, loading: true };
fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(wikiTitle)}`)
.then(r => r.json())
.then(d => {
_wikiThumbCache[key] = { url: d.thumbnail?.source || null, loading: false };
forceUpdate(n => n + 1);
})
.catch(() => {
_wikiThumbCache[key] = { url: null, loading: false };
forceUpdate(n => n + 1);
});
}, [wikiTitle]);
if (!wikiTitle) return { imgUrl: null, wikiUrl: null, loading: false };
const cached = _wikiThumbCache[wikiTitle];
return { imgUrl: cached?.url || null, wikiUrl, loading: cached?.loading || false };
}
// Vessel type → Wikipedia article for generic ships (carriers have their own wiki field)
const VESSEL_TYPE_WIKI: Record<string, string> = {
'tanker': 'https://en.wikipedia.org/wiki/Oil_tanker',
'cargo': 'https://en.wikipedia.org/wiki/Container_ship',
'passenger': 'https://en.wikipedia.org/wiki/Cruise_ship',
'yacht': 'https://en.wikipedia.org/wiki/Superyacht',
'military_vessel': 'https://en.wikipedia.org/wiki/Warship',
};
function NewsFeedInner({ data, selectedEntity, regionDossier, regionDossierLoading }: { data: any, selectedEntity?: { type: string, id: string | number, name?: string, callsign?: string, media_url?: string, extra?: any } | null, regionDossier?: any, regionDossierLoading?: boolean }) {
const [isMinimized, setIsMinimized] = useState(false);
const [expandedIndexes, setExpandedIndexes] = useState<number[]>([]);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
// Intentionally omitting map click triggers for expanding
// as we now show a contextual pop-up on the map directly.
const toggleExpand = (idx: number) => {
if (expandedIndexes.includes(idx)) {
setExpandedIndexes(expandedIndexes.filter(i => i !== idx));
} else {
setExpandedIndexes([...expandedIndexes, idx]);
}
}
const news = data?.news || [];
// Determine the selected flight's model for Wikipedia thumbnail lookup
// (must call hook unconditionally — React rules of hooks)
const selectedFlightModel = (() => {
if (!selectedEntity) return undefined;
const { type, id } = selectedEntity;
let flight: any = null;
if (type === 'flight') flight = data?.commercial_flights?.[id as number];
else if (type === 'private_flight') flight = data?.private_flights?.[id as number];
else if (type === 'private_jet') flight = data?.private_jets?.[id as number];
else if (type === 'military_flight') flight = data?.military_flights?.[id as number];
else if (type === 'tracked_flight') flight = data?.tracked_flights?.[id as number];
return flight?.model;
})();
const { imgUrl: aircraftImgUrl, wikiUrl: aircraftWikiUrl, loading: aircraftImgLoading } = useAircraftImage(selectedFlightModel);
// Region Dossier (right-click intelligence)
if (selectedEntity?.type === 'region_dossier') {
const d = regionDossier;
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-emerald-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,255,128,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-emerald-500/30 bg-emerald-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-emerald-400">REGION DOSSIER</h2>
<span className="text-[8px] text-gray-500">
{selectedEntity.extra ? `${selectedEntity.extra.lat.toFixed(3)}, ${selectedEntity.extra.lng.toFixed(3)}` : ''}
</span>
</div>
{regionDossierLoading ? (
<div className="p-6 flex items-center justify-center">
<span className="text-emerald-400 text-[10px] font-mono animate-pulse tracking-widest">COMPILING INTELLIGENCE...</span>
</div>
) : d && !d.error ? (
<div className="p-3 flex flex-col gap-1.5 max-h-[500px] overflow-y-auto styled-scrollbar text-[10px]">
{/* COUNTRY */}
<div className="text-[9px] text-emerald-500 tracking-widest font-bold border-b border-emerald-900/50 pb-1">COUNTRY LEVEL {d.country?.flag_emoji || ''}</div>
<div className="flex justify-between"><span className="text-gray-500">COUNTRY</span><span className="text-white font-bold">{d.country?.name}</span></div>
{d.country?.official_name && d.country.official_name !== d.country.name && (
<div className="flex justify-between"><span className="text-gray-500">OFFICIAL</span><span className="text-gray-300 text-right max-w-[180px]">{d.country.official_name}</span></div>
)}
<div className="flex justify-between"><span className="text-gray-500">LEADER</span><span className="text-emerald-400 font-bold">{d.country?.leader}</span></div>
<div className="flex justify-between"><span className="text-gray-500">GOVERNMENT</span><span className="text-white font-bold text-right max-w-[180px]">{d.country?.government_type}</span></div>
<div className="flex justify-between"><span className="text-gray-500">POPULATION</span><span className="text-white font-bold">{d.country?.population?.toLocaleString()}</span></div>
<div className="flex justify-between"><span className="text-gray-500">CAPITAL</span><span className="text-white font-bold">{d.country?.capital}</span></div>
<div className="flex justify-between"><span className="text-gray-500">LANGUAGES</span><span className="text-white text-right max-w-[180px]">{d.country?.languages?.join(', ')}</span></div>
{d.country?.currencies?.length > 0 && (
<div className="flex justify-between"><span className="text-gray-500">CURRENCY</span><span className="text-white text-right max-w-[180px]">{d.country.currencies.join(', ')}</span></div>
)}
<div className="flex justify-between"><span className="text-gray-500">REGION</span><span className="text-white">{d.country?.subregion || d.country?.region}</span></div>
{d.country?.area_km2 > 0 && (
<div className="flex justify-between"><span className="text-gray-500">AREA</span><span className="text-white">{d.country.area_km2.toLocaleString()} km²</span></div>
)}
{/* LOCAL */}
{(d.local?.name || d.local?.state) && (
<>
<div className="text-[9px] text-emerald-500 tracking-widest font-bold border-b border-emerald-900/50 pb-1 mt-2">LOCAL LEVEL</div>
{d.local.name && <div className="flex justify-between"><span className="text-gray-500">LOCALITY</span><span className="text-white font-bold">{d.local.name}</span></div>}
{d.local.state && <div className="flex justify-between"><span className="text-gray-500">STATE/PROVINCE</span><span className="text-white font-bold">{d.local.state}</span></div>}
{d.local.description && <div className="flex justify-between"><span className="text-gray-500">TYPE</span><span className="text-gray-300">{d.local.description}</span></div>}
{d.local.summary && (
<div className="mt-1 p-2 bg-black/60 border border-emerald-800/50 rounded text-[9px] text-gray-300 leading-relaxed">
<span className="text-emerald-400 font-bold">&gt;_ INTEL: </span>
{d.local.summary.length > 500 ? d.local.summary.substring(0, 500) + '...' : d.local.summary}
</div>
)}
</>
)}
</div>
) : d?.error ? (
<div className="p-4 text-gray-400 text-[10px]">{d.error}</div>
) : (
<div className="p-4 text-red-400 text-[10px]">INTEL UNAVAILABLE</div>
)}
</motion.div>
);
}
if (selectedEntity?.type === 'tracked_flight') {
const flight = data?.tracked_flights?.[selectedEntity.id as number];
if (flight) {
const callsign = flight.callsign || "UNKNOWN";
const alertColorMap: Record<string, string> = {
'pink': 'text-pink-400', 'red': 'text-red-400',
'darkblue': 'text-blue-400', 'white': 'text-white'
};
const alertBorderMap: Record<string, string> = {
'pink': 'border-pink-500/30', 'red': 'border-red-500/30',
'darkblue': 'border-blue-500/30', 'white': 'border-gray-500/30'
};
const alertBgMap: Record<string, string> = {
'pink': 'bg-pink-950/40', 'red': 'bg-red-950/40',
'darkblue': 'bg-blue-950/40', 'white': 'bg-gray-900/40'
};
const ac = flight.alert_color || 'white';
const headerColor = alertColorMap[ac] || 'text-white';
const borderColor = alertBorderMap[ac] || 'border-gray-500/30';
const bgColor = alertBgMap[ac] || 'bg-gray-900/40';
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className={`w-full bg-black/60 backdrop-blur-md border ${ac === 'pink' ? 'border-pink-800' : ac === 'red' ? 'border-red-800' : ac === 'darkblue' ? 'border-blue-800' : 'border-gray-600'} rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(255,20,147,0.2)] pointer-events-auto overflow-hidden flex-shrink-0`}
>
<div className={`p-3 border-b ${borderColor} ${bgColor} flex justify-between items-center`}>
<h2 className={`text-xs tracking-widest font-bold ${headerColor} flex items-center gap-2`}>
TRACKED AIRCRAFT {flight.alert_category || "ALERT"}
</h2>
<span className="text-[10px] text-gray-500 font-mono">TRK: {callsign}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">OPERATOR</span>
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" ? (
<a
href={`https://en.wikipedia.org/wiki/${encodeURIComponent(flight.alert_operator.replace(/ /g, '_'))}`}
target="_blank"
rel="noreferrer"
className={`text-xs font-bold underline ${headerColor} hover:opacity-80 transition-opacity`}
title={`Search Wikipedia for ${flight.alert_operator}`}
>
{flight.alert_operator}
</a>
) : (
<span className={`text-xs font-bold ${headerColor}`}>UNKNOWN</span>
)}
</div>
{/* Owner/Operator Wikipedia photo */}
{flight.alert_operator && flight.alert_operator !== "UNKNOWN" && (
<div className="border-b border-gray-800 pb-2">
<WikiImage
wikiUrl={`https://en.wikipedia.org/wiki/${encodeURIComponent(flight.alert_operator.replace(/ /g, '_'))}`}
label={flight.alert_operator}
maxH="max-h-36"
accent={ac === 'pink' ? 'hover:border-pink-500/50' : ac === 'red' ? 'hover:border-red-500/50' : 'hover:border-cyan-500/50'}
/>
</div>
)}
{/* Aircraft model Wikipedia photo */}
{aircraftImgUrl && (
<div className="border-b border-gray-800 pb-2">
<a href={aircraftWikiUrl || '#'} target="_blank" rel="noopener noreferrer" className="block">
<img
src={aircraftImgUrl}
alt={AIRCRAFT_WIKI[flight.model] || flight.model}
className={`w-full h-auto max-h-28 object-cover rounded border border-gray-700/50 ${ac === 'pink' ? 'hover:border-pink-500/50' : 'hover:border-cyan-500/50'} transition-colors`}
/>
</a>
{aircraftWikiUrl && (
<a href={aircraftWikiUrl} target="_blank" rel="noopener noreferrer"
className="text-[10px] text-cyan-400 hover:text-cyan-300 underline mt-1 inline-block">
📖 {AIRCRAFT_WIKI[flight.model] || flight.model} Wikipedia
</a>
)}
</div>
)}
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">CATEGORY</span>
<span className={`text-xs font-bold ${headerColor}`}>{flight.alert_category || "N/A"}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">AIRCRAFT</span>
<span className="text-white text-xs font-bold">{flight.alert_type || flight.model || "UNKNOWN"}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">REGISTRATION</span>
<span className="text-white text-xs font-bold">{flight.registration || "N/A"}</span>
</div>
{flight.alert_tag1 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">INTEL TAG</span>
<span className={`text-xs font-bold ${headerColor}`}>{flight.alert_tag1}</span>
</div>
)}
{flight.alert_tag2 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">SECONDARY</span>
<span className="text-white text-xs font-bold">{flight.alert_tag2}</span>
</div>
)}
{flight.alert_tag3 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">DETAIL</span>
<span className="text-gray-400 text-xs">{flight.alert_tag3}</span>
</div>
)}
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">ALTITUDE</span>
<span className="text-white text-xs font-bold">{(Math.round((flight.alt || 0) / 0.3048)).toLocaleString()} ft</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">GROUND SPEED</span>
<span className="text-white text-xs font-bold">{flight.speed_knots ? `${flight.speed_knots} kts` : 'N/A'}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">HEADING</span>
<span className="text-white text-xs font-bold">{Math.round(flight.heading || 0)}°</span>
</div>
{flight.squawk && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">SQUAWK</span>
<span className={`text-xs font-bold ${flight.squawk === '7700' ? 'text-red-400 animate-pulse' : flight.squawk === '7600' ? 'text-yellow-400' : 'text-white'}`}>{flight.squawk}{flight.squawk === '7700' ? ' ⚠ EMERGENCY' : flight.squawk === '7600' ? ' COMMS LOST' : ''}</span>
</div>
)}
{flight.alert_link && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">REFERENCE</span>
<a href={flight.alert_link} target="_blank" rel="noreferrer" className={`text-xs font-bold underline ${headerColor} hover:opacity-80`}>
View Intel Source
</a>
</div>
)}
{flight.icao24 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">FLIGHT RECORD</span>
<a href={`https://adsb.lol/?icao=${flight.icao24}`} target="_blank" rel="noreferrer" className={`${headerColor} hover:opacity-80 text-xs font-bold underline`}>
View History Log
</a>
</div>
)}
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'flight' || selectedEntity?.type === 'military_flight' || selectedEntity?.type === 'private_flight' || selectedEntity?.type === 'private_jet') {
const flightsList = selectedEntity.type === 'flight' ? data?.commercial_flights
: selectedEntity.type === 'private_flight' ? data?.private_flights
: selectedEntity.type === 'private_jet' ? data?.private_jets
: data?.military_flights;
const flight = flightsList?.[selectedEntity.id as number];
if (flight) {
const callsign = flight.callsign || "UNKNOWN";
let airline = "UNKNOWN";
if (selectedEntity.type === 'military_flight') {
airline = "MILITARY ASSET";
} else if (selectedEntity.type === 'private_jet') {
airline = "PRIVATE JET";
} else if (selectedEntity.type === 'private_flight') {
airline = "PRIVATE / GA";
} else if (flight.airline_code) {
// Use the airline code resolved from adsb.lol routeset API
const codeMap: Record<string, string> = {
"UAL": "UNITED AIRLINES", "DAL": "DELTA AIR LINES", "SWA": "SOUTHWEST AIRLINES",
"AAL": "AMERICAN AIRLINES", "BAW": "BRITISH AIRWAYS", "AFR": "AIR FRANCE",
"JBU": "JETBLUE AIRWAYS", "NKS": "SPIRIT AIRLINES", "THY": "TURKISH AIRLINES",
"UAE": "EMIRATES", "QFA": "QANTAS", "ACA": "AIR CANADA",
"FFT": "FRONTIER AIRLINES", "WJA": "WESTJET", "RPA": "REPUBLIC AIRWAYS",
"SKW": "SKYWEST AIRLINES", "ENY": "ENVOY AIR", "ASA": "ALASKA AIRLINES",
"HAL": "HAWAIIAN AIRLINES", "DLH": "LUFTHANSA", "KLM": "KLM",
"EZY": "EASYJET", "RYR": "RYANAIR", "SIA": "SINGAPORE AIRLINES",
"CPA": "CATHAY PACIFIC", "ANA": "ALL NIPPON AIRWAYS", "JAL": "JAPAN AIRLINES",
"QTR": "QATAR AIRWAYS", "ETD": "ETIHAD AIRWAYS", "SAS": "SAS SCANDINAVIAN"
};
airline = codeMap[flight.airline_code] || flight.airline_code;
} else if (callsign !== "UNKNOWN") {
airline = "COMMERCIAL FLIGHT";
}
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-cyan-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,128,255,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-cyan-500/30 bg-cyan-950/40 flex justify-between items-center">
<h2 className={`text-xs tracking-widest font-bold ${selectedEntity.type === 'military_flight' ? 'text-red-400' : selectedEntity.type === 'private_flight' ? 'text-orange-400' : selectedEntity.type === 'private_jet' ? 'text-purple-400' : 'text-cyan-400'} flex items-center gap-2`}>
{selectedEntity.type === 'military_flight' ? "MILITARY BOGEY INTERCEPT" : selectedEntity.type === 'private_flight' ? "PRIVATE TRANSPONDER" : selectedEntity.type === 'private_jet' ? "PRIVATE JET TRANSPONDER" : "COMMERCIAL TRANSPONDER"}
</h2>
<span className="text-[10px] text-gray-500 font-mono">TRK: {callsign}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">OPERATOR</span>
<span className="text-white text-xs font-bold">{airline}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">REGISTRATION</span>
<span className="text-white text-xs font-bold">{flight.registration || "N/A"}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">AIRCRAFT MODEL</span>
<span className="text-white text-xs font-bold">{flight.model || "UNKNOWN"}</span>
</div>
{/* Aircraft photo + Wikipedia link */}
{(aircraftImgUrl || aircraftImgLoading || aircraftWikiUrl) && (
<div className="border-b border-gray-800 pb-3">
{aircraftImgLoading && (
<div className="w-full h-24 rounded bg-gray-800/60 animate-pulse" />
)}
{aircraftImgUrl && (
<a href={aircraftWikiUrl || '#'} target="_blank" rel="noopener noreferrer" className="block">
<img
src={aircraftImgUrl}
alt={AIRCRAFT_WIKI[flight.model] || flight.model}
className="w-full h-auto max-h-32 object-cover rounded border border-gray-700/50 hover:border-cyan-500/50 transition-colors"
style={{ imageRendering: 'auto' }}
/>
</a>
)}
{aircraftWikiUrl && (
<a href={aircraftWikiUrl} target="_blank" rel="noopener noreferrer"
className="text-[10px] text-cyan-400 hover:text-cyan-300 underline mt-1 inline-block">
📖 {AIRCRAFT_WIKI[flight.model] || flight.model} Wikipedia
</a>
)}
</div>
)}
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">ALTITUDE</span>
<span className="text-white text-xs font-bold">{(Math.round((flight.alt || 0) / 0.3048)).toLocaleString()} ft</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">GROUND SPEED</span>
<span className="text-white text-xs font-bold">{flight.speed_knots ? `${flight.speed_knots} kts` : 'N/A'}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">HEADING</span>
<span className="text-white text-xs font-bold">{Math.round(flight.heading || 0)}°</span>
</div>
{flight.squawk && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">SQUAWK</span>
<span className={`text-xs font-bold ${flight.squawk === '7700' ? 'text-red-400 animate-pulse' : flight.squawk === '7600' ? 'text-yellow-400' : 'text-white'}`}>{flight.squawk}{flight.squawk === '7700' ? ' ⚠ EMERGENCY' : flight.squawk === '7600' ? ' COMMS LOST' : ''}</span>
</div>
)}
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">ROUTE</span>
<span className="text-cyan-400 text-xs font-bold">{flight.origin_name !== "UNKNOWN" ? `[${flight.origin_name}] → [${flight.dest_name}]` : "UNKNOWN"}</span>
</div>
{flight.icao24 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">FLIGHT RECORD</span>
<a href={`https://adsb.lol/?icao=${flight.icao24}`} target="_blank" rel="noreferrer" className="text-cyan-400 hover:text-cyan-300 text-xs font-bold underline">
View History Log
</a>
</div>
)}
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'ship') {
const ship = data?.ships?.[selectedEntity.id as number];
if (ship) {
const vesselTypeLabels: Record<string, string> = {
'tanker': 'TANKER',
'cargo': 'CARGO VESSEL',
'passenger': 'PASSENGER / CRUISE',
'yacht': 'PRIVATE YACHT',
'military_vessel': 'MILITARY VESSEL',
'carrier': 'AIRCRAFT CARRIER',
};
const typeLabel = vesselTypeLabels[ship.type] || ship.type?.toUpperCase() || 'VESSEL';
const headerColorMap: Record<string, string> = {
'tanker': 'text-red-400',
'cargo': 'text-red-400',
'passenger': 'text-white',
'yacht': 'text-blue-400',
'military_vessel': 'text-yellow-400',
'carrier': 'text-orange-400',
};
const headerColor = headerColorMap[ship.type] || 'text-gray-400';
const headerTitleMap: Record<string, string> = {
'tanker': 'AIS TANKER INTERCEPT',
'cargo': 'AIS CARGO INTERCEPT',
'passenger': 'AIS PASSENGER VESSEL',
'yacht': 'AIS YACHT SIGNAL',
'military_vessel': 'AIS MILITARY VESSEL',
'carrier': 'CARRIER STRIKE GROUP',
};
const headerTitle = headerTitleMap[ship.type] || 'AIS VESSEL SIGNAL';
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-cyan-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,128,255,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-cyan-500/30 bg-cyan-950/40 flex justify-between items-center">
<h2 className={`text-xs tracking-widest font-bold ${headerColor} flex items-center gap-2`}>
{headerTitle}
</h2>
<span className="text-[10px] text-gray-500 font-mono">MMSI: {ship.mmsi || 'N/A'}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">VESSEL NAME</span>
<span className="text-white text-xs font-bold text-right ml-4">{ship.name || 'UNKNOWN'}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">VESSEL TYPE</span>
<span className={`text-xs font-bold ${headerColor}`}>{typeLabel}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">FLAG STATE</span>
<span className="text-white text-xs font-bold">{ship.country || 'UNKNOWN'}</span>
</div>
{ship.callsign && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">CALLSIGN</span>
<span className="text-white text-xs font-bold">{ship.callsign}</span>
</div>
)}
{ship.imo > 0 && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">IMO NUMBER</span>
<span className="text-white text-xs font-bold">{ship.imo}</span>
</div>
)}
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">DESTINATION</span>
<span className={`text-xs font-bold ${ship.destination && ship.destination !== 'UNKNOWN' ? 'text-cyan-400' : 'text-orange-400'}`}>{ship.destination || 'UNKNOWN'}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">SPEED (SOG)</span>
<span className="text-white text-xs font-bold">{ship.type === 'carrier' ? 'UNKNOWN' : `${ship.sog || 0} kts`}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">COURSE (COG)</span>
<span className="text-white text-xs font-bold">{ship.type === 'carrier' ? 'UNKNOWN' : `${Math.round(ship.cog || 0)}°`}</span>
</div>
{ship.mmsi && (
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">VESSEL RECORD</span>
<a href={`https://www.marinetraffic.com/en/ais/details/ships/mmsi:${ship.mmsi}`} target="_blank" rel="noreferrer" className="text-cyan-400 hover:text-cyan-300 text-xs font-bold underline">
View on MarineTraffic
</a>
</div>
)}
{/* Ship/Carrier Wikipedia photo */}
{(ship.wiki || VESSEL_TYPE_WIKI[ship.type]) && (
<div className="border-t border-gray-800 pt-2">
<WikiImage
wikiUrl={ship.wiki || VESSEL_TYPE_WIKI[ship.type]}
label={ship.type === 'carrier' ? ship.name : typeLabel}
maxH="max-h-32"
accent={ship.type === 'carrier' ? 'hover:border-orange-500/50' : 'hover:border-cyan-500/50'}
/>
</div>
)}
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'gdelt') {
const gdeltItem = data?.gdelt?.[selectedEntity.id as number];
if (gdeltItem && gdeltItem.properties) {
const props = gdeltItem.properties;
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-orange-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(255,140,0,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-orange-500/30 bg-orange-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-orange-400 flex items-center gap-2">
<AlertTriangle size={14} className="text-orange-400" /> MILITARY INCIDENT CLUSTER
</h2>
<span className="text-[10px] text-gray-500 font-mono">ID: {selectedEntity.id}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">LOCATION</span>
<span className="text-white text-xs font-bold text-right ml-4">{props.name || 'UNKNOWN REGION'}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">ARTICLE COUNT</span>
<span className="text-orange-400 text-xs font-bold">{props.count || 1}</span>
</div>
<div className="flex flex-col gap-2 mt-2">
<span className="text-gray-500 text-[10px]">LATEST REPORTS:</span>
<div
className="text-white text-xs whitespace-normal [&_a]:text-orange-400 [&_a]:underline hover:[&_a]:text-orange-300 [&_br]:mb-2"
dangerouslySetInnerHTML={{ __html: props.html || 'No articles available.' }}
/>
</div>
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'liveuamap') {
const item = data?.liveuamap?.find((l: any) => String(l.id) === String(selectedEntity.id));
if (item) {
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-yellow-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(255,255,0,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-yellow-500/30 bg-yellow-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-yellow-400 flex items-center gap-2">
<AlertTriangle size={14} className="text-yellow-400" /> REGIONAL TACTICAL EVENT
</h2>
<span className="text-[10px] text-gray-500 font-mono">ID: {item.id}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">REGION</span>
<span className="text-white text-xs font-bold text-right ml-4">{item.region || 'UNKNOWN'}</span>
</div>
<div className="flex flex-col gap-2 border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">DESCRIPTION</span>
<span className="text-yellow-400 text-xs font-bold leading-tight">{item.title}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2 mt-2">
<span className="text-gray-500 text-[10px]">REPORTED TIME</span>
<span className="text-white text-xs font-bold">{item.timestamp || 'UNKNOWN'}</span>
</div>
{item.link && (
<div className="flex justify-between items-center pb-2 mt-2">
<span className="text-gray-500 text-[10px]">SOURCE</span>
<a href={item.link} target="_blank" rel="noreferrer" className="text-yellow-400 hover:text-yellow-300 text-xs font-bold underline">
View Liveuamap Report
</a>
</div>
)}
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'news') {
const item = data?.news?.[selectedEntity.id as number];
if (item) {
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-red-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(255,0,0,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-red-500/30 bg-red-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-red-400 flex items-center gap-2">
<AlertTriangle size={14} className="text-red-400" /> THREAT INTERCEPT
</h2>
<span className="text-[10px] text-gray-500 font-mono">LVL: {item.risk_score}/10</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">SOURCE</span>
<span className="text-white text-xs font-bold text-right ml-4">{item.source || 'UNKNOWN'}</span>
</div>
<div className="flex flex-col gap-2 border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">HEADLINE</span>
<span className="text-red-400 text-xs font-bold leading-tight">{item.title}</span>
</div>
{item.machine_assessment && (
<div className="mt-2 p-2 bg-black/60 border border-cyan-800/50 rounded-sm text-[9px] text-cyan-400 font-mono leading-tight relative overflow-hidden shadow-[inset_0_0_10px_rgba(0,255,255,0.05)]">
<div className="absolute top-0 left-0 w-[2px] h-full bg-cyan-500 animate-pulse"></div>
<span className="font-bold text-white">&gt;_ SYS.ANALYSIS: </span>
<span className="text-cyan-300 opacity-90">{item.machine_assessment}</span>
</div>
)}
{item.link && (
<div className="flex justify-between items-center pb-2 mt-2">
<span className="text-gray-500 text-[10px]">REFERENCE</span>
<a href={item.link} target="_blank" rel="noreferrer" className="text-red-400 hover:text-red-300 text-xs font-bold underline">
View Source Article
</a>
</div>
)}
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'airport') {
const apt = data?.airports?.find((a: any) => String(a.id) === String(selectedEntity.id));
if (apt) {
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-cyan-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,128,255,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-cyan-500/30 bg-cyan-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-cyan-400 flex items-center gap-2">
AERONAUTICAL HUB
</h2>
<span className="text-[10px] text-gray-500 font-mono">IATA: {apt.iata}</span>
</div>
<div className="p-4 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">FACILITY NAME</span>
<span className="text-white text-[10px] font-bold text-right ml-4 break-words">{apt.name}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">COORDINATES</span>
<span className="text-white text-xs font-bold">{apt.lat.toFixed(4)}, {apt.lng.toFixed(4)}</span>
</div>
<div className="flex justify-between items-center border-b border-gray-800 pb-2">
<span className="text-gray-500 text-[10px]">STATUS</span>
<span className="text-green-400 animate-pulse text-xs font-bold">OPERATIONAL</span>
</div>
</div>
</motion.div>
)
}
}
if (selectedEntity?.type === 'cctv') {
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4 }}
className="w-full bg-black/60 backdrop-blur-md border border-cyan-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,128,255,0.2)] pointer-events-auto overflow-hidden flex-shrink-0"
>
<div className="p-3 border-b border-cyan-500/30 bg-cyan-950/40 flex justify-between items-center">
<h2 className="text-xs tracking-widest font-bold text-cyan-400 flex items-center gap-2">
<AlertTriangle size={14} className="text-red-400" /> {selectedEntity.extra?.last_updated
? new Date(selectedEntity.extra.last_updated + 'Z').toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' }).toUpperCase() + ' — OPTIC INTERCEPT'
: 'OPTIC INTERCEPT'}
</h2>
<span className="text-[10px] text-gray-500 font-mono">ID: {selectedEntity.id}</span>
</div>
<div className="relative w-full h-48 bg-black flex items-center justify-center p-1">
{(() => {
const url = selectedEntity.media_url || '';
const mt = selectedEntity.extra?.media_type || (
url.includes('.mp4') || url.includes('.webm') ? 'video' :
url.includes('.m3u8') || url.includes('hls') ? 'hls' :
url.includes('.mjpg') || url.includes('.mjpeg') || url.includes('mjpg') ? 'mjpeg' :
url.includes('embed') || url.includes('maps/embed') ? 'embed' :
url.includes('mapbox.com') ? 'satellite' : 'image'
);
if (mt === 'video') return (
<video
src={url}
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
/>
);
if (mt === 'hls') return (
<video
src={url}
autoPlay
muted
playsInline
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
/>
);
if (mt === 'embed') return (
<iframe
src={url}
allowFullScreen
loading="lazy"
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
/>
);
if (mt === 'mjpeg') return (
<img
src={url}
alt="MJPEG Feed"
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "https://via.placeholder.com/400x300.png?text=FEED+UNAVAILABLE";
}}
/>
);
// satellite / image — standard img with referrer policy for external tiles
return (
<img
src={url}
alt="CCTV Feed"
className="w-full h-full object-cover border border-cyan-900/50 rounded-sm filter contrast-125 saturate-50"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = "https://via.placeholder.com/400x300.png?text=NO+SIGNAL";
}}
/>
);
})()}
{/* Retro UI Overlay for the camera feed */}
<div className="absolute top-2 left-2 text-[8px] text-cyan-500 bg-black/50 px-1 py-0.5 rounded">
REC // 00:00:00:00
</div>
</div>
<div className="p-3 bg-black/40 text-[9px] text-cyan-500/70 font-mono tracking-widest flex justify-between items-center">
<span>{selectedEntity.name?.toUpperCase() || 'UNKNOWN MOUNT'}</span>
<span className="text-red-500 text-right">
{selectedEntity.extra?.last_updated
? new Date(selectedEntity.extra.last_updated + 'Z').toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' })
: ''}
</span>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
className={`w-full bg-black/40 backdrop-blur-md border border-gray-800 rounded-xl flex flex-col z-10 font-mono shadow-[0_4px_30px_rgba(0,0,0,0.5)] pointer-events-auto overflow-hidden transition-all duration-300 ${isMinimized ? 'h-[50px] flex-shrink-0' : 'flex-1 min-h-0'}`}
>
<div
className="p-3 border-b border-cyan-500/20 bg-cyan-950/20 relative overflow-hidden cursor-pointer hover:bg-cyan-900/30 transition-colors"
onClick={() => setIsMinimized(!isMinimized)}
>
<div className="flex justify-between items-center relative z-10">
<h2 className="text-xs tracking-widest font-bold text-cyan-400 flex items-center gap-2">
<AlertTriangle size={14} /> GLOBAL THREAT INTERCEPT
</h2>
<button className="text-cyan-500 hover:text-white transition-colors">
{isMinimized ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
</button>
</div>
<AnimatePresence>
{!isMinimized && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="text-[9px] text-cyan-500/80 mt-1 flex items-center justify-between font-bold relative z-10"
>
<span className="px-1 border border-cyan-500/30">SYS.STATUS: MONITORING</span>
<span className="flex items-center gap-1"><Clock size={10} /> {data?.last_updated ? formatTime(data.last_updated) : "SCANNING"}</span>
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{!isMinimized && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 overflow-y-auto p-3 flex flex-col gap-2 styled-scrollbar"
>
{news.map((item: any, idx: number) => {
let bgClass, titleClass, badgeClass;
if (item.risk_score >= 9) {
bgClass = "bg-red-950/20 border-red-500/30";
titleClass = "text-red-300 font-bold";
badgeClass = "bg-red-500/10 text-red-400 border-red-500/30";
} else if (item.risk_score >= 7) {
bgClass = "bg-orange-950/20 border-orange-500/30";
titleClass = "text-orange-300 font-bold";
badgeClass = "bg-orange-500/10 text-orange-400 border-orange-500/30";
} else if (item.risk_score >= 4) {
bgClass = "bg-yellow-950/20 border-yellow-500/30";
titleClass = "text-yellow-300 font-bold";
badgeClass = "bg-yellow-500/10 text-yellow-500 border-yellow-500/30";
} else {
bgClass = "bg-green-950/20 border-green-500/30";
titleClass = "text-green-300 font-medium";
badgeClass = "bg-green-500/10 text-green-400 border-green-500/30";
}
const isExpanded = expandedIndexes.includes(idx);
return (
<motion.div
key={idx}
ref={(el) => { itemRefs.current[idx] = el; }}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 + (idx * 0.05) }}
className={`p-2 rounded-sm border-l-[2px] border-r border-t border-b ${bgClass} flex flex-col gap-1 relative group shrink-0`}
>
<div className="flex items-center justify-between text-[8px] text-gray-400 uppercase tracking-widest">
<span className="font-bold flex items-center gap-1 text-cyan-600">
&gt;_ {item.source}
</span>
<span>[{item.published ? formatTime(item.published) : ''}]</span>
</div>
<a href={item.link} target="_blank" rel="noreferrer" className={`text-[11px] ${titleClass} hover:text-white transition-colors leading-tight`}>
{item.title}
</a>
{item.machine_assessment && (
<div className="mt-1 p-1.5 bg-black/60 border border-cyan-800/50 rounded-sm text-[8.5px] text-cyan-400 font-mono leading-tight relative overflow-hidden shadow-[inset_0_0_10px_rgba(0,255,255,0.05)]">
<div className="absolute top-0 left-0 w-[2px] h-full bg-cyan-500 animate-pulse"></div>
<span className="font-bold text-white">&gt;_ SYS.ANALYSIS: </span>
<span className="text-cyan-300 opacity-90">{item.machine_assessment}</span>
</div>
)}
<div className="flex justify-between items-end mt-1 relative z-10">
<span className={`text-[8px] font-bold px-1 rounded-sm border ${badgeClass}`}>
LVL: {item.risk_score}/10
</span>
<div className="flex items-center gap-2">
{item.cluster_count > 1 && (
<button onClick={() => toggleExpand(idx)} className="text-[8px] font-bold text-cyan-500 bg-cyan-950/50 hover:text-white hover:bg-cyan-900 border border-cyan-500/30 px-1.5 py-0.5 rounded-sm transition-colors cursor-pointer">
{isExpanded ? '[- COLLAPSE]' : `[+${item.cluster_count - 1} SOURCES]`}
</button>
)}
{item.coords && (
<span className="text-[8px] text-gray-500 font-mono tracking-tighter">
{item.coords[0].toFixed(2)}, {item.coords[1].toFixed(2)}
</span>
)}
</div>
</div>
<AnimatePresence>
{isExpanded && item.articles && item.articles.length > 1 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-2 pt-2 border-t border-cyan-500/20 flex flex-col gap-2 overflow-hidden"
>
{item.articles.slice(1).map((subItem: any, subIdx: number) => (
<div key={subIdx} className="flex flex-col gap-0.5 pl-2 border-l border-cyan-500/20">
<div className="flex items-center justify-between text-[7.5px] text-gray-500 uppercase font-bold">
<span>&gt;_ {subItem.source}</span>
<span className={
subItem.risk_score >= 9 ? 'text-red-400' :
subItem.risk_score >= 7 ? 'text-orange-400' :
subItem.risk_score >= 4 ? 'text-yellow-500' :
'text-green-400'
}>LVL: {subItem.risk_score}/10</span>
</div>
<a href={subItem.link} target="_blank" rel="noreferrer" className="text-[10px] text-gray-400 hover:text-white transition-colors leading-tight">
{subItem.title}
</a>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
})}
{news.length === 0 && (
<div className="text-cyan-500/50 text-[10px] tracking-widest font-bold text-center mt-6 animate-pulse">
INITIALIZING SECURE HANDSHAKE...
</div>
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
const NewsFeed = React.memo(NewsFeedInner);
export default NewsFeed;