Created Page User
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+100
-13
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user