chore: release [ci skip]
This commit is contained in:
+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.
|
||||
|
||||
@@ -42,6 +42,7 @@ Main interactive button component with multiple variants and sizes.
|
||||
- All standard HTML button attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Button } from "@/components/atoms";
|
||||
|
||||
@@ -59,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
|
||||
@@ -78,6 +80,7 @@ Button designed specifically for icon-only interactions.
|
||||
- All standard HTML button attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { IconButton } from "@/components/atoms";
|
||||
|
||||
@@ -102,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";
|
||||
|
||||
@@ -135,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";
|
||||
|
||||
@@ -161,6 +166,7 @@ Multi-line text input with optional label and error.
|
||||
- All standard HTML textarea attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Textarea } from "@/components/atoms";
|
||||
|
||||
@@ -188,6 +194,7 @@ Semantic heading component with level-based sizing.
|
||||
- All standard HTML heading attributes
|
||||
|
||||
**Sizing:**
|
||||
|
||||
- Level 1: text-3xl font-bold
|
||||
- Level 2: text-2xl font-bold
|
||||
- Level 3: text-xl font-bold
|
||||
@@ -196,6 +203,7 @@ Semantic heading component with level-based sizing.
|
||||
- Level 6: text-sm font-semibold
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Heading } from "@/components/atoms";
|
||||
|
||||
@@ -217,12 +225,14 @@ Paragraph text with semantic variants.
|
||||
- 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";
|
||||
|
||||
@@ -244,6 +254,7 @@ Small caption/note text component.
|
||||
- All standard HTML span attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Caption } from "@/components/atoms";
|
||||
|
||||
@@ -268,6 +279,7 @@ Labeled badge component for highlighting information.
|
||||
- All standard HTML span attributes
|
||||
|
||||
**Variants:**
|
||||
|
||||
- primary: Branded color
|
||||
- secondary: Light gray
|
||||
- success: Green
|
||||
@@ -275,6 +287,7 @@ Labeled badge component for highlighting information.
|
||||
- warning: Yellow
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Badge } from "@/components/atoms";
|
||||
|
||||
@@ -296,6 +309,7 @@ Specialized badge for displaying formatted prices.
|
||||
- All standard HTML span attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { PriceBadge } from "@/components/atoms";
|
||||
|
||||
@@ -304,6 +318,7 @@ import { PriceBadge } from "@/components/atoms";
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
- VND: "50.000 ₫"
|
||||
- USD: "$99.99"
|
||||
|
||||
@@ -321,6 +336,7 @@ Visual separator element.
|
||||
- All standard HTML hr attributes
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Divider } from "@/components/atoms";
|
||||
|
||||
@@ -414,6 +430,7 @@ import type {
|
||||
```
|
||||
|
||||
### Product Card
|
||||
|
||||
```tsx
|
||||
<div className="rounded-lg border p-4">
|
||||
<Heading level={3}>Product Name</Heading>
|
||||
@@ -426,6 +443,7 @@ import type {
|
||||
```
|
||||
|
||||
### Rating Display
|
||||
|
||||
```tsx
|
||||
<div>
|
||||
<Heading level={4}>Reviews</Heading>
|
||||
@@ -439,6 +457,7 @@ import type {
|
||||
## 🧪 Testing Atoms
|
||||
|
||||
### Props Validation
|
||||
|
||||
```tsx
|
||||
// ✅ Valid
|
||||
<Button variant="primary" size="sm">Click</Button>
|
||||
|
||||
@@ -13,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">
|
||||
|
||||
@@ -62,7 +62,13 @@ export default function ProductCard({
|
||||
<Text variant="body2" className="font-bold">
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
<Button onClick={onBuy} variant="primary" size="sm" icon="fa-cart-plus" aria-label={`Mua ${productName}`}>
|
||||
<Button
|
||||
onClick={onBuy}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon="fa-cart-plus"
|
||||
aria-label={`Mua ${productName}`}
|
||||
>
|
||||
Mua
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,10 +16,16 @@ 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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user