'use client'; import React, { useState, useMemo, useEffect } from 'react'; import Link from 'next/link'; import { collection, query, where, onSnapshot, doc, writeBatch, updateDoc, deleteDoc, } 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 } | 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 [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 = rides; 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; }, [rides, paceFilter, searchQuery]); // 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 = () => { if (!leavingRide) 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 = (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}".`, 'info'); return { ...r, userStatus: 'waitlisted' }; } showAlert(`Joined pack for "${r.title}"! Telemetry stream linked.`, 'success'); return { ...r, userStatus: 'joined', participantCount: r.participantCount + 1, }; }) ); }; const handleJoinSuccess = (code: string, rideTitle: string) => { showAlert(`Synchronized telemetry with "${rideTitle}" (Code: ${code})!`, 'success'); }; 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; 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'}
) : isFull ? ( ) : ( )}
); })}
)} {/* 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} />
); }