Created Page User

This commit is contained in:
Thanh Quy - wolf
2026-03-22 15:10:44 +07:00
parent a3ad537210
commit 9da99c6495
30 changed files with 9387 additions and 102 deletions
+92 -21
View File
@@ -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****Geist Mono** từ Google Fonts thông qua
+1
View File
@@ -0,0 +1 @@
# Food Ordering Page - Implementation TODO
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+100 -13
View File
@@ -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); }
+30 -5
View File
@@ -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>
); );
+157 -56
View File
@@ -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>
); );
+60
View File
@@ -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.
+98
View File
@@ -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>
);
}
+149
View File
@@ -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>
);
}
+206
View File
@@ -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>
);
}
+141
View File
@@ -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 </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>
);
}
+219
View File
@@ -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,
},
};
+58
View File
@@ -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;
}
+8041
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

+21
View File
@@ -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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

-1
View File
@@ -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
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

-1
View File
@@ -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
View File
@@ -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
View File
@@ -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

+2 -1
View File
@@ -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"]
} }
+7
View File
@@ -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;
}