Mapcn
blockdelivery-trackersource: ok · hit
Delivery Tracker
Live order tracking with route progress, courier position, and order details.
npx shadcn add "https://mapcn.dev/r/delivery-tracker.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
@mapcn/mapcardbadgebutton
Files2 · source cached 10/9/2026, 5:24:59 PM
src/registry/blocks/delivery-tracker/page.tsx
"use client";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { Clock3, House, Store, Utensils, Truck, UserRound } from "lucide-react";
import {
Map,
MapMarker,
MapRoute,
MarkerContent,
RouteMarker,
RouteProgress,
} from "@/components/ui/map";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
buildRouteUrl,
deliveryMeals,
dropoff,
mapView,
pickup,
progressFraction,
routeStyle,
type OsrmRouteData,
} from "./data";
function formatDistance(meters?: number) {
if (!meters) return "--";
if (meters < 1000) return `${Math.round(meters)} m`;
return `${(meters / 1000).toFixed(1)} km`;
}
function formatDuration(seconds?: number) {
if (!seconds) return "--";
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
export default function Page() {
const [routeData, setRouteData] = useState<OsrmRouteData | null>(null);
const [loading, setLoading] = useState(true);
const { resolvedTheme } = useTheme();
const remainingRouteColor =
resolvedTheme === "dark"
? routeStyle.remaining.color.dark
: routeStyle.remaining.color.light;
useEffect(() => {
async function fetchRoute() {
setLoading(true);
try {
const response = await fetch(buildRouteUrl(pickup, dropoff));
const data = await response.json();
const route = data?.routes?.[0];
if (!route?.geometry?.coordinates) return;
setRouteData({
coordinates: route.geometry.coordinates as [number, number][],
duration: route.duration as number,
distance: route.distance as number,
});
} catch (error) {
console.error("Failed to fetch route:", error);
} finally {
setLoading(false);
}
}
fetchRoute();
}, []);
return (
<div className="flex min-h-screen items-center justify-center p-8">
<div className="bg-sidebar mx-auto grid w-[1200px] rounded-xl border md:grid-cols-[1.05fr_1fr]">
<div className="flex flex-col p-5 md:p-6">
<div className="space-y-1">
<h3 className="text-2xl font-semibold tracking-tight">
Track Delivery
</h3>
<p className="text-muted-foreground text-sm">Mon Feb 10 - 2-3 PM</p>
</div>
<Card className="mt-5">
<CardHeader>
<CardTitle className="font-medium">
Order items ({deliveryMeals.length})
</CardTitle>
</CardHeader>
<CardContent className="space-y-5">
{deliveryMeals.map((meal) => (
<div key={meal.name} className="flex items-center gap-3">
<div className="bg-muted grid size-8 place-items-center rounded-full text-xs">
<Utensils className="text-muted-foreground size-4" />
</div>
<div className="min-w-4 flex-1">
<p className="truncate pb-1 text-sm font-medium">
{meal.name}
</p>
<p className="text-muted-foreground text-xs">
{meal.price}
</p>
</div>
<Badge
variant="secondary"
className="h-6 rounded-full px-2.5"
>
x{meal.quantity}
</Badge>
</div>
))}
<div className="border-border/60 flex items-center justify-between border-t pt-3 text-sm">
<span className="text-muted-foreground">Bundle total</span>
<span className="font-medium">$189.00</span>
</div>
</CardContent>
</Card>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<Card>
<CardContent className="space-y-2">
<p className="text-muted-foreground text-sm">
Pickup confirmed
</p>
<p className="text-sm font-medium">Mon, Feb 10 at 1:48 PM</p>
</CardContent>
</Card>
<Card>
<CardContent className="space-y-2">
<p className="text-muted-foreground text-sm">
Remaining travel
</p>
<p className="text-sm font-medium">
{formatDuration(routeData?.duration)}
<span className="text-muted-foreground font-normal">
{" · "}
{formatDistance(routeData?.distance)}
</span>
</p>
</CardContent>
</Card>
</div>
<div className="mt-6 flex flex-wrap items-center gap-2">
<Button size="sm">
<Clock3 />
View timeline
</Button>
<Button variant="outline" size="sm">
<UserRound />
Contact courier
</Button>
</div>
</div>
<div className="relative h-[450px] overflow-hidden rounded-xl shadow-sm md:h-full">
<Map
loading={loading}
center={mapView.center}
zoom={mapView.zoom}
minZoom={mapView.minZoom}
maxZoom={mapView.maxZoom}
>
<MapRoute
id="delivery-route"
coordinates={routeData?.coordinates ?? []}
progress={progressFraction}
color={remainingRouteColor}
width={routeStyle.remaining.width}
opacity={routeStyle.remaining.opacity}
interactive={false}
>
<RouteProgress
color={routeStyle.progress.color}
width={routeStyle.progress.width}
opacity={routeStyle.progress.opacity}
/>
<RouteMarker at="progress" offset={[0, 10]}>
<MarkerContent>
<div
className="relative grid size-9 place-items-center rounded-full shadow-md"
style={{ backgroundColor: routeStyle.progress.color }}
>
<Truck className="size-4 text-white" />
<div className="bg-popover text-popover-foreground absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2 rounded-md border px-2 py-1 text-xs font-medium whitespace-nowrap shadow-md">
{formatDuration(routeData?.duration)} away
<span className="bg-popover absolute top-full left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 border-r border-b" />
</div>
</div>
</MarkerContent>
</RouteMarker>
</MapRoute>
<MapMarker longitude={pickup.lng} latitude={pickup.lat}>
<MarkerContent>
<div className="grid size-7 place-items-center rounded-full bg-emerald-500 shadow-md">
<Store className="size-3.5 text-white" />
</div>
</MarkerContent>
</MapMarker>
<MapMarker longitude={dropoff.lng} latitude={dropoff.lat}>
<MarkerContent>
<div className="grid size-7 place-items-center rounded-full bg-rose-500 shadow-md">
<House className="size-3.5 text-white" />
</div>
</MarkerContent>
</MapMarker>
</Map>
</div>
</div>
</div>
);
}
src/registry/blocks/delivery-tracker/data.ts
export interface DeliveryMeal {
name: string;
price: string;
quantity: number;
}
export interface OsrmRouteData {
coordinates: [number, number][];
duration: number;
distance: number;
}
export const deliveryMeals: DeliveryMeal[] = [
{ name: "Spicy Tofu Grain Bowl", price: "$44.00", quantity: 1 },
{ name: "Herb Chicken Rice Box", price: "$58.00", quantity: 2 },
{ name: "Roasted Veggie Wrap", price: "$29.00", quantity: 1 },
];
/** Pickup (origin) and dropoff (destination) coordinates. */
export const pickup = { lng: -122.4185, lat: 37.7645 };
export const dropoff = { lng: -122.434, lat: 37.7475 };
/** Initial map viewport. */
export const mapView = {
center: [-122.4263, 37.756] as [number, number],
zoom: 13.6,
minZoom: 12,
maxZoom: 15,
};
/** Fraction of the route the courier has already covered (0–1). */
export const progressFraction = 0.62;
/**
* OSRM demo routing endpoint. Swap in your own routing service by returning a
* URL that responds with GeoJSON route geometry.
*/
export function buildRouteUrl(
from: { lng: number; lat: number },
to: { lng: number; lat: number },
) {
return `https://router.project-osrm.org/route/v1/driving/${from.lng},${from.lat};${to.lng},${to.lat}?overview=full&geometries=geojson`;
}
/**
* Route line styling. WebGL paint can't read CSS variables, so colors are
* concrete hex. `progress` highlights the covered path; `remaining` is themed
* for the road still ahead.
*/
export const routeStyle = {
progress: { color: "#3b82f6", width: 6, opacity: 0.95 },
remaining: {
width: 5.2,
opacity: 0.5,
color: { light: "#6b7280", dark: "#9ca3af" },
},
} as const;