feat(website): build interactive group rides hub with live pack radar, filters, and modals

This commit is contained in:
Developer 2026-09-25 14:35:58 -04:00
parent 46ce052e0c
commit 67c25b5c50
8 changed files with 2206 additions and 580 deletions

View File

@ -1,9 +1,55 @@
'use client';
import React from 'react';
import Link from 'next/link';
import React, { useState } from 'react';
import { INITIAL_GROUP_RIDES } from '../../components/group-rides/fixtures';
import { GroupRideSummary } from '../../components/group-rides/types';
import { LivePackRadar } from '../../components/group-rides/live-pack-radar';
import { GroupRidesDirectory } from '../../components/group-rides/group-rides-directory';
import { JoinRideModal } from '../../components/group-rides/join-ride-modal';
import { CreateRideModal } from '../../components/group-rides/create-ride-modal';
export default function GroupRidesPage() {
const [rides, setRides] = useState<GroupRideSummary[]>(INITIAL_GROUP_RIDES);
const [isJoinModalOpen, setIsJoinModalOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [activeAlert, setActiveAlert] = useState<{ message: string; type: 'success' | 'info' } | null>(
null
);
const showAlert = (message: string, type: 'success' | 'info' = 'success') => {
setActiveAlert({ message, type });
setTimeout(() => {
setActiveAlert(null);
}, 4500);
};
const handleJoinSuccess = (code: string, rideTitle: string) => {
showAlert(`Successfully synchronized telemetry with "${rideTitle}" (Code: ${code})!`, 'success');
};
const handleCreateRide = (newRide: GroupRideSummary) => {
setRides((prev) => [newRide, ...prev]);
showAlert(`Published group run "${newRide.title}"! Pack radar is now active.`, 'success');
};
const handleRequestSlot = (rideId: string) => {
setRides((prev) =>
prev.map((r) => {
if (r.id !== rideId) return r;
if (r.participantCount >= r.participantLimit) {
showAlert(`Added to waitlist for "${r.title}". You will be alerted if a slot opens.`, 'info');
return { ...r, userStatus: 'waitlisted' };
}
showAlert(`Slot requested for "${r.title}". Telemetry sync confirmed with Lead Marshal!`, 'success');
return {
...r,
userStatus: 'joined',
participantCount: r.participantCount + 1,
};
})
);
};
return (
<div className="flagship-page text-on-surface bg-surface min-h-screen">
<div className="flex flex-col w-full">
@ -11,6 +57,30 @@ export default function GroupRidesPage() {
<div className="relative w-full overflow-hidden">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary-container/10 rounded-full blur-[120px] pointer-events-none -z-10"></div>
<div className="absolute top-64 right-10 w-[500px] h-[500px] bg-secondary-fixed/5 rounded-full blur-[140px] pointer-events-none -z-10"></div>
{/* Toast Notification Alert */}
{activeAlert && (
<div
className={`fixed top-20 right-6 z-50 px-5 py-3 rounded-xl shadow-2xl flex items-center gap-3 border backdrop-blur-md transition-all animate-bounce ${
activeAlert.type === 'success'
? 'bg-secondary-container/90 text-on-secondary-container border-secondary-fixed/40'
: 'bg-surface-container-high/90 text-primary border-primary/40'
}`}
>
<span className="material-symbols-outlined text-[20px]">
{activeAlert.type === 'success' ? 'check_circle' : 'info'}
</span>
<span className="font-code-mono text-sm font-semibold">{activeAlert.message}</span>
<button
onClick={() => setActiveAlert(null)}
className="ml-2 hover:opacity-75"
title="Dismiss"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
</div>
)}
{/* Hero Section */}
<section className="max-w-container-max mx-auto px-gutter-desktop pt-space-2xl pb-space-3xl">
<div className="flex flex-col items-start max-w-4xl">
@ -20,377 +90,189 @@ export default function GroupRidesPage() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-secondary-fixed opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-secondary-fixed-dim"></span>
</span>
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-wide">Live Pack Radar &amp; Formation Telemetry</span>
<span className="font-label-caps text-label-caps text-secondary-fixed tracking-wide">
Live Pack Radar &amp; Formation Telemetry
</span>
</div>
{/* Headline */}
<h1 className="font-display-hero text-display-hero text-on-surface tracking-tight mb-space-sm">
Ride in pack formation. <br />
<span className="bg-gradient-to-r from-primary-container via-primary-fixed to-secondary-fixed text-transparent bg-clip-text">Never lose a wingman.</span>
<span className="bg-gradient-to-r from-primary-container via-primary-fixed to-secondary-fixed text-transparent bg-clip-text">
Never lose a wingman.
</span>
</h1>
{/* Subheading */}
<p className="font-body-lg text-body-lg text-on-surface-variant max-w-2xl mb-space-xl">
Real-time rider radar, staggered formation spacing alerts, instant incident broadcast, and automated regroup checkpoints calibrated for fast canyon runs and transcontinental tours.
Real-time rider radar, staggered formation spacing alerts, instant incident broadcast, and
automated regroup checkpoints calibrated for fast canyon runs and transcontinental tours.
</p>
{/* Dual CTAs & Quick Metrics Bar */}
<div className="flex flex-wrap items-center gap-space-md w-full">
<button className="inline-flex items-center justify-center gap-space-xs px-space-xl py-3.5 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm shadow-xl hover:shadow-[0_0_24px_rgba(0,242,254,0.4)] transition-all" id="btn-create-ride">
<button
type="button"
onClick={() => setIsCreateModalOpen(true)}
className="inline-flex items-center justify-center gap-space-xs px-space-xl py-3.5 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm shadow-xl hover:shadow-[0_0_24px_rgba(0,242,254,0.4)] transition-all cursor-pointer"
id="btn-create-ride"
>
<span className="material-symbols-outlined text-[20px]">add_circle</span>
Create Group Ride
</button>
<button className="inline-flex items-center justify-center gap-space-xs px-space-lg py-3.5 rounded-xl bg-surface-container-high hover:bg-surface-container-highest text-on-surface font-headline-sm text-headline-sm transition-all" id="btn-join-code">
<span className="material-symbols-outlined text-primary-fixed-dim text-[20px]">qr_code_scanner</span>
<button
type="button"
onClick={() => setIsJoinModalOpen(true)}
className="inline-flex items-center justify-center gap-space-xs px-space-lg py-3.5 rounded-xl bg-surface-container-high hover:bg-surface-container-highest text-on-surface font-headline-sm text-headline-sm transition-all cursor-pointer border border-outline-variant/30"
id="btn-join-code"
>
<span className="material-symbols-outlined text-primary-fixed-dim text-[20px]">
qr_code_scanner
</span>
Join with Ride Code / QR
</button>
</div>
</div>
</section>
{/* Interactive Live Pack Radar Map & Dashboard */}
{/* Interactive Live Pack Radar Cockpit */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<div className="bg-surface-container-lowest rounded-xl shadow-2xl overflow-hidden">
{/* 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">
<div className="flex items-center gap-space-md">
<div className="flex items-center gap-space-xs">
<span className="h-2 w-2 rounded-full bg-secondary-fixed"></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">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="flex items-center gap-1 hidden lg:flex">
<span className="material-symbols-outlined text-secondary-fixed text-[18px]">cell_tower</span> Mesh Active
</span>
<span className="flex items-center gap-1"><span className="text-secondary-fixed">●</span> 6 Synchronized</span>
<span className="hidden sm:inline">Gap Delta: ±0.4s</span>
<span className="text-primary-container">Repair Squad: Ready</span>
</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 bg-surface-container-low min-h-[540px] flex flex-col justify-between p-space-md overflow-hidden">
{/* Map background placeholder with tactical overlay styling */}
<div className="absolute inset-0 bg-cover bg-center opacity-40 mix-blend-luminosity" data-location="Skyline Boulevard, Woodside, CA" ></div>
{/* Tactical Coordinate Grids (Inline SVG Overlay) */}
<svg className="absolute inset-0 w-full h-full pointer-events-none opacity-30" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern height="48" id="grid-pattern" patternUnits="userSpaceOnUse" width="48">
<path className="text-outline-variant" d="M 48 0 L 0 0 0 48" fill="none" stroke="currentColor" strokeWidth="0.5"></path>
<circle className="text-primary-fixed-dim" cx="0" cy="0" fill="currentColor" r="1.5"></circle>
</pattern>
</defs>
<rect fill="url(#grid-pattern)" height="100%" width="100%"></rect>
{/* Tactical Route Trace */}
<path className="text-primary-container" d="M 120 460 C 220 380, 260 410, 360 300 S 490 260, 560 170 S 710 160, 840 90" fill="none" stroke="currentColor" strokeDasharray="8 6" strokeWidth="4"></path>
{/* Safe Spacing Buffers */}
<circle className="text-secondary-fixed/40" cx="840" cy="90" fill="none" r="32" stroke="currentColor" strokeWidth="1.5"></circle>
<circle className="text-secondary-fixed/40" cx="710" cy="160" fill="none" r="28" stroke="currentColor" strokeWidth="1.5"></circle>
<circle className="text-error/50" cx="560" cy="170" fill="none" r="32" stroke="currentColor" strokeWidth="2"></circle>
<circle className="text-secondary-fixed/40" cx="360" cy="300" fill="none" r="26" stroke="currentColor" strokeWidth="1.5"></circle>
<circle className="text-error-container" cx="260" cy="410" fill="none" r="36" stroke="currentColor" strokeDasharray="4 2" strokeWidth="2.5"></circle>
<circle className="text-primary-container/50" cx="120" cy="460" fill="none" r="30" stroke="currentColor" strokeWidth="1.5"></circle>
</svg>
{/* 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 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">Staggered Dual Column [B]</span>
<span className="font-code-mono text-code-mono text-secondary-fixed">Paceline Lock: Optimal</span>
</div>
{/* Map HUD Top Right: Telemetry Ping */}
<div className="bg-surface/90 backdrop-blur-md px-space-md py-2 rounded-xl shadow-lg flex items-center gap-space-sm">
<div className="text-right">
<span className="font-label-caps text-label-caps text-on-surface-variant block">GPS Frequency</span>
<span className="font-code-mono text-code-mono text-on-surface">10.0 Hz • GNSS Lock</span>
</div>
<span className="material-symbols-outlined text-secondary-fixed text-[24px]">satellite_alt</span>
</div>
</div>
{/* Visual Pin Indicators plotted across the canyon trace */}
<div className="relative z-10 my-auto pointer-events-none">
{/* Lead Rider Marker */}
<div className="absolute -top-32 right-12 flex items-center gap-space-xs bg-surface-container-high/90 px-2.5 py-1 rounded shadow-md pointer-events-auto cursor-pointer hover:bg-surface-bright">
<span className="w-2.5 h-2.5 rounded-full bg-secondary-fixed animate-pulse"></span>
<span className="font-code-mono text-code-mono text-on-surface font-bold">1 • ApexLead</span>
<span className="font-label-caps text-label-caps text-secondary-fixed">68 MPH</span>
</div>
{/* Hard Braking Warning Marker (Rider 5) */}
<div className="absolute bottom-12 left-1/3 flex items-center gap-space-xs bg-error-container/90 px-2.5 py-1.5 rounded shadow-xl pointer-events-auto">
<span className="material-symbols-outlined text-error text-[16px] animate-bounce">warning</span>
<span className="font-code-mono text-code-mono text-on-surface font-bold">5 • Alex_Ducati</span>
<span className="font-label-caps text-label-caps text-error">-1.2G Decel</span>
</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">
<div className="flex items-center gap-space-xs bg-surface/90 backdrop-blur-md p-1.5 rounded-xl">
<button className="px-space-sm py-1 rounded-lg bg-surface-container-highest text-primary font-label-caps text-label-caps">Tactical</button>
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps">Satellite</button>
<button className="px-space-sm py-1 rounded-lg hover:bg-surface-container text-on-surface-variant font-label-caps text-label-caps">Elevation 3D</button>
</div>
<div className="bg-surface/90 backdrop-blur-md px-space-md py-1.5 rounded-xl flex items-center gap-space-sm">
<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'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]">
<div className="flex items-center justify-between pb-space-xs mb-space-xs">
<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">6 Active</span>
</div>
{/* Rider 1: ApexLead */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
<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 bg-secondary-fixed"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">ApexLead</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-secondary-fixed">Pace Setter</span>
</div>
<span className="font-code-mono text-code-mono text-primary-fixed">Lead</span>
</div>
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
<div>Speed: <span className="text-on-surface font-bold">68 MPH</span></div>
<div>Lean: <span className="text-primary-container font-bold">42°</span></div>
<div>Throttle: <span className="text-on-surface">78%</span></div>
</div>
</div>
{/* Rider 2: Ghost99 */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
<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 bg-secondary-fixed"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">Ghost99</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Wingman L</span>
</div>
<span className="font-code-mono text-code-mono text-secondary-fixed">+45m Gap</span>
</div>
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
<div>Speed: <span className="text-on-surface">67 MPH</span></div>
<div>Lean: <span className="text-primary-container font-bold">40°</span></div>
<div>Brake: <span className="text-on-surface">0%</span></div>
</div>
</div>
{/* Rider 3: Sarah_RR (Warning: Tire Pressure) */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all relative overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-error"></div>
<div className="flex items-center justify-between mb-1 pl-1">
<div className="flex items-center gap-space-xs">
<span className="w-2 h-2 rounded-full bg-error animate-pulse"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">Sarah_RR</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Wingman R</span>
</div>
<span className="font-code-mono text-code-mono text-on-surface-variant">+110m Gap</span>
</div>
<div className="mt-2 pl-1">
<div className="flex items-center gap-1 text-error font-code-mono text-code-mono">
<span className="material-symbols-outlined text-[15px]">tire_repair</span>
<span>Alert: Rear tire 28 PSI (low)</span>
</div>
</div>
</div>
{/* Rider 4: Dan_KTM */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
<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 bg-secondary-fixed"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">Dan_KTM</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-on-surface-variant">Mid Pack</span>
</div>
<span className="font-code-mono text-code-mono text-secondary-fixed">+180m Gap</span>
</div>
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
<div>Speed: <span className="text-on-surface">63 MPH</span></div>
<div>Lean: <span className="text-on-surface">28°</span></div>
<div>Temp: <span className="text-on-surface">88°C</span></div>
</div>
</div>
{/* Rider 5: Alex_Ducati (Yellow Flag: Hard Braking) */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all relative overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-error-container"></div>
<div className="flex items-center justify-between mb-1 pl-1">
<div className="flex items-center gap-space-xs">
<span className="w-2 h-2 rounded-full bg-error"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">Alex_Ducati</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-error">Decel Event</span>
</div>
<span className="font-code-mono text-code-mono text-on-surface-variant">+320m Gap</span>
</div>
<div className="mt-2 pl-1 flex items-center justify-between font-code-mono text-code-mono">
<span className="text-error font-bold">Hard Braking (-1.2G)</span>
<span className="text-on-surface-variant">ABS: Engaged</span>
</div>
</div>
{/* Rider 6: Sweep_Marshal */}
<div className="p-space-sm rounded-xl bg-surface-container-high hover:bg-surface-container-highest transition-all">
<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 bg-primary-container"></span>
<span className="font-headline-sm text-headline-sm text-on-surface">Sweep_Marshal</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container-lowest font-label-caps text-label-caps text-primary-container">Rear Guard</span>
</div>
<span className="font-code-mono text-code-mono text-primary-fixed-dim">+450m Gap</span>
</div>
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
<div>Speed: <span className="text-on-surface">59 MPH</span></div>
<div>Radio: <span className="text-secondary-fixed">Open</span></div>
<div>Pack Lock: <span className="text-primary">100%</span></div>
</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">
{/* 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" ></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's Restaurant Checkpoint</span>
</div>
</div>
</div>
<LivePackRadar initialMode="tactical" />
</section>
{/* Group Ride Features Grid */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<div className="flex flex-col mb-space-2xl">
<span className="font-label-caps text-label-caps text-primary-container tracking-wide mb-1">Cockpit Architecture</span>
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">Engineered for Squadron Cohesion</h2>
<p className="font-body-md text-body-md text-on-surface-variant max-w-2xl">High-frequency mesh communications keep the entire formation synchronized through blind apexes, switchbacks, and offline mountain passes.</p>
<span className="font-label-caps text-label-caps text-primary-container tracking-wide mb-1">
Cockpit Architecture
</span>
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
Engineered for Squadron Cohesion
</h2>
<p className="font-body-md text-body-md text-on-surface-variant max-w-2xl">
High-frequency mesh communications keep the entire formation synchronized through blind
apexes, switchbacks, and offline mountain passes.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-space-lg">
{/* Feature 1 */}
<div className="bg-surface-container-low p-space-xl rounded-xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md">
<div className="bg-surface-container-low p-space-xl rounded-2xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md border border-outline-variant/20">
<div>
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-container mb-space-lg">
<span className="material-symbols-outlined text-[26px]">spatial_audio_off</span>
</div>
<span className="font-code-mono text-code-mono text-primary-fixed-dim block mb-1">01 / Paceline Audit</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Staggered Paceline Warnings</h3>
<span className="font-code-mono text-code-mono text-primary-fixed-dim block mb-1">
01 / Paceline Audit
</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">
Staggered Paceline Warnings
</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
Spatial audio HUD chimes dynamically sound whenever rider follow distance or lane positioning falls beneath calculated emergency stopping thresholds.
Spatial audio HUD chimes dynamically sound whenever rider follow distance or lane
positioning falls beneath calculated emergency stopping thresholds.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-container">
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-container border-t border-outline-variant/20">
<span>Response: &lt;12ms</span>
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
</div>
</div>
{/* Feature 2 */}
<div className="bg-surface-container-low p-space-xl rounded-xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md">
<div className="bg-surface-container-low p-space-xl rounded-2xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md border border-outline-variant/20">
<div>
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-error mb-space-lg">
<span className="material-symbols-outlined text-[26px]">fmd_bad</span>
</div>
<span className="font-code-mono text-code-mono text-error block mb-1">02 / Swarm Broadcast</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Crash &amp; Separation Alerts</h3>
<span className="font-code-mono text-code-mono text-error block mb-1">
02 / Swarm Broadcast
</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">
Crash &amp; Separation Alerts
</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
Autonomous IMU-based impact detection pings the entire pack instantly. Visual yellow and red telemetry flags highlight precise coordinates for immediate regrouping.
Autonomous IMU-based impact detection pings the entire pack instantly. Visual yellow and
red telemetry flags highlight precise coordinates for immediate regrouping.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-error">
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-error border-t border-outline-variant/20">
<span>IMU Sensing: 6-Axis</span>
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
</div>
</div>
{/* Feature 3 */}
<div className="bg-surface-container-low p-space-xl rounded-xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md">
<div className="bg-surface-container-low p-space-xl rounded-2xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md border border-outline-variant/20">
<div>
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-secondary-fixed mb-space-lg">
<span className="material-symbols-outlined text-[26px]">hub</span>
</div>
<span className="font-code-mono text-code-mono text-secondary-fixed block mb-1">03 / Offline Link</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Peer-to-Peer Mesh Sync</h3>
<span className="font-code-mono text-code-mono text-secondary-fixed block mb-1">
03 / Offline Link
</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">
Peer-to-Peer Mesh Sync
</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
Never rely on cellular towers in deep canyons. High-throughput ad-hoc Bluetooth and Wi-Fi Direct protocols daisy-chain vital bike metrics from lead to sweep.
Never rely on cellular towers in deep canyons. High-throughput ad-hoc Bluetooth and Wi-Fi
Direct protocols daisy-chain vital bike metrics from lead to sweep.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-secondary-fixed">
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-secondary-fixed border-t border-outline-variant/20">
<span>Zero-cell Ready</span>
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
</div>
</div>
{/* Feature 4 */}
<div className="bg-surface-container-low p-space-xl rounded-xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md">
<div className="bg-surface-container-low p-space-xl rounded-2xl flex flex-col justify-between hover:bg-surface-container transition-all shadow-md border border-outline-variant/20">
<div>
<div className="w-12 h-12 rounded-xl bg-surface-container-high flex items-center justify-center text-primary-fixed mb-space-lg">
<span className="material-symbols-outlined text-[26px]">history_toggle_off</span>
</div>
<span className="font-code-mono text-code-mono text-primary-fixed block mb-1">04 / Mission Debrief</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">Group Replay &amp; Debrief</h3>
<span className="font-code-mono text-code-mono text-primary-fixed block mb-1">
04 / Mission Debrief
</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-sm">
Group Replay &amp; Debrief
</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
Relive the ride with synced multi-bike timelines. Compare apex apex entries, sector speeds, and individual lean angles on an interactive 3D map renderer.
Relive the ride with synced multi-bike timelines. Compare apex entries, sector speeds,
and individual lean angles on an interactive 3D map renderer.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-fixed">
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-fixed border-t border-outline-variant/20">
<span>3D Replay Engine</span>
<span className="material-symbols-outlined text-[16px]">arrow_forward</span>
</div>
</div>
</div>
</section>
{/* Visual Divider / Action Interstitial */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<div className="relative bg-surface-container-low rounded-xl overflow-hidden shadow-2xl p-space-2xl">
<div className="relative bg-surface-container-low rounded-2xl overflow-hidden shadow-2xl p-space-2xl border border-outline-variant/20">
<div className="absolute -right-16 -bottom-16 w-80 h-80 bg-primary-container/10 rounded-full blur-3xl pointer-events-none"></div>
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-space-xl">
<div className="flex flex-col gap-space-xs max-w-xl">
<span className="font-label-caps text-label-caps text-secondary-fixed">Hardware Compatibility</span>
<h3 className="font-headline-lg text-headline-lg text-on-surface tracking-tight">Sync directly with your helmet comms &amp; bike dash</h3>
<span className="font-label-caps text-label-caps text-secondary-fixed">
Hardware Compatibility
</span>
<h3 className="font-headline-lg text-headline-lg text-on-surface tracking-tight">
Sync directly with your helmet comms &amp; bike dash
</h3>
<p className="font-body-md text-body-md text-on-surface-variant">
Integrates seamlessly with Cardo, Sena, Apple Watch, and OBD-II CAN bus links for live peg-to-peg data overlays.
Integrates seamlessly with Cardo, Sena, Apple Watch, and OBD-II CAN bus links for live
peg-to-peg data overlays.
</p>
</div>
<div className="flex items-center gap-space-md">
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high border border-outline-variant/20">
<span className="material-symbols-outlined text-secondary-fixed">headset_mic</span>
<span className="font-code-mono text-code-mono text-on-surface">Cardo / Sena HUD</span>
</div>
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high">
<div className="flex items-center gap-space-sm px-space-md py-3 rounded-xl bg-surface-container-high border border-outline-variant/20">
<span className="material-symbols-outlined text-primary-container">watch</span>
<span className="font-code-mono text-code-mono text-on-surface">Haptic Alerts</span>
</div>
@ -398,193 +280,23 @@ export default function GroupRidesPage() {
</div>
</div>
</section>
{/* Active Public & Regional Group Rides Directory */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<div className="flex flex-col md:flex-row md:items-end justify-between mb-space-xl gap-space-md">
<div>
<span className="font-label-caps text-label-caps text-primary-fixed-dim tracking-wide block mb-1">Regional Operations</span>
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">Active &amp; Upcoming Pack Runs</h2>
</div>
{/* Filter Toggles */}
<div className="flex flex-wrap items-center gap-space-xs bg-surface-container-low p-1 rounded-xl">
<button className="px-space-md py-1.5 rounded-lg bg-surface-container-highest text-on-surface font-label-md text-label-md">All Runs (14)</button>
<button className="px-space-md py-1.5 rounded-lg text-on-surface-variant hover:text-on-surface font-label-md text-label-md">Brisk Pace</button>
<button className="px-space-md py-1.5 rounded-lg text-on-surface-variant hover:text-on-surface font-label-md text-label-md">Track / Apex</button>
<button className="px-space-md py-1.5 rounded-lg text-on-surface-variant hover:text-on-surface font-label-md text-label-md">Weekend Tours</button>
</div>
</div>
{/* Search and Region Filters Bar */}
<div className="bg-surface-container-lowest p-space-md rounded-xl mb-space-lg flex flex-wrap items-center justify-between gap-space-md shadow-md">
<div className="flex items-center gap-space-sm flex-1 min-w-[280px]">
<span className="material-symbols-outlined text-on-surface-variant text-[20px]">search</span>
<input className="w-full bg-transparent font-body-md text-body-md text-on-surface placeholder:text-on-surface-variant outline-none" placeholder="Search by region, canyon, or road marshal..." type="text"/>
</div>
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-on-surface-variant">
<span>Sort by:</span>
<span className="text-primary-container cursor-pointer font-bold">Start Time ↓</span>
</div>
</div>
{/* Rides Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-space-lg">
{/* Ride 1 */}
<div className="bg-surface-container-low rounded-xl overflow-hidden hover:bg-surface-container transition-all flex flex-col justify-between shadow-xl">
<div>
<div className="relative h-44 w-full">
<div className="w-full h-full bg-cover bg-center" data-alt="High performance sport motorcycles carving through early morning mist on California mountain ridge highway with sweeping Pacific ocean view in dark cinematic tones" ></div>
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
Saturday • 06:30 AM PST
</div>
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-primary-container text-on-primary-container font-label-caps text-label-caps font-bold">
Track Pace
</div>
</div>
<div className="p-space-lg">
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
<span>San Francisco, CA</span>
<span>82.4 Miles</span>
</div>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Bay Area Dawn Patrol</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
Fast sweepers from Woodside down to San Gregorio beach before morning traffic. Strict staggered formation enforced.
</p>
<div className="flex items-center justify-between pt-space-sm bg-surface-container-high/60 p-space-sm rounded-xl">
<div className="flex items-center gap-space-xs">
<div className="w-6 h-6 rounded-full bg-primary-fixed-dim flex items-center justify-center text-on-primary-fixed font-bold text-[11px]">AL</div>
<span className="font-code-mono text-code-mono text-on-surface">Lead: ApexLead</span>
</div>
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
<span className="material-symbols-outlined text-[16px]">group</span>
<span>14/16 Riders</span>
</div>
</div>
</div>
</div>
<div className="p-space-lg pt-0">
<button className="w-full py-2.5 rounded-lg bg-surface-container-high hover:bg-primary-container hover:text-on-primary-container text-on-surface font-headline-sm text-headline-sm transition-all flex items-center justify-center gap-space-xs">
<span className="material-symbols-outlined text-[18px]">lock_open</span>
Request Pack Slot
</button>
</div>
</div>
{/* Ride 2 */}
<div className="bg-surface-container-low rounded-xl overflow-hidden hover:bg-surface-container transition-all flex flex-col justify-between shadow-xl">
<div>
<div className="relative h-44 w-full">
<div className="w-full h-full bg-cover bg-center" data-alt="Trio of supernaked Italian motorcycles parked at golden hour canyon overlook with asphalt switchbacks carving through dark mountains" ></div>
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
Sunday • 07:15 AM PST
</div>
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-surface-container-highest text-primary font-label-caps text-label-caps font-bold">
Brisk Pace
</div>
</div>
<div className="p-space-lg">
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
<span>Malibu, CA</span>
<span>64.0 Miles</span>
</div>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Malibu Canyons Fast Pace</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
Tuna Canyon descent to Latigo Canyon technical curves. Regroup checkpoint planned at Rock Store overlook.
</p>
<div className="flex items-center justify-between pt-space-sm bg-surface-container-high/60 p-space-sm rounded-xl">
<div className="flex items-center gap-space-xs">
<div className="w-6 h-6 rounded-full bg-tertiary-fixed-dim flex items-center justify-center text-on-tertiary font-bold text-[11px]">DK</div>
<span className="font-code-mono text-code-mono text-on-surface">Lead: Dan_KTM</span>
</div>
<div className="flex items-center gap-1 font-code-mono text-code-mono text-secondary-fixed">
<span className="material-symbols-outlined text-[16px]">group</span>
<span>9/12 Riders</span>
</div>
</div>
</div>
</div>
<div className="p-space-lg pt-0">
<button className="w-full py-2.5 rounded-lg bg-surface-container-high hover:bg-primary-container hover:text-on-primary-container text-on-surface font-headline-sm text-headline-sm transition-all flex items-center justify-center gap-space-xs">
<span className="material-symbols-outlined text-[18px]">lock_open</span>
Request Pack Slot
</button>
</div>
</div>
{/* Ride 3 */}
<div className="bg-surface-container-low rounded-xl overflow-hidden hover:bg-surface-container transition-all flex flex-col justify-between shadow-xl">
<div>
<div className="relative h-44 w-full">
<div className="w-full h-full bg-cover bg-center" data-alt="Dense green mountain pass winding asphalt curve with neon trail lights of aggressive sport bikes in moody dark atmospheric lighting" ></div>
<div className="absolute inset-0 bg-gradient-to-t from-surface-container-low via-surface-container-low/40 to-transparent"></div>
<div className="absolute top-3 left-3 px-2 py-1 rounded bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed">
Fri-Sun • Oct 12-14
</div>
<div className="absolute top-3 right-3 px-2 py-1 rounded bg-secondary-container text-on-secondary-container font-label-caps text-label-caps font-bold">
Endurance / Chill
</div>
</div>
<div className="p-space-lg">
<div className="flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono mb-1">
<span>Robbinsville, NC</span>
<span>318 Curves</span>
</div>
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs">Tail of the Dragon Run</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
11 miles, 318 technical curves. Telemetry debrief sessions every evening with custom apex analysis and gear logs.
</p>
<div className="flex items-center justify-between pt-space-sm bg-surface-container-high/60 p-space-sm rounded-xl">
<div className="flex items-center gap-space-xs">
<div className="w-6 h-6 rounded-full bg-secondary-fixed flex items-center justify-center text-on-secondary-fixed font-bold text-[11px]">SM</div>
<span className="font-code-mono text-code-mono text-on-surface">Lead: Sweep_Marshal</span>
</div>
<div className="flex items-center gap-1 font-code-mono text-code-mono text-error">
<span className="material-symbols-outlined text-[16px]">group</span>
<span>Full (24/24)</span>
</div>
</div>
</div>
</div>
<div className="p-space-lg pt-0">
<button className="w-full py-2.5 rounded-lg bg-surface-container text-on-surface-variant font-headline-sm text-headline-sm cursor-not-allowed flex items-center justify-center gap-space-xs">
<span className="material-symbols-outlined text-[18px]">hourglass_top</span>
Join Waitlist (3)
</button>
</div>
</div>
</div>
</section>
{/* Modal: Join Code Trigger Overlay (Interactive Script Attached) */}
<div className="fixed inset-0 bg-surface-container-lowest/80 backdrop-blur-md z-50 hidden flex items-center justify-center p-space-md" id="join-modal">
<div className="bg-surface-container-low max-w-md w-full rounded-xl p-space-xl shadow-2xl relative">
<div className="flex items-center justify-between mb-space-lg">
<div className="flex items-center gap-space-xs">
<span className="material-symbols-outlined text-primary-container">qr_code_scanner</span>
<h3 className="font-headline-md text-headline-md text-on-surface">Join Pack Formation</h3>
</div>
<button className="text-on-surface-variant hover:text-on-surface" id="close-modal">
<span className="material-symbols-outlined">close</span>
</button>
</div>
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-lg">
Enter the 6-character encrypted ride code generated by your Lead Pace Setter or scan their cockpit radar QR display.
</p>
<div className="flex flex-col gap-space-md mb-space-lg">
<input className="w-full bg-surface-container-lowest font-code-mono text-headline-sm text-center tracking-wide text-primary px-space-md py-3 rounded-lg outline-none placeholder:text-on-surface-variant" placeholder="Code: e.g. APEX84" type="text"/>
<div className="flex items-center gap-space-md text-on-surface-variant font-code-mono text-code-mono justify-center">
<span className="h-px bg-outline-variant flex-1"></span>
<span>Or Scan Camera</span>
<span className="h-px bg-outline-variant flex-1"></span>
</div>
<div className="bg-surface-container h-36 rounded-lg flex flex-col items-center justify-center gap-2 text-on-surface-variant hover:text-primary transition-colors cursor-pointer">
<span className="material-symbols-outlined text-[36px]">photo_camera</span>
<span className="font-code-mono text-code-mono">Activate Radar Lens</span>
</div>
</div>
<button className="w-full py-3 rounded-lg bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm">
Connect Squad Telemetry
</button>
</div>
</div>
</div>
{/* Active Public & Regional Group Rides Directory */}
<GroupRidesDirectory rides={rides} onRequestSlot={handleRequestSlot} />
{/* Modals */}
<JoinRideModal
isOpen={isJoinModalOpen}
onClose={() => setIsJoinModalOpen(false)}
onJoinSuccess={handleJoinSuccess}
/>
<CreateRideModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onCreateRide={handleCreateRide}
/>
</div>
</div>
</div>
);

View File

@ -0,0 +1,291 @@
'use client';
import React, { useState, useEffect } from 'react';
import { GroupRideSummary } from './types';
interface CreateRideModalProps {
isOpen: boolean;
onClose: () => void;
onCreateRide: (ride: GroupRideSummary) => void;
}
export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideModalProps) {
const [title, setTitle] = useState('');
const [region, setRegion] = useState('');
const [scheduledDate, setScheduledDate] = useState('Saturday');
const [scheduledTime, setScheduledTime] = useState('07:00 AM PST');
const [paceClassification, setPaceClassification] = useState<GroupRideSummary['paceClassification']>('Brisk Pace');
const [formationProfile, setFormationProfile] = useState('Staggered Dual Column [B]');
const [distance, setDistance] = useState('50.0 Miles');
const [meetingPoint, setMeetingPoint] = useState('');
const [participantLimit, setParticipantLimit] = useState(12);
const [description, setDescription] = useState('');
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// Reset form when modal opens
useEffect(() => {
if (isOpen) {
setTitle('');
setRegion('');
setScheduledDate('Saturday');
setScheduledTime('07:00 AM PST');
setPaceClassification('Brisk Pace');
setFormationProfile('Staggered Dual Column [B]');
setDistance('50.0 Miles');
setMeetingPoint('');
setParticipantLimit(12);
setDescription('');
setErrorMsg(null);
}
}, [isOpen]);
// Handle ESC key press
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
setErrorMsg('Please enter a ride title.');
return;
}
if (!region.trim()) {
setErrorMsg('Please enter a location or region.');
return;
}
const newRide: GroupRideSummary = {
id: `ride-${Date.now()}`,
title: title.trim(),
description: description.trim() || 'Custom squadron pack run with MotorSignal live telemetry.',
region: region.trim(),
scheduledDate,
scheduledTime,
paceClassification,
formationProfile,
joinPolicy: 'requestToJoin',
leadHandle: 'You (Organizer)',
leadAvatarText: 'YOU',
participantCount: 1,
participantLimit: Number(participantLimit) || 12,
distance: distance.trim() || '45.0 Miles',
meetingPoint: meetingPoint.trim() || 'Designated Rally Staging Point',
userStatus: 'joined',
};
onCreateRide(newRide);
onClose();
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="create-ride-title"
className="fixed inset-0 z-50 flex items-center justify-center p-space-md bg-surface-container-lowest/80 backdrop-blur-md overflow-y-auto"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="bg-surface-container-low max-w-lg w-full rounded-2xl p-space-xl shadow-2xl relative border border-outline-variant/30 flex flex-col my-8">
{/* Header */}
<div className="flex items-center justify-between mb-space-md">
<div className="flex items-center gap-space-xs">
<span className="material-symbols-outlined text-primary-container text-[24px]">
add_circle
</span>
<h3 id="create-ride-title" className="font-headline-md text-headline-md text-on-surface">
Create Group Ride Run
</h3>
</div>
<button
onClick={onClose}
className="text-on-surface-variant hover:text-on-surface p-1 rounded-lg hover:bg-surface-container transition-colors"
aria-label="Close modal"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
Organize a new squadron run with real-time radar tracking, safety spacing buffers, and synchronized telemetry check-ins.
</p>
{errorMsg && (
<div className="mb-space-md p-space-sm rounded-lg bg-error-container text-on-error-container font-code-mono text-code-mono flex items-center gap-2">
<span className="material-symbols-outlined text-[18px]">error</span>
<span>{errorMsg}</span>
</div>
)}
<form onSubmit={handleSubmit} className="flex flex-col gap-space-md">
{/* Title */}
<div>
<label htmlFor="ride-title-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Ride Title *
</label>
<input
id="ride-title-input"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. Sierra Foothills Canyon Run"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
{/* Region & Distance Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-region-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Region / City *
</label>
<input
id="ride-region-input"
type="text"
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="e.g. Sacramento, CA"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
<div>
<label htmlFor="ride-distance-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Route Distance
</label>
<input
id="ride-distance-input"
type="text"
value={distance}
onChange={(e) => setDistance(e.target.value)}
placeholder="e.g. 58.4 Miles"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Date & Time Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-date-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Scheduled Day
</label>
<input
id="ride-date-input"
type="text"
value={scheduledDate}
onChange={(e) => setScheduledDate(e.target.value)}
placeholder="e.g. Saturday"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
<div>
<label htmlFor="ride-time-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Start Time
</label>
<input
id="ride-time-input"
type="text"
value={scheduledTime}
onChange={(e) => setScheduledTime(e.target.value)}
placeholder="e.g. 06:45 AM PST"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Pace & Formation */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-space-md">
<div>
<label htmlFor="ride-pace-select" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Pace Classification
</label>
<select
id="ride-pace-select"
value={paceClassification}
onChange={(e) => setPaceClassification(e.target.value as any)}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
>
<option value="Track Pace">Track Pace</option>
<option value="Brisk Pace">Brisk Pace</option>
<option value="Endurance / Chill">Endurance / Chill</option>
<option value="Weekend Tours">Weekend Tours</option>
</select>
</div>
<div>
<label htmlFor="ride-limit-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Rider Limit
</label>
<input
id="ride-limit-input"
type="number"
min={2}
max={50}
value={participantLimit}
onChange={(e) => setParticipantLimit(Number(e.target.value))}
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
</div>
{/* Meeting Point */}
<div>
<label htmlFor="ride-meeting-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Meeting Point / Staging Location
</label>
<input
id="ride-meeting-input"
type="text"
value={meetingPoint}
onChange={(e) => setMeetingPoint(e.target.value)}
placeholder="e.g. Chevron Staging Lot at Highway 49"
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2.5 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none"
/>
</div>
{/* Description */}
<div>
<label htmlFor="ride-desc-input" className="block font-label-caps text-label-caps text-on-surface-variant mb-1">
Description &amp; Safety Briefing
</label>
<textarea
id="ride-desc-input"
rows={3}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Formation rules, fuel stop locations, radio channels..."
className="w-full bg-surface-container-lowest font-body-md text-body-md text-on-surface px-space-md py-2 rounded-xl border border-outline-variant/30 focus:border-primary-container outline-none resize-none"
/>
</div>
{/* Buttons */}
<div className="flex items-center justify-end gap-space-sm pt-space-sm">
<button
type="button"
onClick={onClose}
className="px-space-lg py-2.5 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface font-headline-sm text-headline-sm transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-space-xl py-2.5 rounded-xl bg-primary-container text-on-primary-container font-headline-sm text-headline-sm hover:shadow-lg transition-all"
>
Publish Group Ride
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,189 @@
import { GroupRideSummary, RadarRiderTelemetry } from './types';
export const INITIAL_RADAR_RIDERS: RadarRiderTelemetry[] = [
{
id: 'rider-1',
callsign: 'ApexLead',
role: 'Pace Setter',
roleBadge: 'Lead',
speedMph: 68,
leanAngleDeg: 42,
throttlePct: 78,
brakePct: 0,
gapMeters: 0,
gapDeltaSec: 0,
color: '#00F2FE', // cyan
status: 'active',
coordinates: { x: 84, y: 18 },
},
{
id: 'rider-2',
callsign: 'Ghost99',
role: 'Wingman L',
roleBadge: 'Wing L',
speedMph: 67,
leanAngleDeg: 40,
throttlePct: 74,
brakePct: 0,
gapMeters: 45,
gapDeltaSec: 0.4,
color: '#00F2FE',
status: 'active',
coordinates: { x: 71, y: 32 },
},
{
id: 'rider-3',
callsign: 'Sarah_RR',
role: 'Wingman R',
roleBadge: 'Wing R',
speedMph: 65,
leanAngleDeg: 36,
throttlePct: 62,
brakePct: 0,
gapMeters: 110,
gapDeltaSec: 0.9,
color: '#FF6B6B',
status: 'warning',
alertText: 'Rear tire 28 PSI (low)',
coordinates: { x: 56, y: 34 },
},
{
id: 'rider-4',
callsign: 'Dan_KTM',
role: 'Mid Pack',
roleBadge: 'Mid',
speedMph: 63,
leanAngleDeg: 28,
throttlePct: 58,
brakePct: 0,
gapMeters: 180,
gapDeltaSec: 1.4,
color: '#00F2FE',
status: 'active',
coordinates: { x: 36, y: 60 },
},
{
id: 'rider-5',
callsign: 'Alex_Ducati',
role: 'Mid Pack',
roleBadge: 'Decel Event',
speedMph: 48,
leanAngleDeg: 14,
throttlePct: 0,
brakePct: 85,
gapMeters: 320,
gapDeltaSec: 2.5,
color: '#FF4444',
status: 'alert',
alertText: 'Hard Braking (-1.2G) ABS Engaged',
coordinates: { x: 26, y: 78 },
},
{
id: 'rider-6',
callsign: 'Sweep_Marshal',
role: 'Rear Guard',
roleBadge: 'Sweep',
speedMph: 59,
leanAngleDeg: 22,
throttlePct: 52,
brakePct: 0,
gapMeters: 450,
gapDeltaSec: 3.8,
color: '#39FF14', // neon green
status: 'sweeper',
coordinates: { x: 12, y: 88 },
},
];
export const INITIAL_GROUP_RIDES: GroupRideSummary[] = [
{
id: 'ride-bay-dawn-patrol',
title: 'Bay Area Dawn Patrol',
description: 'Fast sweepers from Woodside down to San Gregorio beach before morning traffic. Strict staggered formation enforced.',
region: 'San Francisco, CA',
scheduledDate: 'Saturday',
scheduledTime: '06:30 AM PST',
paceClassification: 'Track Pace',
formationProfile: 'Staggered Dual Column [B]',
joinPolicy: 'requestToJoin',
leadHandle: 'ApexLead',
leadAvatarText: 'AL',
participantCount: 14,
participantLimit: 16,
distance: '82.4 Miles',
meetingPoint: "Alice's Restaurant, Skyline Blvd",
userStatus: 'none',
},
{
id: 'ride-malibu-canyons',
title: 'Malibu Canyons Fast Pace',
description: 'Tuna Canyon descent to Latigo Canyon technical curves. Regroup checkpoint planned at Rock Store overlook.',
region: 'Malibu, CA',
scheduledDate: 'Sunday',
scheduledTime: '07:15 AM PST',
paceClassification: 'Brisk Pace',
formationProfile: 'Staggered Paceline',
joinPolicy: 'requestToJoin',
leadHandle: 'Dan_KTM',
leadAvatarText: 'DK',
participantCount: 9,
participantLimit: 12,
distance: '64.0 Miles',
meetingPoint: 'Rock Store Overlook',
userStatus: 'none',
},
{
id: 'ride-tail-of-dragon',
title: 'Tail of the Dragon Run',
description: '11 miles, 318 technical curves. Telemetry debrief sessions every evening with custom apex analysis and gear logs.',
region: 'Robbinsville, NC',
scheduledDate: 'Fri-Sun',
scheduledTime: 'Oct 12-14',
paceClassification: 'Endurance / Chill',
formationProfile: 'Dynamic Paceline',
joinPolicy: 'open',
leadHandle: 'Sweep_Marshal',
leadAvatarText: 'SM',
participantCount: 24,
participantLimit: 24,
distance: '318 Curves',
meetingPoint: 'Deal’s Gap Motorcycle Resort',
userStatus: 'none',
},
{
id: 'ride-angeles-crest',
title: 'Angeles Crest Sunrise Ridge',
description: 'Sweeping high-altitude pavement through the San Gabriel Mountains. Moderate pace with designated cooldown turnouts.',
region: 'La Cañada Flintridge, CA',
scheduledDate: 'Saturday',
scheduledTime: '06:00 AM PST',
paceClassification: 'Brisk Pace',
formationProfile: 'Staggered Dual Column',
joinPolicy: 'open',
leadHandle: 'Ghost99',
leadAvatarText: 'G9',
participantCount: 7,
participantLimit: 10,
distance: '52.8 Miles',
meetingPoint: 'Shell Station at Foothill Blvd',
userStatus: 'none',
},
{
id: 'ride-lolo-pass',
title: 'Lolo Pass Transcontinental Tour',
description: 'Scenic sweepers following the Lochsa River through Idaho into Montana. Open touring formation with radio check-ins.',
region: 'Missoula, MT',
scheduledDate: 'Weekend',
scheduledTime: 'Aug 18-20',
paceClassification: 'Weekend Tours',
formationProfile: 'Open Touring Formation',
joinPolicy: 'open',
leadHandle: 'Sarah_RR',
leadAvatarText: 'SR',
participantCount: 6,
participantLimit: 15,
distance: '134.5 Miles',
meetingPoint: 'Lolo Hot Springs Rest Area',
userStatus: 'none',
},
];

View File

@ -0,0 +1,334 @@
'use client';
import React, { useState, useMemo } from 'react';
import { GroupRideSummary, RidePaceFilter, RideSortOption } from './types';
interface GroupRidesDirectoryProps {
rides: GroupRideSummary[];
onRequestSlot: (rideId: string) => void;
}
export function GroupRidesDirectory({ rides, onRequestSlot }: GroupRidesDirectoryProps) {
const [filterPace, setFilterPace] = useState<RidePaceFilter>('all');
const [searchQuery, setSearchQuery] = useState('');
const [sortOption, setSortOption] = useState<RideSortOption>('time');
// Count badges for filters
const counts = useMemo(() => {
return {
all: rides.length,
brisk: rides.filter((r) => r.paceClassification === 'Brisk Pace').length,
track: rides.filter((r) => r.paceClassification === 'Track Pace').length,
tour: rides.filter(
(r) =>
r.paceClassification === 'Weekend Tours' ||
r.paceClassification === 'Endurance / Chill'
).length,
};
}, [rides]);
// Filtered & sorted rides
const filteredRides = useMemo(() => {
let result = rides.filter((ride) => {
// Pace classification filter
if (filterPace === 'brisk' && ride.paceClassification !== 'Brisk Pace') return false;
if (filterPace === 'track' && ride.paceClassification !== 'Track Pace') return false;
if (
filterPace === 'tour' &&
ride.paceClassification !== 'Weekend Tours' &&
ride.paceClassification !== 'Endurance / Chill'
)
return false;
// Search query filter
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const matchesTitle = ride.title.toLowerCase().includes(q);
const matchesRegion = ride.region.toLowerCase().includes(q);
const matchesLead = ride.leadHandle.toLowerCase().includes(q);
const matchesDesc = ride.description.toLowerCase().includes(q);
if (!matchesTitle && !matchesRegion && !matchesLead && !matchesDesc) return false;
}
return true;
});
// Sorting
result.sort((a, b) => {
if (sortOption === 'distance') {
const distA = parseFloat(a.distance) || 0;
const distB = parseFloat(b.distance) || 0;
return distB - distA;
}
if (sortOption === 'riders') {
return b.participantCount - a.participantCount;
}
// default: time / alphabetical id
return a.title.localeCompare(b.title);
});
return result;
}, [rides, filterPace, searchQuery, sortOption]);
return (
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl" data-testid="group-rides-directory">
<div className="flex flex-col md:flex-row md:items-end justify-between mb-space-xl gap-space-md">
<div>
<span className="font-label-caps text-label-caps text-primary-fixed-dim tracking-wide block mb-1">
Regional Operations
</span>
<h2 className="font-headline-xl text-headline-xl text-on-surface tracking-tight">
Active &amp; Upcoming Pack Runs
</h2>
</div>
{/* Filter Toggles */}
<div className="flex flex-wrap items-center gap-space-xs bg-surface-container-low p-1 rounded-xl border border-outline-variant/20">
<button
type="button"
onClick={() => setFilterPace('all')}
className={`px-space-md py-1.5 rounded-lg font-label-md text-label-md transition-all ${
filterPace === 'all'
? 'bg-surface-container-highest text-on-surface font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
All Runs ({counts.all})
</button>
<button
type="button"
onClick={() => setFilterPace('brisk')}
className={`px-space-md py-1.5 rounded-lg font-label-md text-label-md transition-all ${
filterPace === 'brisk'
? 'bg-surface-container-highest text-on-surface font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Brisk Pace ({counts.brisk})
</button>
<button
type="button"
onClick={() => setFilterPace('track')}
className={`px-space-md py-1.5 rounded-lg font-label-md text-label-md transition-all ${
filterPace === 'track'
? 'bg-surface-container-highest text-on-surface font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Track / Apex ({counts.track})
</button>
<button
type="button"
onClick={() => setFilterPace('tour')}
className={`px-space-md py-1.5 rounded-lg font-label-md text-label-md transition-all ${
filterPace === 'tour'
? 'bg-surface-container-highest text-on-surface font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Weekend Tours ({counts.tour})
</button>
</div>
</div>
{/* Search and Region Filters Bar */}
<div className="bg-surface-container-lowest p-space-md rounded-xl mb-space-lg flex flex-wrap items-center justify-between gap-space-md shadow-md border border-outline-variant/30">
<div className="flex items-center gap-space-sm flex-1 min-w-[280px]">
<span className="material-symbols-outlined text-on-surface-variant text-[20px]">search</span>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by region, canyon, or road marshal..."
className="w-full bg-transparent font-body-md text-body-md text-on-surface placeholder:text-on-surface-variant outline-none"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="text-on-surface-variant hover:text-on-surface p-1 rounded"
title="Clear search"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
)}
</div>
<div className="flex items-center gap-space-sm font-code-mono text-code-mono text-on-surface-variant">
<span>Sort by:</span>
<select
value={sortOption}
onChange={(e) => setSortOption(e.target.value as RideSortOption)}
className="bg-surface-container px-2 py-1 rounded text-primary-container font-bold outline-none cursor-pointer border border-outline-variant/20"
>
<option value="time">Start Time ↓</option>
<option value="distance">Distance ↓</option>
<option value="riders">Pack Capacity ↓</option>
</select>
</div>
</div>
{/* Rides Cards Grid */}
{filteredRides.length === 0 ? (
<div className="bg-surface-container-low p-space-2xl rounded-2xl text-center border border-outline-variant/30 my-8">
<span className="material-symbols-outlined text-[48px] text-on-surface-variant mb-2">
search_off
</span>
<h3 className="font-headline-md text-headline-md text-on-surface mb-2">No Matching Pack Runs</h3>
<p className="font-body-md text-body-md text-on-surface-variant max-w-md mx-auto mb-4">
No group rides matched your search for &quot;{searchQuery}&quot;. Try adjusting your filters or organize a new group ride.
</p>
<button
onClick={() => {
setSearchQuery('');
setFilterPace('all');
}}
className="px-space-lg py-2 rounded-xl bg-surface-container-high text-primary font-headline-sm text-headline-sm hover:bg-surface-container-highest transition-colors"
>
Reset Filters
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-space-lg">
{filteredRides.map((ride) => {
const isFull = ride.participantCount >= ride.participantLimit;
const isJoined = ride.userStatus === 'joined';
const isRequested = ride.userStatus === 'requested';
const isWaitlisted = ride.userStatus === 'waitlisted';
return (
<div
key={ride.id}
className="bg-surface-container-low rounded-2xl overflow-hidden hover:bg-surface-container transition-all flex flex-col justify-between shadow-xl border border-outline-variant/20 group"
data-testid={`ride-card-${ride.id}`}
>
<div>
{/* Card Header Banner */}
<div className="relative h-44 w-full bg-gradient-to-br from-[#0F1D27] to-[#0A131B] border-b border-outline-variant/20 overflow-hidden">
{/* Atmospheric decorative lines */}
<div className="absolute inset-0 opacity-20 pointer-events-none">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 140 Q 150 40 300 120 T 600 60" fill="none" stroke="#00F2FE" strokeWidth="2" strokeDasharray="6 4" />
</svg>
</div>
<div className="absolute top-3 left-3 px-2.5 py-1 rounded-lg bg-surface/80 backdrop-blur font-label-caps text-label-caps text-secondary-fixed border border-secondary-fixed/30 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">calendar_today</span>
{ride.scheduledDate} • {ride.scheduledTime}
</div>
<div
className={`absolute top-3 right-3 px-2.5 py-1 rounded-lg font-label-caps text-label-caps font-bold ${
ride.paceClassification === 'Track Pace'
? 'bg-primary-container text-on-primary-container'
: ride.paceClassification === 'Brisk Pace'
? 'bg-surface-container-highest text-primary border border-primary/30'
: 'bg-secondary-container text-on-secondary-container'
}`}
>
{ride.paceClassification}
</div>
<div className="absolute bottom-3 left-3 right-3 flex items-center justify-between text-on-surface-variant font-code-mono text-code-mono text-xs bg-surface/60 backdrop-blur px-2.5 py-1 rounded-lg">
<span className="flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">location_on</span>
{ride.region}
</span>
<span className="text-primary font-bold">{ride.distance}</span>
</div>
</div>
{/* Card Body */}
<div className="p-space-lg">
<h3 className="font-headline-md text-headline-md text-on-surface mb-space-xs group-hover:text-primary transition-colors">
{ride.title}
</h3>
<p className="font-body-sm text-body-sm text-on-surface-variant line-clamp-2 mb-space-md">
{ride.description}
</p>
<div className="text-xs font-code-mono text-on-surface-variant mb-space-md flex items-center gap-1 truncate">
<span className="material-symbols-outlined text-[14px] text-secondary-fixed">
flag
</span>
<span className="truncate">Rally: {ride.meetingPoint}</span>
</div>
{/* Lead Marshal & Participant Indicator */}
<div className="flex items-center justify-between pt-space-sm bg-surface-container-high/60 p-space-sm rounded-xl border border-outline-variant/20">
<div className="flex items-center gap-space-xs">
<div className="w-6 h-6 rounded-full bg-primary-fixed-dim flex items-center justify-center text-on-primary-fixed font-bold text-[11px]">
{ride.leadAvatarText}
</div>
<span className="font-code-mono text-code-mono text-on-surface">
Lead: {ride.leadHandle}
</span>
</div>
<div
className={`flex items-center gap-1 font-code-mono text-code-mono ${
isFull ? 'text-error font-bold' : 'text-secondary-fixed'
}`}
>
<span className="material-symbols-outlined text-[16px]">group</span>
<span>
{isFull
? `Full (${ride.participantCount}/${ride.participantLimit})`
: `${ride.participantCount}/${ride.participantLimit} Riders`}
</span>
</div>
</div>
</div>
</div>
{/* Action Button */}
<div className="p-space-lg pt-0">
{isJoined ? (
<button
disabled
className="w-full py-2.5 rounded-xl bg-secondary-fixed/20 border border-secondary-fixed/40 text-secondary-fixed font-headline-sm text-headline-sm flex items-center justify-center gap-space-xs cursor-default"
>
<span className="material-symbols-outlined text-[18px]">check_circle</span>
Joined Pack (Slot Confirmed)
</button>
) : isRequested ? (
<button
disabled
className="w-full py-2.5 rounded-xl bg-primary-container/20 border border-primary-container/40 text-primary-container font-headline-sm text-headline-sm flex items-center justify-center gap-space-xs cursor-default"
>
<span className="material-symbols-outlined text-[18px]">pending</span>
Slot Requested (Pending Lead)
</button>
) : isWaitlisted ? (
<button
disabled
className="w-full py-2.5 rounded-xl bg-surface-container text-on-surface-variant font-headline-sm text-headline-sm flex items-center justify-center gap-space-xs cursor-default"
>
<span className="material-symbols-outlined text-[18px]">hourglass_top</span>
Waitlisted (Standby)
</button>
) : isFull ? (
<button
onClick={() => onRequestSlot(ride.id)}
className="w-full py-2.5 rounded-xl bg-surface-container hover:bg-surface-container-high text-on-surface-variant hover:text-on-surface font-headline-sm text-headline-sm transition-all flex items-center justify-center gap-space-xs"
>
<span className="material-symbols-outlined text-[18px]">hourglass_top</span>
Join Waitlist ({ride.participantLimit} Cap)
</button>
) : (
<button
onClick={() => onRequestSlot(ride.id)}
className="w-full py-2.5 rounded-xl bg-surface-container-high hover:bg-primary-container hover:text-on-primary-container text-on-surface font-headline-sm text-headline-sm transition-all flex items-center justify-center gap-space-xs shadow-sm hover:shadow-[0_0_16px_rgba(0,242,254,0.3)]"
>
<span className="material-symbols-outlined text-[18px]">lock_open</span>
Request Pack Slot
</button>
)}
</div>
</div>
);
})}
</div>
)}
</section>
);
}

View File

@ -0,0 +1,255 @@
'use client';
import React, { useState, useEffect } from 'react';
interface JoinRideModalProps {
isOpen: boolean;
onClose: () => void;
onJoinSuccess: (code: string, rideTitle: string) => void;
}
export function JoinRideModal({ isOpen, onClose, onJoinSuccess }: JoinRideModalProps) {
const [code, setCode] = useState('');
const [isCameraActive, setIsCameraActive] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [successInfo, setSuccessInfo] = useState<{ title: string; lead: string } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Reset state when modal is opened/closed
useEffect(() => {
if (isOpen) {
setCode('');
setErrorMsg(null);
setSuccessInfo(null);
setIsCameraActive(false);
setIsSubmitting(false);
}
}, [isOpen]);
// Handle ESC key press
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.toUpperCase().replace(/[^A-Z0-9_-]/g, '');
setCode(raw);
if (errorMsg) setErrorMsg(null);
};
const processCode = (rideCode: string) => {
const cleanCode = rideCode.trim().toUpperCase();
if (!cleanCode || cleanCode.length < 4) {
setErrorMsg('Please enter a valid ride code (at least 4 characters).');
return;
}
setIsSubmitting(true);
setErrorMsg(null);
// Simulated lookup
setTimeout(() => {
setIsSubmitting(false);
let resolvedTitle = 'Squad Formation Ride';
let resolvedLead = 'ApexLead';
if (cleanCode.includes('APEX') || cleanCode === 'CA-84' || cleanCode === 'APEX84') {
resolvedTitle = 'Bay Area Dawn Patrol';
resolvedLead = 'ApexLead';
} else if (cleanCode.includes('MALIBU') || cleanCode === 'MALIBU7') {
resolvedTitle = 'Malibu Canyons Fast Pace';
resolvedLead = 'Dan_KTM';
} else if (cleanCode.includes('DRAGON') || cleanCode === 'DRAGON318') {
resolvedTitle = 'Tail of the Dragon Run';
resolvedLead = 'Sweep_Marshal';
} else {
resolvedTitle = `Squad Run #${cleanCode}`;
resolvedLead = 'Pack Marshal';
}
setSuccessInfo({ title: resolvedTitle, lead: resolvedLead });
onJoinSuccess(cleanCode, resolvedTitle);
}, 400);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
processCode(code);
};
const handleSimulateScan = () => {
setCode('APEX84');
processCode('APEX84');
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="join-pack-title"
className="fixed inset-0 z-50 flex items-center justify-center p-space-md bg-surface-container-lowest/80 backdrop-blur-md"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="bg-surface-container-low max-w-md w-full rounded-2xl p-space-xl shadow-2xl relative border border-outline-variant/30 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-space-md">
<div className="flex items-center gap-space-xs">
<span className="material-symbols-outlined text-primary-container text-[24px]">
qr_code_scanner
</span>
<h3 id="join-pack-title" className="font-headline-md text-headline-md text-on-surface">
Join Pack Formation
</h3>
</div>
<button
onClick={onClose}
className="text-on-surface-variant hover:text-on-surface p-1 rounded-lg hover:bg-surface-container transition-colors"
aria-label="Close modal"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
{/* Success State */}
{successInfo ? (
<div className="flex flex-col items-center text-center py-4">
<div className="w-14 h-14 rounded-full bg-secondary-fixed/20 text-secondary-fixed flex items-center justify-center mb-3">
<span className="material-symbols-outlined text-[32px]">check_circle</span>
</div>
<span className="font-label-caps text-label-caps text-secondary-fixed mb-1">
Telemetry Linked
</span>
<h4 className="font-headline-md text-headline-md text-on-surface mb-1">
{successInfo.title}
</h4>
<p className="font-body-sm text-body-sm text-on-surface-variant mb-6">
Lead Marshal: <span className="text-on-surface font-bold">{successInfo.lead}</span>.
Your telemetry stream is now synchronized with this pack.
</p>
<button
onClick={onClose}
className="w-full py-3 rounded-xl bg-primary-container text-on-primary-container font-headline-sm text-headline-sm hover:shadow-lg transition-all"
>
Open Pack Radar
</button>
</div>
) : (
/* Input / Scanner Flow */
<form onSubmit={handleSubmit} className="flex flex-col">
<p className="font-body-sm text-body-sm text-on-surface-variant mb-space-md">
Enter the encrypted 6-character ride code generated by your Lead Pace Setter or scan their cockpit radar QR display.
</p>
{/* Error Message */}
{errorMsg && (
<div className="mb-space-md p-space-sm rounded-lg bg-error-container text-on-error-container font-code-mono text-code-mono flex items-center gap-2">
<span className="material-symbols-outlined text-[18px]">error</span>
<span>{errorMsg}</span>
</div>
)}
{/* Code Input */}
<div className="flex flex-col gap-space-md mb-space-lg">
<div className="relative">
<input
aria-label="Ride Code"
type="text"
maxLength={10}
value={code}
onChange={handleCodeChange}
placeholder="e.g. APEX84"
className="w-full bg-surface-container-lowest font-code-mono text-headline-md text-center tracking-widest text-primary px-space-md py-3 rounded-xl outline-none border border-outline-variant/30 focus:border-primary-container focus:ring-1 focus:ring-primary-container placeholder:text-on-surface-variant/40"
autoFocus
/>
</div>
{/* Divider */}
<div className="flex items-center gap-space-md text-on-surface-variant font-code-mono text-code-mono justify-center text-xs">
<span className="h-px bg-outline-variant flex-1"></span>
<span>Or Scan Optical QR</span>
<span className="h-px bg-outline-variant flex-1"></span>
</div>
{/* Optical Lens / Camera Simulation */}
{isCameraActive ? (
<div
className="relative bg-black rounded-xl h-44 overflow-hidden border border-primary-container flex flex-col items-center justify-center p-3"
data-testid="radar-lens-viewfinder"
>
{/* Viewfinder crosshairs */}
<div className="absolute inset-4 border border-secondary-fixed/40 rounded pointer-events-none">
<div className="absolute top-0 left-0 w-3 h-3 border-t-2 border-l-2 border-secondary-fixed"></div>
<div className="absolute top-0 right-0 w-3 h-3 border-t-2 border-r-2 border-secondary-fixed"></div>
<div className="absolute bottom-0 left-0 w-3 h-3 border-b-2 border-l-2 border-secondary-fixed"></div>
<div className="absolute bottom-0 right-0 w-3 h-3 border-b-2 border-r-2 border-secondary-fixed"></div>
{/* Animated scanline */}
<div className="w-full h-0.5 bg-gradient-to-r from-transparent via-primary-container to-transparent animate-pulse mt-12"></div>
</div>
<span className="font-code-mono text-xs text-secondary-fixed mb-2 relative z-10">
Radar Optical Lens Active
</span>
<button
type="button"
onClick={handleSimulateScan}
className="relative z-10 px-3 py-1.5 rounded-lg bg-primary-container text-on-primary-container font-code-mono text-xs hover:bg-primary-fixed transition-colors"
>
Simulate QR Scan (APEX84)
</button>
<button
type="button"
onClick={() => setIsCameraActive(false)}
className="mt-2 text-on-surface-variant hover:text-on-surface text-xs font-code-mono underline"
>
Switch back to manual entry
</button>
</div>
) : (
<div
onClick={() => setIsCameraActive(true)}
className="bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 h-32 rounded-xl flex flex-col items-center justify-center gap-2 text-on-surface-variant hover:text-primary transition-all cursor-pointer group"
data-testid="activate-radar-lens-button"
>
<span className="material-symbols-outlined text-[32px] group-hover:scale-110 transition-transform text-primary-fixed-dim">
photo_camera
</span>
<span className="font-code-mono text-code-mono">Activate Radar Optical Lens</span>
</div>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3.5 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-headline-sm hover:shadow-[0_0_20px_rgba(0,242,254,0.3)] transition-all flex items-center justify-center gap-2 disabled:opacity-50"
>
{isSubmitting ? (
<>
<span className="material-symbols-outlined text-[20px] animate-spin">refresh</span>
Linking Telemetry...
</>
) : (
<>
<span className="material-symbols-outlined text-[20px]">sync_alt</span>
Connect Squad Telemetry
</>
)}
</button>
</form>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,484 @@
'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>
{/* Normal Telemetry Grid */}
<div className="grid grid-cols-3 gap-2 mt-2 font-code-mono text-code-mono text-on-surface-variant">
<div>
Speed: <span className="text-on-surface font-bold">{rider.speedMph} MPH</span>
</div>
<div>
Lean: <span className="text-primary-container font-bold">{rider.leanAngleDeg}°</span>
</div>
<div>
{rider.brakePct && rider.brakePct > 0 ? (
<span className="text-error font-bold">Brake: {rider.brakePct}%</span>
) : rider.throttlePct !== undefined ? (
<span>Throttle: {rider.throttlePct}%</span>
) : (
<span>Delta: ±{rider.gapDeltaSec}s</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>
);
}

View File

@ -0,0 +1,55 @@
export type RadarMapMode = 'tactical' | 'satellite' | 'elevation';
export type RidePaceFilter = 'all' | 'brisk' | 'track' | 'tour';
export type RideSortOption = 'time' | 'distance' | 'riders';
export interface RadarRiderTelemetry {
id: string;
callsign: string;
role: string;
roleBadge: string;
speedMph: number;
leanAngleDeg: number;
throttlePct?: number;
brakePct?: number;
gapMeters: number;
gapDeltaSec: number;
color: string;
status: 'active' | 'warning' | 'alert' | 'sweeper';
alertText?: string;
coordinates: { x: number; y: number }; // percentage on radar canvas (0-100)
}
export interface GroupRideSummary {
id: string;
title: string;
description: string;
region: string;
scheduledTime: string;
scheduledDate: string;
paceClassification: 'Track Pace' | 'Brisk Pace' | 'Endurance / Chill' | 'Weekend Tours';
formationProfile: string;
joinPolicy: 'open' | 'requestToJoin' | 'inviteOnly';
leadHandle: string;
leadAvatarText: string;
participantCount: number;
participantLimit: number;
distance: string;
meetingPoint: string;
userStatus?: 'none' | 'requested' | 'joined' | 'waitlisted';
}
export interface CreateRideFormData {
title: string;
region: string;
description: string;
paceClassification: GroupRideSummary['paceClassification'];
formationProfile: string;
joinPolicy: GroupRideSummary['joinPolicy'];
scheduledDate: string;
scheduledTime: string;
distance: string;
meetingPoint: string;
participantLimit: number;
}

View File

@ -0,0 +1,306 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import GroupRidesPage from '../app/group-rides/page';
import { LivePackRadar } from '../components/group-rides/live-pack-radar';
import { JoinRideModal } from '../components/group-rides/join-ride-modal';
import { CreateRideModal } from '../components/group-rides/create-ride-modal';
import { GroupRidesDirectory } from '../components/group-rides/group-rides-directory';
import { INITIAL_GROUP_RIDES } from '../components/group-rides/fixtures';
describe('Group Rides Interactive Page & Components', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
// ============================================================
// 1. LivePackRadar Component Tests
// ============================================================
describe('LivePackRadar', () => {
it('renders radar HUD, default tactical view, and all 6 rider telemetry cards', () => {
render(<LivePackRadar initialMode="tactical" />);
// Verify header and HUD
expect(screen.getByText(/Swarm Recon: Skyline Blvd > Coast Hwy/i)).toBeDefined();
expect(screen.getByText(/Session #CA-84-SW/i)).toBeDefined();
expect(screen.getByText(/6 Synchronized/i)).toBeDefined();
// Verify GPS Frequency is NOT present, but GNSS Multi-Band Mesh IS present
expect(screen.queryByText(/GPS Frequency/i)).toBeNull();
expect(screen.getByText(/GNSS Multi-Band Mesh/i)).toBeDefined();
expect(screen.getByText(/18 Satellites Locked/i)).toBeDefined();
// Verify all riders in the stream
expect(screen.getByTestId('telemetry-card-ApexLead')).toBeDefined();
expect(screen.getByTestId('telemetry-card-Ghost99')).toBeDefined();
expect(screen.getByTestId('telemetry-card-Sarah_RR')).toBeDefined();
expect(screen.getByTestId('telemetry-card-Dan_KTM')).toBeDefined();
expect(screen.getByTestId('telemetry-card-Alex_Ducati')).toBeDefined();
expect(screen.getByTestId('telemetry-card-Sweep_Marshal')).toBeDefined();
// Verify alert text
expect(screen.getByText(/Rear tire 28 PSI \(low\)/i)).toBeDefined();
expect(screen.getByText(/Hard Braking \(-1.2G\) ABS Engaged/i)).toBeDefined();
});
it('allows toggling between Tactical, Satellite, and Elevation 3D modes', () => {
render(<LivePackRadar initialMode="tactical" />);
const tacticalBtn = screen.getByRole('button', { name: /^tactical$/i });
const satelliteBtn = screen.getByRole('button', { name: /^satellite$/i });
const elevationBtn = screen.getByRole('button', { name: /^elevation 3d$/i });
// Click Satellite
fireEvent.click(satelliteBtn);
expect(screen.getByTestId('satellite-mode-view')).toBeDefined();
expect(screen.getByText(/High-Resolution Orbit Imagery/i)).toBeDefined();
// Click Elevation 3D
fireEvent.click(elevationBtn);
expect(screen.getByTestId('elevation-mode-view')).toBeDefined();
expect(screen.getByText(/Summit: 2,210 ft/i)).toBeDefined();
// Return to Tactical
fireEvent.click(tacticalBtn);
expect(screen.queryByTestId('satellite-mode-view')).toBeNull();
expect(screen.queryByTestId('elevation-mode-view')).toBeNull();
});
it('allows selecting riders from telemetry column or canvas markers', () => {
render(<LivePackRadar initialMode="tactical" />);
// Default selected is ApexLead
expect(screen.getByText(/Selected Node Inspector/i)).toBeDefined();
// Select Alex_Ducati via telemetry card
const alexCard = screen.getByTestId('telemetry-card-Alex_Ducati');
fireEvent.click(alexCard);
// Verify inspector reflects Alex_Ducati
const inspector = screen.getByTestId('selected-node-inspector');
expect(inspector.textContent).toContain('Alex_Ducati');
expect(inspector.textContent).toContain('Decel Event');
// Select Ghost99 via canvas marker
const ghostMarker = screen.getByTestId('radar-marker-Ghost99');
fireEvent.click(ghostMarker);
expect(inspector.textContent).toContain('Ghost99');
});
it('animates/wobbles speed on ping swarm button click', () => {
render(<LivePackRadar initialMode="tactical" />);
const pingBtn = screen.getByRole('button', { name: /ping swarm/i });
fireEvent.click(pingBtn);
// Still renders active telemetry correctly
expect(screen.getByText(/6 Synchronized/i)).toBeDefined();
});
});
// ============================================================
// 2. JoinRideModal Component Tests
// ============================================================
describe('JoinRideModal', () => {
it('handles open/close lifecycle and Escape key', () => {
const onClose = vi.fn();
const onJoinSuccess = vi.fn();
const { rerender } = render(
<JoinRideModal isOpen={false} onClose={onClose} onJoinSuccess={onJoinSuccess} />
);
expect(screen.queryByRole('dialog')).toBeNull();
rerender(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
expect(screen.getByRole('dialog')).toBeDefined();
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).toHaveBeenCalled();
});
it('validates short ride code and rejects submission with error', () => {
const onClose = vi.fn();
const onJoinSuccess = vi.fn();
render(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
const submitBtn = screen.getByRole('button', { name: /connect squad telemetry/i });
fireEvent.click(submitBtn);
expect(screen.getByText(/Please enter a valid ride code/i)).toBeDefined();
expect(onJoinSuccess).not.toHaveBeenCalled();
});
it('activates radar lens camera and simulates QR scan successfully', async () => {
const onClose = vi.fn();
const onJoinSuccess = vi.fn();
render(<JoinRideModal isOpen={true} onClose={onClose} onJoinSuccess={onJoinSuccess} />);
// Click activate optical lens
const lensTrigger = screen.getByTestId('activate-radar-lens-button');
fireEvent.click(lensTrigger);
expect(screen.getByTestId('radar-lens-viewfinder')).toBeDefined();
// Click simulate QR scan
const simulateScanBtn = screen.getByRole('button', { name: /simulate qr scan/i });
fireEvent.click(simulateScanBtn);
await waitFor(() => {
expect(screen.getByText(/Telemetry Linked/i)).toBeDefined();
});
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
expect(onJoinSuccess).toHaveBeenCalledWith('APEX84', 'Bay Area Dawn Patrol');
});
});
// ============================================================
// 3. CreateRideModal Component Tests
// ============================================================
describe('CreateRideModal', () => {
it('validates required fields before submitting', () => {
const onClose = vi.fn();
const onCreateRide = vi.fn();
render(<CreateRideModal isOpen={true} onClose={onClose} onCreateRide={onCreateRide} />);
const submitBtn = screen.getByRole('button', { name: /publish group ride/i });
fireEvent.click(submitBtn);
expect(screen.getByText(/Please enter a ride title/i)).toBeDefined();
expect(onCreateRide).not.toHaveBeenCalled();
});
it('submits valid ride and invokes onCreateRide callback', () => {
const onClose = vi.fn();
const onCreateRide = vi.fn();
render(<CreateRideModal isOpen={true} onClose={onClose} onCreateRide={onCreateRide} />);
const titleInput = screen.getByLabelText(/ride title/i);
const regionInput = screen.getByLabelText(/region \/ city/i);
fireEvent.change(titleInput, { target: { value: 'Skyline Sunrise Blast' } });
fireEvent.change(regionInput, { target: { value: 'Palo Alto, CA' } });
const submitBtn = screen.getByRole('button', { name: /publish group ride/i });
fireEvent.click(submitBtn);
expect(onCreateRide).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Skyline Sunrise Blast',
region: 'Palo Alto, CA',
userStatus: 'joined',
})
);
expect(onClose).toHaveBeenCalled();
});
});
// ============================================================
// 4. GroupRidesDirectory Component Tests
// ============================================================
describe('GroupRidesDirectory', () => {
it('renders initial rides with correct pace tabs and badges', () => {
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
expect(screen.getByText(/Malibu Canyons Fast Pace/i)).toBeDefined();
expect(screen.getByText(/Tail of the Dragon Run/i)).toBeDefined();
// Pace tabs
expect(screen.getByRole('button', { name: /all runs/i })).toBeDefined();
expect(screen.getByRole('button', { name: /brisk pace/i })).toBeDefined();
expect(screen.getByRole('button', { name: /track \/ apex/i })).toBeDefined();
expect(screen.getByRole('button', { name: /weekend tours/i })).toBeDefined();
});
it('filters rides by pace tab', () => {
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
// Click Track / Apex
const trackBtn = screen.getByRole('button', { name: /track \/ apex/i });
fireEvent.click(trackBtn);
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
expect(screen.queryByText(/Malibu Canyons Fast Pace/i)).toBeNull();
});
it('filters rides by search text and shows empty state when no match', () => {
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={vi.fn()} />);
const searchInput = screen.getByPlaceholderText(/search by region, canyon/i);
fireEvent.change(searchInput, { target: { value: 'Robbinsville' } });
expect(screen.getByText(/Tail of the Dragon Run/i)).toBeDefined();
expect(screen.queryByText(/Bay Area Dawn Patrol/i)).toBeNull();
// Non-matching search
fireEvent.change(searchInput, { target: { value: 'nonexistent-canyon-run' } });
expect(screen.getByText(/No Matching Pack Runs/i)).toBeDefined();
// Reset button restores
const resetBtn = screen.getByRole('button', { name: /reset filters/i });
fireEvent.click(resetBtn);
expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined();
});
it('handles requesting a slot', () => {
const onRequestSlot = vi.fn();
render(<GroupRidesDirectory rides={INITIAL_GROUP_RIDES} onRequestSlot={onRequestSlot} />);
const requestButtons = screen.getAllByRole('button', { name: /request pack slot/i });
expect(requestButtons.length).toBeGreaterThan(0);
fireEvent.click(requestButtons[0]);
expect(onRequestSlot).toHaveBeenCalled();
});
});
// ============================================================
// 5. Full GroupRidesPage Integrated Flow
// ============================================================
describe('Full GroupRidesPage Integration', () => {
it('coordinates create ride and join ride modals and alerts', async () => {
render(<GroupRidesPage />);
// Hero check
expect(screen.getByText(/Ride in pack formation/i)).toBeDefined();
expect(screen.getByText(/Never lose a wingman/i)).toBeDefined();
// Open Create Modal from hero button
const createHeroBtn = screen.getByRole('button', { name: /create group ride/i });
fireEvent.click(createHeroBtn);
expect(screen.getByRole('heading', { name: /create group ride run/i })).toBeDefined();
// Fill and submit
fireEvent.change(screen.getByLabelText(/ride title/i), {
target: { value: 'Redwood Highway Sprint' },
});
fireEvent.change(screen.getByLabelText(/region \/ city/i), {
target: { value: 'Eureka, CA' },
});
fireEvent.click(screen.getByRole('button', { name: /publish group ride/i }));
// Directory should now show the new ride at the top
expect(screen.getByRole('heading', { name: /Redwood Highway Sprint/i })).toBeDefined();
expect(screen.getByText(/Eureka, CA/i)).toBeDefined();
// Open Join Modal from hero button
const joinHeroBtn = screen.getByRole('button', { name: /join with ride code \/ qr/i });
fireEvent.click(joinHeroBtn);
expect(screen.getByRole('heading', { name: /join pack formation/i })).toBeDefined();
// Enter code and submit
const codeInput = screen.getByLabelText(/ride code/i);
fireEvent.change(codeInput, { target: { value: 'APEX84' } });
fireEvent.click(screen.getByRole('button', { name: /connect squad telemetry/i }));
await waitFor(() => {
expect(screen.getByText(/Telemetry Linked/i)).toBeDefined();
});
});
});
});