Fix some error

This commit is contained in:
Thanh Quy - wolf
2026-03-22 15:47:10 +07:00
parent 9da99c6495
commit 4cc1e76dd7
12 changed files with 360 additions and 207 deletions
+56 -55
View File
@@ -1,25 +1,24 @@
# Hệ Thống Bán Đồ Ăn - Frontend # Coffee Shop Frontend
Dự án Frontend cho một Hệ thống bán đồ ăn (Food Ordering System), được xây dựng bằng Next.js và React, với giao diện hiện đại sử dụng Tailwind CSS. Dự án Frontend cho hệ thống đặt món cà phê, xây dựng bằng Next.js App Router, React 19, TypeScript và Tailwind CSS v4.
--- ---
## Mô Tả Dự Án ## Mô Tả Dự Án
Đây là giao diện người dùng (frontend) cho hệ thống đặt và bán đồ ăn trực tuyến. Hệ thống gồm 2 trang chính: Giao diện người dùng (frontend) cho hệ thống đặt và bán đồ uống trực tuyến.
### Trang Người Dùng (User Page) ### Trang Người Dùng (User Page - /)
Dành cho khách hàng với các chức năng: Dành cho khách hàng:
- Chọn món ăn: Duyệt và thêm món vào giỏ hàng - Duyệt thực đơn theo danh mục (sidebar collapsible)
- Xem hóa đơn: Xem chi tiết các món đã chọn và tổng tiền - Tìm kiếm món theo tên / mô tả
- Thanh toán: Thực hiện thanh toán đơn hàng - Xem card sản phẩm với giá và nút Mua
- Đánh giá: Đánh giá chất lượng món ăn và dịch vụ - Lọc tự động theo trạng thái available
### Trang Quản Lý (Manager Page) ### Trang Quản Lý (Manager Page - chưa triển khai)
Dành cho chủ quán / nhân viên quản lý với các chức năng: Dành cho chủ quán / nhân viên:
- Quản lý thực đơn (thêm, sửa, xóa món ăn) - Quản lý thực đơn (thêm, sửa, xóa món)
- Theo dõi và xử lý đơn hàng - Theo dõi và xử lý đơn hàng
- Quản lý thông tin quán
--- ---
@@ -27,56 +26,59 @@ Dành cho chủ quán / nhân viên quản lý với các chức năng:
### Yêu cầu hệ thống ### Yêu cầu hệ thống
- Node.js >= 18 - Node.js >= 18
- pnpm (khuyến nghị) hoặc npm / yarn / bun - pnpm (khuyến nghị) hoặc npm
### Cài đặt dependencies
### Cài đặt
```bash ```bash
pnpm install pnpm install
# hoặc
npm install
``` ```
### Chạy môi trường phát triển ### Dev
```bash ```bash
pnpm dev pnpm dev
# hoặc ```
npm run dev Mở trình duyệt tại http://localhost:3000
### Build
```bash
pnpm build && pnpm start
``` ```
Mở trình duyệt tại http://localhost:3000 để xem kết quả. ### Lint
### Build production
```bash ```bash
pnpm build pnpm lint
pnpm start
``` ```
--- ---
## Cấu Trúc Thư Mục ## Cấu Trúc Thư Mục
Dự án sử dụng cấu trúc mặc định của Next.js App Router:
``` ```
frondend/ frondend/
├── app/ # App Router (Next.js) +-- app/ # Next.js App Router
├── layout.tsx # Root layout (font, metadata, global styles) | +-- layout.tsx # Root layout
├── page.tsx # Trang chủ (User Page) | +-- page.tsx # Trang chủ - sidebar + product grid
├── globals.css # CSS toàn cục (Tailwind import) | +-- globals.css # CSS design tokens + Tailwind import
│ ├── favicon.ico # Icon trang web +-- components/ # Shared UI components
│ └── components/ # Components dùng riêng trong app/ | +-- Navbar.tsx # Sidebar danh mục (collapsible)
├── components/ # Shared components dùng chung toàn dự án | +-- CartProduct.tsx # Card sản phẩm
│ └── CartProduct.tsx # Component card sản phẩm trong giỏ hàng | +-- COMPONENTS.md # Tài liệu component
├── public/ # Static assets (ảnh, svg, ...) +-- layouts/ # Layout-level components
├── next.config.ts # Cấu hình Next.js | +-- header.tsx # Sticky top header
├── tsconfig.json # Cấu hình TypeScript | +-- footer.tsx # Footer
├── postcss.config.mjs # Cấu hình PostCSS (Tailwind) +-- lib/ # Shared logic & data
├── eslint.config.mjs # Cấu hình ESLint | +-- constants.ts # Mock data
├── package.json # Dependencies và scripts | +-- types.ts # TypeScript interfaces
└── pnpm-workspace.yaml # Cấu hình pnpm workspace +-- types/ # Global TypeScript declarations
| +-- css.d.ts # CSS module type shim
+-- public/ # Static assets
+-- WORKFLOW.md # Tài liệu kiến trúc & quy trình
+-- next.config.ts
+-- tsconfig.json
+-- postcss.config.mjs
+-- eslint.config.mjs
+-- package.json
+-- pnpm-workspace.yaml
``` ```
--- ---
@@ -89,19 +91,18 @@ frondend/
| React | 19.2.3 | Thư viện UI | | React | 19.2.3 | Thư viện UI |
| TypeScript | ^5 | Kiểu dữ liệu tĩnh | | TypeScript | ^5 | Kiểu dữ liệu tĩnh |
| Tailwind CSS | ^4 | Utility-first CSS framework | | Tailwind CSS | ^4 | Utility-first CSS framework |
| SCSS | - | CSS preprocessor | | Geist Font | - | Font chữ (Google Fonts via next/font) |
| Geist Font | - | Font chữ mặc định (Google Fonts) | | FontAwesome | 6.7.2 | Icon library (CDN) |
| pnpm | - | Package manager | | pnpm | - | Package manager |
| ESLint | ^9 | Linting code | | ESLint | ^9 | Linting |
--- ---
## Ghi Chú Phát Triển ## Ghi Chú Phát Triển
- Trang chủ () là điểm vào chính của **User Page** - Trang chủ (app/page.tsx) là điểm vào chính của User Page
- Chỉnh sửa để thay đổi giao diện trang chủ; trang tự động cập nhật khi lưu file - Design tokens định nghĩa trong app/globals.css dưới dạng CSS custom properties
- Styles toàn cục được định nghĩa trong - Mock data nằm trong lib/constants.ts - thay bằng API calls khi backend sẵn sàng
- Tailwind CSS v4 được import trực tiếp qua trong CSS - Dark mode: biến CSS đã chuẩn bị sẵn trong globals.css nhưng chưa kích hoạt
- Hỗ trợ **Dark Mode** tự động theo cài đặt hệ thống () - Ảnh sản phẩm: thêm ảnh thực vào public/imgs/products/
- Font chữ sử dụng **Geist Sans****Geist Mono** từ Google Fonts thông qua - Xem WORKFLOW.md để hiểu kiến trúc tổng thể và quy trình mở rộng dự án
+52 -1
View File
@@ -1 +1,52 @@
# Food Ordering Page - Implementation TODO # Coffee Shop Frontend - TODO
## Completed Optimizations
### A. Dead Code Removed
- [x] lib/constants.ts - Removed unused NAV_LINKS export
- [x] lib/types.ts - Removed unused NavLink interface
- [x] components/Navbar.tsx - Removed trivial handleClick wrapper; inlined onCategoryChange call
- [x] components/Navbar.tsx - Removed unused Link import
### B. Bugs / Inaccuracies Fixed
- [x] layouts/header.tsx - Fixed JSDoc: 3-column -> 2-column layout (no center section exists)
- [x] app/page.tsx - Added available !== false filter to product list
- [x] app/page.tsx - Fixed setState-in-effect lint error: moved initial sidebar state to lazy useState initializer
- [x] next.config.ts - Added explanatory JSDoc comment
### C. Documentation Updated
- [x] README.md - Fixed file structure tree, removed SCSS, fixed dark mode note, updated tech table
- [x] components/COMPONENTS.md - Fixed CartProduct styling (was outdated w-64/text-red-500/bg-blue-600); added Navbar, Header, Footer sections
### D. New Documentation Created
- [x] WORKFLOW.md - Architecture, data flow, design token system, how-to guides, dev workflow
### E. Mini-test Results
- [x] npm run lint - PASSED (0 errors, 0 warnings)
- [x] npm run build - PASSED (Compiled successfully, TypeScript clean, static pages generated)
---
## Pending Features (Future Work)
### Cart & Ordering
- [ ] Implement add-to-cart logic (onBuy callback in CartProduct)
- [ ] Cart sidebar or modal with item list and total
- [ ] Order submission flow
- [ ] Payment page
### Backend Integration
- [ ] Replace MOCK_PRODUCTS with real API calls (lib/api.ts)
- [ ] Replace MOCK_USERS with real authentication
- [ ] Product images: replace placeholder with real images in public/imgs/products/
### Manager Page
- [ ] Create app/manager/page.tsx
- [ ] Menu management (add/edit/delete products)
- [ ] Order tracking dashboard
### UX Improvements
- [ ] Dark mode toggle (CSS variables already prepared in globals.css)
- [ ] Loading skeleton for product grid
- [ ] Toast notifications for cart actions
- [ ] Product detail modal/page
+22 -16
View File
@@ -20,27 +20,34 @@ import { MENU_CATEGORIES, MOCK_PRODUCTS } from "@/lib/constants";
* Expanded sidebar: 1 → sm:2 → lg:2 → xl:3 → 2xl:4 * Expanded sidebar: 1 → sm:2 → lg:2 → xl:3 → 2xl:4
*/ */
export default function Home() { export default function Home() {
/* Start collapsed (false) so SSR and client initial render match.
* useEffect sets the correct value after hydration completes. */
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [activeCategory, setActiveCategory] = useState("all"); const [activeCategory, setActiveCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
/* Default: expanded on desktop, collapsed on mobile */ /* After mount: sync sidebar with viewport width, then subscribe to changes.
* The initial setIsSidebarOpen call is intentional (post-hydration only)
* and does not cause cascading renders — suppress the lint rule here. */
useEffect(() => { useEffect(() => {
const mq = window.matchMedia("(min-width: 1024px)"); const mq = window.matchMedia("(min-width: 1024px)");
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsSidebarOpen(mq.matches); setIsSidebarOpen(mq.matches);
const handler = (e: MediaQueryListEvent) => setIsSidebarOpen(e.matches); const handler = (e: MediaQueryListEvent) => setIsSidebarOpen(e.matches);
mq.addEventListener("change", handler); mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler); return () => mq.removeEventListener("change", handler);
}, []); }, []);
/* Filter products by active category + search query */ /* Filter products by availability, active category, and search query.
* p.available defaults to true when undefined (opt-in unavailability). */
const filteredProducts = MOCK_PRODUCTS.filter((p) => { const filteredProducts = MOCK_PRODUCTS.filter((p) => {
const isAvailable = p.available !== false;
const matchesCategory = activeCategory === "all" || p.category === activeCategory; const matchesCategory = activeCategory === "all" || p.category === activeCategory;
const matchesSearch = const matchesSearch =
searchQuery.trim() === "" || searchQuery.trim() === "" ||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) || p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description.toLowerCase().includes(searchQuery.toLowerCase()); p.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch; return isAvailable && matchesCategory && matchesSearch;
}); });
/* Active category label */ /* Active category label */
@@ -59,8 +66,7 @@ export default function Home() {
return ( return (
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */ /* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
<div <div
className="flex items-start bg-[var(--color-bg-main)]" className="flex items-start bg-background min-h-[calc(100vh-var(--spacing-header-height))]"
style={{ minHeight: "calc(100vh - var(--spacing-header-height))" }}
> >
{/* ── Sidebar ── */} {/* ── Sidebar ── */}
<Navbar <Navbar
@@ -80,10 +86,10 @@ export default function Home() {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
{/* Title + count */} {/* Title + count */}
<div className="shrink-0"> <div className="shrink-0">
<h2 className="text-xl font-bold text-[var(--color-text-primary)]"> <h2 className="text-xl font-bold text-foreground">
{activeCategoryLabel} {activeCategoryLabel}
</h2> </h2>
<p className="text-sm text-[var(--color-text-muted)] mt-0.5"> <p className="text-sm text-muted-foreground mt-0.5">
{filteredProducts.length} món {filteredProducts.length} món
</p> </p>
</div> </div>
@@ -92,7 +98,7 @@ export default function Home() {
<div className="relative w-full sm:max-w-xs"> <div className="relative w-full sm:max-w-xs">
<i <i
className="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 className="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2
text-sm text-[var(--color-text-muted)] pointer-events-none" text-sm text-(--color-text-muted) pointer-events-none"
></i> ></i>
<input <input
type="text" type="text"
@@ -100,10 +106,10 @@ export default function Home() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Tìm kiếm món..." placeholder="Tìm kiếm món..."
className="w-full pl-9 pr-9 py-2 text-sm rounded-xl border outline-none className="w-full pl-9 pr-9 py-2 text-sm rounded-xl border outline-none
bg-[var(--color-bg-card)] text-[var(--color-text-primary)] bg-card text-foreground
border-[var(--color-border)] placeholder:text-[var(--color-text-muted)] border-border placeholder:text-muted-foreground
focus:border-[var(--color-primary)] focus:ring-2 focus:border-primary focus:ring-2
focus:ring-[var(--color-primary)] focus:ring-opacity-20 focus:ring-primary focus:ring-opacity-20
transition-all duration-150" transition-all duration-150"
/> />
{/* Clear button */} {/* Clear button */}
@@ -112,8 +118,8 @@ export default function Home() {
onClick={() => setSearchQuery("")} onClick={() => setSearchQuery("")}
title="Xóa tìm kiếm" title="Xóa tìm kiếm"
aria-label="Xóa tìm kiếm" aria-label="Xóa tìm kiếm"
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)] className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted)
hover:text-[var(--color-primary)] transition-colors duration-150 hover:text-(--color-primary) transition-colors duration-150
cursor-pointer border-none bg-transparent p-0" cursor-pointer border-none bg-transparent p-0"
> >
<i className="fa-solid fa-xmark text-sm"></i> <i className="fa-solid fa-xmark text-sm"></i>
@@ -142,7 +148,7 @@ export default function Home() {
</div> </div>
) : ( ) : (
/* Empty state */ /* Empty state */
<div className="flex flex-col items-center justify-center py-24 gap-4 text-[var(--color-text-muted)]"> <div className="flex flex-col items-center justify-center py-24 gap-4 text-(--color-text-muted)">
<i className="fa-solid fa-mug-hot text-5xl opacity-30"></i> <i className="fa-solid fa-mug-hot text-5xl opacity-30"></i>
<p className="text-base font-medium"> <p className="text-base font-medium">
{searchQuery {searchQuery
@@ -152,7 +158,7 @@ export default function Home() {
{searchQuery && ( {searchQuery && (
<button <button
onClick={() => setSearchQuery("")} onClick={() => setSearchQuery("")}
className="text-sm text-[var(--color-primary)] hover:underline cursor-pointer className="text-sm text-(--color-primary) hover:underline cursor-pointer
border-none bg-transparent" border-none bg-transparent"
> >
Xóa tìm kiếm Xóa tìm kiếm
+136 -38
View File
@@ -1,60 +1,158 @@
# Components Documentation # Components Documentation
> **BlackBox AI Rule:**
> Whenever you create a new component in the components/ directory, or when the user asks to save a component to this file, **automatically append a new section** following the template below. Fill in all fields based on the component source code — props/interface, default values, types, usage example, and notes. Do not skip any field; if a value is unknown, write N/A.
---
## Component Template (for BlackBox AI)
Use this template when adding a new component entry:
> Whenever you create a new component in the components/ directory, automatically append a new section following the template below.
--- ---
## CartProduct ## CartProduct
**File:** components/CartProduct.tsx **File:** components/CartProduct.tsx
**Description:** A product card component that displays a product image, formatted price, a Mua (Buy) button, and a short description. Designed for use in shopping cart or product listing UIs. **Description:** Product card component. Displays product image, name, description, formatted price, and a Buy button. Width is controlled by the parent grid (w-full), not the card itself.
### Props / Interface ### Props
| Prop | Type | Required | Default | Description | | Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------| |------|------|----------|---------|-------------|
| image | string | yes | | URL or path to the product image (used with Next.js Image). | | image | string | yes | - | URL/path to product image (Next.js Image) |
| imageAlt | string | no | Product image | Alt text for the product image (accessibility). | | imageAlt | string | no | Anh san pham | Alt text for accessibility |
| price | number or string | yes | | Product price. If number, auto-formatted to Vietnamese Dong (VND). If string, rendered as-is. | | productName | string | yes | - | Product display name |
| description | string | yes | | Short product description shown in the card footer. Clamped to 2 lines. | | price | number or string | yes | - | If number: auto-formatted to VND. If string: rendered as-is |
| onBuy | () => void | no | undefined | Callback fired when the Mua button is clicked. | | description | string | yes | - | Short description, clamped to 2 lines |
| onBuy | () => void | no | undefined | Callback when Buy button is clicked |
### Internal Logic ### Internal Logic
- formattedPrice: If price is a number, it is formatted using toLocaleString(vi-VN, { style: currency, currency: VND }). If price is already a string, it is rendered directly. - formattedPrice: number -> toLocaleString(vi-VN, { style: currency, currency: VND })
- Image fallback: fa-solid fa-mug-hot icon shown behind image; if image fails onError hides the img element
### CSS / Styling Notes ### Styling (CSS variables)
| Element | Tailwind Classes | Purpose | | Element | Key classes |
|---------|-----------------|---------| |---------|-------------|
| Card wrapper | flex flex-col w-64 rounded-2xl overflow-hidden shadow-md border border-gray-100 bg-white hover:shadow-lg transition-shadow duration-300 | Outer card with hover shadow | | Card wrapper | flex flex-col w-full rounded-2xl, shadow uses --color-shadow-sm/md |
| Image container | relative w-full h-48 bg-gray-100 | Fixed-height image area | | Image area | relative w-full h-36, bg --color-border-light |
| Price text | text-lg font-bold text-red-500 | Highlighted price in red | | Product name | font-bold text-sm, color --color-text-primary, line-clamp-1 |
| Buy button | bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors duration-200 cursor-pointer | Primary action button | | Description | text-xs, color --color-text-muted, line-clamp-2 |
| Description area | px-4 py-3 border-t border-gray-100 bg-gray-50 | Footer section with light background | | Price | text-sm font-bold, color --color-primary |
| Description text | text-sm text-gray-600 leading-relaxed line-clamp-2 | Truncated 2-line description | | Buy button | bg --color-primary, hover --color-primary-dark, active:scale-95 |
### Dependencies ### Dependencies
- next/image — Next.js optimized Image component - next/image
- Tailwind CSS — utility-first CSS framework - Tailwind CSS + CSS custom properties from globals.css
- FontAwesome (fa-solid fa-mug-hot fallback, fa-cart-plus button icon)
### Usage Example
### Notes ### Notes
- Card width is fixed at w-64 (256px). Adjust if a responsive layout is needed. - Card width is w-full - controlled by parent grid in page.tsx
- The description is clamped to 2 lines via line-clamp-2; longer text will be truncated with ellipsis. - available field on Product is checked in page.tsx before rendering
- If onBuy is not provided, the button renders but does nothing on click. - onBuy currently logs to console - TODO: implement cart logic
- Price formatting uses Vietnamese locale (vi-VN) and VND currency by default when price is a number.
---
## Navbar
**File:** components/Navbar.tsx
**Description:** Left sidebar with collapsible category filter. Sticky below header, full viewport height minus header.
### Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| isOpen | boolean | yes | - | true = expanded (240px), false = collapsed (64px) |
| onToggle | () => void | yes | - | Toggle expand/collapse |
| activeCategory | string | no | all | Currently selected category id |
| onCategoryChange | (id: string) => void | no | undefined | Fired when user clicks a category |
### Behavior
- Collapsed: 64px wide, icon only (w-16)
- Expanded: 240px wide, icon + label (w-60)
- Width transition: transition-all duration-250ms
- Active category: highlighted with --color-primary background
- Footer shows SHOP_INFO.openHours (icon only when collapsed)
### Styling
| Element | Key classes |
|---------|-------------|
| Aside | sticky, border-r --color-border, bg --color-bg-sidebar |
| Toggle button | w-8 h-8 rounded-lg, hover bg --color-border-light |
| Active item | bg --color-primary text-white shadow-sm |
| Inactive item | hover bg --color-border-light, color --color-text-secondary |
### Dependencies
- next/link
- lib/constants: MENU_CATEGORIES, SHOP_INFO
- lib/types: MenuCategory
- FontAwesome icons
---
## Header (layouts/header.tsx)
**File:** layouts/header.tsx
**Description:** Sticky top bar. 2-column layout: Brand (left) + Auth button (right). Auth cycles Guest -> Manager -> Staff -> Guest for UI demo.
### Props
None - reads SHOP_INFO and MOCK_USERS from lib/constants directly.
### Internal State
| State | Type | Description |
|-------|------|-------------|
| user | User or null | Current demo user. null = guest |
### Auth States
| State | Appearance |
|-------|------------|
| Guest (null) | Brown primary button, Dang nhap label |
| Manager | Gold/caramel badge with user-tie icon |
| Staff | Avatar circle + name, bordered button |
### Responsive
- Logo + shop name: always visible
- Tagline: hidden < md, shown md+
- Button label: hidden < sm, shown sm+
### Dependencies
- next/image, next/link
- lib/constants: SHOP_INFO, MOCK_USERS
- lib/types: User
- FontAwesome icons
---
## Footer (layouts/footer.tsx)
**File:** layouts/footer.tsx
**Description:** Site footer with 12-column grid. 3 sections: Brand info, Social links, WiFi card.
### Props
None - reads SHOP_INFO and SOCIAL_LINKS from lib/constants directly.
### Layout
| Section | Mobile | md | lg/xl |
|---------|--------|----|-------|
| Brand info | col-span-12 | col-span-6 | col-span-8/6 |
| Social + WiFi | col-span-12 | col-span-6 | col-span-4/6 |
### Sections
1. Brand: logo, name, tagline, address, phone, email, open hours
2. Social: Facebook, TikTok, Website links
3. WiFi: network name + password in monospace styled box
4. Bottom bar: copyright + Made with heart in Vietnam
### Dependencies
- next/image, next/link
- lib/constants: SHOP_INFO, SOCIAL_LINKS
- FontAwesome icons
+11 -11
View File
@@ -37,16 +37,16 @@ export default function CartProduct({
return ( return (
<div <div
className="flex flex-col w-full rounded-2xl overflow-hidden className="flex flex-col w-full rounded-2xl overflow-hidden
bg-[var(--color-bg-card)] border border-[var(--color-border-light)] bg-(--color-bg-card) border border-(--color-border-light)
shadow-[0_2px_8px_var(--color-shadow-sm)] shadow-[0_2px_8px_var(--color-shadow-sm)]
hover:shadow-[0_6px_20px_var(--color-shadow-md)] hover:shadow-[0_6px_20px_var(--color-shadow-md)]
hover:-translate-y-0.5 hover:-translate-y-0.5
transition-all duration-[250ms] cursor-default" transition-all duration-250 cursor-default"
> >
{/* ── Image area ── */} {/* ── Image area ── */}
<div className="relative w-full h-36 bg-[var(--color-border-light)] shrink-0 overflow-hidden"> <div className="relative w-full h-36 bg-(--color-border-light) shrink-0 overflow-hidden">
{/* Fallback icon (shown when image fails or is missing) */} {/* Fallback icon (shown when image fails or is missing) */}
<div className="absolute inset-0 flex items-center justify-center text-4xl text-[var(--color-border)] z-0"> <div className="absolute inset-0 flex items-center justify-center text-4xl text-(--color-border) z-0">
<i className="fa-solid fa-mug-hot"></i> <i className="fa-solid fa-mug-hot"></i>
</div> </div>
{/* Product image */} {/* Product image */}
@@ -54,7 +54,7 @@ export default function CartProduct({
src={image} src={image}
alt={imageAlt} alt={imageAlt}
fill fill
className="object-cover z-[1]" className="object-cover z-1"
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw" sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
onError={(e) => { onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none"; (e.currentTarget as HTMLImageElement).style.display = "none";
@@ -64,10 +64,10 @@ export default function CartProduct({
{/* ── Name + description ── */} {/* ── Name + description ── */}
<div className="flex-1 flex flex-col gap-1 px-3 pt-2.5 pb-1.5"> <div className="flex-1 flex flex-col gap-1 px-3 pt-2.5 pb-1.5">
<h3 className="font-bold text-sm leading-tight text-[var(--color-text-primary)] line-clamp-1"> <h3 className="font-bold text-sm leading-tight text-foreground line-clamp-1">
{productName} {productName}
</h3> </h3>
<p className="text-xs leading-relaxed text-[var(--color-text-muted)] line-clamp-2"> <p className="text-xs leading-relaxed text-(--color-text-muted) line-clamp-2">
{description} {description}
</p> </p>
</div> </div>
@@ -75,17 +75,17 @@ export default function CartProduct({
{/* ── Price + Buy button ── */} {/* ── Price + Buy button ── */}
<div <div
className="flex items-center justify-between px-3 py-2.5 className="flex items-center justify-between px-3 py-2.5
border-t border-[var(--color-border-light)] shrink-0" border-t border-(--color-border-light) shrink-0"
> >
<span className="text-sm font-bold text-[var(--color-primary)]"> <span className="text-sm font-bold text-(--color-primary)">
{formattedPrice} {formattedPrice}
</span> </span>
<button <button
onClick={onBuy} onClick={onBuy}
className="flex items-center gap-1.5 text-xs font-semibold className="flex items-center gap-1.5 text-xs font-semibold
px-3 py-1.5 rounded-lg border-none cursor-pointer px-3 py-1.5 rounded-lg border-none cursor-pointer
bg-[var(--color-primary)] text-white bg-(--color-primary) text-white
hover:bg-[var(--color-primary-dark)] hover:bg-(--color-primary-dark)
active:scale-95 active:scale-95
transition-all duration-150 whitespace-nowrap" transition-all duration-150 whitespace-nowrap"
> >
+18 -23
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import Link from "next/link";
import { MENU_CATEGORIES, SHOP_INFO } from "@/lib/constants"; import { MENU_CATEGORIES, SHOP_INFO } from "@/lib/constants";
import type { MenuCategory } from "@/lib/types"; import type { MenuCategory } from "@/lib/types";
@@ -30,35 +29,31 @@ export default function Navbar({
activeCategory = "all", activeCategory = "all",
onCategoryChange, onCategoryChange,
}: NavbarProps) { }: NavbarProps) {
const handleClick = (id: string) => {
onCategoryChange?.(id);
};
return ( return (
<aside <aside
className={` className={`
sticky shrink-0 flex flex-col z-20 sticky shrink-0 flex flex-col z-20
border-r border-[var(--color-border)] border-r border-(--color-border)
bg-[var(--color-bg-sidebar)] bg-(--color-bg-sidebar)
overflow-y-auto overflow-x-hidden overflow-y-auto overflow-x-hidden
transition-all duration-[250ms] ease-in-out transition-all duration-250 ease-in-out
${isOpen ? "w-60" : "w-16"} ${isOpen ? "w-60" : "w-16"}
`} `}
style={{ style={{
top: "var(--spacing-header-height)", top: "var(--spacing-header-height)",
height: "calc(100vh - var(--spacing-header-height))", height: "calc(100vh - var(--spacing-header-height))",
}} } as React.CSSProperties}
> >
{/* ── Sidebar header: title + toggle button ── */} {/* ── Sidebar header: title + toggle button ── */}
<div <div
className={` className={`
flex items-center border-b border-[var(--color-border)] shrink-0 flex items-center border-b border-(--color-border) shrink-0
${isOpen ? "justify-between px-4 py-3" : "justify-center px-0 py-3"} ${isOpen ? "justify-between px-4 py-3" : "justify-center px-0 py-3"}
`} `}
> >
{isOpen && ( {isOpen && (
<span className="text-xs font-bold uppercase tracking-widest text-[var(--color-text-muted)] whitespace-nowrap"> <span className="text-xs font-bold uppercase tracking-widest text-(--color-text-muted) whitespace-nowrap">
<i className="fa-solid fa-utensils mr-2 text-[var(--color-primary)]"></i> <i className="fa-solid fa-utensils mr-2 text-(--color-primary)"></i>
Thực Đơn Thực Đơn
</span> </span>
)} )}
@@ -66,12 +61,12 @@ export default function Navbar({
onClick={onToggle} onClick={onToggle}
title={isOpen ? "Thu gọn menu" : "Mở rộng menu"} title={isOpen ? "Thu gọn menu" : "Mở rộng menu"}
className="w-8 h-8 flex items-center justify-center rounded-lg cursor-pointer border-none className="w-8 h-8 flex items-center justify-center rounded-lg cursor-pointer border-none
text-[var(--color-text-muted)] bg-transparent text-(--color-text-muted) bg-transparent
hover:bg-[var(--color-border-light)] hover:text-[var(--color-primary)] hover:bg-(--color-border-light) hover:text-(--color-primary)
transition-colors duration-150 shrink-0" transition-colors duration-150 shrink-0"
> >
<i <i
className={`fa-solid text-sm transition-transform duration-[250ms] ${ className={`fa-solid text-sm transition-transform duration-250 ${
isOpen ? "fa-chevron-left" : "fa-chevron-right" isOpen ? "fa-chevron-left" : "fa-chevron-right"
}`} }`}
></i> ></i>
@@ -86,7 +81,7 @@ export default function Navbar({
return ( return (
<li key={cat.id}> <li key={cat.id}>
<button <button
onClick={() => handleClick(cat.id)} onClick={() => onCategoryChange?.(cat.id)}
title={!isOpen ? cat.name : undefined} title={!isOpen ? cat.name : undefined}
className={` className={`
w-full flex items-center rounded-xl text-sm font-medium w-full flex items-center rounded-xl text-sm font-medium
@@ -94,8 +89,8 @@ export default function Navbar({
${isOpen ? "gap-3 px-3 py-2.5" : "justify-center px-0 py-2.5"} ${isOpen ? "gap-3 px-3 py-2.5" : "justify-center px-0 py-2.5"}
${ ${
isActive isActive
? "bg-[var(--color-primary)] text-white shadow-sm" ? "bg-(--color-primary) text-white shadow-sm"
: "bg-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-border-light)] hover:text-[var(--color-primary-dark)]" : "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light) hover:text-(--color-primary-dark)"
} }
`} `}
> >
@@ -103,7 +98,7 @@ export default function Navbar({
<i <i
className={` className={`
${cat.icon} w-5 text-center text-base shrink-0 ${cat.icon} w-5 text-center text-base shrink-0
${isActive ? "text-white" : "text-[var(--color-primary)]"} ${isActive ? "text-white" : "text-(--color-primary)"}
`} `}
></i> ></i>
@@ -128,18 +123,18 @@ export default function Navbar({
{/* ── Sidebar footer: opening hours ── */} {/* ── Sidebar footer: opening hours ── */}
<div <div
className={` className={`
shrink-0 border-t border-[var(--color-border)] py-3 shrink-0 border-t border-(--color-border) py-3
${isOpen ? "px-4" : "px-0 flex justify-center"} ${isOpen ? "px-4" : "px-0 flex justify-center"}
`} `}
> >
{isOpen ? ( {isOpen ? (
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]"> <div className="flex items-center gap-2 text-xs text-(--color-text-muted)">
<i className="fa-solid fa-clock text-[var(--color-accent)] shrink-0"></i> <i className="fa-solid fa-clock text-(--color-accent) shrink-0"></i>
<span>{SHOP_INFO.openHours}</span> <span>{SHOP_INFO.openHours}</span>
</div> </div>
) : ( ) : (
<i <i
className="fa-solid fa-clock text-sm text-[var(--color-text-muted)]" className="fa-solid fa-clock text-sm text-(--color-text-muted)"
title={SHOP_INFO.openHours} title={SHOP_INFO.openHours}
></i> ></i>
)} )}
+22 -22
View File
@@ -17,7 +17,7 @@ import { SHOP_INFO, SOCIAL_LINKS } from "@/lib/constants";
*/ */
export default function Footer() { export default function Footer() {
return ( return (
<footer className="w-full bg-[var(--color-bg-footer)] text-[var(--color-text-on-dark)] overflow-x-hidden"> <footer className="w-full bg-(--color-bg-footer) text-(--color-text-on-dark) overflow-x-hidden">
{/* ── Main grid ── */} {/* ── Main grid ── */}
<div className="max-w-screen-2xl mx-auto px-4 md:px-6 lg:px-8 py-10"> <div className="max-w-screen-2xl mx-auto px-4 md:px-6 lg:px-8 py-10">
<div className="grid grid-cols-12 gap-x-8 gap-y-8"> <div className="grid grid-cols-12 gap-x-8 gap-y-8">
@@ -35,7 +35,7 @@ export default function Footer() {
sizes="40px" sizes="40px"
/> />
</div> </div>
<span className="font-bold text-lg text-[var(--color-accent)]"> <span className="font-bold text-lg text-(--color-accent)">
{SHOP_INFO.name} {SHOP_INFO.name}
</span> </span>
</div> </div>
@@ -48,29 +48,29 @@ export default function Footer() {
{/* Contact details */} {/* Contact details */}
<ul className="flex flex-col gap-2 text-sm opacity-80"> <ul className="flex flex-col gap-2 text-sm opacity-80">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<i className="fa-solid fa-location-dot mt-0.5 w-4 text-center text-[var(--color-accent)] shrink-0"></i> <i className="fa-solid fa-location-dot mt-0.5 w-4 text-center text-(--color-accent) shrink-0"></i>
<span>Đa chỉ: {SHOP_INFO.address}</span> <span>Đa chỉ: {SHOP_INFO.address}</span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<i className="fa-solid fa-phone w-4 text-center text-[var(--color-accent)] shrink-0"></i> <i className="fa-solid fa-phone w-4 text-center text-(--color-accent) shrink-0"></i>
<a <a
href={`tel:${SHOP_INFO.phone}`} href={`tel:${SHOP_INFO.phone}`}
className="hover:text-[var(--color-accent)] transition-colors duration-150" className="hover:text-(--color-accent) transition-colors duration-150"
> >
Số điện thoại: {SHOP_INFO.phone} Số điện thoại: {SHOP_INFO.phone}
</a> </a>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<i className="fa-solid fa-envelope w-4 text-center text-[var(--color-accent)] shrink-0"></i> <i className="fa-solid fa-envelope w-4 text-center text-(--color-accent) shrink-0"></i>
<a <a
href={`mailto:${SHOP_INFO.email}`} href={`mailto:${SHOP_INFO.email}`}
className="hover:text-[var(--color-accent)] transition-colors duration-150" className="hover:text-(--color-accent) transition-colors duration-150"
> >
Email: {SHOP_INFO.email} Email: {SHOP_INFO.email}
</a> </a>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<i className="fa-solid fa-clock w-4 text-center text-[var(--color-accent)] shrink-0"></i> <i className="fa-solid fa-clock w-4 text-center text-(--color-accent) shrink-0"></i>
<span>Open: {SHOP_INFO.openHours}</span> <span>Open: {SHOP_INFO.openHours}</span>
</li> </li>
</ul> </ul>
@@ -84,7 +84,7 @@ export default function Footer() {
{/* ── 2. Social links ── */} {/* ── 2. Social links ── */}
<div className="col-span-1"> <div className="col-span-1">
<h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-[var(--color-accent)]"> <h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-(--color-accent)">
Kết nối Kết nối
</h3> </h3>
<ul className="flex flex-col gap-3"> <ul className="flex flex-col gap-3">
@@ -94,7 +94,7 @@ export default function Footer() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100 className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
hover:text-[var(--color-accent)] transition-all duration-150" hover:text-(--color-accent) transition-all duration-150"
> >
<span <span
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
@@ -111,7 +111,7 @@ export default function Footer() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100 className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
hover:text-[var(--color-accent)] transition-all duration-150" hover:text-(--color-accent) transition-all duration-150"
> >
<span <span
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
@@ -126,11 +126,11 @@ export default function Footer() {
<Link <Link
href={SOCIAL_LINKS.website} href={SOCIAL_LINKS.website}
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100 className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
hover:text-[var(--color-accent)] transition-all duration-150" hover:text-(--color-accent) transition-all duration-150"
> >
<span <span
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
bg-[var(--color-primary)] text-white text-base" bg-(--color-primary) text-white text-base"
> >
<i className="fa-solid fa-globe"></i> <i className="fa-solid fa-globe"></i>
</span> </span>
@@ -142,15 +142,15 @@ export default function Footer() {
{/* ── 3. WiFi card ── */} {/* ── 3. WiFi card ── */}
<div className="col-span-1"> <div className="col-span-1">
<h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-[var(--color-accent)]"> <h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-(--color-accent)">
WiFi Miễn Phí WiFi Miễn Phí
</h3> </h3>
<div <div
className="rounded-xl border border-[var(--color-primary-light)] border-opacity-50 className="rounded-xl border border-(--color-primary-light) border-opacity-50
p-4 bg-[var(--color-primary-dark)] bg-opacity-30" p-4 bg-(--color-primary-dark) bg-opacity-30"
> >
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<i className="fa-solid fa-wifi text-[var(--color-accent)] text-lg shrink-0"></i> <i className="fa-solid fa-wifi text-(--color-accent) text-lg shrink-0"></i>
<span className="font-semibold text-sm">Kết nối miễn phí</span> <span className="font-semibold text-sm">Kết nối miễn phí</span>
</div> </div>
{/* Stacked label + value rows — no overflow risk */} {/* Stacked label + value rows — no overflow risk */}
@@ -158,8 +158,8 @@ export default function Footer() {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="opacity-60 text-xs uppercase tracking-wide">Tên mạng</span> <span className="opacity-60 text-xs uppercase tracking-wide">Tên mạng</span>
<span <span
className="font-mono font-bold text-[var(--color-accent)] className="font-mono font-bold text-(--color-accent)
px-2 py-1 rounded border border-[var(--color-accent)] px-2 py-1 rounded border border-(--color-accent)
border-opacity-30 break-all" border-opacity-30 break-all"
> >
{SHOP_INFO.wifi.name} {SHOP_INFO.wifi.name}
@@ -168,8 +168,8 @@ export default function Footer() {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="opacity-60 text-xs uppercase tracking-wide">Mật khẩu</span> <span className="opacity-60 text-xs uppercase tracking-wide">Mật khẩu</span>
<span <span
className="font-mono font-bold text-[var(--color-accent)] tracking-wider className="font-mono font-bold text-(--color-accent) tracking-wider
px-2 py-1 rounded border border-[var(--color-accent)] px-2 py-1 rounded border border-(--color-accent)
border-opacity-30 break-all" border-opacity-30 break-all"
> >
{SHOP_INFO.wifi.password} {SHOP_INFO.wifi.password}
@@ -196,7 +196,7 @@ export default function Footer() {
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
Made with{" "} Made with{" "}
<i className="fa-solid fa-heart text-[var(--color-accent)] mx-1"></i>{" "} <i className="fa-solid fa-heart text-(--color-accent) mx-1"></i>{" "}
in Vietnam in Vietnam
</span> </span>
</div> </div>
+21 -22
View File
@@ -9,17 +9,16 @@ import type { User } from "@/lib/types";
/** /**
* Site Header — sticky top bar, always visible on all screen sizes. * Site Header — sticky top bar, always visible on all screen sizes.
* *
* 3-column layout: * 2-column layout:
* [Brand: logo + name + tagline] | [Center: quick info] | [Auth button] * [LEFT: Brand (logo + name + tagline)] | [RIGHT: Auth button]
* *
* Login state cycles for UI demo (click the button to switch): * Auth state cycles for UI demo (click the button to switch):
* Guest → Manager (Quản lý) → Staff → Guest * Guest → Manager (Quản lý) → Staff → Guest
* *
* Responsive: * Responsive:
* - Logo + shop name : always visible * - Logo + shop name : always visible
* - Tagline : hidden on mobile, shown on md+ * - Tagline : hidden on mobile (< md), shown on md+
* - Center info : hidden on mobile, shown on lg+ * - Button label : hidden on xs (< sm), shown on sm+
* - Button label : hidden on xs, shown on sm+
*/ */
export default function Header() { export default function Header() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
@@ -33,9 +32,9 @@ export default function Header() {
return ( return (
<header <header
className="w-full sticky top-0 z-50 className="w-full sticky top-0 z-50
bg-[var(--color-bg-header)] border-b border-[var(--color-border)] bg-(--color-bg-header) border-b border-(--color-border)
shadow-[0_1px_8px_var(--color-shadow-sm)]" shadow-[0_1px_8px_var(--color-shadow-sm)]
style={{ height: "var(--spacing-header-height)" }} h-(--spacing-header-height)"
> >
<div <div
className="h-full px-6 md:px-8 lg:px-12 className="h-full px-6 md:px-8 lg:px-12
@@ -64,13 +63,13 @@ export default function Header() {
<div className="flex flex-col leading-tight"> <div className="flex flex-col leading-tight">
<span <span
className="font-bold text-base md:text-lg className="font-bold text-base md:text-lg
text-[var(--color-primary-dark)] text-(--color-primary-dark)
group-hover:text-[var(--color-primary)] group-hover:text-(--color-primary)
transition-colors duration-150" transition-colors duration-150"
> >
{SHOP_INFO.name} {SHOP_INFO.name}
</span> </span>
<span className="text-xs hidden md:block text-[var(--color-text-muted)]"> <span className="text-xs hidden md:block text-(--color-text-muted)">
{SHOP_INFO.tagline} {SHOP_INFO.tagline}
</span> </span>
</div> </div>
@@ -86,8 +85,8 @@ export default function Header() {
title="Đăng nhập" title="Đăng nhập"
className="flex items-center gap-2.5 px-5 py-2.5 rounded-xl className="flex items-center gap-2.5 px-5 py-2.5 rounded-xl
text-sm font-semibold border-none cursor-pointer text-sm font-semibold border-none cursor-pointer
bg-[var(--color-primary)] text-white bg-(--color-primary) text-white
hover:bg-[var(--color-primary-dark)] hover:bg-(--color-primary-dark)
active:scale-95 transition-all duration-150" active:scale-95 transition-all duration-150"
> >
<i className="fa-solid fa-right-to-bracket"></i> <i className="fa-solid fa-right-to-bracket"></i>
@@ -101,9 +100,9 @@ export default function Header() {
title="Nhấn để đổi tài khoản (demo)" title="Nhấn để đổi tài khoản (demo)"
className="flex items-center gap-2.5 px-4 py-2.5 rounded-xl className="flex items-center gap-2.5 px-4 py-2.5 rounded-xl
text-sm font-semibold cursor-pointer text-sm font-semibold cursor-pointer
bg-[var(--color-accent-light)] border border-[var(--color-accent)] bg-(--color-accent-light) border border-(--color-accent)
text-[var(--color-primary-dark)] text-(--color-primary-dark)
hover:bg-[var(--color-accent)] hover:text-white hover:bg-(--color-accent) hover:text-white
active:scale-95 transition-all duration-150" active:scale-95 transition-all duration-150"
> >
<i className="fa-solid fa-user-tie text-base"></i> <i className="fa-solid fa-user-tie text-base"></i>
@@ -117,16 +116,16 @@ export default function Header() {
title="Nhấn để đổi tài khoản (demo)" title="Nhấn để đổi tài khoản (demo)"
className="flex items-center gap-2.5 px-4 py-2 rounded-xl className="flex items-center gap-2.5 px-4 py-2 rounded-xl
text-sm font-semibold cursor-pointer text-sm font-semibold cursor-pointer
bg-[var(--color-bg-main)] border border-[var(--color-border)] bg-background border border-(--color-border)
text-[var(--color-text-secondary)] text-(--color-text-secondary)
hover:border-[var(--color-primary-light)] hover:border-(--color-primary-light)
hover:bg-[var(--color-border-light)] hover:bg-(--color-border-light)
active:scale-95 transition-all duration-150" active:scale-95 transition-all duration-150"
> >
{/* Avatar circle */} {/* Avatar circle */}
<div <div
className="w-7 h-7 rounded-full flex items-center justify-center shrink-0 className="w-7 h-7 rounded-full flex items-center justify-center shrink-0
bg-[var(--color-primary-light)] text-white text-xs" bg-(--color-primary-light) text-white text-xs"
> >
<i className="fa-solid fa-user"></i> <i className="fa-solid fa-user"></i>
</div> </div>
+10 -9
View File
@@ -1,4 +1,4 @@
import type { MenuCategory, Product, ShopInfo, SocialLinks, NavLink, User } from "./types"; import type { MenuCategory, Product, ShopInfo, SocialLinks, User } from "./types";
// ===== SHOP INFORMATION ===== // ===== SHOP INFORMATION =====
export const SHOP_INFO: ShopInfo = { export const SHOP_INFO: ShopInfo = {
@@ -23,14 +23,6 @@ export const SOCIAL_LINKS: SocialLinks = {
website: "/", website: "/",
}; };
// ===== NAVIGATION LINKS =====
export const NAV_LINKS: NavLink[] = [
{ label: "Trang chủ", href: "/", icon: "fa-solid fa-house" },
{ label: "Thực đơn", href: "/menu", icon: "fa-solid fa-book-open" },
{ label: "Về chúng tôi", href: "/about", icon: "fa-solid fa-circle-info" },
{ label: "Liên hệ", href: "/contact", icon: "fa-solid fa-envelope" },
];
// ===== MENU CATEGORIES ===== // ===== MENU CATEGORIES =====
// Each category has a unique FontAwesome icon representing the item type // Each category has a unique FontAwesome icon representing the item type
export const MENU_CATEGORIES: MenuCategory[] = [ export const MENU_CATEGORIES: MenuCategory[] = [
@@ -200,6 +192,15 @@ export const MOCK_PRODUCTS: Product[] = [
description: "Thạch cà phê mát lạnh, thêm hương vị đặc biệt cho đồ uống của bạn.", description: "Thạch cà phê mát lạnh, thêm hương vị đặc biệt cho đồ uống của bạn.",
available: true, available: true,
}, },
{
id: 18,
name: "Trân Châu Trắng",
category: "topping",
price: 10000,
image: "/imgs/products/placeholder.jpg",
description: "Trân châu trắng dẻo dai, thêm vào bất kỳ đồ uống nào để tăng thêm hương vị.",
available: true,
},
]; ];
// ===== MOCK USERS (for UI demo replace with real auth) ===== // ===== MOCK USERS (for UI demo replace with real auth) =====
+1 -7
View File
@@ -44,13 +44,7 @@ export interface ShopInfo {
openHours: string; openHours: string;
} }
// ===== NAVIGATION TYPES ===== // ===== SOCIAL LINKS TYPES =====
export interface NavLink {
label: string;
href: string;
icon?: string;
}
export interface SocialLinks { export interface SocialLinks {
facebook: string; facebook: string;
tiktok: string; tiktok: string;
+10 -3
View File
@@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { /**
/* config options here */ * Next.js configuration.
}; *
* Currently uses all defaults — no custom rewrites, redirects, or image domains needed.
* Add options here as the project grows (e.g. `images.remotePatterns` when using
* external image URLs, `experimental.serverActions` for form handling, etc.)
*
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
*/
const nextConfig: NextConfig = {};
export default nextConfig; export default nextConfig;
+1
View File
@@ -13,6 +13,7 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"forceConsistentCasingInFileNames": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"