Mục lục

Admin Dashboard: Server Actions & Complex Forms

Xây dựng trang quản trị (Merchant Portal) với layout phức tạp, form validation (Zod) và xử lý dữ liệu bảng (Data Table) phía Server.

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>
  );
}
Quảng cáo
mdhorizontal