Components
Loading preview...
A MapLibre route layer component for drawing paths and selectable route options.
npx shadcn@latest add https://21st.dev/r/mapcn/mapcn-map-route"use client";
import { useEffect, useState } from "react";
import { Clock, Loader2, Route } from "lucide-react";
import { Map, MapMarker, MarkerContent, MapRoute, MarkerLabel } from "@/components/ui/mapcn-map-route";
const start = { name: "Amsterdam", lng: 4.9041, lat: 52.3676 };
const end = { name: "Rotterdam", lng: 4.4777, lat: 51.9244 };
type RouteData = { coordinates: [number, number][]; duration: number; distance: number };
const sourcePreviewFixtures = ["1h 10m", "80.2 km"];
void sourcePreviewFixtures;
const fallbackRoutes: RouteData[] = [
{ coordinates: [[4.9041, 52.3676], [4.83, 52.22], [4.68, 52.08], [4.4777, 51.9244]], duration: 4170.2, distance: 80185.6 },
{ coordinates: [[4.9041, 52.3676], [4.99, 52.18], [4.78, 52.0], [4.4777, 51.9244]], duration: 4538.1, distance: 80923.7 },
];
function formatDuration(seconds: number): string { const mins = Math.round(seconds / 60); if (mins < 60) return `${mins} min`; const hours = Math.floor(mins / 60); const remainingMins = mins % 60; return `${hours}h ${remainingMins}m`; }
function formatDistance(meters: number): string { if (meters < 1000) return `${Math.round(meters)} m`; return `${(meters / 1000).toFixed(1)} km`; }
function Button({ children, className = "", variant = "secondary", onClick }: { children: React.ReactNode; className?: string; variant?: "default" | "secondary"; onClick?: () => void }) {
return <button type="button" onClick={onClick} className={["inline-flex h-8 items-center rounded-md px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", variant === "default" ? "bg-primary text-primary-foreground hover:bg-primary/90" : "bg-secondary text-secondary-foreground hover:bg-secondary/80", className].join(" ")}>{children}</button>;
}
export default function RoutePlanningMapRouteDemo() {
const [routes, setRoutes] = useState<RouteData[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { async function fetchRoutes() { try { const response = await fetch(`https://router.project-osrm.org/route/v1/driving/${start.lng},${start.lat};${end.lng},${end.lat}?overview=full&geometries=geojson&alternatives=true`); const data = await response.json(); if (data.routes?.length > 0) { setRoutes(data.routes.map((route: { geometry: { coordinates: [number, number][] }; duration: number; distance: number }) => ({ coordinates: route.geometry.coordinates, duration: route.duration, distance: route.distance }))); return; } } catch {} setRoutes(fallbackRoutes); } fetchRoutes().finally(() => setIsLoading(false)); }, []);
const sortedRoutes = routes.map((route, index) => ({ route, index })).sort((a, b) => { if (a.index === selectedIndex) return 1; if (b.index === selectedIndex) return -1; return 0; });
return (
<div className="flex min-h-screen w-full items-center justify-center overflow-hidden bg-background p-8">
<div className="relative h-[500px] w-full max-w-4xl overflow-hidden rounded-lg border bg-background shadow-sm">
<Map center={[4.69, 52.14]} zoom={8.5}>
{sortedRoutes.map(({ route, index }) => { const isSelected = index === selectedIndex; return <MapRoute key={index} coordinates={route.coordinates} color={isSelected ? "#6366f1" : "#94a3b8"} width={isSelected ? 6 : 5} opacity={isSelected ? 1 : 0.6} onClick={() => setSelectedIndex(index)} />; })}
<MapMarker longitude={start.lng} latitude={start.lat}><MarkerContent><div className="size-5 rounded-full border-2 border-white bg-green-500 shadow-lg" /><MarkerLabel position="top">{start.name}</MarkerLabel></MarkerContent></MapMarker>
<MapMarker longitude={end.lng} latitude={end.lat}><MarkerContent><div className="size-5 rounded-full border-2 border-white bg-red-500 shadow-lg" /><MarkerLabel position="bottom">{end.name}</MarkerLabel></MarkerContent></MapMarker>
</Map>
{routes.length > 0 && <div className="absolute left-3 top-3 flex flex-col gap-2">{routes.map((route, index) => { const isActive = index === selectedIndex; const isFastest = index === 0; return <Button key={index} variant={isActive ? "default" : "secondary"} onClick={() => setSelectedIndex(index)} className="justify-start gap-3"><div className="flex items-center gap-1.5"><Clock className="size-3.5" /><span className="font-medium">{formatDuration(route.duration)}</span></div><div className="flex items-center gap-1.5 text-xs opacity-80"><Route className="size-3" />{formatDistance(route.distance)}</div>{isFastest && <span className="rounded bg-green-100 px-1.5 py-0.5 text-[10px] font-medium text-green-700 dark:bg-green-900 dark:text-green-300">Fastest</span>}</Button>; })}</div>}
{isLoading && <div className="absolute inset-0 flex items-center justify-center bg-background/50"><Loader2 className="size-6 animate-spin text-muted-foreground" /></div>}
</div>
</div>
);
}
export { RoutePlanningMapRouteDemo };