# Hiring board

> Candidates for an opening as cards in interview stage columns.

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

## Install the block

```bash
npx better-supabase add hiring
```

## Web (Next.js)

Add the shadcn components:

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

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

```tsx
/**
 * Render inside `<Suspense>`: the candidates of one opening as cards in
 * its stage columns. Dragging a card to Hired or Rejected closes the
 * application; to another open stage, moves it. `?job=` picks the opening.
 */
export async function HiringBoard({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const [session, t, query] = await Promise.all([
    getSession(),
    getExtracted("people"),
    searchParams,
  ]);

  if (!can(session, "hiring.read")) {
    forbidden();
  }

  const board = await settle(loadHiringBoard);

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

  const canManage = can(session, "hiring.manage");
  const canWrite = can(session, "hiring.write");
  const jobs = board.kind === "ok" ? board.data.jobs : [];
  const job = jobs.find((item) => item.id === query["job"]) ?? jobs[0];

  const cards =
    board.kind === "ok" && job
      ? board.data.cards.filter((card) => card.jobId === job.id)
      : [];

  const createOpening = canManage ? <CreateOpeningDialog /> : null;

  return (
    <Card data-recipe="hiring-board">
      <CardHeader>
        <CardTitle>{job?.title ?? t("Hiring")}</CardTitle>
        <CardDescription>
          {t("Drag a candidate to the next stage, or to Hired or Rejected.")}
        </CardDescription>
        {jobs.length > 0 || board.kind === "error" ? (
          <CardAction className="flex items-center gap-2">
            {job && canWrite && board.kind === "ok" && cards.length > 0 ? (
              <AddCandidateDialog
                jobId={job.id}
                candidates={board.data.candidates}
              />
            ) : null}
            {createOpening}
          </CardAction>
        ) : null}
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        {board.kind === "error" ? (
          <PeopleLoadError title={t("The hiring board didn't load")} />
        ) : job === undefined ? (
          <Empty className="border">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <BriefcaseIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No openings yet")}</EmptyTitle>
              <EmptyDescription>
                {canManage
                  ? t("Create an opening, then add candidates to it.")
                  : t("Ask a hiring manager to create an opening.")}
              </EmptyDescription>
            </EmptyHeader>
            {createOpening ? (
              <EmptyContent>{createOpening}</EmptyContent>
            ) : null}
          </Empty>
        ) : (
          <>
            {jobs.length > 1 ? (
              <nav
                aria-label={t("Openings")}
                className="flex flex-wrap items-center gap-2"
              >
                {jobs.map((item) => (
                  <Link
                    key={item.id}
                    href={`/people/hiring?job=${item.id}`}
                    aria-current={item.id === job.id ? "page" : undefined}
                    className={buttonVariants({
                      variant: item.id === job.id ? "secondary" : "ghost",
                      size: "sm",
                    })}
                  >
                    {item.title}
                    {item.status === "open" ? null : (
                      <Badge variant="outline">
                        {item.status === "draft" ? t("Draft") : t("Closed")}
                      </Badge>
                    )}
                  </Link>
                ))}
              </nav>
            ) : null}
            {cards.length === 0 ? (
              <Empty className="border">
                <EmptyHeader>
                  <EmptyMedia variant="icon">
                    <UserPlusIcon />
                  </EmptyMedia>
                  <EmptyTitle>{t("No candidates yet")}</EmptyTitle>
                  <EmptyDescription>
                    {t("Candidates start in the first stage of the pipeline.")}
                  </EmptyDescription>
                </EmptyHeader>
                {canWrite ? (
                  <EmptyContent>
                    <AddCandidateDialog
                      jobId={job.id}
                      candidates={board.data.candidates}
                    />
                  </EmptyContent>
                ) : null}
              </Empty>
            ) : (
              <HiringKanban
                key={cards
                  .map((card) => `${card.id}:${card.stageId}`)
                  .join(",")}
                stages={board.data.stages}
                cards={cards}
                canMove={canWrite}
              />
            )}
          </>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Hiring board" in a React web app that uses shadcn/ui components. Candidates for an opening as cards in interview stage columns.

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