- 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
- 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
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
Listings are the backbone of data-driven features. 11 entities follow the same shape — students, teachers, classrooms, assignments, etc. — with consistent UX and optimistic updates.
Features
| Feature | Behaviour |
|---|---|
| Optimistic updates | Add / update / remove instantly with automatic rollback on errors |
| URL-persisted views | Table / grid view toggle via URL query params (nuqs) |
| Infinite scroll | Load-more pagination, no page reloads |
| Centralised queries | Type-safe query builders with role-based authorisation per module |
| Name display | School picks first + second or first + last name (School → Configuration → Name Display); compound Arabic names (عبد الله, أبو بكر) stay whole, names are bidi-isolated |
| Login credentials | 6-digit username (year + school serial) and an easy 4-digit PIN; first sign-in asks for a new password; admins can always reset |
File patterns
Every listing follows the mirror pattern — the URL produces two directories: one in app/ for routing, one in components/<feature>/ for everything else.
| File | Purpose |
|---|---|
content.tsx | Server component — data fetching, tenant context |
table.tsx | Client component — interactive table with state |
columns.tsx | Column definitions (client, uses hooks) |
form.tsx | Create / edit form with validation |
actions.ts | Server actions — validate, scope tenant, mutate |
queries.ts | Query builders with Prisma |
authorization.ts | RBAC permission checks |
validation.ts | Zod schemas + refinements |
types.ts | TypeScript interfaces |
config.ts | Constants, options, labels |
list-params.ts | URL params (nuqs) |
README.md | Feature documentation |
ISSUE.md | Known issues tracker |
Page composition
PageTitle src/components/atom/page-title.tsx
Abc Management — "Manage all abc records in your school"
PageNav src/components/atom/page-nav.tsx
All | Active | Archive | Settings
Toolbar src/components/atom/toolbar.tsx
[search] [filters] [view ≡/⊞] [export ↓] [create +]
DataTable src/components/table/data-table.tsx
OR GridContainer src/components/atom/grid-container.tsx
Modal (when open) src/components/atom/modal/modal
ModalFormLayout
Form (listings/abc/form.tsx)
ModalFooter
Step 1 of 2: Basic Info [Cancel] [Next]
Component flow
Route (page.tsx)
→ Content (content.tsx) ── Server
• getTenantContext()
• Parse URL params
• Fetch via queries.ts
→ Table (table.tsx) ── Client
• useModal()
• usePlatformView()
• useDataTable()
├─ Toolbar (Search, ViewToggle, Export, Create)
├─ DataTable / GridContainer (Columns, row actions, pagination)
└─ Modal (Form, ModalFormLayout, ModalFooter)
Reference implementation
Grades is the canonical reference. All paths under src/components/school-dashboard/listings/grades/:
| Pattern | File |
|---|---|
| Server actions | actions.ts |
| Queries | queries.ts |
| Authorization | authorization.ts |
| Validation | validation.ts |
| Table | table.tsx |
| Columns | columns.tsx |
| Form | form.tsx |
| Content | content.tsx |
Core hooks
| Hook | Path | Purpose |
|---|---|---|
usePlatformData | src/hooks/use-platform-data.ts | Data fetching with optimistic updates and infinite scroll |
usePlatformView | src/hooks/use-platform-view.ts | View mode (table / grid) with URL persistence |
useDataTable | src/components/table/use-data-table.ts | TanStack Table state management |
useModal | src/components/atom/modal/context.tsx | Modal open / close state |
Modules
All 11 listings ship table, grid, search, and export:
Announcements, Assignments, Classes, Classrooms, Events, Grades, Parents, Staff, Students, Subjects, Teachers.
Teachers are assigned to subjects in sections, not to classes: the wizard's Subjects & sections step and the Assign subjects row action share one editor, and saving moves the teacher onto those subjects' periods in the timetable (see timetable, "Assigning teachers to subjects"). The editor picks one grade at a time from G1…G12 chips and shows its subjects as a swipeable row of thumbnail cards; tapping a card takes every free section, and the section chips on the card hand over one section. Grades are entered per student and subject — the student's section, grade and term come with them — so a school without classes can still record a score. Assignments are set for a grade — one section or the whole grade — and a subject; students see and submit the ones set for their section or grade.
Subjects additionally carries a school-side customization layer over the platform catalog: on the subject detail page admins get a Customize panel (
SchoolCatalogCustomization) to hide chapters / lessons / a specific instructor's video, hide a lesson's quiz, set the preferred instructor source, and contribute the school's own videos. It writes per-schoolContentOverride/InstructorPreferencerows enforced by the lumos (LMS) read paths — see the catalog docs (“School-side controls”).
Best practices
Multi-tenant safety
- Always include
schoolIdin every database query. - Get from session:
const { schoolId } = session.user. - Missing
schoolIdbreaks tenant isolation.
Column definitions
- Define handlers before the columns
useMemo. - Columns with hooks (
useModal) must be generated in client components. - Pass callbacks via
getColumns(dictionary, lang, { onDelete, onEdit, onView }).
Optimistic updates
- Call
optimisticRemove(id)before server request. - Call
refresh()on error to rollback. - Use
optimisticUpdate(id, updater)for in-place updates.
Server actions
- Start with
"use server". - Validate with Zod on both client (UX) and server (security).
- Call
revalidatePath()orredirect()after mutations. - Return typed
ActionResponse<T>.
Server action errors
- Return an
ACTION_ERRORScode (actionError(ACTION_ERRORS.SAVE_FAILED)), nevererror.message. A Zod or Prisma message is not a sentence for an admin. - Log the real error on the server (
console.error) before returning the code. - In the form, toast
actionErrorMessage(result.error, dictionary, fallback)from@/lib/resolve-action-error. It returns the translated sentence for the code, or the fallback instead of a rawSNAKE_CASEcode. - In a
catch, toast the translated fallback, nevererr.message. - Wizards finish through one function per entity (
students/wizard/finish.ts,teachers/wizard/finish.ts), so the last step's button and the footer's skip-and-create behave the same way.
Wizard drafts
- Opening an add wizard creates the row up front, because the URL carries its id.
- Closing before entering anything deletes it. The lists never show an empty draft, and a nightly cron removes the ones a closed tab left behind (
empty-drafts.tsholds the single rule). - A draft with any data is kept and can be resumed from the list.
- The student, teacher and parent wizards open instantly. "+" mints the draft id in the browser, opens the wizard on an empty seed and writes the row in the background (
useDraftLauncher). - Their steps switch without a request (
WizardLayout steps,history.pushState), and Next saves in the background through the wizard runtime. Create waits for every save, and a failed save brings its step back. - A step stays mounted after you leave it. An effect that initialises from the loaded row must run once per record, not on every show.
Modal forms
- Use
ModalFormLayoutfor two-column header / form layout. - Use
ModalFooterfor progress bar and navigation. - Call
onSuccess()after successful mutations.
Maturity levels
| Level | Feature | Status |
|---|---|---|
| 1 | Basic CRUD | Complete |
| 2 | Relationship context (counts, quick links) | In progress |
| 3 | Rich detail pages with related data tabs | In progress |
| 4 | Cross-listing actions (bulk operations) | In progress |
| 5 | Automated workflows (triggers, notifications) | Planned |
See also
- Pattern — mirror pattern, naming conventions
- Students — listing with wizard flow
- Multi-tenancy —
schoolIdscoping