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