Compare commits

..

33 Commits

Author SHA1 Message Date
Thanh Quy - wolf dae381d182 Enhance Atoms and Layout Documentation
- Updated ATOMS.md to include new LoginInput and ErrorMessageLogin components with detailed usage examples and props descriptions.
- Adjusted the status and created date in ATOMS.md.
- Refined the layout and styling descriptions in LAYOUTS.md for Header and Footer components, including responsive behavior and accessibility improvements.
- Added new sections for WiFi card and social links in the Footer documentation.
- Improved code formatting and consistency across various components in the codebase.
2026-04-18 11:13:53 +07:00
Thanh Quy - wolf 4892099eaa fix: Address UI/UX accessibility and usability issues from test report
- Add aria-label to search input, category/status dropdowns in ProductsTab
- Add aria-label with item name to Edit/Delete icon buttons
- Add title tooltip to truncated product descriptions
- Add price validation in ProductModal (min=1, reject 0 with inline error)
- Add focus-visible ring to all sidebar nav buttons and links
- Improve StatusBadge contrast: text-amber-700 to text-amber-800 (WCAG AA)
- Add unit annotation (tr = trieu dong VND) to revenue chart subtitles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 10:54:52 +07:00
Thanh Quy - wolf 9772a8f639 feat: Add image upload functionality to ProductModal; implement image preview and removal features 2026-04-17 20:14:49 +07:00
Thanh Quy - wolf d88b067c5c feat: Implement cash and QR payment modals; enhance payment summary card functionality
refactor: Update PaymentSummaryCard to handle new payment methods and improve state management
fix: Add clearCart function to cart context for better cart management
chore: Update package dependencies and improve documentation
2026-04-17 20:09:16 +07:00
Thanh Quy - wolf e85fa90879 fix: Add title attribute to icon button in TextInput for better accessibility 2026-04-17 18:41:31 +07:00
Thanh Quy - wolf 2875d39f2c Refactor README and component documentation for clarity and consistency; update layout and formatting across various components; enhance analytics charts and payment summary card functionality; improve login form imports and structure; optimize product card button implementation; ensure consistent styling and spacing in manager products tab. 2026-04-17 18:31:33 +07:00
Thanh Quy- wolf c99323ff9e Fix review modal 2026-04-17 10:53:29 +07:00
Thanh Quy - wolf cb639704dd fix: Add className prop to Button component for better styling flexibility
Release package / release (pull_request) Successful in 1m25s
2026-04-16 00:21:13 +07:00
Thanh Quy - wolf 6d7dc0b43f feat: Enhance accessibility and improve UI components
- Updated header and navigation components with aria-labels for better accessibility.
- Modified SearchBar and SearchInput components to include aria-labels and titles for clearer user interaction.
- Changed category sidebar button to include aria-label for improved screen reader support.
- Adjusted payment page table headers to include scope attributes for better semantic structure.
- Enhanced search functionality in Home component to reset category when search query is updated.
- Added accessibility features to buttons in ProductCard and other components for improved usability.
- Introduced a new skill for frontend atomic design methodology with Tailwind CSS.
- Created new skills for prompt optimization, UI/UX testing, and website creation automation.
2026-04-16 00:16:31 +07:00
Thanh Quy - wolf 80ff0af3d8 Remove outdated documentation for Register Shift feature 2026-04-15 23:50:10 +07:00
Thanh Quy - wolf 93552cb9d1 Enhance formatting and readability of the Register Shift feature documentation by adjusting line breaks and indentation for better clarity. Update descriptions for roles, functionalities, and UI elements to ensure consistency and improve user understanding. 2026-04-15 23:47:16 +07:00
Thanh Quy - wolf 460e8a4ac1 fix: Resolve merge conflicts and clean up imports in multiple components
Release package / release (pull_request) Failing after 14s
2026-04-13 20:41:24 +07:00
Thanh Quy - wolf e326e11bb4 Fix conflits
Release package / release (pull_request) Failing after 12s
2026-04-13 20:33:01 +07:00
Thanh Quy - wolf 7e4a21d412 chore: Remove obsolete Blackbox configuration files
Release package / release (pull_request) Failing after 2m51s
2026-04-13 20:17:25 +07:00
Thanh Quy - wolf 127cb32b16 acp IconButton 2026-04-10 22:45:08 +07:00
Thanh Quy - wolf 4ddce518ac feat: Implement mobile dropdown navigation and enhance weekly schedule component 2026-04-10 22:37:52 +07:00
Thanh Quy - wolf 95d9435694 feat: Update styling and improve accessibility in shift schedule components 2026-04-10 21:52:52 +07:00
Thanh Quy - wolf 9d9a219ac8 Add Route to staff schedule in manager dashboard 2026-04-10 21:43:15 +07:00
Thanh Quy - wolf 32d40b1dbe Create staff page 2026-04-10 21:38:30 +07:00
Thanh Quy- wolf ace60e424b feat: Enhance Button component with className prop and update PaymentSummaryCard usage
- Added className prop to Button for custom styling
- Updated PaymentSummaryCard to utilize the new Button implementation
- Added nextjs dependency to package.json and package-lock.json
2026-04-09 16:13:34 +07:00
Thanh Quy - wolf 4e643303c3 Merge branch 'atomic_design' into develop
Release package / release (pull_request) Successful in 8m51s
2026-04-06 13:09:47 +07:00
Thanh Quy - wolf 949badaeeb Delete IconButton 2026-04-06 13:08:49 +07:00
Thanh Quy - wolf 7547260e90 update README.md
Release package / release (pull_request) Failing after 8m34s
2026-04-05 16:56:34 +07:00
Thanh Quy - wolf c68ea1f26f Atomic for login page and register page 2026-04-05 16:16:48 +07:00
Thanh Quy - wolf 240155d1f6 Đóng gói code ở payment page 2026-04-04 02:02:23 +07:00
Thanh Quy - wolf ff133edfda Tái cấu trúc manager page 2026-04-03 20:24:36 +07:00
Thanh Quy - wolf 50473d799d Tái cấu trúc code 2026-04-03 20:09:05 +07:00
Thanh Quy - wolf 4f3d807c95 docs: Add Phase 1 implementation summary 2026-04-03 19:41:25 +07:00
Thanh Quy - wolf eca619b79a feat: Implement Atomic Design Phase 1 - Atoms Foundation
 Created complete atoms library (21 components):
- Buttons: Button, IconButton with variants (primary, secondary, danger, ghost)
- Inputs: TextInput, SearchInput, Textarea with validation support
- Typography: Heading (h1-h6), Text (variants), Caption
- Badges: Badge (5 variants), PriceBadge (formatted pricing)
- Dividers: Horizontal/vertical separators

🎯 Updated existing components to use atoms:
- CartProduct: Now uses Button, Text, Caption atoms
- ReviewModal: Now uses Button, Textarea, Heading, Text atoms

📚 Added comprehensive documentation:
- components/atoms/ATOMS.md: Complete atoms reference guide
- Usage examples, theming, import patterns, accessibility

🏗️ Architecture improvements:
- Foundation for molecules/organisms
- Type-safe components with full TypeScript support
- Consistent theming via CSS variables
- Barrel exports for clean imports

 Verified:
- npm run build: Success (✓ Compiled successfully)
- npm run format: All files formatted
- npm run lint: No new errors in atoms

