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
|
```bash
|
||||||
npm run dev
|
pnpm install
|
||||||
# or
|
# hoặc
|
||||||
yarn dev
|
npm install
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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";
|
@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 {
|
:root {
|
||||||
--background: #ffffff;
|
/* --- Brand Colors --- */
|
||||||
--foreground: #171717;
|
--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 {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--color-bg-main);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--color-text-primary);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* ============================================================
|
||||||
:root {
|
BASE STYLES
|
||||||
--background: #0a0a0a;
|
============================================================ */
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background-color: var(--color-bg-main);
|
||||||
color: var(--foreground);
|
color: var(--color-text-primary);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
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 type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import Header from "@/layouts/header";
|
||||||
|
import Footer from "@/layouts/footer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -13,8 +15,13 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Coffee Shop — Hệ thống đặt món",
|
||||||
description: "Generated by create next app",
|
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({
|
export default function RootLayout({
|
||||||
@@ -23,11 +30,29 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
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
|
<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>
|
</body>
|
||||||
</html>
|
</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() {
|
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 (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
|
||||||
<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">
|
<div
|
||||||
<Image
|
className="flex items-start bg-[var(--color-bg-main)]"
|
||||||
className="dark:invert"
|
style={{ minHeight: "calc(100vh - var(--spacing-header-height))" }}
|
||||||
src="/next.svg"
|
>
|
||||||
alt="Next.js logo"
|
{/* ── Sidebar ── */}
|
||||||
width={100}
|
<Navbar
|
||||||
height={20}
|
isOpen={isSidebarOpen}
|
||||||
priority
|
onToggle={() => setIsSidebarOpen((prev) => !prev)}
|
||||||
/>
|
activeCategory={activeCategory}
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
onCategoryChange={(id) => {
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
setActiveCategory(id);
|
||||||
To get started, edit the page.tsx file.
|
setSearchQuery(""); // clear search when switching category
|
||||||
</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
|
{/* ── Main content ── */}
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<main className="flex-1 min-w-0 px-4 py-6 md:px-6 lg:px-8">
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
{/* ── Section heading + search bar ── */}
|
||||||
Templates
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||||||
</a>{" "}
|
{/* Title + count */}
|
||||||
or the{" "}
|
<div className="shrink-0">
|
||||||
<a
|
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
{activeCategoryLabel}
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
</h2>
|
||||||
>
|
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
|
||||||
Learning
|
{filteredProducts.length} món
|
||||||
</a>{" "}
|
</p>
|
||||||
center.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
{/* Search input */}
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
<div className="relative w-full sm:max-w-xs">
|
||||||
<a
|
<i
|
||||||
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]"
|
className="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
text-sm text-[var(--color-text-muted)] pointer-events-none"
|
||||||
target="_blank"
|
></i>
|
||||||
rel="noopener noreferrer"
|
<input
|
||||||
>
|
type="text"
|
||||||
<Image
|
value={searchQuery}
|
||||||
className="dark:invert"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
src="/vercel.svg"
|
placeholder="Tìm kiếm món..."
|
||||||
alt="Vercel logomark"
|
className="w-full pl-9 pr-9 py-2 text-sm rounded-xl border outline-none
|
||||||
width={16}
|
bg-[var(--color-bg-card)] text-[var(--color-text-primary)]
|
||||||
height={16}
|
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
|
{/* Clear button */}
|
||||||
</a>
|
{searchQuery && (
|
||||||
<a
|
<button
|
||||||
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]"
|
onClick={() => setSearchQuery("")}
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
title="Xóa tìm kiếm"
|
||||||
target="_blank"
|
aria-label="Xóa tìm kiếm"
|
||||||
rel="noopener noreferrer"
|
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
|
||||||
Documentation
|
cursor-pointer border-none bg-transparent p-0"
|
||||||
</a>
|
>
|
||||||
|
<i className="fa-solid fa-xmark text-sm"></i>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</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>
|
</main>
|
||||||
</div>
|
</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": {
|
"dependencies": {
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"tailwind": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@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",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts",
|
||||||
|
"types/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"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;
|
||||||
|
}
|
||||||