Contract renewals
Active contracts with their value, end date and notice deadline, the nearest deadline first, so a renewal is decided in time. 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 contracts. 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 examplecontract-renewals.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(); } const SOON_DAYS = 30; /** * Render inside `<Suspense>`: the organization's active contracts, the nearest * notice deadline first, so the team decides on a renewal before the * counterparty's notice period runs out. */ export async function ContractRenewals() { const [state, t, format, locale] = await Promise.all([ getRenewals(), getExtracted("compliance"), getFormatter(), getLocale(), ]); if (state.kind === "no-organization") { return null; } const day = (date: string) => format.dateTime(new Date(date), { dateStyle: "medium", timeZone: "UTC" }); const renewal = (row: RenewalRow) => { const days = row.noticeInDays; if (days === null) { return <Badge variant="secondary">{t("No end date")}</Badge>; } if (days < 0) { return ( <Badge variant="destructive-light"> {row.autoRenew ? t("Renews, notice passed") : t("Notice passed")} </Badge> ); } if (days <= SOON_DAYS) { return ( <Badge variant="warning-light"> {t("Notice in {days, number} days", { days })} </Badge> ); } return row.autoRenew ? ( <Badge variant="info-light">{t("Auto-renews")}</Badge> ) : ( <Badge variant="success-light">{t("Active")}</Badge> ); }; return ( <Card data-recipe="contract-renewals"> <CardHeader> <CardTitle>{t("Contract renewals")}</CardTitle> <CardDescription> {t( "Active contracts, the nearest notice deadline first. A contract that auto-renews extends itself unless someone gives notice before the deadline.", )} </CardDescription> </CardHeader> <CardContent> {state.kind === "error" ? ( <Alert variant="destructive"> <TriangleAlertIcon /> <AlertTitle>{t("The contracts 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"> <FileSignatureIcon /> </EmptyMedia> <EmptyTitle>{t("No active contracts")}</EmptyTitle> <EmptyDescription> {t( "A contract becomes active once every signer has signed. Draft one from a customer's record.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> <Link href="/customers" className={buttonVariants()}> {t("Open customers")} </Link> </EmptyContent> </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Contract")}</TableHead> <TableHead className="text-right">{t("Value")}</TableHead> <TableHead>{t("Ends")}</TableHead> <TableHead>{t("Notice by")}</TableHead> <TableHead>{t("Renewal")}</TableHead> </TableRow> </TableHeader> <TableBody> {state.data.map((row) => ( <TableRow key={row.id}> <TableCell className="font-medium">{row.title}</TableCell> <TableCell className="text-right tabular-nums"> {row.value ? formatMoney(moneyFromJson(row.value), locale) : "–"} </TableCell> <TableCell className="text-xs text-muted-foreground"> {row.endsOn ? day(row.endsOn) : "–"} </TableCell> <TableCell className="text-xs text-muted-foreground"> {row.noticeDeadline ? day(row.noticeDeadline) : "–"} </TableCell> <TableCell>{renewal(row)}</TableCell> </TableRow> ))} </TableBody> </Table> )} </CardContent> </Card> ); } export function ContractRenewalsSkeleton() { return <Skeleton className="h-40 rounded-xl" aria-busy="true" />; }