# Drive explorer

> Folders and files with upload, rename, move and a breadcrumb trail.

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

## Install the block

```bash
npx better-supabase add drive
```

## Web (Next.js)

Add the shadcn components:

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

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

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

import {
  Alert,
  AlertAction,
  AlertDescription,
  AlertTitle,
} from "@/components/reui/alert";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } 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 { Link } from "@/i18n/navigation";

import { getDriveFolder } from "../drive-queries";
import { DriveItemActions } from "./drive-item-actions";
import { CreateFolderDialog, UploadFileButton } from "./drive-toolbar";

// 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>`: one folder of the drive (the root for `null`). */
export async function DriveExplorer({
  folderId,
}: {
  folderId: Promise<string | null>;
}) {
  const [view, t, format] = await Promise.all([
    folderId.then(getDriveFolder),
    getExtracted("drive"),
    getFormatter(),
  ]);

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

  if (view.kind === "error") {
    return (
      <Alert variant="destructive" data-recipe="drive-explorer">
        <TriangleAlertIcon />
        <AlertTitle>{t("The drive 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 current = view.crumbs.at(-1);

  return (
    <Card data-recipe="drive-explorer">
      <CardHeader className="flex flex-wrap items-center justify-between gap-3">
        <Breadcrumb>
          <BreadcrumbList>
            <BreadcrumbItem>
              {current === undefined ? (
                <BreadcrumbPage>{t("My drive")}</BreadcrumbPage>
              ) : (
                <BreadcrumbLink render={<Link href="/drive" />}>
                  {t("My drive")}
                </BreadcrumbLink>
              )}
            </BreadcrumbItem>
            {view.crumbs.map((crumb) => (
              <Fragment key={crumb.id}>
                <BreadcrumbSeparator />
                <BreadcrumbItem>
                  {crumb.id === current?.id ? (
                    <BreadcrumbPage>{crumb.name}</BreadcrumbPage>
                  ) : (
                    <BreadcrumbLink
                      render={<Link href={`/drive/${crumb.id}`} />}
                    >
                      {crumb.name}
                    </BreadcrumbLink>
                  )}
                </BreadcrumbItem>
              </Fragment>
            ))}
          </BreadcrumbList>
        </Breadcrumb>
        <div className="flex gap-2">
          <CreateFolderDialog parentId={view.folderId} />
          <UploadFileButton parentId={view.folderId} />
        </div>
      </CardHeader>
      <CardContent>
        {view.items.length === 0 ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FolderOpenIcon />
              </EmptyMedia>
              <EmptyTitle>{t("This folder is empty")}</EmptyTitle>
              <EmptyDescription>
                {t("Create a folder or upload a file to get started.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <UploadFileButton parentId={view.folderId} />
            </EmptyContent>
          </Empty>
        ) : (
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>{t("Name")}</TableHead>
                <TableHead>{t("Size")}</TableHead>
                <TableHead>{t("Added")}</TableHead>
                <TableHead className="w-12">
                  <span className="sr-only">{t("Actions")}</span>
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {view.items.map((item) => (
                <TableRow key={item.id}>
                  <TableCell className="font-medium">
                    {item.kind === "folder" ? (
                      <Link
                        href={`/drive/${item.id}`}
                        className="flex items-center gap-2 transition-colors hover:text-primary"
                      >
                        <FolderIcon className="size-4 text-muted-foreground" />
                        {item.name}
                      </Link>
                    ) : (
                      <span className="flex items-center gap-2">
                        <FileIcon className="size-4 text-muted-foreground" />
                        {item.name}
                      </span>
                    )}
                  </TableCell>
                  <TableCell className="text-muted-foreground">
                    {item.kind === "folder"
                      ? t("Folder")
                      : item.size === null
                        ? t("Uploading")
                        : t("{size} KB", {
                            size: format.number(item.size / 1024, {
                              maximumFractionDigits: 1,
                            }),
                          })}
                  </TableCell>
                  <TableCell className="text-muted-foreground">
                    {format.dateTime(new Date(item.createdAt), {
                      dateStyle: "medium",
                    })}
                  </TableCell>
                  <TableCell>
                    <DriveItemActions
                      id={item.id}
                      kind={item.kind}
                      name={item.name}
                    />
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        )}
      </CardContent>
    </Card>
  );
}

export function DriveExplorerSkeleton() {
  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 "Drive explorer" in a React web app that uses shadcn/ui components. Folders and files with upload, rename, move and a breadcrumb trail.

It uses the better-supabase `drive` block (Drive). 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 drive` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert breadcrumb 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/drive/components/drive-explorer.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/drive-explorer.md
- Block docs: https://bettersupabase.com/docs/blocks/drive
```
