| name | add-page |
| description | Use when adding a new page or route — choosing the correct route group (kiosk/dashboard/auth), structuring the page file, adding loading.tsx and error.tsx siblings, and following responsive layout rules. |
Add a New Page
Route group decision tree
Is this for shop-floor workers on mobile? → (kiosk)
Is this for managers on desktop/tablet? → (dashboard)
Is this an auth screen (login/PIN)? → (auth)
Route groups don't appear in the URL:
src/app/(kiosk)/my-page/page.tsx → URL: /my-page
src/app/(dashboard)/my-page/page.tsx → URL: /my-page
src/app/(auth)/my-page/page.tsx → URL: /my-page
Minimum file set for every new page
src/app/(group)/my-page/
├── page.tsx ← required — the page itself
├── loading.tsx ← required — Suspense skeleton shown while page loads
└── error.tsx ← recommended — recoverable per-route error boundary
Page template — server component (default)
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Tên màn hình' }
export default function MyPage() {
return (
<div className="space-y-4 p-4">
<h1 className="text-xl font-bold">Tên màn hình</h1>
{/* page content */}
</div>
)
}
Rules for server components (default):
- Export
metadata — always include title
- No
'use client' at the top unless you need state/effects
- Data fetching happens in async sub-components or client components via TanStack Query
Page template — client component (when state/hooks needed)
'use client'
import type { Metadata } from 'next'
import { useMyEntities } from '@/lib/hooks/use-my-domain'
export default function MyClientPage() {
const { data, isLoading } = useMyEntities()
}
If you need both metadata AND client hooks, split into a server wrapper + client component:
import type { Metadata } from 'next'
import { MyPageContent } from './_components/my-page-content'
export const metadata: Metadata = { title: 'My Page' }
export default function MyPage() {
return <MyPageContent />
}
loading.tsx template
import { Skeleton } from '@/components/ui/skeleton'
import { Card, CardContent } from '@/components/ui/card'
export default function MyPageLoading() {
return (
<div className="space-y-4 p-4">
<Skeleton className="h-7 w-40" /> {/* page title */}
{[1, 2, 3].map((i) => (
<Card key={i}>
<CardContent className="p-4">
<Skeleton className="mb-2 h-5 w-32" />
<Skeleton className="h-4 w-48" />
</CardContent>
</Card>
))}
</div>
)
}
Mirror the visual structure of the actual page — same number of "cards", similar heights.
error.tsx template
'use client'
import { useEffect } from 'react'
import { Button } from '@/components/ui/button'
export default function MyPageError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className="flex min-h-[40vh] flex-col items-center justify-center gap-4 p-4 text-center">
<p className="font-medium">Có lỗi xảy ra</p>
<p className="text-sm text-muted-foreground">{error.message}</p>
<Button onClick={reset}>Thử lại</Button>
</div>
)
}
Layout rules by route group
(kiosk) pages — mobile-first
<div className="space-y-4 p-4">
<h1 className="text-xl font-bold">Tiêu đề</h1>
{}
<Card>
<CardContent className="flex items-center justify-between p-4">
{/* content */}
</CardContent>
</Card>
{}
<BigButton>Hành động chính</BigButton>
</div>
Kiosk checklist:
(dashboard) pages — desktop/tablet
<div className="space-y-6">
<h1 className="text-2xl font-bold">Tiêu đề</h1>
{}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Metric" value="123" icon={SomeIcon} />
</div>
{}
<AlertBanner status="GREEN" utilizationPct={8} message="..." />
{}
<div className="rounded-xl border bg-card shadow-sm">
<Table>...</Table>
</div>
</div>
Dashboard checklist:
Responsive breakpoints
| Breakpoint | Width | Context |
|---|
| base | 375 px | Mobile kiosk (shop floor) |
sm: | 640 px | — |
md: | 768 px | Tablet |
lg: | 1024 px | — |
xl: | 1280 px | Desktop dashboard |
Always design base (mobile) first, then add md: / lg: overrides.
Internal links
Always use next/link for navigation — never <a href> for internal routes:
import Link from 'next/link'
<Link href="/cutting-orders" className="...">Lệnh cắt</Link>
For programmatic navigation in client components:
'use client'
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/cutting-orders')