RegistryUI recipes
Shipment tracking
Shipments with their status and the tracking timeline of the selected one, newest event first.
It loads its data on the server through the block's client; the shipments block must be installed (npx better-supabase add shipments). Its page on bettersupabase.com is /recipes/shipment-tracking.
Web, with shadcn components:
npx shadcn add alert badge button card dialog empty field input skeleton table/**
* Render inside `<Suspense>`: the latest shipments with their status, and the
* tracking timeline of the selected one, newest event first. `?shipment=<id>`
* picks the shipment.
*/
export async function ShipmentTracking({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const [session, t, format, query] = await Promise.all([
getSession(),
getExtracted("field"),
getFormatter(),
searchParams,
connection(),
]);
if (!can(session, "shipments.read")) {
forbidden();
}
const picked = v.safeParse(ShipmentParam, query["shipment"]);
const tracking = await settle(() =>
loadShipmentTracking(picked.success ? picked.output : undefined),
);
if (tracking.kind === "no-organization") {
return null;
}
const newShipment = can(session, "shipments.manage") ? (
<NewShipmentDialog />
) : null;
const selected = tracking.kind === "ok" ? tracking.data.selected : null;
return (
<div data-recipe="shipment-tracking" className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle>{t("Shipments")}</CardTitle>
<CardDescription>
{t("The status follows the latest tracking event.")}
</CardDescription>
{newShipment ? <CardAction>{newShipment}</CardAction> : null}
</CardHeader>
<CardContent>
{tracking.kind === "error" ? (
<FieldLoadError title={t("The shipments didn't load")} />
) : tracking.data.shipments.length === 0 ? (
<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">
<PackageIcon />
</EmptyMedia>
<EmptyTitle>{t("No shipments yet")}</EmptyTitle>
<EmptyDescription>
{t("Create a shipment, then buy its label from a carrier.")}
</EmptyDescription>
</EmptyHeader>
{newShipment ? <EmptyContent>{newShipment}</EmptyContent> : null}
</Empty>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("Reference")}</TableHead>
<TableHead>{t("Recipient")}</TableHead>
<TableHead>{t("Destination")}</TableHead>
<TableHead>{t("Tracking number")}</TableHead>
<TableHead>{t("Status")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tracking.data.shipments.map((shipment) => (
<TableRow
key={shipment.id}
data-state={
shipment.id === selected?.id ? "selected" : undefined
}
>
<TableCell>
<Link
href={`/field/shipments?shipment=${shipment.id}`}
className="text-sm font-medium underline-offset-4 transition-colors hover:underline"
aria-current={
shipment.id === selected?.id ? "true" : undefined
}
>
{shipment.reference}
</Link>
</TableCell>
<TableCell className="text-sm">
{shipment.recipient}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{shipment.destination}
</TableCell>
<TableCell className="text-sm tabular-nums">
{shipment.trackingNumber ?? ""}
</TableCell>
<TableCell>
<ShipmentStatusBadge status={shipment.status} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{tracking.kind === "ok" && selected ? (
<Card>
<CardHeader>
<CardTitle>
{t("Tracking for {reference}", { reference: selected.reference })}
</CardTitle>
<CardDescription>
{selected.carrier
? t("Events from {carrier}, newest first.", {
carrier: selected.carrier,
})
: t("Events show once the shipment has a label.")}
</CardDescription>
</CardHeader>
<CardContent>
{tracking.data.timeline.length === 0 ? (
<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">
<TruckIcon />
</EmptyMedia>
<EmptyTitle>{t("No tracking events yet")}</EmptyTitle>
<EmptyDescription>
{t(
"The carrier's webhook adds events as the package moves.",
)}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Link
href={`/field/shipments?shipment=${selected.id}`}
className="text-sm font-medium underline-offset-4 transition-colors hover:underline"
>
{t("Check again")}
</Link>
</EmptyContent>
</Empty>
) : (
<ol className="flex flex-col divide-y">
{tracking.data.timeline.map((step) => (
<li
key={step.id}
className="flex flex-wrap items-center gap-3 py-3"
>
<ShipmentStatusBadge status={step.status} />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm">
{step.description ?? step.location ?? ""}
</span>
{step.description && step.location ? (
<span className="text-xs text-muted-foreground">
{step.location}
</span>
) : null}
</div>
<span className="text-xs text-muted-foreground tabular-nums">
{format.dateTime(new Date(step.occurredAt), {
dateStyle: "medium",
timeStyle: "short",
})}
</span>
</li>
))}
</ol>
)}
</CardContent>
</Card>
) : null}
</div>
);
}Last updated on