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.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert badge button card dialog empty field input skeleton tableInstall the glue
Install the block with
npx better-supabase add shipments. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the screen
React and shadcn
View in the exampleshipment-tracking.tsx /** * 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> ); }