Skip to content

Frontend

Công nghệ

Thành phầnLựa chọnLý do
BundlerVite 6Yêu cầu của dự án; nhanh và có sẵn chế độ build SSR dùng cho prerender
UIReact 18 + React Router 6Router ở chế độ khai báo, dễ dùng chung giữa BrowserRouterStaticRouter
CSSTailwind CSS 4Cấu hình bằng @theme ngay trong CSS, không cần file config riêng
ComponentPrimitive theo phong cách shadcnMã nguồn nằm trong repo (components/ui), sửa được, không phải phụ thuộc phiên bản
Chuyển độngFramer MotionHai helper FadeInStagger, tôn trọng prefers-reduced-motion
Dữ liệuTanStack QueryCần cơ chế dehydrate/hydrate để prerender không phải gọi lại API

Hệ thống thiết kế

Định hướng editorial: nền giấy ngà, tiêu đề serif (Lora), thân chữ sans (Inter), xanh navy làm màu thương hiệu và đỏ son làm điểm nhấn hiếm. Token khai báo trong src/index.css:

css
@theme {
  --color-background: hsl(40 33% 98%);
  --color-foreground: hsl(215 30% 14%);
  --color-primary:    hsl(213 58% 24%);
  --color-accent:     hsl(8 62% 44%);
  /* … */
}

Hai tiện ích riêng: .paper-grid (nền kẻ ô mờ cho dải hero) và .rule-accent (gạch chân nhấn).

Định tuyến

Đường dẫn viết tiếng Việt không dấu, trùng với bề mặt SEO mô tả trong PRD.

Đường dẫnTrang
/Trang chủ
/truong · /truong/:slugDanh sách và hồ sơ trường
/tuyen-sinh · /tuyen-sinh/:slugBảng tuyển sinh và lộ trình từng trường
/de-thi · /de-thi/:slugThư viện đề và chi tiết đề
/ky-thi · /ky-thi/:slugKỳ thi, lịch và chi tiết
/giao-vien · /giao-vien/:slugGiáo viên
/so-sanhSo sánh trường (tối đa 4)
/kham-phaBản đồ hệ thống
/tim-kiemKết quả tìm kiếm (noindex)

Tầng dữ liệu

Mọi hook đều dùng chung dạng khoá [đường_dẫn, tham_số]:

ts
useList<School>('/v1/schools', { province, subject, page, per_page: 24 });
// queryKey: ['/v1/schools', { province: 'ha-noi', page: 1, per_page: 24 }]

Điều này quan trọng vì bước prerender phải prefetch đúng khoá mà client sẽ đọc. JSON.stringify bỏ qua thuộc tính undefined, nên { province: undefined, page: 1 }{ page: 1 } băm ra cùng một khoá — bộ lọc để trống vẫn khớp với dữ liệu đã prerender.

Trạng thái bộ lọc

Bộ lọc nằm trong query string (useSearchParams), không nằm trong state của component. Nhờ vậy mỗi tổ hợp bộ lọc là một URL chia sẻ được, và nút quay lại của trình duyệt hoạt động đúng.

Điểm vào

FileVai trò
src/main.tsxChạy trên trình duyệt. hydrateRoot nếu #root đã có nội dung (trang đã prerender), ngược lại createRoot
src/entry-server.tsxChạy lúc build. Nhận URL + danh sách prefetch, trả về { html, head, state }

Khả năng tiếp cận

  • Mọi ô nhập có aria-label; nút menu có aria-expanded.
  • Vòng focus dùng token --color-ring, không bị tắt ở bất kỳ đâu.
  • prefers-reduced-motion tắt toàn bộ hiệu ứng ở tầng CSS lẫn tầng Framer Motion.
  • Bảng dữ liệu cuộn ngang trong khung riêng, thân trang không bao giờ cuộn ngang.

Tài liệu nội bộ dự án Chuyên Chọn.