This guide documents the reusable Onboarding block that handles user joining and school setup flows: create school, invite admins/teachers/parents, add students, subjects, and initial timetable basics.
src/components/onboarding/
actions.ts # server actions (createSchool, inviteUser, seedDefaults)
validation.ts # zod schemas
types.ts # shared types
form.tsx # RHF form(s)
content.tsx # step content composition
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const schema = z.object({
schoolName: z.string().min(2),
email: z.string().email(),
})
export function CreateSchoolForm({ onSubmit }: { onSubmit: (v: z.infer<typeof schema>) => Promise<void> }) {
const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema) })
return (
<form onSubmit={form.handleSubmit(async (v) => { await onSubmit(v) })} className="space-y-3">
<Input placeholder="School name" {...form.register("schoolName")} />
<Input type="email" placeholder="Email" {...form.register("email")} />
<Button type="submit">Create</Button>
</form>
)
}"use server"
import { z } from "zod"
import { db } from "@/lib/db"
const createSchool = z.object({ schoolName: z.string().min(2), email: z.string().email() })
export async function createSchoolAction(values: z.infer<typeof createSchool>) {
const parsed = createSchool.parse(values)
const school = await db.school.create({ data: { name: parsed.schoolName, email: parsed.email } })
return { ok: true, schoolId: school.id }
}Use sheets/dialogs to segment steps, keep forms minimal, and show toasts for feedback. Mirror UI for RTL.