diff --git a/website/app/group-rides/manage/page.tsx b/website/app/group-rides/manage/page.tsx index d4646d1..520fd75 100644 --- a/website/app/group-rides/manage/page.tsx +++ b/website/app/group-rides/manage/page.tsx @@ -1,12 +1,13 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import Link from 'next/link'; +import { useRouter } from 'next/navigation'; 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 router = useRouter(); const [currentUser, setCurrentUser] = useState(firebaseAuth?.currentUser ?? null); const [authChecked, setAuthChecked] = useState(Boolean(firebaseAuth?.currentUser)); @@ -14,40 +15,25 @@ export default function GroupRidesManageRoute() { const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { setCurrentUser(user); setAuthChecked(true); + if (!user) { + router.replace('/login?redirect=/group-rides/manage'); + } }); return () => unsubscribe(); - }, []); + }, [router]); - // If not logged in, render an access prompt directing them to sign in - if (!currentUser && authChecked) { + // If auth is still checking or user is not logged in (and redirecting), show clean loading skeleton + if (!authChecked || !currentUser) { 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 - -
+
+
+
+ + Verifying rider credentials & permissions... +
); @@ -55,3 +41,4 @@ export default function GroupRidesManageRoute() { return ; } + diff --git a/website/app/group-rides/page.tsx b/website/app/group-rides/page.tsx index b48f439..a5ad6ec 100644 --- a/website/app/group-rides/page.tsx +++ b/website/app/group-rides/page.tsx @@ -8,51 +8,39 @@ import { GroupRidesManagement } from '../../components/group-rides/group-rides-m export default function GroupRidesPage() { const [currentUser, setCurrentUser] = useState(firebaseAuth?.currentUser ?? null); - const [viewOverride, setViewOverride] = useState<'showcase' | 'manage' | null>(null); + const [authChecked, setAuthChecked] = useState(Boolean(firebaseAuth?.currentUser)); 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 unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { setCurrentUser(user); + setAuthChecked(true); }); return () => unsubscribe(); }, []); - // If explicit showcase view requested - if (viewOverride === 'showcase') { + // Loading skeleton to eliminate page flickering while auth state resolves + if (!authChecked) { return ( - setViewOverride('manage')} - /> +
+
+
+
+
+
+
); } - // If user is authenticated: render Group Rides Management Console + // If user is authenticated: render Group Rides Hub / Management Console if (currentUser) { - return ( - setViewOverride('showcase')} - /> - ); + return ; } // If user is not authenticated: render Group Rides Feature Showcase Page - return ( - setViewOverride('manage')} - /> - ); + return ; } + diff --git a/website/components/group-rides/create-ride-modal.tsx b/website/components/group-rides/create-ride-modal.tsx index f553de7..458eb51 100644 --- a/website/components/group-rides/create-ride-modal.tsx +++ b/website/components/group-rides/create-ride-modal.tsx @@ -15,9 +15,12 @@ export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideMod const [scheduledDate, setScheduledDate] = useState('Saturday'); const [scheduledTime, setScheduledTime] = useState('07:00 AM PST'); const [paceClassification, setPaceClassification] = useState('Brisk Pace'); + const [discipline, setDiscipline] = useState>('Sport / Canyon'); + const [joinPolicy, setJoinPolicy] = useState('requestToJoin'); const [formationProfile, setFormationProfile] = useState('Staggered Dual Column [B]'); const [distance, setDistance] = useState('50.0 Miles'); const [meetingPoint, setMeetingPoint] = useState(''); + const [routeSnippet, setRouteSnippet] = useState('Skyline Blvd > Stage Rd > PCH Pacific Coast Hwy'); const [participantLimit, setParticipantLimit] = useState(12); const [description, setDescription] = useState(''); const [errorMsg, setErrorMsg] = useState(null); @@ -30,9 +33,12 @@ export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideMod setScheduledDate('Saturday'); setScheduledTime('07:00 AM PST'); setPaceClassification('Brisk Pace'); + setDiscipline('Sport / Canyon'); + setJoinPolicy('requestToJoin'); setFormationProfile('Staggered Dual Column [B]'); setDistance('50.0 Miles'); setMeetingPoint(''); + setRouteSnippet('Skyline Blvd > Stage Rd > PCH Pacific Coast Hwy'); setParticipantLimit(12); setDescription(''); setErrorMsg(null); @@ -72,15 +78,18 @@ export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideMod scheduledDate, scheduledTime, paceClassification, + discipline, formationProfile, - joinPolicy: 'requestToJoin', + joinPolicy, leadHandle: 'You (Organizer)', leadAvatarText: 'YOU', participantCount: 1, participantLimit: Number(participantLimit) || 12, distance: distance.trim() || '45.0 Miles', meetingPoint: meetingPoint.trim() || 'Designated Rally Staging Point', + routeSnippet: routeSnippet.trim() || 'Custom GPX route uploaded by organizer.', userStatus: 'joined', + status: 'scheduled', }; onCreateRide(newRide); @@ -238,6 +247,41 @@ export function CreateRideModal({ isOpen, onClose, onCreateRide }: CreateRideMod
+ {/* Discipline & Privacy Controls */} +
+
+ + +
+
+ + +
+
+ {/* Meeting Point */}
+ {/* Route Upload or Waypoint Selection */} +
+ +
+ setRouteSnippet(e.target.value)} + placeholder="e.g. Skyline Blvd > Stage Rd > Pescadero Creek" + className="flex-1 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" + /> + +
+
+ {/* Description */}
- {/* Metrics KPI Cards */} + {/* Metrics KPI Cards: Active Squads Nearby, Total Miles in Formation, Pack Safety Score, Mesh Node Health */}
- Active Pack Runs + Active Squads Nearby
- {myTotalRides.length} + {rides.length} Squads
- {myOrganizedRides.length} Lead • {myJoinedRides.length} Wingman + Within 25 Mile Radius
- Rides Organized + Total Miles in Formation
- {myOrganizedRides.length} + 1,420 mi
- Road Captain Authority + Squad Aggregate Logged
- Squad Formations + Pack Safety Score
- {rides.length} Total + 98.4%
- In Regional Network + Optimal Paceline Spacing
- Live Pack Radar + Mesh Node Health
- Online + 6/6 Nodes
- 6-Node Telemetry Mesh + 12ms Latency • Online
@@ -487,6 +497,9 @@ export function GroupRidesManagement({ const isOrg = Boolean(ride.isOrganizer); const isCanceled = ride.status === 'canceled'; + const statusText = isCanceled ? 'Canceled' : (ride.status || 'Scheduled'); + const isActiveRide = ride.status === 'active' || ride.status === 'underway'; + return (
{/* Role & Status Header */}
-
+
{isOrg ? ( Road Captain (Organizer) @@ -509,11 +522,17 @@ export function GroupRidesManagement({ Squad Wingman )} - {isCanceled && ( - - Canceled - - )} + + {statusText} +
{/* Ride Code Button */} @@ -534,11 +553,13 @@ export function GroupRidesManagement({

{ride.title}

-
- - location_on +
+ + {ride.discipline || 'Sport / Canyon'} + + + {ride.paceClassification} - {ride.region} • {ride.distance}
@@ -547,7 +568,7 @@ export function GroupRidesManagement({ {ride.description}

- {/* Meeting Point & Schedule */} + {/* Meeting Point, Schedule, Route Snippet */}
@@ -561,6 +582,14 @@ export function GroupRidesManagement({ Rally: {ride.meetingPoint}
+ {ride.routeSnippet && ( +
+ + route + + {ride.routeSnippet} +
+ )}
group @@ -572,11 +601,33 @@ export function GroupRidesManagement({
- {/* Management Controls Row (ALTER, REMOVE, CANCEL, LEAVE) */} + {/* Management Controls Row (VIEW DETAILS, ALTER, REMOVE, CANCEL, LEAVE, RADAR) */}
- {isOrg ? ( - <> -
+
+ + + {isActiveRide && ( + + )} + + {isOrg && ( + <> )} -
+ + )} +
+
+ {isOrg ? ( - - ) : ( - <> - - check_circle - Pack Slot Active - + ) : ( - - )} + )} +
); @@ -651,6 +700,17 @@ export function GroupRidesManagement({ > All Runs ({rides.length}) + + -
@@ -724,6 +784,14 @@ export function GroupRidesManagement({

{ride.title}

+
+ + {ride.discipline || 'Sport / Canyon'} + + • + {ride.distance} +
+

{ride.description}

@@ -741,6 +809,12 @@ export function GroupRidesManagement({ Rally: {ride.meetingPoint}
+ {ride.routeSnippet && ( +
+ route + {ride.routeSnippet} +
+ )}
@@ -751,9 +825,17 @@ export function GroupRidesManagement({
-
+
+ {isJoined ? ( -
+
check_circle {ride.isOrganizer ? 'Organized by You' : 'Slot Confirmed'}
@@ -761,7 +843,7 @@ export function GroupRidesManagement({ @@ -769,7 +851,7 @@ export function GroupRidesManagement({
diff --git a/website/components/group-rides/group-rides-showcase.tsx b/website/components/group-rides/group-rides-showcase.tsx index de2a723..e4dcc87 100644 --- a/website/components/group-rides/group-rides-showcase.tsx +++ b/website/components/group-rides/group-rides-showcase.tsx @@ -41,12 +41,12 @@ export function GroupRidesShowcase({ {/* Authenticated Rider Quick Notice (if logged-in user visits showcase) */} {isLoggedIn && ( -
+
verified_user - Viewing Public Showcase {userEmail ? `as ${userEmail}` : '(Signed In)'} + You're viewing the public showcase.{userEmail ? ` (${userEmail})` : ''}
{onGoToManagement ? ( @@ -55,15 +55,15 @@ export function GroupRidesShowcase({ onClick={onGoToManagement} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-primary-container text-on-primary-container font-bold hover:shadow-[0_0_12px_rgba(0,242,254,0.4)] transition cursor-pointer" > - Go to Group Rides Console + Return to Group Rides Console arrow_forward ) : ( - Go to Group Rides Console + Return to Group Rides Console arrow_forward )} @@ -114,7 +114,7 @@ export function GroupRidesShowcase({ ) : ( dashboard @@ -140,6 +140,15 @@ export function GroupRidesShowcase({ how_to_reg Get Started Free + + + download + + Download App +
- {/* Feature 2: Crash & Separation Alerts */} + {/* Feature 2: Swarm Broadcast & Crash Separation */}
@@ -216,7 +225,7 @@ export function GroupRidesShowcase({ 02 / Swarm Broadcast

- Crash & Separation Alerts + Swarm Broadcast & Crash Separation

Autonomous 6-axis IMU impact detection pings the entire pack instantly. Visual yellow and @@ -262,7 +271,7 @@ export function GroupRidesShowcase({ 04 / Mission Debrief

- 3D Multi-Bike Replay + 3D Multi-Bike Replay & Debrief

Relive the run with synced multi-bike timelines. Compare apex entries, sector speeds, @@ -275,7 +284,7 @@ export function GroupRidesShowcase({

- {/* Feature 5: Multi-Band GNSS Formation Lock */} + {/* Feature 5: Multi-Band GNSS Precision */}
@@ -285,7 +294,7 @@ export function GroupRidesShowcase({ 05 / Precision Mesh

- Multi-Band GNSS Lock + Multi-Band GNSS Precision

Dual-frequency L1+L5 satellite tracking ensures accurate relative positioning even in @@ -298,25 +307,25 @@ export function GroupRidesShowcase({

- {/* Feature 6: Dynamic Checkpoint Auto-Regroup */} + {/* Feature 6: Offline Cell-Tower Fallback */}
- pin_drop + cell_tower
- 06 / Intelligent Checkpoints + 06 / Zero-Cell Fallback

- Auto-Regroup Checkpoints + Offline Cell-Tower Fallback

- Automatic rally waypoints notify lead marshals when the pack spreads beyond safety - parameters, ensuring no rider is left behind at remote turnouts. + Zero cellular dependency. Dynamic mesh packet hopping ensures formation telemetry, + deceleration warnings, and regroup coordinates stay live without cell tower connectivity.

- Smart Geofencing + Zero-Cell Resilient arrow_forward
@@ -336,22 +345,22 @@ export function GroupRidesShowcase({ 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 telemetry cues without taking your eyes off the corner exit. + Integrates seamlessly with Cardo DMC, Sena Mesh, Bluetooth LE, and Android Auto / Apple CarPlay + dashboards for live peg-to-peg telemetry cues without taking your eyes off the corner exit.

headset_mic - Cardo / Sena Audio + Cardo DMC & Sena Mesh
- watch - Haptic Alerts + bluetooth + Bluetooth LE
- sensors - CAN-Link Telemetry + directions_car + Android Auto / CarPlay
@@ -474,7 +483,7 @@ export function GroupRidesShowcase({
{isLoggedIn ? ( add_circle diff --git a/website/components/group-rides/live-pack-radar.tsx b/website/components/group-rides/live-pack-radar.tsx index 7be62ee..cc29115 100644 --- a/website/components/group-rides/live-pack-radar.tsx +++ b/website/components/group-rides/live-pack-radar.tsx @@ -369,22 +369,23 @@ export function LivePackRadar({ initialMode = 'tactical' }: LivePackRadarProps)
- {/* Normal Telemetry Grid */} -
+ {/* Squad Roster Telemetry Grid: Speed, Lean, Battery, Mesh Ping */} +
- Speed: {rider.speedMph} MPH + Speed + {rider.speedMph} MPH
- Lean: {rider.leanAngleDeg}° + Lean + {rider.leanAngleDeg}°
- {rider.brakePct && rider.brakePct > 0 ? ( - Brake: {rider.brakePct}% - ) : rider.throttlePct !== undefined ? ( - Throttle: {rider.throttlePct}% - ) : ( - Delta: ±{rider.gapDeltaSec}s - )} + Battery + {rider.batteryPct ?? 92}% +
+
+ Mesh Ping + {rider.meshPingMs ?? 12}ms
diff --git a/website/components/group-rides/ride-details-modal.tsx b/website/components/group-rides/ride-details-modal.tsx new file mode 100644 index 0000000..6a1a261 --- /dev/null +++ b/website/components/group-rides/ride-details-modal.tsx @@ -0,0 +1,192 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { GroupRideSummary } from './types'; + +interface RideDetailsModalProps { + isOpen: boolean; + ride: GroupRideSummary | null; + onClose: () => void; + onEdit?: (ride: GroupRideSummary) => void; + onRequestSlot?: (rideId: string) => void; +} + +export function RideDetailsModal({ + isOpen, + ride, + onClose, + onEdit, + onRequestSlot, +}: RideDetailsModalProps) { + 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 isFull = ride.participantCount >= ride.participantLimit; + const isJoined = ride.userStatus === 'joined' || ride.isOrganizer; + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+ {/* Header */} +
+
+
+ sports_motorsports +
+
+

+ {ride.title} +

+ + {ride.discipline || 'Sport / Canyon'} • {ride.paceClassification} + +
+
+ +
+ + {/* Status & Code Row */} +
+
+ Status: + + {ride.status || 'scheduled'} + +
+ {ride.rideCode && ( +
+ Code: + + {ride.rideCode} + +
+ )} +
+ + {/* Detailed Metadata Grid */} +
+
+
+ Date & Time: + + {ride.scheduledDate} • {ride.scheduledTime} + +
+
+ Region: + {ride.region} +
+
+ Total Distance: + {ride.distance} +
+
+ Formation Profile: + {ride.formationProfile} +
+
+ Rider Roster: + + {ride.participantCount} / {ride.participantLimit} Riders + +
+
+ Road Captain / Lead: + {ride.leadHandle} +
+
+ + {/* Meeting Point */} +
+ + Meeting Point & Rally Location + +
+ {ride.meetingPoint} +
+
+ + {/* Route Preview Snippet */} +
+ + Route Preview Waypoints + +
+ {ride.routeSnippet || 'Waypoints synchronized via MotorSignal GNSS Multi-Band Mesh.'} +
+
+ + {/* Description & Safety Briefing */} +
+ + Briefing & Safety Protocol + +

+ {ride.description} +

+
+
+ + {/* Footer Actions */} +
+ + {ride.isOrganizer && onEdit && ( + + )} + {!ride.isOrganizer && !isJoined && onRequestSlot && !isFull && ( + + )} +
+
+
+ ); +} diff --git a/website/components/group-rides/types.ts b/website/components/group-rides/types.ts index 496ad5e..ea093a9 100644 --- a/website/components/group-rides/types.ts +++ b/website/components/group-rides/types.ts @@ -15,6 +15,8 @@ export interface RadarRiderTelemetry { brakePct?: number; gapMeters: number; gapDeltaSec: number; + batteryPct?: number; + meshPingMs?: number; color: string; status: 'active' | 'warning' | 'alert' | 'sweeper'; alertText?: string; @@ -28,7 +30,8 @@ export interface GroupRideSummary { region: string; scheduledTime: string; scheduledDate: string; - paceClassification: 'Track Pace' | 'Brisk Pace' | 'Endurance / Chill' | 'Weekend Tours'; + paceClassification: 'Track Pace' | 'Brisk Pace' | 'Spirited' | 'Casual' | 'Endurance / Chill' | 'Weekend Tours'; + discipline?: 'Sport / Canyon' | 'Touring / ADV' | 'Track / Circuit' | 'Cruiser / Pack'; formationProfile: string; joinPolicy: 'open' | 'requestToJoin' | 'inviteOnly'; leadHandle: string; @@ -37,9 +40,10 @@ export interface GroupRideSummary { participantLimit: number; distance: string; meetingPoint: string; + routeSnippet?: string; userStatus?: 'none' | 'requested' | 'joined' | 'waitlisted'; isOrganizer?: boolean; - status?: 'scheduled' | 'assembling' | 'underway' | 'completed' | 'canceled'; + status?: 'draft' | 'scheduled' | 'upcoming' | 'active' | 'underway' | 'completed' | 'canceled'; rideCode?: string; clubName?: string; } @@ -49,12 +53,14 @@ export interface CreateRideFormData { region: string; description: string; paceClassification: GroupRideSummary['paceClassification']; + discipline?: GroupRideSummary['discipline']; formationProfile: string; joinPolicy: GroupRideSummary['joinPolicy']; scheduledDate: string; scheduledTime: string; distance: string; meetingPoint: string; + routeSnippet?: string; participantLimit: number; } @@ -63,13 +69,15 @@ export interface EditRideFormData { region: string; description: string; paceClassification: GroupRideSummary['paceClassification']; + discipline?: GroupRideSummary['discipline']; formationProfile: string; joinPolicy: GroupRideSummary['joinPolicy']; scheduledDate: string; scheduledTime: string; distance: string; meetingPoint: string; + routeSnippet?: string; participantLimit: number; - status: 'scheduled' | 'assembling' | 'underway' | 'completed' | 'canceled'; + status: 'draft' | 'scheduled' | 'upcoming' | 'active' | 'underway' | 'completed' | 'canceled'; } diff --git a/website/tests/group-rides-page.test.tsx b/website/tests/group-rides-page.test.tsx index 4f63a2f..9550cea 100644 --- a/website/tests/group-rides-page.test.tsx +++ b/website/tests/group-rides-page.test.tsx @@ -11,11 +11,17 @@ import { GroupRidesDirectory } from '../components/group-rides/group-rides-direc import { INITIAL_GROUP_RIDES } from '../components/group-rides/fixtures'; import { SiteHeaderClient } from '../components/site-header-client'; +const mockPush = vi.fn(); +const mockReplace = vi.fn(); + vi.mock('next/navigation', () => ({ usePathname: () => '/', - useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }), + useRouter: () => ({ push: mockPush, replace: mockReplace, back: vi.fn() }), })); +import GroupRidesManageRoute from '../app/group-rides/manage/page'; +import GroupRidesShowcaseRoute from '../app/group-rides/showcase/page'; + describe('Group Rides Interactive Page & Components', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -276,19 +282,35 @@ describe('Group Rides Interactive Page & Components', () => { expect(screen.getByText(/Ride in pack formation/i)).toBeDefined(); expect(screen.getByText(/Never lose a wingman/i)).toBeDefined(); - // Feature highlights check + // Feature highlights check (6-feature grid) expect(screen.getByText(/Staggered Paceline Warnings/i)).toBeDefined(); - expect(screen.getByText(/Crash & Separation Alerts/i)).toBeDefined(); + expect(screen.getByText(/Swarm Broadcast & Crash Separation/i)).toBeDefined(); expect(screen.getByText(/Peer-to-Peer Mesh Sync/i)).toBeDefined(); - expect(screen.getByText(/3D Multi-Bike Replay/i)).toBeDefined(); + expect(screen.getByText(/3D Multi-Bike Replay & Debrief/i)).toBeDefined(); + expect(screen.getByText(/Multi-Band GNSS Precision/i)).toBeDefined(); + expect(screen.getByText(/Offline Cell-Tower Fallback/i)).toBeDefined(); + + // Hardware strip + expect(screen.getByText(/Cardo DMC & Sena Mesh/i)).toBeDefined(); + expect(screen.getAllByText(/Bluetooth LE/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Android Auto \/ CarPlay/i).length).toBeGreaterThan(0); // CTAs check expect(screen.getByRole('link', { name: /get started free/i })).toBeDefined(); - expect(screen.getByRole('link', { name: /sign in to join runs/i })).toBeDefined(); + expect(screen.getByRole('link', { name: /download app/i })).toBeDefined(); + expect(screen.getAllByRole('link', { name: /sign in to join pack/i }).length).toBeGreaterThan(0); // Community preview expect(screen.getByText(/Sample Pack Formations/i)).toBeDefined(); }); + + it('renders logged-in banner when an authenticated user visits showcase', () => { + render(); + expect(screen.getByText(/You're viewing the public showcase\./i)).toBeDefined(); + const returnBtn = screen.getByRole('link', { name: /return to group rides console/i }); + expect(returnBtn).toBeDefined(); + expect(returnBtn.getAttribute('href')).toBe('/group-rides/manage'); + }); }); describe('GroupRidesManagement (For Logged In Users - View, Add, Remove, Alter)', () => { @@ -300,12 +322,26 @@ describe('Group Rides Interactive Page & Components', () => { expect(screen.getByText(/Group Rides Management/i)).toBeDefined(); expect(screen.getByText(/Squad Management & Telemetry Hub/i)).toBeDefined(); + // KPI Stat Cards check + expect(screen.getByText(/Active Squads Nearby/i)).toBeDefined(); + expect(screen.getByText(/Total Miles in Formation/i)).toBeDefined(); + expect(screen.getByText(/Pack Safety Score/i)).toBeDefined(); + expect(screen.getByText(/Mesh Node Health/i)).toBeDefined(); + // 1. VIEW: Verify initial organized and joined rides expect(screen.getByText(/Bay Area Dawn Patrol/i)).toBeDefined(); expect(screen.getByText(/Road Captain \(Organizer\)/i)).toBeDefined(); expect(screen.getByText(/Malibu Canyons Fast Pace/i)).toBeDefined(); expect(screen.getByText(/Squad Wingman/i)).toBeDefined(); + // Details Modal inspection + const detailBtns = screen.getAllByRole('button', { name: /details/i }); + fireEvent.click(detailBtns[0]); + expect(screen.getByRole('dialog', { name: /bay area dawn patrol/i })).toBeDefined(); + const closeDetailBtn = screen.getByRole('button', { name: /^close$/i }); + fireEvent.click(closeDetailBtn); + expect(screen.queryByRole('dialog')).toBeNull(); + // 2. ADD: Create a new group ride const createBtn = screen.getByRole('button', { name: /create group ride/i }); fireEvent.click(createBtn); @@ -371,17 +407,42 @@ describe('Group Rides Interactive Page & Components', () => { const radarTabBtn = screen.getByRole('button', { name: /live squad radar cockpit/i }); fireEvent.click(radarTabBtn); - expect(screen.getByText(/Active Squad Telemetry Stream/i)).toBeDefined(); - expect(screen.getByText(/Swarm Recon: Skyline Blvd > Coast Hwy/i)).toBeDefined(); + expect(screen.getByText(/Live Squad Telemetry Stream: Connected/i)).toBeDefined(); + expect(screen.getByText(/Formation session #CA-84-SW/i)).toBeDefined(); }); }); describe('Full GroupRidesPage Smart Routing', () => { - it('renders feature showcase for unauthenticated users', () => { + it('renders feature showcase for unauthenticated users', async () => { render(); - expect(screen.getByText(/Ride in pack formation/i)).toBeDefined(); + await waitFor(() => { + expect(screen.getByText(/Ride in pack formation/i)).toBeDefined(); + }); expect(screen.getByText(/Never lose a wingman/i)).toBeDefined(); }); + + it('renders group rides management console for authenticated users', () => { + const mockUser = { email: 'lead.rider@motorsignal.app', uid: 'test-user-123' } as any; + render(); + expect(screen.getByText(/Group Rides Management/i)).toBeDefined(); + expect(screen.getByText(/Squad Management & Telemetry Hub/i)).toBeDefined(); + }); + }); + + describe('GroupRidesManageRoute (/group-rides/manage)', () => { + it('redirects unauthenticated users immediately to /login?redirect=/group-rides/manage', async () => { + render(); + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/login?redirect=/group-rides/manage'); + }); + }); + }); + + describe('GroupRidesShowcaseRoute (/group-rides/showcase)', () => { + it('always renders showcase for visitors', async () => { + render(); + expect(screen.getByText(/Ride in pack formation/i)).toBeDefined(); + }); }); // ============================================================