shadimage
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 · light
iconslucide-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;

More from Mapcn

Browse all components in this library →