# Templates gallery

> Templates in a grid with a preview and a button to create a record from one.

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

## Install the block

```bash
npx better-supabase add templates
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
import { LayoutTemplateIcon, TriangleAlertIcon } from "lucide-react";
import { getExtracted, getFormatter } from "next-intl/server";
import { refresh } from "next/cache";

import {
  Alert,
  AlertAction,
  AlertDescription,
  AlertTitle,
} from "@/components/reui/alert";
import { Badge } from "@/components/reui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import { can } from "@/features/user/user-permissions";
import { getSession } from "@/features/user/user-queries";

import { getTemplates } from "../templates-queries";
import { CreateTemplateDialog } from "./create-template-dialog";
import { UseTemplateDialog } from "./use-template-dialog";

// 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();
}

/** The New template button, for members who may manage templates. */
export async function TemplateManagerActions() {
  const session = await getSession();

  return can(session, "templates.manage") ? <CreateTemplateDialog /> : null;
}

/** Render inside `<Suspense>`: the customer templates, each with Use template. */
export async function TemplatesGallery() {
  const [view, session, t, format] = await Promise.all([
    getTemplates(),
    getSession(),
    getExtracted("templates"),
    getFormatter(),
  ]);

  if (view.kind === "no-organization") {
    return null;
  }

  const canApply =
    can(session, "templates.apply") && can(session, "customers.write");

  if (view.kind === "error") {
    return (
      <Alert variant="destructive" data-recipe="templates-gallery">
        <TriangleAlertIcon />
        <AlertTitle>{t("The templates 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>
    );
  }

  if (view.templates.length === 0) {
    return (
      <Empty className="border" data-recipe="templates-gallery">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <LayoutTemplateIcon />
          </EmptyMedia>
          <EmptyTitle>{t("No templates yet")}</EmptyTitle>
          <EmptyDescription>
            {t(
              "A template holds the values a new customer starts with, so the team sets them up the same way.",
            )}
          </EmptyDescription>
        </EmptyHeader>
        {can(session, "templates.manage") ? (
          <EmptyContent>
            <CreateTemplateDialog />
          </EmptyContent>
        ) : null}
      </Empty>
    );
  }

  return (
    <div
      className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
      data-recipe="templates-gallery"
    >
      {view.templates.map((template) => (
        <Card key={template.id}>
          <CardHeader>
            <CardTitle>{template.name}</CardTitle>
            <CardDescription>
              {t("Updated {date}", {
                date: format.dateTime(new Date(template.updatedAt), {
                  dateStyle: "medium",
                }),
              })}
            </CardDescription>
          </CardHeader>
          <CardContent className="flex flex-wrap items-center gap-2">
            {template.status === null ? null : (
              <Badge variant="info-light">
                {template.status === "active" ? t("Active") : t("Lead")}
              </Badge>
            )}
            {template.publishedVersion === null ? (
              <Badge variant="warning-light">{t("Not published")}</Badge>
            ) : (
              <Badge variant="outline">
                {t("Version {version}", {
                  version: String(template.publishedVersion),
                })}
              </Badge>
            )}
            {template.scope === "system" ? (
              <Badge variant="secondary">{t("Built in")}</Badge>
            ) : null}
            {template.isDefault ? (
              <Badge variant="success-light">{t("Default")}</Badge>
            ) : null}
          </CardContent>
          <CardFooter>
            <UseTemplateDialog
              templateId={template.id}
              templateName={template.name}
              disabled={!canApply || template.publishedVersion === null}
            />
          </CardFooter>
        </Card>
      ))}
    </div>
  );
}

export function TemplatesGallerySkeleton() {
  return (
    <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
      <Skeleton className="h-44 rounded-xl" />
      <Skeleton className="h-44 rounded-xl" />
      <Skeleton className="h-44 rounded-xl" />
    </div>
  );
}
```

## Prompt

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

### Web

```text
Build "Templates gallery" in a React web app that uses shadcn/ui components. Templates in a grid with a preview and a button to create a record from one.

It uses the better-supabase `templates` block (Templates). 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 templates` 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/templates/components/templates-gallery.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/templates-gallery.md
- Block docs: https://bettersupabase.com/docs/blocks/templates
```
