refactor(bus-map): make full-screen map the main feature with floating route chips & toggleable drawer
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled

This commit is contained in:
Shanmuga Krishnan S M
2026-08-06 14:27:57 +05:30
parent d896322227
commit 474672357d
2 changed files with 259 additions and 164 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'; import { MapContainer, TileLayer, Marker, Popup, Polyline, useMap } from 'react-leaflet';
import L from 'leaflet'; import L from 'leaflet';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import type { BusRoute } from '@/types'; import type { BusRoute } from '@/types';
@@ -32,34 +32,32 @@ const isRealStopCoordinate = (stop: { name: string; lat?: number; lng?: number }
if (stop.lat === undefined || stop.lng === undefined) return false; if (stop.lat === undefined || stop.lng === undefined) return false;
if (!isValidCoordinate(stop.lat, stop.lng)) return false; if (!isValidCoordinate(stop.lat, stop.lng)) return false;
// Check for RIT Campus coordinate fallback (13.0118, 80.0214)
const isRITCoord = Math.abs(stop.lat - 13.0118) < 0.001 && Math.abs(stop.lng - 80.0214) < 0.001; const isRITCoord = Math.abs(stop.lat - 13.0118) < 0.001 && Math.abs(stop.lng - 80.0214) < 0.001;
const isRITName = stop.name.toLowerCase().includes("rit") || const isRITName =
stop.name.toLowerCase().includes("campus") || stop.name.toLowerCase().includes('rit') ||
stop.name.toLowerCase().includes("college") || stop.name.toLowerCase().includes('campus') ||
stop.name.toLowerCase().includes("rajalakshmi"); stop.name.toLowerCase().includes('college') ||
stop.name.toLowerCase().includes('rajalakshmi');
if (isRITCoord && !isRITName) { if (isRITCoord && !isRITName) {
return false; // Exclude defaulted RIT Campus coordinates return false;
} }
return true; return true;
}; };
const liveBusIcon = L.divIcon({ const liveBusIcon = L.divIcon({
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-emerald-500 border-2 border-white shadow-lg text-white text-sm font-bold"><span class="absolute inset-0 rounded-full bg-emerald-500/40 animate-ping"></span>🚌</div>`, html: `<div class="relative flex items-center justify-center w-10 h-10 rounded-full bg-emerald-500 border-2 border-white shadow-xl text-white text-base font-bold cursor-pointer"><span class="absolute inset-0 rounded-full bg-emerald-500/50 animate-ping"></span>🚌</div>`,
className: '', className: '',
iconSize: [36, 36], iconSize: [40, 40],
iconAnchor: [18, 18], iconAnchor: [20, 20],
}); });
const stoppedBusIcon = L.divIcon({ const stoppedBusIcon = L.divIcon({
html: `<div class="relative flex items-center justify-center w-9 h-9 rounded-full bg-red-500 border-2 border-white shadow-lg text-white text-sm font-bold">🚌</div>`, html: `<div class="relative flex items-center justify-center w-10 h-10 rounded-full bg-red-500 border-2 border-white shadow-xl text-white text-base font-bold cursor-pointer">🚌</div>`,
className: '', className: '',
iconSize: [36, 36], iconSize: [40, 40],
iconAnchor: [18, 18], iconAnchor: [20, 20],
}); });
// Helper component to auto-pan and fit the map bounds to the active route // Helper component to auto-pan and fit the map bounds to the active route
@@ -67,7 +65,7 @@ function MapUpdater({ bounds }: { bounds: L.LatLngBoundsExpression | null }) {
const map = useMap(); const map = useMap();
useEffect(() => { useEffect(() => {
if (bounds && (bounds as any).length > 0) { if (bounds && (bounds as any).length > 0) {
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14, animate: true }); map.fitBounds(bounds, { padding: [60, 60], maxZoom: 14, animate: true });
} }
}, [bounds, map]); }, [bounds, map]);
return null; return null;
@@ -80,8 +78,10 @@ interface BusRouteMapProps {
const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default
export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapProps) { export default function BusRouteMap({ selectedRoute }: BusRouteMapProps) {
const [allLiveLocations, setAllLiveLocations] = useState<Record<string, { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }>>({}); const [allLiveLocations, setAllLiveLocations] = useState<
Record<string, { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }>
>({});
useEffect(() => { useEffect(() => {
const fetchLiveLocations = () => { const fetchLiveLocations = () => {
@@ -90,87 +90,79 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
if (res.ok) return res.json(); if (res.ok) return res.json();
return []; return [];
}) })
.then((data: Array<{ routeNumber: string; latitude: number; longitude: number; lastUpdated: string; stopped: boolean }>) => { .then(
if (Array.isArray(data)) { (
const locMap: Record<string, { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }> = {}; data: Array<{
data.forEach((item) => { routeNumber: string;
if (item.routeNumber && item.latitude && item.longitude) { latitude: number;
locMap[item.routeNumber] = { longitude: number;
latitude: item.latitude, lastUpdated: string;
longitude: item.longitude, stopped: boolean;
lastUpdated: item.lastUpdated, }>
stopped: item.stopped ) => {
}; if (Array.isArray(data)) {
} const locMap: Record<
}); string,
setAllLiveLocations(locMap); { latitude: number; longitude: number; lastUpdated: string; stopped: boolean }
> = {};
data.forEach((item) => {
if (item.routeNumber && item.latitude && item.longitude) {
locMap[item.routeNumber] = {
latitude: item.latitude,
longitude: item.longitude,
lastUpdated: item.lastUpdated,
stopped: item.stopped,
};
}
});
setAllLiveLocations(locMap);
}
} }
}) )
.catch(() => { .catch(() => {
setAllLiveLocations({}); setAllLiveLocations({});
}); });
}; };
// Initial fetch
fetchLiveLocations(); fetchLiveLocations();
// Poll every 3 seconds
const interval = setInterval(fetchLiveLocations, 3000); const interval = setInterval(fetchLiveLocations, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Collect coordinates for the polyline path, filtering out any invalid outliers
// Collect coordinates for polyline path
const pathCoordinates = useMemo(() => { const pathCoordinates = useMemo(() => {
if (!selectedRoute) return []; if (!selectedRoute) return [];
// If route has pre-scraped polyline road path, use it!
if (selectedRoute.polyline && selectedRoute.polyline.length > 0) { if (selectedRoute.polyline && selectedRoute.polyline.length > 0) {
return selectedRoute.polyline; return selectedRoute.polyline;
} }
const coords: [number, number][] = []; const coords: [number, number][] = [];
// Add start stop coords if valid and real
if (isValidCoordinate(selectedRoute.from_lat, selectedRoute.from_lng)) { if (isValidCoordinate(selectedRoute.from_lat, selectedRoute.from_lng)) {
const isStartRIT = Math.abs(selectedRoute.from_lat! - 13.0118) < 0.001 && Math.abs(selectedRoute.from_lng! - 80.0214) < 0.001; coords.push([selectedRoute.from_lat!, selectedRoute.from_lng!]);
const isStartRITName = selectedRoute.from.toLowerCase().includes("rit") ||
selectedRoute.from.toLowerCase().includes("campus") ||
selectedRoute.from.toLowerCase().includes("college");
if (!isStartRIT || isStartRITName) {
coords.push([selectedRoute.from_lat!, selectedRoute.from_lng!]);
}
} }
// Add all intermediary stop coords that are valid and real selectedRoute.stops.forEach((stop) => {
selectedRoute.stops.forEach(stop => {
if (isRealStopCoordinate(stop)) { if (isRealStopCoordinate(stop)) {
coords.push([stop.lat!, stop.lng!]); coords.push([stop.lat!, stop.lng!]);
} }
}); });
// Add end stop coords if valid and real
if (isValidCoordinate(selectedRoute.to_lat, selectedRoute.to_lng)) { if (isValidCoordinate(selectedRoute.to_lat, selectedRoute.to_lng)) {
const isEndRIT = Math.abs(selectedRoute.to_lat! - 13.0118) < 0.001 && Math.abs(selectedRoute.to_lng! - 80.0214) < 0.001; coords.push([selectedRoute.to_lat!, selectedRoute.to_lng!]);
const isEndRITName = selectedRoute.to.toLowerCase().includes("rit") ||
selectedRoute.to.toLowerCase().includes("campus") ||
selectedRoute.to.toLowerCase().includes("college");
if (!isEndRIT || isEndRITName) {
coords.push([selectedRoute.to_lat!, selectedRoute.to_lng!]);
}
} }
return coords; return coords;
}, [selectedRoute]); }, [selectedRoute]);
// Determine map bounds
const mapBounds = useMemo(() => { const mapBounds = useMemo(() => {
if (pathCoordinates.length === 0) return null; if (pathCoordinates.length === 0) return null;
return pathCoordinates as L.LatLngBoundsExpression; return pathCoordinates as L.LatLngBoundsExpression;
}, [pathCoordinates]); }, [pathCoordinates]);
return ( return (
<div className="relative h-full w-full min-h-[350px] bg-slate-950 rounded-2xl overflow-hidden border border-slate-200/80 shadow-md"> <div className="relative h-full w-full bg-slate-950 overflow-hidden">
<MapContainer <MapContainer
center={DEFAULT_CENTER as L.LatLngExpression} center={DEFAULT_CENTER as L.LatLngExpression}
zoom={11} zoom={11}
@@ -179,25 +171,38 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
maxBounds={[[12.65, 79.25], [13.35, 80.45]] as L.LatLngBoundsExpression} maxBounds={[[12.65, 79.25], [13.35, 80.45]] as L.LatLngBoundsExpression}
maxBoundsViscosity={1.0} maxBoundsViscosity={1.0}
className="h-full w-full z-10" className="h-full w-full z-10"
style={{ height: "100%", width: "100%" }} style={{ height: '100%', width: '100%' }}
> >
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/> />
{/* Polyline Route Path */}
{pathCoordinates.length > 0 && selectedRoute && (
<Polyline
positions={pathCoordinates}
pathOptions={{
color: selectedRoute.color || '#F97316',
weight: 5,
opacity: 0.8,
lineCap: 'round',
lineJoin: 'round',
}}
/>
)}
{/* Live GPS Bus Markers */}
{Object.entries(allLiveLocations).map(([rNum, loc]) => { {Object.entries(allLiveLocations).map(([rNum, loc]) => {
const normalizeRouteNum = (str: string) => (str || '').replace(/^[Rr]0*/, '').trim().toUpperCase(); const normalizeRouteNum = (str: string) => (str || '').replace(/^[Rr]0*/, '').trim().toUpperCase();
if (selectedRoute && normalizeRouteNum(selectedRoute.number) !== normalizeRouteNum(rNum)) return null; if (selectedRoute && normalizeRouteNum(selectedRoute.number) !== normalizeRouteNum(rNum)) return null;
const isStale = loc.stopped || (loc.lastUpdated ? (new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000) : false); const isStale =
loc.stopped ||
(loc.lastUpdated ? new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000 : false);
return ( return (
<Marker <Marker key={`live-bus-${rNum}`} position={[loc.latitude, loc.longitude]} icon={isStale ? stoppedBusIcon : liveBusIcon}>
key={`live-bus-${rNum}`}
position={[loc.latitude, loc.longitude]}
icon={isStale ? stoppedBusIcon : liveBusIcon}
>
<Popup> <Popup>
<div className="p-1 font-sans text-center"> <div className="p-1 font-sans text-center">
{loc.stopped ? ( {loc.stopped ? (
@@ -215,7 +220,7 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
)} )}
<h4 className="font-bold text-slate-800 text-xs mt-1">Bus {rNum}</h4> <h4 className="font-bold text-slate-800 text-xs mt-1">Bus {rNum}</h4>
<p className="text-[10px] text-slate-500 mt-0.5"> <p className="text-[10px] text-slate-500 mt-0.5">
{loc.stopped ? "Sharing ended by driver" : isStale ? "Last known location" : "Broadcasting live coordinates"} {loc.stopped ? 'Sharing ended by driver' : isStale ? 'Last known location' : 'Broadcasting live coordinates'}
</p> </p>
</div> </div>
</Popup> </Popup>
@@ -223,13 +228,12 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
); );
})} })}
<MapUpdater bounds={mapBounds} /> <MapUpdater bounds={mapBounds} />
</MapContainer> </MapContainer>
{/* Floating Info Overlay on Selected Route */} {/* Floating Info Card on Selected Route */}
{selectedRoute && ( {selectedRoute && (
<div className="absolute bottom-4 left-4 z-20 bg-white/95 backdrop-blur-md px-4 py-3 rounded-2xl border border-slate-200 shadow-xl max-w-xs pointer-events-auto"> <div className="absolute bottom-6 left-6 z-20 bg-slate-900/90 text-white backdrop-blur-md px-4 py-3 rounded-2xl border border-slate-700/80 shadow-2xl max-w-xs pointer-events-auto">
<div className="flex items-center gap-2 mb-1.5"> <div className="flex items-center gap-2 mb-1.5">
<span <span
className="px-2.5 py-0.5 rounded-full text-xs font-bold text-white shadow-xs" className="px-2.5 py-0.5 rounded-full text-xs font-bold text-white shadow-xs"
@@ -237,12 +241,18 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
> >
{selectedRoute.number} {selectedRoute.number}
</span> </span>
<span className="text-xs font-bold text-slate-800">{selectedRoute.name}</span> <span className="text-xs font-bold text-white">{selectedRoute.name}</span>
</div> </div>
<div className="text-[11px] text-slate-600 space-y-1"> <div className="text-[11px] text-slate-300 space-y-1">
<p>🏁 <strong>Start:</strong> {selectedRoute.from} ({selectedRoute.departureTime})</p> <p>
<p>🏫 <strong>Destination:</strong> RIT Campus ({selectedRoute.arrivalTime})</p> 🏁 <strong>Start:</strong> {selectedRoute.from} ({selectedRoute.departureTime})
<p>📍 <strong>Total Stops:</strong> {selectedRoute.stops.length} mapped stops</p> </p>
<p>
🏫 <strong>Destination:</strong> RIT Campus ({selectedRoute.arrivalTime})
</p>
<p>
📍 <strong>Stops:</strong> {selectedRoute.stops.length} mapped stops
</p>
</div> </div>
</div> </div>
)} )}

View File

@@ -1,9 +1,8 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Search, Loader2, ChevronRight } from 'lucide-react'; import { Search, Loader2, ChevronRight, Menu, X, MapPin, Navigation, Compass, Radio } from 'lucide-react';
import BusCard from '@/components/BusCard/BusCard'; import BusCard from '@/components/BusCard/BusCard';
import BusRouteMap from '@/components/BusRouteMap/BusRouteMap'; import BusRouteMap from '@/components/BusRouteMap/BusRouteMap';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import type { BusRoute } from '@/types'; import type { BusRoute } from '@/types';
import { getBackendUrl } from '@/lib/utils'; import { getBackendUrl } from '@/lib/utils';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -13,6 +12,7 @@ export default function BusRoutes() {
const [busRoutes, setBusRoutes] = useState<BusRoute[]>([]); const [busRoutes, setBusRoutes] = useState<BusRoute[]>([]);
const [loadingBus, setLoadingBus] = useState(true); const [loadingBus, setLoadingBus] = useState(true);
const [selectedBusRoute, setSelectedBusRoute] = useState<BusRoute | null>(null); const [selectedBusRoute, setSelectedBusRoute] = useState<BusRoute | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => { useEffect(() => {
setLoadingBus(true); setLoadingBus(true);
@@ -44,88 +44,173 @@ export default function BusRoutes() {
}); });
}, []); }, []);
const filteredRoutes = busRoutes.filter((r) => const filteredRoutes = busRoutes.filter(
r.name.toLowerCase().includes(busSearch.toLowerCase()) || (r) =>
r.from.toLowerCase().includes(busSearch.toLowerCase()) || r.name.toLowerCase().includes(busSearch.toLowerCase()) ||
r.number.toLowerCase().includes(busSearch.toLowerCase()) r.from.toLowerCase().includes(busSearch.toLowerCase()) ||
r.number.toLowerCase().includes(busSearch.toLowerCase())
); );
return ( return (
<div className="min-h-screen relative overflow-hidden bg-[#FAFAFA]"> <div className="min-h-screen relative overflow-hidden bg-slate-950 text-white">
<div className="relative z-10"> {/* Sleek Dark Header */}
{/* Header */} <div className="bg-slate-900 border-b border-slate-800/80 py-4 px-4 sm:px-8 z-30 relative">
<div className="bg-white border-b border-[#E5E7EB] py-10"> <div className="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="container-custom"> <div>
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3"> <div className="flex items-center gap-2 text-xs text-slate-400 mb-1">
<Link to="/" className="hover:text-[#F97316]">Home</Link> <Link to="/" className="hover:text-orange-400">Home</Link>
<ChevronRight className="w-3 h-3" /> <ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Bus Routes</span> <span className="text-orange-400 font-semibold">Live Bus Tracking</span>
</div> </div>
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}> <div className="flex items-center gap-3">
RIT{' '} <h1 className="text-xl sm:text-2xl font-bold text-white tracking-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}> RIT Live Transport Map
Bus Routes </h1>
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-400 text-[11px] font-bold">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
Live GPS Sync
</span> </span>
</h1> </div>
<p className="text-[#475569] text-sm" style={{ fontFamily: 'Inter, sans-serif' }}> </div>
View all campus bus routes, timings, and pickup locations at a glance.
{/* Controls: Search + Toggle Drawer Button */}
<div className="flex items-center gap-2">
<div className="relative flex-1 sm:w-64">
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Search route or bus no..."
value={busSearch}
onChange={(e) => setBusSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-400 focus:outline-none focus:border-orange-500 transition-colors"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-semibold border transition-all cursor-pointer ${
isSidebarOpen
? 'bg-orange-500 text-white border-orange-400 shadow-lg shadow-orange-500/20'
: 'bg-slate-800 text-slate-200 border-slate-700 hover:bg-slate-700'
}`}
style={{ fontFamily: 'Poppins, sans-serif' }}
>
{isSidebarOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
<span className="hidden sm:inline">
{isSidebarOpen ? 'Hide Routes' : `Routes List (${filteredRoutes.length})`}
</span>
</button>
</div>
</div>
</div>
{/* Main Map Viewport */}
<div className="relative h-[calc(100vh-120px)] min-h-[550px] w-full overflow-hidden">
{loadingBus ? (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-slate-950 z-30 gap-3">
<Loader2 className="w-10 h-10 text-orange-500 animate-spin" />
<p className="text-sm text-slate-400 font-semibold" style={{ fontFamily: 'Poppins, sans-serif' }}>
Initializing Live GPS Bus Tracking Map...
</p> </p>
</div> </div>
</div> ) : (
<>
{/* Quick Horizontal Route Chips Bar on top of Map */}
<div className="absolute top-4 left-4 right-4 z-20 overflow-x-auto pb-2 scrollbar-none flex items-center gap-2">
<button
onClick={() => setSelectedBusRoute(null)}
className={`px-3.5 py-1.5 rounded-full text-xs font-bold whitespace-nowrap shadow-lg backdrop-blur-md transition-all border cursor-pointer ${
selectedBusRoute === null
? 'bg-orange-500 text-white border-orange-400'
: 'bg-slate-900/80 text-slate-300 border-slate-700/80 hover:bg-slate-800'
}`}
>
🌐 All Buses
</button>
<div className="container-custom py-8"> {filteredRoutes.map((route) => {
<div className="relative mb-6"> const isSelected = selectedBusRoute?.number === route.number;
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-[#94A3B8]" /> return (
<input <button
type="text" key={route.id}
placeholder="Search bus routes..." onClick={() => setSelectedBusRoute(route)}
value={busSearch} className={`px-3.5 py-1.5 rounded-full text-xs font-bold whitespace-nowrap shadow-lg backdrop-blur-md transition-all border flex items-center gap-1.5 cursor-pointer ${
onChange={(e) => setBusSearch(e.target.value)} isSelected
className="w-full pl-11 pr-4 py-3 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] transition-colors" ? 'bg-orange-500 text-white border-orange-400 scale-105'
style={{ fontFamily: 'Inter, sans-serif' }} : 'bg-slate-900/80 text-slate-300 border-slate-700/80 hover:bg-slate-800'
/> }`}
</div> >
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: route.color || '#F97316' }} />
{route.number}
</button>
);
})}
</div>
{loadingBus ? ( {/* The Fullscreen Map Component */}
<div className="flex flex-col items-center justify-center py-16 gap-3"> <BusRouteMap selectedRoute={selectedBusRoute} allRoutes={filteredRoutes} />
<Loader2 className="w-8 h-8 text-[#F97316] animate-spin" />
<p className="text-sm text-[#94A3B8]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Loading live bus routes from RIT Transport...
</p>
</div>
) : filteredRoutes.length === 0 ? (
<div className="text-center py-16 text-[#94A3B8]">
<p className="text-sm font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>
No bus routes found matching "{busSearch}"
</p>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
{/* Routes List */}
<div className="lg:col-span-5 max-h-[650px] overflow-y-auto pr-2 space-y-4 scrollbar-thin">
<StaggerContainer>
{filteredRoutes.map((route) => (
<StaggerItem key={route.id} className="mb-4">
<BusCard
route={route}
isSelected={selectedBusRoute?.number === route.number}
onSelect={() => setSelectedBusRoute(route)}
/>
</StaggerItem>
))}
</StaggerContainer>
</div>
{/* Interactive Map */} {/* Sidebar / Bottom Sheet Overlay */}
<div className="lg:col-span-7 h-[450px] lg:h-[650px] sticky top-24"> <AnimatePresence>
<BusRouteMap {isSidebarOpen && (
selectedRoute={selectedBusRoute} <>
allRoutes={filteredRoutes} {/* Backdrop on mobile */}
/> <motion.div
</div> initial={{ opacity: 0 }}
</div> animate={{ opacity: 1 }}
)} exit={{ opacity: 0 }}
</div> onClick={() => setIsSidebarOpen(false)}
className="absolute inset-0 bg-slate-950/60 backdrop-blur-xs z-30 lg:hidden"
/>
{/* Drawer Panel: Floating Panel on Desktop, Bottom Sheet on Mobile */}
<motion.div
initial={{ x: '-100%', opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: '-100%', opacity: 0 }}
transition={{ type: 'spring', bounce: 0.1, duration: 0.3 }}
className="absolute top-0 left-0 bottom-0 z-40 w-full sm:w-[380px] bg-slate-900/95 border-r border-slate-800 backdrop-blur-xl shadow-2xl flex flex-col"
>
{/* Drawer Header */}
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-2">
<Navigation className="w-4 h-4 text-orange-400" />
<h2 className="text-sm font-bold text-white" style={{ fontFamily: 'Poppins, sans-serif' }}>
Select Bus Route ({filteredRoutes.length})
</h2>
</div>
<button
onClick={() => setIsSidebarOpen(false)}
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Route List inside Drawer */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 scrollbar-thin">
{filteredRoutes.map((route) => (
<BusCard
key={route.id}
route={route}
isSelected={selectedBusRoute?.number === route.number}
onSelect={() => {
setSelectedBusRoute(route);
// Auto close sidebar on mobile for instant map view
if (window.innerWidth < 1024) {
setIsSidebarOpen(false);
}
}}
/>
))}
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
)}
</div> </div>
</div> </div>
); );