aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/app/(auth)/login/page.js34
-rw-r--r--frontend/src/app/(auth)/register/page.js34
-rw-r--r--frontend/src/app/admin/page.js293
-rw-r--r--frontend/src/app/favicon.icobin0 -> 25931 bytes
-rw-r--r--frontend/src/app/globals.css122
-rw-r--r--frontend/src/app/layout.js30
-rw-r--r--frontend/src/app/page.js219
-rw-r--r--frontend/src/components/auth/LoginForm.js99
-rw-r--r--frontend/src/components/auth/RegisterForm.js167
-rw-r--r--frontend/src/components/layouts/AdminLayout.js300
-rw-r--r--frontend/src/components/layouts/MainLayout.js256
-rw-r--r--frontend/src/components/ui/button.jsx55
-rw-r--r--frontend/src/components/ui/form.jsx144
-rw-r--r--frontend/src/components/ui/input.jsx24
-rw-r--r--frontend/src/components/ui/label.jsx23
-rw-r--r--frontend/src/components/ui/sonner.jsx26
-rw-r--r--frontend/src/context/auth-context.js126
-rw-r--r--frontend/src/context/cart-context.js120
-rw-r--r--frontend/src/context/theme-context.js54
-rw-r--r--frontend/src/lib/api/auth.js90
-rw-r--r--frontend/src/lib/api/index.js40
-rw-r--r--frontend/src/lib/utils.js6
22 files changed, 2262 insertions, 0 deletions
diff --git a/frontend/src/app/(auth)/login/page.js b/frontend/src/app/(auth)/login/page.js
new file mode 100644
index 0000000..03208e0
--- /dev/null
+++ b/frontend/src/app/(auth)/login/page.js
@@ -0,0 +1,34 @@
+import Link from 'next/link';
+import LoginForm from '@/components/auth/LoginForm';
+
+export const metadata = {
+ title: 'Login - Restaurant Management System',
+ description: 'Login to your account',
+};
+
+const LoginPage = () => {
+ return (
+ <div className="min-h-screen flex flex-col justify-center items-center p-4 bg-gray-50 dark:bg-slate-900">
+ <div className="w-full max-w-md text-center mb-8">
+ <Link href="/" className="text-2xl font-bold text-primary">
+ Restaurant
+ </Link>
+ <h1 className="text-3xl font-bold mt-6">Welcome Back</h1>
+ <p className="text-gray-600 dark:text-gray-400 mt-2">
+ Enter your credentials to access your account
+ </p>
+ </div>
+
+ <LoginForm />
+
+ <p className="mt-8 text-center text-gray-600 dark:text-gray-400">
+ Don't have an account?{' '}
+ <Link href="/register" className="text-primary hover:underline">
+ Register here
+ </Link>
+ </p>
+ </div>
+ );
+};
+
+export default LoginPage; \ No newline at end of file
diff --git a/frontend/src/app/(auth)/register/page.js b/frontend/src/app/(auth)/register/page.js
new file mode 100644
index 0000000..1916413
--- /dev/null
+++ b/frontend/src/app/(auth)/register/page.js
@@ -0,0 +1,34 @@
+import Link from 'next/link';
+import RegisterForm from '@/components/auth/RegisterForm';
+
+export const metadata = {
+ title: 'Register - Restaurant Management System',
+ description: 'Create a new account',
+};
+
+const RegisterPage = () => {
+ return (
+ <div className="min-h-screen flex flex-col justify-center items-center p-4 bg-gray-50 dark:bg-slate-900">
+ <div className="w-full max-w-md text-center mb-8">
+ <Link href="/" className="text-2xl font-bold text-primary">
+ Restaurant
+ </Link>
+ <h1 className="text-3xl font-bold mt-6">Create Account</h1>
+ <p className="text-gray-600 dark:text-gray-400 mt-2">
+ Join us to enjoy delicious food and exclusive offers
+ </p>
+ </div>
+
+ <RegisterForm />
+
+ <p className="mt-8 text-center text-gray-600 dark:text-gray-400">
+ Already have an account?{' '}
+ <Link href="/login" className="text-primary hover:underline">
+ Login here
+ </Link>
+ </p>
+ </div>
+ );
+};
+
+export default RegisterPage; \ No newline at end of file
diff --git a/frontend/src/app/admin/page.js b/frontend/src/app/admin/page.js
new file mode 100644
index 0000000..9f0e737
--- /dev/null
+++ b/frontend/src/app/admin/page.js
@@ -0,0 +1,293 @@
+import AdminLayout from '@/components/layouts/AdminLayout';
+
+export const metadata = {
+ title: 'Admin Dashboard - Restaurant Management System',
+ description: 'Admin dashboard for managing the restaurant',
+};
+
+const AdminDashboard = () => {
+ return (
+ <AdminLayout>
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+ {/* Stats Card: Total Orders */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
+ Total Orders
+ </h3>
+ <p className="text-3xl font-bold">128</p>
+ <div className="mt-2 flex items-center text-sm text-green-600">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-1"
+ >
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
+ <polyline points="17 6 23 6 23 12" />
+ </svg>
+ <span>12% increase</span>
+ </div>
+ </div>
+
+ {/* Stats Card: Revenue */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
+ Revenue
+ </h3>
+ <p className="text-3xl font-bold">$3,240</p>
+ <div className="mt-2 flex items-center text-sm text-green-600">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-1"
+ >
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
+ <polyline points="17 6 23 6 23 12" />
+ </svg>
+ <span>8% increase</span>
+ </div>
+ </div>
+
+ {/* Stats Card: Customers */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
+ Customers
+ </h3>
+ <p className="text-3xl font-bold">64</p>
+ <div className="mt-2 flex items-center text-sm text-green-600">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-1"
+ >
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
+ <polyline points="17 6 23 6 23 12" />
+ </svg>
+ <span>24% increase</span>
+ </div>
+ </div>
+
+ {/* Stats Card: Reservations */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
+ Reservations
+ </h3>
+ <p className="text-3xl font-bold">32</p>
+ <div className="mt-2 flex items-center text-sm text-yellow-600">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-1"
+ >
+ <line x1="5" y1="12" x2="19" y2="12" />
+ </svg>
+ <span>Same as last week</span>
+ </div>
+ </div>
+ </div>
+
+ {/* Recent Orders */}
+ <div className="bg-white dark:bg-slate-800 rounded-lg shadow-md mb-8">
+ <div className="p-6 border-b border-gray-200 dark:border-slate-700">
+ <h2 className="text-lg font-semibold">Recent Orders</h2>
+ </div>
+ <div className="overflow-x-auto">
+ <table className="w-full text-sm">
+ <thead className="bg-gray-50 dark:bg-slate-700">
+ <tr>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Order ID
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Customer
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Date
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Status
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Amount
+ </th>
+ </tr>
+ </thead>
+ <tbody className="divide-y divide-gray-200 dark:divide-slate-700">
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">#ORDER123</td>
+ <td className="px-6 py-4 whitespace-nowrap">John Smith</td>
+ <td className="px-6 py-4 whitespace-nowrap">Apr 28, 2025</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Completed
+ </span>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap">$128.50</td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">#ORDER122</td>
+ <td className="px-6 py-4 whitespace-nowrap">Sarah Johnson</td>
+ <td className="px-6 py-4 whitespace-nowrap">Apr 28, 2025</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-800/20 dark:text-yellow-400">
+ Preparing
+ </span>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap">$75.20</td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">#ORDER121</td>
+ <td className="px-6 py-4 whitespace-nowrap">Michael Brown</td>
+ <td className="px-6 py-4 whitespace-nowrap">Apr 27, 2025</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800 dark:bg-blue-800/20 dark:text-blue-400">
+ Delivered
+ </span>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap">$92.40</td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">#ORDER120</td>
+ <td className="px-6 py-4 whitespace-nowrap">Emma Wilson</td>
+ <td className="px-6 py-4 whitespace-nowrap">Apr 27, 2025</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Completed
+ </span>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap">$54.60</td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">#ORDER119</td>
+ <td className="px-6 py-4 whitespace-nowrap">David Clark</td>
+ <td className="px-6 py-4 whitespace-nowrap">Apr 26, 2025</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-800 dark:bg-red-800/20 dark:text-red-400">
+ Cancelled
+ </span>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap">$0.00</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ {/* Today's Reservations */}
+ <div className="bg-white dark:bg-slate-800 rounded-lg shadow-md">
+ <div className="p-6 border-b border-gray-200 dark:border-slate-700">
+ <h2 className="text-lg font-semibold">Today's Reservations</h2>
+ </div>
+ <div className="overflow-x-auto">
+ <table className="w-full text-sm">
+ <thead className="bg-gray-50 dark:bg-slate-700">
+ <tr>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Customer
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Time
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Table
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Guests
+ </th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+ Status
+ </th>
+ </tr>
+ </thead>
+ <tbody className="divide-y divide-gray-200 dark:divide-slate-700">
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">Jessica Lee</td>
+ <td className="px-6 py-4 whitespace-nowrap">12:30 PM</td>
+ <td className="px-6 py-4 whitespace-nowrap">Table 5</td>
+ <td className="px-6 py-4 whitespace-nowrap">4</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Confirmed
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">Robert Taylor</td>
+ <td className="px-6 py-4 whitespace-nowrap">1:00 PM</td>
+ <td className="px-6 py-4 whitespace-nowrap">Table 8</td>
+ <td className="px-6 py-4 whitespace-nowrap">2</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Confirmed
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">Maria Rodriguez</td>
+ <td className="px-6 py-4 whitespace-nowrap">7:00 PM</td>
+ <td className="px-6 py-4 whitespace-nowrap">Table 12</td>
+ <td className="px-6 py-4 whitespace-nowrap">6</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-800/20 dark:text-yellow-400">
+ Pending
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">James Wilson</td>
+ <td className="px-6 py-4 whitespace-nowrap">7:30 PM</td>
+ <td className="px-6 py-4 whitespace-nowrap">Table 3</td>
+ <td className="px-6 py-4 whitespace-nowrap">2</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Confirmed
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td className="px-6 py-4 whitespace-nowrap">Emily Davis</td>
+ <td className="px-6 py-4 whitespace-nowrap">8:00 PM</td>
+ <td className="px-6 py-4 whitespace-nowrap">Table 7</td>
+ <td className="px-6 py-4 whitespace-nowrap">4</td>
+ <td className="px-6 py-4 whitespace-nowrap">
+ <span className="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800 dark:bg-green-800/20 dark:text-green-400">
+ Confirmed
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </AdminLayout>
+ );
+};
+
+export default AdminDashboard; \ No newline at end of file
diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/frontend/src/app/favicon.ico
Binary files differ
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
new file mode 100644
index 0000000..dc98be7
--- /dev/null
+++ b/frontend/src/app/globals.css
@@ -0,0 +1,122 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/frontend/src/app/layout.js b/frontend/src/app/layout.js
new file mode 100644
index 0000000..de45098
--- /dev/null
+++ b/frontend/src/app/layout.js
@@ -0,0 +1,30 @@
+import { Inter } from "next/font/google";
+import "./globals.css";
+import { ThemeProvider } from "@/context/theme-context";
+import { AuthProvider } from "@/context/auth-context";
+import { CartProvider } from "@/context/cart-context";
+import { Sonner } from "@/components/ui/sonner";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata = {
+ title: "Restaurant Management System",
+ description: "A full-stack application for restaurant management",
+};
+
+export default function RootLayout({ children }) {
+ return (
+ <html lang="en" suppressHydrationWarning>
+ <body className={inter.className}>
+ <ThemeProvider>
+ <AuthProvider>
+ <CartProvider>
+ {children}
+ <Sonner />
+ </CartProvider>
+ </AuthProvider>
+ </ThemeProvider>
+ </body>
+ </html>
+ );
+}
diff --git a/frontend/src/app/page.js b/frontend/src/app/page.js
new file mode 100644
index 0000000..2c7ec34
--- /dev/null
+++ b/frontend/src/app/page.js
@@ -0,0 +1,219 @@
+import Link from 'next/link';
+import MainLayout from '@/components/layouts/MainLayout';
+
+export default function Home() {
+ return (
+ <MainLayout>
+ {/* Hero section */}
+ <section className="py-16 md:py-24">
+ <div className="container mx-auto text-center">
+ <h1 className="text-4xl md:text-6xl font-bold mb-6">
+ Experience the <span className="text-primary">Finest</span> Cuisine
+ </h1>
+ <p className="text-lg md:text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-3xl mx-auto">
+ Welcome to our restaurant, where we serve delicious meals made with fresh ingredients and a passion for culinary excellence.
+ </p>
+ <div className="flex flex-wrap justify-center gap-4">
+ <Link
+ href="/menu"
+ className="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
+ >
+ View Menu
+ </Link>
+ <Link
+ href="/reservations"
+ className="px-6 py-3 bg-white dark:bg-slate-800 border border-gray-300 dark:border-slate-700 rounded-md hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors"
+ >
+ Book a Table
+ </Link>
+ </div>
+ </div>
+ </section>
+
+ {/* Featured Menu Items */}
+ <section className="py-16 bg-gray-50 dark:bg-slate-800/50">
+ <div className="container mx-auto">
+ <h2 className="text-3xl font-bold mb-12 text-center">Featured Menu Items</h2>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
+ {/* Item 1 */}
+ <div className="bg-white dark:bg-slate-800 rounded-lg overflow-hidden shadow-md">
+ <div className="h-48 bg-gray-300 dark:bg-gray-700"></div>
+ <div className="p-6">
+ <h3 className="text-xl font-bold mb-2">Signature Pasta</h3>
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
+ Handmade pasta with our special sauce and fresh herbs.
+ </p>
+ <div className="flex justify-between items-center">
+ <span className="text-lg font-bold">$18.99</span>
+ <button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
+ Order Now
+ </button>
+ </div>
+ </div>
+ </div>
+
+ {/* Item 2 */}
+ <div className="bg-white dark:bg-slate-800 rounded-lg overflow-hidden shadow-md">
+ <div className="h-48 bg-gray-300 dark:bg-gray-700"></div>
+ <div className="p-6">
+ <h3 className="text-xl font-bold mb-2">Premium Steak</h3>
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
+ Juicy, tender steak cooked to perfection with seasonal vegetables.
+ </p>
+ <div className="flex justify-between items-center">
+ <span className="text-lg font-bold">$29.99</span>
+ <button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
+ Order Now
+ </button>
+ </div>
+ </div>
+ </div>
+
+ {/* Item 3 */}
+ <div className="bg-white dark:bg-slate-800 rounded-lg overflow-hidden shadow-md">
+ <div className="h-48 bg-gray-300 dark:bg-gray-700"></div>
+ <div className="p-6">
+ <h3 className="text-xl font-bold mb-2">Seafood Delight</h3>
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
+ Fresh seafood mix with special spices and lemon butter sauce.
+ </p>
+ <div className="flex justify-between items-center">
+ <span className="text-lg font-bold">$24.99</span>
+ <button className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
+ Order Now
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="text-center mt-12">
+ <Link
+ href="/menu"
+ className="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
+ >
+ View Full Menu
+ </Link>
+ </div>
+ </div>
+ </section>
+
+ {/* About Section */}
+ <section className="py-16">
+ <div className="container mx-auto">
+ <div className="flex flex-col md:flex-row items-center gap-12">
+ <div className="md:w-1/2">
+ <div className="h-80 bg-gray-300 dark:bg-gray-700 rounded-lg"></div>
+ </div>
+ <div className="md:w-1/2">
+ <h2 className="text-3xl font-bold mb-6">Our Story</h2>
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
+ Founded in 2010, our restaurant has been serving the community with delicious meals made from the freshest ingredients. Our chefs are passionate about creating unforgettable dining experiences.
+ </p>
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
+ We believe in sustainable practices and source our ingredients from local farmers and suppliers whenever possible.
+ </p>
+ <Link
+ href="/about"
+ className="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors"
+ >
+ Learn More
+ </Link>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ {/* Testimonials */}
+ <section className="py-16 bg-gray-50 dark:bg-slate-800/50">
+ <div className="container mx-auto">
+ <h2 className="text-3xl font-bold mb-12 text-center">What Our Customers Say</h2>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
+ {/* Testimonial 1 */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <div className="flex items-center mb-4">
+ <div className="w-12 h-12 bg-gray-300 dark:bg-gray-700 rounded-full mr-4"></div>
+ <div>
+ <h3 className="font-bold">John Smith</h3>
+ <div className="flex text-yellow-400">
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ </div>
+ </div>
+ </div>
+ <p className="text-gray-600 dark:text-gray-400">
+ "The food was amazing and the service was exceptional. I will definitely be coming back!"
+ </p>
+ </div>
+
+ {/* Testimonial 2 */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <div className="flex items-center mb-4">
+ <div className="w-12 h-12 bg-gray-300 dark:bg-gray-700 rounded-full mr-4"></div>
+ <div>
+ <h3 className="font-bold">Sarah Johnson</h3>
+ <div className="flex text-yellow-400">
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ </div>
+ </div>
+ </div>
+ <p className="text-gray-600 dark:text-gray-400">
+ "Perfect place for a special occasion. The ambiance was lovely and the food was delicious."
+ </p>
+ </div>
+
+ {/* Testimonial 3 */}
+ <div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md">
+ <div className="flex items-center mb-4">
+ <div className="w-12 h-12 bg-gray-300 dark:bg-gray-700 rounded-full mr-4"></div>
+ <div>
+ <h3 className="font-bold">Michael Brown</h3>
+ <div className="flex text-yellow-400">
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ <span>★</span>
+ </div>
+ </div>
+ </div>
+ <p className="text-gray-600 dark:text-gray-400">
+ "I've tried many restaurants in the area, but this one stands out. The flavors are authentic and the staff is friendly."
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ {/* Call to Action */}
+ <section className="py-16 bg-primary text-white">
+ <div className="container mx-auto text-center">
+ <h2 className="text-3xl font-bold mb-6">Ready to Experience Our Cuisine?</h2>
+ <p className="text-xl mb-8 max-w-3xl mx-auto">
+ Book your table now or order online for pickup or delivery.
+ </p>
+ <div className="flex flex-wrap justify-center gap-4">
+ <Link
+ href="/reservations"
+ className="px-6 py-3 bg-white text-primary font-bold rounded-md hover:bg-gray-100 transition-colors"
+ >
+ Book a Table
+ </Link>
+ <Link
+ href="/menu"
+ className="px-6 py-3 bg-transparent border-2 border-white rounded-md hover:bg-white/10 transition-colors"
+ >
+ Order Online
+ </Link>
+ </div>
+ </div>
+ </section>
+ </MainLayout>
+ );
+}
diff --git a/frontend/src/components/auth/LoginForm.js b/frontend/src/components/auth/LoginForm.js
new file mode 100644
index 0000000..fd5aeb7
--- /dev/null
+++ b/frontend/src/components/auth/LoginForm.js
@@ -0,0 +1,99 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useAuth } from '@/context/auth-context';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
+
+// Form validation schema
+const formSchema = z.object({
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(6, 'Password must be at least 6 characters'),
+});
+
+const LoginForm = () => {
+ const { login } = useAuth();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ });
+
+ const onSubmit = async (values) => {
+ try {
+ setIsLoading(true);
+ setError('');
+ await login({
+ email: values.email,
+ password: values.password,
+ });
+ } catch (error) {
+ setError(error.message || 'Login failed. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="w-full max-w-md mx-auto">
+ <div className="bg-white dark:bg-slate-800 p-8 rounded-lg shadow-md">
+ <h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
+
+ {error && (
+ <div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-md mb-4">
+ {error}
+ </div>
+ )}
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="email"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Email</FormLabel>
+ <FormControl>
+ <Input placeholder="[email protected]" type="email" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="password"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Password</FormLabel>
+ <FormControl>
+ <Input placeholder="••••••••" type="password" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <Button
+ type="submit"
+ className="w-full"
+ disabled={isLoading}
+ >
+ {isLoading ? 'Logging in...' : 'Login'}
+ </Button>
+ </form>
+ </Form>
+ </div>
+ </div>
+ );
+};
+
+export default LoginForm; \ No newline at end of file
diff --git a/frontend/src/components/auth/RegisterForm.js b/frontend/src/components/auth/RegisterForm.js
new file mode 100644
index 0000000..3c321ea
--- /dev/null
+++ b/frontend/src/components/auth/RegisterForm.js
@@ -0,0 +1,167 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useAuth } from '@/context/auth-context';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
+
+// Form validation schema
+const formSchema = z.object({
+ name: z.string().min(2, 'Name must be at least 2 characters'),
+ email: z.string().email('Invalid email address'),
+ password: z.string().min(6, 'Password must be at least 6 characters'),
+ confirmPassword: z.string().min(6, 'Password must be at least 6 characters'),
+ phone: z.string().optional(),
+ address: z.string().optional(),
+}).refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords don't match",
+ path: ['confirmPassword'],
+});
+
+const RegisterForm = () => {
+ const { register } = useAuth();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ phone: '',
+ address: '',
+ },
+ });
+
+ const onSubmit = async (values) => {
+ try {
+ setIsLoading(true);
+ setError('');
+
+ // Remove confirmPassword from the data sent to API
+ const { confirmPassword, ...userData } = values;
+
+ await register(userData);
+ } catch (error) {
+ setError(error.message || 'Registration failed. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div className="w-full max-w-md mx-auto">
+ <div className="bg-white dark:bg-slate-800 p-8 rounded-lg shadow-md">
+ <h2 className="text-2xl font-bold mb-6 text-center">Create Account</h2>
+
+ {error && (
+ <div className="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-3 rounded-md mb-4">
+ {error}
+ </div>
+ )}
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+ <FormField
+ control={form.control}
+ name="name"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Name</FormLabel>
+ <FormControl>
+ <Input placeholder="John Doe" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="email"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Email</FormLabel>
+ <FormControl>
+ <Input placeholder="[email protected]" type="email" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="password"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Password</FormLabel>
+ <FormControl>
+ <Input placeholder="••••••••" type="password" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="confirmPassword"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Confirm Password</FormLabel>
+ <FormControl>
+ <Input placeholder="••••••••" type="password" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="phone"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Phone (Optional)</FormLabel>
+ <FormControl>
+ <Input placeholder="(123) 456-7890" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="address"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Address (Optional)</FormLabel>
+ <FormControl>
+ <Input placeholder="123 Main St, City, State" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <Button
+ type="submit"
+ className="w-full"
+ disabled={isLoading}
+ >
+ {isLoading ? 'Creating Account...' : 'Register'}
+ </Button>
+ </form>
+ </Form>
+ </div>
+ </div>
+ );
+};
+
+export default RegisterForm; \ No newline at end of file
diff --git a/frontend/src/components/layouts/AdminLayout.js b/frontend/src/components/layouts/AdminLayout.js
new file mode 100644
index 0000000..fc05708
--- /dev/null
+++ b/frontend/src/components/layouts/AdminLayout.js
@@ -0,0 +1,300 @@
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useAuth } from '@/context/auth-context';
+import { useTheme } from '@/context/theme-context';
+
+const AdminLayout = ({ children }) => {
+ const { user, logout } = useAuth();
+ const { theme, toggleTheme } = useTheme();
+ const router = useRouter();
+
+ // Redirect if not admin
+ if (user && user.role !== 'admin') {
+ router.push('/login');
+ return null;
+ }
+
+ return (
+ <div className="flex min-h-screen bg-gray-50 dark:bg-slate-900">
+ {/* Sidebar */}
+ <aside className="w-64 bg-white dark:bg-slate-800 border-r border-gray-200 dark:border-slate-700 fixed h-full">
+ <div className="p-4 border-b border-gray-200 dark:border-slate-700">
+ <Link href="/admin" className="text-xl font-bold text-primary">
+ Admin Dashboard
+ </Link>
+ </div>
+
+ <nav className="p-4">
+ <ul className="space-y-2">
+ <li>
+ <Link
+ href="/admin"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <rect width="7" height="9" x="3" y="3" rx="1" />
+ <rect width="7" height="5" x="14" y="3" rx="1" />
+ <rect width="7" height="9" x="14" y="12" rx="1" />
+ <rect width="7" height="5" x="3" y="16" rx="1" />
+ </svg>
+ Dashboard
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/users"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
+ <circle cx="9" cy="7" r="4" />
+ <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
+ </svg>
+ Users
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/menu"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <path d="M3 3h18v18H3V3z" />
+ <path d="M9 3v18" />
+ </svg>
+ Menu Management
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/orders"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <path d="M5 7 3 5" />
+ <path d="M9 3 3 9H2v4.5L7 9" />
+ <circle cx="9" cy="15" r="6" />
+ </svg>
+ Orders
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/reservations"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
+ <line x1="16" x2="16" y1="2" y2="6" />
+ <line x1="8" x2="8" y1="2" y2="6" />
+ <line x1="3" x2="21" y1="10" y2="10" />
+ </svg>
+ Reservations
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/reports"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
+ </svg>
+ Reports
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/admin/feedback"
+ className="flex items-center p-2 rounded-md hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="mr-2"
+ >
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
+ </svg>
+ Feedback
+ </Link>
+ </li>
+ </ul>
+ </nav>
+ </aside>
+
+ {/* Main Content */}
+ <div className="ml-64 flex-1 flex flex-col">
+ {/* Header */}
+ <header className="bg-white dark:bg-slate-800 border-b border-gray-200 dark:border-slate-700 py-4 px-6 flex justify-between items-center">
+ <h1 className="text-xl font-semibold">Admin Dashboard</h1>
+
+ <div className="flex items-center space-x-4">
+ {/* Theme Toggle */}
+ <button onClick={toggleTheme} className="hover:text-primary transition-colors">
+ {theme === 'light' ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <circle cx="12" cy="12" r="4" />
+ <path d="M12 2v2" />
+ <path d="M12 20v2" />
+ <path d="m4.93 4.93 1.41 1.41" />
+ <path d="m17.66 17.66 1.41 1.41" />
+ <path d="M2 12h2" />
+ <path d="M20 12h2" />
+ <path d="m6.34 17.66-1.41 1.41" />
+ <path d="m19.07 4.93-1.41 1.41" />
+ </svg>
+ )}
+ </button>
+
+ {/* User Menu */}
+ <div className="relative group">
+ <button className="flex items-center hover:text-primary transition-colors">
+ <span className="mr-2">{user?.name || 'Admin'}</span>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="m6 9 6 6 6-6" />
+ </svg>
+ </button>
+
+ {/* Dropdown Menu */}
+ <div className="absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-md shadow-lg border border-gray-200 dark:border-slate-700 hidden group-hover:block">
+ <div className="py-1">
+ <Link
+ href="/profile"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ Profile
+ </Link>
+ <Link
+ href="/"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-700"
+ >
+ View Website
+ </Link>
+ <button
+ onClick={logout}
+ className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-700 text-red-600"
+ >
+ Logout
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </header>
+
+ {/* Main Content */}
+ <main className="flex-1 p-6 bg-gray-50 dark:bg-slate-900">{children}</main>
+ </div>
+ </div>
+ );
+};
+
+export default AdminLayout; \ No newline at end of file
diff --git a/frontend/src/components/layouts/MainLayout.js b/frontend/src/components/layouts/MainLayout.js
new file mode 100644
index 0000000..51e0e7e
--- /dev/null
+++ b/frontend/src/components/layouts/MainLayout.js
@@ -0,0 +1,256 @@
+import Link from 'next/link';
+import { useAuth } from '@/context/auth-context';
+import { useCart } from '@/context/cart-context';
+import { useTheme } from '@/context/theme-context';
+
+const MainLayout = ({ children }) => {
+ const { user, isAuthenticated, logout } = useAuth();
+ const { getItemCount } = useCart();
+ const { theme, toggleTheme } = useTheme();
+
+ const cartItemCount = getItemCount();
+
+ return (
+ <div className="flex flex-col min-h-screen">
+ {/* Header */}
+ <header className="bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 sticky top-0 z-10">
+ <div className="container mx-auto px-4 py-3 flex items-center justify-between">
+ {/* Logo */}
+ <Link href="/" className="text-xl font-bold text-primary">
+ Restaurant
+ </Link>
+
+ {/* Navigation Menu */}
+ <nav className="hidden md:flex items-center space-x-6">
+ <Link href="/" className="hover:text-primary transition-colors">
+ Home
+ </Link>
+ <Link href="/menu" className="hover:text-primary transition-colors">
+ Menu
+ </Link>
+ <Link href="/reservations" className="hover:text-primary transition-colors">
+ Reservations
+ </Link>
+ <Link href="/about" className="hover:text-primary transition-colors">
+ About
+ </Link>
+ <Link href="/contact" className="hover:text-primary transition-colors">
+ Contact
+ </Link>
+ </nav>
+
+ {/* User Actions */}
+ <div className="flex items-center space-x-4">
+ {/* Cart Icon */}
+ <Link href="/cart" className="relative">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className="hover:text-primary transition-colors"
+ >
+ <circle cx="8" cy="21" r="1" />
+ <circle cx="19" cy="21" r="1" />
+ <path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12" />
+ </svg>
+ {cartItemCount > 0 && (
+ <span className="absolute -top-2 -right-2 bg-primary text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
+ {cartItemCount}
+ </span>
+ )}
+ </Link>
+
+ {/* Theme Toggle */}
+ <button onClick={toggleTheme} className="hover:text-primary transition-colors">
+ {theme === 'light' ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <circle cx="12" cy="12" r="4" />
+ <path d="M12 2v2" />
+ <path d="M12 20v2" />
+ <path d="m4.93 4.93 1.41 1.41" />
+ <path d="m17.66 17.66 1.41 1.41" />
+ <path d="M2 12h2" />
+ <path d="M20 12h2" />
+ <path d="m6.34 17.66-1.41 1.41" />
+ <path d="m19.07 4.93-1.41 1.41" />
+ </svg>
+ )}
+ </button>
+
+ {/* Auth Actions */}
+ {isAuthenticated ? (
+ <div className="relative group">
+ <button className="flex items-center hover:text-primary transition-colors">
+ <span className="mr-2">{user?.name || 'User'}</span>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="m6 9 6 6 6-6" />
+ </svg>
+ </button>
+
+ {/* Dropdown Menu */}
+ <div className="absolute right-0 mt-2 w-48 bg-white dark:bg-slate-900 rounded-md shadow-lg border border-gray-200 dark:border-slate-700 hidden group-hover:block">
+ <div className="py-1">
+ <Link
+ href="/profile"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-800"
+ >
+ Profile
+ </Link>
+ <Link
+ href="/orders"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-800"
+ >
+ Orders
+ </Link>
+ {user?.role === 'admin' && (
+ <Link
+ href="/admin"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-800"
+ >
+ Admin Dashboard
+ </Link>
+ )}
+ {user?.role === 'staff' && (
+ <Link
+ href="/staff"
+ className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-800"
+ >
+ Staff Dashboard
+ </Link>
+ )}
+ <button
+ onClick={logout}
+ className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-slate-800 text-red-600"
+ >
+ Logout
+ </button>
+ </div>
+ </div>
+ </div>
+ ) : (
+ <div className="flex items-center space-x-4">
+ <Link
+ href="/login"
+ className="hover:text-primary transition-colors"
+ >
+ Login
+ </Link>
+ <Link
+ href="/register"
+ className="bg-primary text-white px-4 py-2 rounded-md hover:bg-primary/90 transition-colors"
+ >
+ Register
+ </Link>
+ </div>
+ )}
+ </div>
+ </div>
+ </header>
+
+ {/* Main Content */}
+ <main className="flex-1 container mx-auto px-4 py-8">{children}</main>
+
+ {/* Footer */}
+ <footer className="bg-white dark:bg-slate-900 border-t border-gray-200 dark:border-slate-700">
+ <div className="container mx-auto px-4 py-8">
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
+ {/* Restaurant Info */}
+ <div>
+ <h3 className="text-lg font-bold mb-4">Restaurant</h3>
+ <p className="mb-2">123 Main Street</p>
+ <p className="mb-2">City, State 12345</p>
+ <p className="mb-2">Phone: (123) 456-7890</p>
+ <p>Email: [email protected]</p>
+ </div>
+
+ {/* Opening Hours */}
+ <div>
+ <h3 className="text-lg font-bold mb-4">Opening Hours</h3>
+ <p className="mb-2">Monday - Friday: 9am - 10pm</p>
+ <p className="mb-2">Saturday: 10am - 11pm</p>
+ <p>Sunday: 10am - 9pm</p>
+ </div>
+
+ {/* Quick Links */}
+ <div>
+ <h3 className="text-lg font-bold mb-4">Quick Links</h3>
+ <ul className="space-y-2">
+ <li>
+ <Link href="/menu" className="hover:text-primary transition-colors">
+ Menu
+ </Link>
+ </li>
+ <li>
+ <Link href="/reservations" className="hover:text-primary transition-colors">
+ Reservations
+ </Link>
+ </li>
+ <li>
+ <Link href="/about" className="hover:text-primary transition-colors">
+ About Us
+ </Link>
+ </li>
+ <li>
+ <Link href="/contact" className="hover:text-primary transition-colors">
+ Contact
+ </Link>
+ </li>
+ <li>
+ <Link href="/privacy" className="hover:text-primary transition-colors">
+ Privacy Policy
+ </Link>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div className="border-t border-gray-200 dark:border-slate-700 mt-8 pt-6 text-center">
+ <p>&copy; {new Date().getFullYear()} Restaurant. All rights reserved.</p>
+ </div>
+ </div>
+ </footer>
+ </div>
+ );
+};
+
+export default MainLayout; \ No newline at end of file
diff --git a/frontend/src/components/ui/button.jsx b/frontend/src/components/ui/button.jsx
new file mode 100644
index 0000000..69ad71f
--- /dev/null
+++ b/frontend/src/components/ui/button.jsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva } from "class-variance-authority";
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ data-slot="button"
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props} />
+ );
+}
+
+export { Button, buttonVariants }
diff --git a/frontend/src/components/ui/form.jsx b/frontend/src/components/ui/form.jsx
new file mode 100644
index 0000000..728ea87
--- /dev/null
+++ b/frontend/src/components/ui/form.jsx
@@ -0,0 +1,144 @@
+"use client";
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { Controller, FormProvider, useFormContext, useFormState } from "react-hook-form";
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+const FormFieldContext = React.createContext({})
+
+const FormField = (
+ {
+ ...props
+ }
+) => {
+ return (
+ <FormFieldContext.Provider value={{ name: props.name }}>
+ <Controller {...props} />
+ </FormFieldContext.Provider>
+ );
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within <FormField>")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+const FormItemContext = React.createContext({})
+
+function FormItem({
+ className,
+ ...props
+}) {
+ const id = React.useId()
+
+ return (
+ <FormItemContext.Provider value={{ id }}>
+ <div data-slot="form-item" className={cn("grid gap-2", className)} {...props} />
+ </FormItemContext.Provider>
+ );
+}
+
+function FormLabel({
+ className,
+ ...props
+}) {
+ const { error, formItemId } = useFormField()
+
+ return (
+ <Label
+ data-slot="form-label"
+ data-error={!!error}
+ className={cn("data-[error=true]:text-destructive", className)}
+ htmlFor={formItemId}
+ {...props} />
+ );
+}
+
+function FormControl({
+ ...props
+}) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+ <Slot
+ data-slot="form-control"
+ id={formItemId}
+ aria-describedby={
+ !error
+ ? `${formDescriptionId}`
+ : `${formDescriptionId} ${formMessageId}`
+ }
+ aria-invalid={!!error}
+ {...props} />
+ );
+}
+
+function FormDescription({
+ className,
+ ...props
+}) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+ <p
+ data-slot="form-description"
+ id={formDescriptionId}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props} />
+ );
+}
+
+function FormMessage({
+ className,
+ ...props
+}) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message ?? "") : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+ <p
+ data-slot="form-message"
+ id={formMessageId}
+ className={cn("text-destructive text-sm", className)}
+ {...props}>
+ {body}
+ </p>
+ );
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/src/components/ui/input.jsx b/frontend/src/components/ui/input.jsx
new file mode 100644
index 0000000..1e9bbd1
--- /dev/null
+++ b/frontend/src/components/ui/input.jsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({
+ className,
+ type,
+ ...props
+}) {
+ return (
+ <input
+ type={type}
+ data-slot="input"
+ className={cn(
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ className
+ )}
+ {...props} />
+ );
+}
+
+export { Input }
diff --git a/frontend/src/components/ui/label.jsx b/frontend/src/components/ui/label.jsx
new file mode 100644
index 0000000..1002a4f
--- /dev/null
+++ b/frontend/src/components/ui/label.jsx
@@ -0,0 +1,23 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}) {
+ return (
+ <LabelPrimitive.Root
+ data-slot="label"
+ className={cn(
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+ className
+ )}
+ {...props} />
+ );
+}
+
+export { Label }
diff --git a/frontend/src/components/ui/sonner.jsx b/frontend/src/components/ui/sonner.jsx
new file mode 100644
index 0000000..8079d58
--- /dev/null
+++ b/frontend/src/components/ui/sonner.jsx
@@ -0,0 +1,26 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner";
+
+const Toaster = ({
+ ...props
+}) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ <Sonner
+ theme={theme}
+ className="toaster group"
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)"
+ }
+ }
+ {...props} />
+ );
+}
+
+export { Toaster }
diff --git a/frontend/src/context/auth-context.js b/frontend/src/context/auth-context.js
new file mode 100644
index 0000000..d67b345
--- /dev/null
+++ b/frontend/src/context/auth-context.js
@@ -0,0 +1,126 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { Sonner, toast } from 'sonner';
+import * as authApi from '@/lib/api/auth';
+
+// Create auth context
+const AuthContext = createContext();
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const router = useRouter();
+
+ // Check if user is logged in on initial load
+ useEffect(() => {
+ const checkAuth = async () => {
+ try {
+ const currentUser = authApi.getCurrentUser();
+ if (currentUser) {
+ setUser(currentUser);
+ }
+ } catch (error) {
+ console.error('Authentication check failed:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ checkAuth();
+ }, []);
+
+ // Login user
+ const login = async (credentials) => {
+ try {
+ setLoading(true);
+ const data = await authApi.login(credentials);
+ setUser(data.user);
+
+ toast.success('Login successful!');
+
+ // Redirect based on role
+ if (data.user.role === 'admin') {
+ router.push('/admin');
+ } else if (data.user.role === 'staff') {
+ router.push('/staff');
+ } else {
+ router.push('/');
+ }
+
+ return data;
+ } catch (error) {
+ toast.error(error.message || 'Login failed');
+ throw error;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Register user
+ const register = async (userData) => {
+ try {
+ setLoading(true);
+ const data = await authApi.register(userData);
+ setUser(data.user);
+
+ toast.success('Registration successful!');
+ router.push('/');
+
+ return data;
+ } catch (error) {
+ toast.error(error.message || 'Registration failed');
+ throw error;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Logout user
+ const logout = () => {
+ authApi.logout();
+ setUser(null);
+ router.push('/login');
+ toast.success('Logged out successfully');
+ };
+
+ // Get user profile
+ const getProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await authApi.getProfile();
+ setUser(data.user);
+ return data.user;
+ } catch (error) {
+ console.error('Failed to get user profile:', error);
+ throw error;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <AuthContext.Provider
+ value={{
+ user,
+ loading,
+ isAuthenticated: !!user,
+ login,
+ register,
+ logout,
+ getProfile,
+ }}
+ >
+ {children}
+ <Sonner position="top-right" />
+ </AuthContext.Provider>
+ );
+};
+
+// Custom hook to use auth context
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}; \ No newline at end of file
diff --git a/frontend/src/context/cart-context.js b/frontend/src/context/cart-context.js
new file mode 100644
index 0000000..8d173b7
--- /dev/null
+++ b/frontend/src/context/cart-context.js
@@ -0,0 +1,120 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+import { toast } from 'sonner';
+
+const CartContext = createContext();
+
+export const CartProvider = ({ children }) => {
+ const [items, setItems] = useState([]);
+ const [total, setTotal] = useState(0);
+
+ // Load cart from localStorage on initial load
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const savedCart = localStorage.getItem('cart');
+ if (savedCart) {
+ try {
+ const parsedCart = JSON.parse(savedCart);
+ setItems(parsedCart);
+ calculateTotal(parsedCart);
+ } catch (error) {
+ console.error('Failed to parse cart from localStorage:', error);
+ }
+ }
+ }
+ }, []);
+
+ // Save cart to localStorage whenever it changes
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('cart', JSON.stringify(items));
+ calculateTotal(items);
+ }
+ }, [items]);
+
+ // Calculate total price
+ const calculateTotal = (cartItems) => {
+ const sum = cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
+ setTotal(sum);
+ };
+
+ // Add item to cart
+ const addItem = (item) => {
+ setItems((prevItems) => {
+ // Check if item already exists in cart
+ const existingItemIndex = prevItems.findIndex((i) => i.id === item.id);
+
+ if (existingItemIndex !== -1) {
+ // Item exists, update quantity
+ const updatedItems = [...prevItems];
+ updatedItems[existingItemIndex] = {
+ ...updatedItems[existingItemIndex],
+ quantity: updatedItems[existingItemIndex].quantity + 1,
+ };
+ toast.success(`Added another ${item.name} to your cart`);
+ return updatedItems;
+ } else {
+ // Item doesn't exist, add new item
+ toast.success(`Added ${item.name} to your cart`);
+ return [...prevItems, { ...item, quantity: 1 }];
+ }
+ });
+ };
+
+ // Remove item from cart
+ const removeItem = (itemId) => {
+ setItems((prevItems) => {
+ const itemToRemove = prevItems.find((item) => item.id === itemId);
+ if (itemToRemove) {
+ toast.success(`Removed ${itemToRemove.name} from your cart`);
+ }
+ return prevItems.filter((item) => item.id !== itemId);
+ });
+ };
+
+ // Update item quantity
+ const updateQuantity = (itemId, quantity) => {
+ if (quantity < 1) return;
+
+ setItems((prevItems) =>
+ prevItems.map((item) =>
+ item.id === itemId ? { ...item, quantity } : item
+ )
+ );
+ };
+
+ // Clear cart
+ const clearCart = () => {
+ setItems([]);
+ toast.success('Cart cleared');
+ };
+
+ // Get cart item count
+ const getItemCount = () => {
+ return items.reduce((count, item) => count + item.quantity, 0);
+ };
+
+ return (
+ <CartContext.Provider
+ value={{
+ items,
+ total,
+ addItem,
+ removeItem,
+ updateQuantity,
+ clearCart,
+ getItemCount,
+ }}
+ >
+ {children}
+ </CartContext.Provider>
+ );
+};
+
+// Custom hook to use cart context
+export const useCart = () => {
+ const context = useContext(CartContext);
+ if (!context) {
+ throw new Error('useCart must be used within a CartProvider');
+ }
+ return context;
+}; \ No newline at end of file
diff --git a/frontend/src/context/theme-context.js b/frontend/src/context/theme-context.js
new file mode 100644
index 0000000..655d1b9
--- /dev/null
+++ b/frontend/src/context/theme-context.js
@@ -0,0 +1,54 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+
+const ThemeContext = createContext();
+
+export const ThemeProvider = ({ children }) => {
+ const [theme, setTheme] = useState('light');
+
+ // Initialize theme from localStorage or system preference
+ useEffect(() => {
+ // Check if we're in a browser environment
+ if (typeof window !== 'undefined') {
+ const savedTheme = localStorage.getItem('theme');
+
+ // Use saved theme or system preference
+ if (savedTheme) {
+ setTheme(savedTheme);
+ document.documentElement.classList.toggle('dark', savedTheme === 'dark');
+ } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ setTheme('dark');
+ document.documentElement.classList.add('dark');
+ }
+ }
+ }, []);
+
+ // Update theme in localStorage and toggle dark class
+ const updateTheme = (newTheme) => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('theme', newTheme);
+ document.documentElement.classList.toggle('dark', newTheme === 'dark');
+ setTheme(newTheme);
+ }
+ };
+
+ // Toggle between light and dark theme
+ const toggleTheme = () => {
+ const newTheme = theme === 'light' ? 'dark' : 'light';
+ updateTheme(newTheme);
+ };
+
+ return (
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
+ {children}
+ </ThemeContext.Provider>
+ );
+};
+
+// Custom hook to use theme context
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+}; \ No newline at end of file
diff --git a/frontend/src/lib/api/auth.js b/frontend/src/lib/api/auth.js
new file mode 100644
index 0000000..85ed28f
--- /dev/null
+++ b/frontend/src/lib/api/auth.js
@@ -0,0 +1,90 @@
+import api from './index';
+
+/**
+ * Register a new user
+ * @param {Object} userData - User registration data
+ * @returns {Promise} - Promise with registration response
+ */
+export const register = async (userData) => {
+ try {
+ const response = await api.post('/auth/register', userData);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Registration failed' };
+ }
+};
+
+/**
+ * Login a user
+ * @param {Object} credentials - User login credentials
+ * @returns {Promise} - Promise with login response
+ */
+export const login = async (credentials) => {
+ try {
+ const response = await api.post('/auth/login', credentials);
+
+ // Store token and user data
+ if (response.data.token) {
+ localStorage.setItem('token', response.data.token);
+ localStorage.setItem('user', JSON.stringify(response.data.user));
+ }
+
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Login failed' };
+ }
+};
+
+/**
+ * Get current user profile
+ * @returns {Promise} - Promise with user profile data
+ */
+export const getProfile = async () => {
+ try {
+ const response = await api.get('/auth/profile');
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to get profile' };
+ }
+};
+
+/**
+ * Logout current user
+ */
+export const logout = () => {
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+
+ // Force reload to clear any state
+ if (typeof window !== 'undefined') {
+ window.location.href = '/login';
+ }
+};
+
+/**
+ * Check if user is authenticated
+ * @returns {Boolean} - True if user is authenticated
+ */
+export const isAuthenticated = () => {
+ if (typeof window === 'undefined') return false;
+
+ const token = localStorage.getItem('token');
+ return !!token;
+};
+
+/**
+ * Get current user data
+ * @returns {Object|null} - User data or null if not authenticated
+ */
+export const getCurrentUser = () => {
+ if (typeof window === 'undefined') return null;
+
+ const userStr = localStorage.getItem('user');
+ if (!userStr) return null;
+
+ try {
+ return JSON.parse(userStr);
+ } catch (e) {
+ return null;
+ }
+}; \ No newline at end of file
diff --git a/frontend/src/lib/api/index.js b/frontend/src/lib/api/index.js
new file mode 100644
index 0000000..dca56e3
--- /dev/null
+++ b/frontend/src/lib/api/index.js
@@ -0,0 +1,40 @@
+import axios from 'axios';
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
+
+const api = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor to add authorization header
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+);
+
+// Response interceptor to handle errors
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ // Handle 401 unauthorized errors
+ if (error.response && error.response.status === 401) {
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ if (typeof window !== 'undefined') {
+ window.location.href = '/login';
+ }
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api; \ No newline at end of file
diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js
new file mode 100644
index 0000000..b20bf01
--- /dev/null
+++ b/frontend/src/lib/utils.js
@@ -0,0 +1,6 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs) {
+ return twMerge(clsx(inputs));
+}