- 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
detail.tsx renders a single-entity detail view. Data flows in via props from content.tsx — the detail never fetches itself. Standard layout: error state → header with back button → info card grid → tabs for sub-sections → loading skeleton.
Categories
| Category | Shape | Reference |
|---|---|---|
| School-dashboard full-page | Full-page detail with tabs | listings/announcements/detail.tsx |
| SaaS-dashboard sheet/sidebar | Compact admin overview with quick actions | saas-dashboard/tenants/detail.tsx |
| Server component | No "use client", pure display | exams/results/detail.tsx |
Rules
| # | Rule |
|---|---|
| 1 | Standard props: { data, error, dictionary, lang } |
| 2 | Handle error state first with <Alert variant="destructive"> |
| 3 | Export a Loading skeleton matching the layout |
| 4 | Use <Tabs> for 3+ distinct information groups |
| 5 | Stay under ~300 lines — extract tab content to separate files |
Canonical example
"use client"
import Link from "next/link"
import { ArrowLeft, CircleAlert } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
interface Props {
data: AnnouncementDetailResult | null
error?: string | null
dictionary: Dictionary
lang: Locale
}
export function AnnouncementDetail({ data, error, dictionary, lang }: Props) {
const t = dictionary?.school?.announcements
// 1. Error state first
if (error || !data) {
return (
<div className="space-y-4">
<Button variant="ghost" asChild>
<Link href={`/${lang}/announcements`}>
<ArrowLeft className="me-2 h-4 w-4" />
{t.back}
</Link>
</Button>
<Alert variant="destructive">
<CircleAlert className="h-4 w-4" />
<AlertTitle>{t.errorTitle}</AlertTitle>
<AlertDescription>{error || t.notFound}</AlertDescription>
</Alert>
</div>
)
}
// 2. Header + info grid + body
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" asChild>
<Link href={`/${lang}/announcements`}>
<ArrowLeft className="me-2 h-4 w-4" />
</Link>
</Button>
<div>
<h2>{data.title}</h2>
<Badge variant={data.published ? "default" : "secondary"}>
{data.published ? t.published : t.draft}
</Badge>
</div>
</div>
<Card>
<CardHeader><CardTitle>{t.title}</CardTitle></CardHeader>
<CardContent><div className="prose max-w-none">{data.body}</div></CardContent>
</Card>
</div>
)
}
// 3. Loading skeleton matching layout
export function Loading() {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-48" />
<div className="grid gap-4 md:grid-cols-3">
<Skeleton className="h-32" />
<Skeleton className="h-32" />
<Skeleton className="h-32" />
</div>
<Skeleton className="h-64" />
</div>
)
}Server vs client
| Need | Solution |
|---|---|
| Back navigation | <Link> (server) preferred over router.back() (client) |
| Action buttons | Callbacks from client.tsx |
| Tab state | URL-based tabs (server) or useState (client) |
| Interactive content (video, form) | Extract to client sub-component |
Prefer server component detail. Add "use client" only when interactivity is required.
Standard layout
1. Error state → Alert with back button
2. Header → Back button + title + status badge
3. Info cards → Grid of key metrics
4. Tabs → Sub-sections (overview, history, etc.)
5. Loading → Skeleton matching layoutAnti-patterns
- Monolithic
detail.tsxfiles (e.g.catalog/detail.tsx) — split tabs into separate files. - Missing
Loadingskeleton — consumers shouldn't make up ad-hoc skeletons. - Data fetching inside detail — receive via props from
content.tsx. - Client component just for
router.back()— use<Link>instead.
What belongs where
| Content | In | Not in |
|---|---|---|
| Detail view rendering | detail.tsx | content.tsx |
| Data fetching | content.tsx | detail.tsx |
| Loading skeleton | detail.tsx | page.tsx |
| Tab content (>100 lines) | Separate file (detail-overview.tsx) | detail.tsx |
| Detail result type | types.ts | detail.tsx |
| Error Alert | detail.tsx | content.tsx |