aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-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
9 files changed, 1094 insertions, 0 deletions
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 }