# Hiring board

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

Source: https://bettersupabase.com/docs/registry/ui-recipes/hiring-board

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [hiring block](https://bettersupabase.com/blocks/hiring) must be installed (`npx better-supabase add hiring`). Its page on bettersupabase.com is [/recipes/hiring-board](https://bettersupabase.com/recipes/hiring-board).

Web, with shadcn components:

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

```tsx title="apps/examples/nextjs/src/features/people/components/hiring-board.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>
  );
}
```