'use client'; import React, { useState, useEffect, useMemo, useRef } from 'react'; import Link from 'next/link'; import { type ClubRoute, fetchClubRoutes, createClubRoute, deleteClubRoute, } from '../../lib/club-community'; import { type FounderClubContext, sanitizePortalError } from '../../lib/club-data'; interface FounderRoutesProps { context: FounderClubContext; } export function FounderRoutes({ context }: FounderRoutesProps) { const { club, permissions, currentUser, isDemoFixture } = context; const [routes, setRoutes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [curvatureFilter, setCurvatureFilter] = useState('all'); const [surfaceFilter, setSurfaceFilter] = useState('all'); // Modal states const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [modalError, setModalError] = useState(null); const [routeToDelete, setRouteToDelete] = useState(null); // New route form state const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [distanceMiles, setDistanceMiles] = useState('35.0'); const [estimatedMinutes, setEstimatedMinutes] = useState('45'); const [elevationGainFt, setElevationGainFt] = useState('1500'); const [curvatureRating, setCurvatureRating] = useState<'High Twisties' | 'Moderate' | 'Scenic Cruise' | 'Track Pace'>('High Twisties'); const [surfaceType, setSurfaceType] = useState<'Paved Asphalt' | 'Mixed Surface' | 'Mountain Pass'>('Paved Asphalt'); const [startLocation, setStartLocation] = useState(''); const [endLocation, setEndLocation] = useState(''); const modalRef = useRef(null); useEffect(() => { let active = true; async function loadRoutes() { setLoading(true); setError(null); try { const data = await fetchClubRoutes(club.id, { allowDemoPreview: isDemoFixture }); if (active) setRoutes(data); } catch (err) { if (active) setError(sanitizePortalError(err, 'Failed to load club routes.')); } finally { if (active) setLoading(false); } } loadRoutes(); return () => { active = false; }; }, [club.id, isDemoFixture]); const filteredRoutes = useMemo(() => { return routes.filter((r) => { const matchesSearch = !searchQuery.trim() || r.title.toLowerCase().includes(searchQuery.toLowerCase()) || r.description.toLowerCase().includes(searchQuery.toLowerCase()) || r.curatorName.toLowerCase().includes(searchQuery.toLowerCase()) || r.startLocation.toLowerCase().includes(searchQuery.toLowerCase()) || r.endLocation.toLowerCase().includes(searchQuery.toLowerCase()); const matchesCurvature = curvatureFilter === 'all' || r.curvatureRating === curvatureFilter; const matchesSurface = surfaceFilter === 'all' || r.surfaceType === surfaceFilter; return matchesSearch && matchesCurvature && matchesSurface; }); }, [routes, searchQuery, curvatureFilter, surfaceFilter]); async function handleAddRoute(e: React.FormEvent) { e.preventDefault(); if (!title.trim() || !startLocation.trim() || !endLocation.trim()) { setModalError('Title, start location, and end location are required.'); return; } setIsSubmitting(true); setModalError(null); try { if (isDemoFixture) { const newDemoRoute: ClubRoute = { id: `demo-route-${Date.now()}`, clubId: club.id, title: title.trim(), description: description.trim(), distanceMiles: parseFloat(distanceMiles) || 20, estimatedMinutes: parseInt(estimatedMinutes, 10) || 30, elevationGainFt: parseInt(elevationGainFt, 10) || 500, curvatureRating, surfaceType, startLocation: startLocation.trim(), endLocation: endLocation.trim(), curatorUid: currentUser.uid, curatorName: 'Club Founder', createdAt: new Date().toISOString(), }; setRoutes([newDemoRoute, ...routes]); } else { await createClubRoute(club.id, { title: title.trim(), description: description.trim(), distanceMiles: parseFloat(distanceMiles) || 20, estimatedMinutes: parseInt(estimatedMinutes, 10) || 30, elevationGainFt: parseInt(elevationGainFt, 10) || 500, curvatureRating, surfaceType, startLocation: startLocation.trim(), endLocation: endLocation.trim(), }); const updated = await fetchClubRoutes(club.id); setRoutes(updated); } setIsAddModalOpen(false); setTitle(''); setDescription(''); setStartLocation(''); setEndLocation(''); } catch (err) { setModalError(sanitizePortalError(err, 'Failed to save route.')); } finally { setIsSubmitting(false); } } async function handleDeleteRoute(routeId: string) { try { if (isDemoFixture) { setRoutes((prev) => prev.filter((r) => r.id !== routeId)); } else { await deleteClubRoute(club.id, routeId); setRoutes((prev) => prev.filter((r) => r.id !== routeId)); } setRouteToDelete(null); } catch (err) { setError(sanitizePortalError(err, 'Failed to delete route.')); } } function downloadGpx(route: ClubRoute) { const gpxContent = ` ${route.title} ${route.description} ${route.title} ${route.startLocation} ${route.endLocation} `; const blob = new Blob([gpxContent], { type: 'application/gpx+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${route.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.gpx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } const canCurate = permissions.canPlanRides || permissions.canManageSettings; const hasExampleRoutes = routes.length > 0 && routes.every((r) => r.isExample); return (
{/* Top Header Card */}

Curated Club Routes

{hasExampleRoutes ? `${routes.length} Examples` : `${routes.length} Available`}

Curate twisty runs, elevation passes, and scenic loops for club group rides. Export GPX files directly for offline GPS units and cockpit navigation.

{canCurate && ( )}
{hasExampleRoutes && (
info These are example routes — your club hasn’t curated any yet. Curate a route to replace them with your own.
)} {error && (
error {error}
)} {/* Filter and Search Controls */}
search setSearchQuery(e.target.value)} className="w-full bg-surface-container/60 border border-brand-border/40 rounded-xl pl-9 pr-4 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-brand-cyan/60" />
{/* Curvature Filter */} {/* Surface Filter */}
{/* Routes List Grid */} {loading ? (
{[1, 2, 3].map((n) => (
))}
) : filteredRoutes.length === 0 ? (
route

No Routes Found

{searchQuery || curvatureFilter !== 'all' || surfaceFilter !== 'all' ? 'No routes match your search or filter criteria. Try resetting your filters.' : 'No club routes have been curated yet. Be the first to add a route for upcoming club rides!'}

{(searchQuery || curvatureFilter !== 'all' || surfaceFilter !== 'all') && ( )}
) : (
{filteredRoutes.map((route) => { const isHighCurvature = route.curvatureRating === 'High Twisties'; const isModerate = route.curvatureRating === 'Moderate'; return (
{/* Card Header & Badge */}

{route.title}

{route.isExample && ( Example )}
{route.curvatureRating}

{route.description}

{/* Waypoint Strip */}
trip_origin {route.startLocation} ➔ {route.endLocation}
{/* Metrics Bar */}
Distance
{route.distanceMiles} mi
Est. Time
{route.estimatedMinutes} min
Climb
+{route.elevationGainFt} ft
terrain {route.surfaceType} By {route.curatorName}
{/* Card Actions */}
add_road Stage Ride {canCurate && ( )}
); })}
)} {/* Curate Route Modal */} {isAddModalOpen && (
add_road

Curate Club Route

{modalError && (
{modalError}
)}
setTitle(e.target.value)} className="w-full bg-surface-container border border-brand-border/40 rounded-xl px-3 py-2 text-white placeholder-slate-500 focus:outline-none focus:border-brand-cyan/60" />