MotorSignal-App/website/components/group-rides/live-pack-radar.tsx

486 lines
26 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { RadarMapMode, RadarRiderTelemetry } from './types';
import { INITIAL_RADAR_RIDERS } from './fixtures';
interface LivePackRadarProps {
initialMode?: RadarMapMode;
}
export function LivePackRadar({ initialMode = 'tactical' }: LivePackRadarProps) {
const [mapMode, setMapMode] = useState<RadarMapMode>(initialMode);
const [selectedRiderId, setSelectedRiderId] = useState<string | null>('rider-1');
const [simulationTick, setSimulationTick] = useState(0);
// Active riders list with slight dynamic animation based on simulationTick
const riders: RadarRiderTelemetry[] = INITIAL_RADAR_RIDERS.map((r, idx) => {
// Subtle realistic wobble for demo interactivity
const speedWobble = simulationTick > 0 ? ((simulationTick + idx * 3) % 5) - 2 : 0;
return {
...r,
speedMph: Math.max(0, r.speedMph + speedWobble),
};
});
const selectedRider = riders.find((r) => r.id === selectedRiderId) || riders[0];
const handlePulseSimulation = () => {
setSimulationTick((prev) => prev + 1);
};
return (
<div className="bg-surface-container-lowest rounded-xl shadow-2xl overflow-hidden border border-outline-variant/30">
{/* Tactical Header Strip */}
<div className="bg-surface-container-low px-space-lg py-space-sm flex flex-wrap items-center justify-between gap-space-md border-b border-outline-variant/20">
<div className="flex items-center gap-space-md">
<div className="flex items-center gap-space-xs">
<span className="h-2.5 w-2.5 rounded-full bg-secondary-fixed animate-pulse"></span>
<span className="font-label-caps text-label-caps text-on-surface tracking-wide">
Swarm Recon: Skyline Blvd &gt; Coast Hwy
</span>
</div>
<span className="font-code-mono text-code-mono text-on-surface-variant bg-surface-container px-2 py-0.5 rounded border border-outline-variant/20">
Session #CA-84-SW
</span>
</div>
<div className="flex items-center gap-space-lg font-code-mono text-code-mono text-on-surface-variant">
<span className="hidden lg:flex items-center gap-1.5 text-secondary-fixed">
<span className="material-symbols-outlined text-[18px]">cell_tower</span>
Mesh Active
</span>
<span className="flex items-center gap-1.5">
<span className="text-secondary-fixed">●</span> {riders.length} Synchronized
</span>
<span className="hidden sm:inline text-on-surface-variant">Gap Delta: ±0.4s</span>
<button
onClick={handlePulseSimulation}
className="flex items-center gap-1 px-2.5 py-1 rounded bg-surface-container hover:bg-surface-container-high text-primary font-code-mono text-code-mono transition-colors"
title="Trigger Telemetry Ping"
>
<span className="material-symbols-outlined text-[16px]">sync</span>
Ping Swarm
</button>
</div>
</div>
{/* Cockpit Layout: Map & Radar Feed */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-0">
{/* Tactical Canvas & Radar Screen */}
<div
className={`lg:col-span-8 relative min-h-[560px] flex flex-col justify-between p-space-md overflow-hidden transition-all duration-300 ${
mapMode === 'tactical'
? 'bg-[#0B1319]'
: mapMode === 'satellite'
? 'bg-[#071015]'
: 'bg-[#0E1520]'
}`}
data-testid="radar-canvas-container"
>
{/* Tactical Background Overlay */}
{mapMode === 'tactical' && (
<>
{/* Tactical Coordinate Grids (Inline SVG Overlay) */}
<svg className="absolute inset-0 w-full h-full pointer-events-none opacity-40" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="tactical-grid-pattern" width="48" height="48" patternUnits="userSpaceOnUse">
<path d="M 48 0 L 0 0 0 48" fill="none" stroke="rgba(0, 242, 254, 0.15)" strokeWidth="0.5" />
<circle cx="0" cy="0" r="1.5" fill="rgba(0, 242, 254, 0.4)" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#tactical-grid-pattern)" />
{/* Tactical Route Trace */}
<path
d="M 80 480 C 180 410, 240 430, 340 330 S 480 280, 560 190 S 700 170, 820 100"
fill="none"
stroke="#00F2FE"
strokeDasharray="8 6"
strokeWidth="3.5"
className="animate-pulse"
/>
{/* Safe Spacing Buffers */}
<circle cx="820" cy="100" r="32" fill="none" stroke="rgba(57, 255, 20, 0.3)" strokeWidth="1.5" />
<circle cx="700" cy="170" r="28" fill="none" stroke="rgba(57, 255, 20, 0.3)" strokeWidth="1.5" />
<circle cx="560" cy="190" r="34" fill="none" stroke="rgba(255, 107, 107, 0.4)" strokeWidth="2" strokeDasharray="4 2" />
<circle cx="340" cy="330" r="26" fill="none" stroke="rgba(57, 255, 20, 0.3)" strokeWidth="1.5" />
<circle cx="240" cy="430" r="36" fill="none" stroke="rgba(255, 68, 68, 0.6)" strokeDasharray="4 2" strokeWidth="2.5" />
<circle cx="80" cy="480" r="30" fill="none" stroke="rgba(0, 242, 254, 0.4)" strokeWidth="1.5" />
</svg>
</>
)}
{/* Satellite Mode Backdrop */}
{mapMode === 'satellite' && (
<div className="absolute inset-0 pointer-events-none" data-testid="satellite-mode-view">
{/* Satellite Dark Topography Simulation */}
<div className="absolute inset-0 bg-gradient-to-b from-[#0A1620]/90 via-[#071118]/80 to-[#040A0F] mix-blend-multiply"></div>
<svg className="absolute inset-0 w-full h-full opacity-30" xmlns="http://www.w3.org/2000/svg">
{/* Topographic Contour lines */}
<path d="M 0 100 Q 250 80 400 180 T 800 120 T 1200 220" fill="none" stroke="#00F2FE" strokeWidth="1" strokeDasharray="2 4" />
<path d="M 0 200 Q 200 150 450 260 T 900 200 T 1200 320" fill="none" stroke="#00F2FE" strokeWidth="1" strokeDasharray="2 4" />
<path d="M 0 320 Q 300 280 500 380 T 950 320 T 1200 440" fill="none" stroke="#00F2FE" strokeWidth="1" strokeDasharray="2 4" />
<path d="M 0 440 Q 350 400 600 480 T 1000 420 T 1200 540" fill="none" stroke="#00F2FE" strokeWidth="1" strokeDasharray="2 4" />
{/* Satellite Vector Track */}
<path
d="M 80 480 C 180 410, 240 430, 340 330 S 480 280, 560 190 S 700 170, 820 100"
fill="none"
stroke="#39FF14"
strokeWidth="3"
/>
</svg>
<div className="absolute top-4 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-surface-container/90 border border-outline-variant/30 text-on-surface font-code-mono text-label-caps text-center">
High-Resolution Orbit Imagery • Multi-Band GNSS Overlaid
</div>
</div>
)}
{/* Elevation 3D Mode Backdrop */}
{mapMode === 'elevation' && (
<div className="absolute inset-0 pointer-events-none" data-testid="elevation-mode-view">
<svg className="absolute inset-0 w-full h-full opacity-40" xmlns="http://www.w3.org/2000/svg">
{/* Elevation isometric grid lines */}
<defs>
<linearGradient id="elevationGrad" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#39FF14" stopOpacity="0.8" />
<stop offset="50%" stopColor="#00F2FE" stopOpacity="0.8" />
<stop offset="100%" stopColor="#FF9F1C" stopOpacity="0.9" />
</linearGradient>
</defs>
{/* Mountain elevation profile ribbon */}
<polygon
points="80,500 180,430 340,350 560,210 700,180 820,110 820,530 80,530"
fill="url(#elevationGrad)"
opacity="0.15"
/>
<path
d="M 80 500 L 180 430 L 340 350 L 560 210 L 700 180 L 820 110"
fill="none"
stroke="url(#elevationGrad)"
strokeWidth="4"
/>
</svg>
{/* Elevation Altitude Badges */}
<div className="absolute bottom-28 left-16 bg-surface/90 px-2 py-1 rounded text-primary font-code-mono text-label-caps border border-primary/30">
Start: 1,480 ft
</div>
<div className="absolute top-28 right-24 bg-surface/90 px-2 py-1 rounded text-secondary-fixed font-code-mono text-label-caps border border-secondary-fixed/30">
Summit: 2,210 ft (+730 ft gain)
</div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 bg-surface/90 px-2.5 py-1 rounded text-on-surface font-code-mono text-label-caps border border-outline-variant/30">
Grade: 8.4% Apex Switchback
</div>
</div>
)}
{/* Map HUD Top Left: Sector Status */}
<div className="relative z-10 flex items-start justify-between">
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2.5 rounded-xl shadow-lg border border-outline-variant/30 flex flex-col gap-0.5">
<span className="font-label-caps text-label-caps text-on-surface-variant">Formation Profile</span>
<span className="font-headline-sm text-headline-sm text-primary font-bold">Staggered Dual Column [B]</span>
<span className="font-code-mono text-code-mono text-secondary-fixed flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">shield</span> Paceline Lock: Optimal
</span>
</div>
{/* Map HUD Top Right: Telemetry & Mesh Lock (GPS Frequency label removed as requested) */}
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2 rounded-xl shadow-lg border border-outline-variant/30 flex items-center gap-space-sm">
<div className="text-right">
<span className="font-label-caps text-label-caps text-secondary-fixed block font-bold">
GNSS Multi-Band Mesh
</span>
<span className="font-code-mono text-code-mono text-on-surface">18 Satellites Locked</span>
</div>
<span className="material-symbols-outlined text-secondary-fixed text-[24px]">satellite_alt</span>
</div>
</div>
{/* Interactive Rider Markers across the canvas */}
<div className="relative z-10 my-auto h-72 w-full">
{riders.map((rider) => {
const isSelected = selectedRiderId === rider.id;
return (
<div
key={rider.id}
onClick={() => setSelectedRiderId(rider.id)}
style={{
left: `${rider.coordinates.x}%`,
top: `${rider.coordinates.y}%`,
}}
className={`absolute -translate-x-1/2 -translate-y-1/2 cursor-pointer transition-all duration-200 group ${
isSelected ? 'z-30 scale-110' : 'z-20 hover:scale-105'
}`}
data-testid={`radar-marker-${rider.callsign}`}
>
{/* Outer targeting reticle if selected */}
{isSelected && (
<div className="absolute -inset-2 rounded-full border-2 border-primary-container animate-ping pointer-events-none opacity-60"></div>
)}
{/* Marker Badge */}
<div
className={`flex items-center gap-1.5 px-2.5 py-1 rounded shadow-xl backdrop-blur-md transition-all ${
isSelected
? 'bg-primary-container text-on-primary-container ring-2 ring-primary-fixed shadow-[0_0_16px_rgba(0,242,254,0.6)]'
: rider.status === 'alert'
? 'bg-error-container/90 text-on-error-container border border-error'
: rider.status === 'warning'
? 'bg-surface-container-high/90 text-on-surface border border-error/50'
: 'bg-surface-container-high/90 text-on-surface border border-outline-variant/30 hover:border-primary-container'
}`}
>
<span
className={`w-2.5 h-2.5 rounded-full ${
rider.status === 'alert'
? 'bg-error animate-bounce'
: rider.status === 'warning'
? 'bg-error animate-pulse'
: isSelected
? 'bg-on-primary-container'
: 'bg-secondary-fixed animate-pulse'
}`}
></span>
<span className="font-code-mono text-code-mono font-bold">{rider.callsign}</span>
<span
className={`font-label-caps text-label-caps font-semibold ${
isSelected
? 'text-on-primary-container'
: rider.status === 'alert'
? 'text-error'
: 'text-secondary-fixed'
}`}
>
{rider.speedMph} MPH
</span>
</div>
{/* Deceleration Warning Callout */}
{rider.status === 'alert' && (
<div className="absolute -bottom-7 left-1/2 -translate-x-1/2 whitespace-nowrap bg-error text-white font-code-mono text-[11px] px-2 py-0.5 rounded shadow-lg flex items-center gap-1">
<span className="material-symbols-outlined text-[13px]">warning</span>
-1.2G Decel
</div>
)}
</div>
);
})}
</div>
{/* Map HUD Bottom Control Strip */}
<div className="relative z-10 flex flex-wrap items-center justify-between gap-space-sm mt-auto pt-space-lg">
{/* Radar View Mode Toggles */}
<div className="flex items-center gap-space-xs bg-surface/90 backdrop-blur-md p-1.5 rounded-xl border border-outline-variant/30">
<button
type="button"
onClick={() => setMapMode('tactical')}
className={`px-space-sm py-1 rounded-lg font-label-caps text-label-caps transition-all ${
mapMode === 'tactical'
? 'bg-primary-container text-on-primary-container font-bold shadow-md'
: 'text-on-surface-variant hover:text-on-surface hover:bg-surface-container'
}`}
>
Tactical
</button>
<button
type="button"
onClick={() => setMapMode('satellite')}
className={`px-space-sm py-1 rounded-lg font-label-caps text-label-caps transition-all ${
mapMode === 'satellite'
? 'bg-primary-container text-on-primary-container font-bold shadow-md'
: 'text-on-surface-variant hover:text-on-surface hover:bg-surface-container'
}`}
>
Satellite
</button>
<button
type="button"
onClick={() => setMapMode('elevation')}
className={`px-space-sm py-1 rounded-lg font-label-caps text-label-caps transition-all ${
mapMode === 'elevation'
? 'bg-primary-container text-on-primary-container font-bold shadow-md'
: 'text-on-surface-variant hover:text-on-surface hover:bg-surface-container'
}`}
>
Elevation 3D
</button>
</div>
{/* Regroup point readout */}
<div className="bg-surface/90 backdrop-blur-md px-space-md py-1.5 rounded-xl flex items-center gap-space-sm border border-outline-variant/30">
<span className="font-label-caps text-label-caps text-on-surface-variant">Regroup Point</span>
<span className="font-code-mono text-code-mono text-primary-container font-bold">
Alice&apos;s Restaurant (14.8 mi)
</span>
</div>
</div>
</div>
{/* Live Swarm Nodes Telemetry Column */}
<div className="lg:col-span-4 bg-surface-container p-space-md flex flex-col gap-space-sm overflow-y-auto max-h-[620px] border-t lg:border-t-0 lg:border-l border-outline-variant/20">
<div className="flex items-center justify-between pb-space-xs mb-space-xs border-b border-outline-variant/20">
<span className="font-label-caps text-label-caps text-on-surface-variant tracking-wide">
Pack Telemetry Stream
</span>
<span className="font-code-mono text-code-mono text-primary-fixed-dim">
{riders.length} Active Nodes
</span>
</div>
{/* Active Rider Telemetry Cards */}
{riders.map((rider) => {
const isSelected = selectedRiderId === rider.id;
return (
<div
key={rider.id}
onClick={() => setSelectedRiderId(rider.id)}
className={`p-space-sm rounded-xl cursor-pointer transition-all border ${
isSelected
? 'bg-surface-container-highest border-primary-container shadow-md ring-1 ring-primary-container/50'
: 'bg-surface-container-high border-transparent hover:bg-surface-container-highest hover:border-outline-variant/30'
}`}
data-testid={`telemetry-card-${rider.callsign}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-space-xs">
<span
className={`w-2 h-2 rounded-full ${
rider.status === 'alert'
? 'bg-error animate-bounce'
: rider.status === 'warning'
? 'bg-error animate-pulse'
: rider.status === 'sweeper'
? 'bg-secondary-fixed'
: 'bg-secondary-fixed'
}`}
></span>
<span className="font-headline-sm text-headline-sm text-on-surface font-bold">
{rider.callsign}
</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-secondary-fixed">
{rider.role}
</span>
</div>
<span
className={`font-code-mono text-code-mono ${
rider.gapMeters === 0 ? 'text-primary-fixed font-bold' : 'text-secondary-fixed'
}`}
>
{rider.gapMeters === 0 ? 'Lead' : `+${rider.gapMeters}m Gap`}
</span>
</div>
{/* Squad Roster Telemetry Grid: Speed, Lean, Battery, Mesh Ping */}
<div className="grid grid-cols-4 gap-1.5 mt-2 font-code-mono text-[11px] text-on-surface-variant">
<div>
<span className="block text-[9px] text-outline">Speed</span>
<span className="text-on-surface font-bold">{rider.speedMph} MPH</span>
</div>
<div>
<span className="block text-[9px] text-outline">Lean</span>
<span className="text-primary-container font-bold">{rider.leanAngleDeg}°</span>
</div>
<div>
<span className="block text-[9px] text-outline">Battery</span>
<span className="text-secondary-fixed font-bold">{rider.batteryPct ?? 92}%</span>
</div>
<div>
<span className="block text-[9px] text-outline">Mesh Ping</span>
<span className="text-primary-fixed-dim font-bold">{rider.meshPingMs ?? 12}ms</span>
</div>
</div>
{/* Warning / Alert Callout */}
{rider.alertText && (
<div className="mt-2 pt-1 border-t border-outline-variant/20 flex items-center gap-1 font-code-mono text-code-mono text-error">
<span className="material-symbols-outlined text-[15px]">
{rider.status === 'alert' ? 'warning' : 'tire_repair'}
</span>
<span className="truncate">{rider.alertText}</span>
</div>
)}
</div>
);
})}
{/* Selected Rider Inspector HUD Drawer */}
{selectedRider && (
<div
className="mt-auto p-space-sm rounded-xl bg-surface-container-lowest border border-primary-container/40"
data-testid="selected-node-inspector"
>
<div className="flex items-center justify-between text-label-caps font-label-caps text-primary-container mb-1">
<span>Selected Node Inspector</span>
<span className="font-code-mono">{selectedRider.callsign}</span>
</div>
<div className="flex items-center justify-between text-code-mono font-code-mono text-on-surface-variant text-[12px]">
<span>Role: {selectedRider.roleBadge}</span>
<span>Gap Delta: ±{selectedRider.gapDeltaSec}s</span>
<span className="text-secondary-fixed">Live Sync: 20Hz</span>
</div>
</div>
)}
</div>
</div>
{/* Formation Safety Metrics Panel Strip */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 bg-surface-container-highest p-space-lg gap-space-lg border-t border-outline-variant/20">
{/* Metric 1: Spacing Integrity */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="font-label-caps text-label-caps text-on-surface-variant">Spacing Integrity</span>
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">verified</span>
</div>
<div className="flex items-baseline gap-space-xs">
<span className="font-telemetry-metric-md text-telemetry-metric-md text-secondary-fixed">94%</span>
<span className="font-code-mono text-code-mono text-on-surface-variant">Safety Index</span>
</div>
<div className="w-full bg-surface-container h-1.5 rounded-full overflow-hidden mt-1">
<div className="bg-secondary-fixed h-full rounded-full w-[94%]"></div>
</div>
</div>
{/* Metric 2: Average Pack Speed */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="font-label-caps text-label-caps text-on-surface-variant">Average Pack Velocity</span>
<span className="material-symbols-outlined text-primary-container text-[18px]">speed</span>
</div>
<div className="flex items-baseline gap-space-xs">
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">64.2</span>
<span className="font-code-mono text-code-mono text-primary-fixed-dim">MPH</span>
</div>
<span className="font-code-mono text-code-mono text-on-surface-variant">
Delta Drift: -1.8 MPH (tightening)
</span>
</div>
{/* Metric 3: Atmospheric & Track Conditions */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="font-label-caps text-label-caps text-on-surface-variant">Surface &amp; Climate</span>
<span className="material-symbols-outlined text-on-surface-variant text-[18px]">thermostat</span>
</div>
<div className="flex items-baseline gap-space-xs">
<span className="font-telemetry-metric-md text-telemetry-metric-md text-on-surface">21°C</span>
<span className="font-code-mono text-code-mono text-on-surface-variant">Dry Asphalt</span>
</div>
<span className="font-code-mono text-code-mono text-secondary-fixed">0% Precip • Grip Optimal</span>
</div>
{/* Metric 4: Next Regroup Waypoint */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="font-label-caps text-label-caps text-on-surface-variant">Regroup Point</span>
<span className="material-symbols-outlined text-primary-container text-[18px]">pin_drop</span>
</div>
<div className="flex items-baseline gap-space-xs">
<span className="font-telemetry-metric-md text-telemetry-metric-md text-primary-container">14.8</span>
<span className="font-code-mono text-code-mono text-primary-fixed-dim">Miles</span>
</div>
<span className="font-code-mono text-code-mono text-on-surface">Alice&apos;s Restaurant Checkpoint</span>
</div>
</div>
</div>
);
}