# Record history

> Every version `subject.versions` kept of a row, with the fields each one changed and a restore per field.

- Block: [versions](https://bettersupabase.com/blocks/versions.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/record-history

## Install the block

```bash
npx better-supabase add versions
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
interface FieldChange {
  readonly field: ProjectHistoryField;
  readonly before: string | undefined;
  readonly after: string | undefined;
}

/** The fields a version changed from the one before it; all of them for the first. */
function changesOf(
  version: ProjectVersion,
  previous: ProjectVersion | undefined,
): readonly FieldChange[] {
  return PROJECT_HISTORY_FIELDS.flatMap((field) => {
    const after = version.fields[field];
    const before = previous?.fields[field];

    return previous !== undefined && before === after
      ? []
      : [{ field, before, after }];
  });
}

/**
 * The project's history: every version the snapshot trigger kept, with the
 * fields it changed. Restore writes one field of an older version back and
 * leaves the others as they are.
 */
export function ProjectHistory({
  projectId,
  versions,
  canSave,
  canRestore,
}: {
  projectId: string;
  versions: readonly ProjectVersion[];
  canSave: boolean;
  canRestore: boolean;
}) {
  const t = useExtracted("projects");

  return (
    <Card size="sm" data-recipe="record-history">
      <CardHeader>
        <CardTitle>{t("History")}</CardTitle>
        <CardDescription>
          {t("Each change to the name, budget or currency.")}
        </CardDescription>
      </CardHeader>
      <CardContent>
        {versions.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <HistoryIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No history yet")}</EmptyTitle>
              <EmptyDescription>
                {t("The next change to this project appears here.")}
              </EmptyDescription>
            </EmptyHeader>
            {canSave ? (
              <EmptyContent>
                <SaveVersionButton projectId={projectId} />
              </EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <ol className="flex flex-col divide-y" data-testid="project-history">
            {versions.map((version, index) => (
              <VersionEntry
                key={version.id}
                projectId={projectId}
                version={version}
                changes={changesOf(version, versions[index + 1])}
                canRestore={canRestore && index > 0}
              />
            ))}
          </ol>
        )}
      </CardContent>
    </Card>
  );
}

function VersionEntry({
  projectId,
  version,
  changes,
  canRestore,
}: {
  projectId: string;
  version: ProjectVersion;
  changes: readonly FieldChange[];
  canRestore: boolean;
}) {
  const t = useExtracted("projects");
  const format = useFormatter();
  const locale = useLocale();
  const errorMessage = useErrorMessage();

  const restore = useAction(restoreProjectFields, {
    onSuccess: () => {
      toast.success(t("Field restored"));
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

  const label = {
    name: t("Name"),
    budget_minor: t("Budget"),
    currency: t("Currency"),
  } as const satisfies Record<ProjectHistoryField, string>;

  const source = {
    manual: t("Saved"),
    auto: t("Edited"),
    restore: t("Restored"),
  } as const satisfies Record<ProjectVersion["source"], string>;

  const shown = (field: ProjectHistoryField, value: string | undefined) => {
    if (value === undefined) {
      return t("Empty");
    }

    return field === "budget_minor"
      ? formatMoney(
          money(BigInt(value), version.fields.currency ?? "EUR"),
          locale,
        )
      : value;
  };

  return (
    <li className="flex flex-col gap-2 py-3">
      <div className="flex items-center gap-2">
        <span className="text-sm font-medium">
          {t("Version {number}", { number: String(version.number) })}
        </span>
        <Badge variant="secondary">{source[version.source]}</Badge>
        <span className="ml-auto text-xs text-muted-foreground">
          {format.dateTime(version.createdAt, {
            dateStyle: "medium",
            timeStyle: "short",
          })}
        </span>
      </div>
      <dl className="flex flex-col gap-1">
        {changes.map((change) => (
          <div key={change.field} className="flex items-center gap-2 text-sm">
            <dt className="w-20 shrink-0 text-muted-foreground">
              {label[change.field]}
            </dt>
            <dd className="flex min-w-0 flex-1 items-center gap-1">
              {change.before === undefined ? null : (
                <>
                  <span className="truncate text-muted-foreground line-through">
                    {shown(change.field, change.before)}
                  </span>
                  <ArrowRightIcon className="size-3 shrink-0 text-muted-foreground" />
                </>
              )}
              <span className="truncate">
                {shown(change.field, change.after)}
              </span>
            </dd>
            {canRestore ? (
              <Button
                size="xs"
                variant="ghost"
                disabled={restore.pending}
                aria-label={t("Restore {field} from version {number}", {
                  field: label[change.field],
                  number: String(version.number),
                })}
                onClick={() => {
                  void restore.run({
                    projectId,
                    versionId: version.id,
                    fields: [change.field],
                  });
                }}
              >
                {t("Restore")}
              </Button>
            ) : null}
          </div>
        ))}
      </dl>
    </li>
  );
}

function SaveVersionButton({ projectId }: { projectId: string }) {
  const t = useExtracted("projects");
  const errorMessage = useErrorMessage();

  const save = useAction(saveProjectVersion, {
    onSuccess: () => {
      toast.success(t("Version saved"));
    },
    onError: (error) => {
      toast.error(errorMessage(error));
    },
  });

  return (
    <Button
      size="sm"
      variant="outline"
      disabled={save.pending}
      onClick={() => {
        void save.run({ projectId });
      }}
    >
      <HistoryIcon />
      {t("Save a version")}
    </Button>
  );
}
```

## Prompt

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

### Web

```text
Build "Record history" in a React web app that uses shadcn/ui components. Every version `subject.versions` kept of a row, with the fields each one changed and a restore per field.

It uses the better-supabase `versions` block (Versions). 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 versions` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add badge button card empty`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/projects/components/project-history.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/record-history.md
- Block docs: https://bettersupabase.com/docs/blocks/versions
```
