Created Page User
@@ -1,36 +1,107 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Hệ Thống Bán Đồ Ăn - Frontend
|
||||
|
||||
## Getting Started
|
||||
Dự án Frontend cho một Hệ thống bán đồ ăn (Food Ordering System), được xây dựng bằng Next.js và React, với giao diện hiện đại sử dụng Tailwind CSS.
|
||||
|
||||
First, run the development server:
|
||||
---
|
||||
|
||||
## Mô Tả Dự Án
|
||||
|
||||
Đây là giao diện người dùng (frontend) cho hệ thống đặt và bán đồ ăn trực tuyến. Hệ thống gồm 2 trang chính:
|
||||
|
||||
### Trang Người Dùng (User Page)
|
||||
Dành cho khách hàng với các chức năng:
|
||||
- Chọn món ăn: Duyệt và thêm món vào giỏ hàng
|
||||
- Xem hóa đơn: Xem chi tiết các món đã chọn và tổng tiền
|
||||
- Thanh toán: Thực hiện thanh toán đơn hàng
|
||||
- Đánh giá: Đánh giá chất lượng món ăn và dịch vụ
|
||||
|
||||
### Trang Quản Lý (Manager Page)
|
||||
Dành cho chủ quán / nhân viên quản lý với các chức năng:
|
||||
- Quản lý thực đơn (thêm, sửa, xóa món ăn)
|
||||
- Theo dõi và xử lý đơn hàng
|
||||
- Quản lý thông tin quán
|
||||
|
||||
---
|
||||
|
||||
## Cách Chạy Dự Án
|
||||
|
||||
### Yêu cầu hệ thống
|
||||
- Node.js >= 18
|
||||
- pnpm (khuyến nghị) hoặc npm / yarn / bun
|
||||
|
||||
### Cài đặt dependencies
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
pnpm install
|
||||
# hoặc
|
||||
npm install
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
### Chạy môi trường phát triển
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
```bash
|
||||
pnpm dev
|
||||
# hoặc
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
Mở trình duyệt tại http://localhost:3000 để xem kết quả.
|
||||
|
||||
## Learn More
|
||||
### Build production
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
---
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
## Cấu Trúc Thư Mục
|
||||
|
||||
## Deploy on Vercel
|
||||
Dự án sử dụng cấu trúc mặc định của Next.js App Router:
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
```
|
||||
frondend/
|
||||
├── app/ # App Router (Next.js)
|
||||
│ ├── layout.tsx # Root layout (font, metadata, global styles)
|
||||
│ ├── page.tsx # Trang chủ (User Page)
|
||||
│ ├── globals.css # CSS toàn cục (Tailwind import)
|
||||
│ ├── favicon.ico # Icon trang web
|
||||
│ └── components/ # Components dùng riêng trong app/
|
||||
├── components/ # Shared components dùng chung toàn dự án
|
||||
│ └── CartProduct.tsx # Component card sản phẩm trong giỏ hàng
|
||||
├── public/ # Static assets (ảnh, svg, ...)
|
||||
├── next.config.ts # Cấu hình Next.js
|
||||
├── tsconfig.json # Cấu hình TypeScript
|
||||
├── postcss.config.mjs # Cấu hình PostCSS (Tailwind)
|
||||
├── eslint.config.mjs # Cấu hình ESLint
|
||||
├── package.json # Dependencies và scripts
|
||||
└── pnpm-workspace.yaml # Cấu hình pnpm workspace
|
||||
```
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
---
|
||||
|
||||
## 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 | Utility-first CSS framework |
|
||||
| SCSS | - | CSS preprocessor |
|
||||
| Geist Font | - | Font chữ mặc định (Google Fonts) |
|
||||
| pnpm | - | Package manager |
|
||||
| ESLint | ^9 | Linting code |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Ghi Chú Phát Triển
|
||||
|
||||
- Trang chủ () là điểm vào chính của **User Page**
|
||||
- Chỉnh sửa để thay đổi giao diện trang chủ; trang tự động cập nhật khi lưu file
|
||||
- Styles toàn cục được định nghĩa trong
|
||||
- Tailwind CSS v4 được import trực tiếp qua trong CSS
|
||||
- Hỗ trợ **Dark Mode** tự động theo cài đặt hệ thống ()
|
||||
- Font chữ sử dụng **Geist Sans** và **Geist Mono** từ Google Fonts thông qua
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,26 +1,113 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ============================================================
|
||||
LIGHT THEME — Coffee Brown & Beige Palette
|
||||
All color/spacing/radius tokens live here so switching to
|
||||
dark theme later only requires overriding these variables
|
||||
in the [data-theme="dark"] block below.
|
||||
============================================================ */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
/* --- Brand Colors --- */
|
||||
--color-primary: #6F4E37; /* coffee brown */
|
||||
--color-primary-light: #A0785A; /* lighter brown */
|
||||
--color-primary-dark: #4A3728; /* dark espresso */
|
||||
--color-accent: #C8973A; /* golden caramel */
|
||||
--color-accent-light: #F0D9A8; /* pale caramel */
|
||||
|
||||
/* --- Background Colors --- */
|
||||
--color-bg-main: #FDF6EC; /* warm beige */
|
||||
--color-bg-card: #FFFFFF;
|
||||
--color-bg-sidebar: #FFFFFF;
|
||||
--color-bg-header: #FFFFFF;
|
||||
--color-bg-footer: #3D2B1F; /* dark espresso */
|
||||
|
||||
/* --- Text Colors --- */
|
||||
--color-text-primary: #2C1A0E; /* very dark brown */
|
||||
--color-text-secondary: #6F4E37; /* coffee brown */
|
||||
--color-text-muted: #A08060; /* warm grey-brown */
|
||||
--color-text-on-dark: #FDF6EC; /* beige on dark bg */
|
||||
--color-text-on-primary: #FFFFFF; /* white on primary */
|
||||
|
||||
/* --- Border & Divider --- */
|
||||
--color-border: #E2C9A8; /* warm tan */
|
||||
--color-border-light: #F0E4D0; /* very light tan */
|
||||
|
||||
/* --- Shadow --- */
|
||||
--color-shadow-sm: rgba(111, 78, 55, 0.08);
|
||||
--color-shadow-md: rgba(111, 78, 55, 0.18);
|
||||
|
||||
/* --- Layout Dimensions --- */
|
||||
--spacing-header-height: 72px;
|
||||
--spacing-sidebar-width: 240px; /* expanded */
|
||||
--spacing-sidebar-collapsed: 64px; /* icon-only */
|
||||
--spacing-content-max: 1536px;
|
||||
|
||||
/* --- Border Radius --- */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* --- Transitions --- */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 250ms ease;
|
||||
--transition-slow: 400ms ease;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
DARK THEME PLACEHOLDER
|
||||
Uncomment & fill values when dark mode is needed.
|
||||
============================================================ */
|
||||
/*
|
||||
[data-theme="dark"] {
|
||||
--color-primary: #A0785A;
|
||||
--color-primary-light: #C8956C;
|
||||
--color-primary-dark: #6F4E37;
|
||||
--color-accent: #D4A96A;
|
||||
--color-accent-light: #7A5C30;
|
||||
|
||||
--color-bg-main: #1A0F08;
|
||||
--color-bg-card: #2D1B0E;
|
||||
--color-bg-sidebar: #231408;
|
||||
--color-bg-header: #1A0F08;
|
||||
--color-bg-footer: #0D0704;
|
||||
|
||||
--color-text-primary: #FDF6EC;
|
||||
--color-text-secondary: #C8956C;
|
||||
--color-text-muted: #8B7060;
|
||||
--color-text-on-dark: #FDF6EC;
|
||||
|
||||
--color-border: #4A2E1A;
|
||||
--color-border-light: #3D2314;
|
||||
}
|
||||
*/
|
||||
|
||||
/* ============================================================
|
||||
TAILWIND THEME BRIDGE
|
||||
============================================================ */
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--color-bg-main);
|
||||
--color-foreground: var(--color-text-primary);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
BASE STYLES
|
||||
============================================================ */
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
background-color: var(--color-bg-main);
|
||||
color: var(--color-text-primary);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
CUSTOM SCROLLBAR
|
||||
============================================================ */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: var(--color-bg-main); }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--color-primary); }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "@/layouts/header";
|
||||
import Footer from "@/layouts/footer";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -13,8 +15,13 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Coffee Shop — Hệ thống đặt món",
|
||||
description: "Đặt món cà phê, trà, nước ép và nhiều hơn nữa tại Coffee Shop.",
|
||||
icons: {
|
||||
icon: "/favicon/favicon.ico",
|
||||
shortcut: "/favicon/favicon.ico",
|
||||
apple: "/favicon/apple-touch-icon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,11 +30,29 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="vi">
|
||||
<head>
|
||||
{/* FontAwesome 6 — icons used throughout the app */}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||
crossOrigin="anonymous"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
||||
>
|
||||
{children}
|
||||
{/* Sticky top header */}
|
||||
<Header />
|
||||
|
||||
{/* Page content (grows to fill remaining height) */}
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer always at bottom */}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,64 +1,165 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import CartProduct from "@/components/CartProduct";
|
||||
import { MENU_CATEGORIES, MOCK_PRODUCTS } from "@/lib/constants";
|
||||
|
||||
/**
|
||||
* Main page — sidebar + product grid layout.
|
||||
*
|
||||
* Layout:
|
||||
* [Sidebar (sticky, collapsible)] | [Main content (scrollable)]
|
||||
*
|
||||
* Sidebar state:
|
||||
* - Desktop (≥ 1024px): expanded by default
|
||||
* - Mobile (< 1024px): collapsed by default
|
||||
*
|
||||
* Product grid columns (responsive, depends on sidebar state):
|
||||
* Collapsed sidebar: 2 → sm:2 → lg:3 → xl:4 → 2xl:5
|
||||
* Expanded sidebar: 1 → sm:2 → lg:2 → xl:3 → 2xl:4
|
||||
*/
|
||||
export default function Home() {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [activeCategory, setActiveCategory] = useState("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
/* Default: expanded on desktop, collapsed on mobile */
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(min-width: 1024px)");
|
||||
setIsSidebarOpen(mq.matches);
|
||||
const handler = (e: MediaQueryListEvent) => setIsSidebarOpen(e.matches);
|
||||
mq.addEventListener("change", handler);
|
||||
return () => mq.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
/* Filter products by active category + search query */
|
||||
const filteredProducts = MOCK_PRODUCTS.filter((p) => {
|
||||
const matchesCategory = activeCategory === "all" || p.category === activeCategory;
|
||||
const matchesSearch =
|
||||
searchQuery.trim() === "" ||
|
||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
|
||||
/* Active category label */
|
||||
const activeCategoryLabel =
|
||||
MENU_CATEGORIES.find((c) => c.id === activeCategory)?.name ?? "Tất cả";
|
||||
|
||||
/* Responsive grid class based on sidebar state
|
||||
* Base (< 480px) : 1 col — very small phones
|
||||
* min-[480px] : 2 cols — larger phones
|
||||
* lg+ : 2/3 cols depending on sidebar
|
||||
*/
|
||||
const gridCols = isSidebarOpen
|
||||
? "grid-cols-1 min-[480px]:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||
: "grid-cols-1 min-[480px]:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5";
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
|
||||
<div
|
||||
className="flex items-start bg-[var(--color-bg-main)]"
|
||||
style={{ minHeight: "calc(100vh - var(--spacing-header-height))" }}
|
||||
>
|
||||
{/* ── Sidebar ── */}
|
||||
<Navbar
|
||||
isOpen={isSidebarOpen}
|
||||
onToggle={() => setIsSidebarOpen((prev) => !prev)}
|
||||
activeCategory={activeCategory}
|
||||
onCategoryChange={(id) => {
|
||||
setActiveCategory(id);
|
||||
setSearchQuery(""); // clear search when switching category
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ── Main content ── */}
|
||||
<main className="flex-1 min-w-0 px-4 py-6 md:px-6 lg:px-8">
|
||||
|
||||
{/* ── Section heading + search bar ── */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||||
{/* Title + count */}
|
||||
<div className="shrink-0">
|
||||
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{activeCategoryLabel}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
|
||||
{filteredProducts.length} món
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="relative w-full sm:max-w-xs">
|
||||
<i
|
||||
className="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2
|
||||
text-sm text-[var(--color-text-muted)] pointer-events-none"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Tìm kiếm món..."
|
||||
className="w-full pl-9 pr-9 py-2 text-sm rounded-xl border outline-none
|
||||
bg-[var(--color-bg-card)] text-[var(--color-text-primary)]
|
||||
border-[var(--color-border)] placeholder:text-[var(--color-text-muted)]
|
||||
focus:border-[var(--color-primary)] focus:ring-2
|
||||
focus:ring-[var(--color-primary)] focus:ring-opacity-20
|
||||
transition-all duration-150"
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
{/* Clear button */}
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
title="Xóa tìm kiếm"
|
||||
aria-label="Xóa tìm kiếm"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]
|
||||
hover:text-[var(--color-primary)] transition-colors duration-150
|
||||
cursor-pointer border-none bg-transparent p-0"
|
||||
>
|
||||
<i className="fa-solid fa-xmark text-sm"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Product grid ── */}
|
||||
{filteredProducts.length > 0 ? (
|
||||
<div className={`grid gap-4 ${gridCols}`}>
|
||||
{filteredProducts.map((product) => (
|
||||
<CartProduct
|
||||
key={product.id}
|
||||
image={product.image}
|
||||
imageAlt={product.name}
|
||||
productName={product.name}
|
||||
price={product.price}
|
||||
description={product.description}
|
||||
onBuy={() => {
|
||||
/* TODO: add to cart logic */
|
||||
console.log("Thêm vào giỏ:", product.name);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Empty state */
|
||||
<div className="flex flex-col items-center justify-center py-24 gap-4 text-[var(--color-text-muted)]">
|
||||
<i className="fa-solid fa-mug-hot text-5xl opacity-30"></i>
|
||||
<p className="text-base font-medium">
|
||||
{searchQuery
|
||||
? `Không tìm thấy món nào cho "${searchQuery}"`
|
||||
: "Chưa có món trong danh mục này"}
|
||||
</p>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="text-sm text-[var(--color-primary)] hover:underline cursor-pointer
|
||||
border-none bg-transparent"
|
||||
>
|
||||
Xóa tìm kiếm
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Components Documentation
|
||||
|
||||
> **BlackBox AI Rule:**
|
||||
> Whenever you create a new component in the components/ directory, or when the user asks to save a component to this file, **automatically append a new section** following the template below. Fill in all fields based on the component source code — props/interface, default values, types, usage example, and notes. Do not skip any field; if a value is unknown, write N/A.
|
||||
|
||||
---
|
||||
|
||||
## Component Template (for BlackBox AI)
|
||||
|
||||
Use this template when adding a new component entry:
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## CartProduct
|
||||
|
||||
**File:** components/CartProduct.tsx
|
||||
**Description:** A product card component that displays a product image, formatted price, a Mua (Buy) button, and a short description. Designed for use in shopping cart or product listing UIs.
|
||||
|
||||
### Props / Interface
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| image | string | yes | — | URL or path to the product image (used with Next.js Image). |
|
||||
| imageAlt | string | no | Product image | Alt text for the product image (accessibility). |
|
||||
| price | number or string | yes | — | Product price. If number, auto-formatted to Vietnamese Dong (VND). If string, rendered as-is. |
|
||||
| description | string | yes | — | Short product description shown in the card footer. Clamped to 2 lines. |
|
||||
| onBuy | () => void | no | undefined | Callback fired when the Mua button is clicked. |
|
||||
|
||||
### Internal Logic
|
||||
|
||||
- formattedPrice: If price is a number, it is formatted using toLocaleString(vi-VN, { style: currency, currency: VND }). If price is already a string, it is rendered directly.
|
||||
|
||||
### CSS / Styling Notes
|
||||
|
||||
| Element | Tailwind Classes | Purpose |
|
||||
|---------|-----------------|---------|
|
||||
| Card wrapper | flex flex-col w-64 rounded-2xl overflow-hidden shadow-md border border-gray-100 bg-white hover:shadow-lg transition-shadow duration-300 | Outer card with hover shadow |
|
||||
| Image container | relative w-full h-48 bg-gray-100 | Fixed-height image area |
|
||||
| Price text | text-lg font-bold text-red-500 | Highlighted price in red |
|
||||
| Buy button | bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors duration-200 cursor-pointer | Primary action button |
|
||||
| Description area | px-4 py-3 border-t border-gray-100 bg-gray-50 | Footer section with light background |
|
||||
| Description text | text-sm text-gray-600 leading-relaxed line-clamp-2 | Truncated 2-line description |
|
||||
|
||||
### Dependencies
|
||||
|
||||
- next/image — Next.js optimized Image component
|
||||
- Tailwind CSS — utility-first CSS framework
|
||||
|
||||
### Usage Example
|
||||
|
||||
|
||||
|
||||
### Notes
|
||||
|
||||
- Card width is fixed at w-64 (256px). Adjust if a responsive layout is needed.
|
||||
- The description is clamped to 2 lines via line-clamp-2; longer text will be truncated with ellipsis.
|
||||
- If onBuy is not provided, the button renders but does nothing on click.
|
||||
- Price formatting uses Vietnamese locale (vi-VN) and VND currency by default when price is a number.
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
interface CartProductProps {
|
||||
image: string;
|
||||
imageAlt?: string;
|
||||
productName: string;
|
||||
price: number | string;
|
||||
description: string;
|
||||
onBuy?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Product card — fills the parent grid cell width (w-full).
|
||||
*
|
||||
* Layout (top → bottom):
|
||||
* 1. Image area (fixed height h-36) with coffee-mug fallback icon
|
||||
* 2. Name + description (flex-1, grows to fill space)
|
||||
* 3. Price + Buy button row (pinned to bottom)
|
||||
*
|
||||
* Responsive: card width is controlled by the parent grid, not the card itself.
|
||||
*/
|
||||
export default function CartProduct({
|
||||
image,
|
||||
imageAlt = "Ảnh sản phẩm",
|
||||
productName,
|
||||
price,
|
||||
description,
|
||||
onBuy,
|
||||
}: CartProductProps) {
|
||||
const formattedPrice =
|
||||
typeof price === "number"
|
||||
? price.toLocaleString("vi-VN", { style: "currency", currency: "VND" })
|
||||
: price;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full rounded-2xl overflow-hidden
|
||||
bg-[var(--color-bg-card)] border border-[var(--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-[250ms] cursor-default"
|
||||
>
|
||||
{/* ── Image area ── */}
|
||||
<div className="relative w-full h-36 bg-[var(--color-border-light)] shrink-0 overflow-hidden">
|
||||
{/* Fallback icon (shown when image fails or is missing) */}
|
||||
<div className="absolute inset-0 flex items-center justify-center text-4xl text-[var(--color-border)] z-0">
|
||||
<i className="fa-solid fa-mug-hot"></i>
|
||||
</div>
|
||||
{/* Product image */}
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
className="object-cover z-[1]"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
</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-[var(--color-text-primary)] line-clamp-1">
|
||||
{productName}
|
||||
</h3>
|
||||
<p className="text-xs leading-relaxed text-[var(--color-text-muted)] line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Price + Buy button ── */}
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2.5
|
||||
border-t border-[var(--color-border-light)] shrink-0"
|
||||
>
|
||||
<span className="text-sm font-bold text-[var(--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-[var(--color-primary)] text-white
|
||||
hover:bg-[var(--color-primary-dark)]
|
||||
active:scale-95
|
||||
transition-all duration-150 whitespace-nowrap"
|
||||
>
|
||||
<i className="fa-solid fa-cart-plus"></i>
|
||||
Mua
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { MENU_CATEGORIES, SHOP_INFO } from "@/lib/constants";
|
||||
import type { MenuCategory } from "@/lib/types";
|
||||
|
||||
interface NavbarProps {
|
||||
/** Whether the sidebar is expanded (true) or icon-only (false) */
|
||||
isOpen: boolean;
|
||||
/** Toggle expand / collapse */
|
||||
onToggle: () => void;
|
||||
/** Currently selected category id */
|
||||
activeCategory?: string;
|
||||
/** Fired when user clicks a category */
|
||||
onCategoryChange?: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Left sidebar — always visible, collapsible on all screen sizes.
|
||||
*
|
||||
* Collapsed → 64 px wide, icon only
|
||||
* Expanded → 240 px wide, icon + label
|
||||
*
|
||||
* Width transition is handled by Tailwind w-16 / w-60 + transition-all.
|
||||
* Parent controls open/close state via isOpen + onToggle props.
|
||||
*/
|
||||
export default function Navbar({
|
||||
isOpen,
|
||||
onToggle,
|
||||
activeCategory = "all",
|
||||
onCategoryChange,
|
||||
}: NavbarProps) {
|
||||
const handleClick = (id: string) => {
|
||||
onCategoryChange?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`
|
||||
sticky shrink-0 flex flex-col z-20
|
||||
border-r border-[var(--color-border)]
|
||||
bg-[var(--color-bg-sidebar)]
|
||||
overflow-y-auto overflow-x-hidden
|
||||
transition-all duration-[250ms] ease-in-out
|
||||
${isOpen ? "w-60" : "w-16"}
|
||||
`}
|
||||
style={{
|
||||
top: "var(--spacing-header-height)",
|
||||
height: "calc(100vh - var(--spacing-header-height))",
|
||||
}}
|
||||
>
|
||||
{/* ── Sidebar header: title + toggle button ── */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center border-b border-[var(--color-border)] shrink-0
|
||||
${isOpen ? "justify-between px-4 py-3" : "justify-center px-0 py-3"}
|
||||
`}
|
||||
>
|
||||
{isOpen && (
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-[var(--color-text-muted)] whitespace-nowrap">
|
||||
<i className="fa-solid fa-utensils mr-2 text-[var(--color-primary)]"></i>
|
||||
Thực Đơn
|
||||
</span>
|
||||
)}
|
||||
<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-[var(--color-text-muted)] bg-transparent
|
||||
hover:bg-[var(--color-border-light)] hover:text-[var(--color-primary)]
|
||||
transition-colors duration-150 shrink-0"
|
||||
>
|
||||
<i
|
||||
className={`fa-solid text-sm transition-transform duration-[250ms] ${
|
||||
isOpen ? "fa-chevron-left" : "fa-chevron-right"
|
||||
}`}
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Category list ── */}
|
||||
<nav className="flex-1 py-2">
|
||||
<ul className="flex flex-col gap-0.5 px-2">
|
||||
{MENU_CATEGORIES.map((cat: MenuCategory) => {
|
||||
const isActive = activeCategory === cat.id;
|
||||
return (
|
||||
<li key={cat.id}>
|
||||
<button
|
||||
onClick={() => handleClick(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
|
||||
${isOpen ? "gap-3 px-3 py-2.5" : "justify-center px-0 py-2.5"}
|
||||
${
|
||||
isActive
|
||||
? "bg-[var(--color-primary)] text-white shadow-sm"
|
||||
: "bg-transparent text-[var(--color-text-secondary)] hover:bg-[var(--color-border-light)] hover:text-[var(--color-primary-dark)]"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Icon */}
|
||||
<i
|
||||
className={`
|
||||
${cat.icon} w-5 text-center text-base shrink-0
|
||||
${isActive ? "text-white" : "text-[var(--color-primary)]"}
|
||||
`}
|
||||
></i>
|
||||
|
||||
{/* Label — hidden when collapsed */}
|
||||
{isOpen && (
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{cat.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Active indicator arrow */}
|
||||
{isOpen && isActive && (
|
||||
<i className="fa-solid fa-chevron-right ml-auto text-xs opacity-70 shrink-0"></i>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* ── Sidebar footer: opening hours ── */}
|
||||
<div
|
||||
className={`
|
||||
shrink-0 border-t border-[var(--color-border)] py-3
|
||||
${isOpen ? "px-4" : "px-0 flex justify-center"}
|
||||
`}
|
||||
>
|
||||
{isOpen ? (
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
|
||||
<i className="fa-solid fa-clock text-[var(--color-accent)] shrink-0"></i>
|
||||
<span>{SHOP_INFO.openHours}</span>
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className="fa-solid fa-clock text-sm text-[var(--color-text-muted)]"
|
||||
title={SHOP_INFO.openHours}
|
||||
></i>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SHOP_INFO, SOCIAL_LINKS } from "@/lib/constants";
|
||||
|
||||
/**
|
||||
* Site Footer — 3-section 12-column grid.
|
||||
*
|
||||
* Sections:
|
||||
* 1. Brand info (logo, name, tagline, address, phone, hours)
|
||||
* 2. Social links (Facebook, TikTok, Website)
|
||||
* 3. WiFi card (network name + password)
|
||||
*
|
||||
* Responsive:
|
||||
* - Mobile : all sections stack full-width (col-span-12)
|
||||
* - md : brand 6 cols | social 3 | wifi 3
|
||||
* - lg : brand 5 cols | social 3 | wifi 4
|
||||
*/
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="w-full bg-[var(--color-bg-footer)] text-[var(--color-text-on-dark)] overflow-x-hidden">
|
||||
{/* ── Main grid ── */}
|
||||
<div className="max-w-screen-2xl mx-auto px-4 md:px-6 lg:px-8 py-10">
|
||||
<div className="grid grid-cols-12 gap-x-8 gap-y-8">
|
||||
|
||||
{/* ── 1. Brand info ── */}
|
||||
<div className="col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-6">
|
||||
{/* Logo + name */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="relative w-10 h-10 shrink-0">
|
||||
<Image
|
||||
src={SHOP_INFO.logo}
|
||||
alt={`Logo ${SHOP_INFO.name}`}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="40px"
|
||||
/>
|
||||
</div>
|
||||
<span className="font-bold text-lg text-[var(--color-accent)]">
|
||||
{SHOP_INFO.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tagline */}
|
||||
<p className="text-sm opacity-75 leading-relaxed mb-4">
|
||||
{SHOP_INFO.tagline}
|
||||
</p>
|
||||
|
||||
{/* Contact details */}
|
||||
<ul className="flex flex-col gap-2 text-sm opacity-80">
|
||||
<li className="flex items-start gap-2">
|
||||
<i className="fa-solid fa-location-dot mt-0.5 w-4 text-center text-[var(--color-accent)] shrink-0"></i>
|
||||
<span>Địa chỉ: {SHOP_INFO.address}</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<i className="fa-solid fa-phone w-4 text-center text-[var(--color-accent)] shrink-0"></i>
|
||||
<a
|
||||
href={`tel:${SHOP_INFO.phone}`}
|
||||
className="hover:text-[var(--color-accent)] transition-colors duration-150"
|
||||
>
|
||||
Số điện thoại: {SHOP_INFO.phone}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<i className="fa-solid fa-envelope w-4 text-center text-[var(--color-accent)] shrink-0"></i>
|
||||
<a
|
||||
href={`mailto:${SHOP_INFO.email}`}
|
||||
className="hover:text-[var(--color-accent)] transition-colors duration-150"
|
||||
>
|
||||
Email: {SHOP_INFO.email}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<i className="fa-solid fa-clock w-4 text-center text-[var(--color-accent)] shrink-0"></i>
|
||||
<span>Open: {SHOP_INFO.openHours}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* ── Right column: Social + WiFi
|
||||
md : side-by-side (each half of the 6-col right block)
|
||||
lg : stacked (WiFi below Social, both full width of the 4-col block)
|
||||
── */}
|
||||
<div className="col-span-12 md:col-span-6 lg:col-span-4 xl:col-span-6 grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
|
||||
{/* ── 2. Social links ── */}
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-[var(--color-accent)]">
|
||||
Kết nối
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3">
|
||||
<li>
|
||||
<a
|
||||
href={SOCIAL_LINKS.facebook}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
|
||||
hover:text-[var(--color-accent)] transition-all duration-150"
|
||||
>
|
||||
<span
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
|
||||
bg-[#1877F2] text-white text-base"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</span>
|
||||
Facebook
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={SOCIAL_LINKS.tiktok}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
|
||||
hover:text-[var(--color-accent)] transition-all duration-150"
|
||||
>
|
||||
<span
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
|
||||
bg-black text-white text-base"
|
||||
>
|
||||
<i className="fa-brands fa-tiktok"></i>
|
||||
</span>
|
||||
TikTok
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={SOCIAL_LINKS.website}
|
||||
className="flex items-center gap-3 text-sm opacity-75 hover:opacity-100
|
||||
hover:text-[var(--color-accent)] transition-all duration-150"
|
||||
>
|
||||
<span
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0
|
||||
bg-[var(--color-primary)] text-white text-base"
|
||||
>
|
||||
<i className="fa-solid fa-globe"></i>
|
||||
</span>
|
||||
Website
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* ── 3. WiFi card ── */}
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-bold text-sm uppercase tracking-wider mb-4 text-[var(--color-accent)]">
|
||||
WiFi Miễn Phí
|
||||
</h3>
|
||||
<div
|
||||
className="rounded-xl border border-[var(--color-primary-light)] border-opacity-50
|
||||
p-4 bg-[var(--color-primary-dark)] bg-opacity-30"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<i className="fa-solid fa-wifi text-[var(--color-accent)] text-lg shrink-0"></i>
|
||||
<span className="font-semibold text-sm">Kết nối miễn phí</span>
|
||||
</div>
|
||||
{/* Stacked label + value rows — no overflow risk */}
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="opacity-60 text-xs uppercase tracking-wide">Tên mạng</span>
|
||||
<span
|
||||
className="font-mono font-bold text-[var(--color-accent)]
|
||||
px-2 py-1 rounded border border-[var(--color-accent)]
|
||||
border-opacity-30 break-all"
|
||||
>
|
||||
{SHOP_INFO.wifi.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="opacity-60 text-xs uppercase tracking-wide">Mật khẩu</span>
|
||||
<span
|
||||
className="font-mono font-bold text-[var(--color-accent)] tracking-wider
|
||||
px-2 py-1 rounded border border-[var(--color-accent)]
|
||||
border-opacity-30 break-all"
|
||||
>
|
||||
{SHOP_INFO.wifi.password}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Bottom bar ── */}
|
||||
<div className="border-t border-white border-opacity-10">
|
||||
<div
|
||||
className="max-w-screen-2xl mx-auto px-4 md:px-6 lg:px-8 py-4
|
||||
flex flex-col sm:flex-row items-center justify-between gap-2
|
||||
text-xs opacity-50"
|
||||
>
|
||||
<span>
|
||||
© {new Date().getFullYear()} {SHOP_INFO.name}. All rights reserved.
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
Made with{" "}
|
||||
<i className="fa-solid fa-heart text-[var(--color-accent)] mx-1"></i>{" "}
|
||||
in Vietnam
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SHOP_INFO, MOCK_USERS } from "@/lib/constants";
|
||||
import type { User } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* Site Header — sticky top bar, always visible on all screen sizes.
|
||||
*
|
||||
* 3-column layout:
|
||||
* [Brand: logo + name + tagline] | [Center: quick info] | [Auth button]
|
||||
*
|
||||
* Login state cycles for UI demo (click the button to switch):
|
||||
* Guest → Manager (Quản lý) → Staff → Guest
|
||||
*
|
||||
* Responsive:
|
||||
* - Logo + shop name : always visible
|
||||
* - Tagline : hidden on mobile, shown on md+
|
||||
* - Center info : hidden on mobile, shown on lg+
|
||||
* - Button label : hidden on xs, shown on sm+
|
||||
*/
|
||||
export default function Header() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
|
||||
const cycleUser = () => {
|
||||
if (!user) setUser(MOCK_USERS.manager);
|
||||
else if (user.role === "manager") setUser(MOCK_USERS.staff);
|
||||
else setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
className="w-full sticky top-0 z-50
|
||||
bg-[var(--color-bg-header)] border-b border-[var(--color-border)]
|
||||
shadow-[0_1px_8px_var(--color-shadow-sm)]"
|
||||
style={{ height: "var(--spacing-header-height)" }}
|
||||
>
|
||||
<div
|
||||
className="h-full px-6 md:px-8 lg:px-12
|
||||
flex items-center justify-between gap-6
|
||||
max-w-screen-2xl mx-auto"
|
||||
>
|
||||
|
||||
{/* ── LEFT: Brand ── */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-4 shrink-0 no-underline group"
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="relative w-10 h-10 md:w-11 md:h-11 shrink-0">
|
||||
<Image
|
||||
src={SHOP_INFO.logo}
|
||||
alt={`Logo ${SHOP_INFO.name}`}
|
||||
fill
|
||||
className="object-contain transition-transform duration-200 group-hover:scale-105"
|
||||
sizes="44px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Name + tagline */}
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span
|
||||
className="font-bold text-base md:text-lg
|
||||
text-[var(--color-primary-dark)]
|
||||
group-hover:text-[var(--color-primary)]
|
||||
transition-colors duration-150"
|
||||
>
|
||||
{SHOP_INFO.name}
|
||||
</span>
|
||||
<span className="text-xs hidden md:block text-[var(--color-text-muted)]">
|
||||
{SHOP_INFO.tagline}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* ── RIGHT: Auth ── */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
|
||||
{!user ? (
|
||||
/* Guest: sign-in button */
|
||||
<button
|
||||
onClick={cycleUser}
|
||||
title="Đăng nhập"
|
||||
className="flex items-center gap-2.5 px-5 py-2.5 rounded-xl
|
||||
text-sm font-semibold border-none cursor-pointer
|
||||
bg-[var(--color-primary)] text-white
|
||||
hover:bg-[var(--color-primary-dark)]
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
<i className="fa-solid fa-right-to-bracket"></i>
|
||||
<span className="hidden sm:inline">Đăng nhập</span>
|
||||
</button>
|
||||
|
||||
) : user.role === "manager" ? (
|
||||
/* Manager: gold badge */
|
||||
<button
|
||||
onClick={cycleUser}
|
||||
title="Nhấn để đổi tài khoản (demo)"
|
||||
className="flex items-center gap-2.5 px-4 py-2.5 rounded-xl
|
||||
text-sm font-semibold cursor-pointer
|
||||
bg-[var(--color-accent-light)] border border-[var(--color-accent)]
|
||||
text-[var(--color-primary-dark)]
|
||||
hover:bg-[var(--color-accent)] hover:text-white
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
<i className="fa-solid fa-user-tie text-base"></i>
|
||||
<span className="hidden sm:inline">Quản lý</span>
|
||||
</button>
|
||||
|
||||
) : (
|
||||
/* Staff / regular user: avatar + name */
|
||||
<button
|
||||
onClick={cycleUser}
|
||||
title="Nhấn để đổi tài khoản (demo)"
|
||||
className="flex items-center gap-2.5 px-4 py-2 rounded-xl
|
||||
text-sm font-semibold cursor-pointer
|
||||
bg-[var(--color-bg-main)] border border-[var(--color-border)]
|
||||
text-[var(--color-text-secondary)]
|
||||
hover:border-[var(--color-primary-light)]
|
||||
hover:bg-[var(--color-border-light)]
|
||||
active:scale-95 transition-all duration-150"
|
||||
>
|
||||
{/* Avatar circle */}
|
||||
<div
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center shrink-0
|
||||
bg-[var(--color-primary-light)] text-white text-xs"
|
||||
>
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
<span className="hidden sm:inline">{user.name}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { MenuCategory, Product, ShopInfo, SocialLinks, NavLink, User } from "./types";
|
||||
|
||||
// ===== SHOP INFORMATION =====
|
||||
export const SHOP_INFO: ShopInfo = {
|
||||
name: "Coffee Shop",
|
||||
tagline: "Hương vị đậm đà – Khoảnh khắc thư giãn",
|
||||
logo: "/imgs/logo.png",
|
||||
address: "123 Đường Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
|
||||
phone: "0901 234 567",
|
||||
managerPhone: "0912 345 678",
|
||||
email: "contact@coffeeshop.vn",
|
||||
wifi: {
|
||||
name: "CoffeeShop_Free",
|
||||
password: "coffee2024",
|
||||
},
|
||||
openHours: "07:00 – 22:00 (Thứ 2 – Chủ nhật)",
|
||||
};
|
||||
|
||||
// ===== SOCIAL LINKS =====
|
||||
export const SOCIAL_LINKS: SocialLinks = {
|
||||
facebook: "https://facebook.com/coffeeshop",
|
||||
tiktok: "https://tiktok.com/@coffeeshop",
|
||||
website: "/",
|
||||
};
|
||||
|
||||
// ===== NAVIGATION LINKS =====
|
||||
export const NAV_LINKS: NavLink[] = [
|
||||
{ label: "Trang chủ", href: "/", icon: "fa-solid fa-house" },
|
||||
{ label: "Thực đơn", href: "/menu", icon: "fa-solid fa-book-open" },
|
||||
{ label: "Về chúng tôi", href: "/about", icon: "fa-solid fa-circle-info" },
|
||||
{ label: "Liên hệ", href: "/contact", icon: "fa-solid fa-envelope" },
|
||||
];
|
||||
|
||||
// ===== MENU CATEGORIES =====
|
||||
// Each category has a unique FontAwesome icon representing the item type
|
||||
export const MENU_CATEGORIES: MenuCategory[] = [
|
||||
{ id: "all", name: "Tất cả", icon: "fa-solid fa-border-all" },
|
||||
{ id: "cafe", name: "Cà Phê", icon: "fa-solid fa-mug-hot" },
|
||||
{ id: "tra", name: "Trà", icon: "fa-solid fa-leaf" },
|
||||
{ id: "sua-chua", name: "Sữa Chua", icon: "fa-solid fa-jar" },
|
||||
{ id: "nuoc-ep", name: "Nước Ép", icon: "fa-solid fa-blender" },
|
||||
{ id: "latte", name: "Latte", icon: "fa-solid fa-mug-saucer" },
|
||||
{ id: "giai-khat", name: "Giải Khát / Ăn Vặt", icon: "fa-solid fa-ice-cream" },
|
||||
{ id: "topping", name: "Topping", icon: "fa-solid fa-layer-group" },
|
||||
];
|
||||
|
||||
// ===== MOCK PRODUCTS =====
|
||||
// Placeholder data – replace with real API calls when backend is ready
|
||||
export const MOCK_PRODUCTS: Product[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Cà Phê Đen",
|
||||
category: "cafe",
|
||||
price: 25000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Cà phê đen truyền thống, đậm đà hương vị Việt Nam, pha phin thủ công.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Cà Phê Sữa",
|
||||
category: "cafe",
|
||||
price: 30000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Cà phê sữa đặc thơm ngon, béo ngậy, kết hợp hoàn hảo giữa cà phê và sữa đặc.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Bạc Xỉu",
|
||||
category: "cafe",
|
||||
price: 32000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Bạc xỉu nhẹ nhàng, ít cà phê nhiều sữa, thích hợp cho người mới uống cà phê.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Cà Phê Trứng",
|
||||
category: "cafe",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Cà phê trứng đặc sản Hà Nội, lớp kem trứng mịn màng phủ trên nền cà phê đậm đà.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Trà Đào Cam Sả",
|
||||
category: "tra",
|
||||
price: 35000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Trà đào thơm mát kết hợp cam tươi và sả, thanh mát và giải nhiệt tuyệt vời.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Trà Xanh Matcha",
|
||||
category: "tra",
|
||||
price: 40000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Matcha Nhật Bản nguyên chất, vị đắng nhẹ đặc trưng, thơm mát và bổ dưỡng.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Trà Vải Hoa Nhài",
|
||||
category: "tra",
|
||||
price: 38000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Trà vải thanh ngọt kết hợp hương hoa nhài dịu dàng, thư giãn tâm hồn.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Sữa Chua Trân Châu",
|
||||
category: "sua-chua",
|
||||
price: 38000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Sữa chua mịn màng kết hợp trân châu đen dẻo dai, chua ngọt hài hòa.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Sữa Chua Dâu",
|
||||
category: "sua-chua",
|
||||
price: 40000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Sữa chua mát lạnh với dâu tươi ngọt chua, giàu vitamin và khoáng chất.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Nước Ép Cam",
|
||||
category: "nuoc-ep",
|
||||
price: 35000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Nước ép cam tươi nguyên chất, giàu vitamin C, tốt cho sức khỏe.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Nước Ép Dưa Hấu",
|
||||
category: "nuoc-ep",
|
||||
price: 30000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Nước ép dưa hấu mát lạnh, giải nhiệt tức thì trong những ngày hè oi bức.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Latte Caramel",
|
||||
category: "latte",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Latte caramel ngọt ngào, thơm béo với lớp foam sữa mịn và sốt caramel.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "Latte Vanilla",
|
||||
category: "latte",
|
||||
price: 45000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Latte vanilla nhẹ nhàng, hương thơm dịu dàng từ vanilla tự nhiên.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "Bánh Mì Nướng Bơ",
|
||||
category: "giai-khat",
|
||||
price: 20000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Bánh mì nướng giòn rụm, phết bơ thơm và mứt dâu, ăn kèm cà phê tuyệt vời.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "Bánh Flan",
|
||||
category: "giai-khat",
|
||||
price: 25000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Bánh flan mềm mịn, ngọt ngào với lớp caramel vàng óng, tan chảy trong miệng.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Trân Châu Đen",
|
||||
category: "topping",
|
||||
price: 10000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Trân châu đen dẻo dai, thêm vào bất kỳ đồ uống nào để tăng thêm hương vị.",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: "Thạch Cà Phê",
|
||||
category: "topping",
|
||||
price: 10000,
|
||||
image: "/imgs/products/placeholder.jpg",
|
||||
description: "Thạch cà phê mát lạnh, thêm hương vị đặc biệt cho đồ uống của bạn.",
|
||||
available: true,
|
||||
},
|
||||
];
|
||||
|
||||
// ===== MOCK USERS (for UI demo – replace with real auth) =====
|
||||
export const MOCK_USERS: Record<string, User> = {
|
||||
manager: {
|
||||
id: 1,
|
||||
name: "Nguyễn Văn An",
|
||||
role: "manager",
|
||||
avatar: null,
|
||||
},
|
||||
staff: {
|
||||
id: 2,
|
||||
name: "Trần Thị Bình",
|
||||
role: "staff",
|
||||
avatar: null,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
// ===== USER TYPES =====
|
||||
export type UserRole = "manager" | "staff" | "customer";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
// ===== MENU TYPES =====
|
||||
export interface MenuCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string; // FontAwesome class e.g. "fa-solid fa-mug-hot"
|
||||
}
|
||||
|
||||
// ===== PRODUCT TYPES =====
|
||||
export interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string; // matches MenuCategory.id
|
||||
price: number;
|
||||
image: string;
|
||||
description: string;
|
||||
available?: boolean;
|
||||
}
|
||||
|
||||
// ===== SHOP INFO TYPES =====
|
||||
export interface WifiInfo {
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ShopInfo {
|
||||
name: string;
|
||||
tagline: string;
|
||||
logo: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
managerPhone: string;
|
||||
email: string;
|
||||
wifi: WifiInfo;
|
||||
openHours: string;
|
||||
}
|
||||
|
||||
// ===== NAVIGATION TYPES =====
|
||||
export interface NavLink {
|
||||
label: string;
|
||||
href: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface SocialLinks {
|
||||
facebook: string;
|
||||
tiktok: string;
|
||||
website: string;
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
"dependencies": {
|
||||
"next": "16.1.7",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |
@@ -28,7 +28,8 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
"**/*.mts",
|
||||
"types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/* Tell TypeScript that CSS files are valid side-effect imports.
|
||||
Next.js handles the actual bundling; this declaration silences
|
||||
the TS language-server warning for `import "./globals.css"`. */
|
||||
declare module "*.css" {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||