Consent register
The purposes an organization asks consent for, with their lawful basis, version, how long a grant lasts and whether it needs a signature. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert badge button card empty skeleton tableInstall the glue
Install the block with
npx better-supabase add consents. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the exampleconsent-register.tsx // 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>`: the purposes the organization asks consent for, * with their lawful basis, version and how long a grant lasts. */ export async function ConsentRegister() { const [state, t] = await Promise.all([ getPurposes(), getExtracted("compliance"), ]); if (state.kind === "no-organization") { return null; } const basis = (legalBasis: LegalBasis) => { switch (legalBasis) { case "consent": { return t("Consent"); } case "contract": { return t("Contract"); } case "legal_obligation": { return t("Legal obligation"); } case "vital_interest": { return t("Vital interest"); } case "public_task": { return t("Public task"); } case "legitimate_interest": { return t("Legitimate interest"); } default: { const never: never = legalBasis; return String(never); } } }; return ( <Card data-recipe="consent-register"> <CardHeader> <CardTitle>{t("Consent register")}</CardTitle> <CardDescription> {t( "What the organization asks consent for. A new version of a purpose asks every record again.", )} </CardDescription> </CardHeader> <CardContent> {state.kind === "error" ? ( <Alert variant="destructive"> <TriangleAlertIcon /> <AlertTitle>{t("The consent purposes 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> ) : state.data.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <FileCheckIcon /> </EmptyMedia> <EmptyTitle>{t("No consent purposes")}</EmptyTitle> <EmptyDescription> {t( "Save a purpose, such as marketing email, before recording consent on a record.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> <a href="https://bettersupabase.com/docs/blocks/consents" className={buttonVariants()} > {t("Read the consents guide")} </a> </EmptyContent> </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Purpose")}</TableHead> <TableHead>{t("Lawful basis")}</TableHead> <TableHead>{t("Version")}</TableHead> <TableHead>{t("Lasts")}</TableHead> <TableHead>{t("Evidence")}</TableHead> </TableRow> </TableHeader> <TableBody> {state.data.map((row) => ( <TableRow key={row.id}> <TableCell> <div className="flex flex-col gap-1"> <span className="font-medium">{row.title}</span> <span className="text-xs text-muted-foreground"> {row.key} </span> </div> </TableCell> <TableCell>{basis(row.legalBasis)}</TableCell> <TableCell className="tabular-nums">{row.version}</TableCell> <TableCell className="text-xs text-muted-foreground"> {row.validMonths === null ? t("Until withdrawn") : t("{months, number} months", { months: row.validMonths, })} </TableCell> <TableCell> <div className="flex gap-1"> {row.requiresSignature ? ( <Badge variant="info-light">{t("Signature")}</Badge> ) : ( <Badge variant="secondary">{t("Any")}</Badge> )} {row.system ? ( <Badge variant="secondary">{t("System")}</Badge> ) : null} </div> </TableCell> </TableRow> ))} </TableBody> </Table> )} </CardContent> </Card> ); } export function ConsentRegisterSkeleton() { return <Skeleton className="h-40 rounded-xl" aria-busy="true" />; }