RegistryUI recipes
Onboarding checklist
The organization's getting-started steps with progress, hidden once every step is done.
Both sides call useOnboarding from better-supabase/blocks/onboarding/react; the onboarding block must be installed (npx better-supabase add onboarding). Its page on bettersupabase.com is /recipes/onboarding-checklist.
Web, with shadcn components:
npx shadcn add button card progress skeletonnpx shadcn add @better-supabase/onboarding-checklist"use client";
import { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf } from "better-supabase/react";
import { CheckCircle2Icon, CircleIcon } from "lucide-react";
import { useExtracted } from "next-intl";
import { buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { gettingStarted } from "@/features/onboarding/checklist";
import { Link } from "@/i18n/navigation";
import { useAuth } from "@/lib/hooks";
import { blockConfig } from "@/lib/supabase/blocks.generated";
/** The API schema `gen` wrote for the onboarding block. */
const API_SCHEMA = blockConfig.onboarding.api;
/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingCard() {
const t = useExtracted("dashboard");
const organizationId = tenantOf(useAuth()) ?? null;
const { progress } = useOnboarding(gettingStarted, {
organizationId,
schema: API_SCHEMA,
});
if (!organizationId) {
return null;
}
if (progress === undefined) {
return <OnboardingCardSkeleton />;
}
if (progress.done) {
return null;
}
const titles = {
customer: t("Add your first customer"),
invite: t("Invite a teammate"),
"api-key": t("Create an API key"),
plan: t("Review your plan"),
};
return (
<Card data-testid="onboarding">
<CardHeader>
<CardTitle>{t("Get started")}</CardTitle>
<CardDescription>
{t("{completed, number} of {total, number} steps done", {
completed: progress.completed,
total: progress.total,
})}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Progress value={(progress.completed / progress.total) * 100} />
<ul className="divide-y">
{progress.steps.map((step) => (
<li key={step.id} className="flex items-center gap-3 py-2">
{step.completed ? (
<CheckCircle2Icon className="size-4 text-primary" />
) : (
<CircleIcon className="size-4 text-muted-foreground" />
)}
<span
className={
step.completed
? "flex-1 text-muted-foreground line-through"
: "flex-1"
}
>
{titles[step.id]}
</span>
{step.completed ? null : (
<Link
href={step.href ?? "/"}
className={buttonVariants({ variant: "outline", size: "sm" })}
>
{t("Start")}
</Link>
)}
</li>
))}
</ul>
</CardContent>
</Card>
);
}
export function OnboardingCardSkeleton() {
return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}React Native, with ActivityIndicator, Text, View from react-native:
npx shadcn add @better-supabase/onboarding-checklistimport { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf, useAuth } from "better-supabase/react";
import { ActivityIndicator, Text, View } from "react-native";
import { API_SCHEMA } from "../lib/blocks";
import { gettingStarted } from "../lib/onboarding";
/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingChecklist() {
const organizationId = tenantOf(useAuth()) ?? null;
const { progress } = useOnboarding(gettingStarted, {
organizationId,
schema: API_SCHEMA,
});
if (!organizationId) {
return null;
}
if (progress === undefined) {
return <ActivityIndicator />;
}
if (progress.done) {
return null;
}
return (
<View style={{ gap: 8, padding: 16, borderWidth: 1, borderRadius: 12 }}>
<Text style={{ fontSize: 17, fontWeight: "600" }}>Get started</Text>
<Text style={{ color: "#737373" }}>
{progress.completed} of {progress.total} steps done
</Text>
<View style={{ height: 6, borderRadius: 3, backgroundColor: "#e5e5e5" }}>
<View
style={{
height: 6,
borderRadius: 3,
backgroundColor: "#111",
width: `${(progress.completed / progress.total) * 100}%`,
}}
/>
</View>
{progress.steps.map((step) => (
<Text
key={step.id}
style={
step.completed
? { color: "#737373", textDecorationLine: "line-through" }
: undefined
}
>
{step.completed ? "✓" : "○"} {step.title}
</Text>
))}
</View>
);
}Last updated on