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,591 +1,303 @@
'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">
{/* Dynamic Atmospheric Glow Background Layers */}
<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>
{/* 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">
{/* Eyebrow Badge */}
<div className="inline-flex items-center gap-space-xs px-space-sm py-1.5 rounded-full bg-surface-container-high border-0 shadow-sm mb-space-md">
<span className="relative flex h-2.5 w-2.5">
<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>
</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>
</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.
{/* Dynamic Atmospheric Glow Background Layers */}
<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">
{/* Eyebrow Badge */}
<div className="inline-flex items-center gap-space-xs px-space-sm py-1.5 rounded-full bg-surface-container-high border-0 shadow-sm mb-space-md">
<span className="relative flex h-2.5 w-2.5">
<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>
</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>
</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.
</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">
{/* Dual CTAs & Quick Metrics Bar */}
<div className="flex flex-wrap items-center gap-space-md w-full">
<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 */}
<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>
</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>
</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>
<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>
<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.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-container">
<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>
<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>
<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.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-error">
<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>
<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>
<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.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-secondary-fixed">
<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>
<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>
<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.
</p>
</div>
<div className="pt-space-lg mt-space-lg flex items-center justify-between font-code-mono text-code-mono text-primary-fixed">
<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="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>
<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.
</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">
<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">
<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>
</div>
</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>
</section>
</div>
{/* Interactive Live Pack Radar Cockpit */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<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>
</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-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>
<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.
</p>
</div>
<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-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>
<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.
</p>
</div>
<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-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>
<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.
</p>
</div>
<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-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>
<p className="font-body-sm text-body-sm text-on-surface-variant leading-relaxed">
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 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-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>
<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.
</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 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 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>
</div>
</div>
</div>
</section>
{/* 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();
});
});
});
});