'use client'; import React, { useState, useMemo, useEffect } from 'react'; import Link from 'next/link'; import { collection, query, where, onSnapshot, doc, writeBatch, updateDoc, deleteDoc, setDoc, arrayUnion, arrayRemove, increment, serverTimestamp, getDoc, } from 'firebase/firestore'; import { firestore } from '../../lib/firebase-client'; import { INITIAL_GROUP_RIDES } from './fixtures'; import { GroupRideSummary } from './types'; import { CreateRideModal } from './create-ride-modal'; import { EditRideModal } from './edit-ride-modal'; import { DeleteConfirmModal } from './delete-confirm-modal'; import { JoinRideModal } from './join-ride-modal'; import { LivePackRadar } from './live-pack-radar'; import { RideDetailsModal } from './ride-details-modal'; interface GroupRidesManagementProps { currentUser?: { uid?: string; email?: string | null; displayName?: string | null; getIdToken?: () => Promise } | null; onViewShowcase?: () => void; } function mapFirestoreDocToSummary( data: any, docId: string, currentUid?: string ): GroupRideSummary { const isOrganizer = Boolean(currentUid && data.organizerUid === currentUid); const isParticipant = Boolean( currentUid && Array.isArray(data.accessUids) && data.accessUids.includes(currentUid) ); let scheduledDate = 'Upcoming'; let scheduledTime = 'TBD'; if (data.scheduledStartAt) { try { const d = new Date(data.scheduledStartAt); if (!isNaN(d.getTime())) { scheduledDate = d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', }); scheduledTime = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', }); } } catch (_) {} } let distance = 'Open Route'; if (typeof data.routeDistanceMeters === 'number' && data.routeDistanceMeters > 0) { distance = `${(data.routeDistanceMeters / 1609.34).toFixed(1)} Miles`; } else if (data.distance) { distance = String(data.distance); } const meetingPoint = data.meetingPoint?.displayName || data.meetingPoint?.address || 'Designated Rally Point'; const region = data.meetingPoint?.address || data.meetingPoint?.displayName || data.region || 'Regional Run'; let routeSnippet = data.routeTitle; if (!routeSnippet && data.destination?.displayName) { routeSnippet = `${meetingPoint} > ${data.destination.displayName}`; } if (!routeSnippet) { routeSnippet = data.routeSnippet || 'Custom pack route staged on MotorSignal'; } const leadHandle = data.organizerDisplayName || data.organizerHandle || (isOrganizer ? 'You (Organizer)' : 'Pack Marshal'); const leadAvatarText = ( data.organizerDisplayName?.[0] || data.organizerHandle?.[0] || (isOrganizer ? 'Y' : 'P') ).toUpperCase(); const status = data.status || 'scheduled'; let userStatus: GroupRideSummary['userStatus'] = 'none'; if (isOrganizer || isParticipant) { userStatus = 'joined'; } return { id: docId, title: data.title || 'Untitled Ride', description: data.description || '', region, scheduledDate, scheduledTime, paceClassification: data.paceClassification || 'Brisk Pace', discipline: data.discipline || 'Sport / Canyon', formationProfile: data.formationProfile || 'Staggered Dual Column [B]', joinPolicy: data.joinPolicy || 'requestToJoin', leadHandle, leadAvatarText, participantCount: typeof data.participantCount === 'number' ? data.participantCount : 1, participantLimit: typeof data.participantLimit === 'number' ? data.participantLimit : 12, distance, meetingPoint, routeSnippet, userStatus, isOrganizer, status, rideCode: data.id || docId, clubName: data.clubName || undefined, }; } type ManagementTab = 'my-rides' | 'discover' | 'radar'; type MyRidesFilter = 'all' | 'organized' | 'joined'; export function GroupRidesManagement({ currentUser, onViewShowcase, }: GroupRidesManagementProps) { const [rides, setRides] = useState(INITIAL_GROUP_RIDES); const [communityRides, setCommunityRides] = useState([]); const [activeTab, setActiveTab] = useState('my-rides'); const [myRidesFilter, setMyRidesFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [paceFilter, setPaceFilter] = useState('all'); // Modals state const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isJoinModalOpen, setIsJoinModalOpen] = useState(false); const [editingRide, setEditingRide] = useState(null); const [deletingRide, setDeletingRide] = useState(null); const [leavingRide, setLeavingRide] = useState(null); const [selectedDetailRide, setSelectedDetailRide] = useState(null); // Toast alert const [activeAlert, setActiveAlert] = useState<{ message: string; type: 'success' | 'info' | 'error'; } | null>(null); const showAlert = (message: string, type: 'success' | 'info' | 'error' = 'success') => { setActiveAlert({ message, type }); setTimeout(() => { setActiveAlert(null); }, 4500); }; // KPIs const myOrganizedRides = useMemo(() => rides.filter((r) => r.isOrganizer), [rides]); const myJoinedRides = useMemo( () => rides.filter((r) => !r.isOrganizer && (r.userStatus === 'joined' || r.userStatus === 'requested')), [rides] ); const myTotalRides = useMemo( () => rides.filter((r) => r.isOrganizer || r.userStatus === 'joined'), [rides] ); // My Rides filtered list const filteredMyRides = useMemo(() => { let list = rides.filter((r) => { if (myRidesFilter === 'organized') return r.isOrganizer; if (myRidesFilter === 'joined') return !r.isOrganizer && (r.userStatus === 'joined' || r.userStatus === 'requested'); return r.isOrganizer || r.userStatus === 'joined' || r.userStatus === 'requested'; }); if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); list = list.filter( (r) => r.title.toLowerCase().includes(q) || r.region.toLowerCase().includes(q) || r.meetingPoint.toLowerCase().includes(q) ); } return list; }, [rides, myRidesFilter, searchQuery]); // Discover Rides filtered list const discoverRides = useMemo(() => { let list = communityRides; if (paceFilter !== 'all') { const pf = paceFilter.toLowerCase(); list = list.filter((r) => { const pace = r.paceClassification.toLowerCase(); if (pf === 'casual') { return pace.includes('casual') || pace.includes('tour') || pace.includes('endurance') || pace.includes('chill'); } return pace.includes(pf); }); } if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); list = list.filter( (r) => r.title.toLowerCase().includes(q) || r.region.toLowerCase().includes(q) || r.leadHandle.toLowerCase().includes(q) || (r.discipline && r.discipline.toLowerCase().includes(q)) ); } return list; }, [communityRides, paceFilter, searchQuery]); useEffect(() => { let active = true; const loadCommunityRides = async () => { if (!currentUser?.getIdToken) return; try { const token = await currentUser.getIdToken(); const response = await fetch('/api/group-rides?pageSize=50', { headers: { Authorization: `Bearer ${token}` }, cache: 'no-store', }); if (!response.ok) throw new Error('Could not load community rides.'); const payload = await response.json(); if (active && Array.isArray(payload.rides)) { setCommunityRides(payload.rides.map((ride: any) => { const date = new Date(ride.scheduledStartAt); return { ...ride, scheduledDate: date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }), scheduledTime: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }), } as GroupRideSummary; })); } } catch (error) { console.warn('[GroupRidesManagement] Community rides load failed:', error); if (active) showAlert('Community rides could not be loaded. Please try again.', 'error'); } }; void loadCommunityRides(); return () => { active = false; }; }, [currentUser?.uid]); // Realtime synchronization with Firestore useEffect(() => { if (!currentUser?.uid || !firestore) { return; } let isSubscribed = true; const ridesMap = new Map(); const updateCombinedRides = () => { if (!isSubscribed) return; const combined = Array.from(ridesMap.values()); combined.sort((a, b) => { if (a.status === 'canceled' && b.status !== 'canceled') return 1; if (a.status !== 'canceled' && b.status === 'canceled') return -1; return b.id.localeCompare(a.id); }); setRides(combined); }; let unsubOrganized: (() => void) | null = null; let unsubJoined: (() => void) | null = null; try { const qOrganized = query( collection(firestore, 'group_rides'), where('organizerUid', '==', currentUser.uid) ); unsubOrganized = onSnapshot( qOrganized, (snapshot) => { snapshot.docs.forEach((docSnap) => { ridesMap.set( docSnap.id, mapFirestoreDocToSummary(docSnap.data(), docSnap.id, currentUser.uid) ); }); snapshot.docChanges().forEach((change) => { if (change.type === 'removed') { ridesMap.delete(change.doc.id); } }); updateCombinedRides(); }, (err) => { console.warn('[GroupRidesManagement] Organized rides snapshot warning:', err); } ); const qJoined = query( collection(firestore, 'group_rides'), where('accessUids', 'array-contains', currentUser.uid) ); unsubJoined = onSnapshot( qJoined, (snapshot) => { snapshot.docs.forEach((docSnap) => { ridesMap.set( docSnap.id, mapFirestoreDocToSummary(docSnap.data(), docSnap.id, currentUser.uid) ); }); snapshot.docChanges().forEach((change) => { if (change.type === 'removed' && ridesMap.get(change.doc.id)?.isOrganizer === false) { ridesMap.delete(change.doc.id); } }); updateCombinedRides(); }, (err) => { console.warn('[GroupRidesManagement] Joined rides snapshot warning:', err); } ); } catch (e) { console.warn('[GroupRidesManagement] Subscription init warning:', e); } return () => { isSubscribed = false; if (unsubOrganized) unsubOrganized(); if (unsubJoined) unsubJoined(); }; }, [currentUser?.uid]); // Handlers for Add, Alter, Remove, Join, Leave const handleCreateRide = async (newRide: GroupRideSummary) => { const uid = currentUser?.uid; const now = new Date(); const rideId = uid ? `ride_${now.getTime()}_${uid.substring(0, Math.min(6, uid.length))}` : `MS-${Math.floor(1000 + Math.random() * 9000)}`; const rideWithOrganizer: GroupRideSummary = { ...newRide, id: rideId, isOrganizer: true, status: 'scheduled', userStatus: 'joined', rideCode: rideId, leadHandle: currentUser?.displayName || currentUser?.email?.split('@')[0] || 'MyHandle', leadAvatarText: (currentUser?.displayName?.[0] || currentUser?.email?.[0] || 'M').toUpperCase(), }; // Optimistic local update setRides((prev) => [rideWithOrganizer, ...prev]); showAlert(`Created new group ride "${newRide.title}"! Ride Code: ${rideId}`, 'success'); if (uid && firestore) { try { const leadDisplayName = currentUser.displayName || currentUser.email?.split('@')[0] || 'Organizer'; const leadHandle = currentUser.displayName?.toLowerCase().replace(/\s+/g, '_') || currentUser.email?.split('@')[0] || 'rider'; let scheduledStartAt = new Date(now.getTime() + 86400000).toISOString(); let departureAt = new Date(now.getTime() + 86400000 + 1800000).toISOString(); if (newRide.scheduledDate) { const parsed = new Date(`${newRide.scheduledDate} ${newRide.scheduledTime}`); if (!isNaN(parsed.getTime()) && parsed.getTime() > now.getTime() - 86400000) { scheduledStartAt = parsed.toISOString(); departureAt = new Date(parsed.getTime() + 30 * 60000).toISOString(); } } const firestoreRideData = { id: rideId, organizerUid: uid, organizerHandle: leadHandle, organizerDisplayName: leadDisplayName, title: newRide.title.trim(), description: newRide.description?.trim() || '', visibility: 'public', joinPolicy: newRide.joinPolicy || 'requestToJoin', status: 'scheduled', scheduledStartAt, departureAt, participantLimit: Number(newRide.participantLimit) || 12, participantCount: 1, accessUids: [uid], meetingPoint: { latitude: 34.5378, longitude: -83.9849, displayName: newRide.meetingPoint || newRide.region || 'Designated Rally Point', address: newRide.region || '', locationType: 'meetup', notes: '', }, stops: [], createdAt: now.toISOString(), updatedAt: now.toISOString(), paceClassification: newRide.paceClassification || 'Brisk Pace', discipline: newRide.discipline || 'Sport / Canyon', formationProfile: newRide.formationProfile || 'Staggered Dual Column [B]', routeTitle: newRide.routeSnippet || '', }; const organizerParticipant = { userId: uid, role: 'organizer', status: 'accepted', riderHandle: leadHandle, riderDisplayName: leadDisplayName, createdAt: now.toISOString(), updatedAt: now.toISOString(), }; const batch = writeBatch(firestore); batch.set(doc(firestore, 'group_rides', rideId), firestoreRideData); batch.set(doc(firestore, 'group_rides', rideId, 'participants', uid), organizerParticipant); await batch.commit(); } catch (err: any) { console.error('Failed to persist group ride to Firestore:', err); } } }; const handleSaveEditedRide = async (updatedRide: GroupRideSummary) => { // Optimistic local update setRides((prev) => prev.map((r) => (r.id === updatedRide.id ? updatedRide : r))); showAlert(`Updated parameters for "${updatedRide.title}".`, 'success'); if (currentUser?.uid && firestore) { try { const now = new Date().toISOString(); await updateDoc(doc(firestore, 'group_rides', updatedRide.id), { title: updatedRide.title.trim(), description: updatedRide.description?.trim() || '', participantLimit: Number(updatedRide.participantLimit) || 12, joinPolicy: updatedRide.joinPolicy || 'requestToJoin', paceClassification: updatedRide.paceClassification || 'Brisk Pace', discipline: updatedRide.discipline || 'Sport / Canyon', formationProfile: updatedRide.formationProfile || 'Staggered Dual Column [B]', routeTitle: updatedRide.routeSnippet || '', updatedAt: now, }); } catch (err: any) { console.error('Failed to update group ride in Firestore:', err); } } }; const handleDeleteRide = async () => { if (!deletingRide) return; const title = deletingRide.title; const rideId = deletingRide.id; setDeletingRide(null); // Optimistic local update setRides((prev) => prev.filter((r) => r.id !== rideId)); showAlert(`Deleted group ride "${title}". Pack telemetry cleared.`, 'info'); if (currentUser?.uid && firestore) { try { await deleteDoc(doc(firestore, 'group_rides', rideId)); } catch (err: any) { console.error('Failed to delete group ride in Firestore:', err); } } }; const handleCancelRide = async (ride: GroupRideSummary) => { // Optimistic local update setRides((prev) => prev.map((r) => (r.id === ride.id ? { ...r, status: 'canceled' } : r)) ); showAlert(`Ride "${ride.title}" status changed to Canceled. Participants notified.`, 'info'); if (currentUser?.uid && firestore) { try { const now = new Date().toISOString(); await updateDoc(doc(firestore, 'group_rides', ride.id), { status: 'canceled', canceledAt: now, canceledReason: 'Canceled by organizer via MotorSignal Web Console', updatedAt: now, }); } catch (err: any) { console.error('Failed to cancel group ride in Firestore:', err); } } }; const handleLeaveRide = async () => { if (!leavingRide) return; const ride = leavingRide; const uid = currentUser?.uid; if (!uid || !firestore) { showAlert('Sign in to leave a group ride.', 'error'); return; } try { const rideRef = doc(firestore, 'group_rides', ride.id); const participantRef = doc(rideRef, 'participants', uid); if (ride.userStatus === 'requested') { await updateDoc(participantRef, { status: 'left', updatedAt: serverTimestamp() }); } else { const snapshot = await getDoc(rideRef); const accessUids = snapshot.data()?.accessUids; if (!Array.isArray(accessUids) || !accessUids.includes(uid)) throw new Error('Ride membership is out of date.'); const batch = writeBatch(firestore); batch.update(participantRef, { status: 'left', updatedAt: serverTimestamp() }); batch.update(rideRef, { accessUids: arrayRemove(uid), participantCount: increment(-1), updatedAt: serverTimestamp(), }); await batch.commit(); } } catch (error) { console.error('Failed to leave group ride:', error); showAlert('Could not leave this ride. Refresh and try again.', 'error'); return; } const title = leavingRide.title; setRides((prev) => prev.map((r) => { if (r.id !== leavingRide.id) return r; return { ...r, userStatus: 'none', participantCount: Math.max(0, r.participantCount - 1), }; }) ); setLeavingRide(null); showAlert(`Left pack for "${title}". Slot relinquished.`, 'info'); }; const handleRequestSlot = async (rideId: string): Promise<'joined' | 'requested' | null> => { const ride = [...communityRides, ...rides].find((item) => item.id === rideId); const uid = currentUser?.uid; if (!ride || !uid || !firestore) { showAlert('Sign in to join a group ride.', 'error'); return null; } if (ride.userStatus === 'joined' || ride.userStatus === 'requested') return ride.userStatus; if (ride.joinPolicy === 'inviteOnly') { showAlert('This ride is invite-only. Ask the organizer for an invitation.', 'info'); return null; } if (ride.participantCount >= ride.participantLimit) { showAlert('This ride is full. Its waitlist is not available yet.', 'info'); return null; } try { const rideRef = doc(firestore, 'group_rides', ride.id); const participantRef = doc(rideRef, 'participants', uid); const member = { userId: uid, role: 'participant', status: ride.joinPolicy === 'open' ? 'accepted' : 'requested', createdAt: serverTimestamp(), updatedAt: serverTimestamp(), }; if (ride.joinPolicy === 'open') { const batch = writeBatch(firestore); batch.set(participantRef, member); batch.update(rideRef, { accessUids: arrayUnion(uid), participantCount: increment(1), updatedAt: serverTimestamp(), }); await batch.commit(); } else { await setDoc(participantRef, member); } const userStatus = ride.joinPolicy === 'open' ? 'joined' : 'requested'; setCommunityRides((prev) => prev.map((item) => item.id === rideId ? { ...item, userStatus, participantCount: item.participantCount + (userStatus === 'joined' ? 1 : 0), } : item)); showAlert(userStatus === 'joined' ? `Joined "${ride.title}".` : `Join request sent for "${ride.title}".`, 'success'); return userStatus; } catch (error) { console.error('Failed to join group ride:', error); showAlert('Could not join this ride. It may have filled; refresh and try again.', 'error'); return null; } }; const handleJoinSuccess = async (code: string) => { const ride = communityRides.find((item) => item.id.toUpperCase() === code.toUpperCase()); if (!ride) throw new Error('No public ride was found for that code.'); const status = await handleRequestSlot(ride.id); if (!status) throw new Error('This ride could not be joined.'); return { title: ride.title, lead: ride.leadHandle, status }; }; const copyRideCode = (code?: string) => { if (!code) return; navigator.clipboard?.writeText(code); showAlert(`Copied ride code "${code}" to clipboard!`, 'success'); }; return (
{/* Dynamic Atmospheric Glow Background Layers */}
{/* Toast Notification Alert */} {activeAlert && (
{activeAlert.type === 'success' ? 'check_circle' : activeAlert.type === 'error' ? 'error' : 'info'} {activeAlert.message}
)} {/* Top Command Bar & Showcase Switch */}
Rider Console Active
• {currentUser?.email ? currentUser.email : 'Rider Account'}
{/* Showcase Mode Link */}
{onViewShowcase ? ( ) : ( visibility Feature Showcase )} person Rider Portal
{/* Header & Quick Action Region */}
groups Squad Management & Telemetry Hub

Group Rides Management

Schedule new pack formations, modify routes, manage roster slots, and monitor live squadron mesh telemetry.

{/* Action Buttons */}
{/* Metrics KPI Cards: Active Squads Nearby, Total Miles in Formation, Pack Safety Score, Mesh Node Health */}
Active Squads Nearby
{rides.length} Squads
Within 25 Mile Radius
Total Miles in Formation
1,420 mi
Squad Aggregate Logged
Pack Safety Score
98.4%
Optimal Paceline Spacing
Mesh Node Health
6/6 Nodes
12ms Latency • Online
{/* Main Navigation Tabs */}
{/* TAB 1: MY PACK RUNS (VIEW, ALTER, REMOVE, LEAVE) */} {activeTab === 'my-rides' && (
{/* Filter Sub-Bar */}
{/* Search input */}
search setSearchQuery(e.target.value)} className="w-full pl-9 pr-3.5 py-1.5 rounded-xl bg-surface-container border border-outline-variant/30 text-xs text-on-surface focus:outline-none focus:ring-1 focus:ring-primary-container" />
{/* Rides List */} {filteredMyRides.length === 0 ? (
sports_motorsports

No group rides in this view

Schedule a new run to lead as Road Captain, or browse the community directory to join an upcoming pack run.

) : (
{filteredMyRides.map((ride) => { 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) ) : ( Squad Wingman )} {statusText}
{/* Ride Code Button */} {ride.rideCode && ( )}
{/* Ride Title & Region */}

{ride.title}

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

{ride.description}

{/* Meeting Point, Schedule, Route Snippet */}
schedule {ride.scheduledDate} • {ride.scheduledTime}
flag Rally: {ride.meetingPoint}
{ride.routeSnippet && (
route {ride.routeSnippet}
)}
group Pack: {ride.participantCount}/{ride.participantLimit} Riders
{/* Management Controls Row (VIEW DETAILS, ALTER, REMOVE, CANCEL, LEAVE, RADAR) */}
{isActiveRide && ( )} {isOrg && ( <> {!isCanceled && ( )} )}
{isOrg ? ( ) : ( )}
); })}
)}
)} {/* TAB 2: EXPLORE COMMUNITY RUNS (VIEW, SEARCH, REQUEST SLOT) */} {activeTab === 'discover' && (
{/* Filter controls */}
search setSearchQuery(e.target.value)} className="w-full pl-9 pr-3.5 py-1.5 rounded-xl bg-surface-container border border-outline-variant/30 text-xs text-on-surface focus:outline-none focus:ring-1 focus:ring-primary-container" />
{/* Directory Grid */}
{discoverRides.map((ride) => { const isJoined = ride.userStatus === 'joined' || ride.isOrganizer; const isFull = ride.participantCount >= ride.participantLimit; const isRequested = ride.userStatus === 'requested'; return (
{ride.paceClassification} {ride.scheduledDate} • {ride.scheduledTime}

{ride.title}

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

{ride.description}

location_on {ride.region}
flag Rally: {ride.meetingPoint}
{ride.routeSnippet && (
route {ride.routeSnippet}
)}
Lead: {ride.leadHandle} {ride.participantCount}/{ride.participantLimit} Riders
{isJoined ? (
check_circle {ride.isOrganizer ? 'Organized by You' : 'Slot Confirmed'}
) : isRequested ? (
Request Pending
) : isFull ? ( ) : ride.joinPolicy === 'inviteOnly' ? ( ) : ( )}
); })}
)} {/* TAB 3: LIVE SQUAD RADAR COCKPIT */} {activeTab === 'radar' && (
Live Squad Telemetry Stream: Connected

Live Formation Radar Cockpit

Formation session #CA-84-SW • 6 squad nodes reporting sub-second GNSS multi-band coordinates and lean angles.

Mesh Link: 12ms Latency
)} {/* Modals */} setIsCreateModalOpen(false)} onCreateRide={handleCreateRide} /> setEditingRide(null)} onSaveRide={handleSaveEditedRide} /> setDeletingRide(null)} onConfirm={handleDeleteRide} /> setLeavingRide(null)} onConfirm={handleLeaveRide} /> setIsJoinModalOpen(false)} onJoinSuccess={handleJoinSuccess} /> setSelectedDetailRide(null)} onEdit={(r) => setEditingRide(r)} onRequestSlot={handleRequestSlot} />
); }