diff options
Diffstat (limited to 'frontend/src')
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 Binary files differnew file mode 100644 index 0000000..718d6fe --- /dev/null +++ b/frontend/src/app/favicon.ico 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>© {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)); +} |