From 69fefd7b56a74611b551aa34cc0d7786d9e41932 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 25 Sep 2026 16:00:47 -0400 Subject: [PATCH] feat(website): create separate showcase and management group rides pages --- website/app/group-rides/manage/page.tsx | 57 ++ website/app/group-rides/page.tsx | 335 +------ website/app/group-rides/showcase/page.tsx | 24 + .../group-rides/delete-confirm-modal.tsx | 101 +++ .../group-rides/edit-ride-modal.tsx | 313 +++++++ website/components/group-rides/fixtures.ts | 21 +- .../group-rides/group-rides-management.tsx | 849 ++++++++++++++++++ .../group-rides/group-rides-showcase.tsx | 534 +++++++++++ website/components/group-rides/types.ts | 20 + website/tests/group-rides-page.test.tsx | 112 ++- 10 files changed, 2055 insertions(+), 311 deletions(-) create mode 100644 website/app/group-rides/manage/page.tsx create mode 100644 website/app/group-rides/showcase/page.tsx create mode 100644 website/components/group-rides/delete-confirm-modal.tsx create mode 100644 website/components/group-rides/edit-ride-modal.tsx create mode 100644 website/components/group-rides/group-rides-management.tsx create mode 100644 website/components/group-rides/group-rides-showcase.tsx diff --git a/website/app/group-rides/manage/page.tsx b/website/app/group-rides/manage/page.tsx new file mode 100644 index 0000000..d4646d1 --- /dev/null +++ b/website/app/group-rides/manage/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { firebaseAuth } from '../../../lib/firebase-client'; +import { GroupRidesManagement } from '../../../components/group-rides/group-rides-management'; + +export default function GroupRidesManageRoute() { + const [currentUser, setCurrentUser] = useState(firebaseAuth?.currentUser ?? null); + const [authChecked, setAuthChecked] = useState(Boolean(firebaseAuth?.currentUser)); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { + setCurrentUser(user); + setAuthChecked(true); + }); + return () => unsubscribe(); + }, []); + + // If not logged in, render an access prompt directing them to sign in + if (!currentUser && authChecked) { + return ( +
+
+
+ lock +
+
+

+ Authentication Required +

+

+ The Group Rides Management Console allows riders to schedule, coordinate, and modify live squad pack runs. Please sign in to your rider account to proceed. +

