Kitchen display
Open tickets per kitchen station, each line moved from queued to served, refreshed over Realtime. 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 skeletonInstall 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 examplekitchen-display.tsx /** * Render inside `<Suspense>`: the open tickets at one station, or at every * station, oldest first. Each ticket moves its lines to the next state, and the * page refreshes when a line changes anywhere in the kitchen. */ export async function KitchenDisplay({ searchParams, }: { searchParams: Promise<Record<string, string | string[] | undefined>>; }) { const [session, params] = await Promise.all([getSession(), searchParams]); if (!can(session, "orders.read") && !can(session, "orders.prepare")) { forbidden(); } const canPrepare = can(session, "orders.prepare") || can(session, "orders.manage"); const [kitchen, t] = await Promise.all([ getKitchen( v.is(StationParam, params["station"]) ? params["station"] : null, ), getExtracted("shop"), ]); if (kitchen.kind === "no-organization") { return null; } if (kitchen.kind === "error") { return ( <Alert variant="destructive" data-recipe="kitchen-display"> <TriangleAlertIcon /> <AlertTitle>{t("The kitchen 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 { organizationId, stations, stationId, tickets } = kitchen.data; return ( <div data-recipe="kitchen-display" className="flex flex-col gap-4"> <KitchenLive organizationId={organizationId} /> {stations.length > 1 ? ( <nav aria-label={t("Stations")} className="flex flex-wrap gap-1"> <Link href="/shop/kitchen" aria-current={stationId === null ? "page" : undefined} className={buttonVariants({ size: "sm", variant: stationId === null ? "secondary" : "ghost", })} > {t("All stations")} </Link> {stations.map((each) => ( <Link key={each.id} href={`/shop/kitchen?station=${each.id}`} aria-current={stationId === each.id ? "page" : undefined} className={buttonVariants({ size: "sm", variant: stationId === each.id ? "secondary" : "ghost", })} > {each.name} </Link> ))} </nav> ) : null} {tickets.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <ChefHatIcon /> </EmptyMedia> <EmptyTitle>{t("No open tickets")}</EmptyTitle> <EmptyDescription> {t( "Send a paid order to the kitchen and its lines show up here.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> <Link href="/shop/orders" className={buttonVariants({ size: "sm" })} > {t("Open orders")} </Link> </EmptyContent> </Empty> ) : ( <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {tickets.map((ticket) => ( <KitchenTicket key={ticket.orderId} ticket={ticket} stationId={stationId} canPrepare={canPrepare} /> ))} </div> )} </div> ); }