# Kitchen display

> Open tickets per kitchen station, each line moved from queued to served, refreshed over Realtime.

- Block: [orders](https://bettersupabase.com/blocks/orders.md)
- Kind: screen
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/kitchen-display

## Install the block

```bash
npx better-supabase add orders
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button card empty skeleton
```

From `apps/examples/nextjs/src/features/shop/components/kitchen-display.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/shop/components/kitchen-display.tsx):

```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>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Kitchen display" in a React web app that uses shadcn/ui components. Open tickets per kitchen station, each line moved from queued to served, refreshed over Realtime.

It uses the better-supabase `orders` block (Orders). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add orders` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button card empty skeleton`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/shop/components/kitchen-display.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/kitchen-display.md
- Block docs: https://bettersupabase.com/docs/blocks/orders
```
