304 lines
17 KiB
TypeScript
304 lines
17 KiB
TypeScript
'use client';
|
|
|
|
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>
|
|
|
|
{/* 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 & 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
|
|
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
|
|
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 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: <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 & 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 & 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 & 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>
|
|
);
|
|
}
|