- 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
content.tsx owns data fetching (server) or hook orchestration (client) and composes children. Pages stay thin (auth + tenant + render); real logic lives here.
Two flavours
| Flavour | When |
|---|---|
| Server content | Async function, fetches via queries.ts, no "use client" |
| Client content | "use client", hooks/state, fetches via server actions |
Decision tree:
| Needs | Use |
|---|---|
| Hooks, events, browser APIs | Client content |
| Data fetching + composition | Server content |
| Both | Server content rendering client sub-components |
Rules
| # | Rule |
|---|---|
| 1 | export default [async] function XxxContent() |
| 2 | import type for type-only imports |
| 3 | Stay under ~150 lines — split sub-components beyond that |
| 4 | Typed dictionary slice (Dictionary["school"]["grades"]), never any |
| 5 | All UI text from the dictionary — no hardcoded strings, no isArabic ? ternaries |
| 6 | Use lang: Locale, not locale |
| 7 | Server content: extract queries to queries.ts |
| 8 | Client content: fetch via server actions from actions.ts |
Server listing — canonical shape
import type { Locale } from "@/components/internationalization/dictionaries"
import type { Dictionary } from "@/components/internationalization/types"
import { getStudentList } from "./queries"
import { StudentsTable } from "./table"
interface Props {
searchParams: Promise<Record<string, string | string[] | undefined>>
dictionary: Dictionary["school"]["students"]
lang: Locale
}
export default async function StudentsContent({
searchParams,
dictionary,
lang,
}: Props) {
const params = await searchParams
const students = await getStudentList(params)
return <StudentsTable data={students} dictionary={dictionary} lang={lang} />
}Templates
| Template | Reference |
|---|---|
| Server listing | school-dashboard/listings/students/content.tsx |
| Server dashboard | school-dashboard/dashboard/content.tsx |
| Client interactive | school-dashboard/attendance/manual/content.tsx |
| Server composition | school-dashboard/profile/content.tsx |
| Client wizard step | school-marketing/application/personal/content.tsx |
Anti-patterns
- Monolith content — files over 300 lines should split into sub-components.
dictionary: any— type the slice.- Dictionary accepted but ignored.
- Inline Prisma queries — belong in
queries.ts. - Inline
isArabicternaries — use the dictionary slice. dbimports inpage.tsx— pages stay thin.- Wrong
"use client"— only present when client logic is needed. - Inline SVGs — extract to a separate file or icon registry.
- Duplicate type definitions — share via
types.ts. - Sequential
awaitfor permission checks — parallelise withPromise.all. console.logleft in.
Naming
- File:
content.tsx. - Function:
<Feature>ContentPascalCase, default export.
Client boundary
When content.tsx needs interactivity (modals, view toggles, form state), keep content.tsx a pure server component and delegate to a sibling client.tsx that owns the "use client" directive. The server content.tsx fetches data and forwards it to <XxxClient />; the client component composes interactive leaves (form.tsx, table.tsx, detail.tsx). Use this split only when actually needed — a listing that just renders a server-fetched table doesn't need a client boundary.
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import type { Dictionary } from "@/components/internationalization/types"
import { TeacherForm } from "./form"
import { TeachersTable } from "./table"
import type { TeacherListItem } from "./types"
interface Props {
data: TeacherListItem[]
dictionary: Dictionary["school"]["teachers"]
lang: Locale
}
export function TeachersClient({ data, dictionary, lang }: Props) {
const [showForm, setShowForm] = useState(false)
return (
<>
<Button onClick={() => setShowForm(true)}>{dictionary.add}</Button>
<TeachersTable data={data} dictionary={dictionary} lang={lang} />
{showForm && (
<TeacherForm
dictionary={dictionary}
onClose={() => setShowForm(false)}
/>
)}
</>
)
}The matching server content.tsx stays trivial — fetch, then forward.
Boundary roles
| File | Role |
|---|---|
page.tsx | Auth + tenant + render <XxxContent /> |
content.tsx | Server, fetches data, renders <XxxClient /> |
client.tsx | Client, manages state, composes leaves |
form.tsx | Client leaf — RHF + Zod, calls server action |
table.tsx | Client leaf — DataTable wrapper |
actions.ts | Server actions called from leaves |
queries.ts | Server reads called from content.tsx |