# Trash bin

> Deleted records with buttons to restore them or delete them for good.

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

## Install the block

```bash
npx better-supabase add trash
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
import { FolderOpenIcon, Trash2Icon, 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, buttonVariants } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { can } from "@/features/user/user-permissions";
import { getSession } from "@/features/user/user-queries";
import { Link } from "@/i18n/navigation";

import { getTrash } from "../trash-queries";
import { TrashItemActions } from "./trash-item-actions";

// 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>`: what was deleted, with Restore and Delete forever. */
export async function TrashBin() {
  const [view, session, t, format] = await Promise.all([
    getTrash(),
    getSession(),
    getExtracted("trash"),
    getFormatter(),
  ]);

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

  const kinds = new Map([
    ["customer", t("Customer")],
    ["drive_node", t("Drive item")],
  ]);

  const canPurge = can(session, "trash.manage");

  return (
    <Card data-recipe="trash-bin">
      <CardHeader>
        <CardTitle>{t("Deleted items")}</CardTitle>
        <CardDescription>
          {t(
            "Restore an item to put it back where it was, with everything deleted along with it.",
          )}
        </CardDescription>
      </CardHeader>
      <CardContent>
        {view.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The trash 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>
        ) : view.items.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <Trash2Icon />
              </EmptyMedia>
              <EmptyTitle>{t("The trash is empty")}</EmptyTitle>
              <EmptyDescription>
                {t("Items you delete in the drive or elsewhere show up here.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Link href="/drive" className={buttonVariants()}>
                <FolderOpenIcon />
                {t("Open the drive")}
              </Link>
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Item")}</TableHead>
                <TableHead>{t("Type")}</TableHead>
                <TableHead>{t("Deleted")}</TableHead>
                <TableHead>{t("Removed for good")}</TableHead>
                <TableHead className="text-right">{t("Actions")}</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {view.items.map((item) => {
                const title = item.title ?? t("Untitled");

                return (
                  <TableRow key={item.id}>
                    <TableCell className="font-medium">
                      <div className="flex items-center gap-2">
                        {title}
                        {item.count > 1 ? (
                          <span className="text-xs text-muted-foreground">
                            {t("and {count, number} more", {
                              count: item.count - 1,
                            })}
                          </span>
                        ) : null}
                      </div>
                    </TableCell>
                    <TableCell>
                      <Badge variant="secondary">
                        {kinds.get(item.subjectType) ?? item.subjectType}
                      </Badge>
                    </TableCell>
                    <TableCell className="text-muted-foreground">
                      {format.dateTime(new Date(item.trashedAt), {
                        dateStyle: "medium",
                        timeStyle: "short",
                      })}
                    </TableCell>
                    <TableCell className="text-muted-foreground">
                      {item.purgeAfter === null
                        ? t("Never")
                        : format.dateTime(new Date(item.purgeAfter), {
                            dateStyle: "medium",
                          })}
                    </TableCell>
                    <TableCell>
                      <TrashItemActions
                        id={item.id}
                        title={title}
                        canPurge={canPurge}
                      />
                    </TableCell>
                  </TableRow>
                );
              })}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

export function TrashBinSkeleton() {
  return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}
```

## Prompt

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

### Web

```text
Build "Trash bin" in a React web app that uses shadcn/ui components. Deleted records with buttons to restore them or delete them for good.

It uses the better-supabase `trash` block (Trash). 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 trash` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button card empty skeleton table`.
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/trash/components/trash-bin.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/trash-bin.md
- Block docs: https://bettersupabase.com/docs/blocks/trash
```
