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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+100 -13
View File
@@ -1,26 +1,113 @@
@import "tailwindcss";
/* ============================================================
LIGHT THEME — Coffee Brown & Beige Palette
All color/spacing/radius tokens live here so switching to
dark theme later only requires overriding these variables
in the [data-theme="dark"] block below.
============================================================ */
:root {
--background: #ffffff;
--foreground: #171717;
/* --- Brand Colors --- */
--color-primary: #6F4E37; /* coffee brown */
--color-primary-light: #A0785A; /* lighter brown */
--color-primary-dark: #4A3728; /* dark espresso */
--color-accent: #C8973A; /* golden caramel */
--color-accent-light: #F0D9A8; /* pale caramel */
/* --- Background Colors --- */
--color-bg-main: #FDF6EC; /* warm beige */
--color-bg-card: #FFFFFF;
--color-bg-sidebar: #FFFFFF;
--color-bg-header: #FFFFFF;
--color-bg-footer: #3D2B1F; /* dark espresso */
/* --- Text Colors --- */
--color-text-primary: #2C1A0E; /* very dark brown */
--color-text-secondary: #6F4E37; /* coffee brown */
--color-text-muted: #A08060; /* warm grey-brown */
--color-text-on-dark: #FDF6EC; /* beige on dark bg */
--color-text-on-primary: #FFFFFF; /* white on primary */
/* --- Border & Divider --- */
--color-border: #E2C9A8; /* warm tan */
--color-border-light: #F0E4D0; /* very light tan */
/* --- Shadow --- */
--color-shadow-sm: rgba(111, 78, 55, 0.08);
--color-shadow-md: rgba(111, 78, 55, 0.18);
/* --- Layout Dimensions --- */
--spacing-header-height: 72px;
--spacing-sidebar-width: 240px; /* expanded */
--spacing-sidebar-collapsed: 64px; /* icon-only */
--spacing-content-max: 1536px;
/* --- Border Radius --- */
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-full: 9999px;
/* --- Transitions --- */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 400ms ease;
}
/* ============================================================
DARK THEME PLACEHOLDER
Uncomment & fill values when dark mode is needed.
============================================================ */
/*
[data-theme="dark"] {
--color-primary: #A0785A;
--color-primary-light: #C8956C;
--color-primary-dark: #6F4E37;
--color-accent: #D4A96A;
--color-accent-light: #7A5C30;
--color-bg-main: #1A0F08;
--color-bg-card: #2D1B0E;
--color-bg-sidebar: #231408;
--color-bg-header: #1A0F08;
--color-bg-footer: #0D0704;
--color-text-primary: #FDF6EC;
--color-text-secondary: #C8956C;
--color-text-muted: #8B7060;
--color-text-on-dark: #FDF6EC;
--color-border: #4A2E1A;
--color-border-light: #3D2314;
}
*/
/* ============================================================
TAILWIND THEME BRIDGE
============================================================ */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-background: var(--color-bg-main);
--color-foreground: var(--color-text-primary);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* ============================================================
BASE STYLES
============================================================ */
body {
background: var(--background);
color: var(--foreground);
background-color: var(--color-bg-main);
color: var(--color-text-primary);
font-family: Arial, Helvetica, sans-serif;
}
/* ============================================================
CUSTOM SCROLLBAR
============================================================ */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--color-bg-main); }
::-webkit-scrollbar-thumb {
background: var(--color-primary-light);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover { background: var(--color-primary); }
+30 -5
View File
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "@/layouts/header";
import Footer from "@/layouts/footer";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +15,13 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Coffee Shop — Hệ thống đặt món",
description: "Đặt món cà phê, trà, nước ép và nhiều hơn nữa tại Coffee Shop.",
icons: {
icon: "/favicon/favicon.ico",
shortcut: "/favicon/favicon.ico",
apple: "/favicon/apple-touch-icon.png",
},
};
export default function RootLayout({
@@ -23,11 +30,29 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="vi">
<head>
{/* FontAwesome 6 — icons used throughout the app */}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
>
{children}
{/* Sticky top header */}
<Header />
{/* Page content (grows to fill remaining height) */}
<div className="flex-1">
{children}
</div>
{/* Footer always at bottom */}
<Footer />
</body>
</html>
);
+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() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [activeCategory, setActiveCategory] = useState("all");
const [searchQuery, setSearchQuery] = useState("");
/* Default: expanded on desktop, collapsed on mobile */
useEffect(() => {
const mq = window.matchMedia("(min-width: 1024px)");
setIsSidebarOpen(mq.matches);
const handler = (e: MediaQueryListEvent) => setIsSidebarOpen(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
/* Filter products by active category + search query */
const filteredProducts = MOCK_PRODUCTS.filter((p) => {
const matchesCategory = activeCategory === "all" || p.category === activeCategory;
const matchesSearch =
searchQuery.trim() === "" ||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
/* Active category label */
const activeCategoryLabel =
MENU_CATEGORIES.find((c) => c.id === activeCategory)?.name ?? "Tất cả";
/* Responsive grid class based on sidebar state
* Base (< 480px) : 1 col — very small phones
* min-[480px] : 2 cols — larger phones
* lg+ : 2/3 cols depending on sidebar
*/
const gridCols = isSidebarOpen
? "grid-cols-1 min-[480px]:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
: "grid-cols-1 min-[480px]:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5";
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
<div
className="flex items-start bg-[var(--color-bg-main)]"
style={{ minHeight: "calc(100vh - var(--spacing-header-height))" }}
>
{/* ── Sidebar ── */}
<Navbar
isOpen={isSidebarOpen}
onToggle={() => setIsSidebarOpen((prev) => !prev)}
activeCategory={activeCategory}
onCategoryChange={(id) => {
setActiveCategory(id);
setSearchQuery(""); // clear search when switching category
}}
/>
{/* ── Main content ── */}
<main className="flex-1 min-w-0 px-4 py-6 md:px-6 lg:px-8">
{/* ── Section heading + search bar ── */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
{/* Title + count */}
<div className="shrink-0">
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">
{activeCategoryLabel}
</h2>
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">
{filteredProducts.length} món
</p>
</div>
{/* Search input */}
<div className="relative w-full sm:max-w-xs">
<i
className="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2
text-sm text-[var(--color-text-muted)] pointer-events-none"
></i>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Tìm kiếm món..."
className="w-full pl-9 pr-9 py-2 text-sm rounded-xl border outline-none
bg-[var(--color-bg-card)] text-[var(--color-text-primary)]
border-[var(--color-border)] placeholder:text-[var(--color-text-muted)]
focus:border-[var(--color-primary)] focus:ring-2
focus:ring-[var(--color-primary)] focus:ring-opacity-20
transition-all duration-150"
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
{/* Clear button */}
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
title="Xóa tìm kiếm"
aria-label="Xóa tìm kiếm"
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--color-text-muted)]
hover:text-[var(--color-primary)] transition-colors duration-150
cursor-pointer border-none bg-transparent p-0"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
)}
</div>
</div>
{/* ── Product grid ── */}
{filteredProducts.length > 0 ? (
<div className={`grid gap-4 ${gridCols}`}>
{filteredProducts.map((product) => (
<CartProduct
key={product.id}
image={product.image}
imageAlt={product.name}
productName={product.name}
price={product.price}
description={product.description}
onBuy={() => {
/* TODO: add to cart logic */
console.log("Thêm vào giỏ:", product.name);
}}
/>
))}
</div>
) : (
/* Empty state */
<div className="flex flex-col items-center justify-center py-24 gap-4 text-[var(--color-text-muted)]">
<i className="fa-solid fa-mug-hot text-5xl opacity-30"></i>
<p className="text-base font-medium">
{searchQuery
? `Không tìm thấy món nào cho "${searchQuery}"`
: "Chưa có món trong danh mục này"}
</p>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="text-sm text-[var(--color-primary)] hover:underline cursor-pointer
border-none bg-transparent"
>
Xóa tìm kiếm
</button>
)}
</div>
)}
</main>
</div>
);