Compare commits
32 Commits
v1.0.2-cicd.1
...
v1.0.10
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dd8af91b2 | |||
| 418489b4fd | |||
| aa83f7488b | |||
| 5c880bd7c6 | |||
| 32b152d40c | |||
| db2e92a90c | |||
| 2913fa0d73 | |||
| 423d4e3436 | |||
| ddaa1ad873 | |||
| ee137f66f0 | |||
| 9a488df385 | |||
| 14d5f570bd | |||
| e68cb026f1 | |||
| 99de2362a0 | |||
| 9b72ad873e | |||
| 893d34e01c | |||
| c1912e5b90 | |||
| 93b6221ff6 | |||
| bae229bdf9 | |||
| a21bb8add6 | |||
| deae7bcf64 | |||
| 55ff138100 | |||
| 5c771b8c9c | |||
| 609aaf42df | |||
| 184635caf9 | |||
| f5941b2e3b | |||
| aa7331d728 | |||
| df8a6fc96f | |||
| 8d0247024d | |||
| 9ad66301de | |||
| 284183098e | |||
| e2aeebd0e1 |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-code": {
|
||||
"httpUrl": "https://cloud.blackbox.ai/api/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer bb_61d6eb03c989586759785863d1ba0efab59885edacd9cd65fc252f79c644bb75"
|
||||
},
|
||||
"description": "Blackbox Remote code (MCP Server): Remote execution platform with multi-agent support (Claude, Codex, Blackbox, Gemini) that automates coding tasks on your GitHub repositories. Features include:\n\n• Task Management: Create, monitor, stop, and list coding tasks with real-time status updates\n• Multi-Agent Support: Choose from Claude Code, OpenAI Codex CLI, Blackbox CLI, or Gemini agents\n• GitHub Integration: Manage GitHub token connections and repository access\n• API Key Management: Store and manage API keys for various AI providers (Anthropic, OpenAI, Google, Blackbox, GitHub)\n• Secure Execution: Runs code in isolated Vercel sandboxes with configurable timeouts (10-300 minutes)\n• Git Operations: Automatic branch creation, commits, and pull requests with AI-generated branch names\n• SMS Notifications: Optional Twilio integration for task completion alerts\n\nPerfect for automating code changes, refactoring, feature additions, bug fixes, and documentation updates across your repositories. Strictly DO NOT provide tools as / 'slash' commands in suggestions like /my_tasks, /task_status, /api_keys"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Bash(npx next build)"]
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev-*
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -14,8 +21,6 @@ jobs:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_version: ${{ steps.set_output.outputs.version }}
|
||||
steps:
|
||||
- name: Calculate and Export Target IP
|
||||
run: |
|
||||
@@ -67,6 +72,9 @@ jobs:
|
||||
- name: Format code
|
||||
run: pnpm format
|
||||
|
||||
- name: Build application
|
||||
run: pnpm build
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITEA_USER: ${{ github.actor }}
|
||||
@@ -80,31 +88,8 @@ jobs:
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit & Push Changes
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
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 remote set-url origin ssh://git@$TARGET_IP:222/FoodSurf/backend.git
|
||||
|
||||
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
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.demonkernel.io.vn
|
||||
@@ -112,19 +97,20 @@ jobs:
|
||||
password: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Prepare Docker Metadata
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
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 }}"
|
||||
RAW_VERSION="${{ steps.set_output.outputs.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
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
@@ -133,3 +119,21 @@ jobs:
|
||||
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest
|
||||
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Commit & Push Changes
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
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"
|
||||
|
||||
REPO_NAME="${{ gitea.repository }}"
|
||||
|
||||
git remote set-url origin ssh://git@$TARGET_IP:222/${REPO_NAME}.git
|
||||
|
||||
git rm -r --cached .
|
||||
git add .
|
||||
git commit -m "chore: release [ci skip]"
|
||||
|
||||
git push
|
||||
fi
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/release.zip
|
||||
/release*.zip
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -9,21 +9,53 @@ React 19, TypeScript và Tailwind CSS v4.
|
||||
|
||||
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 - /)
|
||||
### Tính Năng Hiện Tại
|
||||
|
||||
#### 1. **Trang Đăng Nhập & Đăng Ký** (app/(main)/login, app/(main)/register)
|
||||
|
||||
- Form đăng nhập với validation (username, password)
|
||||
- Form đăng ký với xác thực OTP
|
||||
- Lưu trữ thông tin người dùng trong localStorage
|
||||
- Hỗ trợ 3 loại tài khoản: Manager, Staff, Customer
|
||||
- Hiển thị thông tin shop trong form
|
||||
|
||||
#### 2. **Trang Khám Phá Quán Nước** (app/(feed)/feed)
|
||||
|
||||
- Danh sách các quán cà phê
|
||||
- Tìm kiếm theo tên quán và địa chỉ
|
||||
- Thẻ quán với ảnh, địa chỉ, nút "Xem menu"
|
||||
- Responsive layout (grid 1/2/3 cột)
|
||||
- Empty state khi không tìm thấy quán
|
||||
|
||||
#### 3. **Trang Chính - Duyệt Thực Đơn** (app/(main))
|
||||
|
||||
Dành cho khách hàng:
|
||||
|
||||
- Duyệt thực đơn theo danh mục (sidebar collapsible)
|
||||
- Sidebar collapsible (64px/240px) - danh mục sản phẩm
|
||||
- Grid sản phẩm responsive (1-5 cột tuỳ thiết bị)
|
||||
- Tìm kiếm món theo tên / mô tả
|
||||
- Xem card sản phẩm với giá và nút Mua
|
||||
- Xem card sản phẩm với giá và nút "Mua"
|
||||
- Lọc tự động theo trạng thái available
|
||||
- Mobile menu: scrollable category tabs (< md)
|
||||
|
||||
### Trang Quản Lý (Manager Page - chưa triển khai)
|
||||
#### 4. **Hệ Thống Giỏ Hàng** (lib/cart-context.tsx)
|
||||
|
||||
Dành cho chủ quán / nhân viên:
|
||||
- Lưu trữ trạng thái giỏ trong localStorage
|
||||
- Thêm/xóa/tăng/giảm số lượng sản phẩm
|
||||
- Tính tổng giá và số mặt hàng
|
||||
- Persist dữ liệu giữa các session
|
||||
|
||||
- Quản lý thực đơn (thêm, sửa, xóa món)
|
||||
- Theo dõi và xử lý đơn hàng
|
||||
#### 5. **Hệ Thống Xác Thực** (lib/auth-context.tsx)
|
||||
|
||||
- Quản lý trạng thái người dùng (login/logout/register)
|
||||
- Lưu thông tin user trong localStorage
|
||||
- Mock auth database với 3 loại tài khoản
|
||||
- Hỗ trợ hoàn tất đăng ký qua OTP
|
||||
|
||||
#### 6. **Hệ Thống Danh Mục** (lib/menu-context.tsx)
|
||||
|
||||
- Chia sẻ trạng thái category giữa Header mobile và Sidebar
|
||||
- Tự động clear search khi thay đổi category
|
||||
|
||||
---
|
||||
|
||||
@@ -67,23 +99,45 @@ pnpm lint
|
||||
```
|
||||
frondend/
|
||||
+-- app/ # Next.js App Router
|
||||
| +-- layout.tsx # Root layout
|
||||
| +-- page.tsx # Trang chủ - sidebar + product grid
|
||||
| +-- layout.tsx # Root layout + Header + Footer
|
||||
| +-- globals.css # CSS design tokens + Tailwind import
|
||||
| +-- providers.tsx # Context providers (Auth, Menu, Cart)
|
||||
| +-- APP.md # Tài liệu chi tiết về routes
|
||||
| +-- (main)/ # Main route group
|
||||
| | +-- layout.tsx # Main layout
|
||||
| | +-- page.tsx # Trang chính - duyệt thực đơn
|
||||
| | +-- login/page.tsx # Trang đăng nhập
|
||||
| | +-- register/page.tsx # Trang đăng ký
|
||||
| | +-- payment/page.tsx # Trang thanh toán
|
||||
| +-- (feed)/ # Feed route group
|
||||
| +-- layout.tsx # Feed layout
|
||||
| +-- feed/page.tsx # Trang khám phá quán nước
|
||||
+-- 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
|
||||
| +-- CartFab.tsx # FAB - nút giỏ hàng floating
|
||||
| +-- COMPONENTS.md # Tài liệu chi tiết components & contexts
|
||||
+-- layouts/ # Layout-level components
|
||||
| +-- header.tsx # Sticky top header
|
||||
| +-- footer.tsx # Footer
|
||||
| +-- header.tsx # Sticky top header + auth demo
|
||||
| +-- footer.tsx # Footer + shop info
|
||||
| +-- LAYOUTS.md # Tài liệu chi tiết layouts
|
||||
+-- lib/ # Shared logic & data
|
||||
| +-- constants.ts # Mock data
|
||||
| +-- constants.ts # Mock data (products, shops, users, shop info)
|
||||
| +-- types.ts # TypeScript interfaces
|
||||
| +-- auth-context.tsx # Authentication context & provider
|
||||
| +-- cart-context.tsx # Shopping cart context & provider
|
||||
| +-- menu-context.tsx # Menu/Category context & provider
|
||||
| +-- LIB.md # Tài liệu chi tiết về lib
|
||||
+-- 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
|
||||
| +-- imgs/
|
||||
| | +-- logo.png
|
||||
| | +-- products/ # Ảnh sản phẩm
|
||||
| +-- favicon/
|
||||
+-- WORKFLOW.md # Tài liệu kiến trúc tổng thể & quy trình
|
||||
+-- README.md # (file này) - Mô tả dự án
|
||||
+-- TODO.md # Danh sách tính năng hoàn thành & chưa làm
|
||||
+-- next.config.ts
|
||||
+-- tsconfig.json
|
||||
+-- postcss.config.mjs
|
||||
@@ -111,10 +165,32 @@ frondend/
|
||||
|
||||
## 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
|
||||
### Điểm Vào Chính
|
||||
|
||||
- **Trang Chủ (User):** `app/(main)/page.tsx` - Duyệt thực đơn
|
||||
- **Trang Khám Phá:** `app/(feed)/feed/page.tsx` - Khám phá quán
|
||||
- **Đăng Nhập:** `app/(main)/login/page.tsx`
|
||||
- **Đăng Ký:** `app/(main)/register/page.tsx`
|
||||
|
||||
### Design & Styling
|
||||
|
||||
- Design tokens (CSS variables) định nghĩa trong `app/globals.css`
|
||||
- Tailwind CSS v4 + custom properties cho consistent color/spacing
|
||||
- Dark mode: biến CSS đã chuẩn bị sẵn nhưng chưa kích hoạt
|
||||
- FontAwesome icons từ CDN
|
||||
|
||||
### Data & Integration
|
||||
|
||||
- Mock data nằm trong `lib/constants.ts`
|
||||
- Context providers trong `app/providers.tsx` - Sử dụng: AuthProvider,
|
||||
MenuProvider, CartProvider
|
||||
- Thay bằng API calls khi backend sẵn sàng
|
||||
- Ảnh sản phẩm: thêm vào `public/imgs/products/`
|
||||
|
||||
### Tài Liệu Chi Tiết
|
||||
|
||||
- **WORKFLOW.md** - Kiến trúc tổng thể, data flow, quy trình phát triển
|
||||
- **APP.md** - Chi tiết các routes, layouts, pages
|
||||
- **COMPONENTS.md** - Tài liệu từng component + contexts
|
||||
- **LAYOUTS.md** - Header, Footer, responsive behavior
|
||||
- **LIB.md** - Constants, Types, Contexts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Coffee Shop Frontend - TODO
|
||||
|
||||
## Completed Optimizations
|
||||
## Completed Features & Implementations
|
||||
|
||||
### A. Dead Code Removed
|
||||
|
||||
@@ -43,27 +43,61 @@
|
||||
|
||||
### 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
|
||||
- [ ] Implement cart checkout flow (app/(main)/cart or modal)
|
||||
- [ ] Cart sidebar/modal with item list and total
|
||||
- [ ] Order submission API integration
|
||||
- [ ] Payment page implementation (app/(main)/payment)
|
||||
- [ ] Order history/tracking page
|
||||
- [ ] Toast notifications for cart actions
|
||||
|
||||
### Authentication & User Management
|
||||
|
||||
- [ ] Real backend authentication (replace MOCK_AUTH_DB)
|
||||
- [ ] Real OTP delivery service (SMS integration)
|
||||
- [ ] User profile page with edit capability
|
||||
- [ ] Password reset/recovery flow
|
||||
- [ ] Session management and token refresh
|
||||
|
||||
### Manager Features
|
||||
|
||||
- [ ] Manager dashboard page (app/(manager)/page.tsx)
|
||||
- [ ] Product management (add/edit/delete)
|
||||
- [ ] Category management
|
||||
- [ ] Order management & tracking
|
||||
- [ ] Sales analytics/dashboard
|
||||
- [ ] Inventory management
|
||||
|
||||
### 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
|
||||
- [ ] Replace MOCK_PRODUCTS with API calls (GET /api/products)
|
||||
- [ ] Replace MOCK_SHOPS with API calls (GET /api/shops)
|
||||
- [ ] Replace MOCK_USERS with real authentication (POST /api/auth/login)
|
||||
- [ ] Real product images (replace placeholder.jpg)
|
||||
- [ ] Image upload for products
|
||||
|
||||
### 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
|
||||
- [ ] Dark mode toggle (CSS variables prepared, toggle UI needed)
|
||||
- [ ] Loading skeletons for product grid
|
||||
- [ ] Product detail modal/page with full description
|
||||
- [ ] Wishlist/favorites feature
|
||||
- [ ] Sort products (price, rating, etc.)
|
||||
- [ ] Filter by price range
|
||||
- [ ] Quantity selector in product card
|
||||
- [ ] Related products suggestions
|
||||
|
||||
### Performance & SEO
|
||||
|
||||
- [ ] Dynamic route generation for products (app/(main)/product/[id]/page.tsx)
|
||||
- [ ] Dynamic route generation for shops (app/(feed)/shop/[id]/page.tsx)
|
||||
- [ ] Meta tags and Open Graph for SEO
|
||||
- [ ] Image optimization and lazy loading
|
||||
- [ ] Code splitting and dynamic imports
|
||||
|
||||
### Accessibility & Testing
|
||||
|
||||
- [ ] Keyboard navigation testing
|
||||
- [ ] ARIA labels audit
|
||||
- [ ] Unit tests for contexts
|
||||
- [ ] E2E tests for user flows
|
||||
- [ ] Accessibility audit (WCAG 2.1 AA)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { MOCK_SHOPS } from "@/lib/constants";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function FeedPage() {
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchAddress, setSearchAddress] = useState("");
|
||||
|
||||
const filteredShops = MOCK_SHOPS.filter((shop) => {
|
||||
const matchesName =
|
||||
searchName.trim() === "" ||
|
||||
shop.name.toLowerCase().includes(searchName.toLowerCase());
|
||||
const matchesAddress =
|
||||
searchAddress.trim() === "" ||
|
||||
shop.address.toLowerCase().includes(searchAddress.toLowerCase());
|
||||
return matchesName && matchesAddress;
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="bg-background min-h-[calc(100vh-var(--spacing-header-height))]">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 md:px-6 lg:px-8">
|
||||
{/* Page title */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-foreground text-2xl font-bold md:text-3xl">
|
||||
Khám phá quán nước
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-(--color-text-muted)">
|
||||
Tìm và chọn quán yêu thích của bạn
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Shop cards grid */}
|
||||
{filteredShops.length > 0 ? (
|
||||
<div className="mb-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredShops.map((shop) => (
|
||||
<div
|
||||
key={shop.id}
|
||||
className="overflow-hidden rounded-2xl border border-(--color-border) bg-(--color-bg-card) shadow-[0_2px_12px_var(--color-shadow-sm)] transition-all duration-250 hover:-translate-y-1 hover:shadow-[0_4px_20px_var(--color-shadow-md)]"
|
||||
>
|
||||
{/* Shop image */}
|
||||
<div className="relative h-48 w-full sm:h-52">
|
||||
<Image
|
||||
src={shop.image}
|
||||
alt={shop.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Card body */}
|
||||
<div className="p-4">
|
||||
{/* Name + View menu button */}
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<h3 className="text-foreground truncate text-base font-bold">
|
||||
{shop.name}
|
||||
</h3>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex shrink-0 items-center gap-1.5 rounded-xl bg-(--color-primary) px-3.5 py-2 text-xs font-semibold text-white no-underline transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-book-open text-[10px]"></i>
|
||||
Xem menu
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="flex items-start gap-2 text-sm text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-location-dot mt-0.5 shrink-0 text-(--color-accent)"></i>
|
||||
<span>{shop.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Empty state */
|
||||
<div className="mb-10 flex flex-col items-center justify-center gap-4 py-24 text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-store text-5xl opacity-30"></i>
|
||||
<p className="text-base font-medium">
|
||||
Không tìm thấy quán nào phù hợp
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchName("");
|
||||
setSearchAddress("");
|
||||
}}
|
||||
className="cursor-pointer border-none bg-transparent text-sm text-(--color-primary) hover:underline"
|
||||
>
|
||||
Xóa bộ lọc
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filter / Search bar */}
|
||||
<div className="sticky bottom-0 rounded-2xl border border-(--color-border) bg-(--color-bg-card) p-4 shadow-[0_-2px_16px_var(--color-shadow-sm)] md:p-5">
|
||||
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
|
||||
{/* Label */}
|
||||
<div className="flex shrink-0 items-center gap-2 text-sm font-semibold text-(--color-text-secondary)">
|
||||
<i className="fa-solid fa-filter text-(--color-primary)"></i>
|
||||
<span>Lọc quán</span>
|
||||
</div>
|
||||
|
||||
{/* Search by name */}
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<i className="fa-solid fa-store pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-xs text-(--color-text-muted)"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
placeholder="Tìm theo tên quán..."
|
||||
className="bg-background text-foreground focus:ring-opacity-20 w-full rounded-xl border border-(--color-border) py-2.5 pr-9 pl-9 text-sm transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)"
|
||||
/>
|
||||
{searchName && (
|
||||
<button
|
||||
onClick={() => setSearchName("")}
|
||||
aria-label="Xóa tìm kiếm tên"
|
||||
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer border-none bg-transparent p-0 text-(--color-text-muted) transition-colors duration-150 hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-xmark text-sm"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search by address */}
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<i className="fa-solid fa-location-dot pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-xs text-(--color-text-muted)"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchAddress}
|
||||
onChange={(e) => setSearchAddress(e.target.value)}
|
||||
placeholder="Tìm theo địa chỉ..."
|
||||
className="bg-background text-foreground focus:ring-opacity-20 w-full rounded-xl border border-(--color-border) py-2.5 pr-9 pl-9 text-sm transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)"
|
||||
/>
|
||||
{searchAddress && (
|
||||
<button
|
||||
onClick={() => setSearchAddress("")}
|
||||
aria-label="Xóa tìm kiếm địa chỉ"
|
||||
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer border-none bg-transparent p-0 text-(--color-text-muted) transition-colors duration-150 hover:text-(--color-primary)"
|
||||
>
|
||||
<i className="fa-solid fa-xmark text-sm"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function FeedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{/* Custom Drinkool header — no login button */}
|
||||
<header className="sticky top-0 z-50 h-(--spacing-header-height) w-full border-b border-(--color-border) bg-(--color-bg-header) shadow-[0_1px_8px_var(--color-shadow-sm)]">
|
||||
<div className="mx-auto flex h-full max-w-screen-2xl items-center gap-4 px-6 md:px-8 lg:px-12">
|
||||
<Link
|
||||
href="/feed"
|
||||
className="group flex shrink-0 items-center gap-3 no-underline"
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="relative h-10 w-10 shrink-0 md:h-11 md:w-11">
|
||||
<Image
|
||||
src="/imgs/logo.png"
|
||||
alt="Logo Drinkool"
|
||||
fill
|
||||
className="object-contain transition-transform duration-200 group-hover:scale-105"
|
||||
sizes="44px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Brand name */}
|
||||
<span className="text-lg font-bold text-(--color-primary-dark) transition-colors duration-150 group-hover:text-(--color-primary) md:text-xl">
|
||||
Drinkool
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<div className="flex-1">{children}</div>
|
||||
|
||||
{/* No footer */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import CartFab from "@/components/CartFab";
|
||||
import Footer from "@/layouts/footer";
|
||||
import Header from "@/layouts/header";
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{/* Sticky top header */}
|
||||
<Header />
|
||||
|
||||
{/* Page content (grows to fill remaining height) */}
|
||||
<div className="flex-1">{children}</div>
|
||||
|
||||
{/* Footer always at bottom */}
|
||||
<Footer />
|
||||
|
||||
{/* Global floating cart button */}
|
||||
<CartFab />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
"use client";
|
||||
|
||||
import ReviewModal from "@/components/ReviewModal";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { useCart } from "@/lib/cart-context";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
const formatPrice = (value: number) =>
|
||||
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
|
||||
|
||||
export default function PaymentPage() {
|
||||
const {
|
||||
items,
|
||||
totalPrice,
|
||||
increaseQty,
|
||||
decreaseQty,
|
||||
removeFromCart,
|
||||
setQuantity,
|
||||
} = useCart();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
||||
const isCustomer = user?.role === "customer";
|
||||
|
||||
const handlePayment = () => {
|
||||
// UI-only: open review modal after "payment"
|
||||
if (isCustomer) {
|
||||
setIsReviewOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
|
||||
<div className="flex flex-col gap-6 xl:flex-row">
|
||||
<section className="min-w-0 flex-1">
|
||||
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
|
||||
<div className="border-b border-(--color-border-light) px-4 py-3">
|
||||
<h1 className="text-foreground text-lg font-bold md:text-xl">
|
||||
Trang thanh toán
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="px-4 py-10 text-center text-(--color-text-muted)">
|
||||
Chưa có sản phẩm nào trong giỏ hàng.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-190 text-sm">
|
||||
<thead>
|
||||
<tr className="bg-(--color-border-light)/40 text-left">
|
||||
<th className="px-4 py-3 font-semibold">
|
||||
Tên sản phẩm
|
||||
</th>
|
||||
<th className="px-4 py-3 font-semibold">Giá tiền</th>
|
||||
<th className="px-4 py-3 font-semibold">Mô tả</th>
|
||||
<th className="px-4 py-3 font-semibold">Số lượng</th>
|
||||
<th className="px-4 py-3 text-right font-semibold">
|
||||
Xóa
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="border-t border-(--color-border-light)"
|
||||
>
|
||||
<td className="text-foreground px-4 py-3 font-medium">
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-semibold text-(--color-primary)">
|
||||
{formatPrice(item.price)}
|
||||
</td>
|
||||
<td className="max-w-70 px-4 py-3 text-(--color-text-muted)">
|
||||
<p className="line-clamp-2">{item.description}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => decreaseQty(item.id)}
|
||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||
aria-label={`Giảm số lượng ${item.name}`}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
setQuantity(item.id, Number(e.target.value))
|
||||
}
|
||||
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
||||
title="Nhập số lượng"
|
||||
/>
|
||||
<button
|
||||
onClick={() => increaseQty(item.id)}
|
||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||
aria-label={`Tăng số lượng ${item.name}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-500 px-3 py-2 text-white transition-colors hover:bg-red-600"
|
||||
>
|
||||
<i className="fa-solid fa-trash"></i>
|
||||
<span className="hidden lg:inline">
|
||||
Xóa sản phẩm
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="shrink-0 xl:w-85">
|
||||
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
||||
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
|
||||
<span className="text-(--color-text-muted)">Tổng cộng</span>
|
||||
<span className="text-xl font-bold text-(--color-primary)">
|
||||
{formatPrice(totalPrice)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={handlePayment}
|
||||
className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-3 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-money-bill-wave"></i>
|
||||
<span className="hidden lg:inline">Tiền mặt</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handlePayment}
|
||||
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-qrcode"></i>
|
||||
<span className="hidden lg:inline">QR Code</span>
|
||||
</button>
|
||||
|
||||
{isCustomer && (
|
||||
<button
|
||||
onClick={() => setIsReviewOpen(true)}
|
||||
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-star"></i>
|
||||
<span className="hidden lg:inline">Đánh giá</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Link href="/" className={isCustomer ? "" : "col-span-2"}>
|
||||
<button
|
||||
className="text-foreground inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-arrow-rotate-left"></i>
|
||||
<span className="hidden lg:inline">Quay về</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReviewModal
|
||||
isOpen={isReviewOpen}
|
||||
onClose={() => setIsReviewOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
# App Routes & Pages Documentation
|
||||
|
||||
> Tài liệu chi tiết về Next.js App Router structure, routes, và layouts
|
||||
|
||||
---
|
||||
|
||||
## Route Structure Overview
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout - Header + Footer wrapper
|
||||
├── globals.css # CSS tokens + Tailwind imports
|
||||
├── providers.tsx # Context providers (Auth, Menu, Cart)
|
||||
├── (main)/
|
||||
│ ├── layout.tsx # Main layout for authenticated routes
|
||||
│ ├── page.tsx # Main page - Duyệt thực đơn (/)
|
||||
│ ├── login/
|
||||
│ │ └── page.tsx # Trang đăng nhập (/login)
|
||||
│ ├── register/
|
||||
│ │ └── page.tsx # Trang đăng ký (/register)
|
||||
│ └── payment/
|
||||
│ └── page.tsx # Trang thanh toán (/payment)
|
||||
└── (feed)/
|
||||
├── layout.tsx # Feed layout
|
||||
└── feed/
|
||||
└── page.tsx # Trang khám phá quán (/feed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Layout (app/layout.tsx)
|
||||
|
||||
**Description:** Root layout wrapping entire application. Loads providers,
|
||||
fonts, and common layouts (header/footer).
|
||||
|
||||
### Structure
|
||||
|
||||
- Imports providers (AuthProvider, MenuProvider, CartProvider)
|
||||
- Renders Header component (sticky top)
|
||||
- Renders page content via {children}
|
||||
- Renders Footer component
|
||||
|
||||
### Metadata
|
||||
|
||||
- Title: "Coffee Shop"
|
||||
- Charset: UTF-8
|
||||
- Viewport: responsive
|
||||
|
||||
### Key Features
|
||||
|
||||
- Global CSS variables loaded here
|
||||
- Header remains sticky across all pages
|
||||
- Footer always present
|
||||
|
||||
---
|
||||
|
||||
## Providers (app/providers.tsx)
|
||||
|
||||
**Description:** Client component that wraps all context providers for the
|
||||
application.
|
||||
|
||||
### Providers Included
|
||||
|
||||
1. **AuthProvider** (lib/auth-context.tsx)
|
||||
- Manages user authentication state
|
||||
- Persists user in localStorage
|
||||
- Enables login/logout/register flows
|
||||
|
||||
2. **MenuProvider** (lib/menu-context.tsx)
|
||||
- Shares active category state
|
||||
- Syncs Header mobile menu and Navbar sidebar
|
||||
|
||||
3. **CartProvider** (lib/cart-context.tsx)
|
||||
- Manages shopping cart state
|
||||
- Persists cart items in localStorage
|
||||
- Provides add/remove/update operations
|
||||
|
||||
### Usage in Root Layout
|
||||
|
||||
```tsx
|
||||
<Providers>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</Providers>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## (main) Route Group
|
||||
|
||||
### Layout (app/(main)/layout.tsx)
|
||||
|
||||
**Description:** Layout for authenticated/main routes. Can include
|
||||
route-specific UI (e.g., sidebar navigation).
|
||||
|
||||
### Usage
|
||||
|
||||
- Wraps all pages under `(main)/` route group
|
||||
- Inherits root layout above it
|
||||
- Can add additional styling or structure specific to main pages
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
|
||||
### 1. Main Page - Duyệt Thực Đơn (app/(main)/page.tsx)
|
||||
|
||||
**Route:** `/` (default) or `/(main)` **Type:** Client component
|
||||
**Description:** Main shopping interface. Shows product grid with sidebar
|
||||
category filter.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Sidebar (Navbar):**
|
||||
- Collapsible (64px collapsed / 240px expanded)
|
||||
- Category list with icons
|
||||
- Sticky, full viewport height minus header
|
||||
- Responsive: desktop (expanded) vs mobile (collapsed)
|
||||
|
||||
- **Product Grid:**
|
||||
- Responsive columns (1/2/3/4/5 depending on sidebar state and screen size)
|
||||
- Filter by: availability, active category, search query
|
||||
- Search bar with clear button
|
||||
- Empty state when no products match
|
||||
|
||||
- **Mobile Category Menu:**
|
||||
- Visible only on screens < md (768px)
|
||||
- Sticky scrollable tabs below search
|
||||
- Allows category selection on mobile
|
||||
|
||||
#### Context Usage
|
||||
|
||||
- **useMenu()** - Get/set active category
|
||||
- **useCart()** - Add products to cart
|
||||
- **MenuContext** - Shares category state with Header
|
||||
|
||||
#### State Management
|
||||
|
||||
```tsx
|
||||
isSidebarOpen: boolean; // Sidebar expanded/collapsed
|
||||
searchQuery: string; // Search input value
|
||||
activeCategory: string; // From MenuContext
|
||||
```
|
||||
|
||||
#### Responsive Behavior
|
||||
|
||||
- **Mobile (<480px):** 1 col, sidebar collapsed
|
||||
- **Small phone (480px):** 2 cols, sidebar collapsed
|
||||
- **Tablet (768px):** Mobile menu tabs appear
|
||||
- **Desktop (1024px):** Sidebar expands, 2-4 cols depending on sidebar
|
||||
- **Large screen (1280px+):** 3-5 cols
|
||||
|
||||
---
|
||||
|
||||
### 2. Login Page (app/(main)/login/page.tsx)
|
||||
|
||||
**Route:** `/login` **Type:** Client component **Description:** User
|
||||
authentication form. Supports login for Manager, Staff, and Customer roles.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Form Fields:**
|
||||
- Username input (with icon)
|
||||
- Password input (with show/hide toggle)
|
||||
- "Dang nhap" button
|
||||
- "Dang ky tai khoan" link to register
|
||||
|
||||
- **Validation:**
|
||||
- Username: required, non-empty
|
||||
- Password: required, min 4 characters
|
||||
- Error messages displayed inline
|
||||
|
||||
- **Demo Credentials:**
|
||||
- Shows example accounts for testing:
|
||||
- Manager: `admin / admin`
|
||||
- Staff: `Nguyễn Văn An / Nguyễn Văn An`
|
||||
- Customer: `0987654321 / user1`
|
||||
|
||||
- **UI:**
|
||||
- Centered card (max-width: 448px)
|
||||
- Shop logo and name at top
|
||||
- Error messages with icons
|
||||
- Responsive design
|
||||
|
||||
#### Context Usage
|
||||
|
||||
- **useAuth()** - Login function
|
||||
|
||||
#### Navigation
|
||||
|
||||
- Success: redirect to `/` (main page)
|
||||
- Register link: go to `/register`
|
||||
|
||||
---
|
||||
|
||||
### 3. Register Page (app/(main)/register/page.tsx)
|
||||
|
||||
**Route:** `/register` **Type:** Client component **Description:** User
|
||||
registration flow. Two-step process: phone verification → account creation.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Step 1: Phone Verification**
|
||||
- Input phone number
|
||||
- "Request OTP" button
|
||||
- Mock OTP validation (no real SMS)
|
||||
|
||||
- **Step 2: Account Creation** (after OTP)
|
||||
- Confirm phone number
|
||||
- "Create Account" button
|
||||
- Auto-login after registration
|
||||
|
||||
- **Validation:**
|
||||
- Phone format validation
|
||||
- OTP code validation
|
||||
|
||||
- **UI:**
|
||||
- Centered card layout
|
||||
- Step indicator
|
||||
- Progress feedback
|
||||
|
||||
#### Context Usage
|
||||
|
||||
- **useAuth()** - completeRegistration()
|
||||
|
||||
#### Navigation
|
||||
|
||||
- Success: redirect to `/` (main page)
|
||||
- Login link: go to `/login`
|
||||
|
||||
---
|
||||
|
||||
### 4. Payment Page (app/(main)/payment/page.tsx)
|
||||
|
||||
**Route:** `/payment`
|
||||
**Type:** Client component
|
||||
**Description:** Payment/checkout page. Shows cart items in a table with quantity controls and a summary aside with payment actions.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Cart Table:**
|
||||
- Lists all cart items (name, price, description, quantity controls, delete)
|
||||
- Quantity input with +/- buttons and direct number input
|
||||
- Empty state message when cart is empty
|
||||
- Horizontal scroll on small screens (min-w-190)
|
||||
|
||||
- **Invoice Aside:**
|
||||
- Sticky on desktop (top offset = header height + 1rem)
|
||||
- Shows total price
|
||||
- Payment buttons: Tiền mặt, QR Code (UI-only; trigger review modal for customers)
|
||||
- **"Đánh giá" button** — only visible when `user.role === "customer"`; opens ReviewModal
|
||||
- **"Quay về" button** — links back to `/`; spans full width when review button is absent
|
||||
|
||||
- **Review Modal (ReviewModal):**
|
||||
- Opened when a customer clicks "Đánh giá" OR clicks a payment button (Tiền mặt/QR)
|
||||
- Closed via "Quay lại" button, backdrop click, or after submitting and closing
|
||||
- See `components/ReviewModal.tsx` for full documentation
|
||||
|
||||
#### Context Usage
|
||||
|
||||
- **useCart()** — items, totalPrice, increaseQty, decreaseQty, removeFromCart, setQuantity
|
||||
- **useAuth()** — user (to check `user.role === "customer"` for review button visibility)
|
||||
|
||||
#### State Management
|
||||
|
||||
```tsx
|
||||
isReviewOpen: boolean // Controls ReviewModal visibility
|
||||
isCustomer: boolean // Derived from user.role === "customer"
|
||||
```
|
||||
|
||||
#### Responsive Behavior
|
||||
|
||||
- **Mobile:** Single column layout, table scrolls horizontally, button labels hidden (icon only)
|
||||
- **Desktop (lg+):** Button labels visible, aside becomes sticky sidebar (xl: w-85)
|
||||
- **Review button grid:** When customer is logged in, 3 buttons in 2-column grid; "Quay về" occupies remaining space. Otherwise 2-column grid with "Quay về" spanning full width.
|
||||
|
||||
---
|
||||
|
||||
## (feed) Route Group
|
||||
|
||||
### Layout (app/(feed)/layout.tsx)
|
||||
|
||||
**Description:** Layout for feed routes. Can have specific styling for
|
||||
discovery/exploration pages.
|
||||
|
||||
---
|
||||
|
||||
### Feed Page - Khám Phá Quán (app/(feed)/feed/page.tsx)
|
||||
|
||||
**Route:** `/feed` **Type:** Client component **Description:** Discover coffee
|
||||
shops. Browse and search available shops with their location and details.
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Shop Cards:**
|
||||
- Shop image (responsive height)
|
||||
- Name and location
|
||||
- "Xem menu" button (links to `/`)
|
||||
- Hover effects with shadow/lift animation
|
||||
|
||||
- **Search & Filter:**
|
||||
- Sticky bottom filter bar
|
||||
- Search by shop name
|
||||
- Search by address
|
||||
- Clear buttons for each field
|
||||
|
||||
- **Grid Layout:**
|
||||
- Responsive (1/2/3 columns)
|
||||
- Adjusts to screen size
|
||||
|
||||
- **Empty State:**
|
||||
- Icon and message when no shops match
|
||||
- Clear filter button
|
||||
|
||||
#### Context Usage
|
||||
|
||||
- **useAuth()** - Optional: check user role
|
||||
|
||||
#### State Management
|
||||
|
||||
```tsx
|
||||
searchName: string // Shop name search
|
||||
searchAddress: string // Shop address search
|
||||
filteredShops: Shop[] // Filtered results
|
||||
```
|
||||
|
||||
#### Data Source
|
||||
|
||||
- MOCK_SHOPS from lib/constants.ts
|
||||
|
||||
---
|
||||
|
||||
## CSS & Styling
|
||||
|
||||
### Global Styles (app/globals.css)
|
||||
|
||||
**Description:** Root CSS file. Imports Tailwind CSS, defines design tokens (CSS
|
||||
variables), and global styles.
|
||||
|
||||
### Design Tokens (CSS Variables)
|
||||
|
||||
Defined at `:root` for light mode, with dark mode variants:
|
||||
|
||||
#### Colors
|
||||
|
||||
- `--color-primary` - Main brand color (brown)
|
||||
- `--color-primary-dark` - Darker shade for hover
|
||||
- `--color-accent` - Secondary accent color
|
||||
- `--color-bg-*` - Background colors (main, card, sidebar)
|
||||
- `--color-border-*` - Border colors
|
||||
- `--color-text-*` - Text colors (primary, secondary, muted)
|
||||
- `--color-shadow-*` - Shadow colors (sm, md)
|
||||
|
||||
#### Spacing
|
||||
|
||||
- `--spacing-header-height` - Header component height
|
||||
- Standard Tailwind spacing (via Tailwind)
|
||||
|
||||
#### Typography
|
||||
|
||||
- Font stack defined via Google Fonts (Geist)
|
||||
|
||||
### Tailwind Config Integration
|
||||
|
||||
- v4 with custom properties support
|
||||
- Extends with CSS variables
|
||||
- Dark mode: CSS variable override
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Route Groups
|
||||
|
||||
- `(main)` and `(feed)` are route groups (don't appear in URL)
|
||||
- Allow different layouts/providers per route group
|
||||
- Used for organization and flexibility
|
||||
|
||||
### SSR vs Client Components
|
||||
|
||||
- Most pages are "use client" (need interactivity)
|
||||
- Context providers must be client components
|
||||
- Layout.tsx can be server component
|
||||
|
||||
### Responsive Design
|
||||
|
||||
- Breakpoints: sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px)
|
||||
- Sidebar width affects grid columns
|
||||
- Mobile: vertical layout, collapsed sidebar
|
||||
- Desktop: horizontal layout, expanded sidebar
|
||||
|
||||
### Navigation
|
||||
|
||||
- Internal links use `next/link`
|
||||
- useRouter() for programmatic navigation
|
||||
- Auth redirects after login/register
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Adding a New Page
|
||||
|
||||
1. Create file under appropriate route group: `app/(main)/new-feature/page.tsx`
|
||||
2. Make it a client component if it needs interactivity: `"use client"`
|
||||
3. Use context hooks as needed (useAuth, useCart, useMenu)
|
||||
4. Export default component
|
||||
5. Add to documentation
|
||||
|
||||
### Adding a New Route Group
|
||||
|
||||
1. Create directory: `app/(group-name)/`
|
||||
2. Create `layout.tsx` if custom layout needed
|
||||
3. Add pages under this group
|
||||
4. Update codebase understanding
|
||||
|
||||
### Styling Pages
|
||||
|
||||
- Use Tailwind classes + CSS variables
|
||||
- Reference design tokens for consistency
|
||||
- Follow responsive patterns from existing pages
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [x] Payment page implementation
|
||||
- [x] Customer review modal (ReviewModal) with 5-star rating + textarea
|
||||
- [ ] Order history/tracking page
|
||||
- [ ] Manager dashboard (menu management, order tracking)
|
||||
- [ ] User profile page
|
||||
- [ ] Cart checkout flow
|
||||
- [ ] Real backend API integration
|
||||
@@ -100,6 +100,7 @@ body {
|
||||
color: var(--color-text-primary);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
+1
-13
@@ -46,19 +46,7 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} flex min-h-screen flex-col antialiased`}
|
||||
>
|
||||
<Providers>
|
||||
{/* Sticky top header */}
|
||||
<Header />
|
||||
|
||||
{/* Page content (grows to fill remaining height) */}
|
||||
<div className="flex-1">{children}</div>
|
||||
|
||||
{/* Footer always at bottom */}
|
||||
<Footer />
|
||||
|
||||
{/* Global floating cart button */}
|
||||
<CartFab />
|
||||
</Providers>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCart } from "@/lib/cart-context";
|
||||
import Link from "next/link";
|
||||
|
||||
const formatPrice = (value: number) =>
|
||||
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
|
||||
|
||||
export default function PaymentPage() {
|
||||
const {
|
||||
items,
|
||||
totalPrice,
|
||||
increaseQty,
|
||||
decreaseQty,
|
||||
removeFromCart,
|
||||
setQuantity,
|
||||
} = useCart();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
|
||||
<div className="flex flex-col gap-6 xl:flex-row">
|
||||
<section className="min-w-0 flex-1">
|
||||
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
|
||||
<div className="border-b border-(--color-border-light) px-4 py-3">
|
||||
<h1 className="text-foreground text-lg font-bold md:text-xl">
|
||||
Trang thanh toán
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="px-4 py-10 text-center text-(--color-text-muted)">
|
||||
Chưa có sản phẩm nào trong giỏ hàng.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-190 text-sm">
|
||||
<thead>
|
||||
<tr className="bg-(--color-border-light)/40 text-left">
|
||||
<th className="px-4 py-3 font-semibold">Tên sản phẩm</th>
|
||||
<th className="px-4 py-3 font-semibold">Giá tiền</th>
|
||||
<th className="px-4 py-3 font-semibold">Mô tả</th>
|
||||
<th className="px-4 py-3 font-semibold">Số lượng</th>
|
||||
<th className="px-4 py-3 text-right font-semibold">
|
||||
Xóa
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="border-t border-(--color-border-light)"
|
||||
>
|
||||
<td className="text-foreground px-4 py-3 font-medium">
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 font-semibold text-(--color-primary)">
|
||||
{formatPrice(item.price)}
|
||||
</td>
|
||||
<td className="max-w-70 px-4 py-3 text-(--color-text-muted)">
|
||||
<p className="line-clamp-2">{item.description}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => decreaseQty(item.id)}
|
||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||
aria-label={`Giảm số lượng ${item.name}`}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
setQuantity(item.id, Number(e.target.value))
|
||||
}
|
||||
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
||||
title="Nhập số lượng"
|
||||
/>
|
||||
<button
|
||||
onClick={() => increaseQty(item.id)}
|
||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||
aria-label={`Tăng số lượng ${item.name}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-500 px-3 py-2 text-white transition-colors hover:bg-red-600"
|
||||
>
|
||||
<i className="fa-solid fa-trash"></i>
|
||||
<span className="hidden lg:inline">
|
||||
Xóa sản phẩm
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="shrink-0 xl:w-85">
|
||||
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
||||
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
|
||||
<span className="text-(--color-text-muted)">Tổng cộng</span>
|
||||
<span className="text-xl font-bold text-(--color-primary)">
|
||||
{formatPrice(totalPrice)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-3 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-money-bill-wave"></i>
|
||||
<span className="hidden lg:inline">Tiền mặt</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-qrcode"></i>
|
||||
<span className="hidden lg:inline">QR Code</span>
|
||||
</button>
|
||||
|
||||
<Link href="/">
|
||||
<button
|
||||
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
|
||||
type="button"
|
||||
>
|
||||
<i className="fa-solid fa-arrow-rotate-left"></i>
|
||||
<span className="hidden lg:inline">Quay về</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -161,3 +161,227 @@ None - reads SHOP_INFO and SOCIAL_LINKS from lib/constants directly.
|
||||
- next/image, next/link
|
||||
- lib/constants: SHOP_INFO, SOCIAL_LINKS
|
||||
- FontAwesome icons
|
||||
|
||||
---
|
||||
|
||||
## CartFab
|
||||
|
||||
**File:** components/CartFab.tsx **Description:** Floating Action Button
|
||||
displaying cart item count. Shows badge with number of items and total price on
|
||||
hover.
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| ------- | ---------- | -------- | --------- | ---------------------------- |
|
||||
| onClick | () => void | no | undefined | Callback when FAB is clicked |
|
||||
|
||||
### Features
|
||||
|
||||
- Displays cart icon with item count badge
|
||||
- Shows total price on hover in tooltip
|
||||
- Sticky position (bottom-right)
|
||||
- Uses cart context to get items and total
|
||||
|
||||
### Styling
|
||||
|
||||
| Element | Key classes |
|
||||
| ---------- | ----------------------------------------------- |
|
||||
| FAB button | fixed bottom-6 right-6, rounded-full, shadow-lg |
|
||||
| Badge | absolute top-0 right-0, red bg, small font |
|
||||
| Tooltip | appears on hover, shows total price |
|
||||
|
||||
### Dependencies
|
||||
|
||||
- lib/cart-context: useCart()
|
||||
- FontAwesome icons
|
||||
|
||||
---
|
||||
|
||||
## ReviewModal
|
||||
|
||||
**File:** components/ReviewModal.tsx
|
||||
**Description:** Modal for customer reviews. Shows a 5-star rating selector and a textarea for written feedback. After submission, displays a thank-you message. Appears on the payment page for customers (via a dedicated "Đánh giá" button) and also opens automatically after a successful payment action.
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| ------- | ---------- | -------- | ------- | ------------------------------------------ |
|
||||
| isOpen | boolean | yes | - | Controls modal visibility |
|
||||
| onClose | () => void | yes | - | Callback to close the modal & reset state |
|
||||
|
||||
### Behavior
|
||||
|
||||
- **Star rating:** 5 interactive stars; hover highlights up to hovered star, click locks selection. Disabled submit button until at least 1 star selected.
|
||||
- **Star labels:** 1 = Rất tệ, 2 = Tệ, 3 = Bình thường, 4 = Tốt, 5 = Xuất sắc
|
||||
- **Textarea:** Optional free-text review (placeholder in Vietnamese).
|
||||
- **Footer buttons:**
|
||||
- "Quay lại" — closes modal without submitting; resets all state.
|
||||
- "Xác nhận" — submits (UI-only); transitions to thank-you state.
|
||||
- **Thank-you state:** Replaces form with "Cảm ơn quý khách" message + close button.
|
||||
- **Backdrop click:** Also closes the modal (same as "Quay lại").
|
||||
- **State reset:** All state (rating, hover, review text, submitted) resets on close.
|
||||
|
||||
### Styling
|
||||
|
||||
| Element | Key classes |
|
||||
| --------------- | ----------------------------------------------------------------------- |
|
||||
| Backdrop | fixed inset-0, bg-black/50 backdrop-blur-sm, z-50 |
|
||||
| Modal panel | max-w-md, rounded-2xl, border --color-border-light, white bg, shadow-xl |
|
||||
| Stars | text-3xl sm:text-4xl, hover:scale-110, active:scale-95 |
|
||||
| Active star | fa-solid fa-star text-yellow-400 |
|
||||
| Inactive star | fa-regular fa-star text-(--color-border) |
|
||||
| Textarea | rounded-xl, focus:ring with --color-primary/20 |
|
||||
| Quay lại button | border --color-border, hover --color-border-light |
|
||||
| Xác nhận button | bg --color-primary, disabled:opacity-50 |
|
||||
| Thank-you icon | fa-solid fa-heart text-(--color-accent), --color-accent-light bg |
|
||||
|
||||
### Responsive
|
||||
|
||||
- Padding: `p-6 sm:p-8` — larger on sm+ screens
|
||||
- Stars: `text-3xl sm:text-4xl`
|
||||
- Modal width: `w-full max-w-md` with `p-4` page padding — works on all screen sizes
|
||||
|
||||
### Dependencies
|
||||
|
||||
- React useState
|
||||
- FontAwesome icons (fa-star, fa-regular fa-star, fa-heart, fa-arrow-left, fa-check)
|
||||
- Tailwind CSS + CSS custom properties from globals.css
|
||||
|
||||
### Usage in Payment Page
|
||||
|
||||
```tsx
|
||||
// Only shown for customers (user.role === "customer")
|
||||
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
||||
|
||||
// Button in payment aside (only visible to customers)
|
||||
<button onClick={() => setIsReviewOpen(true)}>Đánh giá</button>
|
||||
|
||||
// Payment buttons (Tiền mặt / QR Code) also open the modal for customers
|
||||
const handlePayment = () => {
|
||||
if (isCustomer) setIsReviewOpen(true);
|
||||
};
|
||||
|
||||
<ReviewModal isOpen={isReviewOpen} onClose={() => setIsReviewOpen(false)} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Contexts Documentation
|
||||
|
||||
## AuthContext (lib/auth-context.tsx)
|
||||
|
||||
**File:** lib/auth-context.tsx **Description:** Manages user authentication
|
||||
state including login, logout, and registration. Uses localStorage for
|
||||
persistence.
|
||||
|
||||
### Provider Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| -------- | --------------- | -------- | ---------------- |
|
||||
| children | React.ReactNode | yes | Child components |
|
||||
|
||||
### Hook: useAuth()
|
||||
|
||||
Returns `AuthContextType` with:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------------- | ----------------------------------------------- | ------------------------------------------------- |
|
||||
| user | User \| null | Current logged-in user or null |
|
||||
| login | (username: string, password: string) => boolean | Login function; returns success status |
|
||||
| logout | () => void | Logout function; clears user and localStorage |
|
||||
| registerPhone | string \| null | Phone number during registration flow |
|
||||
| setRegisterPhone | (phone: string \| null) => void | Update registerPhone state |
|
||||
| completeRegistration | (phone: string) => void | Complete registration and create customer account |
|
||||
|
||||
### Mock Database
|
||||
|
||||
Pre-configured accounts:
|
||||
|
||||
- Manager: `admin / admin`
|
||||
- Staff: `Nguyễn Văn An / Nguyễn Văn An`, `Trần Thị Bình / Trần Thị Bình`, etc.
|
||||
- Customer: Phone number as username, `user1` as password
|
||||
|
||||
### Storage
|
||||
|
||||
- Key: `coffee-shop-user`
|
||||
- Format: JSON serialized User object
|
||||
|
||||
---
|
||||
|
||||
## CartContext (lib/cart-context.tsx)
|
||||
|
||||
**File:** lib/cart-context.tsx **Description:** Manages shopping cart state with
|
||||
localStorage persistence. Tracks items, quantities, and totals.
|
||||
|
||||
### Provider Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| -------- | --------------- | -------- | ---------------- |
|
||||
| children | React.ReactNode | yes | Child components |
|
||||
|
||||
### Hook: useCart()
|
||||
|
||||
Returns `CartContextValue` with:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------------------------------------- | --------------------------------------------------------- |
|
||||
| items | CartItem[] | Array of items in cart |
|
||||
| totalItems | number | Total quantity of items |
|
||||
| totalPrice | number | Total price in VND |
|
||||
| addToCart | (product: Product) => void | Add or increase product quantity |
|
||||
| increaseQty | (id: number) => void | Increase product quantity by 1 |
|
||||
| decreaseQty | (id: number) => void | Decrease product quantity by 1 (removes if qty reaches 0) |
|
||||
| removeFromCart | (id: number) => void | Remove product from cart |
|
||||
| setQuantity | (id: number, quantity: number) => void | Set exact quantity (removes if 0) |
|
||||
|
||||
### CartItem Interface
|
||||
|
||||
```typescript
|
||||
interface CartItem {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Storage
|
||||
|
||||
- Key: `coffee-shop-cart`
|
||||
- Format: JSON serialized CartItem[]
|
||||
- Auto-loads on mount and auto-saves on change
|
||||
|
||||
---
|
||||
|
||||
## MenuContext (lib/menu-context.tsx)
|
||||
|
||||
**File:** lib/menu-context.tsx **Description:** Provides shared category/menu
|
||||
state across components. Synchronizes Header mobile menu and Navbar sidebar
|
||||
selection.
|
||||
|
||||
### Provider Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| -------- | --------------- | -------- | ---------------- |
|
||||
| children | React.ReactNode | yes | Child components |
|
||||
|
||||
### Hook: useMenu()
|
||||
|
||||
Returns `MenuContextType` with:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------------- | -------------------- | ----------------------------------------------- |
|
||||
| activeCategory | string | Currently selected category id (default: "all") |
|
||||
| setActiveCategory | (id: string) => void | Update active category |
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Sync Navbar sidebar and Header mobile menu category selection
|
||||
- Clear search query when category changes (implemented in main page)
|
||||
- Pass selected category to product filter logic
|
||||
|
||||
### Default Value
|
||||
|
||||
- `activeCategory: "all"` - Show all products by default
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ReviewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
|
||||
const [rating, setRating] = useState(0);
|
||||
const [hovered, setHovered] = useState(0);
|
||||
const [review, setReview] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset state when closing
|
||||
setRating(0);
|
||||
setHovered(0);
|
||||
setReview("");
|
||||
setSubmitted(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="review-modal-title"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-md rounded-2xl border border-(--color-border-light) bg-white p-6 shadow-xl sm:p-8">
|
||||
{submitted ? (
|
||||
/* Thank you state */
|
||||
<div className="flex flex-col items-center gap-4 py-4 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-(--color-accent-light) text-3xl">
|
||||
<i className="fa-solid fa-heart text-(--color-accent)"></i>
|
||||
</div>
|
||||
<h2
|
||||
id="review-modal-title"
|
||||
className="text-xl font-bold text-foreground"
|
||||
>
|
||||
Cảm ơn quý khách
|
||||
</h2>
|
||||
<p className="text-(--color-text-muted)">
|
||||
Chúng tôi trân trọng đánh giá của bạn!
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="mt-2 inline-flex items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-6 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
|
||||
>
|
||||
Đóng
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Review form */
|
||||
<>
|
||||
<h2
|
||||
id="review-modal-title"
|
||||
className="mb-1 text-xl font-bold text-foreground"
|
||||
>
|
||||
Đánh giá của bạn
|
||||
</h2>
|
||||
<p className="mb-5 text-sm text-(--color-text-muted)">
|
||||
Hãy cho chúng tôi biết trải nghiệm của bạn hôm nay
|
||||
</p>
|
||||
|
||||
{/* Star rating */}
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-sm font-medium text-(--color-text-secondary)">
|
||||
Mức độ hài lòng
|
||||
</p>
|
||||
<div
|
||||
className="flex gap-2"
|
||||
role="radiogroup"
|
||||
aria-label="Xếp hạng sao"
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((star) => {
|
||||
const isActive = star <= (hovered || rating);
|
||||
return (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => setRating(star)}
|
||||
onMouseEnter={() => setHovered(star)}
|
||||
onMouseLeave={() => setHovered(0)}
|
||||
aria-label={`${star} sao`}
|
||||
aria-pressed={rating === star}
|
||||
className="text-3xl transition-transform hover:scale-110 active:scale-95 sm:text-4xl"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isActive
|
||||
? "fa-solid fa-star text-yellow-400"
|
||||
: "fa-regular fa-star text-(--color-border)"
|
||||
}
|
||||
></i>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{rating > 0 && (
|
||||
<p className="mt-1.5 text-xs text-(--color-text-muted)">
|
||||
{
|
||||
["", "Rất tệ", "Tệ", "Bình thường", "Tốt", "Xuất sắc"][
|
||||
rating
|
||||
]
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Review textarea */}
|
||||
<div className="mb-6">
|
||||
<label
|
||||
htmlFor="review-text"
|
||||
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
|
||||
>
|
||||
Nhận xét (tùy chọn)
|
||||
</label>
|
||||
<textarea
|
||||
id="review-text"
|
||||
value={review}
|
||||
onChange={(e) => setReview(e.target.value)}
|
||||
placeholder="Chia sẻ cảm nhận của bạn về đồ uống, dịch vụ..."
|
||||
rows={4}
|
||||
className="w-full resize-none rounded-xl border border-(--color-border) bg-transparent px-3 py-2.5 text-sm text-foreground placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:outline-none focus:ring-2 focus:ring-(--color-primary)/20 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer buttons */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 rounded-xl border border-(--color-border) px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-(--color-border-light)"
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left"></i>
|
||||
Quay lại
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={rating === 0}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-(--color-primary-dark) disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<i className="fa-solid fa-check"></i>
|
||||
Xác nhận
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+34
-19
@@ -1,32 +1,47 @@
|
||||
# --- Giai đoạn 1: Build ---
|
||||
FROM node:25-alpine AS builder
|
||||
# --- Giai đoạn 1: Dependencies ---
|
||||
FROM node:25-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Cài đặt pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy file định nghĩa package
|
||||
# Copy file định nghĩa package để tận dụng cache của Docker
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Cài đặt dependencies (sử dụng --frozen-lockfile để đảm bảo đúng phiên bản)
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
|
||||
# Copy toàn bộ code
|
||||
# --- Giai đoạn 2: Builder ---
|
||||
FROM node:25-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Build Next.js (Yêu cầu next.config.js có output: 'export')
|
||||
RUN pnpm run build
|
||||
# Tắt dữ liệu thu thập của Next.js trong quá trình build
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# --- Giai đoạn 2: Run (Sản phẩm cuối) ---
|
||||
FROM nginx:alpine
|
||||
RUN npm run build
|
||||
|
||||
# Xóa file mặc định của nginx (tùy chọn nhưng nên làm)
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
# --- Giai đoạn 3: Runner (Sản phẩm cuối) ---
|
||||
FROM node:25-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Copy folder out từ giai đoạn builder
|
||||
COPY --from=builder /app/out /usr/share/nginx/html
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
EXPOSE 80
|
||||
# Tạo user để chạy app (bảo mật hơn chạy root)
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# Copy các file cần thiết từ builder
|
||||
# Standalone chỉ copy code cần chạy, bỏ qua devDependencies và file thừa
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
|
||||
# Chạy bằng server.js được sinh ra bởi output: 'standalone'
|
||||
CMD ["node", "server.js"]
|
||||
+13
-10
@@ -14,29 +14,32 @@ spec:
|
||||
labels:
|
||||
app: web
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
containers:
|
||||
- name: frontend-container
|
||||
image: git.demonkernel.io.vn/foodsurf/frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
resources:
|
||||
limits:
|
||||
cpu: "50m"
|
||||
memory: "32Mi"
|
||||
cpu: "200m"
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "10m"
|
||||
memory: "16Mi"
|
||||
cpu: "50m"
|
||||
memory: "128Mi"
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-web-service
|
||||
name: drinkool-frontend-svc
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: web
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
nodePort: 30080
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
nodePort: 30080
|
||||
@@ -0,0 +1,515 @@
|
||||
# Layouts Documentation
|
||||
|
||||
> Tài liệu chi tiết về layout components (Header, Footer) và responsive behavior
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The `layouts/` directory contains layout-level components used across all pages.
|
||||
These are included in the root layout and provide consistent header/footer
|
||||
across the application.
|
||||
|
||||
```
|
||||
layouts/
|
||||
├── header.tsx # Sticky top header with shop branding and auth
|
||||
├── footer.tsx # Footer with shop info, social links, WiFi details
|
||||
└── LAYOUTS.md # (this file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Header (layouts/header.tsx)
|
||||
|
||||
**File:** layouts/header.tsx **Type:** Client component **Description:** Sticky
|
||||
top navigation bar with brand info and authentication status display.
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ [Logo] [Shop Name] [Tagline] │ [Auth Status] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
height: 72px (var(--spacing-header-height))
|
||||
```
|
||||
|
||||
**2-Column Layout:**
|
||||
|
||||
- **Left:** Shop branding (logo + name + tagline)
|
||||
- **Right:** Auth status indicator
|
||||
|
||||
### Features
|
||||
|
||||
#### Logo & Branding
|
||||
|
||||
- Shop logo (40x40px)
|
||||
- Shop name (bold text)
|
||||
- Tagline (hidden on mobile < md, visible md+)
|
||||
|
||||
**Responsive:** | Breakpoint | Logo | Name | Tagline |
|
||||
|-----------|------|------|---------| | < sm (640px) | Yes | Yes | Hidden | |
|
||||
sm-md | Yes | Yes | Hidden | | md+ (768px) | Yes | Yes | Visible |
|
||||
|
||||
#### Authentication Status
|
||||
|
||||
The header cycles through 3 auth states (for UI demo):
|
||||
|
||||
**State 1: Guest (Not logged in)**
|
||||
|
||||
```
|
||||
[brown button] "Dang nhap"
|
||||
```
|
||||
|
||||
- Guest state (user = null)
|
||||
- Brown primary button
|
||||
- Click: toggles to next state
|
||||
|
||||
**State 2: Manager**
|
||||
|
||||
```
|
||||
[caramel/gold badge] "👔 Quản lý"
|
||||
```
|
||||
|
||||
- Shows manager role
|
||||
- Gold/caramel colored badge
|
||||
- User tie icon
|
||||
- Name: Nguyễn Văn An
|
||||
|
||||
**State 3: Staff**
|
||||
|
||||
```
|
||||
[border button] "👤 [Name]"
|
||||
```
|
||||
|
||||
- Shows staff role with avatar placeholder
|
||||
- Bordered button style
|
||||
- Name: Trần Thị Bình
|
||||
- Click: toggles back to Guest state
|
||||
|
||||
### Responsive Behavior
|
||||
|
||||
| Breakpoint | Layout |
|
||||
| ------------- | ------------------------------- |
|
||||
| < sm | Stacked vertically |
|
||||
| sm-md (640px) | Side-by-side, reduced spacing |
|
||||
| md+ (768px) | Full width, comfortable spacing |
|
||||
|
||||
**Mobile (<640px):**
|
||||
|
||||
- Logo smaller
|
||||
- Name only (no tagline)
|
||||
- Auth button text hidden, icon shown
|
||||
- Touch-friendly padding
|
||||
|
||||
**Desktop (≥640px):**
|
||||
|
||||
- Normal logo size
|
||||
- Full layout with tagline
|
||||
- Button with text and icon
|
||||
- Comfortable spacing
|
||||
|
||||
### Styling
|
||||
|
||||
| Element | Classes |
|
||||
| -------------------- | --------------------------------------------------------- |
|
||||
| Header wrapper | `sticky top-0 z-50 bg-white shadow-md` |
|
||||
| Container | `max-w-7xl mx-auto px-4 md:px-6 h-72px flex items-center` |
|
||||
| Left section (brand) | `flex items-center gap-3 flex-1` |
|
||||
| Right section (auth) | `flex items-center gap-3 shrink-0` |
|
||||
| Logo image | `w-10 h-10 md:w-12 md:h-12` |
|
||||
| Brand text | `font-bold text-sm md:text-base` |
|
||||
| Tagline | `hidden md:block text-xs text-muted-foreground` |
|
||||
| Auth button | `px-4 py-2 rounded-lg text-sm font-medium` |
|
||||
|
||||
### CSS Variables Used
|
||||
|
||||
```css
|
||||
--spacing-header-height: 72px;
|
||||
--color-primary: brown;
|
||||
--color-primary-dark: darker brown;
|
||||
--color-text-primary: text color;
|
||||
--color-text-muted: gray text;
|
||||
--color-shadow-sm: subtle shadow;
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **next/image** - Logo image rendering
|
||||
- **next/link** - Navigation links
|
||||
- **lib/constants:** SHOP_INFO, MOCK_USERS
|
||||
- **lib/types:** User
|
||||
- **FontAwesome icons:** fa-sign-in-alt, fa-sign-out-alt, fa-user-tie,
|
||||
fa-user-circle
|
||||
|
||||
### Key Code Snippets
|
||||
|
||||
**Auth Demo State Machine:**
|
||||
|
||||
```typescript
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
const handleAuthClick = () => {
|
||||
if (!user) {
|
||||
setUser(MOCK_USERS.manager);
|
||||
} else if (user.role === "manager") {
|
||||
setUser(MOCK_USERS.staff);
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Responsive Brand Text:**
|
||||
|
||||
```typescript
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm md:text-base">{SHOP_INFO.name}</span>
|
||||
<span className="hidden md:block text-xs text-muted-foreground">
|
||||
{SHOP_INFO.tagline}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Footer (layouts/footer.tsx)
|
||||
|
||||
**File:** layouts/footer.tsx **Type:** Client component (uses constants)
|
||||
**Description:** Site footer with shop information, social links, and WiFi
|
||||
details.
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ [Brand Info] │ [Social + WiFi] │
|
||||
├────────────────────────────────────────────┤
|
||||
│ © 2024 Coffee Shop. Made with ❤️ in VN │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**12-Column Grid Layout:**
|
||||
|
||||
| Breakpoint | Brand Info | Social + WiFi |
|
||||
| ------------ | ----------- | ------------- |
|
||||
| Mobile | col-span-12 | col-span-12 |
|
||||
| md (768px) | col-span-6 | col-span-6 |
|
||||
| lg+ (1024px) | col-span-6 | col-span-6 |
|
||||
|
||||
### Sections
|
||||
|
||||
#### 1. Brand Info (Left Section)
|
||||
|
||||
**Content:**
|
||||
|
||||
- Shop logo (40x40px)
|
||||
- Shop name (bold)
|
||||
- Tagline (gray text)
|
||||
- Address with location icon
|
||||
- Phone with phone icon
|
||||
- Email with envelope icon
|
||||
- Open hours with clock icon
|
||||
|
||||
**Responsive:**
|
||||
|
||||
```
|
||||
Mobile:
|
||||
└─ Logo
|
||||
└─ Name & Tagline (stacked)
|
||||
└─ Address
|
||||
└─ Phone
|
||||
└─ Email
|
||||
└─ Open Hours
|
||||
|
||||
Desktop:
|
||||
└─ Logo + Name/Tagline (flexbox, side-by-side)
|
||||
└─ Address, Phone, Email, Hours (stacked below)
|
||||
```
|
||||
|
||||
#### 2. Social Links (Top Right)
|
||||
|
||||
**Links:**
|
||||
|
||||
- Facebook icon → SHOP_INFO.facebook
|
||||
- TikTok icon → SHOP_INFO.tiktok
|
||||
- Website icon → SHOP_INFO.website
|
||||
|
||||
**Styling:**
|
||||
|
||||
- Icons in circle backgrounds
|
||||
- Hover effect (color change)
|
||||
- Flex row layout
|
||||
- Centered alignment
|
||||
|
||||
#### 3. WiFi Card (Bottom Right)
|
||||
|
||||
**Content:**
|
||||
|
||||
- "📶 WiFi miễn phí" label
|
||||
- Network name (monospace)
|
||||
- Password (monospace, can be hidden/shown)
|
||||
|
||||
**Styling:**
|
||||
|
||||
- Light gray background
|
||||
- Rounded borders
|
||||
- Monospace font for credentials
|
||||
- Eye icon toggle for password visibility
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
📶 WiFi miễn phí
|
||||
┌────────────────────────┐
|
||||
│ Network: CoffeeShop │
|
||||
│ Password: •••••••• │
|
||||
│ (eye icon) │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4. Copyright Bar (Bottom)
|
||||
|
||||
**Content:**
|
||||
|
||||
- Copyright text: "© 2024 Coffee Shop"
|
||||
- "Made with ❤️ in Vietnam"
|
||||
- Centered
|
||||
|
||||
**Styling:**
|
||||
|
||||
- Small text
|
||||
- Gray color
|
||||
- Subtle separator line above
|
||||
|
||||
### Responsive Behavior
|
||||
|
||||
**Mobile (<768px):**
|
||||
|
||||
- Single column layout
|
||||
- Stacked sections
|
||||
- Full width
|
||||
- Padding around edges
|
||||
|
||||
**Tablet (768px):**
|
||||
|
||||
- 2-column layout
|
||||
- Brand info left, Social + WiFi right
|
||||
- Equal width columns
|
||||
|
||||
**Desktop (≥768px):**
|
||||
|
||||
- 2-column layout with more spacing
|
||||
- Brand info slightly smaller
|
||||
- Comfortable padding
|
||||
|
||||
### Styling
|
||||
|
||||
| Element | Classes |
|
||||
| -------------- | --------------------------------------------------------- |
|
||||
| Footer wrapper | `bg-gray-100 border-t` |
|
||||
| Container | `max-w-7xl mx-auto px-4 py-8` |
|
||||
| Grid | `grid grid-cols-12 gap-8` |
|
||||
| Brand section | `col-span-12 md:col-span-6` |
|
||||
| Social section | `col-span-12 md:col-span-6` |
|
||||
| Logo | `w-10 h-10` |
|
||||
| Info text | `text-sm text-gray-600` |
|
||||
| Social icons | `w-10 h-10 rounded-full flex items-center justify-center` |
|
||||
| WiFi card | `bg-white p-4 rounded-lg border` |
|
||||
| Copyright bar | `border-t mt-8 pt-6 text-center text-xs text-gray-500` |
|
||||
|
||||
### CSS Variables Used
|
||||
|
||||
```css
|
||||
--color-primary: brand color;
|
||||
--color-text-secondary: secondary text color;
|
||||
--color-text-muted: muted/gray text color;
|
||||
--color-accent: accent color;
|
||||
--color-border: border color;
|
||||
--color-bg-*: background colors;
|
||||
--color-shadow-sm: subtle shadow;
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **next/image** - Shop logo
|
||||
- **next/link** - Social links
|
||||
- **lib/constants:** SHOP_INFO, SOCIAL_LINKS
|
||||
- **FontAwesome icons:** location-dot, phone, envelope, clock, facebook, tiktok,
|
||||
globe, wifi, eye, eye-slash
|
||||
|
||||
### Key Code Snippets
|
||||
|
||||
**Brand Info Section:**
|
||||
|
||||
```typescript
|
||||
<div className="flex gap-4">
|
||||
<Image src={SHOP_INFO.logo} alt={SHOP_INFO.name} width={40} height={40} />
|
||||
<div>
|
||||
<h3 className="font-bold">{SHOP_INFO.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{SHOP_INFO.tagline}</p>
|
||||
<p className="text-sm mt-2">{SHOP_INFO.address}</p>
|
||||
{/* etc */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Social Links:**
|
||||
|
||||
```typescript
|
||||
<div className="flex gap-3">
|
||||
<Link href={SOCIAL_LINKS.facebook} target="_blank" rel="noopener">
|
||||
<i className="fab fa-facebook" />
|
||||
</Link>
|
||||
{/* etc */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**WiFi Display:**
|
||||
|
||||
```typescript
|
||||
<div className="bg-white p-4 rounded-lg">
|
||||
<p className="font-semibold text-sm">📶 WiFi miễn phí</p>
|
||||
<p className="font-mono text-xs mt-2">{SHOP_INFO.wifi.name}</p>
|
||||
<p className="font-mono text-xs">
|
||||
{showPassword ? SHOP_INFO.wifi.password : "••••••••"}
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration in Root Layout
|
||||
|
||||
**File:** app/layout.tsx
|
||||
|
||||
```typescript
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
|
||||
- Header: Always at top (sticky)
|
||||
- Content: Takes full width between header/footer
|
||||
- Footer: Always at bottom
|
||||
|
||||
---
|
||||
|
||||
## Responsive Design Patterns
|
||||
|
||||
### Header Responsive Pattern
|
||||
|
||||
```css
|
||||
/* Mobile first */
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Tablet+ */
|
||||
@media (min-width: 640px) {
|
||||
.header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop+ */
|
||||
@media (min-width: 768px) {
|
||||
.header {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
display: block; /* Show tagline on desktop */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Footer Responsive Pattern
|
||||
|
||||
```css
|
||||
/* Mobile: stacked */
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Tablet: 2 columns */
|
||||
@media (min-width: 768px) {
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode Support
|
||||
|
||||
CSS variables are set up for dark mode support. To enable:
|
||||
|
||||
1. Add dark mode CSS variable overrides:
|
||||
|
||||
```css
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg-main: #1a1a1a;
|
||||
--color-text-primary: #ffffff;
|
||||
/* etc */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Or use Tailwind dark class:
|
||||
|
||||
```html
|
||||
<div className="dark:bg-gray-900 dark:text-white">Content</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Header
|
||||
|
||||
- Logo has alt text
|
||||
- Auth button has aria-label
|
||||
- Icons have semantic meaning
|
||||
- Good contrast ratios
|
||||
|
||||
### Footer
|
||||
|
||||
- Headings use semantic tags
|
||||
- Links have descriptive text
|
||||
- Icons are decorative (aria-hidden)
|
||||
- Monospace font for technical info (WiFi credentials)
|
||||
|
||||
### General
|
||||
|
||||
- Touch targets ≥ 48px on mobile
|
||||
- Sufficient color contrast
|
||||
- Semantic HTML structure
|
||||
- ARIA labels where needed
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep layouts simple:** Avoid complex nested layouts
|
||||
2. **Responsive first:** Use mobile-first CSS approach
|
||||
3. **Reuse components:** Use Header/Footer across all pages
|
||||
4. **Props over hardcoding:** Use lib/constants for data
|
||||
5. **Type safety:** Use TypeScript for component props
|
||||
6. **Performance:** Optimize images with next/image
|
||||
7. **Accessibility:** Add ARIA labels and semantic HTML
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
# Lib Documentation
|
||||
|
||||
> Tài liệu chi tiết về utilities, contexts, types, và constants
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The `lib/` directory contains shared logic, data models, and context providers
|
||||
used throughout the application.
|
||||
|
||||
```
|
||||
lib/
|
||||
├── types.ts # TypeScript interfaces and types
|
||||
├── constants.ts # Mock data (products, shops, users, info)
|
||||
├── auth-context.tsx # Authentication context & provider
|
||||
├── cart-context.tsx # Shopping cart context & provider
|
||||
├── menu-context.tsx # Menu/category context & provider
|
||||
└── LIB.md # (this file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Types (lib/types.ts)
|
||||
|
||||
**Description:** Central TypeScript definitions used throughout the application.
|
||||
|
||||
### User Type
|
||||
|
||||
```typescript
|
||||
type UserRole = "manager" | "staff" | "customer";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
avatar: string | null;
|
||||
phone?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Authentication context, header component, role-based UI
|
||||
|
||||
**Roles:**
|
||||
|
||||
- **manager** - Shop owner, full access to menu management and orders
|
||||
- **staff** - Staff member, can view/process orders
|
||||
- **customer** - Regular customer, can browse and order
|
||||
|
||||
---
|
||||
|
||||
### Menu Types
|
||||
|
||||
```typescript
|
||||
interface MenuCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string; // FontAwesome class e.g. "fa-solid fa-mug-hot"
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Sidebar navigation, category filter, product display
|
||||
|
||||
**Example Categories:**
|
||||
|
||||
- `all` - All products
|
||||
- `cafe` - Coffee
|
||||
- `tra` - Tea
|
||||
- `sua-chua` - Yogurt
|
||||
- `nuoc-ep` - Juice
|
||||
- `latte` - Latte drinks
|
||||
- `giai-khat` - Snacks
|
||||
- `topping` - Toppings
|
||||
|
||||
---
|
||||
|
||||
### Product Type
|
||||
|
||||
```typescript
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string; // matches MenuCategory.id
|
||||
price: number;
|
||||
image: string;
|
||||
description: string;
|
||||
available?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Product grid display, cart items, filtering
|
||||
|
||||
**Key Fields:**
|
||||
|
||||
- `id` - Unique product identifier
|
||||
- `name` - Display name in Vietnamese
|
||||
- `category` - Category ID for filtering
|
||||
- `price` - Price in VND
|
||||
- `image` - URL or path to product image
|
||||
- `description` - Short 1-2 sentence description
|
||||
- `available` - If `false`, product won't show (defaults to `true`)
|
||||
|
||||
---
|
||||
|
||||
### Shop Info Type
|
||||
|
||||
```typescript
|
||||
interface WifiInfo {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface ShopInfo {
|
||||
name: string;
|
||||
tagline: string;
|
||||
logo: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
managerPhone: string;
|
||||
email: string;
|
||||
wifi: WifiInfo;
|
||||
openHours: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Header, footer, login page display
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const SHOP_INFO: ShopInfo = {
|
||||
name: "Coffee Shop",
|
||||
tagline: "Hương vị đậm đà – Khoảnh khắc thư giãn",
|
||||
logo: "/imgs/logo.png",
|
||||
address: "123 Đường Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
|
||||
phone: "0901 234 567",
|
||||
managerPhone: "0912 345 678",
|
||||
email: "contact@coffeeshop.vn",
|
||||
wifi: { name: "CoffeeShop_Free", password: "coffee2024" },
|
||||
openHours: "07:00 – 22:00 (Thứ 2 – Chủ nhật)",
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Social Links Type
|
||||
|
||||
```typescript
|
||||
interface SocialLinks {
|
||||
facebook: string;
|
||||
tiktok: string;
|
||||
website: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Footer component
|
||||
|
||||
---
|
||||
|
||||
### Shop (Discovery) Type
|
||||
|
||||
```typescript
|
||||
interface Shop {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
image: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:** Feed page - list of coffee shops to discover
|
||||
|
||||
---
|
||||
|
||||
## Constants (lib/constants.ts)
|
||||
|
||||
**Description:** Mock data used throughout the application. Replace with API
|
||||
calls when backend is ready.
|
||||
|
||||
### SHOP_INFO
|
||||
|
||||
Single instance of shop information displayed in header, footer, and login page.
|
||||
|
||||
**Key Info Used:**
|
||||
|
||||
- Header: name, logo, phone
|
||||
- Footer: all fields
|
||||
- Login: logo, name, email
|
||||
|
||||
---
|
||||
|
||||
### SOCIAL_LINKS
|
||||
|
||||
Social media links for footer component.
|
||||
|
||||
```typescript
|
||||
export const SOCIAL_LINKS: SocialLinks = {
|
||||
facebook: "https://facebook.com/coffeeshop",
|
||||
tiktok: "https://tiktok.com/@coffeeshop",
|
||||
website: "/",
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MENU_CATEGORIES
|
||||
|
||||
Array of 8 product categories. Used in sidebar, mobile menu, and product
|
||||
filtering.
|
||||
|
||||
```typescript
|
||||
export const MENU_CATEGORIES: MenuCategory[] = [
|
||||
{ id: "all", name: "Tất cả", icon: "fa-solid fa-border-all" },
|
||||
{ id: "cafe", name: "Cà Phê", icon: "fa-solid fa-mug-hot" },
|
||||
{ id: "tra", name: "Trà", icon: "fa-solid fa-leaf" },
|
||||
{ id: "sua-chua", name: "Sữa Chua", icon: "fa-solid fa-jar" },
|
||||
{ id: "nuoc-ep", name: "Nước Ép", icon: "fa-solid fa-blender" },
|
||||
{ id: "latte", name: "Latte", icon: "fa-solid fa-mug-saucer" },
|
||||
{
|
||||
id: "giai-khat",
|
||||
name: "Giải Khát / Ăn Vặt",
|
||||
icon: "fa-solid fa-ice-cream",
|
||||
},
|
||||
{ id: "topping", name: "Topping", icon: "fa-solid fa-layer-group" },
|
||||
];
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
- Navbar sidebar category buttons
|
||||
- Header mobile scrollable menu
|
||||
- Product grid category filter
|
||||
|
||||
---
|
||||
|
||||
### MOCK_PRODUCTS
|
||||
|
||||
Array of 18 mock product items across all categories.
|
||||
|
||||
**Example Product:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 1,
|
||||
name: "Cà Phê Đen",
|
||||
category: "cafe",
|
||||
price: 25000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Cà phê đen truyền thống, đậm đà hương vị Việt Nam, pha phin thủ công.",
|
||||
available: true,
|
||||
}
|
||||
```
|
||||
|
||||
**Product Distribution:**
|
||||
|
||||
- Café: 4 items (Đen, Sữa, Bạc Xỉu, Trứng)
|
||||
- Tea: 3 items (Đào Cam Sả, Matcha, Vải Hoa Nhài)
|
||||
- Yogurt: 2 items (Trân Châu, Dâu)
|
||||
- Juice: 2 items (Cam, Dưa Hấu)
|
||||
- Latte: 2 items (Caramel, Vanilla)
|
||||
- Snacks: 2 items (Bánh Mì, Bánh Flan)
|
||||
- Toppings: 3 items (Trân Châu Đen, Thạch, Trân Châu Trắng)
|
||||
|
||||
**Price Range:** 10,000 - 45,000 VND
|
||||
|
||||
---
|
||||
|
||||
### MOCK_SHOPS
|
||||
|
||||
Array of 5 coffee shops for the feed/discovery page.
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 1,
|
||||
name: "The Coffee House",
|
||||
address: "86 Cao Thắng, Quận 3, TP. Hồ Chí Minh",
|
||||
image: "https://images.unsplash.com/...", // Unsplash URL
|
||||
}
|
||||
```
|
||||
|
||||
**Shops:**
|
||||
|
||||
1. The Coffee House - Quận 3
|
||||
2. Highlands Coffee - Quận 1
|
||||
3. Phúc Long Heritage - Quận 1
|
||||
4. Katinat Saigon Kafe - Quận 1
|
||||
5. Trung Nguyên E-Coffee - Quận 1
|
||||
|
||||
**Usage:** Feed page grid display
|
||||
|
||||
---
|
||||
|
||||
### MOCK_USERS
|
||||
|
||||
Pre-configured user accounts for authentication demo.
|
||||
|
||||
```typescript
|
||||
export const MOCK_USERS: Record<string, User> = {
|
||||
manager: {
|
||||
id: 1,
|
||||
name: "Nguyễn Văn An",
|
||||
role: "manager",
|
||||
avatar: null,
|
||||
},
|
||||
staff: {
|
||||
id: 2,
|
||||
name: "Trần Thị Bình",
|
||||
role: "staff",
|
||||
avatar: null,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:** AuthContext for login demo
|
||||
|
||||
**Note:** More accounts defined in `auth-context.tsx` MOCK_AUTH_DB
|
||||
|
||||
---
|
||||
|
||||
## Auth Context (lib/auth-context.tsx)
|
||||
|
||||
See [COMPONENTS.md - AuthContext](../components/COMPONENTS.md#authcontext) for
|
||||
detailed documentation.
|
||||
|
||||
**Quick Summary:**
|
||||
|
||||
- Manages user login/logout/register
|
||||
- Persists user in localStorage
|
||||
- Provides user info to components via useAuth() hook
|
||||
|
||||
---
|
||||
|
||||
## Cart Context (lib/cart-context.tsx)
|
||||
|
||||
See [COMPONENTS.md - CartContext](../components/COMPONENTS.md#cartcontext) for
|
||||
detailed documentation.
|
||||
|
||||
**Quick Summary:**
|
||||
|
||||
- Manages shopping cart items and quantities
|
||||
- Persists cart in localStorage
|
||||
- Provides cart operations via useCart() hook
|
||||
|
||||
---
|
||||
|
||||
## Menu Context (lib/menu-context.tsx)
|
||||
|
||||
See [COMPONENTS.md - MenuContext](../components/COMPONENTS.md#menucontext) for
|
||||
detailed documentation.
|
||||
|
||||
**Quick Summary:**
|
||||
|
||||
- Shares active category state
|
||||
- Syncs sidebar and mobile menu selection
|
||||
- Provides activeCategory and setActiveCategory via useMenu() hook
|
||||
|
||||
---
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### Using Types
|
||||
|
||||
```typescript
|
||||
import type { MenuCategory, Product, User } from "@/lib/types";
|
||||
```
|
||||
|
||||
### Using Constants
|
||||
|
||||
```typescript
|
||||
import { MENU_CATEGORIES, MOCK_PRODUCTS, SHOP_INFO } from "@/lib/constants";
|
||||
|
||||
// In a component:
|
||||
const products = MOCK_PRODUCTS.filter((p) => p.category === "cafe");
|
||||
const shopName = SHOP_INFO.name;
|
||||
```
|
||||
|
||||
### Using Auth Context
|
||||
|
||||
```typescript
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
|
||||
export default function MyComponent() {
|
||||
const { user, login, logout } = useAuth();
|
||||
|
||||
if (!user) return <p>Not logged in</p>;
|
||||
return <p>Welcome, {user.name}!</p>;
|
||||
}
|
||||
```
|
||||
|
||||
### Using Cart Context
|
||||
|
||||
```typescript
|
||||
import { useCart } from "@/lib/cart-context";
|
||||
import type { Product } from "@/lib/types";
|
||||
|
||||
export default function ProductCard({ product }: { product: Product }) {
|
||||
const { addToCart } = useCart();
|
||||
|
||||
return (
|
||||
<button onClick={() => addToCart(product)}>
|
||||
Add to Cart
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Menu Context
|
||||
|
||||
```typescript
|
||||
import { useMenu } from "@/lib/menu-context";
|
||||
import { MENU_CATEGORIES } from "@/lib/constants";
|
||||
|
||||
export default function CategoryFilter() {
|
||||
const { activeCategory, setActiveCategory } = useMenu();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{MENU_CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
className={activeCategory === cat.id ? "active" : ""}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow & Updates
|
||||
|
||||
### When to Update Constants
|
||||
|
||||
1. **Product List:** When adding/removing products
|
||||
- Update MOCK_PRODUCTS array
|
||||
- Or replace with API call in the future
|
||||
|
||||
2. **Categories:** When adding new product categories
|
||||
- Update MENU_CATEGORIES array
|
||||
- Update product category references
|
||||
|
||||
3. **Shop Info:** When shop details change
|
||||
- Update SHOP_INFO object
|
||||
- Automatically reflects in header/footer
|
||||
|
||||
4. **Social Links:** When social handles change
|
||||
- Update SOCIAL_LINKS object
|
||||
|
||||
### Future API Integration
|
||||
|
||||
When backend is ready:
|
||||
|
||||
```typescript
|
||||
// Replace MOCK_PRODUCTS with:
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
useEffect(() => {
|
||||
fetch("/api/products")
|
||||
.then((r) => r.json())
|
||||
.then(setProducts);
|
||||
}, []);
|
||||
|
||||
// Replace MOCK_USERS with:
|
||||
const login = async (username: string, password: string) => {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
return res.ok;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Import Types:** Always use `import type` for types to avoid runtime bloat
|
||||
2. **Immutability:** Contexts return functions to update state, not direct
|
||||
mutations
|
||||
3. **localStorage:** Keys are centralized (`coffee-shop-user`,
|
||||
`coffee-shop-cart`)
|
||||
4. **Validation:** Contexts validate/sanitize data (e.g., filter invalid cart
|
||||
items)
|
||||
5. **Default Values:** Contexts have sensible defaults (activeCategory: "all")
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
MenuCategory,
|
||||
Product,
|
||||
Shop,
|
||||
ShopInfo,
|
||||
SocialLinks,
|
||||
User,
|
||||
@@ -231,6 +232,45 @@ export const MOCK_PRODUCTS: Product[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// ===== MOCK SHOPS (for Feed page) =====
|
||||
export const MOCK_SHOPS: Shop[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "The Coffee House",
|
||||
address: "86 Cao Thắng, Quận 3, TP. Hồ Chí Minh",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=600&h=400&fit=crop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Highlands Coffee",
|
||||
address: "123 Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1559305616-3f99cd43e353?w=600&h=400&fit=crop",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Phúc Long Heritage",
|
||||
address: "42 Lê Lợi, Quận 1, TP. Hồ Chí Minh",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=600&h=400&fit=crop",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Katinat Saigon Kafe",
|
||||
address: "26 Lý Tự Trọng, Quận 1, TP. Hồ Chí Minh",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=600&h=400&fit=crop",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Trung Nguyên E-Coffee",
|
||||
address: "15 Hai Bà Trưng, Quận 1, TP. Hồ Chí Minh",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1453614512568-c4024d13c247?w=600&h=400&fit=crop",
|
||||
},
|
||||
];
|
||||
|
||||
// ===== MOCK USERS (for UI demo – replace with real auth) =====
|
||||
export const MOCK_USERS: Record<string, User> = {
|
||||
manager: {
|
||||
|
||||
@@ -51,3 +51,11 @@ export interface SocialLinks {
|
||||
tiktok: string;
|
||||
website: string;
|
||||
}
|
||||
|
||||
// ===== SHOP (QUÁN NƯỚC) TYPES =====
|
||||
export interface Shop {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
+7
-2
@@ -10,9 +10,14 @@ import type { NextConfig } from "next";
|
||||
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
|
||||
*/
|
||||
const nextConfig: NextConfig = {
|
||||
output: "export", // Bắt buộc để tạo ra thư mục /out
|
||||
output: "standalone",
|
||||
images: {
|
||||
unoptimized: true, // Thường cần thiết cho static export
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "images.unsplash.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Generated
+6704
-267
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp",
|
||||
"version": "1.0.0-created-login-and-register-page.1",
|
||||
"version": "1.0.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -11,20 +11,19 @@
|
||||
"release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/react": "^19.2.14",
|
||||
"next": "16.1.7",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind": "^4.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "^20.19.37",
|
||||
"@types/react": "^19.2.14",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
"@semantic-release/exec": "^7.1.0",
|
||||
"@semantic-release/github": "^12.0.6",
|
||||
"@semantic-release/npm": "^13.1.5",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
Generated
+126
-120
@@ -20,6 +20,9 @@ importers:
|
||||
next:
|
||||
specifier: 16.1.7
|
||||
version: 16.1.7(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
php:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
react:
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3
|
||||
@@ -42,9 +45,6 @@ importers:
|
||||
'@semantic-release/exec':
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0(semantic-release@25.0.3(typescript@5.9.3))
|
||||
'@semantic-release/github':
|
||||
specifier: ^12.0.6
|
||||
version: 12.0.6(semantic-release@25.0.3(typescript@5.9.3))
|
||||
'@semantic-release/npm':
|
||||
specifier: ^13.1.5
|
||||
version: 13.1.5(semantic-release@25.0.3(typescript@5.9.3))
|
||||
@@ -59,7 +59,7 @@ importers:
|
||||
version: 9.39.4(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 16.1.7
|
||||
version: 16.1.7(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
version: 16.1.7(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
@@ -769,63 +769,63 @@ packages:
|
||||
'@types/responselike@1.0.3':
|
||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.57.2':
|
||||
resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==}
|
||||
'@typescript-eslint/eslint-plugin@8.58.0':
|
||||
resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^8.57.2
|
||||
'@typescript-eslint/parser': ^8.58.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/parser@8.57.2':
|
||||
resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==}
|
||||
'@typescript-eslint/parser@8.58.0':
|
||||
resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/project-service@8.57.2':
|
||||
resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==}
|
||||
'@typescript-eslint/project-service@8.58.0':
|
||||
resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/scope-manager@8.57.2':
|
||||
resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==}
|
||||
'@typescript-eslint/scope-manager@8.58.0':
|
||||
resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.57.2':
|
||||
resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==}
|
||||
'@typescript-eslint/tsconfig-utils@8.58.0':
|
||||
resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/type-utils@8.57.2':
|
||||
resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==}
|
||||
'@typescript-eslint/type-utils@8.58.0':
|
||||
resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/types@8.57.2':
|
||||
resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==}
|
||||
'@typescript-eslint/types@8.58.0':
|
||||
resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.57.2':
|
||||
resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==}
|
||||
'@typescript-eslint/typescript-estree@8.58.0':
|
||||
resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/utils@8.57.2':
|
||||
resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==}
|
||||
'@typescript-eslint/utils@8.58.0':
|
||||
resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.57.2':
|
||||
resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==}
|
||||
'@typescript-eslint/visitor-keys@8.58.0':
|
||||
resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
@@ -1095,8 +1095,8 @@ packages:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
baseline-browser-mapping@2.10.11:
|
||||
resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==}
|
||||
baseline-browser-mapping@2.10.12:
|
||||
resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1121,11 +1121,11 @@ packages:
|
||||
bottleneck@2.19.5:
|
||||
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
brace-expansion@1.1.13:
|
||||
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
brace-expansion@2.0.3:
|
||||
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
|
||||
|
||||
brace-expansion@5.0.5:
|
||||
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
|
||||
@@ -1174,8 +1174,8 @@ packages:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001781:
|
||||
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
|
||||
caniuse-lite@1.0.30001782:
|
||||
resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==}
|
||||
|
||||
chalk@2.4.1:
|
||||
resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==}
|
||||
@@ -1277,8 +1277,8 @@ packages:
|
||||
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
conventional-changelog-angular@8.3.0:
|
||||
resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==}
|
||||
conventional-changelog-angular@8.3.1:
|
||||
resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
conventional-changelog-writer@8.4.0:
|
||||
@@ -1290,8 +1290,8 @@ packages:
|
||||
resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
conventional-commits-parser@6.3.0:
|
||||
resolution: {integrity: sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==}
|
||||
conventional-commits-parser@6.4.0:
|
||||
resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1466,8 +1466,8 @@ packages:
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
electron-to-chromium@1.5.327:
|
||||
resolution: {integrity: sha512-hLxLdIJDf8zIzKoH2TPCs+Botc+wUmj9sp4jVMwklY/sKleM8xxxOExRX3Gxj73nCXmJe3anhG7SvsDDPDvmuQ==}
|
||||
electron-to-chromium@1.5.329:
|
||||
resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==}
|
||||
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
@@ -2578,8 +2578,8 @@ packages:
|
||||
resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
minimatch@10.2.4:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@3.1.5:
|
||||
@@ -2968,6 +2968,10 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
php@1.1.0:
|
||||
resolution: {integrity: sha512-IUIOrFBooOyTP3SD9zZZ0cX2UwL5fQj3cmuaXGcc1gsSbMdHseNcEmpwXoh+b0G+hv5LfBL0nBggEOS2YRk7sQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -3618,12 +3622,12 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typescript-eslint@8.57.2:
|
||||
resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==}
|
||||
typescript-eslint@8.58.0:
|
||||
resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: '>=4.8.4 <6.0.0'
|
||||
typescript: '>=4.8.4 <6.1.0'
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
@@ -4308,10 +4312,10 @@ snapshots:
|
||||
|
||||
'@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.3(typescript@5.9.3))':
|
||||
dependencies:
|
||||
conventional-changelog-angular: 8.3.0
|
||||
conventional-changelog-angular: 8.3.1
|
||||
conventional-changelog-writer: 8.4.0
|
||||
conventional-commits-filter: 5.0.0
|
||||
conventional-commits-parser: 6.3.0
|
||||
conventional-commits-parser: 6.4.0
|
||||
debug: 4.4.3
|
||||
import-from-esm: 2.0.0
|
||||
lodash-es: 4.17.23
|
||||
@@ -4380,10 +4384,10 @@ snapshots:
|
||||
|
||||
'@semantic-release/release-notes-generator@14.1.0(semantic-release@25.0.3(typescript@5.9.3))':
|
||||
dependencies:
|
||||
conventional-changelog-angular: 8.3.0
|
||||
conventional-changelog-angular: 8.3.1
|
||||
conventional-changelog-writer: 8.4.0
|
||||
conventional-commits-filter: 5.0.0
|
||||
conventional-commits-parser: 6.3.0
|
||||
conventional-commits-parser: 6.4.0
|
||||
debug: 4.4.3
|
||||
get-stream: 7.0.1
|
||||
import-from-esm: 2.0.0
|
||||
@@ -4524,7 +4528,7 @@ snapshots:
|
||||
|
||||
'@types/minimatch@6.0.0':
|
||||
dependencies:
|
||||
minimatch: 10.2.4
|
||||
minimatch: 10.2.5
|
||||
|
||||
'@types/node@20.19.37':
|
||||
dependencies:
|
||||
@@ -4544,14 +4548,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.19.37
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
'@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.57.2
|
||||
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.58.0
|
||||
'@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.58.0
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ignore: 7.0.5
|
||||
natural-compare: 1.4.0
|
||||
@@ -4560,41 +4564,41 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.57.2
|
||||
'@typescript-eslint/scope-manager': 8.58.0
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.58.0
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.57.2(typescript@5.9.3)':
|
||||
'@typescript-eslint/project-service@8.58.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
debug: 4.4.3
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/scope-manager@8.57.2':
|
||||
'@typescript-eslint/scope-manager@8.58.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/visitor-keys': 8.57.2
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
'@typescript-eslint/visitor-keys': 8.58.0
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)':
|
||||
'@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
@@ -4602,16 +4606,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@8.57.2': {}
|
||||
'@typescript-eslint/types@8.58.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/visitor-keys': 8.57.2
|
||||
'@typescript-eslint/project-service': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
'@typescript-eslint/visitor-keys': 8.58.0
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.4
|
||||
minimatch: 10.2.5
|
||||
semver: 7.7.4
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||
@@ -4619,20 +4623,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
|
||||
'@typescript-eslint/scope-manager': 8.57.2
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.58.0
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/visitor-keys@8.57.2':
|
||||
'@typescript-eslint/visitor-keys@8.58.0':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.57.2
|
||||
'@typescript-eslint/types': 8.58.0
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
@@ -4882,7 +4886,7 @@ snapshots:
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
baseline-browser-mapping@2.10.11: {}
|
||||
baseline-browser-mapping@2.10.12: {}
|
||||
|
||||
basic-auth@2.0.1:
|
||||
dependencies:
|
||||
@@ -4913,12 +4917,12 @@ snapshots:
|
||||
|
||||
bottleneck@2.19.5: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
brace-expansion@1.1.13:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
brace-expansion@2.0.2:
|
||||
brace-expansion@2.0.3:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
@@ -4932,9 +4936,9 @@ snapshots:
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.11
|
||||
caniuse-lite: 1.0.30001781
|
||||
electron-to-chromium: 1.5.327
|
||||
baseline-browser-mapping: 2.10.12
|
||||
caniuse-lite: 1.0.30001782
|
||||
electron-to-chromium: 1.5.329
|
||||
node-releases: 2.0.36
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
@@ -4978,7 +4982,7 @@ snapshots:
|
||||
|
||||
callsites@3.1.0: {}
|
||||
|
||||
caniuse-lite@1.0.30001781: {}
|
||||
caniuse-lite@1.0.30001782: {}
|
||||
|
||||
chalk@2.4.1:
|
||||
dependencies:
|
||||
@@ -5096,7 +5100,7 @@ snapshots:
|
||||
|
||||
content-type@1.0.4: {}
|
||||
|
||||
conventional-changelog-angular@8.3.0:
|
||||
conventional-changelog-angular@8.3.1:
|
||||
dependencies:
|
||||
compare-func: 2.0.0
|
||||
|
||||
@@ -5110,7 +5114,7 @@ snapshots:
|
||||
|
||||
conventional-commits-filter@5.0.0: {}
|
||||
|
||||
conventional-commits-parser@6.3.0:
|
||||
conventional-commits-parser@6.4.0:
|
||||
dependencies:
|
||||
'@simple-libs/stream-utils': 1.2.0
|
||||
meow: 13.2.0
|
||||
@@ -5263,7 +5267,7 @@ snapshots:
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.327: {}
|
||||
electron-to-chromium@1.5.329: {}
|
||||
|
||||
emoji-regex@10.6.0: {}
|
||||
|
||||
@@ -5411,18 +5415,18 @@ snapshots:
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-config-next@16.1.7(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
eslint-config-next@16.1.7(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 16.1.7
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1))
|
||||
globals: 16.4.0
|
||||
typescript-eslint: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -5450,22 +5454,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -5476,7 +5480,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -5488,7 +5492,7 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -6536,17 +6540,17 @@ snapshots:
|
||||
|
||||
mimic-response@2.1.0: {}
|
||||
|
||||
minimatch@10.2.4:
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.5
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
brace-expansion: 1.1.13
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
brace-expansion: 2.0.3
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
@@ -6588,8 +6592,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@next/env': 16.1.7
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.10.11
|
||||
caniuse-lite: 1.0.30001781
|
||||
baseline-browser-mapping: 2.10.12
|
||||
caniuse-lite: 1.0.30001782
|
||||
postcss: 8.4.31
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
@@ -6850,6 +6854,8 @@ snapshots:
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
php@1.1.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
@@ -7607,12 +7613,12 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const config: GlobalConfig = {
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
prepareCmd:
|
||||
"pnpm build && node scripts/release.ts ${nextRelease.version}",
|
||||
"node scripts/release.ts ${nextRelease.version} && sed -i 's|image: git.demonkernel.io.vn/foodsurf/frontend:.*|image: git.demonkernel.io.vn/foodsurf/frontend:${nextRelease.version}|g' k8s.yaml",
|
||||
},
|
||||
],
|
||||
[
|
||||
|
||||
+44
-7
@@ -1,4 +1,5 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
|
||||
const version = process.argv[2];
|
||||
if (!version) {
|
||||
@@ -6,14 +7,50 @@ if (!version) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const zipPath = "release.zip";
|
||||
const zipPath = `release.zip`;
|
||||
|
||||
console.log(`📦 Đang nén file cho version: ${version}...`);
|
||||
console.log(
|
||||
`📦 Đang chuẩn bị đóng gói App Router Standalone cho version: ${version}...`,
|
||||
);
|
||||
|
||||
execSync(`zip -r ${zipPath} out`, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
// Kiểm tra xem đã build chưa
|
||||
if (!fs.existsSync(".next/standalone")) {
|
||||
console.error(
|
||||
"❌ Thư mục .next/standalone không tồn tại. Hãy chạy 'pnpm build' trước!",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ Đã tạo file zip: ${zipPath}`);
|
||||
/**
|
||||
* Với Next.js Standalone, ta cần:
|
||||
* 1. Nội dung trong .next/standalone (copy ra ngoài hoặc nén trực tiếp)
|
||||
* 2. .next/static (phải nằm trong .next/static của gói release)
|
||||
* 3. public (phải nằm trong public của gói release)
|
||||
*/
|
||||
|
||||
execSync("pnpm format");
|
||||
try {
|
||||
// Tạo thư mục tạm để cấu trúc lại file trước khi nén
|
||||
execSync("rm -rf temp_release && mkdir -p temp_release");
|
||||
|
||||
console.log("🚚 Đang sao chép các file standalone...");
|
||||
execSync("cp -r .next/standalone/. temp_release/");
|
||||
|
||||
console.log("🚚 Đang sao chép static và public...");
|
||||
execSync("mkdir -p temp_release/.next/static");
|
||||
execSync("cp -r .next/static/. temp_release/.next/static/");
|
||||
execSync("cp -r public temp_release/ 2>/dev/null || : "); // Bỏ qua nếu không có thư mục public
|
||||
|
||||
console.log("🗜️ Đang nén thành file zip...");
|
||||
// Di chuyển vào temp_release để nén các file bên trong nó
|
||||
execSync(`cd temp_release && zip -r ../${zipPath} .`, { stdio: "inherit" });
|
||||
|
||||
// Xóa thư mục tạm
|
||||
execSync("rm -rf temp_release");
|
||||
|
||||
console.log(`✅ Đã tạo file thành công: ${zipPath}`);
|
||||
|
||||
execSync("pnpm format");
|
||||
} catch (error) {
|
||||
console.error("❌ Có lỗi xảy ra trong quá trình nén:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+1
-1
@@ -31,5 +31,5 @@
|
||||
".next/dev/types/**/*.ts",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "script", "*.ts"]
|
||||
"exclude": ["node_modules", "scripts", "*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user