chore: release [ci skip]
This commit is contained in:
+39
-20
@@ -1,9 +1,7 @@
|
|||||||
# Phase 1 Implementation Summary: Atomic Design Foundation
|
# Phase 1 Implementation Summary: Atomic Design Foundation
|
||||||
|
|
||||||
**Status:** ✅ COMPLETE
|
**Status:** ✅ COMPLETE **Date:** 2026-04-03 **Commit:** eca619b **Branch:**
|
||||||
**Date:** 2026-04-03
|
atomic_design
|
||||||
**Commit:** eca619b
|
|
||||||
**Branch:** atomic_design
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,6 +10,7 @@
|
|||||||
### ✨ Created 21 Atom Components
|
### ✨ Created 21 Atom Components
|
||||||
|
|
||||||
#### Buttons (2 components)
|
#### Buttons (2 components)
|
||||||
|
|
||||||
```
|
```
|
||||||
components/atoms/buttons/
|
components/atoms/buttons/
|
||||||
├── Button.tsx (4 variants: primary, secondary, danger, ghost)
|
├── Button.tsx (4 variants: primary, secondary, danger, ghost)
|
||||||
@@ -20,12 +19,14 @@ components/atoms/buttons/
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
|
||||||
- Multiple size options (sm, md, lg)
|
- Multiple size options (sm, md, lg)
|
||||||
- Icon support with positioning (left/right)
|
- Icon support with positioning (left/right)
|
||||||
- Full HTML button attribute support
|
- Full HTML button attribute support
|
||||||
- Active/disabled states with visual feedback
|
- Active/disabled states with visual feedback
|
||||||
|
|
||||||
#### Inputs (3 components)
|
#### Inputs (3 components)
|
||||||
|
|
||||||
```
|
```
|
||||||
components/atoms/inputs/
|
components/atoms/inputs/
|
||||||
├── TextInput.tsx (with label, error, icon support)
|
├── TextInput.tsx (with label, error, icon support)
|
||||||
@@ -35,12 +36,14 @@ components/atoms/inputs/
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
|
||||||
- Error state handling with red border
|
- Error state handling with red border
|
||||||
- Icon integration with callback support
|
- Icon integration with callback support
|
||||||
- Accessible form field structure
|
- Accessible form field structure
|
||||||
- Controlled/uncontrolled patterns
|
- Controlled/uncontrolled patterns
|
||||||
|
|
||||||
#### Typography (3 components)
|
#### Typography (3 components)
|
||||||
|
|
||||||
```
|
```
|
||||||
components/atoms/typography/
|
components/atoms/typography/
|
||||||
├── Heading.tsx (h1-h6 with semantic sizing)
|
├── Heading.tsx (h1-h6 with semantic sizing)
|
||||||
@@ -50,11 +53,13 @@ components/atoms/typography/
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
|
||||||
- Semantic HTML (`<h1>` through `<h6>`)
|
- Semantic HTML (`<h1>` through `<h6>`)
|
||||||
- Consistent font sizing and weights
|
- Consistent font sizing and weights
|
||||||
- CSS variable color application
|
- CSS variable color application
|
||||||
|
|
||||||
#### Badges (2 components)
|
#### Badges (2 components)
|
||||||
|
|
||||||
```
|
```
|
||||||
components/atoms/badges/
|
components/atoms/badges/
|
||||||
├── Badge.tsx (5 variants: primary, secondary, success, danger, warning)
|
├── Badge.tsx (5 variants: primary, secondary, success, danger, warning)
|
||||||
@@ -63,11 +68,13 @@ components/atoms/badges/
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
|
||||||
- Multiple size options (sm, md)
|
- Multiple size options (sm, md)
|
||||||
- Formatted price output (VND/USD)
|
- Formatted price output (VND/USD)
|
||||||
- Color-coded variants
|
- Color-coded variants
|
||||||
|
|
||||||
#### Dividers (1 component)
|
#### Dividers (1 component)
|
||||||
|
|
||||||
```
|
```
|
||||||
components/atoms/dividers/
|
components/atoms/dividers/
|
||||||
├── Divider.tsx (horizontal/vertical separators)
|
├── Divider.tsx (horizontal/vertical separators)
|
||||||
@@ -108,12 +115,14 @@ components/atoms/dividers/
|
|||||||
### 🔧 Updated Existing Components
|
### 🔧 Updated Existing Components
|
||||||
|
|
||||||
#### CartProduct.tsx
|
#### CartProduct.tsx
|
||||||
|
|
||||||
- ✅ Replaced inline button with `<Button>` atom
|
- ✅ Replaced inline button with `<Button>` atom
|
||||||
- ✅ Replaced inline text with `<Text>` atom
|
- ✅ Replaced inline text with `<Text>` atom
|
||||||
- ✅ Replaced inline caption with `<Caption>` atom
|
- ✅ Replaced inline caption with `<Caption>` atom
|
||||||
- **Result:** Cleaner component, more maintainable
|
- **Result:** Cleaner component, more maintainable
|
||||||
|
|
||||||
#### ReviewModal.tsx
|
#### ReviewModal.tsx
|
||||||
|
|
||||||
- ✅ Replaced modal buttons with `<Button>` atoms
|
- ✅ Replaced modal buttons with `<Button>` atoms
|
||||||
- ✅ Replaced heading with `<Heading>` atom
|
- ✅ Replaced heading with `<Heading>` atom
|
||||||
- ✅ Replaced text with `<Text>` atom
|
- ✅ Replaced text with `<Text>` atom
|
||||||
@@ -124,15 +133,15 @@ components/atoms/dividers/
|
|||||||
|
|
||||||
## 📊 Statistics
|
## 📊 Statistics
|
||||||
|
|
||||||
| Metric | Count |
|
| Metric | Count |
|
||||||
|--------|-------|
|
| ------------------------- | --------------------- |
|
||||||
| **New Atoms Created** | 21 |
|
| **New Atoms Created** | 21 |
|
||||||
| **Type Files** | 5 |
|
| **Type Files** | 5 |
|
||||||
| **Total Component Files** | 26 |
|
| **Total Component Files** | 26 |
|
||||||
| **Documentation Files** | 5 |
|
| **Documentation Files** | 5 |
|
||||||
| **New Lines of Code** | ~2,500 (atoms + docs) |
|
| **New Lines of Code** | ~2,500 (atoms + docs) |
|
||||||
| **Build Status** | ✅ Success |
|
| **Build Status** | ✅ Success |
|
||||||
| **TypeScript Errors** | 0 (in atoms) |
|
| **TypeScript Errors** | 0 (in atoms) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -174,6 +183,7 @@ components/atoms/
|
|||||||
## ✅ Quality Assurance
|
## ✅ Quality Assurance
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
✓ Compiled successfully in 3.7s
|
✓ Compiled successfully in 3.7s
|
||||||
✓ npm run build: PASS
|
✓ npm run build: PASS
|
||||||
@@ -182,18 +192,21 @@ components/atoms/
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Type Safety
|
### Type Safety
|
||||||
|
|
||||||
- ✅ Full TypeScript coverage
|
- ✅ Full TypeScript coverage
|
||||||
- ✅ All props typed
|
- ✅ All props typed
|
||||||
- ✅ No `any` types
|
- ✅ No `any` types
|
||||||
- ✅ Interface exports for reuse
|
- ✅ Interface exports for reuse
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
- ✅ Semantic HTML throughout
|
- ✅ Semantic HTML throughout
|
||||||
- ✅ ARIA labels where needed
|
- ✅ ARIA labels where needed
|
||||||
- ✅ Focus visible states
|
- ✅ Focus visible states
|
||||||
- ✅ Keyboard navigation support
|
- ✅ Keyboard navigation support
|
||||||
|
|
||||||
### Consistency
|
### Consistency
|
||||||
|
|
||||||
- ✅ CSS variables for theming
|
- ✅ CSS variables for theming
|
||||||
- ✅ Consistent naming conventions
|
- ✅ Consistent naming conventions
|
||||||
- ✅ Barrel exports for clean imports
|
- ✅ Barrel exports for clean imports
|
||||||
@@ -204,27 +217,30 @@ components/atoms/
|
|||||||
## 📖 Usage Examples
|
## 📖 Usage Examples
|
||||||
|
|
||||||
### Basic Button
|
### Basic Button
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
|
|
||||||
<Button variant="primary" size="sm" icon="fa-cart-plus">
|
<Button variant="primary" size="sm" icon="fa-cart-plus">
|
||||||
Mua
|
Mua
|
||||||
</Button>
|
</Button>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Form Field
|
### Form Field
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { TextInput, Button } from "@/components/atoms";
|
import { Button, TextInput } from "@/components/atoms";
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
error={emailError}
|
error={emailError}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Typography
|
### Typography
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Heading, Text, Caption } from "@/components/atoms";
|
import { Heading, Text, Caption } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -234,6 +250,7 @@ import { Heading, Text, Caption } from "@/components/atoms";
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Price Display
|
### Price Display
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { PriceBadge } from "@/components/atoms";
|
import { PriceBadge } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -275,6 +292,7 @@ import { PriceBadge } from "@/components/atoms";
|
|||||||
## 🚀 Next Steps (Phase 2)
|
## 🚀 Next Steps (Phase 2)
|
||||||
|
|
||||||
### Molecules to Create
|
### Molecules to Create
|
||||||
|
|
||||||
1. **ProductCard** - Image + Text + Badge + Button
|
1. **ProductCard** - Image + Text + Badge + Button
|
||||||
2. **FormField** - Label + Input + Error + Validation
|
2. **FormField** - Label + Input + Error + Validation
|
||||||
3. **SearchBar** - SearchInput + Button
|
3. **SearchBar** - SearchInput + Button
|
||||||
@@ -288,6 +306,7 @@ import { PriceBadge } from "@/components/atoms";
|
|||||||
## 💡 Design Decisions
|
## 💡 Design Decisions
|
||||||
|
|
||||||
### Why Barrel Exports?
|
### Why Barrel Exports?
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// ❌ Bad: Scattered imports
|
// ❌ Bad: Scattered imports
|
||||||
import Button from "@/components/atoms/buttons/Button";
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
@@ -298,12 +317,14 @@ import { Button, Text } from "@/components/atoms";
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Why TypeScript Types in Separate Files?
|
### Why TypeScript Types in Separate Files?
|
||||||
|
|
||||||
- Clear separation of concerns
|
- Clear separation of concerns
|
||||||
- Easier to maintain interfaces
|
- Easier to maintain interfaces
|
||||||
- Reusable types across modules
|
- Reusable types across modules
|
||||||
- Better for large components
|
- Better for large components
|
||||||
|
|
||||||
### Why CSS Variables?
|
### Why CSS Variables?
|
||||||
|
|
||||||
- Single point for theme changes
|
- Single point for theme changes
|
||||||
- Easy dark mode switching
|
- Easy dark mode switching
|
||||||
- Consistent across all atoms
|
- Consistent across all atoms
|
||||||
@@ -357,7 +378,5 @@ feat: Implement Atomic Design Phase 1 - Atoms Foundation
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** Ready for Phase 2 🚀
|
**Status:** Ready for Phase 2 🚀 **Last Updated:** 2026-04-03 **Contributor:**
|
||||||
**Last Updated:** 2026-04-03
|
Claude Code + Anthropic AI
|
||||||
**Contributor:** Claude Code + Anthropic AI
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,15 @@ Dành cho khách hàng:
|
|||||||
|
|
||||||
Dành cho quản lý:
|
Dành cho quản lý:
|
||||||
|
|
||||||
- Sidebar desktop: Brand, tab navigation (Thực đơn / Combo / Danh mục), link tới Analytics
|
- 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
|
- CRUD sản phẩm, combo, danh mục qua modals
|
||||||
- Auth guard: tự động redirect non-manager về `/`
|
- Auth guard: tự động redirect non-manager về `/`
|
||||||
|
|
||||||
#### 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)
|
- Summary cards: Doanh thu, đơn hàng, lợi nhuận, giá trị đơn trung bình (so sánh
|
||||||
|
kỳ trước)
|
||||||
- Bộ chọn kỳ: Ngày / Tuần / Tháng / Năm
|
- Bộ chọn kỳ: Ngày / Tuần / Tháng / Năm
|
||||||
- Biểu đồ SVG thuần: Line, Bar, Pie — hover tooltips tương tác
|
- Biểu đồ SVG thuần: Line, Bar, Pie — hover tooltips tương tác
|
||||||
- Bảng top 5 sản phẩm và bảng chi tiết có thể sắp xếp
|
- Bảng top 5 sản phẩm và bảng chi tiết có thể sắp xếp
|
||||||
@@ -196,18 +198,18 @@ frondend/
|
|||||||
|
|
||||||
## Công Nghệ Sử Dụng
|
## Công Nghệ Sử Dụng
|
||||||
|
|
||||||
| Công nghệ | Phiên bản | Mục đích |
|
| Công nghệ | Phiên bản | Mục đích |
|
||||||
| ------------ | --------- | ------------------------------------- |
|
| ---------------- | --------- | ------------------------------------- |
|
||||||
| Next.js | 16.1.7 | React Framework (App Router) |
|
| Next.js | 16.1.7 | React Framework (App Router) |
|
||||||
| React | 19.2.3 | Thư viện UI |
|
| React | 19.2.3 | Thư viện UI |
|
||||||
| TypeScript | ^5 | Kiểu dữ liệu tĩnh |
|
| TypeScript | ^5 | Kiểu dữ liệu tĩnh |
|
||||||
| Tailwind CSS | ^4.2.2 | Utility-first CSS framework |
|
| Tailwind CSS | ^4.2.2 | Utility-first CSS framework |
|
||||||
| Geist Font | - | Font chữ (Google Fonts via next/font) |
|
| Geist Font | - | Font chữ (Google Fonts via next/font) |
|
||||||
| FontAwesome | 6.7.2 | Icon library (CDN) |
|
| FontAwesome | 6.7.2 | Icon library (CDN) |
|
||||||
| pnpm | - | Package manager |
|
| pnpm | - | Package manager |
|
||||||
| ESLint | ^9 | Linting |
|
| ESLint | ^9 | Linting |
|
||||||
| Prettier | ^3 | Code formatting |
|
| Prettier | ^3 | Code formatting |
|
||||||
| semantic-release | ^25 | Automated versioning & release |
|
| semantic-release | ^25 | Automated versioning & release |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -225,11 +227,11 @@ frondend/
|
|||||||
|
|
||||||
### Tài Khoản Demo
|
### Tài Khoản Demo
|
||||||
|
|
||||||
| Loại tài khoản | Tên đăng nhập | Mật khẩu |
|
| Loại tài khoản | Tên đăng nhập | Mật khẩu |
|
||||||
| -------------- | -------------- | -------------- |
|
| -------------- | ------------- | ------------- |
|
||||||
| Manager | admin | admin |
|
| Manager | admin | admin |
|
||||||
| Staff | Nguyễn Văn An | Nguyễn Văn An |
|
| Staff | Nguyễn Văn An | Nguyễn Văn An |
|
||||||
| Customer | 0987654321 | user1 |
|
| Customer | 0987654321 | user1 |
|
||||||
|
|
||||||
### Design & Styling
|
### Design & Styling
|
||||||
|
|
||||||
@@ -243,14 +245,18 @@ frondend/
|
|||||||
Dự án tuân theo **Atomic Design** pattern — chi tiết tại `Atomic.md`:
|
Dự án tuân theo **Atomic Design** pattern — chi tiết tại `Atomic.md`:
|
||||||
|
|
||||||
- **Atoms** - Nguyên tố cơ bản: Button, Input, Badge, Text, Heading, Divider
|
- **Atoms** - Nguyên tố cơ bản: Button, Input, Badge, Text, Heading, Divider
|
||||||
- **Molecules** - Nhóm atoms: ProductCard, ShopCard, SearchBar, PaymentSummaryCard
|
- **Molecules** - Nhóm atoms: ProductCard, ShopCard, SearchBar,
|
||||||
- **Organisms** - Phần UI phức tạp: CategorySidebar, CartFab, ProductGrid, ShopGrid, ReviewModal, analytics charts, manager tabs/modals
|
PaymentSummaryCard
|
||||||
- **Templates** - Bố cục trang: MainLayout, AuthLayout, FeedLayout, ManagerLayout
|
- **Organisms** - Phần UI phức tạp: CategorySidebar, CartFab, ProductGrid,
|
||||||
|
ShopGrid, ReviewModal, analytics charts, manager tabs/modals
|
||||||
|
- **Templates** - Bố cục trang: MainLayout, AuthLayout, FeedLayout,
|
||||||
|
ManagerLayout
|
||||||
|
|
||||||
### Data & Integration
|
### Data & Integration
|
||||||
|
|
||||||
- Mock data nằm trong `lib/constants.ts`
|
- Mock data nằm trong `lib/constants.ts`
|
||||||
- Context providers trong `app/providers.tsx`: AuthProvider, MenuProvider, CartProvider
|
- Context providers trong `app/providers.tsx`: AuthProvider, MenuProvider,
|
||||||
|
CartProvider
|
||||||
- ManagerProvider được thêm bởi `app/(manager)/layout.tsx`
|
- ManagerProvider được thêm bởi `app/(manager)/layout.tsx`
|
||||||
- Thay bằng API calls khi backend sẵn sàng
|
- Thay bằng API calls khi backend sẵn sàng
|
||||||
- Ảnh sản phẩm: thêm vào `public/imgs/products/`
|
- Ảnh sản phẩm: thêm vào `public/imgs/products/`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { SHOP_INFO } from "@/lib/constants";
|
|
||||||
import LoginForm from "@/components/organisms/forms/LoginForm";
|
import LoginForm from "@/components/organisms/forms/LoginForm";
|
||||||
|
import { SHOP_INFO } from "@/lib/constants";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
@@ -28,7 +28,7 @@ export default function LoginPage() {
|
|||||||
Đăng nhập vào hệ thống
|
Đăng nhập vào hệ thống
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Login Form */}
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
|
|
||||||
|
|||||||
+3
-6
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { SearchBar } from "@/components/molecules/search-bar";
|
||||||
import { CategorySidebar } from "@/components/organisms/navigation";
|
import { CategorySidebar } from "@/components/organisms/navigation";
|
||||||
import { ProductGrid } from "@/components/organisms/product-grid";
|
import { ProductGrid } from "@/components/organisms/product-grid";
|
||||||
import { SearchBar } from "@/components/molecules/search-bar";
|
|
||||||
import { useMenu } from "@/lib/menu-context";
|
|
||||||
import { MENU_CATEGORIES } from "@/lib/constants";
|
import { MENU_CATEGORIES } from "@/lib/constants";
|
||||||
|
import { useMenu } from "@/lib/menu-context";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,10 +72,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Product grid (organism handles mobile category menu + grid) ── */}
|
{/* ── Product grid (organism handles mobile category menu + grid) ── */}
|
||||||
<ProductGrid
|
<ProductGrid searchQuery={searchQuery} isSidebarOpen={isSidebarOpen} />
|
||||||
searchQuery={searchQuery}
|
|
||||||
isSidebarOpen={isSidebarOpen}
|
|
||||||
/>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function PaymentPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => decreaseQty(item.id)}
|
onClick={() => decreaseQty(item.id)}
|
||||||
className="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
aria-label={`Giảm số lượng ${item.name}`}
|
aria-label={`Giảm số lượng ${item.name}`}
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
@@ -92,7 +92,7 @@ export default function PaymentPage() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => increaseQty(item.id)}
|
onClick={() => increaseQty(item.id)}
|
||||||
className="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
aria-label={`Tăng số lượng ${item.name}`}
|
aria-label={`Tăng số lượng ${item.name}`}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import { SHOP_INFO } from "@/lib/constants";
|
import { SHOP_INFO } from "@/lib/constants";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import Button from "@/components/atoms/buttons/Button";
|
|
||||||
|
|
||||||
// Static OTP for demo (in production, this would be sent via SMS)
|
// Static OTP for demo (in production, this would be sent via SMS)
|
||||||
const DEMO_OTP = "123456";
|
const DEMO_OTP = "123456";
|
||||||
@@ -161,7 +161,12 @@ export default function RegisterPage() {
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button variant="primaryNoBorder" type="submit" style="login" size="lg">
|
<Button
|
||||||
|
variant="primaryNoBorder"
|
||||||
|
type="submit"
|
||||||
|
style="login"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
Tiếp tục
|
Tiếp tục
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -227,12 +232,22 @@ export default function RegisterPage() {
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button variant="primaryNoBorder" type="submit" style="login" size="lg">
|
<Button
|
||||||
|
variant="primaryNoBorder"
|
||||||
|
type="submit"
|
||||||
|
style="login"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
Hoàn tất đăng ký
|
Hoàn tất đăng ký
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<Button variant="bgWhite" onClick={handleBackToPhone} size="lg" style="login">
|
<Button
|
||||||
|
variant="bgWhite"
|
||||||
|
onClick={handleBackToPhone}
|
||||||
|
size="lg"
|
||||||
|
style="login"
|
||||||
|
>
|
||||||
Thay đổi số điện thoại
|
Thay đổi số điện thoại
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import {
|
|||||||
SummaryCard,
|
SummaryCard,
|
||||||
} from "@/components/organisms/analytics";
|
} from "@/components/organisms/analytics";
|
||||||
import type { PieSlice } from "@/components/organisms/analytics";
|
import type { PieSlice } from "@/components/organisms/analytics";
|
||||||
import { calcChange, formatCurrency, formatCurrencyFull } from "@/lib/analytics-utils";
|
import {
|
||||||
|
calcChange,
|
||||||
|
formatCurrency,
|
||||||
|
formatCurrencyFull,
|
||||||
|
} from "@/lib/analytics-utils";
|
||||||
import {
|
import {
|
||||||
MENU_CATEGORIES,
|
MENU_CATEGORIES,
|
||||||
MOCK_PRODUCT_SALES,
|
MOCK_PRODUCT_SALES,
|
||||||
@@ -31,8 +35,15 @@ const PERIOD_LABELS: Record<AnalyticsPeriod, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CATEGORY_COLORS = [
|
const CATEGORY_COLORS = [
|
||||||
"#6F4E37", "#C8973A", "#A0785A", "#8B6914", "#D4A96A",
|
"#6F4E37",
|
||||||
"#4A3728", "#F0D9A8", "#A08060", "#3D2B1F",
|
"#C8973A",
|
||||||
|
"#A0785A",
|
||||||
|
"#8B6914",
|
||||||
|
"#D4A96A",
|
||||||
|
"#4A3728",
|
||||||
|
"#F0D9A8",
|
||||||
|
"#A08060",
|
||||||
|
"#3D2B1F",
|
||||||
];
|
];
|
||||||
|
|
||||||
const REVENUE_MAP: Record<AnalyticsPeriod, RevenueDataPoint[]> = {
|
const REVENUE_MAP: Record<AnalyticsPeriod, RevenueDataPoint[]> = {
|
||||||
@@ -47,8 +58,8 @@ type ChartType = (typeof CHART_TYPES)[number];
|
|||||||
|
|
||||||
const CHART_META: Record<ChartType, { icon: string; label: string }> = {
|
const CHART_META: Record<ChartType, { icon: string; label: string }> = {
|
||||||
line: { icon: "fa-chart-line", label: "Line" },
|
line: { icon: "fa-chart-line", label: "Line" },
|
||||||
bar: { icon: "fa-chart-bar", label: "Bar" },
|
bar: { icon: "fa-chart-bar", label: "Bar" },
|
||||||
pie: { icon: "fa-chart-pie", label: "Pie" },
|
pie: { icon: "fa-chart-pie", label: "Pie" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Category filter select ───────────────────────────────────────────────────
|
// ─── Category filter select ───────────────────────────────────────────────────
|
||||||
@@ -74,7 +85,9 @@ function CategorySelect({
|
|||||||
>
|
>
|
||||||
<option value="all">Tất cả</option>
|
<option value="all">Tất cả</option>
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<option key={c.id} value={c.id}>{c.name}</option>
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,27 +106,28 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
// Split into halves for bar comparison
|
// Split into halves for bar comparison
|
||||||
const half = Math.floor(revenueData.length / 2);
|
const half = Math.floor(revenueData.length / 2);
|
||||||
const barCurrent = revenueData.slice(half);
|
const barCurrent = revenueData.slice(half);
|
||||||
const barPrevious = revenueData.slice(0, half).slice(0, barCurrent.length);
|
const barPrevious = revenueData.slice(0, half).slice(0, barCurrent.length);
|
||||||
|
|
||||||
// Filtered product sales
|
// Filtered product sales
|
||||||
const filteredSales = useMemo(
|
const filteredSales = useMemo(
|
||||||
() => categoryFilter === "all"
|
() =>
|
||||||
? MOCK_PRODUCT_SALES
|
categoryFilter === "all"
|
||||||
: MOCK_PRODUCT_SALES.filter((p) => p.category === categoryFilter),
|
? MOCK_PRODUCT_SALES
|
||||||
|
: MOCK_PRODUCT_SALES.filter((p) => p.category === categoryFilter),
|
||||||
[categoryFilter],
|
[categoryFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Summary stats
|
// Summary stats
|
||||||
const totalRevenue = revenueData.reduce((s, d) => s + d.revenue, 0);
|
const totalRevenue = revenueData.reduce((s, d) => s + d.revenue, 0);
|
||||||
const totalOrders = revenueData.reduce((s, d) => s + d.orders, 0);
|
const totalOrders = revenueData.reduce((s, d) => s + d.orders, 0);
|
||||||
const totalProfit = filteredSales.reduce((s, d) => s + d.profit, 0);
|
const totalProfit = filteredSales.reduce((s, d) => s + d.profit, 0);
|
||||||
const avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
|
const avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
|
||||||
|
|
||||||
// Period-over-period comparisons
|
// Period-over-period comparisons
|
||||||
const curRevenue = barCurrent.reduce((s, d) => s + d.revenue, 0);
|
const curRevenue = barCurrent.reduce((s, d) => s + d.revenue, 0);
|
||||||
const prevRevenue = barPrevious.reduce((s, d) => s + d.revenue, 0);
|
const prevRevenue = barPrevious.reduce((s, d) => s + d.revenue, 0);
|
||||||
const curOrders = barCurrent.reduce((s, d) => s + d.orders, 0);
|
const curOrders = barCurrent.reduce((s, d) => s + d.orders, 0);
|
||||||
const prevOrders = barPrevious.reduce((s, d) => s + d.orders, 0);
|
const prevOrders = barPrevious.reduce((s, d) => s + d.orders, 0);
|
||||||
const revComp = calcChange(curRevenue, prevRevenue);
|
const revComp = calcChange(curRevenue, prevRevenue);
|
||||||
const ordComp = calcChange(curOrders, prevOrders);
|
const ordComp = calcChange(curOrders, prevOrders);
|
||||||
@@ -142,11 +156,13 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
// Totals for summary row
|
// Totals for summary row
|
||||||
const filteredRevenue = filteredSales.reduce((s, d) => s + d.revenue, 0);
|
const filteredRevenue = filteredSales.reduce((s, d) => s + d.revenue, 0);
|
||||||
const filteredProfit = filteredSales.reduce((s, d) => s + d.profit, 0);
|
const filteredProfit = filteredSales.reduce((s, d) => s + d.profit, 0);
|
||||||
const filteredUnits = filteredSales.reduce((s, d) => s + d.unitsSold, 0);
|
const filteredUnits = filteredSales.reduce((s, d) => s + d.unitsSold, 0);
|
||||||
const avgMargin = filteredSales.length > 0
|
const avgMargin =
|
||||||
? filteredSales.reduce((s, d) => s + d.profitMargin, 0) / filteredSales.length
|
filteredSales.length > 0
|
||||||
: 0;
|
? filteredSales.reduce((s, d) => s + d.profitMargin, 0) /
|
||||||
|
filteredSales.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background min-h-screen">
|
<div className="bg-background min-h-screen">
|
||||||
@@ -167,7 +183,9 @@ export default function AnalyticsPage() {
|
|||||||
<h1 className="text-foreground text-lg leading-tight font-bold">
|
<h1 className="text-foreground text-lg leading-tight font-bold">
|
||||||
Thống kê & Phân tích tài chính
|
Thống kê & Phân tích tài chính
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-(--color-text-muted)">Financial Analytics Dashboard</p>
|
<p className="text-xs text-(--color-text-muted)">
|
||||||
|
Financial Analytics Dashboard
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,8 +210,12 @@ export default function AnalyticsPage() {
|
|||||||
onChange={(e) => setPeriod(e.target.value as AnalyticsPeriod)}
|
onChange={(e) => setPeriod(e.target.value as AnalyticsPeriod)}
|
||||||
className="text-foreground block rounded-lg border border-(--color-border) bg-(--color-bg-card) px-2 py-1.5 text-xs sm:hidden"
|
className="text-foreground block rounded-lg border border-(--color-border) bg-(--color-bg-card) px-2 py-1.5 text-xs sm:hidden"
|
||||||
>
|
>
|
||||||
{(Object.entries(PERIOD_LABELS) as [AnalyticsPeriod, string][]).map(([k, v]) => (
|
{(
|
||||||
<option key={k} value={k}>{v}</option>
|
Object.entries(PERIOD_LABELS) as [AnalyticsPeriod, string][]
|
||||||
|
).map(([k, v]) => (
|
||||||
|
<option key={k} value={k}>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,18 +229,42 @@ export default function AnalyticsPage() {
|
|||||||
Tổng quan
|
Tổng quan
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
<SummaryCard icon="fa-solid fa-sack-dollar" title="Tổng doanh thu"
|
<SummaryCard
|
||||||
value={formatCurrency(totalRevenue)} subtitle={PERIOD_LABELS[period]}
|
icon="fa-solid fa-sack-dollar"
|
||||||
change={revComp.change} changePercent={revComp.changePercent} isPositive={revComp.isPositive} />
|
title="Tổng doanh thu"
|
||||||
<SummaryCard icon="fa-solid fa-receipt" title="Số đơn hàng"
|
value={formatCurrency(totalRevenue)}
|
||||||
value={totalOrders.toLocaleString()} subtitle="Tổng đơn trong kỳ"
|
subtitle={PERIOD_LABELS[period]}
|
||||||
change={ordComp.change} changePercent={ordComp.changePercent} isPositive={ordComp.isPositive} />
|
change={revComp.change}
|
||||||
<SummaryCard icon="fa-solid fa-circle-dollar-to-slot" title="Tổng lợi nhuận"
|
changePercent={revComp.changePercent}
|
||||||
value={formatCurrency(totalProfit)} subtitle="Ước tính từ dữ liệu bán hàng"
|
isPositive={revComp.isPositive}
|
||||||
change={proComp.change} changePercent={proComp.changePercent} isPositive={proComp.isPositive} />
|
/>
|
||||||
<SummaryCard icon="fa-solid fa-basket-shopping" title="Giá trị đơn TB"
|
<SummaryCard
|
||||||
value={formatCurrency(avgOrderValue)} subtitle="Doanh thu / số đơn hàng"
|
icon="fa-solid fa-receipt"
|
||||||
change={0} changePercent={0} isPositive={true} />
|
title="Số đơn hàng"
|
||||||
|
value={totalOrders.toLocaleString()}
|
||||||
|
subtitle="Tổng đơn trong kỳ"
|
||||||
|
change={ordComp.change}
|
||||||
|
changePercent={ordComp.changePercent}
|
||||||
|
isPositive={ordComp.isPositive}
|
||||||
|
/>
|
||||||
|
<SummaryCard
|
||||||
|
icon="fa-solid fa-circle-dollar-to-slot"
|
||||||
|
title="Tổng lợi nhuận"
|
||||||
|
value={formatCurrency(totalProfit)}
|
||||||
|
subtitle="Ước tính từ dữ liệu bán hàng"
|
||||||
|
change={proComp.change}
|
||||||
|
changePercent={proComp.changePercent}
|
||||||
|
isPositive={proComp.isPositive}
|
||||||
|
/>
|
||||||
|
<SummaryCard
|
||||||
|
icon="fa-solid fa-basket-shopping"
|
||||||
|
title="Giá trị đơn TB"
|
||||||
|
value={formatCurrency(avgOrderValue)}
|
||||||
|
subtitle="Doanh thu / số đơn hàng"
|
||||||
|
change={0}
|
||||||
|
changePercent={0}
|
||||||
|
isPositive={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -241,7 +287,9 @@ export default function AnalyticsPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<i className={`fa-solid text-xs ${CHART_META[t].icon}`}></i>
|
<i className={`fa-solid text-xs ${CHART_META[t].icon}`}></i>
|
||||||
<span className="hidden sm:inline">{CHART_META[t].label}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{CHART_META[t].label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -249,19 +297,29 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
{activeChart === "line" && (
|
{activeChart === "line" && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-xs text-(--color-text-muted)">Doanh thu theo thời gian — {PERIOD_LABELS[period]}</p>
|
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||||
|
Doanh thu theo thời gian — {PERIOD_LABELS[period]}
|
||||||
|
</p>
|
||||||
<LineChart data={revenueData} height={220} />
|
<LineChart data={revenueData} height={220} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeChart === "bar" && (
|
{activeChart === "bar" && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-xs text-(--color-text-muted)">So sánh doanh thu nửa đầu và nửa sau kỳ hiện tại</p>
|
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||||
<BarChart current={barCurrent} previous={barPrevious} height={220} />
|
So sánh doanh thu nửa đầu và nửa sau kỳ hiện tại
|
||||||
|
</p>
|
||||||
|
<BarChart
|
||||||
|
current={barCurrent}
|
||||||
|
previous={barPrevious}
|
||||||
|
height={220}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeChart === "pie" && (
|
{activeChart === "pie" && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-xs text-(--color-text-muted)">Tỷ trọng doanh thu theo danh mục sản phẩm</p>
|
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||||
|
Tỷ trọng doanh thu theo danh mục sản phẩm
|
||||||
|
</p>
|
||||||
<PieChart data={pieData} />
|
<PieChart data={pieData} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -274,7 +332,10 @@ export default function AnalyticsPage() {
|
|||||||
<i className="fa-solid fa-fire mr-2 text-orange-500"></i>
|
<i className="fa-solid fa-fire mr-2 text-orange-500"></i>
|
||||||
Top sản phẩm bán chạy
|
Top sản phẩm bán chạy
|
||||||
</h2>
|
</h2>
|
||||||
<CategorySelect value={categoryFilter} onChange={setCategoryFilter} />
|
<CategorySelect
|
||||||
|
value={categoryFilter}
|
||||||
|
onChange={setCategoryFilter}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{top5.map((p, i) => {
|
{top5.map((p, i) => {
|
||||||
@@ -286,15 +347,24 @@ export default function AnalyticsPage() {
|
|||||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-(--color-accent-light) text-xs font-bold text-(--color-primary)">
|
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-(--color-accent-light) text-xs font-bold text-(--color-primary)">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-foreground truncate text-sm font-medium">{p.name}</span>
|
<span className="text-foreground truncate text-sm font-medium">
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-3 text-xs">
|
<div className="flex shrink-0 items-center gap-3 text-xs">
|
||||||
<span className="text-(--color-text-muted) tabular-nums">{p.unitsSold} ly</span>
|
<span className="text-(--color-text-muted) tabular-nums">
|
||||||
<span className="font-semibold text-(--color-primary) tabular-nums">{formatCurrency(p.revenue)}</span>
|
{p.unitsSold} ly
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-(--color-primary) tabular-nums">
|
||||||
|
{formatCurrency(p.revenue)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-background h-2 overflow-hidden rounded-full">
|
<div className="bg-background h-2 overflow-hidden rounded-full">
|
||||||
<div className="h-full rounded-full bg-(--color-primary) transition-all duration-500" style={{ width: `${pct}%` }} />
|
<div
|
||||||
|
className="h-full rounded-full bg-(--color-primary) transition-all duration-500"
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -309,30 +379,51 @@ export default function AnalyticsPage() {
|
|||||||
<i className="fa-solid fa-table text-foreground mr-2"></i>
|
<i className="fa-solid fa-table text-foreground mr-2"></i>
|
||||||
Phân tích chi tiết sản phẩm
|
Phân tích chi tiết sản phẩm
|
||||||
</h2>
|
</h2>
|
||||||
<CategorySelect value={categoryFilter} onChange={setCategoryFilter} label="Lọc danh mục:" />
|
<CategorySelect
|
||||||
|
value={categoryFilter}
|
||||||
|
onChange={setCategoryFilter}
|
||||||
|
label="Lọc danh mục:"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-3 text-xs text-(--color-text-muted)">
|
<p className="mb-3 text-xs text-(--color-text-muted)">
|
||||||
Click vào tiêu đề cột để sắp xếp. Hiển thị {filteredSales.length} sản phẩm.
|
Click vào tiêu đề cột để sắp xếp. Hiển thị {filteredSales.length}{" "}
|
||||||
|
sản phẩm.
|
||||||
</p>
|
</p>
|
||||||
<ProductTable data={filteredSales} />
|
<ProductTable data={filteredSales} />
|
||||||
|
|
||||||
{/* Summary row */}
|
{/* Summary row */}
|
||||||
<div className="bg-background mt-4 flex flex-wrap gap-4 rounded-xl p-4 text-sm">
|
<div className="bg-background mt-4 flex flex-wrap gap-4 rounded-xl p-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-(--color-text-muted)">Tổng doanh thu: </span>
|
<span className="text-(--color-text-muted)">
|
||||||
<span className="font-semibold text-(--color-primary)">{formatCurrencyFull(filteredRevenue)}</span>
|
Tổng doanh thu:{" "}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-(--color-primary)">
|
||||||
|
{formatCurrencyFull(filteredRevenue)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-(--color-text-muted)">Tổng lợi nhuận: </span>
|
<span className="text-(--color-text-muted)">
|
||||||
<span className="font-semibold text-green-600">{formatCurrencyFull(filteredProfit)}</span>
|
Tổng lợi nhuận:{" "}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-green-600">
|
||||||
|
{formatCurrencyFull(filteredProfit)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-(--color-text-muted)">Tổng sản lượng: </span>
|
<span className="text-(--color-text-muted)">
|
||||||
<span className="text-foreground font-semibold">{filteredUnits.toLocaleString()} ly</span>
|
Tổng sản lượng:{" "}
|
||||||
|
</span>
|
||||||
|
<span className="text-foreground font-semibold">
|
||||||
|
{filteredUnits.toLocaleString()} ly
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-(--color-text-muted)">Biên LN trung bình: </span>
|
<span className="text-(--color-text-muted)">
|
||||||
<span className="font-semibold text-yellow-700">{avgMargin.toFixed(1)}%</span>
|
Biên LN trung bình:{" "}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-yellow-700">
|
||||||
|
{avgMargin.toFixed(1)}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+42
-25
@@ -1,7 +1,7 @@
|
|||||||
# Components Documentation
|
# Components Documentation
|
||||||
|
|
||||||
> This project follows **Atomic Design** pattern.
|
> This project follows **Atomic Design** pattern. See `Atomic.md` at project
|
||||||
> See `Atomic.md` at project root for full structure guide.
|
> root for full structure guide.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
@@ -19,17 +19,21 @@ components/
|
|||||||
|
|
||||||
### ProductCard (`molecules/cards/ProductCard.tsx`)
|
### ProductCard (`molecules/cards/ProductCard.tsx`)
|
||||||
|
|
||||||
**Description:** Product card molecule. Displays product image, name, description, formatted price, and a Buy button. Width is controlled by the parent grid (w-full).
|
**Description:** Product card molecule. Displays product image, name,
|
||||||
|
description, formatted price, and a Buy button. Width is controlled by the
|
||||||
|
parent grid (w-full).
|
||||||
|
|
||||||
**Atomic level:** Molecule (composed of Button, Caption, Text atoms)
|
**Atomic level:** Molecule (composed of Button, Caption, Text atoms)
|
||||||
|
|
||||||
### ShopCard (`molecules/cards/ShopCard.tsx`)
|
### ShopCard (`molecules/cards/ShopCard.tsx`)
|
||||||
|
|
||||||
**Description:** Shop discovery card. Displays shop image, name, address, and a link to the menu.
|
**Description:** Shop discovery card. Displays shop image, name, address, and a
|
||||||
|
link to the menu.
|
||||||
|
|
||||||
### SearchBar (`molecules/search-bar/SearchBar.tsx`)
|
### SearchBar (`molecules/search-bar/SearchBar.tsx`)
|
||||||
|
|
||||||
**Description:** Search input with clear button. Controlled component — value and onChange come from parent.
|
**Description:** Search input with clear button. Controlled component — value
|
||||||
|
and onChange come from parent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,35 +41,41 @@ components/
|
|||||||
|
|
||||||
### CategorySidebar (`organisms/navigation/CategorySidebar.tsx`)
|
### CategorySidebar (`organisms/navigation/CategorySidebar.tsx`)
|
||||||
|
|
||||||
**Description:** Left sidebar with collapsible category filter. Sticky below header, full viewport height minus header.
|
**Description:** Left sidebar with collapsible category filter. Sticky below
|
||||||
|
header, full viewport height minus header.
|
||||||
|
|
||||||
### CartFab (`organisms/cart/CartFab.tsx`)
|
### CartFab (`organisms/cart/CartFab.tsx`)
|
||||||
|
|
||||||
**Description:** Floating Action Button displaying cart item count. Links to /payment page.
|
**Description:** Floating Action Button displaying cart item count. Links to
|
||||||
|
/payment page.
|
||||||
|
|
||||||
### ProductGrid (`organisms/product-grid/ProductGrid.tsx`)
|
### ProductGrid (`organisms/product-grid/ProductGrid.tsx`)
|
||||||
|
|
||||||
**Description:** Full product grid organism with mobile category tabs, filtering, and empty state. Uses useCart and useMenu contexts.
|
**Description:** Full product grid organism with mobile category tabs,
|
||||||
|
filtering, and empty state. Uses useCart and useMenu contexts.
|
||||||
|
|
||||||
### ShopGrid (`organisms/shop-grid/ShopGrid.tsx`)
|
### ShopGrid (`organisms/shop-grid/ShopGrid.tsx`)
|
||||||
|
|
||||||
**Description:** Responsive grid of ShopCard molecules for the feed/discovery page.
|
**Description:** Responsive grid of ShopCard molecules for the feed/discovery
|
||||||
|
page.
|
||||||
|
|
||||||
### ReviewModal (`organisms/modals/ReviewModal.tsx`)
|
### ReviewModal (`organisms/modals/ReviewModal.tsx`)
|
||||||
|
|
||||||
**Description:** Modal for customer reviews. Shows 5-star rating and textarea. After submission, displays thank-you message.
|
**Description:** Modal for customer reviews. Shows 5-star rating and textarea.
|
||||||
|
After submission, displays thank-you message.
|
||||||
|
|
||||||
### Analytics Charts (`organisms/analytics/`)
|
### Analytics Charts (`organisms/analytics/`)
|
||||||
|
|
||||||
**Description:** Pure-SVG chart and table components for the Financial Analytics dashboard. All components are interactive with hover tooltips.
|
**Description:** Pure-SVG chart and table components for the Financial Analytics
|
||||||
|
dashboard. All components are interactive with hover tooltips.
|
||||||
|
|
||||||
| Component | File | Description |
|
| Component | File | Description |
|
||||||
|-----------|------|-------------|
|
| ------------ | ---------------- | ---------------------------------------------------------- |
|
||||||
| LineChart | LineChart.tsx | Revenue trend line chart with area fill and hover tooltips |
|
| LineChart | LineChart.tsx | Revenue trend line chart with area fill and hover tooltips |
|
||||||
| BarChart | BarChart.tsx | Grouped bar chart comparing current vs previous period |
|
| BarChart | BarChart.tsx | Grouped bar chart comparing current vs previous period |
|
||||||
| PieChart | PieChart.tsx | Pie chart with interactive legend for category breakdown |
|
| PieChart | PieChart.tsx | Pie chart with interactive legend for category breakdown |
|
||||||
| SummaryCard | SummaryCard.tsx | Metric card with period-over-period comparison indicator |
|
| SummaryCard | SummaryCard.tsx | Metric card with period-over-period comparison indicator |
|
||||||
| ProductTable | ProductTable.tsx | Sortable product sales table with profit margin badges |
|
| ProductTable | ProductTable.tsx | Sortable product sales table with profit margin badges |
|
||||||
|
|
||||||
**Usage:** Imported via `@/components/organisms/analytics` barrel index.
|
**Usage:** Imported via `@/components/organisms/analytics` barrel index.
|
||||||
|
|
||||||
@@ -75,11 +85,13 @@ components/
|
|||||||
|
|
||||||
### MainLayout (`templates/main-layout/MainLayout.tsx`)
|
### MainLayout (`templates/main-layout/MainLayout.tsx`)
|
||||||
|
|
||||||
**Description:** Main layout template — wraps content with Header, Footer, and CartFab. Used by (main) route group.
|
**Description:** Main layout template — wraps content with Header, Footer, and
|
||||||
|
CartFab. Used by (main) route group.
|
||||||
|
|
||||||
### AuthLayout (`templates/auth-layout/AuthLayout.tsx`)
|
### AuthLayout (`templates/auth-layout/AuthLayout.tsx`)
|
||||||
|
|
||||||
**Description:** Auth layout template — centers content in screen. Used by login/register pages.
|
**Description:** Auth layout template — centers content in screen. Used by
|
||||||
|
login/register pages.
|
||||||
|
|
||||||
### FeedLayout (`templates/feed-layout/FeedLayout.tsx`)
|
### FeedLayout (`templates/feed-layout/FeedLayout.tsx`)
|
||||||
|
|
||||||
@@ -87,7 +99,8 @@ components/
|
|||||||
|
|
||||||
### ManagerLayout (`templates/manager-layout/ManagerLayout.tsx`)
|
### ManagerLayout (`templates/manager-layout/ManagerLayout.tsx`)
|
||||||
|
|
||||||
**Description:** Manager layout template — auth guard + ManagerProvider. Used by the manager route group.
|
**Description:** Manager layout template — auth guard + ManagerProvider. Used by
|
||||||
|
the manager route group.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,16 +108,20 @@ components/
|
|||||||
|
|
||||||
### AuthContext (`lib/auth-context.tsx`)
|
### AuthContext (`lib/auth-context.tsx`)
|
||||||
|
|
||||||
Manages user authentication state including login, logout, and registration. Uses localStorage for persistence.
|
Manages user authentication state including login, logout, and registration.
|
||||||
|
Uses localStorage for persistence.
|
||||||
|
|
||||||
### CartContext (`lib/cart-context.tsx`)
|
### CartContext (`lib/cart-context.tsx`)
|
||||||
|
|
||||||
Manages shopping cart state with localStorage persistence. Tracks items, quantities, and totals.
|
Manages shopping cart state with localStorage persistence. Tracks items,
|
||||||
|
quantities, and totals.
|
||||||
|
|
||||||
### MenuContext (`lib/menu-context.tsx`)
|
### MenuContext (`lib/menu-context.tsx`)
|
||||||
|
|
||||||
Provides shared activeCategory state across components. Synchronizes Header mobile menu and CategorySidebar selection.
|
Provides shared activeCategory state across components. Synchronizes Header
|
||||||
|
mobile menu and CategorySidebar selection.
|
||||||
|
|
||||||
### ManagerContext (`lib/manager-context.tsx`)
|
### ManagerContext (`lib/manager-context.tsx`)
|
||||||
|
|
||||||
Manages menu CRUD state (products, combos, categories) for the manager dashboard.
|
Manages menu CRUD state (products, combos, categories) for the manager
|
||||||
|
dashboard.
|
||||||
|
|||||||
+79
-29
@@ -1,16 +1,18 @@
|
|||||||
# Atoms Components Library
|
# Atoms Components Library
|
||||||
|
|
||||||
**Status:** ✅ Phase 1 Implementation Complete
|
**Status:** ✅ Phase 1 Implementation Complete **Created:** 2026-04-03
|
||||||
**Created:** 2026-04-03
|
|
||||||
**Foundation:** Atomic Design Pattern
|
**Foundation:** Atomic Design Pattern
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Overview
|
## 📚 Overview
|
||||||
|
|
||||||
Atoms are the basic building blocks of your UI. They are small, focused, reusable components that have no dependencies on other components (except potentially other atoms).
|
Atoms are the basic building blocks of your UI. They are small, focused,
|
||||||
|
reusable components that have no dependencies on other components (except
|
||||||
|
potentially other atoms).
|
||||||
|
|
||||||
**Key Principles:**
|
**Key Principles:**
|
||||||
|
|
||||||
- ✅ No business logic
|
- ✅ No business logic
|
||||||
- ✅ No context/hooks (useAuth, useCart, etc.)
|
- ✅ No context/hooks (useAuth, useCart, etc.)
|
||||||
- ✅ Pure props-based
|
- ✅ Pure props-based
|
||||||
@@ -25,9 +27,11 @@ Atoms are the basic building blocks of your UI. They are small, focused, reusabl
|
|||||||
### Buttons (`buttons/`)
|
### Buttons (`buttons/`)
|
||||||
|
|
||||||
#### **Button.tsx**
|
#### **Button.tsx**
|
||||||
|
|
||||||
Main interactive button component with multiple variants and sizes.
|
Main interactive button component with multiple variants and sizes.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `variant` - "primary" | "secondary" | "danger" | "ghost" (default: "primary")
|
- `variant` - "primary" | "secondary" | "danger" | "ghost" (default: "primary")
|
||||||
- `size` - "sm" | "md" | "lg" (default: "md")
|
- `size` - "sm" | "md" | "lg" (default: "md")
|
||||||
- `icon` - FontAwesome icon class (optional)
|
- `icon` - FontAwesome icon class (optional)
|
||||||
@@ -38,6 +42,7 @@ Main interactive button component with multiple variants and sizes.
|
|||||||
- All standard HTML button attributes
|
- All standard HTML button attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Button } from "@/components/atoms";
|
import { Button } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ import { Button } from "@/components/atoms";
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Styling:**
|
**Styling:**
|
||||||
|
|
||||||
- Primary: Branded color with dark hover
|
- Primary: Branded color with dark hover
|
||||||
- Secondary: Border style with light background on hover
|
- Secondary: Border style with light background on hover
|
||||||
- Danger: Red for destructive actions
|
- Danger: Red for destructive actions
|
||||||
@@ -63,15 +69,18 @@ import { Button } from "@/components/atoms";
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### **IconButton.tsx**
|
#### **IconButton.tsx**
|
||||||
|
|
||||||
Button designed specifically for icon-only interactions.
|
Button designed specifically for icon-only interactions.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `variant` - "primary" | "secondary" | "danger" | "ghost"
|
- `variant` - "primary" | "secondary" | "danger" | "ghost"
|
||||||
- `size` - "sm" (8x8) | "md" (10x10) | "lg" (12x12)
|
- `size` - "sm" (8x8) | "md" (10x10) | "lg" (12x12)
|
||||||
- `icon` - FontAwesome icon class (required)
|
- `icon` - FontAwesome icon class (required)
|
||||||
- All standard HTML button attributes
|
- All standard HTML button attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { IconButton } from "@/components/atoms";
|
import { IconButton } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -84,9 +93,11 @@ import { IconButton } from "@/components/atoms";
|
|||||||
### Inputs (`inputs/`)
|
### Inputs (`inputs/`)
|
||||||
|
|
||||||
#### **TextInput.tsx**
|
#### **TextInput.tsx**
|
||||||
|
|
||||||
General text input field with optional label, error, and icon.
|
General text input field with optional label, error, and icon.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `label` - string (optional)
|
- `label` - string (optional)
|
||||||
- `error` - string (optional, shows red border and error text)
|
- `error` - string (optional, shows red border and error text)
|
||||||
- `icon` - FontAwesome icon class (optional)
|
- `icon` - FontAwesome icon class (optional)
|
||||||
@@ -94,6 +105,7 @@ General text input field with optional label, error, and icon.
|
|||||||
- All standard HTML input attributes
|
- All standard HTML input attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { TextInput } from "@/components/atoms";
|
import { TextInput } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -115,9 +127,11 @@ import { TextInput } from "@/components/atoms";
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### **SearchInput.tsx**
|
#### **SearchInput.tsx**
|
||||||
|
|
||||||
Search input with built-in search icon and clear button.
|
Search input with built-in search icon and clear button.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `value` - string (controlled input)
|
- `value` - string (controlled input)
|
||||||
- `onChange` - callback when text changes
|
- `onChange` - callback when text changes
|
||||||
- `onClear` - callback for clear button (required for button to show)
|
- `onClear` - callback for clear button (required for button to show)
|
||||||
@@ -125,6 +139,7 @@ Search input with built-in search icon and clear button.
|
|||||||
- All standard HTML input attributes
|
- All standard HTML input attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { SearchInput } from "@/components/atoms";
|
import { SearchInput } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -135,20 +150,23 @@ const [query, setQuery] = useState("");
|
|||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
onClear={() => setQuery("")}
|
onClear={() => setQuery("")}
|
||||||
placeholder="Search products..."
|
placeholder="Search products..."
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### **Textarea.tsx**
|
#### **Textarea.tsx**
|
||||||
|
|
||||||
Multi-line text input with optional label and error.
|
Multi-line text input with optional label and error.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `label` - string (optional)
|
- `label` - string (optional)
|
||||||
- `error` - string (optional)
|
- `error` - string (optional)
|
||||||
- All standard HTML textarea attributes
|
- All standard HTML textarea attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Textarea } from "@/components/atoms";
|
import { Textarea } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -158,7 +176,7 @@ import { Textarea } from "@/components/atoms";
|
|||||||
rows={4}
|
rows={4}
|
||||||
value={review}
|
value={review}
|
||||||
onChange={(e) => setReview(e.target.value)}
|
onChange={(e) => setReview(e.target.value)}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -166,14 +184,17 @@ import { Textarea } from "@/components/atoms";
|
|||||||
### Typography (`typography/`)
|
### Typography (`typography/`)
|
||||||
|
|
||||||
#### **Heading.tsx**
|
#### **Heading.tsx**
|
||||||
|
|
||||||
Semantic heading component with level-based sizing.
|
Semantic heading component with level-based sizing.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `level` - 1 | 2 | 3 | 4 | 5 | 6 (default: 2)
|
- `level` - 1 | 2 | 3 | 4 | 5 | 6 (default: 2)
|
||||||
- `children` - ReactNode
|
- `children` - ReactNode
|
||||||
- All standard HTML heading attributes
|
- All standard HTML heading attributes
|
||||||
|
|
||||||
**Sizing:**
|
**Sizing:**
|
||||||
|
|
||||||
- Level 1: text-3xl font-bold
|
- Level 1: text-3xl font-bold
|
||||||
- Level 2: text-2xl font-bold
|
- Level 2: text-2xl font-bold
|
||||||
- Level 3: text-xl font-bold
|
- Level 3: text-xl font-bold
|
||||||
@@ -182,6 +203,7 @@ Semantic heading component with level-based sizing.
|
|||||||
- Level 6: text-sm font-semibold
|
- Level 6: text-sm font-semibold
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Heading } from "@/components/atoms";
|
import { Heading } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -193,20 +215,24 @@ import { Heading } from "@/components/atoms";
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### **Text.tsx**
|
#### **Text.tsx**
|
||||||
|
|
||||||
Paragraph text with semantic variants.
|
Paragraph text with semantic variants.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `variant` - "body1" | "body2" | "caption" | "label" (default: "body1")
|
- `variant` - "body1" | "body2" | "caption" | "label" (default: "body1")
|
||||||
- `children` - ReactNode
|
- `children` - ReactNode
|
||||||
- All standard HTML paragraph attributes
|
- All standard HTML paragraph attributes
|
||||||
|
|
||||||
**Variants:**
|
**Variants:**
|
||||||
|
|
||||||
- body1: text-base (main content)
|
- body1: text-base (main content)
|
||||||
- body2: text-sm (secondary content)
|
- body2: text-sm (secondary content)
|
||||||
- caption: text-xs (smallest text)
|
- caption: text-xs (smallest text)
|
||||||
- label: text-sm font-medium (form labels)
|
- label: text-sm font-medium (form labels)
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Text } from "@/components/atoms";
|
import { Text } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -219,13 +245,16 @@ import { Text } from "@/components/atoms";
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### **Caption.tsx**
|
#### **Caption.tsx**
|
||||||
|
|
||||||
Small caption/note text component.
|
Small caption/note text component.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `children` - ReactNode
|
- `children` - ReactNode
|
||||||
- All standard HTML span attributes
|
- All standard HTML span attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Caption } from "@/components/atoms";
|
import { Caption } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -238,15 +267,19 @@ import { Caption } from "@/components/atoms";
|
|||||||
### Badges (`badges/`)
|
### Badges (`badges/`)
|
||||||
|
|
||||||
#### **Badge.tsx**
|
#### **Badge.tsx**
|
||||||
|
|
||||||
Labeled badge component for highlighting information.
|
Labeled badge component for highlighting information.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
- `variant` - "primary" | "secondary" | "success" | "danger" | "warning" (default: "primary")
|
|
||||||
|
- `variant` - "primary" | "secondary" | "success" | "danger" | "warning"
|
||||||
|
(default: "primary")
|
||||||
- `size` - "sm" | "md" (default: "md")
|
- `size` - "sm" | "md" (default: "md")
|
||||||
- `children` - ReactNode
|
- `children` - ReactNode
|
||||||
- All standard HTML span attributes
|
- All standard HTML span attributes
|
||||||
|
|
||||||
**Variants:**
|
**Variants:**
|
||||||
|
|
||||||
- primary: Branded color
|
- primary: Branded color
|
||||||
- secondary: Light gray
|
- secondary: Light gray
|
||||||
- success: Green
|
- success: Green
|
||||||
@@ -254,6 +287,7 @@ Labeled badge component for highlighting information.
|
|||||||
- warning: Yellow
|
- warning: Yellow
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Badge } from "@/components/atoms";
|
import { Badge } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -265,14 +299,17 @@ import { Badge } from "@/components/atoms";
|
|||||||
---
|
---
|
||||||
|
|
||||||
#### **PriceBadge.tsx**
|
#### **PriceBadge.tsx**
|
||||||
|
|
||||||
Specialized badge for displaying formatted prices.
|
Specialized badge for displaying formatted prices.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `price` - number
|
- `price` - number
|
||||||
- `currency` - string (default: "VND")
|
- `currency` - string (default: "VND")
|
||||||
- All standard HTML span attributes
|
- All standard HTML span attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { PriceBadge } from "@/components/atoms";
|
import { PriceBadge } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -281,6 +318,7 @@ import { PriceBadge } from "@/components/atoms";
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Output:**
|
**Output:**
|
||||||
|
|
||||||
- VND: "50.000 ₫"
|
- VND: "50.000 ₫"
|
||||||
- USD: "$99.99"
|
- USD: "$99.99"
|
||||||
|
|
||||||
@@ -289,13 +327,16 @@ import { PriceBadge } from "@/components/atoms";
|
|||||||
### Dividers (`dividers/`)
|
### Dividers (`dividers/`)
|
||||||
|
|
||||||
#### **Divider.tsx**
|
#### **Divider.tsx**
|
||||||
|
|
||||||
Visual separator element.
|
Visual separator element.
|
||||||
|
|
||||||
**Props:**
|
**Props:**
|
||||||
|
|
||||||
- `orientation` - "horizontal" | "vertical" (default: "horizontal")
|
- `orientation` - "horizontal" | "vertical" (default: "horizontal")
|
||||||
- All standard HTML hr attributes
|
- All standard HTML hr attributes
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Divider } from "@/components/atoms";
|
import { Divider } from "@/components/atoms";
|
||||||
|
|
||||||
@@ -332,34 +373,41 @@ Change one variable in `globals.css` to update all atoms across the app.
|
|||||||
## 🔄 Import Patterns
|
## 🔄 Import Patterns
|
||||||
|
|
||||||
### Individual Imports
|
### Individual Imports
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Button } from "@/components/atoms/buttons";
|
|
||||||
import { TextInput, SearchInput } from "@/components/atoms/inputs";
|
|
||||||
import { Heading, Text, Caption } from "@/components/atoms/typography";
|
|
||||||
import { Badge, PriceBadge } from "@/components/atoms/badges";
|
import { Badge, PriceBadge } from "@/components/atoms/badges";
|
||||||
|
import { Button } from "@/components/atoms/buttons";
|
||||||
import { Divider } from "@/components/atoms/dividers";
|
import { Divider } from "@/components/atoms/dividers";
|
||||||
|
import { SearchInput, TextInput } from "@/components/atoms/inputs";
|
||||||
|
import { Caption, Heading, Text } from "@/components/atoms/typography";
|
||||||
```
|
```
|
||||||
|
|
||||||
### Barrel Exports (Recommended)
|
### Barrel Exports (Recommended)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
TextInput,
|
|
||||||
SearchInput,
|
|
||||||
Textarea,
|
|
||||||
Heading,
|
|
||||||
Text,
|
|
||||||
Caption,
|
|
||||||
Badge,
|
Badge,
|
||||||
PriceBadge,
|
Button,
|
||||||
|
Caption,
|
||||||
Divider,
|
Divider,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
PriceBadge,
|
||||||
|
SearchInput,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
} from "@/components/atoms";
|
} from "@/components/atoms";
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type Imports
|
### Type Imports
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/atoms";
|
import type {
|
||||||
|
ButtonProps,
|
||||||
|
HeadingProps,
|
||||||
|
TextInputProps,
|
||||||
|
} from "@/components/atoms";
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -367,13 +415,10 @@ import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/ato
|
|||||||
## ✅ Usage Examples
|
## ✅ Usage Examples
|
||||||
|
|
||||||
### Form Field
|
### Form Field
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TextInput
|
<TextInput label="Name" placeholder="Enter your name" type="text" />
|
||||||
label="Name"
|
|
||||||
placeholder="Enter your name"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
@@ -385,11 +430,12 @@ import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/ato
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Product Card
|
### Product Card
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<div className="rounded-lg border p-4">
|
<div className="rounded-lg border p-4">
|
||||||
<Heading level={3}>Product Name</Heading>
|
<Heading level={3}>Product Name</Heading>
|
||||||
<Text variant="body2">Product description</Text>
|
<Text variant="body2">Product description</Text>
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="mt-4 flex items-center justify-between">
|
||||||
<PriceBadge price={50000} />
|
<PriceBadge price={50000} />
|
||||||
<Button icon="fa-cart-plus">Add to Cart</Button>
|
<Button icon="fa-cart-plus">Add to Cart</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,6 +443,7 @@ import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/ato
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Rating Display
|
### Rating Display
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<div>
|
<div>
|
||||||
<Heading level={4}>Reviews</Heading>
|
<Heading level={4}>Reviews</Heading>
|
||||||
@@ -410,6 +457,7 @@ import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/ato
|
|||||||
## 🧪 Testing Atoms
|
## 🧪 Testing Atoms
|
||||||
|
|
||||||
### Props Validation
|
### Props Validation
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// ✅ Valid
|
// ✅ Valid
|
||||||
<Button variant="primary" size="sm">Click</Button>
|
<Button variant="primary" size="sm">Click</Button>
|
||||||
@@ -420,7 +468,9 @@ import type { ButtonProps, TextInputProps, HeadingProps } from "@/components/ato
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
All atoms include:
|
All atoms include:
|
||||||
|
|
||||||
- Semantic HTML
|
- Semantic HTML
|
||||||
- ARIA labels where appropriate
|
- ARIA labels where appropriate
|
||||||
- Keyboard navigation support
|
- Keyboard navigation support
|
||||||
@@ -466,7 +516,8 @@ components/atoms/
|
|||||||
|
|
||||||
## 🚀 Next Steps
|
## 🚀 Next Steps
|
||||||
|
|
||||||
Once atoms are comfortable, the next phase is creating **Molecules** - combinations of atoms:
|
Once atoms are comfortable, the next phase is creating **Molecules** -
|
||||||
|
combinations of atoms:
|
||||||
|
|
||||||
- **ProductCard** - Image + Text + Badge + Button
|
- **ProductCard** - Image + Text + Badge + Button
|
||||||
- **FormField** - Label + Input + Error Text
|
- **FormField** - Label + Input + Error Text
|
||||||
@@ -489,6 +540,5 @@ See `OPTIMIZATION_PLAN.md` for the full roadmap.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Status:** ✅ Complete & Ready for Use
|
**Status:** ✅ Complete & Ready for Use **Last Updated:** 2026-04-03 **Phase:**
|
||||||
**Last Updated:** 2026-04-03
|
1 of 5
|
||||||
**Phase:** 1 of 5
|
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ export default function Button({
|
|||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const styles = {
|
const styles = {
|
||||||
base: "font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5",
|
base: "font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5",
|
||||||
payment: "inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl transition-colors",
|
payment:
|
||||||
login: "w-full cursor-pointer rounded-xl py-3 font-semibold transition-all duration-150",
|
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl transition-colors",
|
||||||
}
|
login:
|
||||||
|
"w-full cursor-pointer rounded-xl py-3 font-semibold transition-all duration-150",
|
||||||
|
};
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary:
|
primary:
|
||||||
"bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-95",
|
"bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-95",
|
||||||
@@ -25,8 +27,10 @@ export default function Button({
|
|||||||
"border border-(--color-border) hover:bg-(--color-border-light) active:scale-95",
|
"border border-(--color-border) hover:bg-(--color-border-light) active:scale-95",
|
||||||
danger: "bg-red-500 text-white hover:bg-red-600 active:scale-95",
|
danger: "bg-red-500 text-white hover:bg-red-600 active:scale-95",
|
||||||
ghost: "bg-transparent hover:bg-(--color-border-light) active:scale-95",
|
ghost: "bg-transparent hover:bg-(--color-border-light) active:scale-95",
|
||||||
primaryNoBorder: "border-none bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-98",
|
primaryNoBorder:
|
||||||
bgWhite: "border-2 border-(--color-primary) bg-white text-(--color-primary) hover:bg-(--color-primary) hover:text-white active:scale-98",
|
"border-none bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-98",
|
||||||
|
bgWhite:
|
||||||
|
"border-2 border-(--color-primary) bg-white text-(--color-primary) hover:bg-(--color-primary) hover:text-white active:scale-98",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { ButtonHTMLAttributes } from "react";
|
import { ButtonHTMLAttributes } from "react";
|
||||||
|
|
||||||
export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style'> {
|
export interface ButtonProps extends Omit<
|
||||||
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
"style"
|
||||||
|
> {
|
||||||
style?: "base" | "payment" | "login";
|
style?: "base" | "payment" | "login";
|
||||||
variant?: "primary" | "secondary" | "danger" | "ghost" | "primaryNoBorder" | "bgWhite";
|
variant?:
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "danger"
|
||||||
|
| "ghost"
|
||||||
|
| "primaryNoBorder"
|
||||||
|
| "bgWhite";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
icon?: string; // FontAwesome class like "fa-solid fa-cart-plus"
|
icon?: string; // FontAwesome class like "fa-solid fa-cart-plus"
|
||||||
iconPosition?: "left" | "right";
|
iconPosition?: "left" | "right";
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|||||||
export interface ErrorMessageLoginProps {
|
export interface ErrorMessageLoginProps {
|
||||||
message: string;
|
message: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,5 @@ export default function ErrorMessageLogin({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return type === "primary" ? primaryType() : secondaryType();
|
||||||
type === "primary" ? primaryType() : secondaryType()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
export { default as ErrorMessageLogin } from "./ErrorMessageLogin";
|
export { default as ErrorMessageLogin } from "./ErrorMessageLogin";
|
||||||
export type {
|
export type { ErrorMessageLoginProps } from "./Error.types";
|
||||||
ErrorMessageLoginProps,
|
|
||||||
} from "./Error.types";
|
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export default function LoginInput({
|
|||||||
type={showPassword ? "text" : type}
|
type={showPassword ? "text" : type}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder= {type === "password" ? "Mật khẩu" : "admin / số điện thoại / tên nhân viên"}
|
placeholder={
|
||||||
|
type === "password"
|
||||||
|
? "Mật khẩu"
|
||||||
|
: "admin / số điện thoại / tên nhân viên"
|
||||||
|
}
|
||||||
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors ? "border-red-400" : "border-(--color-border)"} `}
|
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors ? "border-red-400" : "border-(--color-border)"} `}
|
||||||
/>
|
/>
|
||||||
{isPassword()}
|
{isPassword()}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Button from "@/components/atoms/buttons/Button";
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { ReviewModal } from "@/components/organisms/modals";
|
import { ReviewModal } from "@/components/organisms/modals";
|
||||||
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import type { PaymentSummaryCardProps } from "./Card.types";
|
import type { PaymentSummaryCardProps } from "./Card.types";
|
||||||
@@ -14,14 +13,13 @@ export default function PaymentSummaryCard({
|
|||||||
isCustomer = false,
|
isCustomer = false,
|
||||||
backHref,
|
backHref,
|
||||||
}: PaymentSummaryCardProps) {
|
}: PaymentSummaryCardProps) {
|
||||||
|
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
||||||
const [isReviewOpen, setIsReviewOpen] = useState(false);
|
const handlePayment = () => {
|
||||||
const handlePayment = () => {
|
|
||||||
// UI-only: open review modal after "payment"
|
// UI-only: open review modal after "payment"
|
||||||
if (isCustomer) {
|
if (isCustomer) {
|
||||||
setIsReviewOpen(true);
|
setIsReviewOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<aside className="shrink-0 xl:w-85">
|
<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">
|
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
||||||
@@ -67,7 +65,10 @@ export default function PaymentSummaryCard({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Link href={backHref || "/"} className={isCustomer ? "" : "col-span-2"}>
|
<Link
|
||||||
|
href={backHref || "/"}
|
||||||
|
className={isCustomer ? "" : "col-span-2"}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
style="payment"
|
style="payment"
|
||||||
onClick={() => setIsReviewOpen(false)}
|
onClick={() => setIsReviewOpen(false)}
|
||||||
|
|||||||
@@ -16,15 +16,25 @@ interface BarChartProps {
|
|||||||
* Tooltip auto-flips above/below bar top to stay inside the viewBox.
|
* Tooltip auto-flips above/below bar top to stay inside the viewBox.
|
||||||
*/
|
*/
|
||||||
export function BarChart({ current, previous, height = 200 }: BarChartProps) {
|
export function BarChart({ current, previous, height = 200 }: BarChartProps) {
|
||||||
const [hovered, setHovered] = useState<{ set: "cur" | "prev"; idx: number } | null>(null);
|
const [hovered, setHovered] = useState<{
|
||||||
|
set: "cur" | "prev";
|
||||||
|
idx: number;
|
||||||
|
} | null>(null);
|
||||||
const W = 800;
|
const W = 800;
|
||||||
const H = height;
|
const H = height;
|
||||||
const padL = 56, padR = 16, padT = 16, padB = 40;
|
const padL = 56,
|
||||||
|
padR = 16,
|
||||||
|
padT = 16,
|
||||||
|
padB = 40;
|
||||||
const chartW = W - padL - padR;
|
const chartW = W - padL - padR;
|
||||||
const chartH = H - padT - padB;
|
const chartH = H - padT - padB;
|
||||||
|
|
||||||
const n = current.length;
|
const n = current.length;
|
||||||
const maxVal = Math.max(...current.map((d) => d.revenue), ...previous.map((d) => d.revenue)) || 1;
|
const maxVal =
|
||||||
|
Math.max(
|
||||||
|
...current.map((d) => d.revenue),
|
||||||
|
...previous.map((d) => d.revenue),
|
||||||
|
) || 1;
|
||||||
const groupW = chartW / n;
|
const groupW = chartW / n;
|
||||||
const barW = groupW * 0.35;
|
const barW = groupW * 0.35;
|
||||||
const gap = groupW * 0.05;
|
const gap = groupW * 0.05;
|
||||||
@@ -47,8 +57,24 @@ export function BarChart({ current, previous, height = 200 }: BarChartProps) {
|
|||||||
>
|
>
|
||||||
{gridLines.map((g, i) => (
|
{gridLines.map((g, i) => (
|
||||||
<g key={i}>
|
<g key={i}>
|
||||||
<line x1={padL} y1={g.y} x2={W - padR} y2={g.y} stroke="#E2C9A8" strokeWidth="1" strokeDasharray={i === yTicks ? "0" : "4 3"} />
|
<line
|
||||||
<text x={padL - 6} y={g.y + 4} textAnchor="end" fontSize="10" fill="#A08060">{formatCurrency(g.val)}</text>
|
x1={padL}
|
||||||
|
y1={g.y}
|
||||||
|
x2={W - padR}
|
||||||
|
y2={g.y}
|
||||||
|
stroke="#E2C9A8"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray={i === yTicks ? "0" : "4 3"}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={padL - 6}
|
||||||
|
y={g.y + 4}
|
||||||
|
textAnchor="end"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{formatCurrency(g.val)}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -62,48 +88,121 @@ export function BarChart({ current, previous, height = 200 }: BarChartProps) {
|
|||||||
const isHovPrev = hovered?.set === "prev" && hovered.idx === i;
|
const isHovPrev = hovered?.set === "prev" && hovered.idx === i;
|
||||||
return (
|
return (
|
||||||
<g key={i}>
|
<g key={i}>
|
||||||
<rect x={prevX} y={padT + chartH - prevH} width={barW} height={prevH} rx="3"
|
<rect
|
||||||
|
x={prevX}
|
||||||
|
y={padT + chartH - prevH}
|
||||||
|
width={barW}
|
||||||
|
height={prevH}
|
||||||
|
rx="3"
|
||||||
fill={isHovPrev ? "#A0785A" : "#E2C9A8"}
|
fill={isHovPrev ? "#A0785A" : "#E2C9A8"}
|
||||||
style={{ cursor: "pointer", transition: "fill 150ms" }}
|
style={{ cursor: "pointer", transition: "fill 150ms" }}
|
||||||
onMouseEnter={() => setHovered({ set: "prev", idx: i })} />
|
onMouseEnter={() => setHovered({ set: "prev", idx: i })}
|
||||||
<rect x={curX} y={padT + chartH - curH} width={barW} height={curH} rx="3"
|
/>
|
||||||
|
<rect
|
||||||
|
x={curX}
|
||||||
|
y={padT + chartH - curH}
|
||||||
|
width={barW}
|
||||||
|
height={curH}
|
||||||
|
rx="3"
|
||||||
fill={isHovCur ? "#4A3728" : "#6F4E37"}
|
fill={isHovCur ? "#4A3728" : "#6F4E37"}
|
||||||
style={{ cursor: "pointer", transition: "fill 150ms" }}
|
style={{ cursor: "pointer", transition: "fill 150ms" }}
|
||||||
onMouseEnter={() => setHovered({ set: "cur", idx: i })} />
|
onMouseEnter={() => setHovered({ set: "cur", idx: i })}
|
||||||
|
/>
|
||||||
{i % step === 0 && (
|
{i % step === 0 && (
|
||||||
<text x={groupX + groupW / 2} y={H - 8} textAnchor="middle" fontSize="10" fill="#A08060">{d.label}</text>
|
<text
|
||||||
|
x={groupX + groupW / 2}
|
||||||
|
y={H - 8}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
</text>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{hovered !== null && (() => {
|
{hovered !== null &&
|
||||||
const d = hovered.set === "cur" ? current[hovered.idx] : previous[hovered.idx];
|
(() => {
|
||||||
if (!d) return null;
|
const d =
|
||||||
const groupX = padL + hovered.idx * groupW;
|
hovered.set === "cur"
|
||||||
const tipW = 130, tipH = 50;
|
? current[hovered.idx]
|
||||||
const tipX = Math.min(Math.max(groupX - tipW / 2, padL), W - padR - tipW);
|
: previous[hovered.idx];
|
||||||
const barH = (d.revenue / maxVal) * chartH;
|
if (!d) return null;
|
||||||
const barTopY = padT + chartH - barH;
|
const groupX = padL + hovered.idx * groupW;
|
||||||
const aboveY = barTopY - tipH - 8;
|
const tipW = 130,
|
||||||
const tipY = Math.min(Math.max(aboveY >= padT ? aboveY : barTopY + 8, padT), padT + chartH - tipH);
|
tipH = 50;
|
||||||
return (
|
const tipX = Math.min(
|
||||||
<g>
|
Math.max(groupX - tipW / 2, padL),
|
||||||
<rect x={tipX} y={tipY} width={tipW} height={tipH} rx="6" fill="#3D2B1F" opacity="0.92" />
|
W - padR - tipW,
|
||||||
<text x={tipX + tipW / 2} y={tipY + 15} textAnchor="middle" fontSize="10" fill="#F0D9A8">
|
);
|
||||||
{d.label} ({hovered.set === "cur" ? "Hiện tại" : "Trước"})
|
const barH = (d.revenue / maxVal) * chartH;
|
||||||
</text>
|
const barTopY = padT + chartH - barH;
|
||||||
<text x={tipX + tipW / 2} y={tipY + 30} textAnchor="middle" fontSize="11" fontWeight="600" fill="#C8973A">{formatCurrency(d.revenue)}</text>
|
const aboveY = barTopY - tipH - 8;
|
||||||
<text x={tipX + tipW / 2} y={tipY + 44} textAnchor="middle" fontSize="10" fill="#A08060">{d.orders} đơn hàng</text>
|
const tipY = Math.min(
|
||||||
</g>
|
Math.max(aboveY >= padT ? aboveY : barTopY + 8, padT),
|
||||||
);
|
padT + chartH - tipH,
|
||||||
})()}
|
);
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x={tipX}
|
||||||
|
y={tipY}
|
||||||
|
width={tipW}
|
||||||
|
height={tipH}
|
||||||
|
rx="6"
|
||||||
|
fill="#3D2B1F"
|
||||||
|
opacity="0.92"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 15}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#F0D9A8"
|
||||||
|
>
|
||||||
|
{d.label} ({hovered.set === "cur" ? "Hiện tại" : "Trước"})
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 30}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="11"
|
||||||
|
fontWeight="600"
|
||||||
|
fill="#C8973A"
|
||||||
|
>
|
||||||
|
{formatCurrency(d.revenue)}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 44}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{d.orders} đơn hàng
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<rect x={padL} y={4} width={10} height={10} rx="2" fill="#6F4E37" />
|
<rect x={padL} y={4} width={10} height={10} rx="2" fill="#6F4E37" />
|
||||||
<text x={padL + 13} y={13} fontSize="10" fill="#6F4E37">Hiện tại</text>
|
<text x={padL + 13} y={13} fontSize="10" fill="#6F4E37">
|
||||||
<rect x={padL + 65} y={4} width={10} height={10} rx="2" fill="#E2C9A8" />
|
Hiện tại
|
||||||
<text x={padL + 78} y={13} fontSize="10" fill="#A08060">Kỳ trước</text>
|
</text>
|
||||||
|
<rect
|
||||||
|
x={padL + 65}
|
||||||
|
y={4}
|
||||||
|
width={10}
|
||||||
|
height={10}
|
||||||
|
rx="2"
|
||||||
|
fill="#E2C9A8"
|
||||||
|
/>
|
||||||
|
<text x={padL + 78} y={13} fontSize="10" fill="#A08060">
|
||||||
|
Kỳ trước
|
||||||
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export function LineChart({ data, height = 200 }: LineChartProps) {
|
|||||||
const [hovered, setHovered] = useState<number | null>(null);
|
const [hovered, setHovered] = useState<number | null>(null);
|
||||||
const W = 800;
|
const W = 800;
|
||||||
const H = height;
|
const H = height;
|
||||||
const padL = 56, padR = 16, padT = 16, padB = 40;
|
const padL = 56,
|
||||||
|
padR = 16,
|
||||||
|
padT = 16,
|
||||||
|
padB = 40;
|
||||||
const chartW = W - padL - padR;
|
const chartW = W - padL - padR;
|
||||||
const chartH = H - padT - padB;
|
const chartH = H - padT - padB;
|
||||||
|
|
||||||
@@ -66,47 +69,122 @@ export function LineChart({ data, height = 200 }: LineChartProps) {
|
|||||||
|
|
||||||
{gridLines.map((g, i) => (
|
{gridLines.map((g, i) => (
|
||||||
<g key={i}>
|
<g key={i}>
|
||||||
<line x1={padL} y1={g.y} x2={W - padR} y2={g.y} stroke="#E2C9A8" strokeWidth="1" strokeDasharray={i === yTicks ? "0" : "4 3"} />
|
<line
|
||||||
<text x={padL - 6} y={g.y + 4} textAnchor="end" fontSize="10" fill="#A08060">{formatCurrency(g.val)}</text>
|
x1={padL}
|
||||||
|
y1={g.y}
|
||||||
|
x2={W - padR}
|
||||||
|
y2={g.y}
|
||||||
|
stroke="#E2C9A8"
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray={i === yTicks ? "0" : "4 3"}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={padL - 6}
|
||||||
|
y={g.y + 4}
|
||||||
|
textAnchor="end"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{formatCurrency(g.val)}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<path d={areaD} fill="url(#areaGrad)" />
|
<path d={areaD} fill="url(#areaGrad)" />
|
||||||
<path d={pathD} fill="none" stroke="#6F4E37" strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round" />
|
<path
|
||||||
|
d={pathD}
|
||||||
|
fill="none"
|
||||||
|
stroke="#6F4E37"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
|
||||||
{points.map((p, i) =>
|
{points.map((p, i) =>
|
||||||
i % step === 0 ? (
|
i % step === 0 ? (
|
||||||
<text key={i} x={p.x} y={H - 8} textAnchor="middle" fontSize="10" fill="#A08060">{p.data.label}</text>
|
<text
|
||||||
|
key={i}
|
||||||
|
x={p.x}
|
||||||
|
y={H - 8}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{p.data.label}
|
||||||
|
</text>
|
||||||
) : null,
|
) : null,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{points.map((p) => (
|
{points.map((p) => (
|
||||||
<circle
|
<circle
|
||||||
key={p.index}
|
key={p.index}
|
||||||
cx={p.x} cy={p.y}
|
cx={p.x}
|
||||||
|
cy={p.y}
|
||||||
r={hovered === p.index ? 5 : 3}
|
r={hovered === p.index ? 5 : 3}
|
||||||
fill={hovered === p.index ? "#C8973A" : "#6F4E37"}
|
fill={hovered === p.index ? "#C8973A" : "#6F4E37"}
|
||||||
stroke="#FDF6EC" strokeWidth="2"
|
stroke="#FDF6EC"
|
||||||
|
strokeWidth="2"
|
||||||
style={{ cursor: "pointer", transition: "r 150ms" }}
|
style={{ cursor: "pointer", transition: "r 150ms" }}
|
||||||
onMouseEnter={() => setHovered(p.index)}
|
onMouseEnter={() => setHovered(p.index)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{hovered !== null && (() => {
|
{hovered !== null &&
|
||||||
const p = points[hovered];
|
(() => {
|
||||||
const tipW = 120, tipH = 48;
|
const p = points[hovered];
|
||||||
const tipX = Math.min(Math.max(p.x - tipW / 2, padL), W - padR - tipW);
|
const tipW = 120,
|
||||||
const aboveY = p.y - tipH - 10;
|
tipH = 48;
|
||||||
const tipY = Math.min(Math.max(aboveY >= padT ? aboveY : p.y + 10, padT), padT + chartH - tipH);
|
const tipX = Math.min(
|
||||||
return (
|
Math.max(p.x - tipW / 2, padL),
|
||||||
<g>
|
W - padR - tipW,
|
||||||
<rect x={tipX} y={tipY} width={tipW} height={tipH} rx="6" fill="#3D2B1F" opacity="0.92" />
|
);
|
||||||
<text x={tipX + tipW / 2} y={tipY + 16} textAnchor="middle" fontSize="10" fill="#F0D9A8">{p.data.label}</text>
|
const aboveY = p.y - tipH - 10;
|
||||||
<text x={tipX + tipW / 2} y={tipY + 30} textAnchor="middle" fontSize="11" fontWeight="600" fill="#C8973A">{formatCurrency(p.data.revenue)}</text>
|
const tipY = Math.min(
|
||||||
<text x={tipX + tipW / 2} y={tipY + 44} textAnchor="middle" fontSize="10" fill="#A08060">{p.data.orders} đơn</text>
|
Math.max(aboveY >= padT ? aboveY : p.y + 10, padT),
|
||||||
</g>
|
padT + chartH - tipH,
|
||||||
);
|
);
|
||||||
})()}
|
return (
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x={tipX}
|
||||||
|
y={tipY}
|
||||||
|
width={tipW}
|
||||||
|
height={tipH}
|
||||||
|
rx="6"
|
||||||
|
fill="#3D2B1F"
|
||||||
|
opacity="0.92"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 16}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#F0D9A8"
|
||||||
|
>
|
||||||
|
{p.data.label}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 30}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="11"
|
||||||
|
fontWeight="600"
|
||||||
|
fill="#C8973A"
|
||||||
|
>
|
||||||
|
{formatCurrency(p.data.revenue)}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={tipX + tipW / 2}
|
||||||
|
y={tipY + 44}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#A08060"
|
||||||
|
>
|
||||||
|
{p.data.orders} đơn
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
export interface PieSlice {
|
export interface PieSlice {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -78,7 +78,14 @@ export function PieChart({ data }: PieChartProps) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hovered !== null && (
|
{hovered !== null && (
|
||||||
<text x={CX} y={CY + 5} textAnchor="middle" fontSize="12" fontWeight="bold" fill="#3D2B1F">
|
<text
|
||||||
|
x={CX}
|
||||||
|
y={CY + 5}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="12"
|
||||||
|
fontWeight="bold"
|
||||||
|
fill="#3D2B1F"
|
||||||
|
>
|
||||||
{slices[hovered].percent.toFixed(1)}%
|
{slices[hovered].percent.toFixed(1)}%
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
@@ -93,14 +100,22 @@ export function PieChart({ data }: PieChartProps) {
|
|||||||
onMouseEnter={() => setHovered(s.index)}
|
onMouseEnter={() => setHovered(s.index)}
|
||||||
onMouseLeave={() => setHovered(null)}
|
onMouseLeave={() => setHovered(null)}
|
||||||
>
|
>
|
||||||
<span className="inline-block h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: s.color }} />
|
<span
|
||||||
|
className="inline-block h-3 w-3 shrink-0 rounded-full"
|
||||||
|
style={{ backgroundColor: s.color }}
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className="max-w-35 truncate"
|
className="max-w-35 truncate"
|
||||||
style={{ color: hovered === s.index ? "#3D2B1F" : "#6F4E37", fontWeight: hovered === s.index ? 600 : 400 }}
|
style={{
|
||||||
|
color: hovered === s.index ? "#3D2B1F" : "#6F4E37",
|
||||||
|
fontWeight: hovered === s.index ? 600 : 400,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-(--color-text-muted)">{s.percent.toFixed(1)}%</span>
|
<span className="text-xs text-(--color-text-muted)">
|
||||||
|
{s.percent.toFixed(1)}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,18 +32,33 @@ export function ProductTable({ data }: ProductTableProps) {
|
|||||||
|
|
||||||
const handleSort = (key: keyof ProductSalesStats) => {
|
const handleSort = (key: keyof ProductSalesStats) => {
|
||||||
if (key === sortKey) setSortDir((d) => (d === "desc" ? "asc" : "desc"));
|
if (key === sortKey) setSortDir((d) => (d === "desc" ? "asc" : "desc"));
|
||||||
else { setSortKey(key); setSortDir("desc"); }
|
else {
|
||||||
|
setSortKey(key);
|
||||||
|
setSortDir("desc");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortIcon = (col: keyof ProductSalesStats) => (
|
const sortIcon = (col: keyof ProductSalesStats) => (
|
||||||
<i className={`fa-solid ml-1 text-xs ${
|
<i
|
||||||
sortKey === col
|
className={`fa-solid ml-1 text-xs ${
|
||||||
? sortDir === "desc" ? "fa-sort-down text-(--color-primary)" : "fa-sort-up text-(--color-primary)"
|
sortKey === col
|
||||||
: "fa-sort text-(--color-text-muted)"
|
? sortDir === "desc"
|
||||||
}`} />
|
? "fa-sort-down text-(--color-primary)"
|
||||||
|
: "fa-sort-up text-(--color-primary)"
|
||||||
|
: "fa-sort text-(--color-text-muted)"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SortTh = ({ col, label, className = "" }: { col: keyof ProductSalesStats; label: string; className?: string }) => (
|
const SortTh = ({
|
||||||
|
col,
|
||||||
|
label,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
col: keyof ProductSalesStats;
|
||||||
|
label: string;
|
||||||
|
className?: string;
|
||||||
|
}) => (
|
||||||
<th
|
<th
|
||||||
className={`cursor-pointer px-4 py-3 font-semibold text-(--color-text-secondary) hover:text-(--color-primary) ${className}`}
|
className={`cursor-pointer px-4 py-3 font-semibold text-(--color-text-secondary) hover:text-(--color-primary) ${className}`}
|
||||||
onClick={() => handleSort(col)}
|
onClick={() => handleSort(col)}
|
||||||
@@ -57,38 +72,67 @@ export function ProductTable({ data }: ProductTableProps) {
|
|||||||
<table className="w-full min-w-175 text-sm">
|
<table className="w-full min-w-175 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-background border-b border-(--color-border-light)">
|
<tr className="bg-background border-b border-(--color-border-light)">
|
||||||
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">#</th>
|
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
|
||||||
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">Sản phẩm</th>
|
#
|
||||||
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">Danh mục</th>
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
|
||||||
|
Sản phẩm
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left font-semibold text-(--color-text-secondary)">
|
||||||
|
Danh mục
|
||||||
|
</th>
|
||||||
<SortTh col="unitsSold" label="Số lượng" className="text-right" />
|
<SortTh col="unitsSold" label="Số lượng" className="text-right" />
|
||||||
<SortTh col="revenue" label="Doanh thu" className="text-right" />
|
<SortTh col="revenue" label="Doanh thu" className="text-right" />
|
||||||
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">Giá nhập</th>
|
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">
|
||||||
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">Giá bán</th>
|
Giá nhập
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-right font-semibold text-(--color-text-secondary)">
|
||||||
|
Giá bán
|
||||||
|
</th>
|
||||||
<SortTh col="profit" label="Lợi nhuận" className="text-right" />
|
<SortTh col="profit" label="Lợi nhuận" className="text-right" />
|
||||||
<SortTh col="profitMargin" label="Biên LN" className="text-right" />
|
<SortTh col="profitMargin" label="Biên LN" className="text-right" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sorted.map((row, i) => (
|
{sorted.map((row, i) => (
|
||||||
<tr key={row.productId} className="border-b border-(--color-border-light) bg-(--color-bg-card) transition-colors hover:bg-(--color-accent-light)/30">
|
<tr
|
||||||
|
key={row.productId}
|
||||||
|
className="border-b border-(--color-border-light) bg-(--color-bg-card) transition-colors hover:bg-(--color-accent-light)/30"
|
||||||
|
>
|
||||||
<td className="px-4 py-3 text-(--color-text-muted)">{i + 1}</td>
|
<td className="px-4 py-3 text-(--color-text-muted)">{i + 1}</td>
|
||||||
<td className="text-foreground px-4 py-3 font-medium">{row.name}</td>
|
<td className="text-foreground px-4 py-3 font-medium">
|
||||||
|
{row.name}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className="rounded-full bg-(--color-accent-light) px-2 py-0.5 text-xs text-(--color-primary)">
|
<span className="rounded-full bg-(--color-accent-light) px-2 py-0.5 text-xs text-(--color-primary)">
|
||||||
{categoryName(row.category)}
|
{categoryName(row.category)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-foreground px-4 py-3 text-right tabular-nums">{row.unitsSold.toLocaleString()}</td>
|
<td className="text-foreground px-4 py-3 text-right tabular-nums">
|
||||||
<td className="px-4 py-3 text-right font-medium text-(--color-primary) tabular-nums">{formatCurrencyFull(row.revenue)}</td>
|
{row.unitsSold.toLocaleString()}
|
||||||
<td className="px-4 py-3 text-right text-(--color-text-muted) tabular-nums">{formatCurrencyFull(row.costPrice)}</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right text-(--color-text-secondary) tabular-nums">{formatCurrencyFull(row.sellingPrice)}</td>
|
<td className="px-4 py-3 text-right font-medium text-(--color-primary) tabular-nums">
|
||||||
<td className="px-4 py-3 text-right font-medium text-green-600 tabular-nums">{formatCurrencyFull(row.profit)}</td>
|
{formatCurrencyFull(row.revenue)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-(--color-text-muted) tabular-nums">
|
||||||
|
{formatCurrencyFull(row.costPrice)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right text-(--color-text-secondary) tabular-nums">
|
||||||
|
{formatCurrencyFull(row.sellingPrice)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right font-medium text-green-600 tabular-nums">
|
||||||
|
{formatCurrencyFull(row.profit)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${
|
<span
|
||||||
row.profitMargin >= 70 ? "bg-green-100 text-green-700"
|
className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${
|
||||||
: row.profitMargin >= 60 ? "bg-yellow-100 text-yellow-700"
|
row.profitMargin >= 70
|
||||||
: "bg-red-100 text-red-600"
|
? "bg-green-100 text-green-700"
|
||||||
}`}>
|
: row.profitMargin >= 60
|
||||||
|
? "bg-yellow-100 text-yellow-700"
|
||||||
|
: "bg-red-100 text-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{row.profitMargin.toFixed(1)}%
|
{row.profitMargin.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export function SummaryCard({
|
|||||||
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-(--color-accent-light) text-lg text-(--color-primary)">
|
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-(--color-accent-light) text-lg text-(--color-primary)">
|
||||||
<i className={icon}></i>
|
<i className={icon}></i>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium text-(--color-text-muted)">{title}</span>
|
<span className="text-sm font-medium text-(--color-text-muted)">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-foreground text-2xl font-bold tabular-nums">{value}</p>
|
<p className="text-foreground text-2xl font-bold tabular-nums">{value}</p>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import Button from "@/components/atoms/buttons/Button";
|
||||||
import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
|
import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
|
||||||
import LoginInput from "@/components/atoms/inputs/LoginInput";
|
import LoginInput from "@/components/atoms/inputs/LoginInput";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { FormEvent, useState } from "react";
|
import { FormEvent, useState } from "react";
|
||||||
import Button from "@/components/atoms/buttons/Button";
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -102,7 +102,12 @@ export default function LoginForm() {
|
|||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
{/* Login Button */}
|
{/* Login Button */}
|
||||||
<Button variant="primaryNoBorder" type="submit" style="login" size="lg">
|
<Button
|
||||||
|
variant="primaryNoBorder"
|
||||||
|
type="submit"
|
||||||
|
style="login"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
Đăng nhập
|
Đăng nhập
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,10 @@ export default function ProductsTab() {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
filtered.map((p) => (
|
filtered.map((p) => (
|
||||||
<tr key={p.id} className="hover:bg-background transition-colors">
|
<tr
|
||||||
|
key={p.id}
|
||||||
|
className="hover:bg-background transition-colors"
|
||||||
|
>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-foreground font-medium">{p.name}</p>
|
<p className="text-foreground font-medium">{p.name}</p>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: frontend-container
|
- name: frontend-container
|
||||||
image: git.demonkernel.io.vn/foodsurf/frontend:1.0.10
|
image: git.demonkernel.io.vn/foodsurf/frontend:1.1.0
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 3000
|
- containerPort: 3000
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/node": "^20.19.37",
|
"@types/node": "^20.19.37",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/node": "^20.19.37",
|
"@types/node": "^20.19.39",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
|
|||||||
Generated
+73
-84
@@ -12,8 +12,8 @@ importers:
|
|||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
version: 4.2.2
|
version: 4.2.2
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.19.37
|
specifier: ^20.19.39
|
||||||
version: 20.19.37
|
version: 20.19.39
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.2.14
|
specifier: ^19.2.14
|
||||||
version: 19.2.14
|
version: 19.2.14
|
||||||
@@ -171,14 +171,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||||
engines: {node: '>=0.1.90'}
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
'@emnapi/core@1.9.1':
|
'@emnapi/core@1.9.2':
|
||||||
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
|
resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
|
||||||
|
|
||||||
'@emnapi/runtime@1.9.1':
|
'@emnapi/runtime@1.9.2':
|
||||||
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
|
resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.2.0':
|
'@emnapi/wasi-threads@1.2.1':
|
||||||
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
|
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.9.1':
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
@@ -749,8 +749,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
||||||
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/node@20.19.37':
|
'@types/node@20.19.39':
|
||||||
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
|
resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==}
|
||||||
|
|
||||||
'@types/normalize-package-data@2.4.4':
|
'@types/normalize-package-data@2.4.4':
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||||
@@ -1092,8 +1092,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.12:
|
baseline-browser-mapping@2.10.15:
|
||||||
resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==}
|
resolution: {integrity: sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -1171,8 +1171,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001782:
|
caniuse-lite@1.0.30001786:
|
||||||
resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==}
|
resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==}
|
||||||
|
|
||||||
chalk@2.4.1:
|
chalk@2.4.1:
|
||||||
resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==}
|
resolution: {integrity: sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==}
|
||||||
@@ -1463,8 +1463,8 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.329:
|
electron-to-chromium@1.5.331:
|
||||||
resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==}
|
resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==}
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
@@ -1567,8 +1567,8 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.10:
|
||||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==}
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1:
|
eslint-import-resolver-typescript@3.10.1:
|
||||||
resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==}
|
resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==}
|
||||||
@@ -2424,8 +2424,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
lodash-es@4.17.23:
|
lodash-es@4.18.1:
|
||||||
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
|
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||||
|
|
||||||
lodash.capitalize@4.2.1:
|
lodash.capitalize@4.2.1:
|
||||||
resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==}
|
resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==}
|
||||||
@@ -2463,12 +2463,12 @@ packages:
|
|||||||
lodash@4.17.11:
|
lodash@4.17.11:
|
||||||
resolution: {integrity: sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==}
|
resolution: {integrity: sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==}
|
||||||
|
|
||||||
lodash@4.17.23:
|
|
||||||
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
|
||||||
|
|
||||||
lodash@4.17.5:
|
lodash@4.17.5:
|
||||||
resolution: {integrity: sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==}
|
resolution: {integrity: sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==}
|
||||||
|
|
||||||
|
lodash@4.18.1:
|
||||||
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2480,8 +2480,8 @@ packages:
|
|||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
lru-cache@11.2.7:
|
lru-cache@11.3.0:
|
||||||
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
|
resolution: {integrity: sha512-sr8xPKE25m6vJVcrdn6NxtC0fVfuPowbscLypegRgOm0yXSqr5JNHCAY3hnusdJ7HRBW04j6Ip4khvHU778DuQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@@ -2660,8 +2660,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
|
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
node-releases@2.0.36:
|
node-releases@2.0.37:
|
||||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
|
||||||
|
|
||||||
node-rsa@0.4.2:
|
node-rsa@0.4.2:
|
||||||
resolution: {integrity: sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA==}
|
resolution: {integrity: sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA==}
|
||||||
@@ -3192,11 +3192,6 @@ packages:
|
|||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
resolve@1.22.11:
|
|
||||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
|
||||||
engines: {node: '>= 0.4'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
resolve@2.0.0-next.6:
|
resolve@2.0.0-next.6:
|
||||||
resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==}
|
resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3643,8 +3638,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
undici@7.24.6:
|
undici@7.24.7:
|
||||||
resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==}
|
resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unicode-emoji-modifier-base@1.0.0:
|
unicode-emoji-modifier-base@1.0.0:
|
||||||
@@ -3971,18 +3966,18 @@ snapshots:
|
|||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/core@1.9.1':
|
'@emnapi/core@1.9.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.2.0
|
'@emnapi/wasi-threads': 1.2.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/runtime@1.9.1':
|
'@emnapi/runtime@1.9.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.2.0':
|
'@emnapi/wasi-threads@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
@@ -4129,7 +4124,7 @@ snapshots:
|
|||||||
|
|
||||||
'@img/sharp-wasm32@0.34.5':
|
'@img/sharp-wasm32@0.34.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/runtime': 1.9.1
|
'@emnapi/runtime': 1.9.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@img/sharp-win32-arm64@0.34.5':
|
'@img/sharp-win32-arm64@0.34.5':
|
||||||
@@ -4162,8 +4157,8 @@ snapshots:
|
|||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.9.1
|
'@emnapi/core': 1.9.2
|
||||||
'@emnapi/runtime': 1.9.1
|
'@emnapi/runtime': 1.9.2
|
||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -4295,7 +4290,7 @@ snapshots:
|
|||||||
fs-extra: 8.1.0
|
fs-extra: 8.1.0
|
||||||
globby: 10.0.2
|
globby: 10.0.2
|
||||||
got: 10.7.0
|
got: 10.7.0
|
||||||
lodash: 4.17.23
|
lodash: 4.18.1
|
||||||
querystring: 0.2.1
|
querystring: 0.2.1
|
||||||
url-join: 4.0.1
|
url-join: 4.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4311,7 +4306,7 @@ snapshots:
|
|||||||
conventional-commits-parser: 6.4.0
|
conventional-commits-parser: 6.4.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
import-from-esm: 2.0.0
|
import-from-esm: 2.0.0
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
micromatch: 4.0.8
|
micromatch: 4.0.8
|
||||||
semantic-release: 25.0.3(typescript@5.9.3)
|
semantic-release: 25.0.3(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4327,7 +4322,7 @@ snapshots:
|
|||||||
aggregate-error: 3.1.0
|
aggregate-error: 3.1.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
execa: 9.6.1
|
execa: 9.6.1
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
parse-json: 8.3.0
|
parse-json: 8.3.0
|
||||||
semantic-release: 25.0.3(typescript@5.9.3)
|
semantic-release: 25.0.3(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4346,12 +4341,12 @@ snapshots:
|
|||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
issue-parser: 7.0.1
|
issue-parser: 7.0.1
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
mime: 4.1.0
|
mime: 4.1.0
|
||||||
p-filter: 4.1.0
|
p-filter: 4.1.0
|
||||||
semantic-release: 25.0.3(typescript@5.9.3)
|
semantic-release: 25.0.3(typescript@5.9.3)
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
undici: 7.24.6
|
undici: 7.24.7
|
||||||
url-join: 5.0.0
|
url-join: 5.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4364,7 +4359,7 @@ snapshots:
|
|||||||
env-ci: 11.2.0
|
env-ci: 11.2.0
|
||||||
execa: 9.6.1
|
execa: 9.6.1
|
||||||
fs-extra: 11.3.4
|
fs-extra: 11.3.4
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
nerf-dart: 1.0.0
|
nerf-dart: 1.0.0
|
||||||
normalize-url: 9.0.0
|
normalize-url: 9.0.0
|
||||||
npm: 11.12.1
|
npm: 11.12.1
|
||||||
@@ -4385,7 +4380,7 @@ snapshots:
|
|||||||
get-stream: 7.0.1
|
get-stream: 7.0.1
|
||||||
import-from-esm: 2.0.0
|
import-from-esm: 2.0.0
|
||||||
into-stream: 7.0.0
|
into-stream: 7.0.0
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
read-package-up: 11.0.0
|
read-package-up: 11.0.0
|
||||||
semantic-release: 25.0.3(typescript@5.9.3)
|
semantic-release: 25.0.3(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4483,7 +4478,7 @@ snapshots:
|
|||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.0
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
javascript-natural-sort: 0.7.1
|
javascript-natural-sort: 0.7.1
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
minimatch: 9.0.9
|
minimatch: 9.0.9
|
||||||
parse-imports-exports: 0.2.4
|
parse-imports-exports: 0.2.4
|
||||||
prettier: 3.8.1
|
prettier: 3.8.1
|
||||||
@@ -4499,7 +4494,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-cache-semantics': 4.2.0
|
'@types/http-cache-semantics': 4.2.0
|
||||||
'@types/keyv': 3.1.4
|
'@types/keyv': 3.1.4
|
||||||
'@types/node': 20.19.37
|
'@types/node': 20.19.39
|
||||||
'@types/responselike': 1.0.3
|
'@types/responselike': 1.0.3
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
@@ -4507,7 +4502,7 @@ snapshots:
|
|||||||
'@types/glob@7.2.0':
|
'@types/glob@7.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 6.0.0
|
'@types/minimatch': 6.0.0
|
||||||
'@types/node': 20.19.37
|
'@types/node': 20.19.39
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.2.0': {}
|
'@types/http-cache-semantics@4.2.0': {}
|
||||||
|
|
||||||
@@ -4517,13 +4512,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/keyv@3.1.4':
|
'@types/keyv@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.37
|
'@types/node': 20.19.39
|
||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
'@types/minimatch@6.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
|
|
||||||
'@types/node@20.19.37':
|
'@types/node@20.19.39':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
@@ -4539,7 +4534,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/responselike@1.0.3':
|
'@types/responselike@1.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.37
|
'@types/node': 20.19.39
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4879,7 +4874,7 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.12: {}
|
baseline-browser-mapping@2.10.15: {}
|
||||||
|
|
||||||
basic-auth@2.0.1:
|
basic-auth@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4929,10 +4924,10 @@ snapshots:
|
|||||||
|
|
||||||
browserslist@4.28.2:
|
browserslist@4.28.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.10.12
|
baseline-browser-mapping: 2.10.15
|
||||||
caniuse-lite: 1.0.30001782
|
caniuse-lite: 1.0.30001786
|
||||||
electron-to-chromium: 1.5.329
|
electron-to-chromium: 1.5.331
|
||||||
node-releases: 2.0.36
|
node-releases: 2.0.37
|
||||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
@@ -4975,7 +4970,7 @@ snapshots:
|
|||||||
|
|
||||||
callsites@3.1.0: {}
|
callsites@3.1.0: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001782: {}
|
caniuse-lite@1.0.30001786: {}
|
||||||
|
|
||||||
chalk@2.4.1:
|
chalk@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5260,7 +5255,7 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.329: {}
|
electron-to-chromium@1.5.331: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
@@ -5412,7 +5407,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 16.1.7
|
'@next/eslint-plugin-next': 16.1.7
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.10
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
|
||||||
@@ -5428,11 +5423,11 @@ snapshots:
|
|||||||
- eslint-plugin-import-x
|
- eslint-plugin-import-x
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
resolve: 1.22.11
|
resolve: 2.0.0-next.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5451,13 +5446,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.10
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5472,8 +5467,8 @@ snapshots:
|
|||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 9.39.4(jiti@2.6.1)
|
eslint: 9.39.4(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.10
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -6018,7 +6013,7 @@ snapshots:
|
|||||||
|
|
||||||
hosted-git-info@9.0.2:
|
hosted-git-info@9.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 11.2.7
|
lru-cache: 11.3.0
|
||||||
|
|
||||||
http-cache-semantics@4.2.0: {}
|
http-cache-semantics@4.2.0: {}
|
||||||
|
|
||||||
@@ -6420,7 +6415,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
||||||
lodash-es@4.17.23: {}
|
lodash-es@4.18.1: {}
|
||||||
|
|
||||||
lodash.capitalize@4.2.1: {}
|
lodash.capitalize@4.2.1: {}
|
||||||
|
|
||||||
@@ -6446,10 +6441,10 @@ snapshots:
|
|||||||
|
|
||||||
lodash@4.17.11: {}
|
lodash@4.17.11: {}
|
||||||
|
|
||||||
lodash@4.17.23: {}
|
|
||||||
|
|
||||||
lodash@4.17.5: {}
|
lodash@4.17.5: {}
|
||||||
|
|
||||||
|
lodash@4.18.1: {}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
@@ -6458,7 +6453,7 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.2.7: {}
|
lru-cache@11.3.0: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6585,8 +6580,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.7
|
'@next/env': 16.1.7
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
baseline-browser-mapping: 2.10.12
|
baseline-browser-mapping: 2.10.15
|
||||||
caniuse-lite: 1.0.30001782
|
caniuse-lite: 1.0.30001786
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
@@ -6621,7 +6616,7 @@ snapshots:
|
|||||||
object.entries: 1.1.9
|
object.entries: 1.1.9
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
node-releases@2.0.36: {}
|
node-releases@2.0.37: {}
|
||||||
|
|
||||||
node-rsa@0.4.2:
|
node-rsa@0.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7047,12 +7042,6 @@ snapshots:
|
|||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
resolve@1.22.11:
|
|
||||||
dependencies:
|
|
||||||
is-core-module: 2.16.1
|
|
||||||
path-parse: 1.0.7
|
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
|
||||||
|
|
||||||
resolve@2.0.0-next.6:
|
resolve@2.0.0-next.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -7120,7 +7109,7 @@ snapshots:
|
|||||||
hook-std: 4.0.0
|
hook-std: 4.0.0
|
||||||
hosted-git-info: 9.0.2
|
hosted-git-info: 9.0.2
|
||||||
import-from-esm: 2.0.0
|
import-from-esm: 2.0.0
|
||||||
lodash-es: 4.17.23
|
lodash-es: 4.18.1
|
||||||
marked: 15.0.12
|
marked: 15.0.12
|
||||||
marked-terminal: 7.3.0(marked@15.0.12)
|
marked-terminal: 7.3.0(marked@15.0.12)
|
||||||
micromatch: 4.0.8
|
micromatch: 4.0.8
|
||||||
@@ -7631,7 +7620,7 @@ snapshots:
|
|||||||
|
|
||||||
undici@6.24.1: {}
|
undici@6.24.1: {}
|
||||||
|
|
||||||
undici@7.24.6: {}
|
undici@7.24.7: {}
|
||||||
|
|
||||||
unicode-emoji-modifier-base@1.0.0: {}
|
unicode-emoji-modifier-base@1.0.0: {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user