Giao diện
Frontend
Công nghệ
| Thành phần | Lựa chọn | Lý do |
|---|---|---|
| Bundler | Vite 6 | Yêu cầu của dự án; nhanh và có sẵn chế độ build SSR dùng cho prerender |
| UI | React 18 + React Router 6 | Router ở chế độ khai báo, dễ dùng chung giữa BrowserRouter và StaticRouter |
| CSS | Tailwind CSS 4 | Cấu hình bằng @theme ngay trong CSS, không cần file config riêng |
| Component | Primitive theo phong cách shadcn | Mã nguồn nằm trong repo (components/ui), sửa được, không phải phụ thuộc phiên bản |
| Chuyển động | Framer Motion | Hai helper FadeIn và Stagger, tôn trọng prefers-reduced-motion |
| Dữ liệu | TanStack Query | Cầ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ẫn | Trang |
|---|---|
/ | Trang chủ |
/truong · /truong/:slug | Danh sách và hồ sơ trường |
/tuyen-sinh · /tuyen-sinh/:slug | Bảng tuyển sinh và lộ trình từng trường |
/de-thi · /de-thi/:slug | Thư viện đề và chi tiết đề |
/ky-thi · /ky-thi/:slug | Kỳ thi, lịch và chi tiết |
/giao-vien · /giao-vien/:slug | Giáo viên |
/so-sanh | So sánh trường (tối đa 4) |
/kham-pha | Bản đồ hệ thống |
/tim-kiem | Kế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 } và { 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
| File | Vai trò |
|---|---|
src/main.tsx | Chạ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.tsx | Chạ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-motiontắ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.