resolve conflict
This commit is contained in:
+52
-47
@@ -1,40 +1,44 @@
|
||||
# Components Documentation
|
||||
|
||||
> Whenever you create a new component in the components/ directory, automatically append a new section following the template below.
|
||||
> Whenever you create a new component in the components/ directory,
|
||||
> automatically append a new section following the template below.
|
||||
|
||||
---
|
||||
|
||||
## CartProduct
|
||||
|
||||
**File:** components/CartProduct.tsx
|
||||
**Description:** Product card component. Displays product image, name, description, formatted price, and a Buy button. Width is controlled by the parent grid (w-full), not the card itself.
|
||||
**File:** components/CartProduct.tsx **Description:** Product card component.
|
||||
Displays product image, name, description, formatted price, and a Buy button.
|
||||
Width is controlled by the parent grid (w-full), not the card itself.
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| image | string | yes | - | URL/path to product image (Next.js Image) |
|
||||
| imageAlt | string | no | Anh san pham | Alt text for accessibility |
|
||||
| productName | string | yes | - | Product display name |
|
||||
| price | number or string | yes | - | If number: auto-formatted to VND. If string: rendered as-is |
|
||||
| description | string | yes | - | Short description, clamped to 2 lines |
|
||||
| onBuy | () => void | no | undefined | Callback when Buy button is clicked |
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| ----------- | ---------------- | -------- | ------------ | ----------------------------------------------------------- |
|
||||
| image | string | yes | - | URL/path to product image (Next.js Image) |
|
||||
| imageAlt | string | no | Anh san pham | Alt text for accessibility |
|
||||
| productName | string | yes | - | Product display name |
|
||||
| price | number or string | yes | - | If number: auto-formatted to VND. If string: rendered as-is |
|
||||
| description | string | yes | - | Short description, clamped to 2 lines |
|
||||
| onBuy | () => void | no | undefined | Callback when Buy button is clicked |
|
||||
|
||||
### Internal Logic
|
||||
|
||||
- formattedPrice: number -> toLocaleString(vi-VN, { style: currency, currency: VND })
|
||||
- Image fallback: fa-solid fa-mug-hot icon shown behind image; if image fails onError hides the img element
|
||||
- formattedPrice: number -> toLocaleString(vi-VN, { style: currency, currency:
|
||||
VND })
|
||||
- Image fallback: fa-solid fa-mug-hot icon shown behind image; if image fails
|
||||
onError hides the img element
|
||||
|
||||
### Styling (CSS variables)
|
||||
|
||||
| Element | Key classes |
|
||||
|---------|-------------|
|
||||
| Element | Key classes |
|
||||
| ------------ | ------------------------------------------------------------------ |
|
||||
| Card wrapper | flex flex-col w-full rounded-2xl, shadow uses --color-shadow-sm/md |
|
||||
| Image area | relative w-full h-36, bg --color-border-light |
|
||||
| Product name | font-bold text-sm, color --color-text-primary, line-clamp-1 |
|
||||
| Description | text-xs, color --color-text-muted, line-clamp-2 |
|
||||
| Price | text-sm font-bold, color --color-primary |
|
||||
| Buy button | bg --color-primary, hover --color-primary-dark, active:scale-95 |
|
||||
| Image area | relative w-full h-36, bg --color-border-light |
|
||||
| Product name | font-bold text-sm, color --color-text-primary, line-clamp-1 |
|
||||
| Description | text-xs, color --color-text-muted, line-clamp-2 |
|
||||
| Price | text-sm font-bold, color --color-primary |
|
||||
| Buy button | bg --color-primary, hover --color-primary-dark, active:scale-95 |
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -52,17 +56,17 @@
|
||||
|
||||
## Navbar
|
||||
|
||||
**File:** components/Navbar.tsx
|
||||
**Description:** Left sidebar with collapsible category filter. Sticky below header, full viewport height minus header.
|
||||
**File:** components/Navbar.tsx **Description:** Left sidebar with collapsible
|
||||
category filter. Sticky below header, full viewport height minus header.
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| isOpen | boolean | yes | - | true = expanded (240px), false = collapsed (64px) |
|
||||
| onToggle | () => void | yes | - | Toggle expand/collapse |
|
||||
| activeCategory | string | no | all | Currently selected category id |
|
||||
| onCategoryChange | (id: string) => void | no | undefined | Fired when user clicks a category |
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| ---------------- | -------------------- | -------- | --------- | ------------------------------------------------- |
|
||||
| isOpen | boolean | yes | - | true = expanded (240px), false = collapsed (64px) |
|
||||
| onToggle | () => void | yes | - | Toggle expand/collapse |
|
||||
| activeCategory | string | no | all | Currently selected category id |
|
||||
| onCategoryChange | (id: string) => void | no | undefined | Fired when user clicks a category |
|
||||
|
||||
### Behavior
|
||||
|
||||
@@ -74,11 +78,11 @@
|
||||
|
||||
### Styling
|
||||
|
||||
| Element | Key classes |
|
||||
|---------|-------------|
|
||||
| Aside | sticky, border-r --color-border, bg --color-bg-sidebar |
|
||||
| Toggle button | w-8 h-8 rounded-lg, hover bg --color-border-light |
|
||||
| Active item | bg --color-primary text-white shadow-sm |
|
||||
| Element | Key classes |
|
||||
| ------------- | ----------------------------------------------------------- |
|
||||
| Aside | sticky, border-r --color-border, bg --color-bg-sidebar |
|
||||
| Toggle button | w-8 h-8 rounded-lg, hover bg --color-border-light |
|
||||
| Active item | bg --color-primary text-white shadow-sm |
|
||||
| Inactive item | hover bg --color-border-light, color --color-text-secondary |
|
||||
|
||||
### Dependencies
|
||||
@@ -92,8 +96,9 @@
|
||||
|
||||
## Header (layouts/header.tsx)
|
||||
|
||||
**File:** layouts/header.tsx
|
||||
**Description:** Sticky top bar. 2-column layout: Brand (left) + Auth button (right). Auth cycles Guest -> Manager -> Staff -> Guest for UI demo.
|
||||
**File:** layouts/header.tsx **Description:** Sticky top bar. 2-column layout:
|
||||
Brand (left) + Auth button (right). Auth cycles Guest -> Manager -> Staff ->
|
||||
Guest for UI demo.
|
||||
|
||||
### Props
|
||||
|
||||
@@ -101,17 +106,17 @@ None - reads SHOP_INFO and MOCK_USERS from lib/constants directly.
|
||||
|
||||
### Internal State
|
||||
|
||||
| State | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| user | User or null | Current demo user. null = guest |
|
||||
| State | Type | Description |
|
||||
| ----- | ------------ | ------------------------------- |
|
||||
| user | User or null | Current demo user. null = guest |
|
||||
|
||||
### Auth States
|
||||
|
||||
| State | Appearance |
|
||||
|-------|------------|
|
||||
| State | Appearance |
|
||||
| ------------ | ------------------------------------- |
|
||||
| Guest (null) | Brown primary button, Dang nhap label |
|
||||
| Manager | Gold/caramel badge with user-tie icon |
|
||||
| Staff | Avatar circle + name, bordered button |
|
||||
| Manager | Gold/caramel badge with user-tie icon |
|
||||
| Staff | Avatar circle + name, bordered button |
|
||||
|
||||
### Responsive
|
||||
|
||||
@@ -130,8 +135,8 @@ None - reads SHOP_INFO and MOCK_USERS from lib/constants directly.
|
||||
|
||||
## Footer (layouts/footer.tsx)
|
||||
|
||||
**File:** layouts/footer.tsx
|
||||
**Description:** Site footer with 12-column grid. 3 sections: Brand info, Social links, WiFi card.
|
||||
**File:** layouts/footer.tsx **Description:** Site footer with 12-column grid. 3
|
||||
sections: Brand info, Social links, WiFi card.
|
||||
|
||||
### Props
|
||||
|
||||
@@ -139,9 +144,9 @@ None - reads SHOP_INFO and SOCIAL_LINKS from lib/constants directly.
|
||||
|
||||
### Layout
|
||||
|
||||
| Section | Mobile | md | lg/xl |
|
||||
|---------|--------|----|-------|
|
||||
| Brand info | col-span-12 | col-span-6 | col-span-8/6 |
|
||||
| Section | Mobile | md | lg/xl |
|
||||
| ------------- | ----------- | ---------- | ------------ |
|
||||
| Brand info | col-span-12 | col-span-6 | col-span-8/6 |
|
||||
| Social + WiFi | col-span-12 | col-span-6 | col-span-4/6 |
|
||||
|
||||
### Sections
|
||||
|
||||
+3
-12
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCart } from "@/lib/cart-context";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function CartFab() {
|
||||
const { totalItems } = useCart();
|
||||
@@ -12,19 +12,10 @@ export default function CartFab() {
|
||||
<Link
|
||||
href="/payment"
|
||||
aria-label="Đi đến trang thanh toán"
|
||||
className="fixed right-5 bottom-6 z-70 w-14 h-14 rounded-full
|
||||
bg-(--color-primary) text-white shadow-xl
|
||||
flex items-center justify-center
|
||||
hover:bg-(--color-primary-dark)
|
||||
active:scale-95 transition-all duration-150"
|
||||
className="fixed right-5 bottom-6 z-70 flex h-14 w-14 items-center justify-center rounded-full bg-(--color-primary) text-white shadow-xl transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-cart-shopping text-lg"></i>
|
||||
<span
|
||||
className="absolute -top-1.5 -right-1.5 min-w-6 h-6 px-1.5
|
||||
rounded-full bg-(--color-accent) text-(--color-primary-dark)
|
||||
text-xs font-bold flex items-center justify-center
|
||||
border-2 border-white"
|
||||
>
|
||||
<span className="absolute -top-1.5 -right-1.5 flex h-6 min-w-6 items-center justify-center rounded-full border-2 border-white bg-(--color-accent) px-1.5 text-xs font-bold text-(--color-primary-dark)">
|
||||
{totalItems}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -35,18 +35,11 @@ export default function CartProduct({
|
||||
: price;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full rounded-2xl overflow-hidden
|
||||
bg-(--color-bg-card) border border-(--color-border-light)
|
||||
shadow-[0_2px_8px_var(--color-shadow-sm)]
|
||||
hover:shadow-[0_6px_20px_var(--color-shadow-md)]
|
||||
hover:-translate-y-0.5
|
||||
transition-all duration-250 cursor-default"
|
||||
>
|
||||
<div className="flex w-full cursor-default flex-col overflow-hidden rounded-2xl border border-(--color-border-light) bg-(--color-bg-card) shadow-[0_2px_8px_var(--color-shadow-sm)] transition-all duration-250 hover:-translate-y-0.5 hover:shadow-[0_6px_20px_var(--color-shadow-md)]">
|
||||
{/* ── Image area ── */}
|
||||
<div className="relative w-full h-36 bg-(--color-border-light) shrink-0 overflow-hidden">
|
||||
<div className="relative h-36 w-full shrink-0 overflow-hidden bg-(--color-border-light)">
|
||||
{/* Fallback icon (shown when image fails or is missing) */}
|
||||
<div className="absolute inset-0 flex items-center justify-center text-4xl text-(--color-border) z-0">
|
||||
<div className="absolute inset-0 z-0 flex items-center justify-center text-4xl text-(--color-border)">
|
||||
<i className="fa-solid fa-mug-hot"></i>
|
||||
</div>
|
||||
{/* Product image */}
|
||||
@@ -54,7 +47,7 @@ export default function CartProduct({
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
className="object-cover z-1"
|
||||
className="z-1 object-cover"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = "none";
|
||||
@@ -63,31 +56,23 @@ export default function CartProduct({
|
||||
</div>
|
||||
|
||||
{/* ── Name + description ── */}
|
||||
<div className="flex-1 flex flex-col gap-1 px-3 pt-2.5 pb-1.5">
|
||||
<h3 className="font-bold text-sm leading-tight text-foreground line-clamp-1">
|
||||
<div className="flex flex-1 flex-col gap-1 px-3 pt-2.5 pb-1.5">
|
||||
<h3 className="text-foreground line-clamp-1 text-sm leading-tight font-bold">
|
||||
{productName}
|
||||
</h3>
|
||||
<p className="text-xs leading-relaxed text-(--color-text-muted) line-clamp-2">
|
||||
<p className="line-clamp-2 text-xs leading-relaxed text-(--color-text-muted)">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Price + Buy button ── */}
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2.5
|
||||
border-t border-(--color-border-light) shrink-0"
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between border-t border-(--color-border-light) px-3 py-2.5">
|
||||
<span className="text-sm font-bold text-(--color-primary)">
|
||||
{formattedPrice}
|
||||
</span>
|
||||
<button
|
||||
onClick={onBuy}
|
||||
className="flex items-center gap-1.5 text-xs font-semibold
|
||||
px-3 py-1.5 rounded-lg border-none cursor-pointer
|
||||
bg-(--color-primary) text-white
|
||||
hover:bg-(--color-primary-dark)
|
||||
active:scale-95
|
||||
transition-all duration-150 whitespace-nowrap"
|
||||
className="flex cursor-pointer items-center gap-1.5 rounded-lg border-none bg-(--color-primary) px-3 py-1.5 text-xs font-semibold whitespace-nowrap text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-95"
|
||||
>
|
||||
<i className="fa-solid fa-cart-plus"></i>
|
||||
Mua
|
||||
|
||||
+21
-61
@@ -31,36 +31,21 @@ export default function Navbar({
|
||||
}: NavbarProps) {
|
||||
return (
|
||||
<aside
|
||||
className={`
|
||||
sticky shrink-0 flex-col z-20
|
||||
border-r border-(--color-border)
|
||||
bg-(--color-bg-sidebar)
|
||||
overflow-y-auto overflow-x-hidden
|
||||
transition-all duration-250 ease-in-out
|
||||
hidden md:flex
|
||||
xl:w-60
|
||||
${isOpen ? "w-60" : "w-16"}
|
||||
`}
|
||||
style={{
|
||||
top: "var(--spacing-header-height)",
|
||||
height: "calc(100vh - var(--spacing-header-height))",
|
||||
} as React.CSSProperties}
|
||||
className={`sticky z-20 hidden shrink-0 flex-col overflow-x-hidden overflow-y-auto border-r border-(--color-border) bg-(--color-bg-sidebar) transition-all duration-250 ease-in-out md:flex xl:w-60 ${isOpen ? "w-60" : "w-16"} `}
|
||||
style={
|
||||
{
|
||||
top: "var(--spacing-header-height)",
|
||||
height: "calc(100vh - var(--spacing-header-height))",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* ── Sidebar header: title + toggle button ── */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center border-b border-(--color-border) shrink-0
|
||||
xl:justify-between xl:px-4 xl:py-3
|
||||
${isOpen ? "justify-between px-4 py-3" : "justify-center px-0 py-3"}
|
||||
`}
|
||||
className={`flex shrink-0 items-center border-b border-(--color-border) xl:justify-between xl:px-4 xl:py-3 ${isOpen ? "justify-between px-4 py-3" : "justify-center px-0 py-3"} `}
|
||||
>
|
||||
{/* Title — shown when expanded, always shown on xl+ */}
|
||||
<span
|
||||
className={`
|
||||
text-xs font-bold uppercase tracking-widest
|
||||
text-(--color-text-muted) whitespace-nowrap
|
||||
${isOpen ? "block" : "hidden"} xl:block
|
||||
`}
|
||||
className={`text-xs font-bold tracking-widest whitespace-nowrap text-(--color-text-muted) uppercase ${isOpen ? "block" : "hidden"} xl:block`}
|
||||
>
|
||||
<i className="fa-solid fa-utensils mr-2 text-(--color-primary)"></i>
|
||||
Thực Đơn
|
||||
@@ -70,10 +55,7 @@ export default function Navbar({
|
||||
<button
|
||||
onClick={onToggle}
|
||||
title={isOpen ? "Thu gọn menu" : "Mở rộng menu"}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-lg cursor-pointer border-none
|
||||
text-(--color-text-muted) bg-transparent
|
||||
hover:bg-(--color-border-light) hover:text-(--color-primary)
|
||||
transition-colors duration-150 shrink-0 xl:hidden"
|
||||
className="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--color-text-muted) transition-colors duration-150 hover:bg-(--color-border-light) hover:text-(--color-primary) xl:hidden"
|
||||
>
|
||||
<i
|
||||
className={`fa-solid text-sm transition-transform duration-250 ${
|
||||
@@ -93,36 +75,23 @@ export default function Navbar({
|
||||
<button
|
||||
onClick={() => onCategoryChange?.(cat.id)}
|
||||
title={!isOpen ? cat.name : undefined}
|
||||
className={`
|
||||
w-full flex items-center rounded-xl text-sm font-medium
|
||||
cursor-pointer border-none transition-all duration-150
|
||||
xl:gap-3 xl:px-3 xl:py-2.5 xl:justify-start
|
||||
${isOpen ? "gap-3 px-3 py-2.5" : "justify-center px-0 py-2.5"}
|
||||
${
|
||||
isActive
|
||||
? "bg-(--color-primary) text-white shadow-sm"
|
||||
: "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light) hover:text-(--color-primary-dark)"
|
||||
}
|
||||
`}
|
||||
className={`flex w-full cursor-pointer items-center rounded-xl border-none text-sm font-medium transition-all duration-150 xl:justify-start xl:gap-3 xl:px-3 xl:py-2.5 ${isOpen ? "gap-3 px-3 py-2.5" : "justify-center px-0 py-2.5"} ${
|
||||
isActive
|
||||
? "bg-(--color-primary) text-white shadow-sm"
|
||||
: "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light) hover:text-(--color-primary-dark)"
|
||||
} `}
|
||||
>
|
||||
{/* Icon */}
|
||||
<i
|
||||
className={`
|
||||
${cat.icon} w-5 text-center text-base shrink-0
|
||||
${isActive ? "text-white" : "text-(--color-primary)"}
|
||||
`}
|
||||
className={` ${cat.icon} w-5 shrink-0 text-center text-base ${isActive ? "text-white" : "text-(--color-primary)"} `}
|
||||
></i>
|
||||
|
||||
{/* Label — hidden when collapsed, always shown on xl+ */}
|
||||
<span
|
||||
className={`
|
||||
whitespace-nowrap overflow-hidden text-ellipsis
|
||||
${isOpen ? "block" : "hidden"} xl:block
|
||||
`}
|
||||
className={`overflow-hidden text-ellipsis whitespace-nowrap ${isOpen ? "block" : "hidden"} xl:block`}
|
||||
>
|
||||
{cat.name}
|
||||
</span>
|
||||
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
@@ -132,29 +101,20 @@ export default function Navbar({
|
||||
|
||||
{/* ── Sidebar footer: opening hours ── */}
|
||||
<div
|
||||
className={`
|
||||
shrink-0 border-t border-(--color-border) py-3
|
||||
xl:px-4
|
||||
${isOpen ? "px-4" : "px-0 flex justify-center"}
|
||||
`}
|
||||
className={`shrink-0 border-t border-(--color-border) py-3 xl:px-4 ${isOpen ? "px-4" : "flex justify-center px-0"} `}
|
||||
>
|
||||
{/* Text row — shown when expanded, always shown on xl+ */}
|
||||
<div
|
||||
className={`
|
||||
items-center gap-2 text-xs text-(--color-text-muted)
|
||||
${isOpen ? "flex" : "hidden"} xl:flex
|
||||
`}
|
||||
className={`items-center gap-2 text-xs text-(--color-text-muted) ${isOpen ? "flex" : "hidden"} xl:flex`}
|
||||
>
|
||||
<i className="fa-solid fa-clock text-(--color-accent) shrink-0"></i>
|
||||
<i className="fa-solid fa-clock shrink-0 text-(--color-accent)"></i>
|
||||
<span>{SHOP_INFO.openHours}</span>
|
||||
</div>
|
||||
|
||||
{/* Icon-only — shown when collapsed, hidden on xl+ */}
|
||||
<span className="xl:hidden">
|
||||
<i
|
||||
className={`
|
||||
fa-solid fa-clock text-sm text-(--color-text-muted)
|
||||
${isOpen ? "hidden" : "block"}`}
|
||||
className={`fa-solid fa-clock text-sm text-(--color-text-muted) ${isOpen ? "hidden" : "block"}`}
|
||||
title={SHOP_INFO.openHours}
|
||||
></i>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user