MotorSignal-App/website/components/group-rides/group-rides-management.tsx

1373 lines
63 KiB
TypeScript

'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<string> } | 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<GroupRideSummary[]>(INITIAL_GROUP_RIDES);
const [communityRides, setCommunityRides] = useState<GroupRideSummary[]>([]);
const [activeTab, setActiveTab] = useState<ManagementTab>('my-rides');
const [myRidesFilter, setMyRidesFilter] = useState<MyRidesFilter>('all');
const [searchQuery, setSearchQuery] = useState('');
const [paceFilter, setPaceFilter] = useState<string>('all');
// Modals state
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isJoinModalOpen, setIsJoinModalOpen] = useState(false);
const [editingRide, setEditingRide] = useState<GroupRideSummary | null>(null);
const [deletingRide, setDeletingRide] = useState<GroupRideSummary | null>(null);
const [leavingRide, setLeavingRide] = useState<GroupRideSummary | null>(null);
const [selectedDetailRide, setSelectedDetailRide] = useState<GroupRideSummary | null>(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<string, GroupRideSummary>();
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 (
<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'
: activeAlert.type === 'error'
? 'bg-error-container/90 text-on-error-container border-error/40'
: 'bg-surface-container-high/90 text-primary border-primary/40'
}`}
>
<span className="material-symbols-outlined text-[20px]">
{activeAlert.type === 'success' ? 'check_circle' : activeAlert.type === 'error' ? 'error' : '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>
)}
{/* Top Command Bar & Showcase Switch */}
<div className="bg-surface-container-low border-b border-outline-variant/30 py-3 px-4 sm:px-8">
<div className="max-w-container-max mx-auto flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<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 w-2 bg-secondary-fixed-dim"></span>
</span>
<span className="font-mono text-xs font-semibold text-secondary-fixed">
Rider Console Active
</span>
</div>
<span className="text-outline-variant/40 hidden sm:inline">•</span>
<span className="text-xs text-on-surface-variant hidden sm:inline font-mono">
{currentUser?.email ? currentUser.email : 'Rider Account'}
</span>
</div>
{/* Showcase Mode Link */}
<div className="flex items-center gap-3">
{onViewShowcase ? (
<button
type="button"
onClick={onViewShowcase}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface hover:text-primary-container transition cursor-pointer"
title="View public features showcase"
>
<span className="material-symbols-outlined text-[16px] text-primary-container">
visibility
</span>
<span>Feature Showcase</span>
</button>
) : (
<Link
href="/group-rides/showcase"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface hover:text-primary-container transition"
title="View public features showcase"
>
<span className="material-symbols-outlined text-[16px] text-primary-container">
visibility
</span>
<span>Feature Showcase</span>
</Link>
)}
<Link
href="/account"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface transition"
>
<span className="material-symbols-outlined text-[16px] text-secondary">
person
</span>
<span className="hidden sm:inline">Rider Portal</span>
</Link>
</div>
</div>
</div>
{/* Header & Quick Action Region */}
<section className="max-w-container-max mx-auto px-gutter-desktop pt-space-xl pb-space-xl">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pb-space-lg border-b border-outline-variant/20">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary-container/15 border border-primary-container/30 text-primary-container text-xs font-mono font-semibold mb-2">
<span className="material-symbols-outlined text-[16px]">groups</span>
<span>Squad Management &amp; Telemetry Hub</span>
</div>
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight">
Group Rides Management
</h1>
<p className="font-body-md text-sm sm:text-base text-on-surface-variant max-w-2xl mt-1">
Schedule new pack formations, modify routes, manage roster slots, and monitor live squadron
mesh telemetry.
</p>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => setIsCreateModalOpen(true)}
className="px-5 py-3 rounded-xl bg-gradient-to-r from-primary-container to-primary-fixed text-on-primary-container font-headline-sm text-xs sm:text-sm font-bold shadow-lg hover:shadow-[0_0_20px_rgba(0,242,254,0.45)] transition cursor-pointer flex items-center gap-2"
id="btn-create-ride-mgmt"
>
<span className="material-symbols-outlined text-[18px]">add_circle</span>
<span>Create Group Ride</span>
</button>
<button
type="button"
onClick={() => setIsJoinModalOpen(true)}
className="px-4 py-3 rounded-xl bg-surface-container-high hover:bg-surface-container-highest border border-outline-variant/30 text-on-surface font-headline-sm text-xs sm:text-sm font-semibold transition cursor-pointer flex items-center gap-2"
id="btn-join-code-mgmt"
>
<span className="material-symbols-outlined text-primary-fixed-dim text-[18px]">
qr_code_scanner
</span>
<span>Join with Code</span>
</button>
</div>
</div>
{/* Metrics KPI Cards: Active Squads Nearby, Total Miles in Formation, Pack Safety Score, Mesh Node Health */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mt-6">
<div className="p-4 rounded-2xl bg-surface-container-low border border-outline-variant/20 flex flex-col justify-between">
<span className="font-mono text-xs text-on-surface-variant">Active Squads Nearby</span>
<div className="text-2xl font-extrabold font-mono text-on-surface mt-1">
{rides.length} Squads
</div>
<span className="text-[11px] font-mono text-secondary-fixed mt-1">
Within 25 Mile Radius
</span>
</div>
<div className="p-4 rounded-2xl bg-surface-container-low border border-outline-variant/20 flex flex-col justify-between">
<span className="font-mono text-xs text-on-surface-variant">Total Miles in Formation</span>
<div className="text-2xl font-extrabold font-mono text-primary-container mt-1">
1,420 mi
</div>
<span className="text-[11px] font-mono text-on-surface-variant mt-1">
Squad Aggregate Logged
</span>
</div>
<div className="p-4 rounded-2xl bg-surface-container-low border border-outline-variant/20 flex flex-col justify-between">
<span className="font-mono text-xs text-on-surface-variant">Pack Safety Score</span>
<div className="text-2xl font-extrabold font-mono text-secondary-fixed mt-1">
98.4%
</div>
<span className="text-[11px] font-mono text-on-surface-variant mt-1">
Optimal Paceline Spacing
</span>
</div>
<div className="p-4 rounded-2xl bg-surface-container-low border border-outline-variant/20 flex flex-col justify-between">
<span className="font-mono text-xs text-on-surface-variant">Mesh Node Health</span>
<div className="text-2xl font-extrabold font-mono text-primary-fixed mt-1">
6/6 Nodes
</div>
<span className="text-[11px] font-mono text-secondary-fixed mt-1">
12ms Latency • Online
</span>
</div>
</div>
</section>
{/* Main Navigation Tabs */}
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-2xl">
<div className="flex border-b border-outline-variant/20 gap-2 sm:gap-6 overflow-x-auto">
<button
type="button"
onClick={() => setActiveTab('my-rides')}
className={`pb-3 text-sm font-semibold transition whitespace-nowrap flex items-center gap-2 cursor-pointer border-b-2 ${
activeTab === 'my-rides'
? 'border-primary-container text-primary-container font-bold'
: 'border-transparent text-on-surface-variant hover:text-on-surface'
}`}
>
<span className="material-symbols-outlined text-[18px]">sports_motorsports</span>
<span>My Pack Runs ({myTotalRides.length})</span>
</button>
<button
type="button"
onClick={() => setActiveTab('discover')}
className={`pb-3 text-sm font-semibold transition whitespace-nowrap flex items-center gap-2 cursor-pointer border-b-2 ${
activeTab === 'discover'
? 'border-primary-container text-primary-container font-bold'
: 'border-transparent text-on-surface-variant hover:text-on-surface'
}`}
>
<span className="material-symbols-outlined text-[18px]">explore</span>
<span>Explore Community Rides ({communityRides.length})</span>
</button>
<button
type="button"
onClick={() => setActiveTab('radar')}
className={`pb-3 text-sm font-semibold transition whitespace-nowrap flex items-center gap-2 cursor-pointer border-b-2 ${
activeTab === 'radar'
? 'border-primary-container text-primary-container font-bold'
: 'border-transparent text-on-surface-variant hover:text-on-surface'
}`}
>
<span className="material-symbols-outlined text-[18px]">radar</span>
<span>Live Squad Radar Cockpit</span>
</button>
</div>
</section>
{/* TAB 1: MY PACK RUNS (VIEW, ALTER, REMOVE, LEAVE) */}
{activeTab === 'my-rides' && (
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
{/* Filter Sub-Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-2 bg-surface-container-low p-1 rounded-xl border border-outline-variant/20">
<button
type="button"
onClick={() => setMyRidesFilter('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
myRidesFilter === 'all'
? 'bg-surface-container-high text-on-surface'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
All My Rides ({myTotalRides.length})
</button>
<button
type="button"
onClick={() => setMyRidesFilter('organized')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
myRidesFilter === 'organized'
? 'bg-surface-container-high text-primary-container'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Organized by Me ({myOrganizedRides.length})
</button>
<button
type="button"
onClick={() => setMyRidesFilter('joined')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
myRidesFilter === 'joined'
? 'bg-surface-container-high text-secondary-fixed'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Joined as Wingman ({myJoinedRides.length})
</button>
</div>
{/* Search input */}
<div className="relative max-w-xs w-full">
<span className="material-symbols-outlined absolute left-3 top-2.5 text-on-surface-variant text-[18px]">
search
</span>
<input
type="text"
placeholder="Search my pack runs..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
{/* Rides List */}
{filteredMyRides.length === 0 ? (
<div className="text-center py-16 px-4 bg-surface-container-low rounded-2xl border border-outline-variant/20 space-y-4">
<div className="w-14 h-14 rounded-2xl bg-surface-container-high mx-auto flex items-center justify-center text-primary-container">
<span className="material-symbols-outlined text-[28px]">sports_motorsports</span>
</div>
<div>
<h3 className="font-headline-md text-base font-bold text-on-surface">
No group rides in this view
</h3>
<p className="font-body-sm text-xs text-on-surface-variant mt-1 max-w-md mx-auto">
Schedule a new run to lead as Road Captain, or browse the community directory to join an upcoming pack run.
</p>
</div>
<div className="flex justify-center gap-3 pt-2">
<button
type="button"
onClick={() => setIsCreateModalOpen(true)}
className="px-4 py-2 rounded-xl bg-primary-container text-on-primary-container text-xs font-bold"
>
+ Create Group Ride
</button>
<button
type="button"
onClick={() => setActiveTab('discover')}
className="px-4 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface"
>
Browse Community Runs
</button>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-space-lg">
{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 (
<div
key={ride.id}
className={`rounded-2xl border flex flex-col justify-between transition-all shadow-md overflow-hidden ${
isCanceled
? 'bg-surface-container-low/60 border-error/30 opacity-75'
: 'bg-surface-container-low border-outline-variant/30 hover:border-outline-variant/60 hover:shadow-xl'
}`}
>
<div className="p-space-lg">
{/* Role & Status Header */}
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-1.5 flex-wrap">
{isOrg ? (
<span className="px-2 py-0.5 rounded bg-primary-container/20 text-primary-container border border-primary-container/30 text-[10px] font-mono font-bold">
Road Captain (Organizer)
</span>
) : (
<span className="px-2 py-0.5 rounded bg-secondary-fixed/20 text-secondary-fixed border border-secondary-fixed/30 text-[10px] font-mono font-bold">
Squad Wingman
</span>
)}
<span
className={`px-2 py-0.5 rounded text-[10px] font-mono font-bold uppercase border ${
isCanceled
? 'bg-error/20 text-error border-error/30'
: isActiveRide
? 'bg-secondary-fixed/20 text-secondary-fixed border-secondary-fixed/30 animate-pulse'
: 'bg-surface-container-high text-on-surface-variant border-outline-variant/30'
}`}
>
{statusText}
</span>
</div>
{/* Ride Code Button */}
{ride.rideCode && (
<button
type="button"
onClick={() => copyRideCode(ride.rideCode)}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-container text-[10px] font-mono text-on-surface-variant hover:text-primary-container transition cursor-pointer"
title="Click to copy ride code"
>
<span>{ride.rideCode}</span>
<span className="material-symbols-outlined text-[12px]">content_copy</span>
</button>
)}
</div>
{/* Ride Title & Region */}
<h3 className="font-headline-md text-lg font-bold text-on-surface mb-1">
{ride.title}
</h3>
<div className="flex flex-wrap items-center gap-2 text-xs font-mono text-on-surface-variant mb-2">
<span className="px-1.5 py-0.5 rounded bg-surface-container text-primary-container text-[10px] font-semibold border border-outline-variant/30">
{ride.discipline || 'Sport / Canyon'}
</span>
<span className="px-1.5 py-0.5 rounded bg-surface-container text-secondary-fixed text-[10px] font-semibold border border-outline-variant/30">
{ride.paceClassification}
</span>
<span>•</span>
<span>{ride.distance}</span>
</div>
<p className="font-body-sm text-xs text-on-surface-variant line-clamp-2 mb-3">
{ride.description}
</p>
{/* Meeting Point, Schedule, Route Snippet */}
<div className="p-2.5 rounded-xl bg-surface-container/60 border border-outline-variant/20 text-xs font-mono space-y-1 mb-3">
<div className="flex items-center gap-1.5 text-on-surface">
<span className="material-symbols-outlined text-[14px] text-primary-container">
schedule
</span>
<span>{ride.scheduledDate} • {ride.scheduledTime}</span>
</div>
<div className="flex items-center gap-1.5 text-on-surface-variant truncate">
<span className="material-symbols-outlined text-[14px] text-secondary-fixed">
flag
</span>
<span className="truncate">Rally: {ride.meetingPoint}</span>
</div>
{ride.routeSnippet && (
<div className="flex items-center gap-1.5 text-on-surface-variant truncate">
<span className="material-symbols-outlined text-[14px] text-primary-fixed-dim">
route
</span>
<span className="truncate text-[11px] text-primary-fixed-dim">{ride.routeSnippet}</span>
</div>
)}
<div className="flex items-center gap-1.5 text-on-surface-variant">
<span className="material-symbols-outlined text-[14px] text-primary-fixed-dim">
group
</span>
<span>
Pack: {ride.participantCount}/{ride.participantLimit} Riders
</span>
</div>
</div>
</div>
{/* Management Controls Row (VIEW DETAILS, ALTER, REMOVE, CANCEL, LEAVE, RADAR) */}
<div className="p-space-lg pt-0 border-t border-outline-variant/20 mt-1 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
onClick={() => setSelectedDetailRide(ride)}
className="px-2.5 py-1.5 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface transition cursor-pointer flex items-center gap-1"
title="View full ride details"
>
<span className="material-symbols-outlined text-[15px]">info</span>
<span>Details</span>
</button>
{isActiveRide && (
<button
type="button"
onClick={() => setActiveTab('radar')}
className="px-2.5 py-1.5 rounded-xl bg-secondary-fixed/20 hover:bg-secondary-fixed/30 border border-secondary-fixed/40 text-secondary-fixed text-xs font-semibold transition cursor-pointer flex items-center gap-1"
title="Open Live Radar feed for this active run"
>
<span className="material-symbols-outlined text-[15px]">radar</span>
<span>Open Live Radar</span>
</button>
)}
{isOrg && (
<>
<button
type="button"
onClick={() => setEditingRide(ride)}
className="px-3 py-1.5 rounded-xl bg-primary-container/15 hover:bg-primary-container/25 text-primary-container border border-primary-container/30 text-xs font-semibold transition cursor-pointer flex items-center gap-1"
title="Alter ride parameters"
>
<span className="material-symbols-outlined text-[15px]">edit</span>
<span>Alter / Edit</span>
</button>
{!isCanceled && (
<button
type="button"
onClick={() => handleCancelRide(ride)}
className="px-2.5 py-1.5 rounded-xl bg-surface-container hover:bg-surface-container-high text-xs font-semibold text-on-surface-variant hover:text-error transition cursor-pointer"
title="Cancel ride"
>
Cancel
</button>
)}
</>
)}
</div>
<div>
{isOrg ? (
<button
type="button"
onClick={() => setDeletingRide(ride)}
className="px-2.5 py-1.5 rounded-xl text-xs font-semibold text-error hover:bg-error/10 transition cursor-pointer flex items-center gap-1"
title="Delete group ride permanently"
>
<span className="material-symbols-outlined text-[15px]">delete</span>
<span>Delete</span>
</button>
) : (
<button
type="button"
onClick={() => setLeavingRide(ride)}
className="px-3 py-1.5 rounded-xl bg-surface-container hover:bg-error/15 text-error border border-outline-variant/30 hover:border-error/40 text-xs font-semibold transition cursor-pointer flex items-center gap-1"
title="Leave group ride"
>
<span className="material-symbols-outlined text-[15px]">logout</span>
<span>Leave Pack</span>
</button>
)}
</div>
</div>
</div>
);
})}
</div>
)}
</section>
)}
{/* TAB 2: EXPLORE COMMUNITY RUNS (VIEW, SEARCH, REQUEST SLOT) */}
{activeTab === 'discover' && (
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
{/* Filter controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<div className="flex flex-wrap items-center gap-2 bg-surface-container-low p-1 rounded-xl border border-outline-variant/20">
<button
type="button"
onClick={() => setPaceFilter('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
paceFilter === 'all'
? 'bg-surface-container-high text-on-surface font-bold'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
All Runs ({rides.length})
</button>
<button
type="button"
onClick={() => setPaceFilter('Casual')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
paceFilter === 'Casual'
? 'bg-surface-container-high text-on-surface font-bold'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Casual
</button>
<button
type="button"
onClick={() => setPaceFilter('Brisk')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
paceFilter === 'Brisk'
? 'bg-surface-container-high text-on-surface font-bold'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Brisk
</button>
<button
type="button"
onClick={() => setPaceFilter('Spirited')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
paceFilter === 'Spirited'
? 'bg-surface-container-high text-on-surface font-bold'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Spirited
</button>
<button
type="button"
onClick={() => setPaceFilter('Track')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition ${
paceFilter === 'Track'
? 'bg-surface-container-high text-on-surface font-bold'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
Track Pace
</button>
</div>
<div className="relative max-w-sm w-full">
<span className="material-symbols-outlined absolute left-3 top-2.5 text-on-surface-variant text-[18px]">
search
</span>
<input
type="text"
placeholder="Search by title, city, or lead..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
{/* Directory Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-space-lg">
{discoverRides.map((ride) => {
const isJoined = ride.userStatus === 'joined' || ride.isOrganizer;
const isFull = ride.participantCount >= ride.participantLimit;
const isRequested = ride.userStatus === 'requested';
return (
<div
key={ride.id}
className="bg-surface-container-low rounded-2xl border border-outline-variant/30 overflow-hidden shadow-md hover:shadow-xl transition flex flex-col justify-between"
>
<div className="p-space-lg">
<div className="flex items-center justify-between gap-2 mb-2">
<span className="font-mono text-[10px] uppercase px-2 py-0.5 rounded bg-surface-container-high text-primary-container border border-outline-variant/30 font-semibold">
{ride.paceClassification}
</span>
<span className="font-mono text-xs text-on-surface-variant">
{ride.scheduledDate} • {ride.scheduledTime}
</span>
</div>
<h3 className="font-headline-md text-base font-bold text-on-surface mb-1">
{ride.title}
</h3>
<div className="flex flex-wrap items-center gap-2 text-xs font-mono text-on-surface-variant mb-2">
<span className="px-1.5 py-0.5 rounded bg-surface-container text-primary-container text-[10px] font-semibold border border-outline-variant/30">
{ride.discipline || 'Sport / Canyon'}
</span>
<span>•</span>
<span>{ride.distance}</span>
</div>
<p className="font-body-sm text-xs text-on-surface-variant line-clamp-2 mb-3">
{ride.description}
</p>
<div className="space-y-1 text-xs font-mono text-on-surface-variant mb-3">
<div className="flex items-center gap-1.5 truncate">
<span className="material-symbols-outlined text-[14px] text-secondary-fixed">
location_on
</span>
<span className="truncate">{ride.region}</span>
</div>
<div className="flex items-center gap-1.5 truncate">
<span className="material-symbols-outlined text-[14px] text-primary-container">
flag
</span>
<span className="truncate">Rally: {ride.meetingPoint}</span>
</div>
{ride.routeSnippet && (
<div className="flex items-center gap-1.5 truncate text-[11px] text-primary-fixed-dim">
<span className="material-symbols-outlined text-[14px]">route</span>
<span className="truncate">{ride.routeSnippet}</span>
</div>
)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-outline-variant/20 text-xs font-mono text-on-surface-variant">
<span>Lead: {ride.leadHandle}</span>
<span className={isFull ? 'text-error font-bold' : 'text-secondary-fixed'}>
{ride.participantCount}/{ride.participantLimit} Riders
</span>
</div>
</div>
<div className="p-space-lg pt-0 flex items-center gap-2">
<button
type="button"
onClick={() => setSelectedDetailRide(ride)}
className="px-3 py-2 rounded-xl bg-surface-container hover:bg-surface-container-high border border-outline-variant/30 text-xs font-semibold text-on-surface transition"
title="View full ride parameters"
>
Details
</button>
{isJoined ? (
<div className="flex-1 py-2 rounded-xl bg-secondary-fixed/20 border border-secondary-fixed/30 text-secondary-fixed text-xs font-bold flex items-center justify-center gap-1.5">
<span className="material-symbols-outlined text-[16px]">check_circle</span>
<span>{ride.isOrganizer ? 'Organized by You' : 'Slot Confirmed'}</span>
</div>
) : isRequested ? (
<div className="flex-1 py-2 rounded-xl bg-surface-container text-on-surface-variant text-xs font-semibold text-center">Request Pending</div>
) : isFull ? (
<button type="button" disabled className="flex-1 py-2 rounded-xl bg-surface-container text-on-surface-variant text-xs font-semibold cursor-not-allowed">Ride Full</button>
) : ride.joinPolicy === 'inviteOnly' ? (
<button type="button" disabled className="flex-1 py-2 rounded-xl bg-surface-container text-on-surface-variant text-xs font-semibold cursor-not-allowed">Invite Only</button>
) : (
<button
type="button"
onClick={() => handleRequestSlot(ride.id)}
className="flex-1 py-2 rounded-xl bg-primary-container hover:bg-primary-fixed text-on-primary-container text-xs font-bold transition flex items-center justify-center gap-1.5 shadow-sm"
>
<span className="material-symbols-outlined text-[16px]">how_to_reg</span>
<span>Request Pack Slot</span>
</button>
)}
</div>
</div>
);
})}
</div>
</section>
)}
{/* TAB 3: LIVE SQUAD RADAR COCKPIT */}
{activeTab === 'radar' && (
<section className="max-w-container-max mx-auto px-gutter-desktop mb-space-4xl">
<div className="mb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-4 rounded-2xl bg-surface-container-low border border-outline-variant/20">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="w-2 h-2 rounded-full bg-secondary-fixed animate-ping" />
<span className="font-mono text-xs text-primary-container font-semibold">
Live Squad Telemetry Stream: Connected
</span>
</div>
<h2 className="font-headline-lg text-xl font-bold text-on-surface">
Live Formation Radar Cockpit
</h2>
<p className="font-body-sm text-xs text-on-surface-variant mt-0.5">
Formation session #CA-84-SW • 6 squad nodes reporting sub-second GNSS multi-band coordinates and lean angles.
</p>
</div>
<div className="flex items-center gap-2">
<span className="px-3 py-1 rounded-xl bg-surface-container border border-outline-variant/30 text-xs font-mono text-secondary-fixed">
Mesh Link: 12ms Latency
</span>
</div>
</div>
<LivePackRadar initialMode="tactical" />
</section>
)}
{/* Modals */}
<CreateRideModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onCreateRide={handleCreateRide}
/>
<EditRideModal
isOpen={Boolean(editingRide)}
ride={editingRide}
onClose={() => setEditingRide(null)}
onSaveRide={handleSaveEditedRide}
/>
<DeleteConfirmModal
isOpen={Boolean(deletingRide)}
title="Delete Group Ride"
message={`Are you sure you want to permanently delete "${deletingRide?.title}"? All telemetry sync channels, participant slots, and ride history will be deleted.`}
confirmText="Delete Ride"
cancelText="Keep Ride"
danger={true}
onClose={() => setDeletingRide(null)}
onConfirm={handleDeleteRide}
/>
<DeleteConfirmModal
isOpen={Boolean(leavingRide)}
title="Leave Group Ride"
message={`Are you sure you want to leave the pack for "${leavingRide?.title}"? Your telemetry channel will be disconnected and your slot will be released.`}
confirmText="Leave Pack"
cancelText="Stay in Pack"
danger={false}
onClose={() => setLeavingRide(null)}
onConfirm={handleLeaveRide}
/>
<JoinRideModal
isOpen={isJoinModalOpen}
onClose={() => setIsJoinModalOpen(false)}
onJoinSuccess={handleJoinSuccess}
/>
<RideDetailsModal
isOpen={Boolean(selectedDetailRide)}
ride={selectedDetailRide}
onClose={() => setSelectedDetailRide(null)}
onEdit={(r) => setEditingRide(r)}
onRequestSlot={handleRequestSlot}
/>
</div>
</div>
</div>
);
}