Khác với Storefront (ưu tiên Read), Admin Dashboard ưu tiên Write (Tạo, Sửa, Xóa) và Data Visualization.
1. Layout Composition (Nested Layouts)
Next.js App Router cực mạnh trong việc chia Layout.
Code:
apps/merchant/app/
├── layout.tsx (Root HTML)
├── (dashboard)/
│ ├── layout.tsx (Sidebar + Header + AuthCheck)
│ ├── products/
│ │ ├── page.tsx (Table)
│ │ └── create/page.tsx (Form)
│ └── orders/
└── (auth)/
├── login/page.tsx
└── layout.tsx (Center Layout cho Login)2. Server-Side Data Table
Không dùng Client-side pagination cho bảng dữ liệu lớn. Chúng ta đẩy params lên URL (?page=2&search=iphone).
tsx:
// apps/merchant/app/products/page.tsx
import { DataTable } from "@repo/ui/data-table";
export default async function ProductsPage({ searchParams }) {
const page = Number(searchParams.page) || 1;
const search = searchParams.search || "";
// Gọi DB trực tiếp (vì đây là Server Component)
const products = await db.product.findMany({
skip: (page - 1) * 10,
take: 10,
where: { name: { contains: search } }
});
return (
<DataTable
data={products}
columns={columns}
totalPage={100}
/>
);
}3. Mutations với Server Actions
Thay vì tạo API Route (/api/products), chúng ta dùng Server Actions để gọi function trực tiếp từ Form.
tsx:
// apps/merchant/actions/create-product.ts
"use server";
import { productSchema } from "@repo/config/schema";
import { revalidatePath } from "next/cache";
export async function createProductAction(formData: FormData) {
// 1. Validate Shared Schema
const rawData = Object.fromEntries(formData);
const validated = productSchema.safeParse(rawData);
if (!validated.success) {
return { error: validated.error.flatten() };
}
// 2. Save DB
await db.product.create({ data: validated.data });
// 3. Revalidate & Redirect
revalidatePath("/products");
redirect("/products");
}4. UI Feedback (useFormStatus)
Để hiện loading spinner khi đang submit form mà không cần useState thủ công.
tsx:
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button disabled={pending}>
{pending ? "Đang lưu..." : "Tạo sản phẩm"}
</Button>
);
}