Next: Phase 2 - Create molecules (ProductCard, FormField, SearchBar, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:40:07 +07:00
Thanh Quy - wolf d3a6d1164c Merge branch 'main' into develop
Release package / release (pull_request) Successful in 20m59s
2026-04-03 15:13:39 +07:00
Thanh Quy - wolf e211ee5835 Feature Analytics 2026-04-03 15:09:03 +07:00
Thanh Quy - wolf 17d82f7239 docs: comprehensive AGENTS guide for AI development
Update AGENTS.md with detailed project architecture, code conventions, and quick reference guide to help AI understand the codebase structure and development workflow more efficiently.

Key additions:
- Complete folder structure with descriptions (app/, components/, layouts/, lib/)
- File organization and naming conventions
- Code style (TypeScript, Prettier, ESLint, Tailwind)
- Responsive design guidelines
- Markdown documentation update rules
- Type safety and data flow patterns
- Quick reference guide for common tasks
- CSS variables reference
- Useful npm commands
- Common code patterns
- Project status (completed, in progress, planned features)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 09:23:49 +07:00
Thanh Quy - wolf e0e64ded9a Manage Product and List Product 2026-04-01 15:06:37 +07:00
31 changed files with 1742 additions and 563 deletions
+209
View File
@@ -0,0 +1,209 @@
---
name: agent-orchestrator
description: >
Breaks down large, multi-step software tasks into structured sub-agents on the
Claude Code CLI using the Task tool. Use this skill whenever the user
describes a complex project or feature that has multiple components - e.g.,
"build a login API with JWT and unit tests", "set up a CI/CD pipeline with
Docker and GitHub Actions", "create a full-stack todo app with auth, REST API,
and frontend". Trigger this skill when the task clearly has more than one
distinct sub-system, layer, or concern that could be worked on in parallel or
in sequence. DO NOT trigger for simple, single-step tasks like "write a
function to reverse a string" or "fix this bug".
---
# Agent Orchestrator
A skill for decomposing large engineering tasks into parallel and sequential
sub-agents on Claude Code CLI using the **Task tool**.
## When to Use
Trigger when the user's task has **multiple distinct components** - for example:
- "Build a REST API with authentication, rate limiting, and unit tests"
- "Set up a monorepo with shared packages, CI pipeline, and deployment configs"
- "Create a data pipeline: ingest transform store visualize"
Do **not** trigger for single-step tasks (e.g., "rename this variable", "add a
README").
---
## Prompt Template
Copy the block below and paste it into your Agent Code CLI session. Replace the
placeholder in the `TASK` variable with your actual task description.
---
```
You are a senior engineering orchestrator operating inside the Agent Code CLI.
TASK: "Build a login API with JWT and unit tests"
---
Your job is to analyze this task, decompose it into well-scoped sub-tasks, identify dependencies between them, and execute them using the Task tool.
Follow these steps exactly:
---
## STEP 1 - Guard: Is this task complex enough?
If the task can be completed in a single step by a single agent (e.g., "rename a variable", "write one function"), respond with:
> "This task is simple enough to handle directly - no orchestration needed."
Then complete it yourself. Stop here.
Otherwise, continue.
---
## STEP 2 - Decompose the task into sub-tasks
Analyze the TASK string. Break it into a flat list of concrete, independently scoped sub-tasks. Each sub-task must:
- Have a clear, specific goal
- Be executable by a single focused agent
- Produce a concrete output (file, module, config, test suite, etc.)
Output the list in this format:
Sub-tasks:
[T1] <sub-task title> - <one-sentence description>
[T2] <sub-task title> - <one-sentence description>
[T3] ...
---
## STEP 3 - Identify dependencies and execution mode
For each sub-task, decide whether it can run in parallel with others, or must run after a specific predecessor.
Label each sub-task as one of:
[PARALLEL] - no dependencies, can start immediately
[SEQUENTIAL - after Tx] - must run only after sub-task Tx completes
Rules:
- If a sub-task depends on an artifact produced by another (a file, schema, interface, module), mark it SEQUENTIAL.
- If two sub-tasks are fully independent (different files, no shared state), mark both PARALLEL.
- Do not start any SEQUENTIAL sub-task until its prerequisite is confirmed complete.
Output the dependency plan in this format:
Dependency Plan:
[T1] <title> [PARALLEL]
[T2] <title> [PARALLEL]
[T3] <title> [SEQUENTIAL - after T1]
[T4] <title> [SEQUENTIAL - after T2, T3]
---
## STEP 4 - Confirm plan before execution
Print the full decomposition and dependency plan to the user.
Then print:
> "Starting execution. Spawning parallel sub-agents now..."
Do not skip this confirmation step.
---
## STEP 5 - Execute using the Task tool
Use the **Task tool** to spawn sub-agents. Follow this protocol:
### 5a. Spawn all [PARALLEL] sub-tasks simultaneously
- Call the Task tool for each PARALLEL sub-task at the same time (do not wait for one to finish before spawning the next).
- Each Task call must include a self-contained prompt describing exactly what to build, where to put files, and what the expected output is.
### 5b. Monitor and gate SEQUENTIAL sub-tasks
- Wait for each prerequisite Task to complete before spawning its dependent.
- Once a prerequisite completes, immediately spawn the next Task.
### 5c. Task prompt format
Each task spawned via the Task tool must include:
- Role: "You are a focused sub-agent. Complete exactly this sub-task and nothing else."
- Sub-task goal (from Step 2)
- Output location (file path or module name)
- Any relevant interfaces or contracts from predecessor tasks (if SEQUENTIAL)
- Instruction to report back: "When done, summarize what you built and list output files."
---
## STEP 6 - Synthesize and report
After all Tasks complete:
1. Collect each sub-agent's summary.
2. Verify that all expected outputs exist.
3. Report to the user:
- What was built
- File/module structure
- Any issues or gaps found
- Suggested next steps (e.g., "Run `npm test` to verify", "Review the generated OpenAPI spec")
---
Begin now with STEP 1.
```
---
## Tips for Customizing the Template
- **Replace the TASK value** with your actual task string. Keep it on one line,
in quotes.
- **Add context if needed**: Append lines after `TASK:` like
`STACK: "Node.js, PostgreSQL, Jest"` or
`CONSTRAINTS: "Use ESM modules only"` - the orchestrator will incorporate
them.
- **For monorepos or specific file layouts**: Add a `STRUCTURE:` hint, e.g.:
```
STRUCTURE: "src/api/, src/auth/, src/tests/, docker-compose.yml"
```
- **To limit parallelism** (e.g., on resource-constrained machines): Add
`MAX_PARALLEL_AGENTS: 2` after the TASK line.
---
## Example Usage
**Input task:**
> "Build a login API with JWT authentication and unit tests"
**Expected orchestrator output (before execution):**
```
Sub-tasks:
[T1] Project scaffold - Initialize Node.js project, folder structure, and dependencies
[T2] Database schema - Define User model and migration for PostgreSQL
[T3] Auth service - Implement JWT sign/verify logic and password hashing
[T4] Login route - Build POST /auth/login endpoint with validation
[T5] Middleware - Create JWT auth middleware for protected routes
[T6] Unit tests - Write Jest tests for auth service and login route
Dependency Plan:
[T1] Project scaffold [PARALLEL]
[T2] Database schema [PARALLEL]
[T3] Auth service [SEQUENTIAL - after T1]
[T4] Login route [SEQUENTIAL - after T3]
[T5] Middleware [SEQUENTIAL - after T3]
[T6] Unit tests [SEQUENTIAL - after T4, T5]
Starting execution. Spawning parallel sub-agents now...
```
T1 and T2 spawn immediately. T3 spawns once T1 completes. T4 and T5 spawn once
T3 completes. T6 spawns last.
---
## Notes
- This skill requires Agent Code CLI with the **Task tool** enabled.
- Each spawned sub-agent has no memory of the others - pass all needed context
explicitly in the Task prompt.
- For very large tasks (10+ sub-tasks), consider breaking the orchestration into
phases and applying this skill recursively per phase.
+2 -2
View File
@@ -15,10 +15,10 @@
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
},
}
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000]
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "uname -a",
+71 -9
View File
@@ -17,8 +17,10 @@ Chứa route/page/layout theo Next.js App Router.
- **`(main)/`** - Main shopping interface (duyệt menu, đăng nhập, đăng ký,
thanh toán)
- **`(feed)/`** - Feed/discovery page (khám phá quán)
- **`(manager)/`** - Manager dashboard (quản lý sản phẩm, đơn hàng) - **[ĐANG
PHÁT TRIỂN]**
- **`(manager)/`** - Manager dashboard (quản lý sản phẩm, phân tích tài
chính) - **[ĐANG PHÁT TRIỂN ~70%]**
- **`(staff)/`** - Staff shift schedule (lịch làm việc nhân viên) - **[HOÀN
THÀNH]**
- **Tài liệu:** `app/APP.md` - Chi tiết routes, pages, CSS tokens
### `components/` - Reusable UI Components (Atomic Design)
@@ -80,7 +82,14 @@ Sections phức tạp kết hợp molecules + atoms, có logic, data filtering:
- **`cart/`** - CartFab, CartSummary, CartList
- **`product-grid/`** - ProductGrid, ProductFilters
- **`forms/`** - LoginForm, RegisterForm, CheckoutForm, ReviewForm
- **`modals/`** - ReviewModal, ConfirmModal
- **`modals/`** - ReviewModal, ConfirmModal, CashPaymentModal, QRPaymentModal,
PaymentSuccessModal
- **`manager/`** - ProductModal (image upload), ComboModal, CategoryModal,
DeleteConfirm, StatusBadge, ProductsTab, CombosTab, CategoriesTab
- **`analytics/`** - BarChart, LineChart, PieChart (SVG), ProductTable,
SummaryCard
- **`shift-schedule/`** - WeeklySchedule, MonthlyCalendar, MobileShiftView,
ShiftCreateModal, ShiftDetailModal
- **`shop-grid/`** - ShopGrid, ShopFilters
- **`hero-section/`** - HeroSection
- **`featured-section/`** - FeaturedProducts, FeaturedShops
@@ -103,6 +112,7 @@ Page layouts, không có data cụ thể, children composition:
- **`main-layout/`** - MainLayout (header + sidebar + content + footer)
- **`feed-layout/`** - FeedLayout
- **`manager-layout/`** - ManagerLayout
- **`staff-layout/`** - StaffLayout (navigation shell for staff pages)
- **`checkout-layout/`** - CheckoutLayout
- **`auth-layout/`** - AuthLayout
@@ -161,8 +171,12 @@ Chứa logic dùng chung, context, constants, types:
- **`auth-context.tsx`** - AuthProvider + useAuth() hook (login, logout,
register)
- **`cart-context.tsx`** - CartProvider + useCart() hook (add, remove, quantity
operations)
operations, clearCart)
- **`menu-context.tsx`** - MenuProvider + useMenu() hook (category state)
- **`manager-context.tsx`** - ManagerProvider + useManager() hook (CRUD for
products, combos, categories)
- **`shift-context.tsx`** - ShiftProvider + useShift() hook (staff shift
management: create, update, delete, date selection)
- **Tài liệu:** `lib/LIB.md` - Chi tiết types, constants, contexts
### `public/` - Static Assets
@@ -233,11 +247,41 @@ components/
│ ├── product-grid/
│ │ ├── ProductGrid.tsx
│ │ └── ProductGrid.types.ts
│ ├── modals/
│ │ ├── ReviewModal.tsx
│ │ ├── CashPaymentModal.tsx
│ │ ├── QRPaymentModal.tsx
│ │ └── PaymentSuccessModal.tsx
│ ├── manager/
│ │ ├── ProductModal.tsx
│ │ ├── ComboModal.tsx
│ │ ├── CategoryModal.tsx
│ │ ├── DeleteConfirm.tsx
│ │ ├── StatusBadge.tsx
│ │ ├── ProductsTab.tsx
│ │ ├── CombosTab.tsx
│ │ └── CategoriesTab.tsx
│ ├── analytics/
│ │ ├── BarChart.tsx
│ │ ├── LineChart.tsx
│ │ ├── PieChart.tsx
│ │ ├── ProductTable.tsx
│ │ └── SummaryCard.tsx
│ ├── shift-schedule/
│ │ ├── WeeklySchedule.tsx
│ │ ├── MonthlyCalendar.tsx
│ │ ├── MobileShiftView.tsx
│ │ ├── ShiftCreateModal.tsx
│ │ └── ShiftDetailModal.tsx
│ └── index.ts
├── templates/
│ ├── main-layout/
│ │ ├── MainLayout.tsx
│ │ └── MainLayout.types.ts
│ ├── manager-layout/
│ │ └── ManagerLayout.tsx
│ ├── staff-layout/
│ │ └── StaffLayout.tsx
│ └── index.ts
└── ATOMIC_DESIGN.md
```
@@ -314,9 +358,15 @@ components/
- **Mock data first** - Use `lib/constants.ts` for development, replace with API
later
- **Contexts for state sharing:**
- `AuthContext` (lib/auth-context.tsx) - User state + auth operations
- `CartContext` (lib/cart-context.tsx) - Shopping cart + operations
- `MenuContext` (lib/menu-context.tsx) - Active category state
- `AuthContext` (lib/auth-context.tsx) - User state + auth operations
`useAuth()`
- `CartContext` (lib/cart-context.tsx) - Shopping cart + operations →
`useCart()` (includes `clearCart`)
- `MenuContext` (lib/menu-context.tsx) - Active category state → `useMenu()`
- `ManagerContext` (lib/manager-context.tsx) - Manager dashboard state (CRUD
for products/combos/categories) → `useManager()`
- `ShiftContext` (lib/shift-context.tsx) - Staff shift management (create,
update, delete shifts, date selection) → `useShift()`
- **localStorage keys:** `coffee-shop-user`, `coffee-shop-cart` (defined in
contexts)
@@ -625,21 +675,33 @@ className = "w-full sm:w-1/2 md:w-1/3 p-4 md:p-6 hidden md:block";
## 5) Project Status
> Last Updated: 2026-04-18
### ✅ Completed Features
- User authentication (login, register, logout)
- Product grid display with category filtering
- Shopping cart with add/remove/update operations
- Payment page with review modal
- Payment page with PaymentSummaryCard + review modal
- Cash payment modal (CashPaymentModal) with change calculation
- QR payment modal (QRPaymentModal) with QR code generation (`qrcode.react`) and
5-second auto-confirm countdown
- Payment success modal (PaymentSuccessModal) with cart clear
- Shop discovery (feed page)
- Responsive design (mobile, tablet, desktop)
- Header + Footer with navigation
- Sidebar category filter
- Manager dashboard — Products, Combos, Categories tabs with full CRUD
- Product image upload in ProductModal
- Financial Analytics dashboard (LineChart, BarChart, PieChart, ProductTable)
- Staff shift schedule (`/staff/schedule`) — weekly/monthly views, mobile view,
CRUD shifts, department filter
### 🚀 In Progress
- Manager dashboard (~70% complete) - `app/(manager)/` route group
- Remaining: order management, inventory tracking, real API integration
- Atomic Design refactoring (components reorganization)
- Manager dashboard (quản lý sản phẩm) - `app/(manager)/` route group
### 📋 Planned Features
+58 -22
View File
@@ -3,6 +3,8 @@
Dự án Frontend cho hệ thống đặt món cà phê, xây dựng bằng Next.js App Router,
React 19, TypeScript và Tailwind CSS v4.
_Last Updated: 2026-04-18_
---
## Mô Tả Dự Án
@@ -11,7 +13,9 @@ Giao diện người dùng (frontend) cho hệ thống đặt và bán đồ u
### Tính Năng Hiện Tại
#### 1. **Trang Đăng Nhập & Đăng Ký** (`app/(main)/login`, `app/(main)/register`)
#### Tính Năng Khách Hàng (Customer Features)
##### 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
@@ -19,7 +23,7 @@ Giao diện người dùng (frontend) cho hệ thống đặt và bán đồ u
- 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`)
##### 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ỉ
@@ -27,9 +31,7 @@ Giao diện người dùng (frontend) cho hệ thống đặt và bán đồ u
- 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:
##### 3. **Trang Chính - Duyệt Thực Đơn** (`app/(main)`)
- 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ị)
@@ -38,41 +40,52 @@ Dành cho khách hàng:
- Lọc tự động theo trạng thái available
- Mobile menu: scrollable category tabs (< md)
#### 4. **Trang Thanh Toán** (`app/(main)/payment`)
##### 4. **Trang Thanh Toán** (`app/(main)/payment`)
- Bảng danh sách sản phẩm trong giỏ hàng với điều chỉnh số lượng
- Invoice aside sticky với tổng giá và nút thanh toán (Tiền mặt, QR)
- Modal đánh giá 5 sao dành riêng cho khách hàng (ReviewModal)
- **CashPaymentModal:** nhập tiền mặt, tính tiền thối, báo lỗi nếu tiền không đủ
- **QRPaymentModal:** tạo mã QR thanh toán (qrcode.react), tự xác nhận sau 5
giây đếm ngược
- **PaymentSuccessModal:** xác nhận thanh toán thành công, tự động xoá giỏ hàng
#### 5. **Hệ Thống Giỏ Hàng** (`lib/cart-context.tsx`)
##### 5. **Hệ Thống Giỏ Hàng** (`lib/cart-context.tsx`)
- 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
- `clearCart()` để xoá toàn bộ giỏ sau thanh toán
#### 6. **Hệ Thống Xác Thực** (`lib/auth-context.tsx`)
##### 6. **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
#### 7. **Hệ Thống Danh Mục** (`lib/menu-context.tsx`)
##### 7. **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
#### 8. **Manager Dashboard** (`app/(manager)/manager`)
#### Tính Năng Quản Lý (Manager Features)
Dành cho quản lý:
##### 8. **Manager Dashboard** (`app/(manager)/manager`) — ~70% hoàn thành
- Sidebar desktop: Brand, tab navigation (Thực đơn / Combo / Danh mục), link tới
Analytics
- CRUD sản phẩm, combo, danh mục qua modals
- Auth guard: tự động redirect non-manager về `/`
- **Products tab:** CRUD sản phẩm qua ProductModal, bao gồm:
- Upload ảnh sản phẩm với xem trước, nút "Đổi ảnh" và xoá ảnh
- Giao diện vùng kéo thả với dashed border
- Toggle trạng thái available
- Dialog xác nhận xoá
- **Combos tab:** CRUD combo qua ComboModal, toggle trạng thái, xác nhận xoá
- **Categories tab:** CRUD danh mục qua CategoryModal, xác nhận xoá
#### 9. **Trang Phân Tích Tài Chính** (`app/(manager)/manager/analytics`)
##### 9. **Trang Phân Tích Tài Chính** (`app/(manager)/manager/analytics`)
- Summary cards: Doanh thu, đơn hàng, lợi nhuận, giá trị đơn trung bình (so sánh
kỳ trước)
@@ -81,6 +94,24 @@ Dành cho quản lý:
- Bảng top 5 sản phẩm và bảng chi tiết có thể sắp xếp
- Lọc theo danh mục
#### Tính Năng Nhân Viên (Staff Features)
##### 10. **Lịch Làm Việc Nhân Viên** (`app/(staff)/staff/schedule`)
- Xem lịch theo tuần và theo tháng
- Mobile shift view (danh sách ca theo ngày)
- Lọc theo phòng ban / bộ phận
- Modal tạo ca làm việc mới (ShiftCreateModal)
- Modal xem chi tiết ca (ShiftDetailModal)
- Auth guard: chỉ staff/manager mới truy cập được
#### Cải Thiện Tiếp Cận (Accessibility)
- Thuộc tính `title` trên các nút icon (tooltip cho screen reader)
- ARIA roles trên modals: `role="dialog"`, `aria-modal`, `aria-labelledby`
- Semantic HTML: nhãn form với `htmlFor` đúng chuẩn
- `alt` text đầy đủ cho ảnh sản phẩm
---
## Cách Chạy Dự Án
@@ -144,9 +175,12 @@ frondend/
| | +-- layout.tsx # Feed layout
| | +-- feed/page.tsx # Trang khám phá quán nước (/feed)
| +-- (manager)/ # Manager route group
| +-- layout.tsx # Manager layout - auth guard + ManagerProvider
| +-- manager/page.tsx # Manager Dashboard (/manager)
| +-- manager/analytics/page.tsx # Financial Analytics (/manager/analytics)
| | +-- layout.tsx # Manager layout - auth guard + ManagerProvider
| | +-- manager/page.tsx # Manager Dashboard (/manager)
| | +-- manager/analytics/page.tsx # Financial Analytics (/manager/analytics)
| +-- (staff)/ # Staff route group
| +-- layout.tsx # Staff layout - auth guard
| +-- staff/schedule/page.tsx # Lịch làm việc nhân viên (/staff/schedule)
+-- components/ # Atomic Design UI components
| +-- atoms/ # Nguyên tử: Button, Input, Badge, Text...
| +-- molecules/ # Phân tử: ProductCard, ShopCard, SearchBar...
@@ -202,8 +236,9 @@ frondend/
| ---------------- | --------- | ------------------------------------- |
| Next.js | 16.1.7 | React Framework (App Router) |
| React | 19.2.3 | Thư viện UI |
| TypeScript | ^5 | Kiểu dữ liệu tĩnh |
| TypeScript | ^5.9.3 | Kiểu dữ liệu tĩnh |
| Tailwind CSS | ^4.2.2 | Utility-first CSS framework |
| qrcode.react | ^4.2.0 | Tạo mã QR thanh toán |
| Geist Font | - | Font chữ (Google Fonts via next/font) |
| FontAwesome | 6.7.2 | Icon library (CDN) |
| pnpm | - | Package manager |
@@ -224,14 +259,15 @@ frondend/
- **Thanh Toán:** `app/(main)/payment/page.tsx`
- **Manager Dashboard:** `app/(manager)/manager/page.tsx`
- **Phân Tích Tài Chính:** `app/(manager)/manager/analytics/page.tsx`
- **Lịch Làm Việc Nhân Viên:** `app/(staff)/staff/schedule/page.tsx`
### Tài Khoản Demo
| Loại tài khoản | Tên đăng nhập | Mật khẩu |
| -------------- | ------------- | ------------- |
| Manager | admin | admin |
| Staff | Nguyễn Văn An | Nguyễn Văn An |
| Customer | 0987654321 | user1 |
| Loại tài khoản | Tên đăng nhập | Mật khẩu / OTP |
| -------------- | ------------- | -------------------------- |
| Manager | admin | admin |
| Staff | Nguyễn Văn An | Nguyễn Văn An |
| Customer | 0987654321 | bất kỳ 6 chữ số (mock OTP) |
### Design & Styling
+73 -19
View File
@@ -1,5 +1,9 @@
# Coffee Shop Frontend - TODO
_Last Updated: 2026-04-18_
---
## Completed Features & Implementations
### A. Dead Code Removed
@@ -37,17 +41,66 @@
- [x] npm run build - PASSED (Compiled successfully, TypeScript clean, static
pages generated)
### F. Cart & Payment (Completed)
- [x] Cart checkout flow — cart context with addToCart, removeFromCart,
setQuantity, clearCart
- [x] Payment page implementation (`app/(main)/payment`)
- [x] CashPaymentModal: cash input, change calculation, insufficient cash error
- [x] QRPaymentModal: QR code generation via qrcode.react, 5-second countdown
auto-confirm
- [x] PaymentSuccessModal: success confirmation, auto-clears cart
### G. Manager Dashboard — ~70% Complete (In Progress)
- [x] Manager Dashboard page (`app/(manager)/manager/page.tsx`) with auth guard
- [x] Products tab with CRUD (ProductModal)
- [x] Image upload in ProductModal: file input, preview, "Đổi ảnh" button,
remove button, dashed border drop zone
- [x] Combos tab with CRUD (ComboModal)
- [x] Categories tab with CRUD (CategoryModal)
- [x] Delete confirmation dialogs for all entities
- [x] Status toggle buttons (available/unavailable)
- [x] Sidebar navigation with link to Analytics
### H. Financial Analytics Dashboard (Completed)
- [x] Analytics page (`app/(manager)/manager/analytics`)
- [x] Summary cards: Revenue, Orders, Profit, Average Order Value (with period
comparison)
- [x] Period selector: Day / Week / Month / Year
- [x] SVG charts: LineChart, BarChart, PieChart with hover tooltips
- [x] Top 5 products table and sortable detail table
- [x] Category filter
### I. Staff Shift Schedule (Completed)
- [x] Staff schedule module (`app/(staff)/staff/schedule`)
- [x] Weekly calendar view
- [x] Monthly calendar view
- [x] Mobile shift view (list per day)
- [x] ShiftCreateModal: create new shifts
- [x] ShiftDetailModal: view shift details
- [x] Department/role filtering
- [x] Auth guard for staff/manager only
### J. Accessibility Improvements (Completed)
- [x] title attributes on icon buttons (tooltips for screen readers)
- [x] ARIA roles on modals: role="dialog", aria-modal, aria-labelledby
- [x] Semantic HTML: form labels with correct htmlFor
- [x] Image alt text for product images
---
## Pending Features (Future Work)
### Cart & Ordering
- [ ] Implement cart checkout flow (app/(main)/cart or modal)
- [ ] Cart sidebar/modal with item list and total
- [ ] Cart sidebar/modal with item list and total (quick-access without going to
payment page)
- [ ] Order submission API integration
- [ ] Payment page implementation (app/(main)/payment)
- [ ] Order history/tracking page
- [ ] Order history/tracking page for customers
- [ ] Toast notifications for cart actions
### Authentication & User Management
@@ -58,22 +111,22 @@
- [ ] Password reset/recovery flow
- [ ] Session management and token refresh
### Manager Features
### Manager Features (Remaining ~30%)
- [ ] Manager dashboard page (app/(manager)/page.tsx)
- [ ] Product management (add/edit/delete)
- [ ] Category management
- [ ] Order management & tracking
- [ ] Sales analytics/dashboard
- [ ] Inventory management
- [ ] Order management & tracking for manager
- [ ] Inventory management beyond available toggle (stock counts, low-stock
alerts)
- [ ] Staff management tab in Manager Dashboard
- [ ] Shift approval workflow (manager approves staff shift requests)
### Backend Integration
- [ ] 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
- [ ] Real product images hosted on server/CDN (replace public/imgs/products/)
- [ ] Real analytics data from backend (GET /api/analytics)
- [ ] Real shift/schedule data from backend
### UX Improvements
@@ -85,11 +138,12 @@
- [ ] Filter by price range
- [ ] Quantity selector in product card
- [ ] Related products suggestions
- [ ] Push notifications for order status updates
### Performance & SEO
- [ ] Dynamic route generation for products (app/(main)/product/[id]/page.tsx)
- [ ] Dynamic route generation for shops (app/(feed)/shop/[id]/page.tsx)
- [ ] 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
@@ -97,7 +151,7 @@
### Accessibility & Testing
- [ ] Keyboard navigation testing
- [ ] ARIA labels audit
- [ ] Unit tests for contexts
- [ ] E2E tests for user flows
- [ ] Accessibility audit (WCAG 2.1 AA)
- [ ] Full ARIA labels audit (WCAG 2.1 AA)
- [ ] Unit tests for contexts (auth, cart, menu, manager)
- [ ] E2E tests for user flows (checkout, login, manager CRUD)
- [ ] Accessibility audit tool (axe, Lighthouse)
+2 -11
View File
@@ -2,10 +2,8 @@
import Button from "@/components/atoms/buttons/Button";
import PaymentSummaryCard from "@/components/molecules/cards/PaymentSummaryCard";
import ReviewModal from "@/components/organisms/modals/ReviewModal";
import { useAuth } from "@/lib/auth-context";
import { useCart } from "@/lib/cart-context";
import { useState } from "react";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
@@ -21,9 +19,6 @@ export default function PaymentPage() {
} = useCart();
const { user } = useAuth();
const [isReviewOpen, setIsReviewOpen] = useState(false);
const isCustomer = user?.role === "customer";
return (
<div>
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
@@ -129,17 +124,13 @@ export default function PaymentPage() {
</section>
<PaymentSummaryCard
cartItems={items}
totalPrice={totalPrice}
isCustomer={isCustomer}
role={user?.role ?? "customer"}
backHref="/"
/>
</div>
</div>
<ReviewModal
isOpen={isReviewOpen}
onClose={() => setIsReviewOpen(false)}
/>
</div>
);
}
+2
View File
@@ -299,6 +299,7 @@ export default function AnalyticsPage() {
<>
<p className="mb-3 text-xs text-(--color-text-muted)">
Doanh thu theo thời gian {PERIOD_LABELS[period]}
<span className="ml-2 opacity-70">(tr = triệu đng VND)</span>
</p>
<LineChart data={revenueData} height={220} />
</>
@@ -307,6 +308,7 @@ export default function AnalyticsPage() {
<>
<p className="mb-3 text-xs text-(--color-text-muted)">
So sánh doanh thu nửa đu nửa sau kỳ hiện tại
<span className="ml-2 opacity-70">(tr = triệu đng VND)</span>
</p>
<BarChart
current={barCurrent}
+5 -5
View File
@@ -57,7 +57,7 @@ export default function ManagerPage() {
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex w-full cursor-pointer items-center gap-3 rounded-xl border-none px-3 py-2.5 text-sm font-medium transition-all ${
className={`flex w-full cursor-pointer items-center gap-3 rounded-xl border-none px-3 py-2.5 text-sm font-medium transition-all focus-visible:ring-2 focus-visible:ring-(--color-primary)/50 focus-visible:outline-none ${
activeTab === tab.id
? "bg-(--color-primary) text-white shadow-sm"
: "hover:bg-background bg-transparent text-(--color-text-secondary) hover:text-(--color-primary-dark)"
@@ -82,7 +82,7 @@ export default function ManagerPage() {
</p>
<Link
href="/manager/analytics"
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark)"
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark) focus-visible:ring-2 focus-visible:ring-(--color-primary)/50 focus-visible:outline-none"
>
<i className="fa-solid fa-chart-line w-4 text-center"></i>
<span className="flex-1 text-left">Tài chính</span>
@@ -92,7 +92,7 @@ export default function ManagerPage() {
</Link>
<Link
href="/staff/schedule"
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark)"
className="hover:bg-background flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-(--color-text-secondary) no-underline transition-all hover:text-(--color-primary-dark) focus-visible:ring-2 focus-visible:ring-(--color-primary)/50 focus-visible:outline-none"
>
<i className="fa-solid fa-calendar-days w-4 text-center"></i>
<span className="flex-1 text-left">Ca làm</span>
@@ -125,7 +125,7 @@ export default function ManagerPage() {
</Link>
<button
onClick={logout}
className="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border-none bg-transparent py-2 text-xs font-medium text-red-500 transition hover:bg-red-50"
className="flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border-none bg-transparent py-2 text-xs font-medium text-red-500 transition hover:bg-red-50 focus-visible:ring-2 focus-visible:ring-red-400/50 focus-visible:outline-none"
>
<i className="fa-solid fa-right-from-bracket"></i>
Đăng xuất
@@ -158,7 +158,7 @@ export default function ManagerPage() {
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex cursor-pointer items-center gap-1.5 rounded-xl border-none px-3 py-2 text-xs font-medium transition ${
className={`flex cursor-pointer items-center gap-1.5 rounded-xl border-none px-3 py-2 text-xs font-medium transition focus-visible:ring-2 focus-visible:ring-(--color-primary)/50 focus-visible:outline-none ${
activeTab === tab.id
? "bg-(--color-primary) text-white"
: "bg-background text-(--color-text-secondary) hover:text-(--color-primary)"
+109 -18
View File
@@ -24,12 +24,16 @@ app/
│ ├── layout.tsx # Feed layout
│ └── feed/
│ └── page.tsx # Trang khám phá quán (/feed)
── (manager)/
├── layout.tsx # Manager layout - auth guard + ManagerProvider
└── manager/
├── page.tsx # Manager Dashboard - quản lý thực đơn (/manager)
└── analytics/
└── page.tsx # Financial Analytics (/manager/analytics)
── (manager)/
├── layout.tsx # Manager layout - auth guard + ManagerProvider
└── manager/
├── page.tsx # Manager Dashboard - quản lý thực đơn (/manager)
└── analytics/
└── page.tsx # Financial Analytics (/manager/analytics)
└── (staff)/
└── staff/
└── schedule/
└── page.tsx # Staff Shift Schedule (/staff/schedule)
```
---
@@ -241,7 +245,7 @@ registration flow. Two-step process: phone verification → account creation.
**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.
summary aside (PaymentSummaryCard) with payment actions and modal flows.
#### Key Features
@@ -251,27 +255,46 @@ summary aside with payment actions.
- Empty state message when cart is empty
- Horizontal scroll on small screens (min-w-190)
- **Invoice Aside:**
- **PaymentSummaryCard (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)
- Shows all cart items with subtotals and grand total
- Payment method buttons: Tiền mặt (Cash), QR Code
- **"Đá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
#### Payment Modal Flow
- **Cash Payment (CashPaymentModal):**
- Opened when user clicks "Tiền mặt"
- Input: cash received amount
- Shows change due or insufficient cash error inline
- Displays invoice item list
- Confirm → opens PaymentSuccessModal
- **QR Payment (QRPaymentModal):**
- Opened when user clicks "QR Code"
- Generates QR code via `qrcode.react` (`QRCodeSVG` component)
- 5-second countdown timer — auto-confirms on expiry
- Displays invoice item list
- Confirm → opens PaymentSuccessModal
- **PaymentSuccessModal:**
- Shown after confirming Cash or QR payment
- Displays success confirmation message
- Clears cart via `clearCart()` from CartContext on close/confirm
- **ReviewModal:**
- Opened when a customer clicks "Đánh giá"
- 5-star rating + textarea for comment
- Closed via "Quay lại" button, backdrop click, or after submitting
- See `components/ReviewModal.tsx` for full documentation
#### Context Usage
- **useCart()** — items, totalPrice, increaseQty, decreaseQty, removeFromCart,
setQuantity
setQuantity, clearCart
- **useAuth()** — user (to check `user.role === "customer"` for review button
visibility)
@@ -279,6 +302,9 @@ summary aside with payment actions.
```tsx
isReviewOpen: boolean; // Controls ReviewModal visibility
isCashModalOpen: boolean; // Controls CashPaymentModal visibility
isQRModalOpen: boolean; // Controls QRPaymentModal visibility
isSuccessModalOpen: boolean; // Controls PaymentSuccessModal visibility
isCustomer: boolean; // Derived from user.role === "customer"
```
@@ -502,13 +528,78 @@ categoryFilter: string; // category id or "all"
---
## (staff) Route Group
### Layout
**Description:** Route group for staff-specific pages. Uses `StaffLayout` from
`components/templates/staff-layout/` which provides the staff navigation shell.
---
### Staff Shift Schedule (app/(staff)/staff/schedule/page.tsx)
**Route:** `/staff/schedule` **Type:** Client component **Description:** Weekly
and monthly shift schedule management for staff members.
#### Key Features
- **Weekly Schedule View (WeeklySchedule.tsx):**
- Grid-based view of shifts for each day of the current week
- Shows shift time, role, and department per cell
- Click a shift to open ShiftDetailModal
- **Monthly Calendar View (MonthlyCalendar.tsx):**
- Full month calendar grid
- Shifts rendered as colored chips per day
- Navigate between months
- **Mobile Shift View (MobileShiftView.tsx):**
- Optimized list-based view for small screens
- Shows upcoming shifts for the current/selected date
- **ShiftCreateModal:**
- Form to create a new shift: date, start/end time, role, department
- Validates time range before submitting
- **ShiftDetailModal:**
- Shows full details of a selected shift
- Options to edit or delete the shift
- **Department Filter:**
- Dropdown to filter schedule view by department
#### Context Usage
- **useShift()** (lib/shift-context.tsx) — shifts, createShift, updateShift,
deleteShift, selectedDate, setSelectedDate
#### State Management
```tsx
activeView: "weekly" | "monthly"; // Toggle between calendar views
isCreateModalOpen: boolean; // Controls ShiftCreateModal
selectedShift: Shift | null; // Shift passed to ShiftDetailModal
departmentFilter: string; // Currently selected department
```
#### Responsive Behavior
- **Mobile (<768px):** MobileShiftView displayed instead of grid calendars
- **Tablet/Desktop (768px+):** WeeklySchedule or MonthlyCalendar displayed
- **Department filter:** Sticky top bar on all screen sizes
---
## Future Enhancements
- [x] Payment page implementation
- [x] Customer review modal (ReviewModal) with 5-star rating + textarea
- [x] Manager dashboard (menu management)
- [x] Financial Analytics dashboard (charts, profit analysis)
- [x] Staff shift schedule (weekly/monthly views, CRUD, department filter)
- [x] Cash and QR payment modals with invoice display
- [x] Image upload for products in manager dashboard
- [ ] Order history/tracking page
- [ ] User profile page
- [ ] Cart checkout flow
- [ ] Real backend API integration
+180 -7
View File
@@ -7,14 +7,31 @@
```
components/
├── atoms/ # Basic building blocks (Button, Input, Text, Badge...)
├── molecules/ # Groups of atoms (ProductCard, SearchBar...)
├── organisms/ # Complex UI sections (CategorySidebar, CartFab, ProductGrid, ReviewModal, analytics charts...)
└── templates/ # Page layout structures (MainLayout, AuthLayout...)
├── atoms/ # Basic building blocks — 14 atoms (Button, Input, Text, Badge...) — see atoms/ATOMS.md
├── molecules/ # Groups of atoms — 5 molecules (ProductCard, ShopCard, ShiftCard, SearchBar, PaymentSummaryCard)
├── organisms/ # Complex UI sections — 27 organisms (analytics, cart, forms, manager, modals, navigation, grids, shift-schedule)
└── templates/ # Page layout structures — 5 templates (MainLayout, AuthLayout, FeedLayout, ManagerLayout, StaffLayout)
```
---
## ATOMS
See [`atoms/ATOMS.md`](atoms/ATOMS.md) for full atom documentation (14 atoms).
**Summary of atom groups:**
| Folder | Atoms |
| ----------- | -------------------------------------------- |
| buttons/ | Button, IconButton |
| inputs/ | TextInput, SearchInput, Textarea, LoginInput |
| typography/ | Heading, Text, Caption |
| badges/ | Badge, PriceBadge |
| dividers/ | Divider |
| errors/ | ErrorMessageLogin |
---
## MOLECULES
### ProductCard (`molecules/cards/ProductCard.tsx`)
@@ -25,16 +42,58 @@ parent grid (w-full).
**Atomic level:** Molecule (composed of Button, Caption, Text atoms)
---
### ShopCard (`molecules/cards/ShopCard.tsx`)
**Description:** Shop discovery card. Displays shop image, name, address, and a
link to the menu.
**Atomic level:** Molecule
---
### ShiftCard (`molecules/cards/ShiftCard.tsx`)
**Description:** Card displaying a single shift slot. Shows start/end time,
duration, wage, status badge, and registered staff list. Supports a compact mode
for use in calendar grids.
**Atomic level:** Molecule
**Props:**
```ts
interface ShiftCardProps {
shift: ShiftSlot; // Shift slot data (status, times, staff, etc.)
compact?: boolean; // Compact mode for calendar cells (default: false)
onClick?: (shift: ShiftSlot) => void; // Click handler
}
```
**Status variants:** `available` (blue), `registered` (dark blue),
`approved_leave` (purple), `absent` (red)
**Used in:** `WeeklySchedule`, `MobileShiftView`, `MonthlyCalendar`
---
### SearchBar (`molecules/search-bar/SearchBar.tsx`)
**Description:** Search input with clear button. Controlled component — value
and onChange come from parent.
**Atomic level:** Molecule (wraps SearchInput atom)
---
### PaymentSummaryCard (`molecules/cards/PaymentSummaryCard.tsx`)
**Description:** Summary card displayed on the payment page. Shows cart items,
subtotal, and payment method selection. Triggers cash or QR payment modals.
**Atomic level:** Molecule
---
## ORGANISMS
@@ -44,25 +103,35 @@ and onChange come from parent.
**Description:** Left sidebar with collapsible category filter. Sticky below
header, full viewport height minus header.
---
### CartFab (`organisms/cart/CartFab.tsx`)
**Description:** Floating Action Button displaying cart item count. Links to
/payment page.
---
### ProductGrid (`organisms/product-grid/ProductGrid.tsx`)
**Description:** Full product grid organism with mobile category tabs,
filtering, and empty state. Uses useCart and useMenu contexts.
---
### ShopGrid (`organisms/shop-grid/ShopGrid.tsx`)
**Description:** Responsive grid of ShopCard molecules for the feed/discovery
page.
### ReviewModal (`organisms/modals/ReviewModal.tsx`)
---
**Description:** Modal for customer reviews. Shows 5-star rating and textarea.
After submission, displays thank-you message.
### LoginForm (`organisms/forms/LoginForm.tsx`)
**Description:** Login form organism. Uses LoginInput and ErrorMessageLogin
atoms. Handles admin/staff/customer login flows via useAuth context.
---
### Analytics Charts (`organisms/analytics/`)
@@ -81,6 +150,101 @@ dashboard. All components are interactive with hover tooltips.
---
### Manager Organisms (`organisms/manager/`)
Components for the manager dashboard product/category/combo management.
| Component | File | Description |
| ---------------- | ----------------- | ------------------------------------------------------- |
| CategoriesTab | CategoriesTab.tsx | Category list with add/edit/delete actions |
| CategoryModal | CategoryModal.tsx | Modal form to create/edit a category |
| ComboModal | ComboModal.tsx | Modal form to create/edit a combo |
| CombosTab | CombosTab.tsx | Combo list with add/edit/delete actions |
| DeleteConfirm | DeleteConfirm.tsx | Generic delete confirmation dialog |
| ProductModal | ProductModal.tsx | Modal form to create/edit a product (with image upload) |
| ProductsTab | ProductsTab.tsx | Product list with add/edit/delete actions |
| StatusBadge | StatusBadge.tsx | Badge showing active/inactive status |
| Manager.types.ts | Manager.types.ts | Shared TypeScript interfaces for manager components |
---
### Modals (`organisms/modals/`)
| Component | File | Description |
| ------------------- | ----------------------- | --------------------------------------------------------- |
| ReviewModal | ReviewModal.tsx | 5-star rating and textarea review submission modal |
| CashPaymentModal | CashPaymentModal.tsx | Cash payment — input received amount, calculate change |
| QRPaymentModal | QRPaymentModal.tsx | QR code payment — generates QR, 5s countdown auto-confirm |
| PaymentSuccessModal | PaymentSuccessModal.tsx | Success screen — auto-redirects to home after 1 second |
| Modal.types.ts | Modal.types.ts | Shared TypeScript interfaces for all modal components |
#### CashPaymentModal
**Props:**
```ts
interface CashPaymentModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
items: InvoiceItem[]; // { name, quantity, price }[]
totalAmount: number;
}
```
- ARIA: `role="dialog"`, `aria-modal="true"`,
`aria-labelledby="cash-modal-title"`
- Shows invoice list, cash received input, change due calculation, insufficient
cash error
#### QRPaymentModal
**Props:**
```ts
interface QRPaymentModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
items: InvoiceItem[];
totalAmount: number;
}
```
- Uses `qrcode.react` (`QRCodeSVG`) to render a QR code
- 5-second countdown timer — auto-calls `onConfirm` when it reaches 0
- ARIA: `role="dialog"`, `aria-modal="true"`, `aria-labelledby="qr-modal-title"`
#### PaymentSuccessModal
**Props:**
```ts
interface PaymentSuccessModalProps {
isOpen: boolean;
onClose: () => void;
}
```
- Auto-redirects to `/` after 1 second via `useRouter`
- ARIA: `role="dialog"`, `aria-modal="true"`
---
### Shift Schedule (`organisms/shift-schedule/`)
Components for the staff shift scheduling feature.
| Component | File | Description |
| ---------------- | -------------------- | ----------------------------------------------------- |
| WeeklySchedule | WeeklySchedule.tsx | Weekly calendar grid showing shifts per day |
| MonthlyCalendar | MonthlyCalendar.tsx | Monthly calendar view with shift indicators per day |
| MobileShiftView | MobileShiftView.tsx | Mobile-optimized list/card view of shifts |
| ShiftCreateModal | ShiftCreateModal.tsx | Form modal for registering for or creating a shift |
| ShiftDetailModal | ShiftDetailModal.tsx | Detail view of a shift with its registered staff list |
---
## TEMPLATES
### MainLayout (`templates/main-layout/MainLayout.tsx`)
@@ -102,6 +266,11 @@ login/register pages.
**Description:** Manager layout template — auth guard + ManagerProvider. Used by
the manager route group.
### StaffLayout (`templates/staff-layout/`)
**Description:** Staff layout template — wraps staff-facing pages (shift
schedule, etc.). Used by the staff route group.
---
## Contexts Documentation
@@ -125,3 +294,7 @@ mobile menu and CategorySidebar selection.
Manages menu CRUD state (products, combos, categories) for the manager
dashboard.
---
**Last Updated:** 2026-04-18
+131 -35
View File
@@ -1,7 +1,7 @@
# Atoms Components Library
**Status:**Phase 1 Implementation Complete **Created:** 2026-04-03
**Foundation:** Atomic Design Pattern
**Status:**Complete — 14 atoms **Created:** 2026-04-03 **Foundation:**
Atomic Design Pattern
---
@@ -22,7 +22,7 @@ potentially other atoms).
---
## 🎯 Atoms Created
## 🎯 Atoms Created (14 total)
### Buttons (`buttons/`)
@@ -181,6 +181,58 @@ import { Textarea } from "@/components/atoms";
---
#### **LoginInput.tsx**
Specialized input for the login form. Supports username/phone and password
fields. Password fields include a built-in show/hide toggle button. Displays a
user icon on desktop viewports.
**Props:**
```ts
interface LoginInputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string; // Field label (rendered as <label>)
type: string; // Input type — "text" | "password"
name: string; // Input id and name (used for label htmlFor)
value: string; // Controlled value
errors?: string; // Error string — adds red border when set
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
```
**Usage:**
```tsx
import LoginInput from "@/components/atoms/inputs/LoginInput";
<LoginInput
label="Tên đăng nhập"
type="text"
name="username"
value={username}
errors={errors.username}
onChange={(e) => setUsername(e.target.value)}
/>
<LoginInput
label="Mật khẩu"
type="password"
name="password"
value={password}
errors={errors.password}
onChange={(e) => setPassword(e.target.value)}
/>
```
**Notes:**
- Password type shows a show/hide toggle button (ARIA-labelled)
- Desktop: renders a `fa-user` icon at left via `lg:block`
- Error state: red border via `border-red-400` when `errors` is set
- Used in: `LoginForm.tsx`
---
### Typography (`typography/`)
#### **Heading.tsx**
@@ -346,6 +398,50 @@ import { Divider } from "@/components/atoms";
---
### Errors (`errors/`)
#### **ErrorMessageLogin.tsx**
Displays error messages in the login form. Two visual styles controlled by the
`type` prop: a prominent alert box (`primary`) and a compact inline message
(`secondary`). Both include a `fa-circle-exclamation` icon.
**Props:**
```ts
interface ErrorMessageLoginProps {
message: string; // Error message text to display
type?: string; // "primary" (default) | "secondary"
}
```
**Variants:**
- `primary` (default): Rounded alert box — red border, red-50 background,
`text-red-600`, `text-sm`. Used for form-level errors (e.g. wrong password).
- `secondary`: Inline paragraph — `text-red-500`, `text-xs`. Used for
field-level errors below individual inputs.
**Usage:**
```tsx
import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
{
/* Form-level error */
}
<ErrorMessageLogin message="Tên đăng nhập hoặc mật khẩu không đúng" />;
{
/* Field-level inline error */
}
<ErrorMessageLogin message="Vui lòng nhập mật khẩu" type="secondary" />;
```
**Used in:** `LoginForm.tsx`
---
## 🎨 Theming
All atoms use CSS variables for consistent theming:
@@ -378,7 +474,8 @@ Change one variable in `globals.css` to update all atoms across the app.
import { Badge, PriceBadge } from "@/components/atoms/badges";
import { Button } from "@/components/atoms/buttons";
import { Divider } from "@/components/atoms/dividers";
import { SearchInput, TextInput } from "@/components/atoms/inputs";
import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
import { LoginInput, SearchInput, TextInput } from "@/components/atoms/inputs";
import { Caption, Heading, Text } from "@/components/atoms/typography";
```
@@ -400,6 +497,9 @@ import {
} from "@/components/atoms";
```
> Note: `ErrorMessageLogin` and `LoginInput` are imported directly from their
> subfolders as they are auth-specific and not re-exported from the root barrel.
### Type Imports
```tsx
@@ -483,50 +583,46 @@ All atoms include:
```
components/atoms/
├── buttons/
│ ├── Button.tsx
│ ├── Button.types.ts
│ ├── IconButton.tsx
│ └── index.ts
├── inputs/
│ ├── TextInput.tsx
│ ├── SearchInput.tsx
│ ├── Textarea.tsx
│ ├── Input.types.ts
│ └── index.ts
├── typography/
│ ├── Heading.tsx
│ ├── Text.tsx
│ ├── Caption.tsx
│ ├── Typography.types.ts
│ └── index.ts
├── badges/
│ ├── Badge.tsx
│ ├── Badge.types.ts
│ ├── PriceBadge.tsx
│ └── index.ts
├── buttons/
│ ├── Button.tsx
│ ├── Button.types.ts
│ ├── IconButton.tsx
│ └── index.ts
├── dividers/
│ ├── Divider.tsx
│ └── index.ts
├── errors/ ← auth error display
│ ├── Error.types.ts
│ └── ErrorMessageLogin.tsx
├── inputs/
│ ├── Input.types.ts
│ ├── LoginInput.tsx ← auth-specific input
│ ├── SearchInput.tsx
│ ├── Textarea.tsx
│ ├── TextInput.tsx
│ └── index.ts
├── typography/
│ ├── Caption.tsx
│ ├── Heading.tsx
│ ├── Text.tsx
│ ├── Typography.types.ts
│ └── index.ts
├── index.ts (barrel export)
└── ATOMS.md (this file)
```
---
## 🚀 Next Steps
## 🚀 Status
Once atoms are comfortable, the next phase is creating **Molecules** -
combinations of atoms:
- **ProductCard** - Image + Text + Badge + Button
- **FormField** - Label + Input + Error Text
- **SearchBar** - SearchInput + Button
- **RatingInput** - Interactive 5-star rating
- **PriceTag** - Price display with formatting
- And more...
See `OPTIMIZATION_PLAN.md` for the full roadmap.
Atoms are stable and in use across molecules and organisms. Molecules are
implemented — see `components/COMPONENTS.md` for details. See
`OPTIMIZATION_PLAN.md` for the full roadmap.
---
@@ -540,5 +636,5 @@ See `OPTIMIZATION_PLAN.md` for the full roadmap.
---
**Status:** ✅ Complete & Ready for Use **Last Updated:** 2026-04-03 **Phase:**
1 of 5
**Status:** ✅ Complete & Ready for Use **Last Updated:** 2026-04-18 **Atoms:**
14
+1
View File
@@ -24,6 +24,7 @@ export default function TextInput({
/>
{icon && (
<button
title="Thao tác biểu tượng"
type="button"
onClick={onIconClick}
className="absolute top-1/2 right-3 -translate-y-1/2 text-(--color-text-muted) transition-colors hover:text-(--color-primary)"
+4 -3
View File
@@ -1,4 +1,5 @@
import type { Product } from "@/lib/types";
import type { CartItem } from "@/lib/cart-context";
import type { Product, UserRole } from "@/lib/types";
export interface ProductCardProps {
image: string;
@@ -18,8 +19,8 @@ export interface ShopCardProps {
}
export interface PaymentSummaryCardProps {
cartItems: CartItem[];
totalPrice: number;
isCustomer: boolean;
role: UserRole;
backHref: string;
onPay?: () => void;
}
@@ -1,25 +1,46 @@
"use client";
import Button from "@/components/atoms/buttons/Button";
import { ReviewModal } from "@/components/organisms/modals";
import {
CashPaymentModal,
PaymentSuccessModal,
QRPaymentModal,
} from "@/components/organisms/modals";
import type { InvoiceItem } from "@/components/organisms/modals";
import { useCart } from "@/lib/cart-context";
import Link from "next/link";
import { useState } from "react";
import type { PaymentSummaryCardProps } from "./Card.types";
type ModalState = "none" | "cash" | "qr" | "success";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
export default function PaymentSummaryCard({
cartItems,
totalPrice,
isCustomer = false,
role,
backHref,
}: PaymentSummaryCardProps) {
const [isReviewOpen, setIsReviewOpen] = useState(false);
const handlePayment = () => {
// UI-only: open review modal after "payment"
if (isCustomer) {
setIsReviewOpen(true);
}
const [openModal, setOpenModal] = useState<ModalState>("none");
const { clearCart } = useCart();
const isCustomer = role === "customer";
const invoiceItems: InvoiceItem[] = cartItems.map((item) => ({
name: item.name,
quantity: item.quantity,
price: item.price,
}));
const handlePaymentConfirm = () => setOpenModal("success");
const handleSuccessClose = () => {
clearCart();
setOpenModal("none");
};
return (
<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">
@@ -33,45 +54,32 @@ export default function PaymentSummaryCard({
</div>
<div className="mt-4 grid grid-cols-2 gap-3">
<Button
style="payment"
onClick={handlePayment}
icon="fa-solid fa-money-bill-wave"
size="md"
variant="primary"
>
Tiền mặt
</Button>
{!isCustomer && (
<Button
style="payment"
onClick={() => setOpenModal("cash")}
icon="fa-solid fa-money-bill-wave"
size="md"
variant="primary"
>
Tiền mặt
</Button>
)}
<Button
style="payment"
onClick={handlePayment}
onClick={() => setOpenModal("qr")}
icon="fa-solid fa-qrcode"
size="md"
variant="secondary"
variant={isCustomer ? "primary" : "secondary"}
className={isCustomer ? "col-span-2" : ""}
>
QR Code
</Button>
{isCustomer && (
<Link href={backHref || "/"} className="col-span-2">
<Button
style="payment"
onClick={handlePayment}
icon="fa-solid fa-star"
size="md"
variant="primary"
>
Đánh giá
</Button>
)}
<Link
href={backHref || "/"}
className={isCustomer ? "" : "col-span-2"}
>
<Button
style="payment"
onClick={() => setIsReviewOpen(false)}
icon="fa-solid fa-arrow-rotate-left"
size="md"
variant="secondary"
@@ -83,9 +91,25 @@ export default function PaymentSummaryCard({
</div>
</div>
<ReviewModal
isOpen={isReviewOpen}
onClose={() => setIsReviewOpen(false)}
<CashPaymentModal
isOpen={openModal === "cash"}
onClose={() => setOpenModal("none")}
onConfirm={handlePaymentConfirm}
items={invoiceItems}
totalAmount={totalPrice}
/>
<QRPaymentModal
isOpen={openModal === "qr"}
onClose={() => setOpenModal("none")}
onConfirm={handlePaymentConfirm}
items={invoiceItems}
totalAmount={totalPrice}
/>
<PaymentSuccessModal
isOpen={openModal === "success"}
onClose={handleSuccessClose}
/>
</aside>
);
+3 -3
View File
@@ -4,7 +4,7 @@ import LoginInput from "@/components/atoms/inputs/LoginInput";
import { useAuth } from "@/lib/auth-context";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { SubmitEvent, useState } from "react";
import { FormEvent, useState } from "react";
export default function LoginForm() {
const router = useRouter();
@@ -39,12 +39,12 @@ export default function LoginForm() {
return isValid;
};
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
const success = await login(username, password);
const success = login(username, password);
if (success) {
router.push("/");
+94 -6
View File
@@ -1,7 +1,8 @@
"use client";
import type { Product } from "@/lib/types";
import { useState } from "react";
import Image from "next/image";
import { useRef, useState } from "react";
import type { ProductModalProps } from "./Manager.types";
@@ -12,6 +13,10 @@ export default function ProductModal({
onClose,
}: ProductModalProps) {
const isEdit = product !== null;
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [priceError, setPriceError] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const [form, setForm] = useState<Omit<Product, "id">>({
name: product?.name ?? "",
category: product?.category ?? categories[0]?.id ?? "",
@@ -21,8 +26,28 @@ export default function ProductModal({
available: product?.available ?? true,
});
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
setImageFile(file);
if (file) {
setImagePreview(URL.createObjectURL(file));
} else {
setImagePreview(null);
}
};
const handleRemoveImage = () => {
setImageFile(null);
setImagePreview(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (form.price <= 0) {
setPriceError("Giá phải lớn hơn 0đ");
return;
}
if (isEdit && product) {
onSave({ ...form, id: product.id });
} else {
@@ -93,15 +118,19 @@ export default function ProductModal({
<input
required
type="number"
min={0}
min={1}
step={1000}
value={form.price}
onChange={(e) =>
setForm({ ...form, price: Number(e.target.value) })
}
className={inputCls}
onChange={(e) => {
setPriceError("");
setForm({ ...form, price: Number(e.target.value) });
}}
className={`${inputCls} ${priceError ? "border-red-400 focus:border-red-400 focus:ring-red-400/20" : ""}`}
placeholder="25000"
/>
{priceError && (
<p className="mt-1 text-xs text-red-500">{priceError}</p>
)}
</div>
</div>
@@ -120,6 +149,65 @@ export default function ProductModal({
/>
</div>
<div>
<label
className="mb-1 block text-sm font-medium text-(--color-text-secondary)"
htmlFor="product-image-upload"
>
nh sản phẩm
</label>
<input
ref={fileInputRef}
id="product-image-upload"
type="file"
accept="image/*"
onChange={handleImageChange}
className="sr-only"
tabIndex={0}
/>
{imagePreview ? (
<div className="relative h-40 overflow-hidden rounded-xl border border-(--color-border)">
<Image
src={imagePreview}
alt="Xem trước ảnh sản phẩm"
fill
unoptimized
className="object-cover"
/>
<button
type="button"
onClick={handleRemoveImage}
title="Xóa ảnh"
className="absolute top-2 right-2 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full border-none bg-black/50 text-xs text-white transition hover:bg-black/70"
>
<i className="fa-solid fa-xmark"></i>
</button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="absolute right-2 bottom-2 cursor-pointer rounded-lg border-none bg-black/50 px-3 py-1 text-xs text-white transition hover:bg-black/70"
>
Đi nh
</button>
</div>
) : (
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-(--color-border) bg-transparent px-4 py-6 text-(--color-text-muted) transition hover:border-(--color-primary) hover:text-(--color-primary)"
>
<i className="fa-solid fa-cloud-arrow-up text-2xl"></i>
<span className="text-sm font-medium">Nhấn đ chọn nh</span>
<span className="text-xs">PNG, JPG, WEBP (tối đa 5MB)</span>
</button>
)}
{imageFile && (
<p className="mt-1 truncate text-xs text-(--color-text-muted)">
{imageFile.name}
</p>
)}
</div>
<div className="bg-background flex items-center justify-between rounded-xl border border-(--color-border-light) px-4 py-3">
<div>
<p className="text-foreground text-sm font-medium">Trạng thái</p>
+9 -1
View File
@@ -60,6 +60,7 @@ export default function ProductsTab() {
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Tìm kiếm món..."
aria-label="Tìm kiếm món"
className="text-foreground w-full rounded-xl border border-(--color-border) bg-white py-2 pr-9 pl-9 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20"
/>
{search && (
@@ -78,6 +79,7 @@ export default function ProductsTab() {
onChange={(e) => setFilterCategory(e.target.value)}
className="text-foreground cursor-pointer rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary)"
title="Lọc theo danh mục"
aria-label="Lọc theo danh mục"
>
<option value="all">Tất cả danh mục</option>
{categories.map((cat) => (
@@ -96,6 +98,7 @@ export default function ProductsTab() {
}
className="text-foreground cursor-pointer rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary)"
title="Lọc theo trạng thái"
aria-label="Lọc theo trạng thái"
>
<option value="all">Tất cả trạng thái</option>
<option value="available">Còn hàng</option>
@@ -160,7 +163,10 @@ export default function ProductsTab() {
<div>
<p className="text-foreground font-medium">{p.name}</p>
{p.description && (
<p className="mt-0.5 max-w-xs truncate text-xs text-(--color-text-muted)">
<p
title={p.description}
className="mt-0.5 max-w-xs truncate text-xs text-(--color-text-muted)"
>
{p.description}
</p>
)}
@@ -191,6 +197,7 @@ export default function ProductsTab() {
<button
onClick={() => setModalProduct(p)}
title="Chỉnh sửa"
aria-label={`Chỉnh sửa: ${p.name}`}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-(--color-border-light) bg-transparent text-(--color-text-muted) transition hover:border-(--color-primary-light) hover:text-(--color-primary)"
>
<i className="fa-solid fa-pen text-xs"></i>
@@ -198,6 +205,7 @@ export default function ProductsTab() {
<button
onClick={() => setDeleteTarget(p)}
title="Xóa"
aria-label={`Xóa: ${p.name}`}
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-transparent text-(--color-text-muted) transition hover:border-red-200 hover:bg-red-50 hover:text-red-500"
>
<i className="fa-solid fa-trash text-xs"></i>
+1 -1
View File
@@ -6,7 +6,7 @@ export default function StatusBadge({ available }: StatusBadgeProps) {
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${
available
? "bg-emerald-100 text-emerald-700"
: "bg-amber-100 text-amber-700"
: "bg-amber-100 text-amber-800"
}`}
>
<span
@@ -0,0 +1,144 @@
"use client";
import Button from "@/components/atoms/buttons/Button";
import { useEffect, useState } from "react";
import type { CashPaymentModalProps } from "./Modal.types";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
export default function CashPaymentModal({
isOpen,
onClose,
onConfirm,
items,
totalAmount,
}: CashPaymentModalProps) {
const [cashReceived, setCashReceived] = useState("");
useEffect(() => {
if (isOpen) setCashReceived("");
}, [isOpen]);
if (!isOpen) return null;
const parsedCash = cashReceived === "" ? 0 : Number(cashReceived);
const isInsufficient = cashReceived !== "" && parsedCash < totalAmount;
const changeAmount = parsedCash - totalAmount;
const canConfirm = cashReceived !== "" && !isInsufficient && parsedCash >= 0;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="cash-modal-title"
>
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
aria-hidden="true"
/>
<div className="relative w-full max-w-lg rounded-2xl border border-(--color-border-light) bg-white p-6 shadow-xl">
<h2
id="cash-modal-title"
className="text-foreground mb-4 text-xl font-bold"
>
Thanh toán tiền mặt
</h2>
{/* Invoice list */}
<div className="mb-4">
<h3 className="mb-2 text-sm font-semibold tracking-wide text-(--color-text-secondary) uppercase">
Hóa đơn
</h3>
<div className="overflow-hidden rounded-xl border border-(--color-border-light)">
{items.map((item, index) => (
<div
key={index}
className="flex items-center justify-between border-b border-(--color-border-light) px-4 py-2.5 last:border-b-0"
>
<span className="text-foreground font-medium">{item.name}</span>
<div className="flex items-center gap-3">
<span className="text-sm text-(--color-text-muted)">
x{item.quantity}
</span>
<span className="font-semibold text-(--color-primary)">
{formatPrice(item.price * item.quantity)}
</span>
</div>
</div>
))}
<div className="flex items-center justify-between bg-(--color-border-light)/30 px-4 py-3">
<span className="text-foreground font-bold">Tổng cộng</span>
<span className="text-lg font-bold text-(--color-primary)">
{formatPrice(totalAmount)}
</span>
</div>
</div>
</div>
{/* Cash input section */}
<div className="mb-5">
<label
htmlFor="cash-input"
className="mb-1.5 block text-sm font-medium text-(--color-text-secondary)"
>
Số tiền nhận
</label>
<input
id="cash-input"
type="number"
min="0"
step={1000}
value={cashReceived}
onChange={(e) => setCashReceived(e.target.value)}
placeholder="0"
className="w-full rounded-lg border border-(--color-border) bg-transparent px-3 py-2 text-right text-lg font-semibold focus:ring-2 focus:ring-(--color-primary) focus:outline-none"
/>
{isInsufficient && (
<p className="mt-1.5 text-sm text-red-500">
<i className="fa-solid fa-triangle-exclamation mr-1"></i>
Số tiền không đ
</p>
)}
{cashReceived !== "" && !isInsufficient && (
<div className="mt-2 flex items-center justify-between rounded-lg bg-(--color-border-light)/40 px-3 py-2">
<span className="text-sm text-(--color-text-secondary)">
Tiền thối
</span>
<span className="font-semibold text-(--color-primary)">
{formatPrice(changeAmount)}
</span>
</div>
)}
</div>
{/* Footer buttons */}
<div className="flex gap-3">
<Button
type="button"
variant="secondary"
size="md"
icon="fa-solid fa-arrow-left"
className="flex-1"
onClick={onClose}
>
Quay lại
</Button>
<Button
type="button"
variant="primary"
size="md"
icon="fa-solid fa-check"
className="flex-1"
onClick={onConfirm}
disabled={!canConfirm}
>
Xác nhận
</Button>
</div>
</div>
</div>
);
}
@@ -12,3 +12,30 @@ export interface ConfirmModalProps {
confirmLabel?: string;
cancelLabel?: string;
}
export interface InvoiceItem {
name: string;
quantity: number;
price: number;
}
export interface CashPaymentModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
items: InvoiceItem[];
totalAmount: number;
}
export interface QRPaymentModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
items: InvoiceItem[];
totalAmount: number;
}
export interface PaymentSuccessModalProps {
isOpen: boolean;
onClose: () => void;
}
@@ -0,0 +1,54 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
interface PaymentSuccessModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function PaymentSuccessModal({
isOpen,
onClose,
}: PaymentSuccessModalProps) {
const router = useRouter();
useEffect(() => {
if (!isOpen) return;
const timer = setTimeout(() => {
router.push("/");
onClose();
}, 1000);
return () => clearTimeout(timer);
}, [isOpen, router, onClose]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
aria-hidden="true"
/>
{/* Modal */}
<div className="relative w-full max-w-sm rounded-2xl border border-(--color-border-light) bg-white p-8 shadow-xl">
<div className="flex flex-col items-center gap-4 text-center">
<i className="fa-solid fa-circle-check text-5xl text-(--color-primary)"></i>
<h2 className="text-foreground text-xl font-bold">
Thanh toán thành công!
</h2>
<p className="text-sm text-(--color-text-muted)">
Cảm ơn quý khách. Đang chuyển về trang chủ...
</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,116 @@
"use client";
import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react";
import type { QRPaymentModalProps } from "./Modal.types";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
const COUNTDOWN_SECONDS = 5;
export default function QRPaymentModal({
isOpen,
onClose,
onConfirm,
items,
totalAmount,
}: QRPaymentModalProps) {
const [qrValue, setQrValue] = useState("");
const [timeLeft, setTimeLeft] = useState(COUNTDOWN_SECONDS);
useEffect(() => {
if (!isOpen) return;
setQrValue(`payment:${totalAmount}:${Date.now()}`);
setTimeLeft(COUNTDOWN_SECONDS);
const interval = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(interval);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isOpen, totalAmount]);
useEffect(() => {
if (timeLeft === 0 && isOpen) {
onConfirm();
}
}, [timeLeft, isOpen, onConfirm]);
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="qr-modal-title"
>
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
aria-hidden="true"
onClick={onClose}
/>
<div className="relative w-full max-w-lg rounded-2xl border border-(--color-border-light) bg-white p-6 shadow-xl">
<h2
id="qr-modal-title"
className="text-foreground mb-4 text-xl font-bold"
>
Thanh toán QR Code
</h2>
{/* Invoice list */}
<div className="mb-4">
<h3 className="mb-2 text-sm font-semibold tracking-wide text-(--color-text-secondary) uppercase">
Hóa đơn
</h3>
<div className="overflow-hidden rounded-xl border border-(--color-border-light)">
{items.map((item, index) => (
<div
key={index}
className="flex items-center justify-between border-b border-(--color-border-light) px-4 py-2.5 last:border-b-0"
>
<span className="text-foreground font-medium">{item.name}</span>
<div className="flex items-center gap-3">
<span className="text-sm text-(--color-text-muted)">
x{item.quantity}
</span>
<span className="font-semibold text-(--color-primary)">
{formatPrice(item.price * item.quantity)}
</span>
</div>
</div>
))}
<div className="flex items-center justify-between bg-(--color-border-light)/30 px-4 py-3">
<span className="text-foreground font-bold">Tổng cộng</span>
<span className="text-lg font-bold text-(--color-primary)">
{formatPrice(totalAmount)}
</span>
</div>
</div>
</div>
{/* QR code section */}
<div className="flex flex-col items-center gap-3 rounded-xl border border-(--color-border-light) px-4 py-5">
<p className="text-sm font-medium text-(--color-text-secondary)">
Quét QR đ thanh toán
</p>
{qrValue && <QRCodeSVG value={qrValue} size={180} />}
<p className="text-sm text-(--color-text-muted)">
Tự đng xác nhận sau{" "}
<span className="font-bold text-(--color-primary)">{timeLeft}</span>{" "}
giây...
</p>
</div>
</div>
</div>
);
}
+8 -5
View File
@@ -1,7 +1,7 @@
"use client";
import { Button, Heading, Text, Textarea } from "@/components/atoms";
import { useState } from "react";
import { useEffect, useState } from "react";
import type { ReviewModalProps } from "./Modal.types";
@@ -11,6 +11,12 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
const [review, setReview] = useState("");
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (!submitted) return;
const timer = setTimeout(handleClose, 1000);
return () => clearTimeout(timer);
}, [submitted]);
if (!isOpen) return null;
const handleSubmit = () => {
@@ -54,9 +60,6 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
<Text variant="body2" className="mt-2">
Chúng tôi trân trọng đánh giá của bạn!
</Text>
<Button onClick={handleClose} variant="primary" className="mt-4">
Đóng
</Button>
</div>
) : (
/* Review form */
@@ -98,7 +101,7 @@ export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
className={
isActive
? "fa-solid fa-star text-yellow-400"
: "fa-regular fa-star text-(--color-border)"
: "fa-regular fa-star text-gray-300"
}
></i>
</button>
+5
View File
@@ -1,2 +1,7 @@
export { default as ReviewModal } from "./ReviewModal";
export { default as PaymentSuccessModal } from "./PaymentSuccessModal";
export { default as CashPaymentModal } from "./CashPaymentModal";
export { default as QRPaymentModal } from "./QRPaymentModal";
export type { ReviewModalProps, ConfirmModalProps } from "./Modal.types";
export type { CashPaymentModalProps, InvoiceItem } from "./Modal.types";
export type { QRPaymentModalProps } from "./Modal.types";
+1 -1
View File
@@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: frontend-container
image: git.demonkernel.io.vn/foodsurf/frontend:1.2.0
image: git.demonkernel.io.vn/foodsurf/frontend:1.1.2
ports:
- containerPort: 3000
resources:
+224 -276
View File
@@ -2,6 +2,8 @@
> Tài liệu chi tiết về layout components (Header, Footer) và responsive behavior
> **Last Updated:** 2026-04-18
---
## Overview
@@ -21,180 +23,197 @@ layouts/
## Header (layouts/header.tsx)
**File:** layouts/header.tsx **Type:** Client component **Description:** Sticky
top navigation bar with brand info and authentication status display.
**File:** layouts/header.tsx **Type:** Client component (`"use client"`)
**Description:** Sticky top navigation bar with brand info and authentication
status display. Uses `useAuth()` context for real auth state and `useRouter()`
for navigation.
### Layout
```
┌─────────────────────────────────────────────────────┐
│ [Logo] [Shop Name] [Tagline] │ [Auth Status]
│ [Logo] [Shop Name] [Tagline] │ [Auth Button(s)]
└─────────────────────────────────────────────────────┘
height: 72px (var(--spacing-header-height))
height: var(--spacing-header-height) (72px)
```
**2-Column Layout:**
- **Left:** Shop branding (logo + name + tagline)
- **Right:** Auth status indicator
- **Left:** Shop branding (logo + name + tagline) — wrapped in `<Link href="/">`
- **Right:** Auth status indicator (varies by user role)
### Features
#### Logo & Branding
- Shop logo (40x40px)
- Shop name (bold text)
- Shop logo rendered with `next/image` (fill, 40x40px / 44x44px on md+)
- Shop name (bold, color transitions on hover)
- Tagline (hidden on mobile < md, visible md+)
- Entire left section is a link to `/`
**Responsive:** | Breakpoint | Logo | Name | Tagline |
|-----------|------|------|---------| | < sm (640px) | Yes | Yes | Hidden | |
sm-md | Yes | Yes | Hidden | | md+ (768px) | Yes | Yes | Visible |
**Responsive:**
| Breakpoint | Logo | Name | Tagline |
| ------------ | --------- | ---- | ------- |
| < sm (640px) | h-10/w-10 | Yes | Hidden |
| sm-md | h-10/w-10 | Yes | Hidden |
| md+ (768px) | h-11/w-11 | Yes | Visible |
#### Authentication Status
The header cycles through 3 auth states (for UI demo):
The header reads from `useAuth()` and reflects the actual logged-in user role.
**State 1: Guest (Not logged in)**
**State 1: Guest (user = null)**
```
[brown button] "Dang nhap"
[brown button] "Đăng nhp" ← icon always shown, label hidden on xs
```
- Guest state (user = null)
- Brown primary button
- Click: toggles to next state
- `title="Đăng nhập"` on button
- Clicking navigates to `/login`
- Icon: `fa-solid fa-right-to-bracket`
**State 2: Manager**
**State 2: Manager (user.role === "manager")**
```
[caramel/gold badge] "👔 Quản lý"
[Dashboard link] [Logout icon button]
```
- Shows manager role
- Gold/caramel colored badge
- User tie icon
- Name: Nguyễn Văn An
- Dashboard link → `/manager`
- Icon: `fa-solid fa-user-tie`
- Label "Dashboard" hidden on xs, visible sm+
- Logout button: `title="Đăng xuất"`, `aria-label="Đăng xuất"`
- Logout icon: `fa-solid fa-right-from-bracket`
- Hover: red tint on logout button
**State 3: Staff**
**State 3: Staff (user.role === "staff")**
```
[border button] "👤 [Name]"
[Ca làm link] [Avatar + Name button (logout on click)]
```
- Shows staff role with avatar placeholder
- Bordered button style
- Name: Trần Thị Bình
- Click: toggles back to Guest state
- Ca làm link → `/staff/schedule`
- Icon: `fa-solid fa-calendar-check`
- Label "Ca làm" hidden on xs, visible sm+
- Staff button: `title="Nhấn để đăng xuất"`, `aria-label="Đăng xuất"`
- Avatar: colored circle with `fa-solid fa-user`
- Name hidden on xs, visible sm+
**State 4: Customer (user.role === "customer")**
```
[Customer icon + "Khách hàng" button (logout on click)]
```
- `title="Khách hàng - {user.phone} - Nhấn để đăng xuất"`
- Avatar circle with `fa-solid fa-user`
- Label "Khách hàng" hidden on xs, visible sm+
### Responsive Behavior
| Breakpoint | Layout |
| ------------- | ------------------------------- |
| < sm | Stacked vertically |
| sm-md (640px) | Side-by-side, reduced spacing |
| md+ (768px) | Full width, comfortable spacing |
| Breakpoint | Layout |
| ------------- | -------------------------------------- |
| < sm (640px) | Button labels hidden, icons only |
| sm-md (640px) | Button labels visible, reduced spacing |
| md+ (768px) | Full layout including tagline |
**Mobile (<640px):**
- Logo smaller
- Name only (no tagline)
- Auth button text hidden, icon shown
- Logo 40x40px
- Shop name only (no tagline)
- Auth button shows icon only (label hidden via `hidden sm:inline`)
- Touch-friendly padding
**Desktop (≥640px):**
**Desktop (≥768px):**
- Normal logo size
- Logo 44x44px
- Full layout with tagline
- Button with text and icon
- Buttons show icon + text label
- Comfortable spacing
### Accessibility
- Logo image: `alt="Logo {SHOP_INFO.name}"`
- Guest button: `title="Đăng nhập"`
- Manager logout button: `title="Đăng xuất"`, `aria-label="Đăng xuất"`
- Staff logout button: `title="Nhấn để đăng xuất"`, `aria-label="Đăng xuất"`
- Customer button: `title="Khách hàng - {phone} - Nhấn để đăng xuất"`
- Icons are presentational (no aria-hidden needed — buttons have
title/aria-label)
### 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` |
| Element | Classes |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Header wrapper | `sticky top-0 z-50 h-(--spacing-header-height) w-full border-b bg-(--color-bg-header) shadow-[...]` |
| Container | `mx-auto flex h-full max-w-screen-2xl items-center justify-between gap-6 px-6 md:px-8 lg:px-12` |
| Left section (brand) | `group flex shrink-0 items-center gap-4 no-underline` (Link) |
| Logo wrapper | `relative h-10 w-10 shrink-0 md:h-11 md:w-11` |
| Brand text | `text-base font-bold text-(--color-primary-dark) md:text-lg` |
| Tagline | `hidden text-xs text-(--color-text-muted) md:block` |
| Right section (auth) | `flex shrink-0 items-center gap-3` |
| Guest button | `flex items-center gap-2.5 rounded-xl bg-(--color-primary) px-5 py-2.5 text-sm font-semibold text-white` |
| Manager link | `flex items-center gap-2 rounded-xl border border-(--color-accent) bg-(--color-accent-light)` |
| Logout button | `flex items-center gap-2 rounded-xl border border-(--color-border) bg-transparent px-3 py-2.5 hover:border-red-300 hover:bg-red-50 hover:text-red-500` |
| Staff button | `flex items-center gap-2.5 rounded-xl border border-(--color-border) bg-background` |
| Customer button | `flex items-center gap-2.5 rounded-xl bg-(--color-primary-light) px-4 py-2 text-white` |
### 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;
--spacing-header-height /* 72px — header height */
--color-bg-header /* header background */
--color-border /* border color */
--color-shadow-sm /* subtle shadow */
--color-primary /* brand primary (brown) */
--color-primary-dark /* darker brown */
--color-primary-light /* lighter brown */
--color-accent /* accent/gold color */
--color-accent-light /* light accent background */
--color-text-muted /* gray/muted text */
--color-text-secondary /* secondary text color */
--color-border-light /* light border */
```
### 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>
```
- **next/image** Logo image rendering
- **next/link** — Brand link, dashboard link, schedule link
- **next/navigation** — `useRouter` for programmatic navigation
- **lib/auth-context:** `useAuth` — user state, logout
- **lib/constants:** `SHOP_INFO`
---
## Footer (layouts/footer.tsx)
**File:** layouts/footer.tsx **Type:** Client component (uses constants)
**File:** layouts/footer.tsx **Type:** Server component (no `"use client"`)
**Description:** Site footer with shop information, social links, and WiFi
details.
details. Copyright year is dynamic via `new Date().getFullYear()`.
### Layout
```
┌────────────────────────────────────────────┐
│ [Brand Info] │ [Social + WiFi] │
├────────────────────────────────────────────┤
│ © 2024 Coffee Shop. Made with ❤️ in VN
└────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────
│ [Brand Info] │ [Social + WiFi]
├────────────────────────────────────────────────────────────────
│ © {year} Coffee Shop. All rights reserved. | Powered by ...
└────────────────────────────────────────────────────────────────
```
**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 |
| Breakpoint | Brand Info | Right column (Social + WiFi) |
| ------------ | ----------- | -------------------------------- |
| Mobile | col-span-12 | col-span-12 |
| md (768px) | col-span-6 | col-span-6 |
| lg+ (1024px) | col-span-8 | col-span-4 |
| xl+ | col-span-6 | col-span-6 (with 2-col sub-grid) |
The right column (Social + WiFi) uses an internal sub-grid:
- Mobile/lg: stacked (1 column)
- xl+: side-by-side (2 columns)
### Sections
@@ -202,179 +221,114 @@ details.
**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
- Shop logo (40x40px, `next/image`)
- Shop name (bold, accent color)
- Tagline (gray, 75% opacity)
- Address with `fa-solid fa-location-dot`
- Phone (clickable `tel:` link) with `fa-solid fa-phone`
- Email (clickable `mailto:` link) with `fa-solid fa-envelope`
- Open hours with `fa-solid fa-clock`
**Responsive:**
#### 2. Social Links
```
Mobile:
└─ Logo
└─ Name & Tagline (stacked)
└─ Address
└─ Phone
└─ Email
└─ Open Hours
**Links (heading: "Kết nối"):**
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
- Facebook → `SOCIAL_LINKS.facebook` (branded blue icon, opens `_blank`)
- TikTok → `SOCIAL_LINKS.tiktok` (black icon, opens `_blank`)
- Website → `SOCIAL_LINKS.website` (primary color icon, uses `next/link`)
**Styling:**
- Icons in circle backgrounds
- Hover effect (color change)
- Flex row layout
- Centered alignment
- Each item: icon badge (8x8 rounded-lg) + label text
- Hover: accent color + full opacity
#### 3. WiFi Card (Bottom Right)
#### 3. WiFi Card
**Content:**
**Content (heading: "WiFi Miễn Phí"):**
- "📶 WiFi miễn phí" label
- Network name (monospace)
- Password (monospace, can be hidden/shown)
- WiFi icon (`fa-solid fa-wifi`)
- Network name (monospace, accent-bordered)
- Password (monospace, accent-bordered, always visible — no toggle)
**Note:** Current implementation shows password in plain text (no hide/show
toggle). This differs from older documentation.
**Styling:**
- Light gray background
- Rounded borders
- Semi-transparent dark panel (`bg-(--color-primary-dark) bg-opacity-30`)
- Rounded xl border
- Stacked label + value rows (no overflow risk)
- Monospace font for credentials
- Eye icon toggle for password visibility
**Example:**
```
📶 WiFi miễn phí
┌────────────────────────┐
│ Network: CoffeeShop │
│ Password: •••••••• │
│ (eye icon) │
└────────────────────────┘
```
#### 4. Copyright Bar (Bottom)
#### 4. Bottom Bar
**Content:**
- Copyright text: "© 2024 Coffee Shop"
- "Made with ❤️ in Vietnam"
- Centered
**Styling:**
- Small text
- Gray color
- Subtle separator line above
- Left: `© {year} {SHOP_INFO.name}. All rights reserved.`
- Right: `Được vận hành ❤ bằng Drinkool`
- Responsive: stacked on mobile, side-by-side on sm+
### Responsive Behavior
**Mobile (<768px):**
- Single column layout
- Stacked sections
- Stacked sections (brand, then social+WiFi)
- Full width
- Padding around edges
**Tablet (768px):**
- 2-column layout
- Brand info left, Social + WiFi right
- Equal width columns
- 2-column: brand (6 cols) | social+WiFi (6 cols)
- Social and WiFi stacked within right column
**Desktop (≥768px):**
**Desktop (≥1024px):**
- 2-column layout with more spacing
- Brand info slightly smaller
- Comfortable padding
- Brand 8 cols, right 4 cols
- Social and WiFi stacked in right col
**XL (≥1280px):**
- Brand 6 cols, right 6 cols
- Social and WiFi side-by-side in right col sub-grid
### 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` |
| Element | Classes |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| Footer wrapper | `w-full overflow-x-hidden bg-(--color-bg-footer) text-(--color-text-on-dark)` |
| Container | `mx-auto max-w-screen-2xl px-4 py-10 md:px-6 lg:px-8` |
| Grid | `grid grid-cols-12 gap-x-8 gap-y-8` |
| Brand section | `col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-6` |
| Right section | `col-span-12 grid grid-cols-1 gap-6 md:col-span-6 lg:col-span-4 xl:col-span-6 xl:grid-cols-2` |
| Logo | `relative h-10 w-10 shrink-0` |
| Shop name | `text-lg font-bold text-(--color-accent)` |
| Tagline | `text-sm leading-relaxed opacity-75` |
| Contact list | `flex flex-col gap-2 text-sm opacity-80` |
| Social icon badge | `flex h-8 w-8 shrink-0 items-center justify-center rounded-lg` |
| WiFi card | `rounded-xl border border-(--color-primary-light) bg-(--color-primary-dark) p-4` |
| WiFi value | `rounded border border-(--color-accent) px-2 py-1 font-mono font-bold text-(--color-accent)` |
| Bottom bar | `border-t border-white border-opacity-10` |
| Copyright row | `mx-auto flex max-w-screen-2xl flex-col sm:flex-row items-center justify-between gap-2 px-4 py-4 text-xs opacity-50` |
### 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;
--color-bg-footer /* dark footer background */
--color-text-on-dark /* light text for dark backgrounds */
--color-accent /* accent/gold color */
--color-primary /* brand primary (brown) */
--color-primary-dark /* darker brown */
--color-primary-light /* lighter brown */
--color-border /* border color */
```
### 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>
```
- **next/image** Shop logo
- **next/link** — Website social link
- **lib/constants:** `SHOP_INFO`, `SOCIAL_LINKS`
- **FontAwesome icons:** `location-dot`, `phone`, `envelope`, `clock`,
`fa-brands fa-facebook-f`, `fa-brands fa-tiktok`, `globe`, `wifi`, `heart`
---
@@ -398,7 +352,7 @@ export default function RootLayout({ children }) {
**Result:**
- Header: Always at top (sticky)
- Header: Always at top (sticky, z-50)
- Content: Takes full width between header/footer
- Footer: Always at bottom
@@ -409,30 +363,22 @@ export default function RootLayout({ children }) {
### Header Responsive Pattern
```css
/* Mobile first */
.header {
flex-direction: column;
gap: 1rem;
padding: 1rem;
/* Mobile first — icon-only auth buttons */
.header-auth-label {
display: none; /* hidden on xs */
}
/* Tablet+ */
/* sm+ — show button labels */
@media (min-width: 640px) {
.header {
flex-direction: row;
justify-content: space-between;
padding: 1.5rem;
.header-auth-label {
display: inline; /* hidden sm:inline */
}
}
/* Desktop+ */
/* md+ — show tagline */
@media (min-width: 768px) {
.header {
padding: 2rem;
}
.tagline {
display: block; /* Show tagline on desktop */
display: block; /* hidden md:block */
}
}
```
@@ -440,17 +386,14 @@ export default function RootLayout({ children }) {
### Footer Responsive Pattern
```css
/* Mobile: stacked */
/* Mobile: single column */
.footer-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(12, 1fr);
}
/* Tablet: 2 columns */
@media (min-width: 768px) {
.footer-grid {
grid-template-columns: 1fr 1fr;
}
}
/* md: brand 6 cols, right 6 cols */
/* lg: brand 8 cols, right 4 cols */
/* xl: brand 6 cols, right 6 cols — right col uses xl:grid-cols-2 */
```
---
@@ -483,33 +426,38 @@ CSS variables are set up for dark mode support. To enable:
### Header
- Logo has alt text
- Auth button has aria-label
- Icons have semantic meaning
- Good contrast ratios
- Logo image has descriptive `alt` text: `"Logo {SHOP_INFO.name}"`
- Guest button: `title="Đăng nhập"`
- Manager logout button: `title="Đăng xuất"` + `aria-label="Đăng xuất"`
- Staff logout button: `title="Nhấn để đăng xuất"` + `aria-label="Đăng xuất"`
- Customer button: `title="Khách hàng - {phone} - Nhấn để đăng xuất"`
- Interactive elements have hover and active states (visual feedback)
- Touch targets are sized appropriately (px-4/5 + py-2/2.5)
### Footer
- Headings use semantic tags
- Links have descriptive text
- Icons are decorative (aria-hidden)
- Monospace font for technical info (WiFi credentials)
- Phone and email are clickable `<a>` links (`tel:`, `mailto:`)
- Social links open in `_blank` with `rel="noopener noreferrer"`
- Section headings use `<h3>` semantic tags
- Icons are decorative within labelled list items
- WiFi credentials in monospace font for readability
### General
- Touch targets ≥ 48px on mobile
- Sufficient color contrast
- Semantic HTML structure
- ARIA labels where needed
- Touch targets ≥ 44px (py-2.5 + px-4/5 buttons)
- `active:scale-95` on interactive buttons for tactile feedback
- Semantic HTML structure (`<header>`, `<footer>`, `<nav>` implicit via links)
- ARIA labels on icon-only buttons (logout buttons)
---
## Best Practices
1. **Keep layouts simple:** Avoid complex nested layouts
2. **Responsive first:** Use mobile-first CSS approach
2. **Responsive first:** Use mobile-first CSS approach (`hidden sm:inline`)
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
6. **Performance:** Optimize images with next/image (`fill`, `sizes`,
`priority`)
7. **Accessibility:** Add title/aria-label on icon-only buttons
+9 -24
View File
@@ -12,7 +12,7 @@ import type { User } from "./types";
interface AuthContextType {
user: User | null;
login: (username: string, password: string) => Promise<boolean>;
login: (username: string, password: string) => boolean;
logout: () => void;
registerPhone: string | null;
setRegisterPhone: (phone: string | null) => void;
@@ -112,31 +112,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}, []);
const login = async (phone: string, password: string): Promise<boolean> => {
try {
const response = await fetch("/api/customer/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phone, password }),
});
const login = (username: string, password: string): boolean => {
const authEntry = MOCK_AUTH_DB[username as keyof typeof MOCK_AUTH_DB];
if (response.ok) {
const userData = await response.json();
setUser(userData);
localStorage.setItem("coffee-shop-user", JSON.stringify(userData));
return true;
} else {
console.error("Đăng nhập thất bại:", response.statusText);
return false;
}
} catch (error) {
console.error("Lỗi kết nối API:", error);
return false;
if (authEntry && authEntry.password === password) {
setUser(authEntry.user);
localStorage.setItem("coffee-shop-user", JSON.stringify(authEntry.user));
return true;
}
return false;
};
const logout = () => {
+4
View File
@@ -20,6 +20,7 @@ interface CartContextValue {
decreaseQty: (id: number) => void;
removeFromCart: (id: number) => void;
setQuantity: (id: number, quantity: number) => void;
clearCart: () => void;
}
const STORAGE_KEY = "coffee-shop-cart";
@@ -91,6 +92,8 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const clearCart = () => setItems([]);
const setQuantity = (id: number, quantity: number) => {
const safeQty = Number.isFinite(quantity)
? Math.max(0, Math.floor(quantity))
@@ -127,6 +130,7 @@ export function CartProvider({ children }: { children: React.ReactNode }) {
decreaseQty,
removeFromCart,
setQuantity,
clearCart,
}),
[items, totalItems, totalPrice],
);
+38 -10
View File
@@ -1,18 +1,19 @@
{
"name": "temp",
"version": "1.2.0",
"version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "temp",
"version": "1.2.0",
"version": "1.1.2",
"dependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^20.19.37",
"@types/node": "^20.19.39",
"@types/react": "^19.2.14",
"next": "16.1.7",
"nextjs": "^0.0.3",
"qrcode.react": "^4.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind": "^4.0.0",
@@ -27,7 +28,7 @@
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "16.1.7",
"prettier": "^3.8.1",
"prettier": "^3.8.2",
"prettier-plugin-embed": "^0.5.1",
"prettier-plugin-groovy": "^0.2.1",
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -126,6 +127,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1381,6 +1383,7 @@
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
@@ -2380,6 +2383,7 @@
"integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@babel/generator": "^7.28.0",
"@babel/parser": "^7.28.0",
@@ -2495,9 +2499,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
"version": "20.19.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -2515,6 +2519,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2584,6 +2589,7 @@
"integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.2",
"@typescript-eslint/types": "8.57.2",
@@ -3109,6 +3115,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3731,6 +3738,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5238,6 +5246,7 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5423,6 +5432,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -8394,6 +8404,7 @@
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -10806,6 +10817,7 @@
"dev": true,
"inBundle": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11611,11 +11623,12 @@
}
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -11834,6 +11847,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@@ -11930,6 +11952,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11939,6 +11962,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -12304,6 +12328,7 @@
"integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/error": "^4.0.0",
@@ -13561,6 +13586,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -13786,6 +13812,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14437,6 +14464,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "temp",
"version": "1.2.0",
"version": "1.1.2",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,6 +15,8 @@
"@types/node": "^20.19.39",
"@types/react": "^19.2.14",
"next": "16.1.7",
"nextjs": "^0.0.3",
"qrcode.react": "^4.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind": "^4.0.0",
@@ -29,7 +31,7 @@
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "16.1.7",
"prettier": "^3.8.3",
"prettier": "^3.8.2",
"prettier-plugin-embed": "^0.5.1",
"prettier-plugin-groovy": "^0.2.1",
"prettier-plugin-tailwindcss": "^0.7.2",
+91 -64
View File
@@ -20,9 +20,6 @@ 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)
nextjs:
specifier: ^0.0.3
version: 0.0.3
react:
specifier: 19.2.3
version: 19.2.3
@@ -50,7 +47,7 @@ importers:
version: 13.1.5(semantic-release@25.0.3(typescript@5.9.3))
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
version: 6.0.2(prettier@3.8.3)
version: 6.0.2(prettier@3.8.2)
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.14)
@@ -61,17 +58,17 @@ importers:
specifier: 16.1.7
version: 16.1.7(@typescript-eslint/parser@8.58.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)
prettier:
specifier: ^3.8.3
version: 3.8.3
specifier: ^3.8.2
version: 3.8.2
prettier-plugin-embed:
specifier: ^0.5.1
version: 0.5.1
prettier-plugin-groovy:
specifier: ^0.2.1
version: 0.2.1(prettier@3.8.3)
version: 0.2.1(prettier@3.8.2)
prettier-plugin-tailwindcss:
specifier: ^0.7.2
version: 0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3)
version: 0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.2))(prettier@3.8.2)
semantic-release:
specifier: ^25.0.3
version: 25.0.3(typescript@5.9.3)
@@ -174,11 +171,11 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/core@1.9.2':
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/runtime@1.9.2':
resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
@@ -267,89 +264,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -416,24 +429,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.7':
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.1.7':
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.1.7':
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.7':
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
@@ -631,24 +648,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
@@ -843,41 +864,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -1063,8 +1092,8 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
baseline-browser-mapping@2.10.19:
resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
baseline-browser-mapping@2.10.18:
resolution: {integrity: sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1142,8 +1171,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001788:
resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==}
caniuse-lite@1.0.30001787:
resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
chalk@2.4.1:
resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==}
@@ -1434,8 +1463,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.340:
resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==}
electron-to-chromium@1.5.336:
resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -1591,11 +1620,11 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
eslint-plugin-react-hooks@7.1.0:
resolution: {integrity: sha512-LDicyhrRFrIaheDYryeM2W8gWyZXnAs4zIr2WVPiOSeTmIu2RjR4x/9N0xLaRWZ+9hssBDGo3AadcohuzAvSvg==}
eslint-plugin-react-hooks@7.0.1:
resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
engines: {node: '>=18'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
eslint-plugin-react@7.37.5:
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
@@ -1860,8 +1889,8 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
get-tsconfig@4.13.7:
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
git-log-parser@1.2.1:
resolution: {integrity: sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==}
@@ -2337,24 +2366,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -2616,10 +2649,6 @@ packages:
sass:
optional: true
nextjs@0.0.3:
resolution: {integrity: sha512-mYbDUo4/sRAZ8TqK63PCpYnFiLg7BICG/ot9+guOrUKd4/Fo71ZmEQ41IZbH6nqbQvG7SXTBuofJXAIWfNho0w==}
engines: {node: '>=0.8.21'}
nocache@2.0.0:
resolution: {integrity: sha512-YdKcy2x0dDwOh+8BEuHvA+mnOKAhmMQDgKBOCUGaLpewdmsRYguYZSom3yA+/OrE61O/q+NMQANnun65xpI1Hw==}
@@ -2963,8 +2992,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.10:
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
postcss@8.5.9:
resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -3035,8 +3064,8 @@ packages:
prettier-plugin-svelte:
optional: true
prettier@3.8.3:
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
prettier@3.8.2:
resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==}
engines: {node: '>=14'}
hasBin: true
@@ -3605,8 +3634,8 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@6.25.0:
resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
undici@6.24.1:
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
engines: {node: '>=18.17'}
undici@7.25.0:
@@ -3816,7 +3845,7 @@ snapshots:
'@actions/http-client@4.0.0':
dependencies:
tunnel: 0.0.6
undici: 6.25.0
undici: 6.24.1
'@actions/io@3.0.2': {}
@@ -3937,13 +3966,13 @@ snapshots:
'@colors/colors@1.5.0':
optional: true
'@emnapi/core@1.10.0':
'@emnapi/core@1.9.2':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.10.0':
'@emnapi/runtime@1.9.2':
dependencies:
tslib: 2.8.1
optional: true
@@ -4095,7 +4124,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
'@emnapi/runtime': 1.9.2
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -4128,8 +4157,8 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@emnapi/core': 1.9.2
'@emnapi/runtime': 1.9.2
'@tybys/wasm-util': 0.10.1
optional: true
@@ -4439,10 +4468,10 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
postcss: 8.5.10
postcss: 8.5.9
tailwindcss: 4.2.2
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.2)':
dependencies:
'@babel/generator': 7.29.1
'@babel/parser': 7.29.2
@@ -4452,7 +4481,7 @@ snapshots:
lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
prettier: 3.8.3
prettier: 3.8.2
transitivePeerDependencies:
- supports-color
@@ -4845,7 +4874,7 @@ snapshots:
balanced-match@4.0.4: {}
baseline-browser-mapping@2.10.19: {}
baseline-browser-mapping@2.10.18: {}
basic-auth@2.0.1:
dependencies:
@@ -4895,9 +4924,9 @@ snapshots:
browserslist@4.28.2:
dependencies:
baseline-browser-mapping: 2.10.19
caniuse-lite: 1.0.30001788
electron-to-chromium: 1.5.340
baseline-browser-mapping: 2.10.18
caniuse-lite: 1.0.30001787
electron-to-chromium: 1.5.336
node-releases: 2.0.37
update-browserslist-db: 1.2.3(browserslist@4.28.2)
@@ -4941,7 +4970,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001788: {}
caniuse-lite@1.0.30001787: {}
chalk@2.4.1:
dependencies:
@@ -5226,7 +5255,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.340: {}
electron-to-chromium@1.5.336: {}
emoji-regex@10.6.0: {}
@@ -5382,7 +5411,7 @@ snapshots:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.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-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.1.0(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.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
optionalDependencies:
@@ -5406,7 +5435,7 @@ snapshots:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 9.39.4(jiti@2.6.1)
get-tsconfig: 4.14.0
get-tsconfig: 4.13.7
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.16
@@ -5475,7 +5504,7 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@7.1.0(eslint@9.39.4(jiti@2.6.1)):
eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.2
@@ -5853,7 +5882,7 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
get-tsconfig@4.14.0:
get-tsconfig@4.13.7:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -6550,8 +6579,8 @@ snapshots:
dependencies:
'@next/env': 16.1.7
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.19
caniuse-lite: 1.0.30001788
baseline-browser-mapping: 2.10.18
caniuse-lite: 1.0.30001787
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
@@ -6570,8 +6599,6 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
nextjs@0.0.3: {}
nocache@2.0.0: {}
node-emoji@2.2.0:
@@ -6835,7 +6862,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.10:
postcss@8.5.9:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -6854,17 +6881,17 @@ snapshots:
transitivePeerDependencies:
- babel-plugin-macros
prettier-plugin-groovy@0.2.1(prettier@3.8.3):
prettier-plugin-groovy@0.2.1(prettier@3.8.2):
dependencies:
prettier: 3.8.3
prettier: 3.8.2
prettier-plugin-tailwindcss@0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3))(prettier@3.8.3):
prettier-plugin-tailwindcss@0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.2))(prettier@3.8.2):
dependencies:
prettier: 3.8.3
prettier: 3.8.2
optionalDependencies:
'@trivago/prettier-plugin-sort-imports': 6.0.2(prettier@3.8.3)
'@trivago/prettier-plugin-sort-imports': 6.0.2(prettier@3.8.2)
prettier@3.8.3: {}
prettier@3.8.2: {}
pretty-ms@9.3.0:
dependencies:
@@ -7590,7 +7617,7 @@ snapshots:
undici-types@6.21.0: {}
undici@6.25.0: {}
undici@6.24.1: {}
undici@7.25.0: {}