diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5ebbba1..c686b3e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,26 +1,31 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/alpine { - "name": "Alpine", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:alpine-3.22", + "name": "Alpine", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:alpine-3.22", - "features": { - "ghcr.io/muhmdraouf/devcontainers-features/alpine-apk:0": { - "packages": "pnpm", - "upgradePackages": true - } - }, + "features": { + "ghcr.io/muhmdraouf/devcontainers-features/alpine-apk:0": { + "packages": "pnpm", + "upgradePackages": true + } + }, + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + } - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", - // Configure tool-specific properties. - // "customizations": {}, + // Configure tool-specific properties. + // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..5100987 --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,122 @@ +name: Release package + +on: + push: + branches: + - main + - dev-* + +jobs: + release: + permissions: + contents: write + issues: write + pull-requests: write + id-token: write + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.set_output.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: latest + + - uses: pnpm/action-setup@v3 + with: + version: latest + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Update dependencies + run: pnpm update + + - name: Format code + run: pnpm format + + - name: Release + env: + GITEA_USER: ${{ github.actor }} + GITEA_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: pnpm release + + - name: Set Version Output + id: set_output + run: | + # Đọc version hiện tại trong package.json sau khi release + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Commit & Push Changes + if: always() + run: | + if [ -n "$(git status --porcelain)" ]; then + git config --global user.name "gitea-actions" + git config --global user.email "actions-user@noreply.git.demonkernel.io.vn" + + git rm -r --cached . + git add . + git commit -m "chore: release [ci skip]" + git push + fi + + build-and-push: + needs: release + # if: gitea.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.demonkernel.io.vn + username: ${{ github.actor }} + password: ${{ secrets.ACCESS_TOKEN }} + + - name: Prepare Docker Metadata + id: meta + run: | + # Lowercase Repository + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + + # Lấy version từ job release và lowercase nó (cho các bản prerelease) + RAW_VERSION="${{ needs.release.outputs.new_version }}" + VERSION_LOWER=$(echo "$RAW_VERSION" | tr '[:upper:]' '[:lower:]') + + echo "repo=$REPO_LOWER" >> $GITHUB_OUTPUT + echo "version=$VERSION_LOWER" >> $GITHUB_OUTPUT + + - name: Build and Push Docker Image + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: | + git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest + git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.version }} + + deploy-via-portainer: + if: gitea.ref == 'refs/heads/main' + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Trigger Portainer Webhook + run: | + curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" diff --git a/.gitignore b/.gitignore index 5ef6a52..8bdfd8e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ !.yarn/plugins !.yarn/releases !.yarn/versions +/.pnpm-store # testing /coverage @@ -19,6 +20,7 @@ # production /build +/release.zip # misc .DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b05cabc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,21 @@ +{ + "endOfLine": "lf", + "importOrder": ["^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "proseWrap": "always", + "plugins": [ + "@trivago/prettier-plugin-sort-imports", + "prettier-plugin-groovy", + "prettier-plugin-embed", + "prettier-plugin-tailwindcss" + ], + "importOrderParserPlugins": ["typescript", "decorators-legacy", "jsx"], + "embeddedMarkdownComments": ["tw", "tx"], + "printWidth": 80, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "tailwindAttributes": ["/.*(C|c)lassName/"] +} \ No newline at end of file diff --git a/README.md b/README.md index e215bc4..f3fdc1b 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,120 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Coffee Shop Frontend -## Getting Started +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. -First, run the development server: +--- + +## Mô Tả Dự Án + +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: + +- 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 - 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 + +--- + +## Cách Chạy Dự Án + +### Yêu cầu hệ thống + +- Node.js >= 18 +- pnpm (khuyến nghị) hoặc npm + +### Cài đặt ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +pnpm install ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +### Dev -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +pnpm dev +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Mở trình duyệt tại http://localhost:3000 -## Learn More +### Build -To learn more about Next.js, take a look at the following resources: +```bash +pnpm build && pnpm start +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +### Lint -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +pnpm lint +``` -## Deploy on Vercel +--- -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Cấu Trúc Thư Mục -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +``` +frondend/ ++-- 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 +``` + +--- + +## Công Nghệ Sử Dụng + +| Công nghệ | Phiên bản | Mục đích | +| ------------ | --------- | ------------------------------------- | +| Next.js | 16.1.7 | React Framework (App Router) | +| React | 19.2.3 | Thư viện UI | +| TypeScript | ^5 | Kiểu dữ liệu tĩnh | +| Tailwind CSS | ^4 | Utility-first CSS framework | +| Geist Font | - | Font chữ (Google Fonts via next/font) | +| FontAwesome | 6.7.2 | Icon library (CDN) | +| pnpm | - | Package manager | +| ESLint | ^9 | Linting | + +--- + +## Ghi Chú Phát Triển + +- 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 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e0b819e --- /dev/null +++ b/TODO.md @@ -0,0 +1,69 @@ +# 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 diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/globals.css b/app/globals.css index a2dc41e..2774e4d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,114 @@ @import "tailwindcss"; +/* ============================================================ + LIGHT THEME — Coffee Brown & Beige Palette + All color/spacing/radius tokens live here so switching to + dark theme later only requires overriding these variables + in the [data-theme="dark"] block below. + ============================================================ */ :root { - --background: #ffffff; - --foreground: #171717; + /* --- Brand Colors --- */ + --color-primary: #6F4E37; /* coffee brown */ + --color-primary-light: #A0785A; /* lighter brown */ + --color-primary-dark: #4A3728; /* dark espresso */ + --color-accent: #C8973A; /* golden caramel */ + --color-accent-light: #F0D9A8; /* pale caramel */ + + /* --- Background Colors --- */ + --color-bg-main: #FDF6EC; /* warm beige */ + --color-bg-card: #FFFFFF; + --color-bg-sidebar: #FFFFFF; + --color-bg-header: #FFFFFF; + --color-bg-footer: #3D2B1F; /* dark espresso */ + + /* --- Text Colors --- */ + --color-text-primary: #2C1A0E; /* very dark brown */ + --color-text-secondary: #6F4E37; /* coffee brown */ + --color-text-muted: #A08060; /* warm grey-brown */ + --color-text-on-dark: #FDF6EC; /* beige on dark bg */ + --color-text-on-primary: #FFFFFF; /* white on primary */ + + /* --- Border & Divider --- */ + --color-border: #E2C9A8; /* warm tan */ + --color-border-light: #F0E4D0; /* very light tan */ + + /* --- Shadow --- */ + --color-shadow-sm: rgba(111, 78, 55, 0.08); + --color-shadow-md: rgba(111, 78, 55, 0.18); + + /* --- Layout Dimensions --- */ + --spacing-header-height: 72px; + --spacing-sidebar-width: 240px; /* expanded */ + --spacing-sidebar-collapsed: 64px; /* icon-only */ + --spacing-content-max: 1536px; + + /* --- Border Radius --- */ + --radius-sm: 6px; + --radius-md: 12px; + --radius-lg: 20px; + --radius-full: 9999px; + + /* --- Transitions --- */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 400ms ease; } +/* ============================================================ + DARK THEME PLACEHOLDER + Uncomment & fill values when dark mode is needed. + ============================================================ */ +/* +[data-theme="dark"] { + --color-primary: #A0785A; + --color-primary-light: #C8956C; + --color-primary-dark: #6F4E37; + --color-accent: #D4A96A; + --color-accent-light: #7A5C30; + + --color-bg-main: #1A0F08; + --color-bg-card: #2D1B0E; + --color-bg-sidebar: #231408; + --color-bg-header: #1A0F08; + --color-bg-footer: #0D0704; + + --color-text-primary: #FDF6EC; + --color-text-secondary: #C8956C; + --color-text-muted: #8B7060; + --color-text-on-dark: #FDF6EC; + + --color-border: #4A2E1A; + --color-border-light: #3D2314; +} +*/ + +/* ============================================================ + TAILWIND THEME BRIDGE + ============================================================ */ @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); + --color-background: var(--color-bg-main); + --color-foreground: var(--color-text-primary); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +/* ============================================================ + BASE STYLES + ============================================================ */ +body { + background-color: var(--color-bg-main); + color: var(--color-text-primary); + font-family: Arial, Helvetica, sans-serif; + scroll-behavior: smooth; } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +/* ============================================================ + CUSTOM SCROLLBAR + ============================================================ */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--color-bg-main); } +::-webkit-scrollbar-thumb { + background: var(--color-primary-light); + border-radius: var(--radius-full); } +::-webkit-scrollbar-thumb:hover { background: var(--color-primary); } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..c4e4c0d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,11 @@ +import CartFab from "@/components/CartFab"; +import Footer from "@/layouts/footer"; +import Header from "@/layouts/header"; import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; + import "./globals.css"; +import { Providers } from "./providers"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +18,13 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Coffee Shop — Hệ thống đặt món", + description: "Đặt món cà phê, trà, nước ép và nhiều hơn nữa tại Coffee Shop.", + icons: { + icon: "/favicon/favicon.ico", + shortcut: "/favicon/favicon.ico", + apple: "/favicon/apple-touch-icon.png", + }, }; export default function RootLayout({ @@ -23,11 +33,32 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + + + {/* FontAwesome 6 — icons used throughout the app */} + + - {children} + + {/* Sticky top header */} +
+ + {/* Page content (grows to fill remaining height) */} +
{children}
+ + {/* Footer always at bottom */} +