Fix some error
This commit is contained in:
@@ -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
|
||||
|
||||
Đâ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)
|
||||
Dành cho khách hàng với các chức năng:
|
||||
- Chọn món ăn: Duyệt và thêm món vào giỏ hàng
|
||||
- Xem hóa đơn: Xem chi tiết các món đã chọn và tổng tiền
|
||||
- Thanh toán: Thực hiện thanh toán đơn hàng
|
||||
- Đánh giá: Đánh giá chất lượng món ăn và dịch vụ
|
||||
### Trang Người Dùng (User Page - /)
|
||||
Dành cho khách hàng:
|
||||
- Duyệt thực đơn theo danh mục (sidebar collapsible)
|
||||
- Tìm kiếm món theo tên / mô tả
|
||||
- Xem card sản phẩm với giá và nút Mua
|
||||
- Lọc tự động theo trạng thái available
|
||||
|
||||
### Trang Quản Lý (Manager Page)
|
||||
Dành cho chủ quán / nhân viên quản lý với các chức năng:
|
||||
- Quản lý thực đơn (thêm, sửa, xóa món ăn)
|
||||
### Trang Quản Lý (Manager Page - chưa triển khai)
|
||||
Dành cho chủ quán / nhân viên:
|
||||
- Quản lý thực đơn (thêm, sửa, xóa món)
|
||||
- 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
|
||||
- Node.js >= 18
|
||||
- pnpm (khuyến nghị) hoặc npm / yarn / bun
|
||||
|
||||
### Cài đặt dependencies
|
||||
- pnpm (khuyến nghị) hoặc npm
|
||||
|
||||
### Cài đặt
|
||||
```bash
|
||||
pnpm install
|
||||
# hoặc
|
||||
npm install
|
||||
```
|
||||
|
||||
### Chạy môi trường phát triển
|
||||
|
||||
### Dev
|
||||
```bash
|
||||
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ả.
|
||||
|
||||
### Build production
|
||||
|
||||
### Lint
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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/
|
||||
├── app/ # App Router (Next.js)
|
||||
│ ├── layout.tsx # Root layout (font, metadata, global styles)
|
||||
│ ├── page.tsx # Trang chủ (User Page)
|
||||
│ ├── globals.css # CSS toàn cục (Tailwind import)
|
||||
│ ├── favicon.ico # Icon trang web
|
||||
│ └── components/ # Components dùng riêng trong app/
|
||||
├── components/ # Shared components dùng chung toàn dự án
|
||||
│ └── CartProduct.tsx # Component card sản phẩm trong giỏ hàng
|
||||
├── public/ # Static assets (ảnh, svg, ...)
|
||||
├── next.config.ts # Cấu hình Next.js
|
||||
├── tsconfig.json # Cấu hình TypeScript
|
||||
├── postcss.config.mjs # Cấu hình PostCSS (Tailwind)
|
||||
├── eslint.config.mjs # Cấu hình ESLint
|
||||
├── package.json # Dependencies và scripts
|
||||
└── pnpm-workspace.yaml # Cấu hình pnpm workspace
|
||||
+-- app/ # Next.js App Router
|
||||
| +-- layout.tsx # Root layout
|
||||
| +-- page.tsx # Trang chủ - sidebar + product grid
|
||||
| +-- globals.css # CSS design tokens + Tailwind import
|
||||
+-- components/ # Shared UI components
|
||||
| +-- Navbar.tsx # Sidebar danh mục (collapsible)
|
||||
| +-- CartProduct.tsx # Card sản phẩm
|
||||
| +-- COMPONENTS.md # Tài liệu component
|
||||
+-- layouts/ # Layout-level components
|
||||
| +-- header.tsx # Sticky top header
|
||||
| +-- footer.tsx # Footer
|
||||
+-- lib/ # Shared logic & data
|
||||
| +-- constants.ts # Mock data
|
||||
| +-- types.ts # TypeScript interfaces
|
||||
+-- 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 |
|
||||
| TypeScript | ^5 | Kiểu dữ liệu tĩnh |
|
||||
| Tailwind CSS | ^4 | Utility-first CSS framework |
|
||||
| SCSS | - | CSS preprocessor |
|
||||
| Geist Font | - | Font chữ mặc định (Google Fonts) |
|
||||
| Geist Font | - | Font chữ (Google Fonts via next/font) |
|
||||
| FontAwesome | 6.7.2 | Icon library (CDN) |
|
||||
| pnpm | - | Package manager |
|
||||
| ESLint | ^9 | Linting code |
|
||||
|
||||
| ESLint | ^9 | Linting |
|
||||
|
||||
---
|
||||
|
||||
## Ghi Chú Phát Triển
|
||||
|
||||
- Trang chủ () 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
|
||||
- Styles toàn cục được định nghĩa trong
|
||||
- Tailwind CSS v4 được import trực tiếp qua trong CSS
|
||||
- Hỗ trợ **Dark Mode** tự động theo cài đặt hệ thống ()
|
||||
- Font chữ sử dụng **Geist Sans** và **Geist Mono** từ Google Fonts thông qua
|
||||
- Trang chủ (app/page.tsx) là điểm vào chính của User Page
|
||||
- Design tokens định nghĩa trong app/globals.css dưới dạng CSS custom properties
|
||||
- Mock data nằm trong lib/constants.ts - thay bằng API calls khi backend sẵn sàng
|
||||
- Dark mode: biến CSS đã chuẩn bị sẵn trong globals.css nhưng chưa kích hoạt
|
||||
- Ảnh sản phẩm: thêm ảnh thực vào public/imgs/products/
|
||||
- Xem WORKFLOW.md để hiểu kiến trúc tổng thể và quy trình mở rộng dự án
|
||||
|
||||
@@ -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
@@ -20,27 +20,34 @@ import { MENU_CATEGORIES, MOCK_PRODUCTS } from "@/lib/constants";
|
||||
* Expanded sidebar: 1 → sm:2 → lg:2 → xl:3 → 2xl:4
|
||||
*/
|
||||
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 [activeCategory, setActiveCategory] = useState("all");
|
||||
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(() => {
|
||||
const mq = window.matchMedia("(min-width: 1024px)");
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsSidebarOpen(mq.matches);
|
||||
const handler = (e: MediaQueryListEvent) => setIsSidebarOpen(e.matches);
|
||||
mq.addEventListener("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 isAvailable = p.available !== false;
|
||||
const matchesCategory = activeCategory === "all" || p.category === activeCategory;
|
||||
const matchesSearch =
|
||||
searchQuery.trim() === "" ||
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
return isAvailable && matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
/* Active category label */
|
||||
@@ -59,8 +66,7 @@ export default function Home() {
|
||||
return (
|
||||
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
|
||||
<div
|
||||
className="flex items-start bg-[var(--color-bg-main)]"
|
||||
style={{ minHeight: "calc(100vh - var(--spacing-header-height))" }}
|
||||
className="flex items-start bg-background min-h-[calc(100vh-var(--spacing-header-height))]"
|
||||
>
|
||||
{/* ── Sidebar ── */}
|
||||
<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">
|
||||
{/* Title + count */}
|
||||
<div className="shrink-0">
|
||||
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
<h2 className="text-xl font-bold text-foreground">
|
||||
{activeCategoryLabel}
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -92,7 +98,7 @@ export default function Home() {
|
||||
<div className="relative w-full sm:max-w-xs">
|
||||
<i
|
||||
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>
|
||||
<input
|
||||
type="text"
|
||||
@@ -100,10 +106,10 @@ export default function Home() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Tìm kiếm món..."
|
||||
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)]
|
||||
border-[var(--color-border)] placeholder:text-[var(--color-text-muted)]
|
||||
focus:border-[var(--color-primary)] focus:ring-2
|
||||
focus:ring-[var(--color-primary)] focus:ring-opacity-20
|
||||
bg-card text-foreground
|
||||
border-border placeholder:text-muted-foreground
|
||||
focus:border-primary focus:ring-2
|
||||
focus:ring-primary focus:ring-opacity-20
|
||||
transition-all duration-150"
|
||||
/>
|
||||
{/* Clear button */}
|
||||
@@ -112,8 +118,8 @@ export default function Home() {
|
||||
onClick={() => setSearchQuery("")}
|
||||
title="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)]
|
||||
hover:text-[var(--color-primary)] transition-colors duration-150
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted)
|
||||
hover:text-(--color-primary) transition-colors duration-150
|
||||
cursor-pointer border-none bg-transparent p-0"
|
||||
>
|
||||
<i className="fa-solid fa-xmark text-sm"></i>
|
||||
@@ -142,7 +148,7 @@ export default function Home() {
|
||||
</div>
|
||||
) : (
|
||||
/* 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>
|
||||
<p className="text-base font-medium">
|
||||
{searchQuery
|
||||
@@ -152,7 +158,7 @@ export default function Home() {
|
||||
{searchQuery && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Xóa tìm kiếm
|
||||
|
||||
+136
-38
@@ -1,60 +1,158 @@
|
||||
# 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:
|
||||
|
||||
# Components Documentation
|
||||
|
||||
> Whenever you create a new component in the components/ directory, automatically append a new section following the template below.
|
||||
|
||||
---
|
||||
|
||||
## CartProduct
|
||||
|
||||
**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 |
|
||||
|------|------|----------|---------|-------------|
|
||||
| image | string | yes | — | URL or path to the product image (used with Next.js Image). |
|
||||
| imageAlt | string | no | Product image | Alt text for the product image (accessibility). |
|
||||
| price | number or string | yes | — | Product price. If number, auto-formatted to Vietnamese Dong (VND). If string, rendered as-is. |
|
||||
| description | string | yes | — | Short product description shown in the card footer. Clamped to 2 lines. |
|
||||
| onBuy | () => void | no | undefined | Callback fired when the Mua button is clicked. |
|
||||
| image | string | yes | - | URL/path to product image (Next.js Image) |
|
||||
| imageAlt | string | no | Anh san pham | Alt text for accessibility |
|
||||
| productName | string | yes | - | Product display name |
|
||||
| price | number or string | yes | - | If number: auto-formatted to VND. If string: rendered as-is |
|
||||
| description | string | yes | - | Short description, clamped to 2 lines |
|
||||
| onBuy | () => void | no | undefined | Callback when Buy button is clicked |
|
||||
|
||||
### 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 |
|
||||
|---------|-----------------|---------|
|
||||
| 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 |
|
||||
| Image container | relative w-full h-48 bg-gray-100 | Fixed-height image area |
|
||||
| Price text | text-lg font-bold text-red-500 | Highlighted price in red |
|
||||
| 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 area | px-4 py-3 border-t border-gray-100 bg-gray-50 | Footer section with light background |
|
||||
| Description text | text-sm text-gray-600 leading-relaxed line-clamp-2 | Truncated 2-line description |
|
||||
| Element | Key classes |
|
||||
|---------|-------------|
|
||||
| Card wrapper | flex flex-col w-full rounded-2xl, shadow uses --color-shadow-sm/md |
|
||||
| Image area | relative w-full h-36, bg --color-border-light |
|
||||
| Product name | font-bold text-sm, color --color-text-primary, line-clamp-1 |
|
||||
| Description | text-xs, color --color-text-muted, line-clamp-2 |
|
||||
| Price | text-sm font-bold, color --color-primary |
|
||||
| Buy button | bg --color-primary, hover --color-primary-dark, active:scale-95 |
|
||||
|
||||
### Dependencies
|
||||
|
||||
- next/image — Next.js optimized Image component
|
||||
- Tailwind CSS — utility-first CSS framework
|
||||
|
||||
### Usage Example
|
||||
|
||||
|
||||
- next/image
|
||||
- Tailwind CSS + CSS custom properties from globals.css
|
||||
- FontAwesome (fa-solid fa-mug-hot fallback, fa-cart-plus button icon)
|
||||
|
||||
### Notes
|
||||
|
||||
- Card width is fixed at w-64 (256px). Adjust if a responsive layout is needed.
|
||||
- The description is clamped to 2 lines via line-clamp-2; longer text will be truncated with ellipsis.
|
||||
- If onBuy is not provided, the button renders but does nothing on click.
|
||||
- Price formatting uses Vietnamese locale (vi-VN) and VND currency by default when price is a number.
|
||||
- Card width is w-full - controlled by parent grid in page.tsx
|
||||
- available field on Product is checked in page.tsx before rendering
|
||||
- onBuy currently logs to console - TODO: implement cart logic
|
||||
|
||||
---
|
||||
|
||||
## 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
@@ -37,16 +37,16 @@ export default function CartProduct({
|
||||
return (
|
||||
<div
|
||||
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)]
|
||||
hover:shadow-[0_6px_20px_var(--color-shadow-md)]
|
||||
hover:-translate-y-0.5
|
||||
transition-all duration-[250ms] cursor-default"
|
||||
transition-all duration-250 cursor-default"
|
||||
>
|
||||
{/* ── 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) */}
|
||||
<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>
|
||||
</div>
|
||||
{/* Product image */}
|
||||
@@ -54,7 +54,7 @@ export default function CartProduct({
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
className="object-cover z-[1]"
|
||||
className="object-cover z-1"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = "none";
|
||||
@@ -64,10 +64,10 @@ export default function CartProduct({
|
||||
|
||||
{/* ── Name + description ── */}
|
||||
<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}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -75,17 +75,17 @@ export default function CartProduct({
|
||||
{/* ── Price + Buy button ── */}
|
||||
<div
|
||||
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}
|
||||
</span>
|
||||
<button
|
||||
onClick={onBuy}
|
||||
className="flex items-center gap-1.5 text-xs font-semibold
|
||||
px-3 py-1.5 rounded-lg border-none cursor-pointer
|
||||
bg-[var(--color-primary)] text-white
|
||||
hover:bg-[var(--color-primary-dark)]
|
||||
bg-(--color-primary) text-white
|
||||
hover:bg-(--color-primary-dark)
|
||||
active:scale-95
|
||||
transition-all duration-150 whitespace-nowrap"
|
||||
>
|
||||
|
||||
+18
-23
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { MENU_CATEGORIES, SHOP_INFO } from "@/lib/constants";
|
||||
import type { MenuCategory } from "@/lib/types";
|
||||
|
||||
@@ -30,35 +29,31 @@ export default function Navbar({
|
||||
activeCategory = "all",
|
||||
onCategoryChange,
|
||||
}: NavbarProps) {
|
||||
const handleClick = (id: string) => {
|
||||
onCategoryChange?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`
|
||||
sticky shrink-0 flex flex-col z-20
|
||||
border-r border-[var(--color-border)]
|
||||
bg-[var(--color-bg-sidebar)]
|
||||
border-r border-(--color-border)
|
||||
bg-(--color-bg-sidebar)
|
||||
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"}
|
||||
`}
|
||||
style={{
|
||||
top: "var(--spacing-header-height)",
|
||||
height: "calc(100vh - var(--spacing-header-height))",
|
||||
}}
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{/* ── Sidebar header: title + toggle button ── */}
|
||||
<div
|
||||
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 && (
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-[var(--color-text-muted)] whitespace-nowrap">
|
||||
<i className="fa-solid fa-utensils mr-2 text-[var(--color-primary)]"></i>
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-(--color-text-muted) whitespace-nowrap">
|
||||
<i className="fa-solid fa-utensils mr-2 text-(--color-primary)"></i>
|
||||
Thực Đơn
|
||||
</span>
|
||||
)}
|
||||
@@ -66,12 +61,12 @@ export default function Navbar({
|
||||
onClick={onToggle}
|
||||
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
|
||||
text-[var(--color-text-muted)] bg-transparent
|
||||
hover:bg-[var(--color-border-light)] hover:text-[var(--color-primary)]
|
||||
text-(--color-text-muted) bg-transparent
|
||||
hover:bg-(--color-border-light) hover:text-(--color-primary)
|
||||
transition-colors duration-150 shrink-0"
|
||||
>
|
||||
<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"
|
||||
}`}
|
||||
></i>
|
||||
@@ -86,7 +81,7 @@ export default function Navbar({
|
||||
return (
|
||||
<li key={cat.id}>
|
||||
<button
|
||||
onClick={() => handleClick(cat.id)}
|
||||
onClick={() => onCategoryChange?.(cat.id)}
|
||||
title={!isOpen ? cat.name : undefined}
|
||||
className={`
|
||||
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"}
|
||||
${
|
||||
isActive
|
||||
? "bg-[var(--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-(--color-primary) text-white shadow-sm"
|
||||
: "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
|
||||
className={`
|
||||
${cat.icon} w-5 text-center text-base shrink-0
|
||||
${isActive ? "text-white" : "text-[var(--color-primary)]"}
|
||||
${isActive ? "text-white" : "text-(--color-primary)"}
|
||||
`}
|
||||
></i>
|
||||
|
||||
@@ -128,18 +123,18 @@ export default function Navbar({
|
||||
{/* ── Sidebar footer: opening hours ── */}
|
||||
<div
|
||||
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 ? (
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
|
||||
<i className="fa-solid fa-clock text-[var(--color-accent)] shrink-0"></i>
|
||||
<div className="flex items-center gap-2 text-xs text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-clock text-(--color-accent) shrink-0"></i>
|
||||
<span>{SHOP_INFO.openHours}</span>
|
||||
</div>
|
||||
) : (
|
||||
<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}
|
||||
></i>
|
||||
)}
|
||||
|
||||
+22
-22
@@ -17,7 +17,7 @@ import { SHOP_INFO, SOCIAL_LINKS } from "@/lib/constants";
|
||||
*/
|
||||
export default function Footer() {
|
||||
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 ── */}
|
||||
<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">
|
||||
@@ -35,7 +35,7 @@ export default function Footer() {
|
||||
sizes="40px"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-bold text-lg text-[var(--color-accent)]">
|
||||
<span className="font-bold text-lg text-(--color-accent)">
|
||||
{SHOP_INFO.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -48,29 +48,29 @@ export default function Footer() {
|
||||
{/* Contact details */}
|
||||
<ul className="flex flex-col gap-2 text-sm opacity-80">
|
||||
<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>
|
||||
</li>
|
||||
<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
|
||||
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}
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
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}
|
||||
</a>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -84,7 +84,7 @@ export default function Footer() {
|
||||
|
||||
{/* ── 2. Social links ── */}
|
||||
<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
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3">
|
||||
@@ -94,7 +94,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
|
||||
@@ -111,7 +111,7 @@ export default function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
|
||||
@@ -126,11 +126,11 @@ export default function Footer() {
|
||||
<Link
|
||||
href={SOCIAL_LINKS.website}
|
||||
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
|
||||
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>
|
||||
</span>
|
||||
@@ -142,15 +142,15 @@ export default function Footer() {
|
||||
|
||||
{/* ── 3. WiFi card ── */}
|
||||
<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í
|
||||
</h3>
|
||||
<div
|
||||
className="rounded-xl border border-[var(--color-primary-light)] border-opacity-50
|
||||
p-4 bg-[var(--color-primary-dark)] bg-opacity-30"
|
||||
className="rounded-xl border border-(--color-primary-light) border-opacity-50
|
||||
p-4 bg-(--color-primary-dark) bg-opacity-30"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
{/* Stacked label + value rows — no overflow risk */}
|
||||
@@ -158,8 +158,8 @@ export default function Footer() {
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="opacity-60 text-xs uppercase tracking-wide">Tên mạng</span>
|
||||
<span
|
||||
className="font-mono font-bold text-[var(--color-accent)]
|
||||
px-2 py-1 rounded border border-[var(--color-accent)]
|
||||
className="font-mono font-bold text-(--color-accent)
|
||||
px-2 py-1 rounded border border-(--color-accent)
|
||||
border-opacity-30 break-all"
|
||||
>
|
||||
{SHOP_INFO.wifi.name}
|
||||
@@ -168,8 +168,8 @@ export default function Footer() {
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="opacity-60 text-xs uppercase tracking-wide">Mật khẩu</span>
|
||||
<span
|
||||
className="font-mono font-bold text-[var(--color-accent)] tracking-wider
|
||||
px-2 py-1 rounded border border-[var(--color-accent)]
|
||||
className="font-mono font-bold text-(--color-accent) tracking-wider
|
||||
px-2 py-1 rounded border border-(--color-accent)
|
||||
border-opacity-30 break-all"
|
||||
>
|
||||
{SHOP_INFO.wifi.password}
|
||||
@@ -196,7 +196,7 @@ export default function Footer() {
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
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
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+21
-22
@@ -9,17 +9,16 @@ import type { User } from "@/lib/types";
|
||||
/**
|
||||
* Site Header — sticky top bar, always visible on all screen sizes.
|
||||
*
|
||||
* 3-column layout:
|
||||
* [Brand: logo + name + tagline] | [Center: quick info] | [Auth button]
|
||||
* 2-column layout:
|
||||
* [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
|
||||
*
|
||||
* Responsive:
|
||||
* - Logo + shop name : always visible
|
||||
* - Tagline : hidden on mobile, shown on md+
|
||||
* - Center info : hidden on mobile, shown on lg+
|
||||
* - Button label : hidden on xs, shown on sm+
|
||||
* - Tagline : hidden on mobile (< md), shown on md+
|
||||
* - Button label : hidden on xs (< sm), shown on sm+
|
||||
*/
|
||||
export default function Header() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
@@ -33,9 +32,9 @@ export default function Header() {
|
||||
return (
|
||||
<header
|
||||
className="w-full sticky top-0 z-50
|
||||
bg-[var(--color-bg-header)] border-b border-[var(--color-border)]
|
||||
shadow-[0_1px_8px_var(--color-shadow-sm)]"
|
||||
style={{ height: "var(--spacing-header-height)" }}
|
||||
bg-(--color-bg-header) border-b border-(--color-border)
|
||||
shadow-[0_1px_8px_var(--color-shadow-sm)]
|
||||
h-(--spacing-header-height)"
|
||||
>
|
||||
<div
|
||||
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">
|
||||
<span
|
||||
className="font-bold text-base md:text-lg
|
||||
text-[var(--color-primary-dark)]
|
||||
group-hover:text-[var(--color-primary)]
|
||||
text-(--color-primary-dark)
|
||||
group-hover:text-(--color-primary)
|
||||
transition-colors duration-150"
|
||||
>
|
||||
{SHOP_INFO.name}
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -86,8 +85,8 @@ export default function Header() {
|
||||
title="Đăng nhập"
|
||||
className="flex items-center gap-2.5 px-5 py-2.5 rounded-xl
|
||||
text-sm font-semibold border-none cursor-pointer
|
||||
bg-[var(--color-primary)] text-white
|
||||
hover:bg-[var(--color-primary-dark)]
|
||||
bg-(--color-primary) text-white
|
||||
hover:bg-(--color-primary-dark)
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
<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)"
|
||||
className="flex items-center gap-2.5 px-4 py-2.5 rounded-xl
|
||||
text-sm font-semibold cursor-pointer
|
||||
bg-[var(--color-accent-light)] border border-[var(--color-accent)]
|
||||
text-[var(--color-primary-dark)]
|
||||
hover:bg-[var(--color-accent)] hover:text-white
|
||||
bg-(--color-accent-light) border border-(--color-accent)
|
||||
text-(--color-primary-dark)
|
||||
hover:bg-(--color-accent) hover:text-white
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
<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)"
|
||||
className="flex items-center gap-2.5 px-4 py-2 rounded-xl
|
||||
text-sm font-semibold cursor-pointer
|
||||
bg-[var(--color-bg-main)] border border-[var(--color-border)]
|
||||
text-[var(--color-text-secondary)]
|
||||
hover:border-[var(--color-primary-light)]
|
||||
hover:bg-[var(--color-border-light)]
|
||||
bg-background border border-(--color-border)
|
||||
text-(--color-text-secondary)
|
||||
hover:border-(--color-primary-light)
|
||||
hover:bg-(--color-border-light)
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
{/* Avatar circle */}
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
|
||||
+10
-9
@@ -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 =====
|
||||
export const SHOP_INFO: ShopInfo = {
|
||||
@@ -23,14 +23,6 @@ export const SOCIAL_LINKS: SocialLinks = {
|
||||
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 =====
|
||||
// Each category has a unique FontAwesome icon representing the item type
|
||||
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.",
|
||||
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) =====
|
||||
|
||||
+1
-7
@@ -44,13 +44,7 @@ export interface ShopInfo {
|
||||
openHours: string;
|
||||
}
|
||||
|
||||
// ===== NAVIGATION TYPES =====
|
||||
export interface NavLink {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
// ===== SOCIAL LINKS TYPES =====
|
||||
export interface SocialLinks {
|
||||
facebook: string;
|
||||
tiktok: string;
|
||||
|
||||
+10
-3
@@ -1,7 +1,14 @@
|
||||
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;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
|
||||
Reference in New Issue
Block a user