+
+
+ + Sign In to Access Management + + + Explore Feature Showcase + +
+
+
+ ); + } + + return ; +} diff --git a/website/app/group-rides/page.tsx b/website/app/group-rides/page.tsx index 2489b66..b48f439 100644 --- a/website/app/group-rides/page.tsx +++ b/website/app/group-rides/page.tsx @@ -1,303 +1,58 @@ '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'; +import React, { useState, useEffect } from 'react'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { firebaseAuth } from '../../lib/firebase-client'; +import { GroupRidesShowcase } from '../../components/group-rides/group-rides-showcase'; +import { GroupRidesManagement } from '../../components/group-rides/group-rides-management'; export default function GroupRidesPage() { - const [rides, setRides] = useState(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 [currentUser, setCurrentUser] = useState(firebaseAuth?.currentUser ?? null); + const [viewOverride, setViewOverride] = useState<'showcase' | 'manage' | null>(null); - const showAlert = (message: string, type: 'success' | 'info' = 'success') => { - setActiveAlert({ message, type }); - setTimeout(() => { - setActiveAlert(null); - }, 4500); - }; + useEffect(() => { + // Check URL parameters for explicit view overrides (?view=showcase or ?view=manage) + if (typeof window !== 'undefined') { + const params = new URLSearchParams(window.location.search); + const view = params.get('view'); + if (view === 'showcase' || view === 'manage') { + setViewOverride(view); + } + } - const handleJoinSuccess = (code: string, rideTitle: string) => { - showAlert(`Successfully synchronized telemetry with "${rideTitle}" (Code: ${code})!`, 'success'); - }; + const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { + setCurrentUser(user); + }); - const handleCreateRide = (newRide: GroupRideSummary) => { - setRides((prev) => [newRide, ...prev]); - showAlert(`Published group run "${newRide.title}"! Pack radar is now active.`, 'success'); - }; + return () => unsubscribe(); + }, []); - 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, - }; - }) + // If explicit showcase view requested + if (viewOverride === 'showcase') { + return ( + setViewOverride('manage')} + /> ); - }; + } + // If user is authenticated: render Group Rides Management Console + if (currentUser) { + return ( + setViewOverride('showcase')} + /> + ); + } + + // If user is not authenticated: render Group Rides Feature Showcase Page return ( -
-
- {/* Dynamic Atmospheric Glow Background Layers */} -
-
-
- - {/* Toast Notification Alert */} - {activeAlert && ( -
- - {activeAlert.type === 'success' ? 'check_circle' : 'info'} - - {activeAlert.message} - -
- )} - - {/* Hero Section */} -
-
- {/* Eyebrow Badge */} -
- - - - - - Live Pack Radar & Formation Telemetry - -
- - {/* Headline */} -

- Ride in pack formation.
- - Never lose a wingman. - -

- - {/* Subheading */} -

- Real-time rider radar, staggered formation spacing alerts, instant incident broadcast, and - automated regroup checkpoints calibrated for fast canyon runs and transcontinental tours. -

- - {/* Dual CTAs & Quick Metrics Bar */} -
- - -
-
-
- - {/* Interactive Live Pack Radar Cockpit */} -
- -
- - {/* Group Ride Features Grid */} -
-
- - Cockpit Architecture - -

- Engineered for Squadron Cohesion -

-

- High-frequency mesh communications keep the entire formation synchronized through blind - apexes, switchbacks, and offline mountain passes. -

-
- -
- {/* Feature 1 */} -
-
-
- spatial_audio_off -
- - 01 / Paceline Audit - -

- Staggered Paceline Warnings -

-

- Spatial audio HUD chimes dynamically sound whenever rider follow distance or lane - positioning falls beneath calculated emergency stopping thresholds. -

-
-
- Response: <12ms - arrow_forward -
-
- - {/* Feature 2 */} -
-
-
- fmd_bad -
- - 02 / Swarm Broadcast - -

- Crash & Separation Alerts -

-

- Autonomous IMU-based impact detection pings the entire pack instantly. Visual yellow and - red telemetry flags highlight precise coordinates for immediate regrouping. -

-
-
- IMU Sensing: 6-Axis - arrow_forward -
-
- - {/* Feature 3 */} -
-
-
- hub -
- - 03 / Offline Link - -

- Peer-to-Peer Mesh Sync -

-

- 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. -

-
-
- Zero-cell Ready - arrow_forward -
-
- - {/* Feature 4 */} -
-
-
- history_toggle_off -
- - 04 / Mission Debrief - -

- Group Replay & Debrief -

-

- Relive the ride with synced multi-bike timelines. Compare apex entries, sector speeds, - and individual lean angles on an interactive 3D map renderer. -

-
-
- 3D Replay Engine - arrow_forward -
-
-
-
- - {/* Visual Divider / Action Interstitial */} -
-
-
-
-
- - Hardware Compatibility - -

- Sync directly with your helmet comms & bike dash -

-

- Integrates seamlessly with Cardo, Sena, Apple Watch, and OBD-II CAN bus links for live - peg-to-peg data overlays. -

-
-
-
- headset_mic - Cardo / Sena HUD -
-
- watch - Haptic Alerts -
-
-
-
-
- - {/* Active Public & Regional Group Rides Directory */} - - - {/* Modals */} - setIsJoinModalOpen(false)} - onJoinSuccess={handleJoinSuccess} - /> - - setIsCreateModalOpen(false)} - onCreateRide={handleCreateRide} - /> -
-
-
+ setViewOverride('manage')} + /> ); } diff --git a/website/app/group-rides/showcase/page.tsx b/website/app/group-rides/showcase/page.tsx new file mode 100644 index 0000000..800d339 --- /dev/null +++ b/website/app/group-rides/showcase/page.tsx @@ -0,0 +1,24 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { onAuthStateChanged, User } from 'firebase/auth'; +import { firebaseAuth } from '../../../lib/firebase-client'; +import { GroupRidesShowcase } from '../../../components/group-rides/group-rides-showcase'; + +export default function GroupRidesShowcaseRoute() { + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { + setCurrentUser(user); + }); + return () => unsubscribe(); + }, []); + + return ( + + ); +} diff --git a/website/components/group-rides/delete-confirm-modal.tsx b/website/components/group-rides/delete-confirm-modal.tsx new file mode 100644 index 0000000..ac56923 --- /dev/null +++ b/website/components/group-rides/delete-confirm-modal.tsx @@ -0,0 +1,101 @@ +'use client'; + +import React, { useEffect } from 'react'; + +interface DeleteConfirmModalProps { + isOpen: boolean; + title: string; + message: string; + confirmText?: string; + cancelText?: string; + danger?: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ + isOpen, + title, + message, + confirmText = 'Confirm', + cancelText = 'Keep Ride', + danger = true, + onClose, + onConfirm, +}: DeleteConfirmModalProps) { + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') onClose(); + } + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+ {/* Icon & Title */} +
+
+ + {danger ? 'warning' : 'help'} + +
+
+

+ {title} +

+ + Formation Telemetry Safeguard + +
+
+ + {/* Message */} +

+ {message} +

+ + {/* Actions */} +
+ + +
+
+
+ ); +} diff --git a/website/components/group-rides/edit-ride-modal.tsx b/website/components/group-rides/edit-ride-modal.tsx new file mode 100644 index 0000000..17e9c56 --- /dev/null +++ b/website/components/group-rides/edit-ride-modal.tsx @@ -0,0 +1,313 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { GroupRideSummary, EditRideFormData } from './types'; + +interface EditRideModalProps { + isOpen: boolean; + ride: GroupRideSummary | null; + onClose: () => void; + onSaveRide: (updatedRide: GroupRideSummary) => void; +} + +export function EditRideModal({ + isOpen, + ride, + onClose, + onSaveRide, +}: EditRideModalProps) { + const [formData, setFormData] = useState({ + title: '', + region: '', + description: '', + paceClassification: 'Brisk Pace', + formationProfile: 'Staggered Paceline', + joinPolicy: 'requestToJoin', + scheduledDate: '', + scheduledTime: '', + distance: '', + meetingPoint: '', + participantLimit: 12, + status: 'scheduled', + }); + + useEffect(() => { + if (ride) { + setFormData({ + title: ride.title, + region: ride.region, + description: ride.description, + paceClassification: ride.paceClassification, + formationProfile: ride.formationProfile, + joinPolicy: ride.joinPolicy, + scheduledDate: ride.scheduledDate, + scheduledTime: ride.scheduledTime, + distance: ride.distance, + meetingPoint: ride.meetingPoint, + participantLimit: ride.participantLimit, + status: ride.status ?? 'scheduled', + }); + } + }, [ride]); + + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') onClose(); + } + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen || !ride) return null; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.title.trim() || !formData.region.trim()) return; + + const updated: GroupRideSummary = { + ...ride, + title: formData.title.trim(), + region: formData.region.trim(), + description: formData.description.trim(), + paceClassification: formData.paceClassification, + formationProfile: formData.formationProfile.trim(), + joinPolicy: formData.joinPolicy, + scheduledDate: formData.scheduledDate.trim(), + scheduledTime: formData.scheduledTime.trim(), + distance: formData.distance.trim(), + meetingPoint: formData.meetingPoint.trim(), + participantLimit: formData.participantLimit, + status: formData.status, + }; + + onSaveRide(updated); + onClose(); + }; + + return ( +
+
+ {/* Header */} +
+
+
+ edit_note +
+
+

+ Alter Group Ride Parameters +

+ + ID: {ride.id} {ride.rideCode ? `• Code: ${ride.rideCode}` : ''} + +
+
+ +
+ + {/* Edit Form */} +
+ {/* Title */} +
+ + setFormData({ ...formData, title: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container font-sans" + placeholder="e.g. Skyline Dawn Patrol" + /> +
+ + {/* Region & Rally Point */} +
+
+ + setFormData({ ...formData, region: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container" + placeholder="e.g. San Francisco, CA" + /> +
+
+ + setFormData({ ...formData, meetingPoint: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container" + placeholder="e.g. Alice's Restaurant" + /> +
+
+ + {/* Date, Time, Distance */} +
+
+ + setFormData({ ...formData, scheduledDate: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container" + placeholder="e.g. Saturday" + /> +
+
+ + setFormData({ ...formData, scheduledTime: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container" + placeholder="e.g. 06:30 AM PST" + /> +
+
+ + setFormData({ ...formData, distance: e.target.value })} + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container" + placeholder="e.g. 74 Miles" + /> +
+
+ + {/* Pace Classification, Status, Rider Limit */} +
+
+ + +
+
+ + +
+
+ + + setFormData({ ...formData, participantLimit: parseInt(e.target.value) || 10 }) + } + className="w-full px-3.5 py-2 rounded-xl bg-surface-container border border-outline-variant/30 text-on-surface text-sm focus:outline-none focus:ring-2 focus:ring-primary-container font-mono" + /> +
+
+ + {/* Description */} +
+ +