Onboarding (Blocks, Flows, and Patterns)
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.
Architecture Overview
- UI: shadcn/ui primitives (Input, Button, Select, Dialog, Sheet) composed into small steps.
- Forms: React Hook Form for controlled state with minimal re-renders.
- Validation: Zod schemas collocated per feature; parsed again on the server.
- Actions: Next.js Server Actions ("use server") for mutations, revalidate and redirect on success.
- Tenancy: all writes include schoolId, derived from subdomain/session.
Folder Pattern (per feature)
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
Zod + RHF Example
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>
)
}
Server Action Pattern
"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 }
}
Flow Orchestration
- Owner: Create school -> choose plan (trial by default) -> invite admin.
- Admin: Configure classes and subjects -> invite teachers and parents.
- Teacher: Join class -> mark attendance -> post first announcement.
- Parent: Accept invite -> verify -> view child.
Use sheets/dialogs to segment steps, keep forms minimal, and show toasts for feedback. Mirror UI for RTL.
Multi-tenant Considerations
- Derive schoolId from subdomain/session and include in every mutation.
- Seed defaults per school (roles, example classes/subjects) via server action.
- Enforce unique constraints scoped by schoolId in Prisma schema.
Checklists
- Form + schema + action exist and are wired.
- Success and error toasts present.
- Revalidate path or redirect after mutations.
- Add tests or manual steps in PR.