diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000..59ca91b
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,213 @@
+"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";
+
+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 [showPassword, setShowPassword] = useState(false);
+
+ const validate = (): boolean => {
+ const newErrors = { username: "", password: "", general: "" };
+ let isValid = true;
+
+ if (!username.trim()) {
+ newErrors.username = "Vui lòng nhập tên đăng nhập";
+ isValid = false;
+ }
+
+ if (!password.trim()) {
+ newErrors.password = "Vui lòng nhập mật khẩu";
+ isValid = false;
+ } else if (password.length < 4) {
+ newErrors.password = "Mật khẩu phải có ít nhất 4 ký tự";
+ isValid = false;
+ }
+
+ setErrors(newErrors);
+ return isValid;
+ };
+
+ 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" });
+ }
+ };
+
+ return (
+
+ {/* Login Form Card */}
+
+
+ {/* Logo & Shop Name */}
+
+
+
+
+
+ {SHOP_INFO.name}
+
+
+ Đăng nhập vào hệ thống
+
+
+
+ {/* Error Message */}
+ {errors.general && (
+
+
+ {errors.general}
+
+ )}
+
+ {/* Login Form */}
+
+
+ {/* Demo Credentials Info */}
+
+
Tài khoản demo:
+
+ - • Quản lý:
admin / admin
+ - • Nhân viên:
Nguyễn Văn An / Nguyễn Văn An
+ - • Khách hàng:
0987654321 / user1
+
+
+
+
+ );
+}
diff --git a/app/providers.tsx b/app/providers.tsx
index 53443d4..92eb966 100644
--- a/app/providers.tsx
+++ b/app/providers.tsx
@@ -2,6 +2,7 @@
import { MenuProvider } from "@/lib/menu-context";
import { CartProvider } from "@/lib/cart-context";
+import { AuthProvider } from "@/lib/auth-context";
/**
* Client-side providers wrapper.
@@ -10,8 +11,10 @@ import { CartProvider } from "@/lib/cart-context";
*/
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/app/register/page.tsx b/app/register/page.tsx
new file mode 100644
index 0000000..571aa19
--- /dev/null
+++ b/app/register/page.tsx
@@ -0,0 +1,285 @@
+"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";
+
+// Static OTP for demo (in production, this would be sent via SMS)
+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("");
+ const [errors, setErrors] = useState({ phone: "", otp: "" });
+
+ // Validate Vietnamese phone number
+ const validatePhone = (phoneNumber: string): boolean => {
+ // Vietnamese phone format: 10 digits starting with 0
+ // Valid prefixes: 03, 05, 07, 08, 09
+ const phoneRegex = /^(0[3|5|7|8|9])[0-9]{8}$/;
+ return phoneRegex.test(phoneNumber);
+ };
+
+ 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)" });
+ return;
+ }
+
+ // Move to OTP step
+ setStep("otp");
+ setErrors({ phone: "", otp: "" });
+ };
+
+ const handleOtpSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ if (!otp.trim()) {
+ setErrors({ ...errors, otp: "Vui lòng nhập mã OTP" });
+ return;
+ }
+
+ if (otp !== DEMO_OTP) {
+ setErrors({ ...errors, otp: "Mã OTP không đúng" });
+ return;
+ }
+
+ // Complete registration
+ completeRegistration(phone);
+ router.push("/");
+ };
+
+ const handleBackToPhone = () => {
+ setStep("phone");
+ setOtp("");
+ setErrors({ phone: "", otp: "" });
+ };
+
+ return (
+
+ {/* Register Form Card */}
+
+
+ {/* Logo & Shop Name */}
+
+
+
+
+
+ {SHOP_INFO.name}
+
+
+ {step === "phone" ? "Đăng ký tài khoản khách hàng" : "Xác thực số điện thoại"}
+
+
+
+ {/* Step Indicator */}
+
+
+ {/* Phone Step */}
+ {step === "phone" && (
+
+ )}
+
+ {/* OTP Step */}
+ {step === "otp" && (
+
+ )}
+
+
+ );
+}
diff --git a/layouts/header.tsx b/layouts/header.tsx
index 9209a36..5b4ef0a 100644
--- a/layouts/header.tsx
+++ b/layouts/header.tsx
@@ -1,10 +1,10 @@
"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";
+import { useRouter } from "next/navigation";
+import { SHOP_INFO } from "@/lib/constants";
+import { useAuth } from "@/lib/auth-context";
/**
* Site Header — sticky top bar, always visible on all screen sizes.
@@ -12,8 +12,11 @@ import type { User } from "@/lib/types";
* 2-column layout:
* [LEFT: Brand (logo + name + tagline)] | [RIGHT: Auth button]
*
- * Auth state cycles for UI demo (click the button to switch):
- * Guest → Manager (Quản lý) → Staff → Guest
+ * Auth states:
+ * - Guest: Shows "Đăng nhập" button → navigates to /login
+ * - Manager: Shows "Quản lý" badge with logout on click
+ * - Staff: Shows staff name with logout on click
+ * - Customer: Shows "Khách hàng" with phone and logout on click
*
* Responsive:
* - Logo + shop name : always visible
@@ -21,12 +24,15 @@ import type { User } from "@/lib/types";
* - Button label : hidden on xs (< sm), shown on sm+
*/
export default function Header() {
- const [user, setUser] = useState(null);
+ const router = useRouter();
+ const { user, logout } = useAuth();
- const cycleUser = () => {
- if (!user) setUser(MOCK_USERS.manager);
- else if (user.role === "manager") setUser(MOCK_USERS.staff);
- else setUser(null);
+ const handleAuthClick = () => {
+ if (!user) {
+ router.push("/login");
+ } else {
+ logout();
+ }
};
return (
@@ -81,7 +87,7 @@ export default function Header() {
{!user ? (
/* Guest: sign-in button */
- ) : (
- /* Staff / regular user: avatar + name */
+ ) : user.role === "staff" ? (
+ /* Staff: avatar + name */
+
+ ) : (
+ /* Customer: phone icon + label */
+
)}
diff --git a/lib/auth-context.tsx b/lib/auth-context.tsx
new file mode 100644
index 0000000..63908e3
--- /dev/null
+++ b/lib/auth-context.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { createContext, useContext, useState, useEffect, ReactNode } from "react";
+import type { User } from "./types";
+
+interface AuthContextType {
+ user: User | null;
+ login: (username: string, password: string) => boolean;
+ logout: () => void;
+ registerPhone: string | null;
+ setRegisterPhone: (phone: string | null) => void;
+ completeRegistration: (phone: string) => void;
+}
+
+const AuthContext = createContext(undefined);
+
+// Mock user database
+const MOCK_AUTH_DB = {
+ // Admin
+ admin: { username: "admin", password: "admin", user: { id: 1, name: "Quản lý", role: "manager" as const, avatar: null, phone: "0912345678" } },
+
+ // Staff (username and password are their names)
+ "Nguyễn Văn An": { username: "Nguyễn Văn An", password: "Nguyễn Văn An", user: { id: 2, name: "Nguyễn Văn An", role: "staff" as const, avatar: null, phone: "0901234567" } },
+ "Trần Thị Bình": { username: "Trần Thị Bình", password: "Trần Thị Bình", user: { id: 3, name: "Trần Thị Bình", role: "staff" as const, avatar: null, phone: "0902345678" } },
+ "Lê Văn Cường": { username: "Lê Văn Cường", password: "Lê Văn Cường", user: { id: 4, name: "Lê Văn Cường", role: "staff" as const, avatar: null, phone: "0903456789" } },
+
+ // Customers (username is phone number, password is custom)
+ "0987654321": { username: "0987654321", password: "user1", user: { id: 5, name: "Khách hàng", role: "customer" as const, avatar: null, phone: "0987654321" } },
+ "0976543210": { username: "0976543210", password: "user1", user: { id: 6, name: "Khách hàng", role: "customer" as const, avatar: null, phone: "0976543210" } },
+};
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [registerPhone, setRegisterPhone] = useState(null);
+
+ // Load user from localStorage on mount
+ useEffect(() => {
+ const savedUser = localStorage.getItem("coffee-shop-user");
+ if (savedUser) {
+ try {
+ setUser(JSON.parse(savedUser));
+ } catch (e) {
+ console.error("Failed to parse saved user", e);
+ }
+ }
+ }, []);
+
+ const login = (username: string, password: string): boolean => {
+ const authEntry = MOCK_AUTH_DB[username as keyof typeof MOCK_AUTH_DB];
+
+ if (authEntry && authEntry.password === password) {
+ setUser(authEntry.user);
+ localStorage.setItem("coffee-shop-user", JSON.stringify(authEntry.user));
+ return true;
+ }
+
+ return false;
+ };
+
+ const logout = () => {
+ setUser(null);
+ localStorage.removeItem("coffee-shop-user");
+ };
+
+ const completeRegistration = (phone: string) => {
+ // Create new customer account
+ const newUser: User = {
+ id: Date.now(),
+ name: "Khách hàng",
+ role: "customer",
+ avatar: null,
+ phone,
+ };
+
+ // Add to mock database (in real app, this would be API call)
+ (MOCK_AUTH_DB as any)[phone] = {
+ username: phone,
+ password: "user1", // Default password for new customers
+ user: newUser,
+ };
+
+ setUser(newUser);
+ localStorage.setItem("coffee-shop-user", JSON.stringify(newUser));
+ setRegisterPhone(null);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error("useAuth must be used within an AuthProvider");
+ }
+ return context;
+}
diff --git a/lib/types.ts b/lib/types.ts
index 3d21df5..13b851b 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -6,6 +6,7 @@ export interface User {
name: string;
role: UserRole;
avatar: string | null;
+ phone?: string;
}
// ===== MENU TYPES =====
diff --git a/package-lock.json b/package-lock.json
index d52a89f..87f6471 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -68,7 +68,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1572,7 +1571,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1632,7 +1630,6 @@
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
@@ -2171,7 +2168,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2666,7 +2662,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3475,7 +3470,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -6523,7 +6517,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -6533,7 +6526,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -7478,7 +7470,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -7659,7 +7650,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8019,7 +8009,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}