- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
Finance
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
Sales & GTM
form.tsx renders inputs, runs Zod validation via react-hook-form, and calls server actions on submit. Sits next to validation.ts (schemas) and actions.ts (server actions).
Categories
| Category | Shape | Reference |
|---|---|---|
| CRUD modal | Create / edit / view via useModal() (130–250 lines) | listings/subjects/form.tsx |
| URL-routed wizard step | Page per step under WizardLayout | listings/teachers/wizard/information/form.tsx |
| Application wizard step | forwardRef + useImperativeHandle exposing saveAndNext() | apply/personal/form.tsx |
| Onboarding step | Wizard step for school onboarding | onboarding/branding/form.tsx |
| Auth | Login / register / reset / 2FA — pre-tenant | auth/login/form.tsx |
| Settings / config | Inline update, no modal | timetable/settings/form.tsx |
| Exam / lumos | Dynamic field arrays | exams/qbank/form.tsx |
Rules
| # | Rule |
|---|---|
| 1 | "use client" at the top |
| 2 | Schema imported from validation.ts — never inline |
| 3 | zodResolver typed with useForm<XInput> — never as any |
| 4 | Use <Name>Input from validation.ts for values |
| 5 | Stay under ~200 lines |
| 6 | Submit through a dedicated onSubmit — call server action, handle response, toast, close |
Canonical example
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { createSubject, updateSubject } from "./actions"
import { subjectCreateSchema, type SubjectCreateInput } from "./validation"
const form = useForm<SubjectCreateInput>({
resolver: zodResolver(subjectCreateSchema),
defaultValues: { subjectName: "", departmentId: "" },
})
async function onSubmit(values: SubjectCreateInput) {
const res = currentId
? await updateSubject({ id: currentId, ...values })
: await createSubject(values)
if (res?.success) {
toast.success(currentId ? "Subject updated" : "Subject created")
closeModal()
onSuccess?.()
} else {
toast.error(res?.error || "Failed to save")
}
}Pattern selection
| Scenario | Pattern |
|---|---|
| Simple create form (1–5 fields) | useActionState + native <form action={}> |
| Complex CRUD with edit mode | react-hook-form + zodResolver |
| URL-routed multi-step wizard | WizardLayout + WizardStep |
| Application wizard step | react-hook-form + forwardRef |
| Real-time field validation | react-hook-form + zodResolver |
| File uploads | react-hook-form + custom handlers |
Progressive enhancement (React 19)
For simple forms, use native <form action={}> with useActionState:
"use client"
import { useActionState } from "react"
import { useFormStatus } from "react-dom"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { createItem } from "./actions"
export function SimpleForm() {
const [state, action, pending] = useActionState(createItem, null)
return (
<form action={action} className="space-y-4">
<Input name="title" required />
{state?.error && <p className="text-destructive text-sm">{state.error}</p>}
<SubmitButton />
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return <Button type="submit" disabled={pending}>{pending ? "Saving…" : "Save"}</Button>
}The action signature must accept (prevState, formData).
Wizard mechanics
useModal()exposesmodeandid.id.startsWith("view:")means read-only.createWizardProviderships data with retry; the step readsinitialData.- Application steps expose
{ saveAndNext: () => Promise<boolean> }viaforwardRef. onValidChangecallback emits validity to the parent for next-button enable/disable.
Anti-patterns
zodResolver(schema) as any— fix the schema ordefaultValuesinstead.- Inline schemas — schemas belong in
validation.ts. dictionary: any— type the slice.- Monolith forms over 200 lines — split into step components.
- Mixed concerns — no fetching, no table rendering, no utils. Data arrives via props.
File layout
src/components/<feature>/
config.ts # Option arrays
validation.ts # Zod schemas + inferred types
actions.ts # Server actions
types.ts # Form props, ref types
form.tsx # Client component (this file)
content.tsx # Server component (renders form)