chore: release [ci skip]

This commit is contained in:
gitea-actions
2026-03-25 03:07:52 +00:00
parent 372391381f
commit 6a6afc4436
21 changed files with 851 additions and 828 deletions
+6 -7
View File
@@ -1,9 +1,10 @@
import CartFab from "@/components/CartFab";
import Footer from "@/layouts/footer";
import Header from "@/layouts/header";
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";
import CartFab from "@/components/CartFab";
import { Providers } from "./providers";
const geistSans = Geist({
@@ -43,16 +44,14 @@ export default function RootLayout({
/>
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
className={`${geistSans.variable} ${geistMono.variable} flex min-h-screen flex-col antialiased`}
>
<Providers>
{/* Sticky top header */}
<Header />
{/* Page content (grows to fill remaining height) */}
<div className="flex-1">
{children}
</div>
<div className="flex-1">{children}</div>
{/* Footer always at bottom */}
<Footer />
+66 -61
View File
@@ -1,19 +1,23 @@
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { useAuth } from "@/lib/auth-context";
import { SHOP_INFO } from "@/lib/constants";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
export default function LoginPage() {
const router = useRouter();
const { login } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState({ username: "", password: "", general: "" });
const [errors, setErrors] = useState({
username: "",
password: "",
general: "",
});
const [showPassword, setShowPassword] = useState(false);
const validate = (): boolean => {
@@ -39,28 +43,29 @@ export default function LoginPage() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
const success = login(username, password);
if (success) {
router.push("/");
} else {
setErrors({ username: "", password: "", general: "Tên đăng nhập hoặc mật khẩu không đúng" });
setErrors({
username: "",
password: "",
general: "Tên đăng nhập hoặc mật khẩu không đúng",
});
}
};
return (
<div
className="min-h-screen flex items-center justify-center px-4 py-8 bg-background"
>
<div className="bg-background flex min-h-screen items-center justify-center px-4 py-8">
{/* Login Form Card */}
<div className="w-full max-w-md bg-white rounded-2xl shadow-lg p-8">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-lg">
{/* Logo & Shop Name */}
<div className="flex flex-col items-center mb-8">
<div className="relative w-20 h-20 mb-4">
<div className="mb-8 flex flex-col items-center">
<div className="relative mb-4 h-20 w-20">
<Image
src={SHOP_INFO.logo}
alt={SHOP_INFO.name}
@@ -70,7 +75,7 @@ export default function LoginPage() {
priority
/>
</div>
<h1 className="text-2xl font-bold text-(--color-primary-dark) mb-1">
<h1 className="mb-1 text-2xl font-bold text-(--color-primary-dark)">
{SHOP_INFO.name}
</h1>
<p className="text-sm text-(--color-text-muted)">
@@ -80,7 +85,7 @@ export default function LoginPage() {
{/* Error Message */}
{errors.general && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-600 flex items-center gap-2">
<div className="mb-4 flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
<i className="fa-solid fa-circle-exclamation"></i>
<span>{errors.general}</span>
</div>
@@ -88,14 +93,16 @@ export default function LoginPage() {
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Username Input */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-(--color-text-secondary) mb-2">
<label
htmlFor="username"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
Tên đăng nhập
</label>
<div className="relative">
<i className="fa-solid fa-user absolute left-4 top-1/2 -translate-y-1/2 text-(--color-text-muted) hidden lg:block"></i>
<i className="fa-solid fa-user absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
<input
id="username"
type="text"
@@ -105,19 +112,11 @@ export default function LoginPage() {
setErrors({ ...errors, username: "", general: "" });
}}
placeholder="admin / số điện thoại / tên nhân viên"
className={`
w-full px-10 lg:pl-11 py-3 rounded-xl border outline-none
bg-white text-foreground
placeholder:text-(--color-text-muted)
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150
${errors.username ? "border-red-400" : "border-(--color-border)"}
`}
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.username ? "border-red-400" : "border-(--color-border)"} `}
/>
</div>
{errors.username && (
<p className="mt-1.5 text-xs text-red-500 flex items-center gap-1">
<p className="mt-1.5 flex items-center gap-1 text-xs text-red-500">
<i className="fa-solid fa-circle-exclamation"></i>
{errors.username}
</p>
@@ -126,11 +125,14 @@ export default function LoginPage() {
{/* Password Input */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-(--color-text-secondary) mb-2">
<label
htmlFor="password"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
Mật khẩu
</label>
<div className="relative">
<i className="fa-solid fa-lock absolute left-4 top-1/2 -translate-y-1/2 text-(--color-text-muted) hidden lg:block"></i>
<i className="fa-solid fa-lock absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
<input
id="password"
type={showPassword ? "text" : "password"}
@@ -140,27 +142,21 @@ export default function LoginPage() {
setErrors({ ...errors, password: "", general: "" });
}}
placeholder="Nhập mật khẩu"
className={`
w-full px-10 lg:pl-11 pr-11 py-3 rounded-xl border outline-none
bg-white text-foreground
placeholder:text-(--color-text-muted)
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150
${errors.password ? "border-red-400" : "border-(--color-border)"}
`}
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 pr-11 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.password ? "border-red-400" : "border-(--color-border)"} `}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-(--color-text-muted) hover:text-(--color-primary) transition-colors"
className="absolute top-1/2 right-4 -translate-y-1/2 text-(--color-text-muted) transition-colors hover:text-(--color-primary)"
aria-label={showPassword ? "Ẩn mật khẩu" : "Hiện mật khẩu"}
>
<i className={`fa-solid ${showPassword ? "fa-eye-slash" : "fa-eye"}`}></i>
<i
className={`fa-solid ${showPassword ? "fa-eye-slash" : "fa-eye"}`}
></i>
</button>
</div>
{errors.password && (
<p className="mt-1.5 text-xs text-red-500 flex items-center gap-1">
<p className="mt-1.5 flex items-center gap-1 text-xs text-red-500">
<i className="fa-solid fa-circle-exclamation"></i>
{errors.password}
</p>
@@ -172,10 +168,7 @@ export default function LoginPage() {
{/* Login Button */}
<button
type="submit"
className="w-full py-3 rounded-xl font-semibold text-white
bg-(--color-primary) hover:bg-(--color-primary-dark)
active:scale-98 transition-all duration-150
border-none cursor-pointer"
className="w-full cursor-pointer rounded-xl border-none bg-(--color-primary) py-3 font-semibold text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-98"
>
Đăng nhập
</button>
@@ -183,12 +176,7 @@ export default function LoginPage() {
{/* Register Button */}
<Link
href="/register"
className="w-full py-3 rounded-xl font-semibold
bg-white text-(--color-primary)
border-2 border-(--color-primary)
hover:bg-(--color-primary) hover:text-white
active:scale-98 transition-all duration-150
flex items-center justify-center no-underline"
className="flex w-full items-center justify-center rounded-xl border-2 border-(--color-primary) bg-white py-3 font-semibold text-(--color-primary) no-underline transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
>
Đăng tài khoản
</Link>
@@ -196,12 +184,29 @@ export default function LoginPage() {
</form>
{/* Demo Credentials Info */}
<div className="mt-6 p-4 bg-background rounded-lg">
<p className="text-xs text-(--color-text-muted) mb-2 font-semibold">Tài khoản demo:</p>
<ul className="text-xs text-(--color-text-muted) space-y-1">
<li> Quản : <code className="bg-white px-1.5 py-0.5 rounded">admin / admin</code></li>
<li> Nhân viên: <code className="bg-white px-1.5 py-0.5 rounded">Nguyễn Văn An / Nguyễn Văn An</code></li>
<li> Khách hàng: <code className="bg-white px-1.5 py-0.5 rounded">0987654321 / user1</code></li>
<div className="bg-background mt-6 rounded-lg p-4">
<p className="mb-2 text-xs font-semibold text-(--color-text-muted)">
Tài khoản demo:
</p>
<ul className="space-y-1 text-xs text-(--color-text-muted)">
<li>
Quản :{" "}
<code className="rounded bg-white px-1.5 py-0.5">
admin / admin
</code>
</li>
<li>
Nhân viên:{" "}
<code className="rounded bg-white px-1.5 py-0.5">
Nguyễn Văn An / Nguyễn Văn An
</code>
</li>
<li>
Khách hàng:{" "}
<code className="rounded bg-white px-1.5 py-0.5">
0987654321 / user1
</code>
</li>
</ul>
</div>
</div>
+22 -43
View File
@@ -1,11 +1,11 @@
"use client";
import { useState, useEffect } from "react";
import Navbar from "@/components/Navbar";
import CartProduct from "@/components/CartProduct";
import Navbar from "@/components/Navbar";
import { useCart } from "@/lib/cart-context";
import { MENU_CATEGORIES, MOCK_PRODUCTS } from "@/lib/constants";
import { useMenu } from "@/lib/menu-context";
import { useCart } from "@/lib/cart-context";
import { useEffect, useState } from "react";
/**
* Main page — sidebar + product grid layout.
@@ -56,7 +56,8 @@ export default function Home() {
* p.available defaults to true when undefined (opt-in unavailability). */
const filteredProducts = MOCK_PRODUCTS.filter((p) => {
const isAvailable = p.available !== false;
const matchesCategory = activeCategory === "all" || p.category === activeCategory;
const matchesCategory =
activeCategory === "all" || p.category === activeCategory;
const matchesSearch =
searchQuery.trim() === "" ||
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -79,9 +80,7 @@ export default function Home() {
return (
/* Outer wrapper: flex row, align-items: flex-start so sidebar sticks */
<div
className="flex items-start bg-background min-h-[calc(100vh-var(--spacing-header-height))]"
>
<div className="bg-background flex min-h-[calc(100vh-var(--spacing-header-height))] items-start">
{/* ── Sidebar ── */}
<Navbar
isOpen={isSidebarOpen}
@@ -91,37 +90,28 @@ export default function Home() {
/>
{/* ── Main content ── */}
<main className="flex-1 min-w-0 px-4 py-6 md:px-6 lg:px-8">
<main className="min-w-0 flex-1 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">
<div className="mb-5 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
{/* Title + count */}
<div className="shrink-0">
<h2 className="text-xl font-bold text-foreground">
<h2 className="text-foreground text-xl font-bold">
{activeCategoryLabel}
</h2>
<p className="text-sm text-muted-foreground mt-0.5">
<p className="text-muted-foreground mt-0.5 text-sm">
{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-(--color-text-muted) pointer-events-none"
></i>
<i className="fa-solid fa-magnifying-glass pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-sm text-(--color-text-muted)"></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-card text-foreground
border-border placeholder:text-muted-foreground
focus:border-primary focus:ring-2
focus:ring-primary focus:ring-opacity-20
transition-all duration-150"
className="bg-card text-foreground border-border placeholder:text-muted-foreground focus:border-primary focus:ring-primary focus:ring-opacity-20 w-full rounded-xl border py-2 pr-9 pl-9 text-sm transition-all duration-150 outline-none focus:ring-2"
/>
{/* Clear button */}
{searchQuery && (
@@ -129,9 +119,7 @@ export default function Home() {
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-(--color-text-muted)
hover:text-(--color-primary) transition-colors duration-150
cursor-pointer border-none bg-transparent p-0"
className="absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer border-none bg-transparent p-0 text-(--color-text-muted) transition-colors duration-150 hover:text-(--color-primary)"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
@@ -140,7 +128,7 @@ export default function Home() {
</div>
{/* ── Mobile category menu — visible only on < md, below search, above products ── */}
<div className="md:hidden sticky top-18 pt-2 bg-background z-50 -mx-4 px-4 overflow-x-auto mb-4">
<div className="bg-background sticky top-18 z-50 -mx-4 mb-4 overflow-x-auto px-4 pt-2 md:hidden">
<div className="flex items-center gap-1.5 pb-1">
{MENU_CATEGORIES.map((cat) => {
const isActive = activeCategory === cat.id;
@@ -148,22 +136,14 @@ export default function Home() {
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`
flex items-center gap-1.5 px-3 py-2 rounded-xl
text-sm font-medium whitespace-nowrap shrink-0
cursor-pointer border-none transition-all duration-150
${
isActive
? "bg-(--color-primary) text-white shadow-sm"
: "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light) hover:text-(--color-primary-dark)"
}
`}
className={`flex shrink-0 cursor-pointer items-center gap-1.5 rounded-xl border-none px-3 py-2 text-sm font-medium whitespace-nowrap transition-all duration-150 ${
isActive
? "bg-(--color-primary) text-white shadow-sm"
: "bg-transparent text-(--color-text-secondary) hover:bg-(--color-border-light) hover:text-(--color-primary-dark)"
} `}
>
<i
className={`
${cat.icon} text-sm shrink-0
${isActive ? "text-white" : "text-(--color-primary)"}
`}
className={` ${cat.icon} shrink-0 text-sm ${isActive ? "text-white" : "text-(--color-primary)"} `}
></i>
<span>{cat.name}</span>
</button>
@@ -189,7 +169,7 @@ export default function Home() {
</div>
) : (
/* Empty state */
<div className="flex flex-col items-center justify-center py-24 gap-4 text-(--color-text-muted)">
<div className="flex flex-col items-center justify-center gap-4 py-24 text-(--color-text-muted)">
<i className="fa-solid fa-mug-hot text-5xl opacity-30"></i>
<p className="text-base font-medium">
{searchQuery
@@ -199,8 +179,7 @@ export default function Home() {
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="text-sm text-(--color-primary) hover:underline cursor-pointer
border-none bg-transparent"
className="cursor-pointer border-none bg-transparent text-sm text-(--color-primary) hover:underline"
>
Xóa tìm kiếm
</button>
+42 -27
View File
@@ -1,7 +1,7 @@
"use client";
import Link from 'next/link';
import { useCart } from "@/lib/cart-context";
import Link from "next/link";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
@@ -17,12 +17,14 @@ export default function PaymentPage() {
} = useCart();
return (
<div className="max-w-screen-2xl mx-auto w-full px-4 md:px-6 lg:px-8 py-6 md:py-8">
<div className="flex flex-col xl:flex-row gap-6">
<section className="flex-1 min-w-0">
<div className="rounded-2xl border border-(--color-border-light) bg-card overflow-hidden">
<div className="px-4 py-3 border-b border-(--color-border-light)">
<h1 className="text-lg md:text-xl font-bold text-foreground">Trang thanh toán</h1>
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
<div className="flex flex-col gap-6 xl:flex-row">
<section className="min-w-0 flex-1">
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
<div className="border-b border-(--color-border-light) px-4 py-3">
<h1 className="text-foreground text-lg font-bold md:text-xl">
Trang thanh toán
</h1>
</div>
{items.length === 0 ? (
@@ -38,24 +40,31 @@ export default function PaymentPage() {
<th className="px-4 py-3 font-semibold">Giá tiền</th>
<th className="px-4 py-3 font-semibold"> tả</th>
<th className="px-4 py-3 font-semibold">Số lượng</th>
<th className="px-4 py-3 font-semibold text-right">Xóa</th>
<th className="px-4 py-3 text-right font-semibold">
Xóa
</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="border-t border-(--color-border-light)">
<td className="px-4 py-3 font-medium text-foreground">{item.name}</td>
<td className="px-4 py-3 text-(--color-primary) font-semibold">
<tr
key={item.id}
className="border-t border-(--color-border-light)"
>
<td className="text-foreground px-4 py-3 font-medium">
{item.name}
</td>
<td className="px-4 py-3 font-semibold text-(--color-primary)">
{formatPrice(item.price)}
</td>
<td className="px-4 py-3 text-(--color-text-muted) max-w-70">
<td className="max-w-70 px-4 py-3 text-(--color-text-muted)">
<p className="line-clamp-2">{item.description}</p>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<button
onClick={() => decreaseQty(item.id)}
className="w-8 h-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
aria-label={`Giảm số lượng ${item.name}`}
>
-
@@ -64,13 +73,15 @@ export default function PaymentPage() {
type="number"
min={1}
value={item.quantity}
onChange={(e) => setQuantity(item.id, Number(e.target.value))}
className="w-16 h-8 text-center rounded-lg border border-(--color-border) bg-transparent"
title='Nhập số lượng'
onChange={(e) =>
setQuantity(item.id, Number(e.target.value))
}
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
title="Nhập số lượng"
/>
<button
onClick={() => increaseQty(item.id)}
className="w-8 h-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
aria-label={`Tăng số lượng ${item.name}`}
>
+
@@ -80,10 +91,12 @@ export default function PaymentPage() {
<td className="px-4 py-3 text-right">
<button
onClick={() => removeFromCart(item.id)}
className="inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-red-500 text-white hover:bg-red-600 transition-colors"
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-500 px-3 py-2 text-white transition-colors hover:bg-red-600"
>
<i className="fa-solid fa-trash"></i>
<span className="hidden lg:inline">Xóa sản phẩm</span>
<span className="hidden lg:inline">
Xóa sản phẩm
</span>
</button>
</td>
</tr>
@@ -95,18 +108,20 @@ export default function PaymentPage() {
</div>
</section>
<aside className="xl:w-85 shrink-0">
<div className="sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) bg-card p-4 md:p-5">
<h2 className="text-lg font-bold mb-4">Hóa đơn</h2>
<aside className="shrink-0 xl:w-85">
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
<div className="flex items-center justify-between pb-4 border-b border-(--color-border-light)">
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
<span className="text-(--color-text-muted)">Tổng cộng</span>
<span className="text-xl font-bold text-(--color-primary)">{formatPrice(totalPrice)}</span>
<span className="text-xl font-bold text-(--color-primary)">
{formatPrice(totalPrice)}
</span>
</div>
<div className="mt-4 grid grid-cols-2 gap-3">
<button
className="inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2.5 bg-(--color-primary) text-white hover:bg-(--color-primary-dark) transition-colors cursor-pointer"
className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-3 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
type="button"
>
<i className="fa-solid fa-money-bill-wave"></i>
@@ -114,7 +129,7 @@ export default function PaymentPage() {
</button>
<button
className="inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2.5 border border-(--color-border) text-foreground hover:bg-(--color-border-light) transition-colors cursor-pointer"
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
type="button"
>
<i className="fa-solid fa-qrcode"></i>
@@ -123,7 +138,7 @@ export default function PaymentPage() {
<Link href="/">
<button
className="inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2.5 border border-(--color-border) text-foreground hover:bg-(--color-border-light) transition-colors cursor-pointer"
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
type="button"
>
<i className="fa-solid fa-arrow-rotate-left"></i>
+2 -2
View File
@@ -1,8 +1,8 @@
"use client";
import { MenuProvider } from "@/lib/menu-context";
import { CartProvider } from "@/lib/cart-context";
import { AuthProvider } from "@/lib/auth-context";
import { CartProvider } from "@/lib/cart-context";
import { MenuProvider } from "@/lib/menu-context";
/**
* Client-side providers wrapper.
+60 -78
View File
@@ -1,11 +1,11 @@
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { useAuth } from "@/lib/auth-context";
import { SHOP_INFO } from "@/lib/constants";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
// Static OTP for demo (in production, this would be sent via SMS)
const DEMO_OTP = "123456";
@@ -13,7 +13,7 @@ const DEMO_OTP = "123456";
export default function RegisterPage() {
const router = useRouter();
const { completeRegistration } = useAuth();
const [step, setStep] = useState<"phone" | "otp">("phone");
const [phone, setPhone] = useState("");
const [otp, setOtp] = useState("");
@@ -29,14 +29,17 @@ export default function RegisterPage() {
const handlePhoneSubmit = (e: FormEvent) => {
e.preventDefault();
if (!phone.trim()) {
setErrors({ ...errors, phone: "Vui lòng nhập số điện thoại" });
return;
}
if (!validatePhone(phone)) {
setErrors({ ...errors, phone: "Số điện thoại không hợp lệ (VD: 0987654321)" });
setErrors({
...errors,
phone: "Số điện thoại không hợp lệ (VD: 0987654321)",
});
return;
}
@@ -47,7 +50,7 @@ export default function RegisterPage() {
const handleOtpSubmit = (e: FormEvent) => {
e.preventDefault();
if (!otp.trim()) {
setErrors({ ...errors, otp: "Vui lòng nhập mã OTP" });
return;
@@ -70,15 +73,12 @@ export default function RegisterPage() {
};
return (
<div
className="min-h-screen flex items-center justify-center px-4 py-8 bg-background"
>
<div className="bg-background flex min-h-screen items-center justify-center px-4 py-8">
{/* Register Form Card */}
<div className="w-full max-w-md bg-white rounded-2xl shadow-lg p-8">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-lg">
{/* Logo & Shop Name */}
<div className="flex flex-col items-center mb-8">
<div className="relative w-20 h-20 mb-4">
<div className="mb-8 flex flex-col items-center">
<div className="relative mb-4 h-20 w-20">
<Image
src={SHOP_INFO.logo}
alt={SHOP_INFO.name}
@@ -88,29 +88,35 @@ export default function RegisterPage() {
priority
/>
</div>
<h1 className="text-2xl font-bold text-(--color-primary-dark) mb-1">
<h1 className="mb-1 text-2xl font-bold text-(--color-primary-dark)">
{SHOP_INFO.name}
</h1>
<p className="text-sm text-(--color-text-muted)">
{step === "phone" ? "Đăng ký tài khoản khách hàng" : "Xác thực số điện thoại"}
{step === "phone"
? "Đăng ký tài khoản khách hàng"
: "Xác thực số điện thoại"}
</p>
</div>
{/* Step Indicator */}
<div className="flex items-center justify-center gap-2 mb-6">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
step === "phone"
? "bg-(--color-primary) text-white"
: "bg-(--color-accent-light) text-(--color-primary-dark)"
}`}>
<div className="mb-6 flex items-center justify-center gap-2">
<div
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold ${
step === "phone"
? "bg-(--color-primary) text-white"
: "bg-(--color-accent-light) text-(--color-primary-dark)"
}`}
>
1
</div>
<div className="w-12 h-0.5 bg-(--color-border)"></div>
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold ${
step === "otp"
? "bg-(--color-primary) text-white"
: "bg-(--color-border-light) text-(--color-text-muted)"
}`}>
<div className="h-0.5 w-12 bg-(--color-border)"></div>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold ${
step === "otp"
? "bg-(--color-primary) text-white"
: "bg-(--color-border-light) text-(--color-text-muted)"
}`}
>
2
</div>
</div>
@@ -118,14 +124,16 @@ export default function RegisterPage() {
{/* Phone Step */}
{step === "phone" && (
<form onSubmit={handlePhoneSubmit} className="space-y-5">
{/* Phone Input */}
<div>
<label htmlFor="phone" className="block text-sm font-medium text-(--color-text-secondary) mb-2">
<label
htmlFor="phone"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
Số điện thoại
</label>
<div className="relative">
<i className="fa-solid fa-phone absolute left-4 top-1/2 -translate-y-1/2 text-(--color-text-muted) hidden lg:block"></i>
<i className="fa-solid fa-phone absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
<input
id="phone"
type="tel"
@@ -135,19 +143,11 @@ export default function RegisterPage() {
setErrors({ ...errors, phone: "" });
}}
placeholder="0987654321"
className={`
w-full px-10 lg:pl-11 py-3 rounded-xl border outline-none
bg-white text-foreground
placeholder:text-(--color-text-muted)
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150
${errors.phone ? "border-red-400" : "border-(--color-border)"}
`}
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.phone ? "border-red-400" : "border-(--color-border)"} `}
/>
</div>
{errors.phone && (
<p className="mt-1.5 text-xs text-red-500 flex items-center gap-1">
<p className="mt-1.5 flex items-center gap-1 text-xs text-red-500">
<i className="fa-solid fa-circle-exclamation"></i>
{errors.phone}
</p>
@@ -162,10 +162,7 @@ export default function RegisterPage() {
{/* Submit Button */}
<button
type="submit"
className="w-full py-3 rounded-xl font-semibold text-white
bg-(--color-primary) hover:bg-(--color-primary-dark)
active:scale-98 transition-all duration-150
border-none cursor-pointer"
className="w-full cursor-pointer rounded-xl border-none bg-(--color-primary) py-3 font-semibold text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-98"
>
Tiếp tục
</button>
@@ -173,12 +170,7 @@ export default function RegisterPage() {
{/* Back to Login */}
<Link
href="/login"
className="w-full py-3 rounded-xl font-semibold
bg-white text-(--color-primary)
border-2 border-(--color-primary)
hover:bg-(--color-primary) hover:text-white
active:scale-98 transition-all duration-150
flex items-center justify-center no-underline"
className="flex w-full items-center justify-center rounded-xl border-2 border-(--color-primary) bg-white py-3 font-semibold text-(--color-primary) no-underline transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
>
Quay lại đăng nhập
</Link>
@@ -189,25 +181,30 @@ export default function RegisterPage() {
{/* OTP Step */}
{step === "otp" && (
<form onSubmit={handleOtpSubmit} className="space-y-5">
{/* Info Message */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800 mb-2">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<p className="mb-2 text-sm text-blue-800">
<i className="fa-solid fa-circle-info mr-2"></i>
OTP đã đưc gửi đến số <strong>{phone}</strong>
</p>
<p className="text-xs text-blue-600">
Demo OTP: <code className="bg-white px-2 py-1 rounded font-mono">{DEMO_OTP}</code>
Demo OTP:{" "}
<code className="rounded bg-white px-2 py-1 font-mono">
{DEMO_OTP}
</code>
</p>
</div>
{/* OTP Input */}
<div>
<label htmlFor="otp" className="block text-sm font-medium text-(--color-text-secondary) mb-2">
<label
htmlFor="otp"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
OTP
</label>
<div className="relative">
<i className="fa-solid fa-key absolute left-4 top-1/2 -translate-y-1/2 text-(--color-text-muted) hidden lg:block"></i>
<i className="fa-solid fa-key absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
<input
id="otp"
type="text"
@@ -218,19 +215,11 @@ export default function RegisterPage() {
}}
placeholder="Nhập mã OTP"
maxLength={6}
className={`
w-full px-4 lg:pl-11 py-3 rounded-xl border outline-none
bg-white text-foreground text-center text-lg tracking-widest
placeholder:text-(--color-text-muted) placeholder:text-sm placeholder:tracking-normal
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150
${errors.otp ? "border-red-400" : "border-(--color-border)"}
`}
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-4 py-3 text-center text-lg tracking-widest transition-all duration-150 outline-none placeholder:text-sm placeholder:tracking-normal placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) lg:pl-11 ${errors.otp ? "border-red-400" : "border-(--color-border)"} `}
/>
</div>
{errors.otp && (
<p className="mt-1.5 text-xs text-red-500 flex items-center gap-1">
<p className="mt-1.5 flex items-center gap-1 text-xs text-red-500">
<i className="fa-solid fa-circle-exclamation"></i>
{errors.otp}
</p>
@@ -242,10 +231,7 @@ export default function RegisterPage() {
{/* Submit Button */}
<button
type="submit"
className="w-full py-3 rounded-xl font-semibold text-white
bg-(--color-primary) hover:bg-(--color-primary-dark)
active:scale-98 transition-all duration-150
border-none cursor-pointer"
className="w-full cursor-pointer rounded-xl border-none bg-(--color-primary) py-3 font-semibold text-white transition-all duration-150 hover:bg-(--color-primary-dark) active:scale-98"
>
Hoàn tất đăng
</button>
@@ -254,11 +240,7 @@ export default function RegisterPage() {
<button
type="button"
onClick={handleBackToPhone}
className="w-full py-3 rounded-xl font-semibold
bg-white text-(--color-primary)
border-2 border-(--color-primary)
hover:bg-(--color-primary) hover:text-white
active:scale-98 transition-all duration-150"
className="w-full rounded-xl border-2 border-(--color-primary) bg-white py-3 font-semibold text-(--color-primary) transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
>
Thay đi số điện thoại
</button>
@@ -269,7 +251,7 @@ export default function RegisterPage() {
<button
type="button"
disabled
className="text-sm text-(--color-text-muted) cursor-not-allowed"
className="cursor-not-allowed text-sm text-(--color-text-muted)"
>
Gửi lại OTP (60s)
</button>