Spaces:
Runtime error
Runtime error
Claude
Add resource intelligence layer: mining/RAM datasets, macro & utilities APIs, map layers
5aeda2b unverified | 'use client'; | |
| import { memo, useState } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { | |
| Plane, Satellite, Sun, AlertTriangle, Camera, | |
| CloudLightning, Ship, Network, Database, Ghost, | |
| Flame, Tv, Radio, Mountain, Anchor, Radar | |
| } from 'lucide-react'; | |
| interface LayerPanelProps { | |
| data: any; | |
| activeLayers: any; | |
| setActiveLayers: React.Dispatch<React.SetStateAction<any>>; | |
| isMobile?: boolean; | |
| theme?: 'core' | 'ghost'; | |
| setTheme?: (theme: 'core' | 'ghost') => void; | |
| } | |
| const LAYER_GROUPS = [ | |
| { | |
| label: 'SDK', | |
| fullLabel: 'OSIRIS SDK', | |
| icon: Database, | |
| layers: [ | |
| { key: 'sdk_sea', label: 'Maritime Lines', dataKey: 'sdk_entities' }, | |
| ], | |
| }, | |
| { | |
| label: 'AVIATION', | |
| fullLabel: 'AVIATION', | |
| icon: Plane, | |
| layers: [ | |
| { key: 'flights', label: 'Commercial', dataKey: 'commercial_flights' }, | |
| { key: 'private', label: 'Private', dataKey: 'private_flights' }, | |
| { key: 'jets', label: 'Private Jets', dataKey: 'private_jets' }, | |
| { key: 'military', label: 'Military', dataKey: 'military_flights' }, | |
| ], | |
| }, | |
| { | |
| label: 'MARITIME', | |
| fullLabel: 'MARITIME', | |
| icon: Ship, | |
| layers: [ | |
| { key: 'maritime', label: 'Maritime / Naval', dataKey: 'maritime_ships,maritime_ports,maritime_chokepoints' }, | |
| ], | |
| }, | |
| { | |
| label: 'SPACE', | |
| fullLabel: 'SPACE TRACKING', | |
| icon: Satellite, | |
| layers: [ | |
| { key: 'satellites', label: 'All Satellites', dataKey: 'satellites' }, | |
| { key: 'sat_comms', label: 'Starlink / Comms', dataKey: 'satellites', catKey: 'comms' }, | |
| { key: 'sat_military', label: 'Military / Intel', dataKey: 'satellites', catKey: 'military' }, | |
| { key: 'sat_navigation', label: 'GPS / Navigation', dataKey: 'satellites', catKey: 'navigation' }, | |
| { key: 'sat_earth', label: 'Earth Observation', dataKey: 'satellites', catKey: 'earth_obs' }, | |
| { key: 'sat_science', label: 'Stations / Telescopes', dataKey: 'satellites', catKey: 'science' }, | |
| ], | |
| }, | |
| { | |
| label: 'SURVEIL', | |
| fullLabel: 'SURVEILLANCE', | |
| icon: Camera, | |
| layers: [ | |
| { key: 'cctv', label: 'CCTV Cameras', dataKey: 'cameras' }, | |
| { key: 'live_news', label: 'Live News Feeds', dataKey: 'live_feeds' }, | |
| { key: 'news_intel', label: 'SIGINT News', dataKey: 'sigint_news' }, | |
| ], | |
| }, | |
| { | |
| label: 'HAZARD', | |
| fullLabel: 'NATURAL HAZARDS', | |
| icon: CloudLightning, | |
| layers: [ | |
| { key: 'earthquakes', label: 'Earthquakes', dataKey: 'earthquakes' }, | |
| { key: 'fires', label: 'Active Fires', dataKey: 'fires' }, | |
| { key: 'weather', label: 'Severe Weather', dataKey: 'weather_events' }, | |
| ], | |
| }, | |
| { | |
| label: 'THREAT', | |
| fullLabel: 'THREATS & INTEL', | |
| icon: AlertTriangle, | |
| layers: [ | |
| { key: 'infrastructure', label: 'Nuclear Facilities', dataKey: 'infrastructure' }, | |
| { key: 'global_incidents', label: 'Global Incidents', dataKey: 'gdelt' }, | |
| { key: 'gps_jamming', label: 'GPS Jamming', dataKey: 'gps_jamming' }, | |
| ], | |
| }, | |
| { | |
| label: 'NETWORK', | |
| fullLabel: 'NETWORK INTEL', | |
| icon: Network, | |
| layers: [ | |
| { key: 'malware', label: 'Live Malware', dataKey: 'malware_threats' }, | |
| ], | |
| }, | |
| { | |
| label: 'RESOURCE', | |
| fullLabel: 'RESOURCE INTEL', | |
| icon: Mountain, | |
| layers: [ | |
| { key: 'mining', label: 'Mining Companies', dataKey: 'mining_companies' }, | |
| { key: 'ram_fabs', label: 'Memory / RAM Fabs', dataKey: 'ram_companies' }, | |
| ], | |
| }, | |
| { | |
| label: 'DISPLAY', | |
| fullLabel: 'DISPLAY', | |
| icon: Sun, | |
| layers: [ | |
| { key: 'day_night', label: 'Day / Night Cycle', dataKey: '' }, | |
| { key: 'terrain_3d', label: '3D Terrain & Buildings', dataKey: '' }, | |
| ], | |
| }, | |
| ]; | |
| /* ── Minimal Toggle Switch ── */ | |
| function ToggleSwitch({ active, onClick }: { active: boolean; onClick: () => void }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className="relative flex-shrink-0 cursor-pointer" | |
| style={{ width: 28, height: 14 }} | |
| > | |
| <div | |
| className="absolute inset-0 rounded-full transition-all duration-300" | |
| style={{ | |
| background: active ? 'rgba(255,255,255,0.2)' : 'transparent', | |
| border: active ? '1px solid rgba(255,255,255,0.35)' : '1px solid rgba(255,255,255,0.12)', | |
| boxShadow: active ? '0 0 8px rgba(255,255,255,0.1)' : 'none', | |
| }} | |
| /> | |
| <motion.div | |
| className="absolute top-[2px] rounded-full" | |
| style={{ | |
| width: 10, | |
| height: 10, | |
| background: active ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.2)', | |
| boxShadow: active ? '0 0 6px rgba(255,255,255,0.4)' : 'none', | |
| }} | |
| animate={{ left: active ? 16 : 2 }} | |
| transition={{ type: 'spring', stiffness: 500, damping: 30 }} | |
| /> | |
| </button> | |
| ); | |
| } | |
| function LayerPanel({ data, activeLayers, setActiveLayers, isMobile, theme = 'core', setTheme }: LayerPanelProps) { | |
| const [hoveredGroup, setHoveredGroup] = useState<string | null>(null); | |
| const toggle = (key: string) => setActiveLayers((prev: any) => ({ ...prev, [key]: !prev[key] })); | |
| const getCount = (dk: string, catKey?: string): number | null => { | |
| if (!dk) return null; | |
| if (catKey && data.category_counts) { | |
| return data.category_counts[catKey] || 0; | |
| } | |
| let total = 0; | |
| let found = false; | |
| for (const k of dk.split(',')) { | |
| if (data[k] && Array.isArray(data[k])) { | |
| total += data[k].length; | |
| found = true; | |
| } | |
| } | |
| return found ? total : null; | |
| }; | |
| /* ── MOBILE ── */ | |
| if (isMobile) { | |
| return ( | |
| <div className="flex flex-col gap-5 py-2"> | |
| {LAYER_GROUPS.map((group) => ( | |
| <div key={group.label} className="flex flex-col gap-2"> | |
| <div className="text-[9px] font-mono tracking-[0.2em] uppercase text-white/30 border-b border-white/[0.06] pb-1.5"> | |
| {group.fullLabel} | |
| </div> | |
| <div className="flex flex-col gap-1"> | |
| {group.layers.map((layer) => { | |
| const isLayerActive = activeLayers[layer.key]; | |
| const count = getCount(layer.dataKey, layer.catKey); | |
| return ( | |
| <div key={layer.key} className="flex items-center gap-3 px-1 py-1.5"> | |
| <ToggleSwitch | |
| active={!!isLayerActive} | |
| onClick={() => toggle(layer.key)} | |
| /> | |
| <span className={`text-[10px] font-mono uppercase tracking-wider flex-1 transition-colors ${isLayerActive ? 'text-white/80' : 'text-white/40'}`}> | |
| {layer.label} | |
| </span> | |
| {count !== null && ( | |
| <span className="text-[8px] font-mono tabular-nums text-white/20"> | |
| {count.toLocaleString()} | |
| </span> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| {/* MOBILE GHOST TOGGLE */} | |
| {setTheme && ( | |
| <div className="flex items-center justify-between mt-2 pt-3 border-t border-white/[0.06] px-1"> | |
| <span className="text-[9px] font-mono tracking-[0.2em] text-white/25 uppercase">Ghost Protocol</span> | |
| <button | |
| onClick={() => setTheme(theme === 'core' ? 'ghost' : 'core')} | |
| className="w-8 h-8 rounded-full flex items-center justify-center transition-all" | |
| style={{ | |
| background: theme === 'ghost' ? 'rgba(179, 136, 255, 0.15)' : 'transparent', | |
| boxShadow: theme === 'ghost' ? '0 0 12px rgba(179, 136, 255, 0.3)' : 'none', | |
| }} | |
| > | |
| <Ghost className="w-4 h-4" style={{ color: theme === 'ghost' ? '#B388FF' : 'rgba(255,255,255,0.25)' }} /> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| /* ── DESKTOP ── */ | |
| return ( | |
| <motion.div | |
| initial={{ x: -60, opacity: 0 }} | |
| animate={{ x: 0, opacity: 1 }} | |
| transition={{ type: 'spring', damping: 30, stiffness: 200, delay: 2.8 }} | |
| className="absolute top-0 left-0 h-full w-[48px] flex flex-col items-center pt-24 pb-6 z-50 pointer-events-auto" | |
| style={{ | |
| background: 'rgba(0,0,0,0.15)', | |
| backdropFilter: 'blur(24px) saturate(1.2)', | |
| WebkitBackdropFilter: 'blur(24px) saturate(1.2)', | |
| }} | |
| > | |
| <div className="flex-1 flex flex-col items-center gap-1"> | |
| {LAYER_GROUPS.map((group) => { | |
| const groupActive = group.layers.some(l => activeLayers[l.key]); | |
| const isHovered = hoveredGroup === group.label; | |
| const Icon = group.icon; | |
| return ( | |
| <div | |
| key={group.label} | |
| className="relative flex items-center justify-center" | |
| onMouseEnter={() => setHoveredGroup(group.label)} | |
| onMouseLeave={() => setHoveredGroup(null)} | |
| > | |
| {/* Icon Button */} | |
| <div | |
| className="w-10 h-10 flex items-center justify-center cursor-pointer rounded-lg transition-all duration-300" | |
| style={{ | |
| background: isHovered ? 'rgba(255,255,255,0.05)' : 'transparent', | |
| }} | |
| > | |
| <Icon | |
| className="transition-all duration-300" | |
| style={{ | |
| width: 16, | |
| height: 16, | |
| color: groupActive | |
| ? 'rgba(255,255,255,0.7)' | |
| : isHovered | |
| ? 'rgba(255,255,255,0.4)' | |
| : 'rgba(255,255,255,0.2)', | |
| filter: groupActive ? 'drop-shadow(0 0 4px rgba(255,255,255,0.3))' : 'none', | |
| }} | |
| /> | |
| </div> | |
| {/* Flyout (LEFT side) */} | |
| <AnimatePresence> | |
| {isHovered && ( | |
| <motion.div | |
| initial={{ opacity: 0, x: -8, filter: 'blur(4px)' }} | |
| animate={{ opacity: 1, x: 0, filter: 'blur(0px)' }} | |
| exit={{ opacity: 0, x: -4, filter: 'blur(2px)' }} | |
| transition={{ duration: 0.18, ease: 'easeOut' }} | |
| className="absolute left-[52px] top-1/2 -translate-y-1/2 min-w-[220px] rounded-xl p-3 z-[100] pointer-events-auto" | |
| style={{ | |
| background: 'rgba(0,0,0,0.6)', | |
| backdropFilter: 'blur(40px) saturate(1.5)', | |
| WebkitBackdropFilter: 'blur(40px) saturate(1.5)', | |
| border: '1px solid rgba(255,255,255,0.06)', | |
| boxShadow: '0 8px 32px rgba(0,0,0,0.5)', | |
| }} | |
| > | |
| <div className="text-[9px] font-mono tracking-[0.2em] uppercase text-white/30 mb-2.5 pb-1.5 border-b border-white/[0.04]"> | |
| {group.fullLabel} | |
| </div> | |
| <div className="flex flex-col gap-0.5"> | |
| {group.layers.map((layer) => { | |
| const isLayerActive = activeLayers[layer.key]; | |
| const count = getCount(layer.dataKey, layer.catKey); | |
| return ( | |
| <div | |
| key={layer.key} | |
| className="flex items-center gap-3 px-1 py-[5px] rounded-md hover:bg-white/[0.03] transition-colors cursor-pointer" | |
| onClick={() => toggle(layer.key)} | |
| > | |
| <ToggleSwitch active={!!isLayerActive} onClick={() => {}} /> | |
| <span className={`text-[10px] font-mono uppercase tracking-wider flex-1 transition-colors duration-200 ${isLayerActive ? 'text-white/70' : 'text-white/35'}`}> | |
| {layer.label} | |
| </span> | |
| {count !== null && ( | |
| <span className="text-[9px] font-mono tabular-nums text-white/20"> | |
| {count.toLocaleString()} | |
| </span> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Subtle separator */} | |
| <div className="w-5 h-px bg-white/[0.06] my-2" /> | |
| {/* Ghost Protocol Toggle */} | |
| {setTheme && ( | |
| <button | |
| onClick={() => setTheme(theme === 'core' ? 'ghost' : 'core')} | |
| className="w-10 h-10 flex items-center justify-center rounded-lg transition-all duration-500 cursor-pointer" | |
| style={{ | |
| background: theme === 'ghost' ? 'rgba(179, 136, 255, 0.1)' : 'transparent', | |
| }} | |
| title="Ghost Protocol" | |
| > | |
| <Ghost | |
| className="transition-all duration-500" | |
| style={{ | |
| width: 15, | |
| height: 15, | |
| color: theme === 'ghost' ? '#B388FF' : 'rgba(255,255,255,0.15)', | |
| filter: theme === 'ghost' ? 'drop-shadow(0 0 6px rgba(179, 136, 255, 0.5))' : 'none', | |
| }} | |
| /> | |
| </button> | |
| )} | |
| </motion.div> | |
| ); | |
| } | |
| export default memo(LayerPanel); | |