Order detail
An order with its lines, totals, payment state and fulfillment steps. 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 empty skeleton tableInstall the glue
Install the block with
npx better-supabase add orders. 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 exampleorder-detail.tsx // oxlint-disable-next-line typescript/require-await -- a Server Action must be async, and refresh() returns nothing to await async function retry() { "use server"; refresh(); } /** * Render inside `<Suspense>`: a placed order, or the caller's draft (an * open cart) with the same id, with its lines, totals and next steps. */ export async function OrderDetail({ params, }: { params: Promise<{ id: string }>; }) { const [{ id }, session] = await Promise.all([params, getSession()]); if (!can(session, "orders.read")) { forbidden(); } const [detail, t, format, locale] = await Promise.all([ getOrderDetail(id), getExtracted("shop"), getFormatter(), getLocale(), ]); if (detail.kind === "not-found") { notFound(); } if (detail.kind === "error") { return ( <Alert variant="destructive" data-recipe="order-detail"> <TriangleAlertIcon /> <AlertTitle>{t("The order didn't load")}</AlertTitle> <AlertDescription>{t("Try again in a moment.")}</AlertDescription> <AlertAction> <form action={retry}> <Button size="sm" variant="outline" type="submit"> {t("Retry")} </Button> </form> </AlertAction> </Alert> ); } const view = detail.data; const canManage = can(session, "orders.manage"); const amount = (value: Money) => formatMoney(value, locale); const problemLabel = (problem: string) => { switch (problem) { case "shipping_required": { return t("Pick a shipping rate first."); } case "shipping_unavailable": { return t("No shipping rate covers this address."); } case "promotion_unavailable": { return t("A promotion code no longer applies."); } default: { return problem; } } }; const totals: readonly (readonly [string, Money])[] = [ [t("Subtotal"), view.subtotal], ...(view.discountTotal.amount > 0n ? [[t("Discount"), negateMoney(view.discountTotal)] as const] : []), ...(view.shippingTotal.amount > 0n ? [[t("Shipping"), view.shippingTotal] as const] : []), ...(view.kind === "order" && view.taxTotal.amount > 0n ? [[t("Tax"), view.taxTotal] as const] : []), ]; return ( <div data-recipe="order-detail" className="grid gap-6 lg:grid-cols-3"> <div className="flex flex-col gap-6 lg:col-span-2"> <div className="flex flex-col gap-1"> <div className="flex items-center gap-2"> <h1 className="text-2xl font-semibold tracking-tight"> {view.kind === "order" ? t("Order {number}", { number: view.number }) : t("Draft order")} </h1> <OrderStatusBadge status={view.kind === "order" ? view.status : "draft"} /> </div> <p className="text-sm text-muted-foreground"> {view.kind === "order" ? t("Placed {date}", { date: format.dateTime(new Date(view.placedAt), { dateStyle: "medium", timeStyle: "short", }), }) : t( "Not placed yet. Prices follow the catalog until you place it.", )} </p> </div> <Card> <CardHeader> <CardTitle>{t("Lines")}</CardTitle> </CardHeader> <CardContent> {view.lines.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <ShoppingCartIcon /> </EmptyMedia> <EmptyTitle>{t("This draft is empty")}</EmptyTitle> <EmptyDescription> {t("Add a product from the orders page.")} </EmptyDescription> </EmptyHeader> </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Product")}</TableHead> <TableHead className="text-right"> {t("Quantity")} </TableHead> <TableHead className="text-right"> {t("Unit price")} </TableHead> <TableHead className="text-right">{t("Total")}</TableHead> </TableRow> </TableHeader> <TableBody> {view.lines.map((line) => ( <TableRow key={line.id}> <TableCell> <div className="flex flex-col gap-1"> <span className="text-sm">{line.title}</span> <span className="font-mono text-xs text-muted-foreground"> {line.sku} </span> </div> </TableCell> <TableCell className="text-right tabular-nums"> {line.quantity} </TableCell> <TableCell className="text-right tabular-nums"> {amount(line.unitAmount)} </TableCell> <TableCell className="text-right tabular-nums"> {amount(line.total)} </TableCell> </TableRow> ))} </TableBody> <TableFooter> {totals.map(([label, value]) => ( <TableRow key={label}> <TableCell colSpan={3} className="text-muted-foreground"> {label} </TableCell> <TableCell className="text-right tabular-nums"> {amount(value)} </TableCell> </TableRow> ))} <TableRow> <TableCell colSpan={3} className="font-medium"> {t("Total")} </TableCell> <TableCell className="text-right font-medium tabular-nums"> {amount(view.total)} </TableCell> </TableRow> {view.kind === "order" ? ( <TableRow> <TableCell colSpan={3} className="text-muted-foreground"> {t("Paid")} </TableCell> <TableCell className="text-right tabular-nums"> {amount(view.paidTotal)} </TableCell> </TableRow> ) : null} </TableFooter> </Table> )} </CardContent> </Card> </div> <div className="flex flex-col gap-6"> <Card> <CardHeader> <CardTitle>{t("Next step")}</CardTitle> <CardDescription> {view.kind === "draft" ? t( "Placing the order reserves its stock and fixes its prices.", ) : t("Without a payments provider, record the payment by hand.")} </CardDescription> </CardHeader> <CardContent className="flex flex-col gap-3"> {view.kind === "draft" && view.problems.length > 0 ? ( <Alert variant="warning"> <TriangleAlertIcon /> <AlertTitle> {t("Check this draft before placing it")} </AlertTitle> <AlertDescription> {view.problems.map(problemLabel).join(" ")} </AlertDescription> </Alert> ) : null} {canManage ? ( view.kind === "draft" ? ( <PlaceOrderButton cartId={view.id} disabled={view.lines.length === 0} /> ) : ( <OrderTransitions orderId={view.id} status={view.status} fulfillments={view.fulfillments} /> ) ) : ( <p className="text-sm text-muted-foreground"> {t("Ask an admin to move this order along.")} </p> )} {view.kind === "order" && view.fulfillments.length > 0 ? ( <ul className="flex flex-col gap-2"> {view.fulfillments.map((fulfillment, index) => ( <li key={fulfillment.id} className="flex items-center justify-between gap-2 text-sm" > <span>{t("Shipment {n, number}", { n: index + 1 })}</span> {fulfillment.status === "delivered" ? ( <Badge variant="success-light">{t("Delivered")}</Badge> ) : fulfillment.status === "shipped" ? ( <Badge variant="info-light">{t("Shipped")}</Badge> ) : fulfillment.status === "cancelled" ? ( <Badge variant="destructive-light"> {t("Cancelled")} </Badge> ) : ( <Badge variant="secondary">{t("Packing")}</Badge> )} </li> ))} </ul> ) : null} </CardContent> </Card> <Card> <CardHeader> <CardTitle>{t("Promotions")}</CardTitle> </CardHeader> <CardContent> {view.kind === "draft" && canManage ? ( <PromotionCodeField cartId={view.id} applied={view.promotions.flatMap((promotion) => promotion.code ? [ { code: promotion.code, name: promotion.name, discount: promotion.discount, }, ] : [], )} canCreate={can(session, "promotions.manage")} /> ) : view.promotions.length === 0 ? ( <p className="text-sm text-muted-foreground"> {t("No promotion was applied.")} </p> ) : ( <ul className="flex flex-col gap-2"> {view.promotions.map((promotion) => ( <li key={promotion.code ?? promotion.name} className="flex items-center justify-between gap-2 text-sm" > <span className="flex items-center gap-2"> {promotion.code ? ( <Badge variant="success-light" className="font-mono"> {promotion.code} </Badge> ) : null} <span className="text-muted-foreground"> {promotion.name} </span> </span> <span className="tabular-nums"> {amount(negateMoney(promotion.discount))} </span> </li> ))} </ul> )} </CardContent> </Card> </div> </div> ); }