HamzaAri commited on
Commit
e1ef9fc
·
verified ·
1 Parent(s): bd48eef

🚀 Deploy ScanMenu - Production-ready SaaS web app for digital restaurant menus & QR ordering

Browse files
Files changed (44) hide show
  1. .gitignore +41 -0
  2. Dockerfile +42 -0
  3. README.md +36 -10
  4. eslint.config.mjs +18 -0
  5. next.config.ts +7 -0
  6. package-lock.json +0 -0
  7. package.json +52 -0
  8. postcss.config.mjs +7 -0
  9. public/file.svg +1 -0
  10. public/globe.svg +1 -0
  11. public/next.svg +1 -0
  12. public/vercel.svg +1 -0
  13. public/window.svg +1 -0
  14. src/app/(auth)/login/page.tsx +139 -0
  15. src/app/(auth)/register/page.tsx +148 -0
  16. src/app/(dashboard)/analytics/page.tsx +243 -0
  17. src/app/(dashboard)/billing/page.tsx +210 -0
  18. src/app/(dashboard)/menu-builder/page.tsx +308 -0
  19. src/app/(dashboard)/orders/page.tsx +339 -0
  20. src/app/(dashboard)/overview/page.tsx +244 -0
  21. src/app/(dashboard)/qr-manager/page.tsx +226 -0
  22. src/app/(dashboard)/restaurant-setup/page.tsx +200 -0
  23. src/app/(dashboard)/settings/page.tsx +171 -0
  24. src/app/(public)/restaurant/[slug]/page.tsx +436 -0
  25. src/app/favicon.ico +0 -0
  26. src/app/globals.css +65 -0
  27. src/app/layout.tsx +26 -0
  28. src/app/page.tsx +355 -0
  29. src/app/pricing/page.tsx +271 -0
  30. src/components/layout/dashboard-header.tsx +49 -0
  31. src/components/layout/dashboard-layout.tsx +16 -0
  32. src/components/layout/dashboard-sidebar.tsx +131 -0
  33. src/components/ui/badge.tsx +33 -0
  34. src/components/ui/button.tsx +55 -0
  35. src/components/ui/card.tsx +53 -0
  36. src/components/ui/empty-state.tsx +29 -0
  37. src/components/ui/input.tsx +21 -0
  38. src/components/ui/stat-card.tsx +40 -0
  39. src/components/ui/textarea.tsx +20 -0
  40. src/lib/demo-data.ts +227 -0
  41. src/lib/utils.ts +88 -0
  42. src/stores/cart-store.ts +116 -0
  43. src/types/database.ts +201 -0
  44. tsconfig.json +34 -0
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS base
2
+
3
+ # Install dependencies only when needed
4
+ FROM base AS deps
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+
8
+ COPY package.json package-lock.json* ./
9
+ RUN npm ci
10
+
11
+ # Rebuild the source code only when needed
12
+ FROM base AS builder
13
+ WORKDIR /app
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+
17
+ ENV NEXT_TELEMETRY_DISABLED=1
18
+ RUN npm run build
19
+
20
+ # Production image, copy all the files and run next
21
+ FROM base AS runner
22
+ WORKDIR /app
23
+
24
+ ENV NODE_ENV=production
25
+ ENV NEXT_TELEMETRY_DISABLED=1
26
+ ENV PORT=7860
27
+ ENV HOSTNAME="0.0.0.0"
28
+
29
+ RUN addgroup --system --gid 1001 nodejs
30
+ RUN adduser --system --uid 1001 nextjs
31
+
32
+ COPY --from=builder /app/public ./public
33
+
34
+ # Automatically leverage output traces to reduce image size
35
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
36
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
37
+
38
+ USER nextjs
39
+
40
+ EXPOSE 7860
41
+
42
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,10 +1,36 @@
1
- ---
2
- title: ScanMenu
3
- emoji: 🏆
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ };
6
+
7
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "scanmenu",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.11",
13
+ "@radix-ui/react-dialog": "^1.1.15",
14
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
15
+ "@radix-ui/react-label": "^2.1.8",
16
+ "@radix-ui/react-popover": "^1.1.15",
17
+ "@radix-ui/react-select": "^2.2.6",
18
+ "@radix-ui/react-separator": "^1.1.8",
19
+ "@radix-ui/react-slot": "^1.2.4",
20
+ "@radix-ui/react-switch": "^1.2.6",
21
+ "@radix-ui/react-tabs": "^1.1.13",
22
+ "@radix-ui/react-tooltip": "^1.2.8",
23
+ "@stripe/stripe-js": "^9.1.0",
24
+ "@supabase/ssr": "^0.10.2",
25
+ "@supabase/supabase-js": "^2.103.0",
26
+ "class-variance-authority": "^0.7.1",
27
+ "clsx": "^2.1.1",
28
+ "date-fns": "^4.1.0",
29
+ "framer-motion": "^12.38.0",
30
+ "lucide-react": "^1.8.0",
31
+ "next": "16.2.3",
32
+ "qrcode": "^1.5.4",
33
+ "react": "19.2.4",
34
+ "react-dom": "19.2.4",
35
+ "react-hot-toast": "^2.6.0",
36
+ "recharts": "^3.8.1",
37
+ "stripe": "^22.0.1",
38
+ "tailwind-merge": "^3.5.0",
39
+ "zustand": "^5.0.12"
40
+ },
41
+ "devDependencies": {
42
+ "@tailwindcss/postcss": "^4",
43
+ "@types/node": "^20",
44
+ "@types/qrcode": "^1.5.6",
45
+ "@types/react": "^19",
46
+ "@types/react-dom": "^19",
47
+ "eslint": "^9",
48
+ "eslint-config-next": "16.2.3",
49
+ "tailwindcss": "^4",
50
+ "typescript": "^5"
51
+ }
52
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
public/file.svg ADDED
public/globe.svg ADDED
public/next.svg ADDED
public/vercel.svg ADDED
public/window.svg ADDED
src/app/(auth)/login/page.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Sparkles, Mail, Lock, ArrowRight, Globe } from 'lucide-react';
8
+ import { useRouter } from 'next/navigation';
9
+
10
+ export default function LoginPage() {
11
+ const [email, setEmail] = useState('demo@scanmenu.app');
12
+ const [password, setPassword] = useState('demo1234');
13
+ const [loading, setLoading] = useState(false);
14
+ const router = useRouter();
15
+
16
+ const handleLogin = (e: React.FormEvent) => {
17
+ e.preventDefault();
18
+ setLoading(true);
19
+ setTimeout(() => {
20
+ router.push('/dashboard/overview');
21
+ }, 800);
22
+ };
23
+
24
+ return (
25
+ <div className="flex min-h-screen">
26
+ {/* Left Panel — Branding */}
27
+ <div className="hidden w-1/2 flex-col justify-between bg-zinc-950 p-12 lg:flex">
28
+ <div className="flex items-center gap-2.5">
29
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500">
30
+ <Sparkles className="h-5 w-5 text-white" />
31
+ </div>
32
+ <span className="text-xl font-bold text-white">
33
+ Scan<span className="text-emerald-400">Menu</span>
34
+ </span>
35
+ </div>
36
+
37
+ <div className="space-y-6">
38
+ <blockquote className="space-y-3">
39
+ <p className="text-2xl font-medium leading-relaxed text-white/90">
40
+ &ldquo;ScanMenu transformed our restaurant. Orders are faster, errors dropped to zero, and our revenue increased 32% in just 3 months.&rdquo;
41
+ </p>
42
+ <footer className="flex items-center gap-3">
43
+ <div className="h-10 w-10 rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500" />
44
+ <div>
45
+ <p className="text-sm font-semibold text-white">Maria Santos</p>
46
+ <p className="text-sm text-zinc-400">Owner, Bella Cucina</p>
47
+ </div>
48
+ </footer>
49
+ </blockquote>
50
+ </div>
51
+
52
+ <p className="text-sm text-zinc-500">&copy; 2025 ScanMenu. All rights reserved.</p>
53
+ </div>
54
+
55
+ {/* Right Panel — Form */}
56
+ <div className="flex w-full items-center justify-center px-4 lg:w-1/2">
57
+ <div className="w-full max-w-[400px] space-y-8">
58
+ <div className="lg:hidden flex items-center gap-2.5 justify-center mb-6">
59
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500">
60
+ <Sparkles className="h-5 w-5 text-white" />
61
+ </div>
62
+ <span className="text-xl font-bold text-zinc-900">
63
+ Scan<span className="text-emerald-600">Menu</span>
64
+ </span>
65
+ </div>
66
+
67
+ <div className="text-center lg:text-left">
68
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900">Welcome back</h1>
69
+ <p className="mt-2 text-sm text-zinc-500">Sign in to your account to continue</p>
70
+ </div>
71
+
72
+ <form onSubmit={handleLogin} className="space-y-4">
73
+ <div className="space-y-2">
74
+ <label className="text-sm font-medium text-zinc-700">Email</label>
75
+ <div className="relative">
76
+ <Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
77
+ <Input
78
+ type="email"
79
+ placeholder="name@company.com"
80
+ value={email}
81
+ onChange={(e) => setEmail(e.target.value)}
82
+ className="pl-10"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div className="space-y-2">
88
+ <div className="flex items-center justify-between">
89
+ <label className="text-sm font-medium text-zinc-700">Password</label>
90
+ <button type="button" className="text-xs font-medium text-emerald-600 hover:text-emerald-500">
91
+ Forgot password?
92
+ </button>
93
+ </div>
94
+ <div className="relative">
95
+ <Lock className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
96
+ <Input
97
+ type="password"
98
+ placeholder="••••••••"
99
+ value={password}
100
+ onChange={(e) => setPassword(e.target.value)}
101
+ className="pl-10"
102
+ />
103
+ </div>
104
+ </div>
105
+
106
+ <Button type="submit" variant="primary" size="lg" className="w-full" disabled={loading}>
107
+ {loading ? (
108
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
109
+ ) : (
110
+ <>Sign in <ArrowRight className="h-4 w-4" /></>
111
+ )}
112
+ </Button>
113
+ </form>
114
+
115
+ <div className="relative">
116
+ <div className="absolute inset-0 flex items-center">
117
+ <div className="w-full border-t border-zinc-200" />
118
+ </div>
119
+ <div className="relative flex justify-center text-xs">
120
+ <span className="bg-white px-4 text-zinc-400">or continue with</span>
121
+ </div>
122
+ </div>
123
+
124
+ <Button variant="outline" size="lg" className="w-full">
125
+ <Globe className="h-4 w-4" />
126
+ Google
127
+ </Button>
128
+
129
+ <p className="text-center text-sm text-zinc-500">
130
+ Don&apos;t have an account?{' '}
131
+ <Link href="/register" className="font-semibold text-emerald-600 hover:text-emerald-500">
132
+ Sign up free
133
+ </Link>
134
+ </p>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
src/app/(auth)/register/page.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Sparkles, Mail, Lock, ArrowRight, Globe, User, Store } from 'lucide-react';
8
+ import { useRouter } from 'next/navigation';
9
+
10
+ export default function RegisterPage() {
11
+ const [loading, setLoading] = useState(false);
12
+ const router = useRouter();
13
+
14
+ const handleRegister = (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setLoading(true);
17
+ setTimeout(() => {
18
+ router.push('/dashboard/overview');
19
+ }, 800);
20
+ };
21
+
22
+ return (
23
+ <div className="flex min-h-screen">
24
+ {/* Left Panel */}
25
+ <div className="hidden w-1/2 flex-col justify-between bg-zinc-950 p-12 lg:flex">
26
+ <div className="flex items-center gap-2.5">
27
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500">
28
+ <Sparkles className="h-5 w-5 text-white" />
29
+ </div>
30
+ <span className="text-xl font-bold text-white">
31
+ Scan<span className="text-emerald-400">Menu</span>
32
+ </span>
33
+ </div>
34
+
35
+ <div className="space-y-8">
36
+ <h2 className="text-3xl font-bold leading-tight text-white">
37
+ Launch your digital menu in under 5 minutes
38
+ </h2>
39
+ <div className="space-y-4">
40
+ {[
41
+ 'Create your restaurant profile',
42
+ 'Build beautiful menus with drag & drop',
43
+ 'Generate QR codes for every table',
44
+ 'Start receiving orders instantly',
45
+ ].map((step, i) => (
46
+ <div key={i} className="flex items-center gap-3">
47
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/20 text-sm font-bold text-emerald-400">
48
+ {i + 1}
49
+ </div>
50
+ <p className="text-sm text-zinc-300">{step}</p>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+
56
+ <p className="text-sm text-zinc-500">&copy; 2025 ScanMenu. All rights reserved.</p>
57
+ </div>
58
+
59
+ {/* Right Panel */}
60
+ <div className="flex w-full items-center justify-center px-4 lg:w-1/2">
61
+ <div className="w-full max-w-[400px] space-y-8">
62
+ <div className="lg:hidden flex items-center gap-2.5 justify-center mb-6">
63
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500">
64
+ <Sparkles className="h-5 w-5 text-white" />
65
+ </div>
66
+ <span className="text-xl font-bold text-zinc-900">
67
+ Scan<span className="text-emerald-600">Menu</span>
68
+ </span>
69
+ </div>
70
+
71
+ <div className="text-center lg:text-left">
72
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900">Create your account</h1>
73
+ <p className="mt-2 text-sm text-zinc-500">Start your 14-day free trial. No credit card required.</p>
74
+ </div>
75
+
76
+ <form onSubmit={handleRegister} className="space-y-4">
77
+ <div className="grid grid-cols-2 gap-3">
78
+ <div className="space-y-2">
79
+ <label className="text-sm font-medium text-zinc-700">First Name</label>
80
+ <div className="relative">
81
+ <User className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
82
+ <Input placeholder="Alex" className="pl-10" />
83
+ </div>
84
+ </div>
85
+ <div className="space-y-2">
86
+ <label className="text-sm font-medium text-zinc-700">Last Name</label>
87
+ <Input placeholder="Rivera" />
88
+ </div>
89
+ </div>
90
+
91
+ <div className="space-y-2">
92
+ <label className="text-sm font-medium text-zinc-700">Restaurant Name</label>
93
+ <div className="relative">
94
+ <Store className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
95
+ <Input placeholder="The Garden Kitchen" className="pl-10" />
96
+ </div>
97
+ </div>
98
+
99
+ <div className="space-y-2">
100
+ <label className="text-sm font-medium text-zinc-700">Email</label>
101
+ <div className="relative">
102
+ <Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
103
+ <Input type="email" placeholder="name@company.com" className="pl-10" />
104
+ </div>
105
+ </div>
106
+
107
+ <div className="space-y-2">
108
+ <label className="text-sm font-medium text-zinc-700">Password</label>
109
+ <div className="relative">
110
+ <Lock className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
111
+ <Input type="password" placeholder="Min. 8 characters" className="pl-10" />
112
+ </div>
113
+ </div>
114
+
115
+ <Button type="submit" variant="primary" size="lg" className="w-full" disabled={loading}>
116
+ {loading ? (
117
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
118
+ ) : (
119
+ <>Create Account <ArrowRight className="h-4 w-4" /></>
120
+ )}
121
+ </Button>
122
+ </form>
123
+
124
+ <div className="relative">
125
+ <div className="absolute inset-0 flex items-center">
126
+ <div className="w-full border-t border-zinc-200" />
127
+ </div>
128
+ <div className="relative flex justify-center text-xs">
129
+ <span className="bg-white px-4 text-zinc-400">or continue with</span>
130
+ </div>
131
+ </div>
132
+
133
+ <Button variant="outline" size="lg" className="w-full">
134
+ <Globe className="h-4 w-4" />
135
+ Google
136
+ </Button>
137
+
138
+ <p className="text-center text-sm text-zinc-500">
139
+ Already have an account?{' '}
140
+ <Link href="/login" className="font-semibold text-emerald-600 hover:text-emerald-500">
141
+ Sign in
142
+ </Link>
143
+ </p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
src/app/(dashboard)/analytics/page.tsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { StatCard } from '@/components/ui/stat-card';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Button } from '@/components/ui/button';
8
+ import {
9
+ DollarSign,
10
+ ShoppingBag,
11
+ TrendingUp,
12
+ Users,
13
+ ArrowUpRight,
14
+ ArrowDownRight,
15
+ QrCode,
16
+ Calendar,
17
+ Download,
18
+ } from 'lucide-react';
19
+ import { generateDailyRevenue, generatePopularItems, generateOrdersByType, generateHourlyOrders } from '@/lib/demo-data';
20
+ import { formatCurrency, cn } from '@/lib/utils';
21
+ import { useState } from 'react';
22
+
23
+ const revenueData = generateDailyRevenue(30);
24
+ const popularItems = generatePopularItems();
25
+ const ordersByType = generateOrdersByType();
26
+ const hourlyOrders = generateHourlyOrders();
27
+
28
+ function BarChart({ data, dataKey, maxHeight = 120 }: { data: { [key: string]: string | number }[]; dataKey: string; maxHeight?: number }) {
29
+ const values = data.map((d) => Number(d[dataKey]));
30
+ const max = Math.max(...values);
31
+
32
+ return (
33
+ <div className="flex items-end gap-[3px]" style={{ height: maxHeight }}>
34
+ {data.map((d, i) => (
35
+ <div
36
+ key={i}
37
+ className="group relative flex-1 rounded-t-sm bg-emerald-500/80 transition-all hover:bg-emerald-500"
38
+ style={{ height: `${(Number(d[dataKey]) / max) * 100}%` }}
39
+ >
40
+ <div className="absolute -top-8 left-1/2 -translate-x-1/2 hidden rounded-md bg-zinc-900 px-2 py-1 text-[10px] font-medium text-white shadow-lg group-hover:block whitespace-nowrap">
41
+ {typeof d[dataKey] === 'number' && dataKey === 'revenue' ? formatCurrency(Number(d[dataKey])) : d[dataKey]}
42
+ </div>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ function HeatmapRow({ label, values, max }: { label: string; values: number[]; max: number }) {
50
+ return (
51
+ <div className="flex items-center gap-1">
52
+ <span className="w-10 text-[10px] text-zinc-400 shrink-0">{label}</span>
53
+ {values.map((v, i) => (
54
+ <div
55
+ key={i}
56
+ className="h-6 flex-1 rounded-sm transition-all hover:ring-1 hover:ring-emerald-500"
57
+ style={{
58
+ backgroundColor: `rgba(16, 185, 129, ${v / max})`,
59
+ }}
60
+ title={`${v} orders`}
61
+ />
62
+ ))}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export default function AnalyticsPage() {
68
+ const [period, setPeriod] = useState('30d');
69
+ const totalRevenue = revenueData.reduce((s, d) => s + d.revenue, 0);
70
+ const totalOrders = revenueData.reduce((s, d) => s + d.orders, 0);
71
+ const avgOrder = totalRevenue / totalOrders;
72
+
73
+ return (
74
+ <DashboardLayout>
75
+ <div className="space-y-6">
76
+ {/* Header */}
77
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
78
+ <div>
79
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Analytics</h1>
80
+ <p className="mt-1 text-sm text-zinc-500">Track performance, revenue, and customer insights.</p>
81
+ </div>
82
+ <div className="flex items-center gap-2">
83
+ <div className="flex gap-1 rounded-xl border border-zinc-200 p-1 dark:border-zinc-700">
84
+ {['7d', '14d', '30d', '90d'].map((p) => (
85
+ <button
86
+ key={p}
87
+ onClick={() => setPeriod(p)}
88
+ className={cn(
89
+ 'rounded-lg px-3 py-1.5 text-xs font-medium transition-all',
90
+ period === p ? 'bg-zinc-900 text-white dark:bg-white dark:text-zinc-900' : 'text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800'
91
+ )}
92
+ >
93
+ {p}
94
+ </button>
95
+ ))}
96
+ </div>
97
+ <Button variant="outline" size="sm">
98
+ <Download className="h-4 w-4" /> Export
99
+ </Button>
100
+ </div>
101
+ </div>
102
+
103
+ {/* Stats */}
104
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
105
+ <StatCard title="Total Revenue" value={formatCurrency(totalRevenue)} change="+12.5% vs prev. period" changeType="positive" icon={DollarSign} iconColor="text-emerald-600" />
106
+ <StatCard title="Total Orders" value={totalOrders.toString()} change="+8.2% vs prev. period" changeType="positive" icon={ShoppingBag} iconColor="text-blue-600" />
107
+ <StatCard title="Avg. Order Value" value={formatCurrency(avgOrder)} change="+3.1% vs prev. period" changeType="positive" icon={TrendingUp} iconColor="text-violet-600" />
108
+ <StatCard title="QR Scans" value="2,011" change="+24.3% vs prev. period" changeType="positive" icon={QrCode} iconColor="text-amber-600" />
109
+ </div>
110
+
111
+ {/* Revenue Chart */}
112
+ <Card>
113
+ <CardHeader className="flex flex-row items-center justify-between">
114
+ <div>
115
+ <CardTitle>Revenue Over Time</CardTitle>
116
+ <p className="mt-1 text-sm text-zinc-500">Daily revenue for the last 30 days</p>
117
+ </div>
118
+ <div className="flex items-baseline gap-3">
119
+ <span className="text-2xl font-bold">{formatCurrency(totalRevenue)}</span>
120
+ <Badge variant="success" className="gap-1">
121
+ <ArrowUpRight className="h-3 w-3" /> 12.5%
122
+ </Badge>
123
+ </div>
124
+ </CardHeader>
125
+ <CardContent>
126
+ <BarChart data={revenueData} dataKey="revenue" maxHeight={160} />
127
+ <div className="mt-2 flex justify-between text-[10px] text-zinc-400">
128
+ {revenueData.filter((_, i) => i % 5 === 0).map((d, i) => (
129
+ <span key={i}>{new Date(d.date).toLocaleDateString('en', { month: 'short', day: 'numeric' })}</span>
130
+ ))}
131
+ </div>
132
+ </CardContent>
133
+ </Card>
134
+
135
+ <div className="grid gap-6 lg:grid-cols-2">
136
+ {/* Popular Items */}
137
+ <Card>
138
+ <CardHeader>
139
+ <CardTitle>Top Selling Items</CardTitle>
140
+ </CardHeader>
141
+ <CardContent>
142
+ <div className="space-y-3">
143
+ {popularItems.map((item, i) => {
144
+ const maxOrders = popularItems[0].orders;
145
+ return (
146
+ <div key={item.name} className="flex items-center gap-3">
147
+ <span className="w-6 text-center text-sm font-bold text-zinc-400">
148
+ {i + 1}
149
+ </span>
150
+ <div className="flex-1 min-w-0">
151
+ <div className="flex items-center justify-between">
152
+ <p className="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{item.name}</p>
153
+ <p className="ml-2 shrink-0 text-sm font-semibold text-zinc-900 dark:text-zinc-100">{formatCurrency(item.revenue)}</p>
154
+ </div>
155
+ <div className="mt-1.5 flex items-center gap-2">
156
+ <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
157
+ <div
158
+ className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-cyan-500"
159
+ style={{ width: `${(item.orders / maxOrders) * 100}%` }}
160
+ />
161
+ </div>
162
+ <span className="text-xs text-zinc-400">{item.orders}</span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ );
167
+ })}
168
+ </div>
169
+ </CardContent>
170
+ </Card>
171
+
172
+ {/* Peak Hours */}
173
+ <Card>
174
+ <CardHeader>
175
+ <CardTitle>Peak Hours</CardTitle>
176
+ </CardHeader>
177
+ <CardContent>
178
+ <BarChart data={hourlyOrders} dataKey="orders" maxHeight={140} />
179
+ <div className="mt-2 flex justify-between text-[10px] text-zinc-400">
180
+ {hourlyOrders.map((h, i) => (
181
+ i % 2 === 0 ? <span key={i}>{h.hour}</span> : <span key={i} />
182
+ ))}
183
+ </div>
184
+ <div className="mt-4 grid grid-cols-2 gap-3">
185
+ <div className="rounded-xl bg-emerald-50 p-3 dark:bg-emerald-950/30">
186
+ <p className="text-xs text-emerald-600">Lunch Peak</p>
187
+ <p className="text-lg font-bold text-emerald-700 dark:text-emerald-400">12:00 – 14:00</p>
188
+ </div>
189
+ <div className="rounded-xl bg-violet-50 p-3 dark:bg-violet-950/30">
190
+ <p className="text-xs text-violet-600">Dinner Peak</p>
191
+ <p className="text-lg font-bold text-violet-700 dark:text-violet-400">18:00 – 21:00</p>
192
+ </div>
193
+ </div>
194
+ </CardContent>
195
+ </Card>
196
+ </div>
197
+
198
+ {/* Order Distribution */}
199
+ <div className="grid gap-6 lg:grid-cols-3">
200
+ {ordersByType.map((item) => (
201
+ <Card key={item.type}>
202
+ <CardContent className="pt-6">
203
+ <div className="flex items-center justify-between">
204
+ <div>
205
+ <p className="text-sm text-zinc-500">{item.type}</p>
206
+ <p className="mt-1 text-3xl font-bold">{item.count}</p>
207
+ <p className="mt-0.5 text-xs text-zinc-500">{item.percentage}% of total</p>
208
+ </div>
209
+ <div className="relative h-20 w-20">
210
+ <svg className="h-20 w-20 -rotate-90" viewBox="0 0 36 36">
211
+ <path
212
+ d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
213
+ fill="none"
214
+ stroke="currentColor"
215
+ strokeWidth="3"
216
+ className="text-zinc-100 dark:text-zinc-800"
217
+ />
218
+ <path
219
+ d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
220
+ fill="none"
221
+ stroke="currentColor"
222
+ strokeWidth="3"
223
+ strokeDasharray={`${item.percentage}, 100`}
224
+ className={cn(
225
+ item.type === 'Dine In' && 'text-emerald-500',
226
+ item.type === 'Takeaway' && 'text-blue-500',
227
+ item.type === 'Delivery' && 'text-violet-500'
228
+ )}
229
+ />
230
+ </svg>
231
+ <div className="absolute inset-0 flex items-center justify-center text-sm font-bold">
232
+ {item.percentage}%
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </CardContent>
237
+ </Card>
238
+ ))}
239
+ </div>
240
+ </div>
241
+ </DashboardLayout>
242
+ );
243
+ }
src/app/(dashboard)/billing/page.tsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Check, Sparkles, CreditCard, Download, ArrowRight, Zap, Crown, Building2 } from 'lucide-react';
8
+ import { cn } from '@/lib/utils';
9
+ import { useState } from 'react';
10
+
11
+ const plans = [
12
+ {
13
+ name: 'Starter',
14
+ price: 29,
15
+ yearlyPrice: 24,
16
+ description: 'Perfect for small restaurants just getting started.',
17
+ icon: Zap,
18
+ features: ['1 restaurant', 'Up to 50 menu items', '5 QR codes', 'Basic analytics', 'Email support', '100 orders/month'],
19
+ popular: false,
20
+ current: false,
21
+ },
22
+ {
23
+ name: 'Pro',
24
+ price: 79,
25
+ yearlyPrice: 66,
26
+ description: 'For growing restaurants with advanced needs.',
27
+ icon: Crown,
28
+ features: ['Unlimited restaurants', 'Unlimited menu items', 'Unlimited QR codes', 'Advanced analytics', 'Priority support', 'Unlimited orders', 'Custom branding', 'API access', 'Team members'],
29
+ popular: true,
30
+ current: true,
31
+ },
32
+ {
33
+ name: 'Enterprise',
34
+ price: 199,
35
+ yearlyPrice: 166,
36
+ description: 'For multi-location chains and franchises.',
37
+ icon: Building2,
38
+ features: ['Everything in Pro', 'Multi-location support', 'Dedicated account manager', 'Custom integrations', 'SLA guarantee', 'White-label solution', 'Advanced security', 'Onboarding training'],
39
+ popular: false,
40
+ current: false,
41
+ },
42
+ ];
43
+
44
+ const invoices = [
45
+ { id: 'INV-2024-003', date: 'Mar 1, 2025', amount: 79, status: 'paid' },
46
+ { id: 'INV-2024-002', date: 'Feb 1, 2025', amount: 79, status: 'paid' },
47
+ { id: 'INV-2024-001', date: 'Jan 1, 2025', amount: 79, status: 'paid' },
48
+ ];
49
+
50
+ export default function BillingPage() {
51
+ const [annual, setAnnual] = useState(false);
52
+
53
+ return (
54
+ <DashboardLayout>
55
+ <div className="space-y-8">
56
+ {/* Header */}
57
+ <div>
58
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Billing & Subscription</h1>
59
+ <p className="mt-1 text-sm text-zinc-500">Manage your plan, payment methods, and invoices.</p>
60
+ </div>
61
+
62
+ {/* Current Plan Banner */}
63
+ <div className="relative overflow-hidden rounded-2xl bg-gradient-to-r from-emerald-600 via-emerald-500 to-cyan-500 p-6 text-white shadow-lg sm:p-8">
64
+ <div className="relative z-10">
65
+ <Badge className="bg-white/20 text-white border-white/30 mb-3">Current Plan</Badge>
66
+ <h2 className="text-2xl font-bold">Pro Plan</h2>
67
+ <p className="mt-1 text-emerald-100">Your next billing date is April 1, 2025</p>
68
+ <div className="mt-4 flex items-baseline gap-1">
69
+ <span className="text-4xl font-bold">$79</span>
70
+ <span className="text-emerald-200">/month</span>
71
+ </div>
72
+ </div>
73
+ <div className="absolute -right-8 -top-8 h-40 w-40 rounded-full bg-white/10" />
74
+ <div className="absolute -bottom-12 -right-12 h-48 w-48 rounded-full bg-white/5" />
75
+ </div>
76
+
77
+ {/* Billing Toggle */}
78
+ <div className="flex items-center justify-center gap-3">
79
+ <span className={cn('text-sm font-medium', !annual ? 'text-zinc-900' : 'text-zinc-500')}>Monthly</span>
80
+ <button
81
+ onClick={() => setAnnual(!annual)}
82
+ className={cn(
83
+ 'relative h-6 w-11 rounded-full transition-colors',
84
+ annual ? 'bg-emerald-500' : 'bg-zinc-300 dark:bg-zinc-600'
85
+ )}
86
+ >
87
+ <span className={cn(
88
+ 'absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform',
89
+ annual && 'translate-x-5'
90
+ )} />
91
+ </button>
92
+ <span className={cn('text-sm font-medium', annual ? 'text-zinc-900' : 'text-zinc-500')}>
93
+ Annual <Badge variant="success" className="ml-1 text-[10px]">Save 17%</Badge>
94
+ </span>
95
+ </div>
96
+
97
+ {/* Plans */}
98
+ <div className="grid gap-6 lg:grid-cols-3">
99
+ {plans.map((plan) => (
100
+ <Card key={plan.name} className={cn(
101
+ 'relative overflow-hidden transition-all hover:shadow-lg',
102
+ plan.popular && 'border-emerald-500 shadow-md ring-1 ring-emerald-500/20',
103
+ plan.current && 'border-emerald-500'
104
+ )}>
105
+ {plan.popular && (
106
+ <div className="absolute -right-8 top-6 rotate-45 bg-emerald-500 px-10 py-1 text-[10px] font-bold uppercase tracking-wider text-white shadow-sm">
107
+ Popular
108
+ </div>
109
+ )}
110
+ <CardContent className="pt-6 space-y-6">
111
+ <div>
112
+ <div className="flex items-center gap-2">
113
+ <div className={cn(
114
+ 'flex h-10 w-10 items-center justify-center rounded-xl',
115
+ plan.popular ? 'bg-emerald-100 text-emerald-600' : 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800'
116
+ )}>
117
+ <plan.icon className="h-5 w-5" />
118
+ </div>
119
+ <div>
120
+ <h3 className="text-lg font-bold text-zinc-900 dark:text-zinc-100">{plan.name}</h3>
121
+ </div>
122
+ </div>
123
+ <p className="mt-2 text-sm text-zinc-500">{plan.description}</p>
124
+ </div>
125
+
126
+ <div className="flex items-baseline gap-1">
127
+ <span className="text-4xl font-bold text-zinc-900 dark:text-zinc-100">
128
+ ${annual ? plan.yearlyPrice : plan.price}
129
+ </span>
130
+ <span className="text-zinc-500">/month</span>
131
+ </div>
132
+
133
+ <Button
134
+ variant={plan.current ? 'outline' : plan.popular ? 'primary' : 'default'}
135
+ className="w-full"
136
+ disabled={plan.current}
137
+ >
138
+ {plan.current ? 'Current Plan' : 'Upgrade'}
139
+ {!plan.current && <ArrowRight className="h-4 w-4" />}
140
+ </Button>
141
+
142
+ <div className="space-y-3">
143
+ {plan.features.map((feature) => (
144
+ <div key={feature} className="flex items-center gap-2 text-sm">
145
+ <Check className={cn(
146
+ 'h-4 w-4 shrink-0',
147
+ plan.popular ? 'text-emerald-500' : 'text-zinc-400'
148
+ )} />
149
+ <span className="text-zinc-600 dark:text-zinc-400">{feature}</span>
150
+ </div>
151
+ ))}
152
+ </div>
153
+ </CardContent>
154
+ </Card>
155
+ ))}
156
+ </div>
157
+
158
+ {/* Payment Method & Invoices */}
159
+ <div className="grid gap-6 lg:grid-cols-2">
160
+ {/* Payment Method */}
161
+ <Card>
162
+ <CardHeader className="flex flex-row items-center justify-between">
163
+ <CardTitle>Payment Method</CardTitle>
164
+ <Button variant="outline" size="sm">Update</Button>
165
+ </CardHeader>
166
+ <CardContent>
167
+ <div className="flex items-center gap-4 rounded-xl border border-zinc-200 p-4 dark:border-zinc-700">
168
+ <div className="flex h-12 w-16 items-center justify-center rounded-lg bg-gradient-to-br from-blue-600 to-blue-800">
169
+ <CreditCard className="h-6 w-6 text-white" />
170
+ </div>
171
+ <div>
172
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">Visa ending in 4242</p>
173
+ <p className="text-xs text-zinc-500">Expires 12/2026</p>
174
+ </div>
175
+ <Badge variant="success" className="ml-auto text-[10px]">Default</Badge>
176
+ </div>
177
+ </CardContent>
178
+ </Card>
179
+
180
+ {/* Recent Invoices */}
181
+ <Card>
182
+ <CardHeader className="flex flex-row items-center justify-between">
183
+ <CardTitle>Recent Invoices</CardTitle>
184
+ <Button variant="ghost" size="sm" className="text-xs">View All</Button>
185
+ </CardHeader>
186
+ <CardContent>
187
+ <div className="space-y-3">
188
+ {invoices.map((invoice) => (
189
+ <div key={invoice.id} className="flex items-center justify-between rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
190
+ <div>
191
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{invoice.id}</p>
192
+ <p className="text-xs text-zinc-500">{invoice.date}</p>
193
+ </div>
194
+ <div className="flex items-center gap-3">
195
+ <span className="text-sm font-semibold">${invoice.amount}</span>
196
+ <Badge variant="success" className="text-[10px]">{invoice.status}</Badge>
197
+ <Button variant="ghost" size="icon-sm">
198
+ <Download className="h-3.5 w-3.5" />
199
+ </Button>
200
+ </div>
201
+ </div>
202
+ ))}
203
+ </div>
204
+ </CardContent>
205
+ </Card>
206
+ </div>
207
+ </div>
208
+ </DashboardLayout>
209
+ );
210
+ }
src/app/(dashboard)/menu-builder/page.tsx ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Textarea } from '@/components/ui/textarea';
9
+ import {
10
+ Plus,
11
+ Search,
12
+ MoreHorizontal,
13
+ GripVertical,
14
+ Pencil,
15
+ Trash2,
16
+ Eye,
17
+ EyeOff,
18
+ Star,
19
+ Clock,
20
+ Flame,
21
+ ChevronDown,
22
+ ChevronRight,
23
+ X,
24
+ Image as ImageIcon,
25
+ DollarSign,
26
+ Tag,
27
+ AlertCircle,
28
+ } from 'lucide-react';
29
+ import { useState } from 'react';
30
+ import { demoCategories, demoProducts } from '@/lib/demo-data';
31
+ import { formatCurrency, cn } from '@/lib/utils';
32
+ import { Product, Category } from '@/types/database';
33
+
34
+ function ProductCard({ product, onEdit }: { product: Product; onEdit: () => void }) {
35
+ return (
36
+ <div className="group flex items-start gap-3 rounded-xl border border-zinc-100 bg-white p-3 transition-all hover:border-zinc-200 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700">
37
+ <button className="mt-1 cursor-grab text-zinc-300 opacity-0 transition-opacity group-hover:opacity-100 active:cursor-grabbing">
38
+ <GripVertical className="h-4 w-4" />
39
+ </button>
40
+
41
+ {/* Image placeholder */}
42
+ <div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-zinc-100 to-zinc-50 dark:from-zinc-800 dark:to-zinc-700">
43
+ <ImageIcon className="h-6 w-6 text-zinc-300 dark:text-zinc-500" />
44
+ </div>
45
+
46
+ <div className="flex-1 min-w-0">
47
+ <div className="flex items-start justify-between gap-2">
48
+ <div className="min-w-0">
49
+ <div className="flex items-center gap-2">
50
+ <h4 className="truncate text-sm font-semibold text-zinc-900 dark:text-zinc-100">{product.name}</h4>
51
+ {product.is_featured && <Star className="h-3.5 w-3.5 shrink-0 fill-amber-400 text-amber-400" />}
52
+ </div>
53
+ <p className="mt-0.5 line-clamp-1 text-xs text-zinc-500">{product.description}</p>
54
+ </div>
55
+ <div className="flex items-center gap-1 shrink-0">
56
+ <span className="text-sm font-bold text-zinc-900 dark:text-zinc-100">{formatCurrency(product.price)}</span>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
61
+ {product.is_available ? (
62
+ <Badge variant="success" className="text-[10px]"><Eye className="h-3 w-3 mr-0.5" />Available</Badge>
63
+ ) : (
64
+ <Badge variant="destructive" className="text-[10px]"><EyeOff className="h-3 w-3 mr-0.5" />Hidden</Badge>
65
+ )}
66
+ {product.preparation_time && (
67
+ <Badge variant="secondary" className="text-[10px]"><Clock className="h-3 w-3 mr-0.5" />{product.preparation_time}m</Badge>
68
+ )}
69
+ {product.calories && (
70
+ <Badge variant="secondary" className="text-[10px]"><Flame className="h-3 w-3 mr-0.5" />{product.calories} cal</Badge>
71
+ )}
72
+ {product.tags?.map((tag) => (
73
+ <Badge key={tag} variant="outline" className="text-[10px]">{tag}</Badge>
74
+ ))}
75
+ </div>
76
+ </div>
77
+
78
+ <div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
79
+ <button onClick={onEdit} className="rounded-lg p-1.5 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800">
80
+ <Pencil className="h-3.5 w-3.5" />
81
+ </button>
82
+ <button className="rounded-lg p-1.5 text-zinc-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950/30">
83
+ <Trash2 className="h-3.5 w-3.5" />
84
+ </button>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ function ProductModal({ product, onClose }: { product?: Product; onClose: () => void }) {
91
+ return (
92
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
93
+ <div className="w-full max-w-lg max-h-[90vh] overflow-y-auto rounded-2xl border border-zinc-200 bg-white shadow-xl dark:border-zinc-700 dark:bg-zinc-900">
94
+ <div className="flex items-center justify-between border-b border-zinc-200 p-5 dark:border-zinc-800">
95
+ <h3 className="text-lg font-semibold">{product ? 'Edit Product' : 'Add Product'}</h3>
96
+ <button onClick={onClose} className="rounded-lg p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">
97
+ <X className="h-4 w-4" />
98
+ </button>
99
+ </div>
100
+ <div className="space-y-4 p-5">
101
+ {/* Image upload */}
102
+ <div className="flex items-center justify-center rounded-xl border-2 border-dashed border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-800/50">
103
+ <div className="text-center">
104
+ <ImageIcon className="mx-auto h-8 w-8 text-zinc-300" />
105
+ <p className="mt-2 text-sm font-medium text-zinc-600">Upload product image</p>
106
+ <p className="text-xs text-zinc-400">PNG, JPG up to 5MB</p>
107
+ <Button variant="outline" size="sm" className="mt-3">Choose File</Button>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="space-y-2">
112
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Product Name</label>
113
+ <Input placeholder="e.g., Truffle Burrata" defaultValue={product?.name} />
114
+ </div>
115
+
116
+ <div className="space-y-2">
117
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Description</label>
118
+ <Textarea placeholder="Describe your product..." defaultValue={product?.description} />
119
+ </div>
120
+
121
+ <div className="grid grid-cols-2 gap-3">
122
+ <div className="space-y-2">
123
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Price</label>
124
+ <div className="relative">
125
+ <DollarSign className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
126
+ <Input type="number" step="0.01" placeholder="0.00" defaultValue={product?.price} className="pl-10" />
127
+ </div>
128
+ </div>
129
+ <div className="space-y-2">
130
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Prep Time (min)</label>
131
+ <div className="relative">
132
+ <Clock className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
133
+ <Input type="number" placeholder="15" defaultValue={product?.preparation_time ?? undefined} className="pl-10" />
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <div className="grid grid-cols-2 gap-3">
139
+ <div className="space-y-2">
140
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Calories</label>
141
+ <div className="relative">
142
+ <Flame className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
143
+ <Input type="number" placeholder="350" defaultValue={product?.calories ?? undefined} className="pl-10" />
144
+ </div>
145
+ </div>
146
+ <div className="space-y-2">
147
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Tags</label>
148
+ <div className="relative">
149
+ <Tag className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
150
+ <Input placeholder="vegetarian, popular" defaultValue={product?.tags?.join(', ')} className="pl-10" />
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="space-y-2">
156
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Allergens</label>
157
+ <div className="relative">
158
+ <AlertCircle className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
159
+ <Input placeholder="dairy, gluten, nuts" defaultValue={product?.allergens?.join(', ')} className="pl-10" />
160
+ </div>
161
+ </div>
162
+
163
+ <div className="flex items-center gap-6 rounded-xl bg-zinc-50 p-4 dark:bg-zinc-800/50">
164
+ <label className="flex items-center gap-2 text-sm">
165
+ <input type="checkbox" defaultChecked={product?.is_available ?? true} className="h-4 w-4 rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500" />
166
+ <span className="font-medium text-zinc-700 dark:text-zinc-300">Available</span>
167
+ </label>
168
+ <label className="flex items-center gap-2 text-sm">
169
+ <input type="checkbox" defaultChecked={product?.is_featured ?? false} className="h-4 w-4 rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500" />
170
+ <span className="font-medium text-zinc-700 dark:text-zinc-300">Featured</span>
171
+ </label>
172
+ </div>
173
+ </div>
174
+ <div className="flex justify-end gap-2 border-t border-zinc-200 p-5 dark:border-zinc-800">
175
+ <Button variant="outline" onClick={onClose}>Cancel</Button>
176
+ <Button variant="primary" onClick={onClose}>{product ? 'Save Changes' : 'Add Product'}</Button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export default function MenuBuilderPage() {
184
+ const [search, setSearch] = useState('');
185
+ const [expandedCategories, setExpandedCategories] = useState<string[]>(demoCategories.map((c) => c.id));
186
+ const [editingProduct, setEditingProduct] = useState<Product | null>(null);
187
+ const [showAddProduct, setShowAddProduct] = useState(false);
188
+
189
+ const toggleCategory = (id: string) => {
190
+ setExpandedCategories((prev) =>
191
+ prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
192
+ );
193
+ };
194
+
195
+ const filteredProducts = search
196
+ ? demoProducts.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()) || p.description?.toLowerCase().includes(search.toLowerCase()))
197
+ : demoProducts;
198
+
199
+ return (
200
+ <DashboardLayout>
201
+ <div className="space-y-6">
202
+ {/* Header */}
203
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
204
+ <div>
205
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Menu Builder</h1>
206
+ <p className="mt-1 text-sm text-zinc-500">Manage your menus, categories, and products.</p>
207
+ </div>
208
+ <div className="flex gap-2">
209
+ <Button variant="outline" size="sm">
210
+ <Plus className="h-4 w-4" /> Add Category
211
+ </Button>
212
+ <Button variant="primary" size="sm" onClick={() => setShowAddProduct(true)}>
213
+ <Plus className="h-4 w-4" /> Add Product
214
+ </Button>
215
+ </div>
216
+ </div>
217
+
218
+ {/* Search */}
219
+ <div className="relative">
220
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
221
+ <Input
222
+ placeholder="Search products..."
223
+ value={search}
224
+ onChange={(e) => setSearch(e.target.value)}
225
+ className="pl-10 max-w-sm"
226
+ />
227
+ </div>
228
+
229
+ {/* Categories & Products */}
230
+ <div className="space-y-4">
231
+ {demoCategories.map((category) => {
232
+ const categoryProducts = filteredProducts.filter((p) => p.category_id === category.id);
233
+ const isExpanded = expandedCategories.includes(category.id);
234
+
235
+ return (
236
+ <Card key={category.id}>
237
+ <div
238
+ className="flex cursor-pointer items-center justify-between p-4"
239
+ onClick={() => toggleCategory(category.id)}
240
+ >
241
+ <div className="flex items-center gap-3">
242
+ <button className="cursor-grab text-zinc-300 active:cursor-grabbing">
243
+ <GripVertical className="h-4 w-4" />
244
+ </button>
245
+ {isExpanded ? (
246
+ <ChevronDown className="h-4 w-4 text-zinc-400" />
247
+ ) : (
248
+ <ChevronRight className="h-4 w-4 text-zinc-400" />
249
+ )}
250
+ <div>
251
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{category.name}</h3>
252
+ <p className="text-xs text-zinc-500">{categoryProducts.length} items{category.description && ` • ${category.description}`}</p>
253
+ </div>
254
+ </div>
255
+ <div className="flex items-center gap-2">
256
+ <Badge variant={category.is_active ? 'success' : 'secondary'} className="text-[10px]">
257
+ {category.is_active ? 'Active' : 'Hidden'}
258
+ </Badge>
259
+ <button className="rounded-lg p-1.5 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800" onClick={(e) => e.stopPropagation()}>
260
+ <MoreHorizontal className="h-4 w-4" />
261
+ </button>
262
+ </div>
263
+ </div>
264
+
265
+ {isExpanded && (
266
+ <CardContent className="border-t border-zinc-100 pt-3 dark:border-zinc-800">
267
+ {categoryProducts.length > 0 ? (
268
+ <div className="space-y-2">
269
+ {categoryProducts.map((product) => (
270
+ <ProductCard
271
+ key={product.id}
272
+ product={product}
273
+ onEdit={() => setEditingProduct(product)}
274
+ />
275
+ ))}
276
+ </div>
277
+ ) : (
278
+ <div className="py-8 text-center text-sm text-zinc-400">
279
+ No products in this category
280
+ </div>
281
+ )}
282
+ <button
283
+ onClick={() => setShowAddProduct(true)}
284
+ className="mt-3 flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-zinc-200 py-3 text-sm font-medium text-zinc-400 transition-all hover:border-emerald-300 hover:text-emerald-600 dark:border-zinc-700 dark:hover:border-emerald-700"
285
+ >
286
+ <Plus className="h-4 w-4" /> Add product
287
+ </button>
288
+ </CardContent>
289
+ )}
290
+ </Card>
291
+ );
292
+ })}
293
+ </div>
294
+ </div>
295
+
296
+ {/* Modals */}
297
+ {(editingProduct || showAddProduct) && (
298
+ <ProductModal
299
+ product={editingProduct ?? undefined}
300
+ onClose={() => {
301
+ setEditingProduct(null);
302
+ setShowAddProduct(false);
303
+ }}
304
+ />
305
+ )}
306
+ </DashboardLayout>
307
+ );
308
+ }
src/app/(dashboard)/orders/page.tsx ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import {
8
+ Clock,
9
+ MapPin,
10
+ ShoppingBag,
11
+ Truck,
12
+ Phone,
13
+ Mail,
14
+ ChevronRight,
15
+ CheckCircle2,
16
+ XCircle,
17
+ ChefHat,
18
+ Bell,
19
+ ArrowRight,
20
+ Filter,
21
+ Search,
22
+ } from 'lucide-react';
23
+ import { demoOrders, demoOrderItems } from '@/lib/demo-data';
24
+ import { formatCurrency, formatRelativeTime, getOrderStatusColor, getOrderTypeLabel, cn } from '@/lib/utils';
25
+ import { useState } from 'react';
26
+ import { Order, OrderStatus } from '@/types/database';
27
+ import { Input } from '@/components/ui/input';
28
+
29
+ const statusFlow: OrderStatus[] = ['pending', 'confirmed', 'preparing', 'ready', 'delivered'];
30
+
31
+ function OrderCard({ order, isSelected, onClick }: { order: Order; isSelected: boolean; onClick: () => void }) {
32
+ const items = demoOrderItems.filter((i) => i.order_id === order.id);
33
+ const typeIcons = {
34
+ dine_in: MapPin,
35
+ takeaway: ShoppingBag,
36
+ delivery: Truck,
37
+ };
38
+ const TypeIcon = typeIcons[order.order_type];
39
+
40
+ return (
41
+ <div
42
+ onClick={onClick}
43
+ className={cn(
44
+ 'cursor-pointer rounded-xl border p-4 transition-all',
45
+ isSelected
46
+ ? 'border-emerald-500 bg-emerald-50/50 shadow-sm ring-1 ring-emerald-500/20 dark:bg-emerald-950/20'
47
+ : 'border-zinc-200/60 bg-white hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700'
48
+ )}
49
+ >
50
+ <div className="flex items-start justify-between">
51
+ <div className="flex items-center gap-2">
52
+ <div className={cn('flex h-9 w-9 items-center justify-center rounded-lg', getOrderStatusColor(order.status))}>
53
+ <TypeIcon className="h-4 w-4" />
54
+ </div>
55
+ <div>
56
+ <div className="flex items-center gap-2">
57
+ <span className="text-sm font-bold text-zinc-900 dark:text-zinc-100">#{order.id.split('-')[1]}</span>
58
+ <Badge className={cn('text-[10px]', getOrderStatusColor(order.status))} variant="outline">
59
+ {order.status}
60
+ </Badge>
61
+ </div>
62
+ <p className="text-xs text-zinc-500">{order.customer_name}</p>
63
+ </div>
64
+ </div>
65
+ <div className="text-right">
66
+ <p className="text-sm font-bold text-zinc-900 dark:text-zinc-100">{formatCurrency(order.total)}</p>
67
+ <p className="text-[10px] text-zinc-400">{formatRelativeTime(order.created_at)}</p>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="mt-3 flex items-center justify-between">
72
+ <div className="flex items-center gap-2 text-xs text-zinc-500">
73
+ <span>{getOrderTypeLabel(order.order_type)}</span>
74
+ {order.table_number && <span>• Table {order.table_number}</span>}
75
+ <span>• {items.length} items</span>
76
+ </div>
77
+ <ChevronRight className="h-4 w-4 text-zinc-300" />
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function OrderDetail({ order }: { order: Order }) {
84
+ const items = demoOrderItems.filter((i) => i.order_id === order.id);
85
+ const currentStatusIndex = statusFlow.indexOf(order.status);
86
+
87
+ return (
88
+ <div className="space-y-6">
89
+ {/* Header */}
90
+ <div className="flex items-start justify-between">
91
+ <div>
92
+ <div className="flex items-center gap-3">
93
+ <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Order #{order.id.split('-')[1]}</h2>
94
+ <Badge className={cn('text-xs', getOrderStatusColor(order.status))} variant="outline">
95
+ {order.status}
96
+ </Badge>
97
+ </div>
98
+ <p className="mt-1 text-sm text-zinc-500">{formatRelativeTime(order.created_at)}</p>
99
+ </div>
100
+ </div>
101
+
102
+ {/* Status Timeline */}
103
+ {order.status !== 'cancelled' && (
104
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
105
+ <div className="flex items-center justify-between">
106
+ {statusFlow.map((status, index) => {
107
+ const isComplete = index <= currentStatusIndex;
108
+ const isCurrent = index === currentStatusIndex;
109
+ return (
110
+ <div key={status} className="flex flex-1 items-center">
111
+ <div className="flex flex-col items-center gap-1">
112
+ <div className={cn(
113
+ 'flex h-8 w-8 items-center justify-center rounded-full border-2 transition-all',
114
+ isComplete ? 'border-emerald-500 bg-emerald-500 text-white' : 'border-zinc-200 text-zinc-400 dark:border-zinc-700',
115
+ isCurrent && 'ring-4 ring-emerald-100 dark:ring-emerald-900/30'
116
+ )}>
117
+ {isComplete ? <CheckCircle2 className="h-4 w-4" /> : <span className="text-xs">{index + 1}</span>}
118
+ </div>
119
+ <span className={cn('text-[10px] font-medium capitalize', isComplete ? 'text-emerald-600' : 'text-zinc-400')}>
120
+ {status}
121
+ </span>
122
+ </div>
123
+ {index < statusFlow.length - 1 && (
124
+ <div className={cn('mx-1 h-0.5 flex-1', index < currentStatusIndex ? 'bg-emerald-500' : 'bg-zinc-200 dark:bg-zinc-700')} />
125
+ )}
126
+ </div>
127
+ );
128
+ })}
129
+ </div>
130
+ </div>
131
+ )}
132
+
133
+ {/* Customer Info */}
134
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
135
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Customer</h3>
136
+ <div className="mt-3 space-y-2">
137
+ <div className="flex items-center gap-2 text-sm text-zinc-600">
138
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
139
+ <span className="text-xs font-bold">{order.customer_name?.split(' ').map(n => n[0]).join('')}</span>
140
+ </div>
141
+ <span className="font-medium">{order.customer_name}</span>
142
+ </div>
143
+ {order.customer_phone && (
144
+ <div className="flex items-center gap-2 text-sm text-zinc-500">
145
+ <Phone className="h-4 w-4" /> {order.customer_phone}
146
+ </div>
147
+ )}
148
+ {order.customer_email && (
149
+ <div className="flex items-center gap-2 text-sm text-zinc-500">
150
+ <Mail className="h-4 w-4" /> {order.customer_email}
151
+ </div>
152
+ )}
153
+ {order.delivery_address && (
154
+ <div className="flex items-center gap-2 text-sm text-zinc-500">
155
+ <MapPin className="h-4 w-4" /> {order.delivery_address}
156
+ </div>
157
+ )}
158
+ </div>
159
+ </div>
160
+
161
+ {/* Items */}
162
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
163
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Items</h3>
164
+ <div className="mt-3 space-y-3">
165
+ {items.map((item) => (
166
+ <div key={item.id} className="flex items-center justify-between">
167
+ <div className="flex items-center gap-3">
168
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-50 text-sm font-bold text-emerald-600 dark:bg-emerald-900/30">
169
+ {item.quantity}x
170
+ </div>
171
+ <div>
172
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{item.product_name}</p>
173
+ {item.options && Object.entries(item.options).map(([key, value]) => (
174
+ <p key={key} className="text-xs text-zinc-500">{key}: {String(value)}</p>
175
+ ))}
176
+ </div>
177
+ </div>
178
+ <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{formatCurrency(item.total_price)}</p>
179
+ </div>
180
+ ))}
181
+ </div>
182
+
183
+ <div className="mt-4 space-y-1.5 border-t border-zinc-100 pt-4 dark:border-zinc-800">
184
+ <div className="flex justify-between text-sm text-zinc-500">
185
+ <span>Subtotal</span>
186
+ <span>{formatCurrency(order.subtotal)}</span>
187
+ </div>
188
+ <div className="flex justify-between text-sm text-zinc-500">
189
+ <span>Tax</span>
190
+ <span>{formatCurrency(order.tax)}</span>
191
+ </div>
192
+ <div className="flex justify-between text-base font-bold text-zinc-900 dark:text-zinc-100">
193
+ <span>Total</span>
194
+ <span>{formatCurrency(order.total)}</span>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ {/* Action Buttons */}
200
+ {order.status !== 'delivered' && order.status !== 'cancelled' && (
201
+ <div className="flex gap-2">
202
+ {order.status === 'pending' && (
203
+ <>
204
+ <Button variant="primary" className="flex-1">
205
+ <CheckCircle2 className="h-4 w-4" /> Confirm Order
206
+ </Button>
207
+ <Button variant="destructive" className="flex-1">
208
+ <XCircle className="h-4 w-4" /> Cancel
209
+ </Button>
210
+ </>
211
+ )}
212
+ {order.status === 'confirmed' && (
213
+ <Button variant="primary" className="flex-1">
214
+ <ChefHat className="h-4 w-4" /> Start Preparing
215
+ </Button>
216
+ )}
217
+ {order.status === 'preparing' && (
218
+ <Button variant="primary" className="flex-1">
219
+ <Bell className="h-4 w-4" /> Mark Ready
220
+ </Button>
221
+ )}
222
+ {order.status === 'ready' && (
223
+ <Button variant="primary" className="flex-1">
224
+ <CheckCircle2 className="h-4 w-4" /> Mark Delivered
225
+ </Button>
226
+ )}
227
+ </div>
228
+ )}
229
+ </div>
230
+ );
231
+ }
232
+
233
+ export default function OrdersPage() {
234
+ const [selectedOrder, setSelectedOrder] = useState<Order>(demoOrders[0]);
235
+ const [statusFilter, setStatusFilter] = useState<string>('all');
236
+ const [search, setSearch] = useState('');
237
+
238
+ const filteredOrders = demoOrders.filter((o) => {
239
+ if (statusFilter !== 'all' && o.status !== statusFilter) return false;
240
+ if (search && !o.customer_name?.toLowerCase().includes(search.toLowerCase()) && !o.id.includes(search)) return false;
241
+ return true;
242
+ });
243
+
244
+ const statusCounts = {
245
+ all: demoOrders.length,
246
+ pending: demoOrders.filter((o) => o.status === 'pending').length,
247
+ confirmed: demoOrders.filter((o) => o.status === 'confirmed').length,
248
+ preparing: demoOrders.filter((o) => o.status === 'preparing').length,
249
+ ready: demoOrders.filter((o) => o.status === 'ready').length,
250
+ delivered: demoOrders.filter((o) => o.status === 'delivered').length,
251
+ };
252
+
253
+ return (
254
+ <DashboardLayout>
255
+ <div className="space-y-6">
256
+ {/* Header */}
257
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
258
+ <div>
259
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Orders</h1>
260
+ <p className="mt-1 text-sm text-zinc-500">Manage and track customer orders in real-time.</p>
261
+ </div>
262
+ <div className="flex items-center gap-2">
263
+ <div className="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">
264
+ <span className="relative flex h-2 w-2">
265
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
266
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
267
+ </span>
268
+ Live
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ {/* Status Filter Tabs */}
274
+ <div className="flex gap-2 overflow-x-auto pb-2 scrollbar-none">
275
+ {Object.entries(statusCounts).map(([status, count]) => (
276
+ <button
277
+ key={status}
278
+ onClick={() => setStatusFilter(status)}
279
+ className={cn(
280
+ 'flex items-center gap-1.5 whitespace-nowrap rounded-xl px-4 py-2 text-sm font-medium transition-all',
281
+ statusFilter === status
282
+ ? 'bg-zinc-900 text-white shadow-sm dark:bg-white dark:text-zinc-900'
283
+ : 'bg-white text-zinc-600 hover:bg-zinc-50 border border-zinc-200 dark:bg-zinc-900 dark:border-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800'
284
+ )}
285
+ >
286
+ <span className="capitalize">{status}</span>
287
+ <span className={cn(
288
+ 'rounded-full px-1.5 py-0.5 text-[10px] font-bold',
289
+ statusFilter === status ? 'bg-white/20 text-white dark:bg-zinc-900/30 dark:text-zinc-900' : 'bg-zinc-100 text-zinc-500 dark:bg-zinc-800'
290
+ )}>
291
+ {count}
292
+ </span>
293
+ </button>
294
+ ))}
295
+ </div>
296
+
297
+ {/* Two Column Layout */}
298
+ <div className="grid gap-6 lg:grid-cols-5">
299
+ {/* Order List */}
300
+ <div className="space-y-3 lg:col-span-2">
301
+ <div className="relative">
302
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
303
+ <Input
304
+ placeholder="Search orders..."
305
+ value={search}
306
+ onChange={(e) => setSearch(e.target.value)}
307
+ className="pl-10"
308
+ />
309
+ </div>
310
+ <div className="space-y-2 max-h-[calc(100vh-320px)] overflow-y-auto pr-1 scrollbar-thin">
311
+ {filteredOrders.map((order) => (
312
+ <OrderCard
313
+ key={order.id}
314
+ order={order}
315
+ isSelected={selectedOrder?.id === order.id}
316
+ onClick={() => setSelectedOrder(order)}
317
+ />
318
+ ))}
319
+ </div>
320
+ </div>
321
+
322
+ {/* Order Detail */}
323
+ <div className="lg:col-span-3">
324
+ <div className="sticky top-24">
325
+ {selectedOrder ? (
326
+ <OrderDetail order={selectedOrder} />
327
+ ) : (
328
+ <div className="flex flex-col items-center justify-center py-20 text-center">
329
+ <ShoppingBag className="h-12 w-12 text-zinc-200" />
330
+ <p className="mt-4 text-sm text-zinc-500">Select an order to view details</p>
331
+ </div>
332
+ )}
333
+ </div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </DashboardLayout>
338
+ );
339
+ }
src/app/(dashboard)/overview/page.tsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { StatCard } from '@/components/ui/stat-card';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Button } from '@/components/ui/button';
8
+ import {
9
+ DollarSign,
10
+ ShoppingBag,
11
+ Users,
12
+ TrendingUp,
13
+ ArrowUpRight,
14
+ Clock,
15
+ MapPin,
16
+ Truck,
17
+ Eye,
18
+ } from 'lucide-react';
19
+ import { demoOrders, demoOrderItems, generateDailyRevenue, generatePopularItems, generateOrdersByType } from '@/lib/demo-data';
20
+ import { formatCurrency, formatRelativeTime, getOrderStatusColor, getOrderTypeLabel, cn } from '@/lib/utils';
21
+ import Link from 'next/link';
22
+
23
+ const revenueData = generateDailyRevenue(7);
24
+ const popularItems = generatePopularItems().slice(0, 5);
25
+ const ordersByType = generateOrdersByType();
26
+
27
+ function MiniChart() {
28
+ const data = generateDailyRevenue(14);
29
+ const max = Math.max(...data.map((d) => d.revenue));
30
+ return (
31
+ <div className="flex h-16 items-end gap-1">
32
+ {data.map((d, i) => (
33
+ <div
34
+ key={i}
35
+ className="flex-1 rounded-t-sm bg-emerald-500/80 transition-all hover:bg-emerald-500"
36
+ style={{ height: `${(d.revenue / max) * 100}%` }}
37
+ />
38
+ ))}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export default function OverviewPage() {
44
+ const totalRevenue = revenueData.reduce((s, d) => s + d.revenue, 0);
45
+ const totalOrders = revenueData.reduce((s, d) => s + d.orders, 0);
46
+
47
+ return (
48
+ <DashboardLayout>
49
+ <div className="space-y-8">
50
+ {/* Header */}
51
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
52
+ <div>
53
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">
54
+ Dashboard
55
+ </h1>
56
+ <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
57
+ Welcome back, Alex. Here&apos;s what&apos;s happening today.
58
+ </p>
59
+ </div>
60
+ <div className="flex gap-2">
61
+ <Button variant="outline" size="sm">
62
+ <Eye className="h-4 w-4" />
63
+ View Menu
64
+ </Button>
65
+ <Link href="/dashboard/orders">
66
+ <Button variant="primary" size="sm">
67
+ <ShoppingBag className="h-4 w-4" />
68
+ Live Orders
69
+ </Button>
70
+ </Link>
71
+ </div>
72
+ </div>
73
+
74
+ {/* Stats Grid */}
75
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
76
+ <StatCard
77
+ title="Total Revenue"
78
+ value={formatCurrency(totalRevenue)}
79
+ change="+12.5% from last week"
80
+ changeType="positive"
81
+ icon={DollarSign}
82
+ iconColor="text-emerald-600"
83
+ />
84
+ <StatCard
85
+ title="Orders"
86
+ value={totalOrders.toString()}
87
+ change="+8.2% from last week"
88
+ changeType="positive"
89
+ icon={ShoppingBag}
90
+ iconColor="text-blue-600"
91
+ />
92
+ <StatCard
93
+ title="Avg. Order Value"
94
+ value={formatCurrency(totalRevenue / totalOrders)}
95
+ change="+3.1% from last week"
96
+ changeType="positive"
97
+ icon={TrendingUp}
98
+ iconColor="text-violet-600"
99
+ />
100
+ <StatCard
101
+ title="Active Tables"
102
+ value="5 / 8"
103
+ change="3 available"
104
+ changeType="neutral"
105
+ icon={Users}
106
+ iconColor="text-amber-600"
107
+ />
108
+ </div>
109
+
110
+ <div className="grid gap-6 lg:grid-cols-3">
111
+ {/* Revenue Chart */}
112
+ <Card className="lg:col-span-2">
113
+ <CardHeader className="flex flex-row items-center justify-between">
114
+ <CardTitle>Revenue Overview</CardTitle>
115
+ <div className="flex gap-1 rounded-lg border border-zinc-200 p-1 dark:border-zinc-700">
116
+ <button className="rounded-md bg-zinc-900 px-3 py-1 text-xs font-medium text-white dark:bg-white dark:text-zinc-900">7D</button>
117
+ <button className="rounded-md px-3 py-1 text-xs font-medium text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800">14D</button>
118
+ <button className="rounded-md px-3 py-1 text-xs font-medium text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800">30D</button>
119
+ </div>
120
+ </CardHeader>
121
+ <CardContent>
122
+ <div className="mb-4 flex items-baseline gap-3">
123
+ <span className="text-3xl font-bold tracking-tight">{formatCurrency(totalRevenue)}</span>
124
+ <Badge variant="success" className="gap-1">
125
+ <ArrowUpRight className="h-3 w-3" /> 12.5%
126
+ </Badge>
127
+ </div>
128
+ <MiniChart />
129
+ <div className="mt-3 flex justify-between text-xs text-zinc-400">
130
+ {revenueData.map((d, i) => (
131
+ <span key={i}>{new Date(d.date).toLocaleDateString('en', { weekday: 'short' })}</span>
132
+ ))}
133
+ </div>
134
+ </CardContent>
135
+ </Card>
136
+
137
+ {/* Orders by Type */}
138
+ <Card>
139
+ <CardHeader>
140
+ <CardTitle>Orders by Type</CardTitle>
141
+ </CardHeader>
142
+ <CardContent className="space-y-4">
143
+ {ordersByType.map((item) => (
144
+ <div key={item.type} className="space-y-2">
145
+ <div className="flex items-center justify-between text-sm">
146
+ <div className="flex items-center gap-2">
147
+ {item.type === 'Dine In' && <MapPin className="h-4 w-4 text-emerald-500" />}
148
+ {item.type === 'Takeaway' && <ShoppingBag className="h-4 w-4 text-blue-500" />}
149
+ {item.type === 'Delivery' && <Truck className="h-4 w-4 text-violet-500" />}
150
+ <span className="font-medium">{item.type}</span>
151
+ </div>
152
+ <span className="text-zinc-500">{item.count} ({item.percentage}%)</span>
153
+ </div>
154
+ <div className="h-2 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
155
+ <div
156
+ className={cn(
157
+ 'h-full rounded-full transition-all',
158
+ item.type === 'Dine In' && 'bg-emerald-500',
159
+ item.type === 'Takeaway' && 'bg-blue-500',
160
+ item.type === 'Delivery' && 'bg-violet-500'
161
+ )}
162
+ style={{ width: `${item.percentage}%` }}
163
+ />
164
+ </div>
165
+ </div>
166
+ ))}
167
+ </CardContent>
168
+ </Card>
169
+ </div>
170
+
171
+ <div className="grid gap-6 lg:grid-cols-2">
172
+ {/* Recent Orders */}
173
+ <Card>
174
+ <CardHeader className="flex flex-row items-center justify-between">
175
+ <CardTitle>Recent Orders</CardTitle>
176
+ <Link href="/dashboard/orders">
177
+ <Button variant="ghost" size="sm" className="text-xs">
178
+ View All <ArrowUpRight className="h-3 w-3" />
179
+ </Button>
180
+ </Link>
181
+ </CardHeader>
182
+ <CardContent>
183
+ <div className="space-y-3">
184
+ {demoOrders.slice(0, 5).map((order) => (
185
+ <div key={order.id} className="flex items-center justify-between rounded-xl border border-zinc-100 p-3 transition-all hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50">
186
+ <div className="flex items-center gap-3">
187
+ <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-zinc-100 text-sm font-bold text-zinc-600 dark:bg-zinc-800">
188
+ #{order.id.split('-')[1]}
189
+ </div>
190
+ <div>
191
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{order.customer_name}</p>
192
+ <div className="flex items-center gap-2 text-xs text-zinc-500">
193
+ <span>{getOrderTypeLabel(order.order_type)}</span>
194
+ {order.table_number && <span>• Table {order.table_number}</span>}
195
+ <span>• <Clock className="inline h-3 w-3" /> {formatRelativeTime(order.created_at)}</span>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ <div className="text-right">
200
+ <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{formatCurrency(order.total)}</p>
201
+ <Badge className={cn('mt-1 text-[10px]', getOrderStatusColor(order.status))} variant="outline">
202
+ {order.status}
203
+ </Badge>
204
+ </div>
205
+ </div>
206
+ ))}
207
+ </div>
208
+ </CardContent>
209
+ </Card>
210
+
211
+ {/* Popular Items */}
212
+ <Card>
213
+ <CardHeader className="flex flex-row items-center justify-between">
214
+ <CardTitle>Popular Items</CardTitle>
215
+ <Link href="/dashboard/analytics">
216
+ <Button variant="ghost" size="sm" className="text-xs">
217
+ Analytics <ArrowUpRight className="h-3 w-3" />
218
+ </Button>
219
+ </Link>
220
+ </CardHeader>
221
+ <CardContent>
222
+ <div className="space-y-3">
223
+ {popularItems.map((item, index) => (
224
+ <div key={item.name} className="flex items-center justify-between rounded-xl border border-zinc-100 p-3 transition-all hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50">
225
+ <div className="flex items-center gap-3">
226
+ <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-50 to-cyan-50 text-sm font-bold text-emerald-600 dark:from-emerald-950/30 dark:to-cyan-950/30">
227
+ {index + 1}
228
+ </div>
229
+ <div>
230
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{item.name}</p>
231
+ <p className="text-xs text-zinc-500">{item.orders} orders</p>
232
+ </div>
233
+ </div>
234
+ <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{formatCurrency(item.revenue)}</p>
235
+ </div>
236
+ ))}
237
+ </div>
238
+ </CardContent>
239
+ </Card>
240
+ </div>
241
+ </div>
242
+ </DashboardLayout>
243
+ );
244
+ }
src/app/(dashboard)/qr-manager/page.tsx ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Badge } from '@/components/ui/badge';
7
+ import { Input } from '@/components/ui/input';
8
+ import {
9
+ Plus,
10
+ Download,
11
+ QrCode,
12
+ Eye,
13
+ Copy,
14
+ Printer,
15
+ Trash2,
16
+ ScanLine,
17
+ Link as LinkIcon,
18
+ ExternalLink,
19
+ Table2,
20
+ } from 'lucide-react';
21
+ import { demoQRCodes, demoTables } from '@/lib/demo-data';
22
+ import { cn } from '@/lib/utils';
23
+ import { useState } from 'react';
24
+
25
+ function QRCodePreview({ label, scans, isActive }: { label: string; scans: number; isActive: boolean }) {
26
+ return (
27
+ <div className="flex flex-col items-center gap-3 rounded-2xl border border-zinc-200/60 bg-white p-6 transition-all hover:shadow-md hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700">
28
+ {/* QR Code Visual */}
29
+ <div className="relative">
30
+ <div className="flex h-40 w-40 items-center justify-center rounded-2xl bg-white p-3 shadow-inner ring-1 ring-zinc-100">
31
+ {/* Stylized QR code */}
32
+ <div className="grid h-full w-full grid-cols-7 grid-rows-7 gap-[2px]">
33
+ {Array.from({ length: 49 }).map((_, i) => {
34
+ const row = Math.floor(i / 7);
35
+ const col = i % 7;
36
+ const isCorner = (row < 3 && col < 3) || (row < 3 && col > 3) || (row > 3 && col < 3);
37
+ const isFilled = isCorner || Math.random() > 0.4;
38
+ return (
39
+ <div
40
+ key={i}
41
+ className={cn(
42
+ 'rounded-[2px] transition-colors',
43
+ isFilled ? 'bg-zinc-900' : 'bg-transparent'
44
+ )}
45
+ />
46
+ );
47
+ })}
48
+ </div>
49
+ </div>
50
+ {!isActive && (
51
+ <div className="absolute inset-0 flex items-center justify-center rounded-2xl bg-white/80 backdrop-blur-sm">
52
+ <Badge variant="destructive">Disabled</Badge>
53
+ </div>
54
+ )}
55
+ </div>
56
+
57
+ {/* Info */}
58
+ <div className="text-center">
59
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{label}</h3>
60
+ <div className="mt-1 flex items-center justify-center gap-1 text-xs text-zinc-500">
61
+ <ScanLine className="h-3 w-3" />
62
+ {scans} scans
63
+ </div>
64
+ </div>
65
+
66
+ {/* Actions */}
67
+ <div className="flex gap-1.5">
68
+ <Button variant="outline" size="icon-sm">
69
+ <Download className="h-3.5 w-3.5" />
70
+ </Button>
71
+ <Button variant="outline" size="icon-sm">
72
+ <Copy className="h-3.5 w-3.5" />
73
+ </Button>
74
+ <Button variant="outline" size="icon-sm">
75
+ <Printer className="h-3.5 w-3.5" />
76
+ </Button>
77
+ <Button variant="outline" size="icon-sm">
78
+ <Eye className="h-3.5 w-3.5" />
79
+ </Button>
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ export default function QRManagerPage() {
86
+ const [activeTab, setActiveTab] = useState<'qr' | 'tables'>('qr');
87
+
88
+ return (
89
+ <DashboardLayout>
90
+ <div className="space-y-6">
91
+ {/* Header */}
92
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
93
+ <div>
94
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">QR Codes</h1>
95
+ <p className="mt-1 text-sm text-zinc-500">Generate and manage QR codes for your restaurant.</p>
96
+ </div>
97
+ <div className="flex gap-2">
98
+ <Button variant="outline" size="sm">
99
+ <Download className="h-4 w-4" /> Download All
100
+ </Button>
101
+ <Button variant="primary" size="sm">
102
+ <Plus className="h-4 w-4" /> New QR Code
103
+ </Button>
104
+ </div>
105
+ </div>
106
+
107
+ {/* Tabs */}
108
+ <div className="flex gap-1 rounded-xl border border-zinc-200 bg-zinc-50 p-1 dark:border-zinc-700 dark:bg-zinc-900">
109
+ <button
110
+ onClick={() => setActiveTab('qr')}
111
+ className={cn(
112
+ 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
113
+ activeTab === 'qr' ? 'bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-white' : 'text-zinc-500 hover:text-zinc-700'
114
+ )}
115
+ >
116
+ <QrCode className="h-4 w-4" /> QR Codes
117
+ <Badge variant="secondary" className="ml-1 text-[10px]">{demoQRCodes.length}</Badge>
118
+ </button>
119
+ <button
120
+ onClick={() => setActiveTab('tables')}
121
+ className={cn(
122
+ 'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
123
+ activeTab === 'tables' ? 'bg-white text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-white' : 'text-zinc-500 hover:text-zinc-700'
124
+ )}
125
+ >
126
+ <Table2 className="h-4 w-4" /> Tables
127
+ <Badge variant="secondary" className="ml-1 text-[10px]">{demoTables.length}</Badge>
128
+ </button>
129
+ </div>
130
+
131
+ {activeTab === 'qr' ? (
132
+ <>
133
+ {/* Quick Stats */}
134
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
135
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
136
+ <p className="text-xs font-medium text-zinc-500">Total QR Codes</p>
137
+ <p className="mt-1 text-2xl font-bold">{demoQRCodes.length}</p>
138
+ </div>
139
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
140
+ <p className="text-xs font-medium text-zinc-500">Total Scans</p>
141
+ <p className="mt-1 text-2xl font-bold">{demoQRCodes.reduce((s, q) => s + q.scans, 0).toLocaleString()}</p>
142
+ </div>
143
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
144
+ <p className="text-xs font-medium text-zinc-500">Active</p>
145
+ <p className="mt-1 text-2xl font-bold text-emerald-600">{demoQRCodes.filter((q) => q.is_active).length}</p>
146
+ </div>
147
+ <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
148
+ <p className="text-xs font-medium text-zinc-500">Avg. Scans/Code</p>
149
+ <p className="mt-1 text-2xl font-bold">{Math.round(demoQRCodes.reduce((s, q) => s + q.scans, 0) / demoQRCodes.length)}</p>
150
+ </div>
151
+ </div>
152
+
153
+ {/* QR Code Grid */}
154
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
155
+ {demoQRCodes.map((qr) => (
156
+ <QRCodePreview key={qr.id} label={qr.label} scans={qr.scans} isActive={qr.is_active} />
157
+ ))}
158
+ {/* Add New Card */}
159
+ <button className="flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-zinc-200 p-6 text-zinc-400 transition-all hover:border-emerald-300 hover:text-emerald-600 dark:border-zinc-700 dark:hover:border-emerald-700">
160
+ <div className="rounded-xl bg-zinc-100 p-3 dark:bg-zinc-800">
161
+ <Plus className="h-6 w-6" />
162
+ </div>
163
+ <span className="text-sm font-medium">Create New QR Code</span>
164
+ </button>
165
+ </div>
166
+ </>
167
+ ) : (
168
+ /* Tables View */
169
+ <Card>
170
+ <CardHeader className="flex flex-row items-center justify-between">
171
+ <CardTitle>Table Management</CardTitle>
172
+ <Button variant="primary" size="sm">
173
+ <Plus className="h-4 w-4" /> Add Table
174
+ </Button>
175
+ </CardHeader>
176
+ <CardContent>
177
+ <div className="overflow-x-auto">
178
+ <table className="w-full">
179
+ <thead>
180
+ <tr className="border-b border-zinc-100 text-left dark:border-zinc-800">
181
+ <th className="pb-3 text-xs font-medium text-zinc-500">Table #</th>
182
+ <th className="pb-3 text-xs font-medium text-zinc-500">Name</th>
183
+ <th className="pb-3 text-xs font-medium text-zinc-500">Capacity</th>
184
+ <th className="pb-3 text-xs font-medium text-zinc-500">Status</th>
185
+ <th className="pb-3 text-xs font-medium text-zinc-500">QR Code</th>
186
+ <th className="pb-3 text-xs font-medium text-zinc-500 text-right">Actions</th>
187
+ </tr>
188
+ </thead>
189
+ <tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
190
+ {demoTables.map((table) => (
191
+ <tr key={table.id} className="group">
192
+ <td className="py-3">
193
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-100 text-sm font-bold text-zinc-600 dark:bg-zinc-800">
194
+ {table.number}
195
+ </div>
196
+ </td>
197
+ <td className="py-3 text-sm font-medium text-zinc-900 dark:text-zinc-100">{table.name || '—'}</td>
198
+ <td className="py-3 text-sm text-zinc-500">{table.capacity} seats</td>
199
+ <td className="py-3">
200
+ <Badge variant={table.is_active ? 'success' : 'secondary'} className="text-[10px]">
201
+ {table.is_active ? 'Active' : 'Inactive'}
202
+ </Badge>
203
+ </td>
204
+ <td className="py-3">
205
+ <Button variant="ghost" size="sm" className="text-xs">
206
+ <QrCode className="h-3 w-3" /> View QR
207
+ </Button>
208
+ </td>
209
+ <td className="py-3 text-right">
210
+ <div className="flex justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
211
+ <Button variant="ghost" size="icon-sm"><LinkIcon className="h-3.5 w-3.5" /></Button>
212
+ <Button variant="ghost" size="icon-sm"><Trash2 className="h-3.5 w-3.5 text-red-500" /></Button>
213
+ </div>
214
+ </td>
215
+ </tr>
216
+ ))}
217
+ </tbody>
218
+ </table>
219
+ </div>
220
+ </CardContent>
221
+ </Card>
222
+ )}
223
+ </div>
224
+ </DashboardLayout>
225
+ );
226
+ }
src/app/(dashboard)/restaurant-setup/page.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Textarea } from '@/components/ui/textarea';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Store, MapPin, Phone, Mail, Globe, Clock, Image as ImageIcon, Palette, Save, ExternalLink } from 'lucide-react';
10
+ import { demoRestaurant } from '@/lib/demo-data';
11
+ import { cn } from '@/lib/utils';
12
+
13
+ export default function RestaurantSetupPage() {
14
+ return (
15
+ <DashboardLayout>
16
+ <div className="space-y-6">
17
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
18
+ <div>
19
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Restaurant Profile</h1>
20
+ <p className="mt-1 text-sm text-zinc-500">Manage your restaurant&apos;s public information and branding.</p>
21
+ </div>
22
+ <div className="flex gap-2">
23
+ <Button variant="outline" size="sm">
24
+ <ExternalLink className="h-4 w-4" /> Preview
25
+ </Button>
26
+ <Button variant="primary" size="sm">
27
+ <Save className="h-4 w-4" /> Save Changes
28
+ </Button>
29
+ </div>
30
+ </div>
31
+
32
+ <div className="grid gap-6 lg:grid-cols-3">
33
+ <div className="space-y-6 lg:col-span-2">
34
+ {/* Basic Info */}
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle className="flex items-center gap-2"><Store className="h-4 w-4" /> Basic Information</CardTitle>
38
+ </CardHeader>
39
+ <CardContent className="space-y-4">
40
+ <div className="space-y-2">
41
+ <label className="text-sm font-medium text-zinc-700">Restaurant Name</label>
42
+ <Input defaultValue={demoRestaurant.name} />
43
+ </div>
44
+ <div className="space-y-2">
45
+ <label className="text-sm font-medium text-zinc-700">URL Slug</label>
46
+ <div className="flex items-center gap-2">
47
+ <span className="text-sm text-zinc-400">scanmenu.app/</span>
48
+ <Input defaultValue={demoRestaurant.slug} className="flex-1" />
49
+ </div>
50
+ </div>
51
+ <div className="space-y-2">
52
+ <label className="text-sm font-medium text-zinc-700">Description</label>
53
+ <Textarea defaultValue={demoRestaurant.description} rows={3} />
54
+ </div>
55
+ <div className="grid grid-cols-2 gap-4">
56
+ <div className="space-y-2">
57
+ <label className="text-sm font-medium text-zinc-700">Currency</label>
58
+ <Input defaultValue={demoRestaurant.currency} />
59
+ </div>
60
+ <div className="space-y-2">
61
+ <label className="text-sm font-medium text-zinc-700">Timezone</label>
62
+ <Input defaultValue={demoRestaurant.timezone} />
63
+ </div>
64
+ </div>
65
+ </CardContent>
66
+ </Card>
67
+
68
+ {/* Contact */}
69
+ <Card>
70
+ <CardHeader>
71
+ <CardTitle className="flex items-center gap-2"><Phone className="h-4 w-4" /> Contact Information</CardTitle>
72
+ </CardHeader>
73
+ <CardContent className="space-y-4">
74
+ <div className="space-y-2">
75
+ <label className="text-sm font-medium text-zinc-700">Address</label>
76
+ <div className="relative">
77
+ <MapPin className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
78
+ <Input defaultValue={demoRestaurant.address} className="pl-10" />
79
+ </div>
80
+ </div>
81
+ <div className="grid grid-cols-2 gap-4">
82
+ <div className="space-y-2">
83
+ <label className="text-sm font-medium text-zinc-700">Phone</label>
84
+ <div className="relative">
85
+ <Phone className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
86
+ <Input defaultValue={demoRestaurant.phone} className="pl-10" />
87
+ </div>
88
+ </div>
89
+ <div className="space-y-2">
90
+ <label className="text-sm font-medium text-zinc-700">Email</label>
91
+ <div className="relative">
92
+ <Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
93
+ <Input defaultValue={demoRestaurant.email} className="pl-10" />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ <div className="space-y-2">
98
+ <label className="text-sm font-medium text-zinc-700">Website</label>
99
+ <div className="relative">
100
+ <Globe className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
101
+ <Input defaultValue={demoRestaurant.website} className="pl-10" />
102
+ </div>
103
+ </div>
104
+ </CardContent>
105
+ </Card>
106
+
107
+ {/* Opening Hours */}
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle className="flex items-center gap-2"><Clock className="h-4 w-4" /> Opening Hours</CardTitle>
111
+ </CardHeader>
112
+ <CardContent>
113
+ <div className="space-y-3">
114
+ {Object.entries(demoRestaurant.opening_hours || {}).map(([day, hours]) => (
115
+ <div key={day} className="flex items-center gap-4">
116
+ <span className="w-24 text-sm font-medium capitalize text-zinc-700">{day}</span>
117
+ <div className="flex items-center gap-2 flex-1">
118
+ <Input type="time" defaultValue={hours.open} className="w-32" />
119
+ <span className="text-zinc-400">to</span>
120
+ <Input type="time" defaultValue={hours.close} className="w-32" />
121
+ </div>
122
+ <label className="flex items-center gap-2">
123
+ <input type="checkbox" defaultChecked={!hours.closed} className="h-4 w-4 rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500" />
124
+ <span className="text-xs text-zinc-500">Open</span>
125
+ </label>
126
+ </div>
127
+ ))}
128
+ </div>
129
+ </CardContent>
130
+ </Card>
131
+ </div>
132
+
133
+ {/* Sidebar */}
134
+ <div className="space-y-6">
135
+ {/* Logo */}
136
+ <Card>
137
+ <CardHeader>
138
+ <CardTitle className="flex items-center gap-2"><ImageIcon className="h-4 w-4" /> Logo</CardTitle>
139
+ </CardHeader>
140
+ <CardContent>
141
+ <div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-800/50">
142
+ <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-500 to-cyan-500 text-2xl font-bold text-white shadow-lg">
143
+ GK
144
+ </div>
145
+ <Button variant="outline" size="sm" className="mt-4">Upload Logo</Button>
146
+ </div>
147
+ </CardContent>
148
+ </Card>
149
+
150
+ {/* Cover Image */}
151
+ <Card>
152
+ <CardHeader>
153
+ <CardTitle className="flex items-center gap-2"><ImageIcon className="h-4 w-4" /> Cover Image</CardTitle>
154
+ </CardHeader>
155
+ <CardContent>
156
+ <div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-700 dark:bg-zinc-800/50">
157
+ <ImageIcon className="h-10 w-10 text-zinc-300" />
158
+ <p className="mt-2 text-xs text-zinc-400">Recommended: 1200 x 400</p>
159
+ <Button variant="outline" size="sm" className="mt-3">Upload Cover</Button>
160
+ </div>
161
+ </CardContent>
162
+ </Card>
163
+
164
+ {/* Theme */}
165
+ <Card>
166
+ <CardHeader>
167
+ <CardTitle className="flex items-center gap-2"><Palette className="h-4 w-4" /> Theme Color</CardTitle>
168
+ </CardHeader>
169
+ <CardContent>
170
+ <div className="flex gap-2">
171
+ {['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#14b8a6', '#6366f1'].map((color) => (
172
+ <button
173
+ key={color}
174
+ className={cn(
175
+ "h-8 w-8 rounded-lg ring-offset-2 transition-all hover:scale-110",
176
+ color === demoRestaurant.theme_color ? "ring-2 ring-emerald-500" : "ring-2 ring-transparent"
177
+ )}
178
+ style={{ backgroundColor: color }}
179
+ />
180
+ ))}
181
+ </div>
182
+ </CardContent>
183
+ </Card>
184
+
185
+ {/* Status */}
186
+ <Card>
187
+ <CardContent className="pt-6">
188
+ <div className="flex items-center justify-between">
189
+ <span className="text-sm font-medium text-zinc-700">Restaurant Status</span>
190
+ <Badge variant="success">Active</Badge>
191
+ </div>
192
+ <p className="mt-2 text-xs text-zinc-500">Your restaurant is visible to customers</p>
193
+ </CardContent>
194
+ </Card>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </DashboardLayout>
199
+ );
200
+ }
src/app/(dashboard)/settings/page.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardLayout } from '@/components/layout/dashboard-layout';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Badge } from '@/components/ui/badge';
8
+ import { User, Bell, Shield, Key, Globe, Trash2, Save, UserPlus, Mail } from 'lucide-react';
9
+ import { demoUser } from '@/lib/demo-data';
10
+
11
+ export default function SettingsPage() {
12
+ return (
13
+ <DashboardLayout>
14
+ <div className="space-y-6">
15
+ <div>
16
+ <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Settings</h1>
17
+ <p className="mt-1 text-sm text-zinc-500">Manage your account, notifications, and security.</p>
18
+ </div>
19
+
20
+ <div className="grid gap-6 lg:grid-cols-3">
21
+ <div className="space-y-6 lg:col-span-2">
22
+ {/* Profile */}
23
+ <Card>
24
+ <CardHeader>
25
+ <CardTitle className="flex items-center gap-2"><User className="h-4 w-4" /> Profile</CardTitle>
26
+ </CardHeader>
27
+ <CardContent className="space-y-4">
28
+ <div className="flex items-center gap-4">
29
+ <div className="flex h-16 w-16 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-fuchsia-500 text-lg font-bold text-white">
30
+ AR
31
+ </div>
32
+ <div>
33
+ <Button variant="outline" size="sm">Change Avatar</Button>
34
+ <p className="mt-1 text-xs text-zinc-400">JPG, PNG. Max 2MB</p>
35
+ </div>
36
+ </div>
37
+ <div className="grid grid-cols-2 gap-4">
38
+ <div className="space-y-2">
39
+ <label className="text-sm font-medium text-zinc-700">Full Name</label>
40
+ <Input defaultValue={demoUser.full_name} />
41
+ </div>
42
+ <div className="space-y-2">
43
+ <label className="text-sm font-medium text-zinc-700">Email</label>
44
+ <Input defaultValue={demoUser.email} type="email" />
45
+ </div>
46
+ </div>
47
+ <div className="flex justify-end">
48
+ <Button variant="primary" size="sm"><Save className="h-4 w-4" /> Save</Button>
49
+ </div>
50
+ </CardContent>
51
+ </Card>
52
+
53
+ {/* Security */}
54
+ <Card>
55
+ <CardHeader>
56
+ <CardTitle className="flex items-center gap-2"><Shield className="h-4 w-4" /> Security</CardTitle>
57
+ </CardHeader>
58
+ <CardContent className="space-y-4">
59
+ <div className="space-y-2">
60
+ <label className="text-sm font-medium text-zinc-700">Current Password</label>
61
+ <Input type="password" placeholder="••••••••" />
62
+ </div>
63
+ <div className="grid grid-cols-2 gap-4">
64
+ <div className="space-y-2">
65
+ <label className="text-sm font-medium text-zinc-700">New Password</label>
66
+ <Input type="password" placeholder="Min. 8 characters" />
67
+ </div>
68
+ <div className="space-y-2">
69
+ <label className="text-sm font-medium text-zinc-700">Confirm Password</label>
70
+ <Input type="password" placeholder="Confirm new password" />
71
+ </div>
72
+ </div>
73
+ <div className="flex justify-end">
74
+ <Button variant="primary" size="sm"><Key className="h-4 w-4" /> Update Password</Button>
75
+ </div>
76
+ </CardContent>
77
+ </Card>
78
+
79
+ {/* Notifications */}
80
+ <Card>
81
+ <CardHeader>
82
+ <CardTitle className="flex items-center gap-2"><Bell className="h-4 w-4" /> Notifications</CardTitle>
83
+ </CardHeader>
84
+ <CardContent>
85
+ <div className="space-y-4">
86
+ {[
87
+ { label: 'New orders', description: 'Get notified when a new order is placed', enabled: true },
88
+ { label: 'Order status updates', description: 'Notifications when order status changes', enabled: true },
89
+ { label: 'Daily summary', description: 'Receive a daily summary of your sales', enabled: false },
90
+ { label: 'Weekly reports', description: 'Weekly analytics and performance report', enabled: true },
91
+ { label: 'Marketing emails', description: 'Tips, product updates, and promotions', enabled: false },
92
+ ].map((setting) => (
93
+ <div key={setting.label} className="flex items-center justify-between rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
94
+ <div>
95
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{setting.label}</p>
96
+ <p className="text-xs text-zinc-500">{setting.description}</p>
97
+ </div>
98
+ <button className={`relative h-6 w-11 rounded-full transition-colors ${setting.enabled ? 'bg-emerald-500' : 'bg-zinc-300 dark:bg-zinc-600'}`}>
99
+ <span className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform ${setting.enabled ? 'translate-x-5' : ''}`} />
100
+ </button>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ </CardContent>
105
+ </Card>
106
+ </div>
107
+
108
+ {/* Sidebar */}
109
+ <div className="space-y-6">
110
+ {/* Team */}
111
+ <Card>
112
+ <CardHeader className="flex flex-row items-center justify-between">
113
+ <CardTitle className="text-base">Team Members</CardTitle>
114
+ <Button variant="outline" size="sm"><UserPlus className="h-3.5 w-3.5" /> Invite</Button>
115
+ </CardHeader>
116
+ <CardContent>
117
+ <div className="space-y-3">
118
+ {[
119
+ { name: 'Alex Rivera', email: 'alex@scanmenu.app', role: 'Owner' },
120
+ { name: 'Jordan Kim', email: 'jordan@scanmenu.app', role: 'Staff' },
121
+ { name: 'Sam Patel', email: 'sam@scanmenu.app', role: 'Staff' },
122
+ ].map((member) => (
123
+ <div key={member.email} className="flex items-center gap-3">
124
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-100 text-xs font-bold text-zinc-600 dark:bg-zinc-800">
125
+ {member.name.split(' ').map(n => n[0]).join('')}
126
+ </div>
127
+ <div className="flex-1 min-w-0">
128
+ <p className="text-sm font-medium truncate">{member.name}</p>
129
+ <p className="text-xs text-zinc-500 truncate">{member.email}</p>
130
+ </div>
131
+ <Badge variant={member.role === 'Owner' ? 'success' : 'secondary'} className="text-[10px]">
132
+ {member.role}
133
+ </Badge>
134
+ </div>
135
+ ))}
136
+ </div>
137
+ </CardContent>
138
+ </Card>
139
+
140
+ {/* API Keys */}
141
+ <Card>
142
+ <CardHeader>
143
+ <CardTitle className="text-base flex items-center gap-2"><Key className="h-4 w-4" /> API Keys</CardTitle>
144
+ </CardHeader>
145
+ <CardContent className="space-y-3">
146
+ <div className="rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800/50">
147
+ <p className="text-xs font-medium text-zinc-500">Live Key</p>
148
+ <p className="mt-1 font-mono text-xs text-zinc-700 dark:text-zinc-300">sk_live_•••••••••••4xK2</p>
149
+ </div>
150
+ <Button variant="outline" size="sm" className="w-full">Generate New Key</Button>
151
+ </CardContent>
152
+ </Card>
153
+
154
+ {/* Danger Zone */}
155
+ <Card className="border-red-200 dark:border-red-900/50">
156
+ <CardHeader>
157
+ <CardTitle className="text-base text-red-600">Danger Zone</CardTitle>
158
+ </CardHeader>
159
+ <CardContent className="space-y-3">
160
+ <p className="text-xs text-zinc-500">Permanently delete your restaurant and all associated data.</p>
161
+ <Button variant="destructive" size="sm" className="w-full">
162
+ <Trash2 className="h-4 w-4" /> Delete Restaurant
163
+ </Button>
164
+ </CardContent>
165
+ </Card>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </DashboardLayout>
170
+ );
171
+ }
src/app/(public)/restaurant/[slug]/page.tsx ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Textarea } from '@/components/ui/textarea';
8
+ import {
9
+ ShoppingBag,
10
+ MapPin,
11
+ Phone,
12
+ Clock,
13
+ Star,
14
+ Minus,
15
+ Plus,
16
+ X,
17
+ Search,
18
+ ChevronRight,
19
+ Flame,
20
+ Leaf,
21
+ AlertCircle,
22
+ Truck,
23
+ UtensilsCrossed,
24
+ ArrowLeft,
25
+ CheckCircle2,
26
+ Sparkles,
27
+ ExternalLink,
28
+ } from 'lucide-react';
29
+ import { demoRestaurant, demoCategories, demoProducts } from '@/lib/demo-data';
30
+ import { formatCurrency, cn } from '@/lib/utils';
31
+ import { Product, OrderType, CartItem } from '@/types/database';
32
+ import { useCartStore } from '@/stores/cart-store';
33
+ import Link from 'next/link';
34
+
35
+ function ProductDetailSheet({ product, onClose, onAdd }: { product: Product; onClose: () => void; onAdd: (qty: number) => void }) {
36
+ const [qty, setQty] = useState(1);
37
+
38
+ return (
39
+ <div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center bg-black/40 backdrop-blur-sm" onClick={onClose}>
40
+ <div className="w-full max-w-md max-h-[85vh] overflow-y-auto rounded-t-3xl sm:rounded-3xl bg-white shadow-2xl animate-in slide-in-from-bottom-4 dark:bg-zinc-900" onClick={(e) => e.stopPropagation()}>
41
+ {/* Product Image Placeholder */}
42
+ <div className="relative h-48 bg-gradient-to-br from-emerald-100 to-cyan-100 dark:from-emerald-900/30 dark:to-cyan-900/30">
43
+ <div className="absolute inset-0 flex items-center justify-center">
44
+ <UtensilsCrossed className="h-16 w-16 text-emerald-300/50" />
45
+ </div>
46
+ <button onClick={onClose} className="absolute right-3 top-3 rounded-full bg-white/80 p-2 backdrop-blur-sm">
47
+ <X className="h-4 w-4" />
48
+ </button>
49
+ {product.is_featured && (
50
+ <div className="absolute left-3 top-3">
51
+ <Badge className="bg-amber-500 text-white border-0">
52
+ <Star className="h-3 w-3 mr-0.5 fill-white" /> Featured
53
+ </Badge>
54
+ </div>
55
+ )}
56
+ </div>
57
+
58
+ <div className="p-5 space-y-4">
59
+ <div>
60
+ <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">{product.name}</h2>
61
+ <p className="mt-1 text-sm text-zinc-500">{product.description}</p>
62
+ </div>
63
+
64
+ {/* Meta */}
65
+ <div className="flex flex-wrap gap-2">
66
+ {product.preparation_time && (
67
+ <Badge variant="secondary"><Clock className="h-3 w-3 mr-0.5" />{product.preparation_time} min</Badge>
68
+ )}
69
+ {product.calories && (
70
+ <Badge variant="secondary"><Flame className="h-3 w-3 mr-0.5" />{product.calories} cal</Badge>
71
+ )}
72
+ {product.tags?.map((tag) => (
73
+ <Badge key={tag} variant="outline" className="capitalize">{tag}</Badge>
74
+ ))}
75
+ </div>
76
+
77
+ {/* Allergens */}
78
+ {product.allergens && product.allergens.length > 0 && (
79
+ <div className="flex items-start gap-2 rounded-xl bg-amber-50 p-3 text-sm dark:bg-amber-950/20">
80
+ <AlertCircle className="h-4 w-4 shrink-0 text-amber-600 mt-0.5" />
81
+ <div>
82
+ <p className="font-medium text-amber-800 dark:text-amber-400">Allergens</p>
83
+ <p className="text-amber-700/70 dark:text-amber-500/70 capitalize">{product.allergens.join(', ')}</p>
84
+ </div>
85
+ </div>
86
+ )}
87
+
88
+ {/* Price & Qty */}
89
+ <div className="flex items-center justify-between rounded-xl bg-zinc-50 p-4 dark:bg-zinc-800/50">
90
+ <span className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{formatCurrency(product.price)}</span>
91
+ <div className="flex items-center gap-3">
92
+ <button
93
+ onClick={() => setQty(Math.max(1, qty - 1))}
94
+ className="flex h-9 w-9 items-center justify-center rounded-xl border border-zinc-200 text-zinc-600 hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800"
95
+ >
96
+ <Minus className="h-4 w-4" />
97
+ </button>
98
+ <span className="w-6 text-center text-lg font-bold">{qty}</span>
99
+ <button
100
+ onClick={() => setQty(qty + 1)}
101
+ className="flex h-9 w-9 items-center justify-center rounded-xl bg-emerald-600 text-white hover:bg-emerald-500"
102
+ >
103
+ <Plus className="h-4 w-4" />
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <Button
109
+ variant="primary"
110
+ size="lg"
111
+ className="w-full"
112
+ onClick={() => { onAdd(qty); onClose(); }}
113
+ >
114
+ <ShoppingBag className="h-4 w-4" />
115
+ Add to Order — {formatCurrency(product.price * qty)}
116
+ </Button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function CartSheet({ onClose }: { onClose: () => void }) {
124
+ const { items, removeItem, updateQuantity, getSubtotal, getTax, getTotal, orderType, setOrderType, tableNumber, setTableNumber, customerName, setCustomerName, customerPhone, setCustomerPhone, deliveryAddress, setDeliveryAddress, notes, setNotes, clearCart } = useCartStore();
125
+ const [step, setStep] = useState<'cart' | 'details' | 'success'>('cart');
126
+
127
+ if (step === 'success') {
128
+ return (
129
+ <div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center bg-black/40 backdrop-blur-sm">
130
+ <div className="w-full max-w-md rounded-t-3xl sm:rounded-3xl bg-white p-8 text-center shadow-2xl dark:bg-zinc-900">
131
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-900/30">
132
+ <CheckCircle2 className="h-8 w-8 text-emerald-600" />
133
+ </div>
134
+ <h2 className="mt-4 text-xl font-bold text-zinc-900 dark:text-zinc-100">Order Placed!</h2>
135
+ <p className="mt-2 text-sm text-zinc-500">Your order has been received and is being prepared. Estimated wait time: 15-20 minutes.</p>
136
+ <p className="mt-4 text-sm font-medium text-zinc-700 dark:text-zinc-300">Order #1234</p>
137
+ <Button variant="primary" className="mt-6 w-full" onClick={() => { clearCart(); onClose(); }}>
138
+ Done
139
+ </Button>
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center bg-black/40 backdrop-blur-sm" onClick={onClose}>
147
+ <div className="w-full max-w-md max-h-[90vh] flex flex-col rounded-t-3xl sm:rounded-3xl bg-white shadow-2xl dark:bg-zinc-900" onClick={(e) => e.stopPropagation()}>
148
+ {/* Header */}
149
+ <div className="flex items-center justify-between border-b border-zinc-100 p-5 dark:border-zinc-800">
150
+ <div className="flex items-center gap-2">
151
+ {step === 'details' && (
152
+ <button onClick={() => setStep('cart')} className="rounded-lg p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800">
153
+ <ArrowLeft className="h-4 w-4" />
154
+ </button>
155
+ )}
156
+ <h2 className="text-lg font-bold">{step === 'cart' ? 'Your Order' : 'Order Details'}</h2>
157
+ </div>
158
+ <button onClick={onClose} className="rounded-lg p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800">
159
+ <X className="h-4 w-4" />
160
+ </button>
161
+ </div>
162
+
163
+ <div className="flex-1 overflow-y-auto p-5 space-y-4">
164
+ {step === 'cart' ? (
165
+ <>
166
+ {/* Order Type */}
167
+ <div className="grid grid-cols-3 gap-2">
168
+ {[
169
+ { type: 'dine_in' as OrderType, label: 'Dine In', icon: UtensilsCrossed },
170
+ { type: 'takeaway' as OrderType, label: 'Takeaway', icon: ShoppingBag },
171
+ { type: 'delivery' as OrderType, label: 'Delivery', icon: Truck },
172
+ ].map(({ type, label, icon: Icon }) => (
173
+ <button
174
+ key={type}
175
+ onClick={() => setOrderType(type)}
176
+ className={cn(
177
+ 'flex flex-col items-center gap-1.5 rounded-xl border p-3 text-xs font-medium transition-all',
178
+ orderType === type
179
+ ? 'border-emerald-500 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30'
180
+ : 'border-zinc-200 text-zinc-500 hover:border-zinc-300 dark:border-zinc-700'
181
+ )}
182
+ >
183
+ <Icon className="h-4 w-4" />
184
+ {label}
185
+ </button>
186
+ ))}
187
+ </div>
188
+
189
+ {orderType === 'dine_in' && (
190
+ <Input placeholder="Table number" value={tableNumber || ''} onChange={(e) => setTableNumber(e.target.value)} />
191
+ )}
192
+
193
+ {/* Cart Items */}
194
+ {items.length === 0 ? (
195
+ <div className="py-12 text-center">
196
+ <ShoppingBag className="mx-auto h-10 w-10 text-zinc-200" />
197
+ <p className="mt-3 text-sm text-zinc-500">Your cart is empty</p>
198
+ </div>
199
+ ) : (
200
+ <div className="space-y-3">
201
+ {items.map((item) => (
202
+ <div key={item.id} className="flex items-center gap-3 rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
203
+ <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-emerald-50 dark:bg-emerald-900/30">
204
+ <UtensilsCrossed className="h-5 w-5 text-emerald-500/50" />
205
+ </div>
206
+ <div className="flex-1 min-w-0">
207
+ <p className="text-sm font-medium truncate">{item.product.name}</p>
208
+ <p className="text-xs text-zinc-500">{formatCurrency(item.product.price)} each</p>
209
+ </div>
210
+ <div className="flex items-center gap-2">
211
+ <button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="flex h-7 w-7 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700">
212
+ <Minus className="h-3 w-3" />
213
+ </button>
214
+ <span className="w-5 text-center text-sm font-bold">{item.quantity}</span>
215
+ <button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-600 text-white hover:bg-emerald-500">
216
+ <Plus className="h-3 w-3" />
217
+ </button>
218
+ </div>
219
+ <span className="w-16 text-right text-sm font-semibold">{formatCurrency(item.total)}</span>
220
+ </div>
221
+ ))}
222
+ </div>
223
+ )}
224
+ </>
225
+ ) : (
226
+ /* Details Step */
227
+ <div className="space-y-4">
228
+ <div className="space-y-2">
229
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Your Name</label>
230
+ <Input placeholder="Full name" value={customerName} onChange={(e) => setCustomerName(e.target.value)} />
231
+ </div>
232
+ <div className="space-y-2">
233
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Phone Number</label>
234
+ <Input placeholder="+1 (555) 000-0000" value={customerPhone} onChange={(e) => setCustomerPhone(e.target.value)} />
235
+ </div>
236
+ {orderType === 'delivery' && (
237
+ <div className="space-y-2">
238
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Delivery Address</label>
239
+ <Textarea placeholder="Full delivery address" value={deliveryAddress} onChange={(e) => setDeliveryAddress(e.target.value)} />
240
+ </div>
241
+ )}
242
+ <div className="space-y-2">
243
+ <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Special Instructions</label>
244
+ <Textarea placeholder="Allergies, preferences, etc." value={notes} onChange={(e) => setNotes(e.target.value)} rows={2} />
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+
250
+ {/* Footer */}
251
+ {items.length > 0 && (
252
+ <div className="border-t border-zinc-100 p-5 dark:border-zinc-800">
253
+ <div className="space-y-1.5 mb-4">
254
+ <div className="flex justify-between text-sm text-zinc-500">
255
+ <span>Subtotal</span>
256
+ <span>{formatCurrency(getSubtotal())}</span>
257
+ </div>
258
+ <div className="flex justify-between text-sm text-zinc-500">
259
+ <span>Tax</span>
260
+ <span>{formatCurrency(getTax())}</span>
261
+ </div>
262
+ <div className="flex justify-between text-base font-bold text-zinc-900 dark:text-zinc-100">
263
+ <span>Total</span>
264
+ <span>{formatCurrency(getTotal())}</span>
265
+ </div>
266
+ </div>
267
+ <Button
268
+ variant="primary"
269
+ size="lg"
270
+ className="w-full"
271
+ onClick={() => step === 'cart' ? setStep('details') : setStep('success')}
272
+ >
273
+ {step === 'cart' ? (
274
+ <>Continue <ChevronRight className="h-4 w-4" /></>
275
+ ) : (
276
+ <>Place Order — {formatCurrency(getTotal())}</>
277
+ )}
278
+ </Button>
279
+ </div>
280
+ )}
281
+ </div>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ export default function PublicMenuPage() {
287
+ const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
288
+ const [showCart, setShowCart] = useState(false);
289
+ const [activeCategory, setActiveCategory] = useState(demoCategories[0]?.id);
290
+ const [search, setSearch] = useState('');
291
+ const { addItem, getItemCount, setRestaurant } = useCartStore();
292
+
293
+ useEffect(() => {
294
+ setRestaurant(demoRestaurant.id, demoRestaurant.name);
295
+ }, [setRestaurant]);
296
+
297
+ const itemCount = getItemCount();
298
+
299
+ const filteredProducts = search
300
+ ? demoProducts.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()) || p.description?.toLowerCase().includes(search.toLowerCase()))
301
+ : demoProducts;
302
+
303
+ return (
304
+ <div className="min-h-screen bg-white dark:bg-zinc-950">
305
+ {/* Header */}
306
+ <div className="relative overflow-hidden bg-gradient-to-br from-emerald-600 via-emerald-500 to-cyan-500 text-white">
307
+ <div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCI+PHBhdGggZD0iTTAgMGg2MHY2MEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0wIDBoMXYxSDB6IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMDUpIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCBmaWxsPSJ1cmwoI2EpIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIi8+PC9zdmc+')] opacity-30" />
308
+ <div className="relative px-4 pt-12 pb-6 sm:px-6">
309
+ <Link href="/" className="inline-flex items-center gap-1.5 text-white/70 hover:text-white text-sm mb-4">
310
+ <ArrowLeft className="h-4 w-4" /> Back
311
+ </Link>
312
+ <div className="flex items-center gap-4">
313
+ <div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/20 backdrop-blur-sm text-xl font-bold shadow-lg">
314
+ GK
315
+ </div>
316
+ <div>
317
+ <h1 className="text-2xl font-bold">{demoRestaurant.name}</h1>
318
+ <div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-white/80">
319
+ <span className="flex items-center gap-1"><MapPin className="h-3.5 w-3.5" /> San Francisco</span>
320
+ <span>•</span>
321
+ <span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" /> Open until 10 PM</span>
322
+ <span>•</span>
323
+ <span className="flex items-center gap-1"><Star className="h-3.5 w-3.5 fill-white" /> 4.8</span>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ {/* Category Nav */}
330
+ <div className="relative px-4 pb-3 sm:px-6">
331
+ <div className="flex gap-2 overflow-x-auto scrollbar-none pb-1">
332
+ {demoCategories.map((cat) => (
333
+ <button
334
+ key={cat.id}
335
+ onClick={() => { setActiveCategory(cat.id); setSearch(''); }}
336
+ className={cn(
337
+ 'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all',
338
+ activeCategory === cat.id
339
+ ? 'bg-white text-emerald-700 shadow-sm'
340
+ : 'bg-white/15 text-white hover:bg-white/25'
341
+ )}
342
+ >
343
+ {cat.name}
344
+ </button>
345
+ ))}
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ {/* Search */}
351
+ <div className="sticky top-0 z-20 border-b border-zinc-100 bg-white/80 px-4 py-3 backdrop-blur-xl sm:px-6 dark:border-zinc-800 dark:bg-zinc-950/80">
352
+ <div className="relative">
353
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
354
+ <Input
355
+ placeholder="Search the menu..."
356
+ value={search}
357
+ onChange={(e) => setSearch(e.target.value)}
358
+ className="pl-10 bg-zinc-50 border-zinc-100 dark:bg-zinc-900"
359
+ />
360
+ </div>
361
+ </div>
362
+
363
+ {/* Products */}
364
+ <div className="px-4 py-4 pb-32 sm:px-6">
365
+ {(search ? [{ id: 'search', name: 'Search Results' }] : demoCategories.filter((c) => !activeCategory || c.id === activeCategory)).map((cat) => {
366
+ const catProducts = filteredProducts.filter((p) =>
367
+ search ? true : p.category_id === cat.id
368
+ );
369
+ if (catProducts.length === 0) return null;
370
+
371
+ return (
372
+ <div key={cat.id} className="mb-8">
373
+ <h2 className="mb-4 text-lg font-bold text-zinc-900 dark:text-zinc-100">{cat.name}</h2>
374
+ <div className="grid gap-3 sm:grid-cols-2">
375
+ {catProducts.map((product) => (
376
+ <button
377
+ key={product.id}
378
+ onClick={() => setSelectedProduct(product)}
379
+ className="flex gap-3 rounded-2xl border border-zinc-100 bg-white p-3 text-left transition-all hover:border-zinc-200 hover:shadow-sm active:scale-[0.99] dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
380
+ >
381
+ {/* Image placeholder */}
382
+ <div className="flex h-20 w-20 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-zinc-50 to-zinc-100 dark:from-zinc-800 dark:to-zinc-700">
383
+ <UtensilsCrossed className="h-6 w-6 text-zinc-300 dark:text-zinc-600" />
384
+ </div>
385
+ <div className="flex-1 min-w-0">
386
+ <div className="flex items-center gap-1.5">
387
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 truncate">{product.name}</h3>
388
+ {product.is_featured && <Star className="h-3 w-3 shrink-0 fill-amber-400 text-amber-400" />}
389
+ </div>
390
+ <p className="mt-0.5 line-clamp-2 text-xs text-zinc-500">{product.description}</p>
391
+ <div className="mt-2 flex items-center justify-between">
392
+ <span className="text-sm font-bold text-emerald-600">{formatCurrency(product.price)}</span>
393
+ <div className="flex gap-1">
394
+ {product.tags?.includes('vegetarian') && <Leaf className="h-3.5 w-3.5 text-green-500" />}
395
+ {product.tags?.includes('popular') && <Flame className="h-3.5 w-3.5 text-orange-500" />}
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </button>
400
+ ))}
401
+ </div>
402
+ </div>
403
+ );
404
+ })}
405
+ </div>
406
+
407
+ {/* Floating Cart Button */}
408
+ {itemCount > 0 && (
409
+ <div className="fixed bottom-6 left-4 right-4 z-30 sm:left-auto sm:right-6 sm:w-80">
410
+ <Button
411
+ variant="primary"
412
+ size="xl"
413
+ className="w-full shadow-2xl shadow-emerald-500/30"
414
+ onClick={() => setShowCart(true)}
415
+ >
416
+ <ShoppingBag className="h-5 w-5" />
417
+ View Order ({itemCount})
418
+ <span className="ml-auto">{formatCurrency(useCartStore.getState().getTotal())}</span>
419
+ </Button>
420
+ </div>
421
+ )}
422
+
423
+ {/* Product Detail Sheet */}
424
+ {selectedProduct && (
425
+ <ProductDetailSheet
426
+ product={selectedProduct}
427
+ onClose={() => setSelectedProduct(null)}
428
+ onAdd={(qty) => addItem(selectedProduct, qty)}
429
+ />
430
+ )}
431
+
432
+ {/* Cart Sheet */}
433
+ {showCart && <CartSheet onClose={() => setShowCart(false)} />}
434
+ </div>
435
+ );
436
+ }
src/app/favicon.ico ADDED
src/app/globals.css ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #09090b;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-inter);
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --background: #09090b;
17
+ --foreground: #fafafa;
18
+ }
19
+ }
20
+
21
+ body {
22
+ background: var(--background);
23
+ color: var(--foreground);
24
+ font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
25
+ }
26
+
27
+ /* Scrollbar utilities */
28
+ .scrollbar-none::-webkit-scrollbar {
29
+ display: none;
30
+ }
31
+ .scrollbar-none {
32
+ -ms-overflow-style: none;
33
+ scrollbar-width: none;
34
+ }
35
+
36
+ .scrollbar-thin::-webkit-scrollbar {
37
+ width: 4px;
38
+ }
39
+ .scrollbar-thin::-webkit-scrollbar-track {
40
+ background: transparent;
41
+ }
42
+ .scrollbar-thin::-webkit-scrollbar-thumb {
43
+ background-color: #d4d4d8;
44
+ border-radius: 9999px;
45
+ }
46
+
47
+ /* Animation utilities */
48
+ @keyframes slide-in-from-bottom {
49
+ from {
50
+ transform: translateY(16px);
51
+ opacity: 0;
52
+ }
53
+ to {
54
+ transform: translateY(0);
55
+ opacity: 1;
56
+ }
57
+ }
58
+
59
+ .animate-in {
60
+ animation: slide-in-from-bottom 0.3s ease-out;
61
+ }
62
+
63
+ .slide-in-from-bottom-4 {
64
+ animation: slide-in-from-bottom 0.3s ease-out;
65
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const inter = Inter({
6
+ variable: "--font-inter",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ export const metadata: Metadata = {
11
+ title: "ScanMenu — Digital Menus & QR Ordering for Restaurants",
12
+ description: "Create stunning digital menus, generate QR codes, and let customers order instantly. The modern way to run your restaurant.",
13
+ keywords: ["restaurant", "digital menu", "QR code", "online ordering", "SaaS"],
14
+ };
15
+
16
+ export default function RootLayout({
17
+ children,
18
+ }: Readonly<{
19
+ children: React.ReactNode;
20
+ }>) {
21
+ return (
22
+ <html lang="en" className={`${inter.variable} h-full antialiased`}>
23
+ <body className="min-h-full flex flex-col font-sans">{children}</body>
24
+ </html>
25
+ );
26
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import {
7
+ Sparkles,
8
+ ArrowRight,
9
+ QrCode,
10
+ Smartphone,
11
+ BarChart3,
12
+ CreditCard,
13
+ ChefHat,
14
+ Zap,
15
+ Star,
16
+ Menu,
17
+ X,
18
+ ShoppingBag,
19
+ UtensilsCrossed,
20
+ } from 'lucide-react';
21
+ import { useState } from 'react';
22
+ import { cn } from '@/lib/utils';
23
+
24
+ function Navbar() {
25
+ const [open, setOpen] = useState(false);
26
+ return (
27
+ <nav className="fixed top-0 left-0 right-0 z-50 border-b border-zinc-200/50 bg-white/80 backdrop-blur-xl dark:border-zinc-800/50 dark:bg-zinc-950/80">
28
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6">
29
+ <Link href="/" className="flex items-center gap-2.5">
30
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500 shadow-sm">
31
+ <Sparkles className="h-4 w-4 text-white" />
32
+ </div>
33
+ <span className="text-lg font-bold tracking-tight">
34
+ Scan<span className="text-emerald-600">Menu</span>
35
+ </span>
36
+ </Link>
37
+
38
+ <div className="hidden items-center gap-8 md:flex">
39
+ <a href="#features" className="text-sm font-medium text-zinc-600 hover:text-zinc-900 transition-colors">Features</a>
40
+ <Link href="/pricing" className="text-sm font-medium text-zinc-600 hover:text-zinc-900 transition-colors">Pricing</Link>
41
+ <Link href="/restaurant/the-garden-kitchen" className="text-sm font-medium text-zinc-600 hover:text-zinc-900 transition-colors">Demo Menu</Link>
42
+ </div>
43
+
44
+ <div className="hidden items-center gap-3 md:flex">
45
+ <Link href="/login"><Button variant="ghost" size="sm">Sign In</Button></Link>
46
+ <Link href="/register"><Button variant="primary" size="sm">Get Started Free <ArrowRight className="h-3.5 w-3.5" /></Button></Link>
47
+ </div>
48
+
49
+ <button onClick={() => setOpen(!open)} className="md:hidden rounded-lg p-2 hover:bg-zinc-100">
50
+ {open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
51
+ </button>
52
+ </div>
53
+
54
+ {open && (
55
+ <div className="border-t border-zinc-200 bg-white p-4 md:hidden dark:border-zinc-800 dark:bg-zinc-950">
56
+ <div className="space-y-2">
57
+ <a href="#features" className="block rounded-lg px-3 py-2 text-sm font-medium text-zinc-600 hover:bg-zinc-50">Features</a>
58
+ <Link href="/pricing" className="block rounded-lg px-3 py-2 text-sm font-medium text-zinc-600 hover:bg-zinc-50">Pricing</Link>
59
+ <Link href="/restaurant/the-garden-kitchen" className="block rounded-lg px-3 py-2 text-sm font-medium text-zinc-600 hover:bg-zinc-50">Demo Menu</Link>
60
+ <div className="pt-2 space-y-2 border-t border-zinc-100">
61
+ <Link href="/login"><Button variant="outline" className="w-full">Sign In</Button></Link>
62
+ <Link href="/register"><Button variant="primary" className="w-full">Get Started Free</Button></Link>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ )}
67
+ </nav>
68
+ );
69
+ }
70
+
71
+ const features = [
72
+ { icon: QrCode, title: 'QR Code Menus', description: 'Generate unique QR codes for each table. Customers scan and browse your menu instantly — no app needed.' },
73
+ { icon: Smartphone, title: 'Mobile-First Design', description: 'Beautiful, responsive menus optimized for every screen size. Your menu looks amazing on any device.' },
74
+ { icon: ShoppingBag, title: 'Online Ordering', description: 'Accept dine-in, takeaway, and delivery orders directly. No commissions, no middlemen.' },
75
+ { icon: ChefHat, title: 'Kitchen Dashboard', description: 'Real-time order management for your staff. Track, prepare, and fulfill orders efficiently.' },
76
+ { icon: BarChart3, title: 'Smart Analytics', description: 'Understand your business with detailed insights on revenue, popular items, and peak hours.' },
77
+ { icon: CreditCard, title: 'Integrated Payments', description: 'Accept payments seamlessly with Stripe. Subscriptions, invoicing, and billing made simple.' },
78
+ ];
79
+
80
+ const stats = [
81
+ { value: '10K+', label: 'Restaurants' },
82
+ { value: '2.5M', label: 'Orders Processed' },
83
+ { value: '4.9★', label: 'Average Rating' },
84
+ { value: '32%', label: 'Revenue Increase' },
85
+ ];
86
+
87
+ const testimonials = [
88
+ { name: 'Maria Santos', role: 'Owner, Bella Cucina', text: 'ScanMenu completely transformed how we handle orders. Our customers love the simplicity, and we\'ve seen a 32% increase in average order value.' },
89
+ { name: 'David Park', role: 'Manager, Seoul Kitchen', text: 'The QR code ordering eliminated wait times. We now serve 40% more tables during peak hours. The analytics are incredibly useful.' },
90
+ { name: 'Sophie Laurent', role: 'Owner, Le Petit Bistro', text: 'Setting up was incredibly easy. Within an hour, we had our entire menu digitized with beautiful product pages and QR codes printed.' },
91
+ ];
92
+
93
+ export default function LandingPage() {
94
+ return (
95
+ <div className="min-h-screen bg-white dark:bg-zinc-950">
96
+ <Navbar />
97
+
98
+ {/* Hero */}
99
+ <section className="relative overflow-hidden pt-32 pb-20 sm:pt-40 sm:pb-28">
100
+ <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-emerald-100/50 via-transparent to-transparent dark:from-emerald-900/10" />
101
+ <div className="relative mx-auto max-w-6xl px-4 sm:px-6">
102
+ <div className="text-center">
103
+ <Badge variant="outline" className="mb-6 gap-1.5 rounded-full py-1.5 pl-1.5 pr-3 text-sm">
104
+ <span className="rounded-full bg-emerald-500 px-2 py-0.5 text-[10px] font-bold text-white">NEW</span>
105
+ Real-time order tracking is here
106
+ </Badge>
107
+
108
+ <h1 className="mx-auto max-w-4xl text-4xl font-bold tracking-tight text-zinc-900 sm:text-6xl lg:text-7xl dark:text-white">
109
+ Your restaurant menu,{' '}
110
+ <span className="bg-gradient-to-r from-emerald-600 to-cyan-500 bg-clip-text text-transparent">
111
+ reinvented
112
+ </span>
113
+ </h1>
114
+
115
+ <p className="mx-auto mt-6 max-w-2xl text-lg text-zinc-600 sm:text-xl dark:text-zinc-400">
116
+ Create stunning digital menus, generate QR codes, and let customers order instantly —
117
+ all without installing an app. Start in under 5 minutes.
118
+ </p>
119
+
120
+ <div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
121
+ <Link href="/register">
122
+ <Button variant="primary" size="xl" className="w-full sm:w-auto shadow-lg shadow-emerald-500/20">
123
+ Start Free Trial <ArrowRight className="h-4 w-4" />
124
+ </Button>
125
+ </Link>
126
+ <Link href="/restaurant/the-garden-kitchen">
127
+ <Button variant="outline" size="xl" className="w-full sm:w-auto">
128
+ <Smartphone className="h-4 w-4" /> View Demo Menu
129
+ </Button>
130
+ </Link>
131
+ </div>
132
+
133
+ <p className="mt-4 text-sm text-zinc-500">No credit card required • Free 14-day trial • Cancel anytime</p>
134
+ </div>
135
+
136
+ {/* Dashboard Preview */}
137
+ <div className="mt-16 sm:mt-20">
138
+ <div className="relative rounded-2xl border border-zinc-200/60 bg-zinc-900 p-2 shadow-2xl shadow-zinc-900/20 ring-1 ring-zinc-900/5 sm:rounded-3xl sm:p-3">
139
+ <div className="flex items-center gap-1.5 px-3 pb-2">
140
+ <div className="h-3 w-3 rounded-full bg-red-400" />
141
+ <div className="h-3 w-3 rounded-full bg-amber-400" />
142
+ <div className="h-3 w-3 rounded-full bg-emerald-400" />
143
+ <span className="ml-3 text-xs text-zinc-500">scanmenu.app/dashboard</span>
144
+ </div>
145
+ <div className="overflow-hidden rounded-xl bg-zinc-50 dark:bg-zinc-800">
146
+ <div className="grid grid-cols-4 gap-3 p-4">
147
+ {[
148
+ { label: 'Revenue', value: '$12,845', change: '+12.5%', color: 'text-emerald-600' },
149
+ { label: 'Orders', value: '347', change: '+8.2%', color: 'text-blue-600' },
150
+ { label: 'Avg. Order', value: '$37.02', change: '+3.1%', color: 'text-violet-600' },
151
+ { label: 'Active Tables', value: '5/8', change: '3 free', color: 'text-amber-600' },
152
+ ].map((stat) => (
153
+ <div key={stat.label} className="rounded-xl bg-white p-3 shadow-sm dark:bg-zinc-700">
154
+ <p className="text-[10px] font-medium text-zinc-500">{stat.label}</p>
155
+ <p className="mt-1 text-lg font-bold text-zinc-900 dark:text-white">{stat.value}</p>
156
+ <p className={cn('text-[10px] font-medium', stat.color)}>{stat.change}</p>
157
+ </div>
158
+ ))}
159
+ </div>
160
+ <div className="px-4 pb-4">
161
+ <div className="flex h-32 items-end gap-[3px] rounded-xl bg-white p-3 shadow-sm dark:bg-zinc-700">
162
+ {Array.from({ length: 30 }).map((_, i) => (
163
+ <div
164
+ key={i}
165
+ className="flex-1 rounded-t-sm bg-emerald-500/80"
166
+ style={{ height: `${30 + Math.random() * 70}%` }}
167
+ />
168
+ ))}
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </section>
176
+
177
+ {/* Stats */}
178
+ <section className="border-y border-zinc-200 bg-zinc-50 py-12 dark:border-zinc-800 dark:bg-zinc-900/50">
179
+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
180
+ <div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
181
+ {stats.map((stat) => (
182
+ <div key={stat.label} className="text-center">
183
+ <p className="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white">{stat.value}</p>
184
+ <p className="mt-1 text-sm font-medium text-zinc-500">{stat.label}</p>
185
+ </div>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ </section>
190
+
191
+ {/* Features */}
192
+ <section id="features" className="py-20 sm:py-28">
193
+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
194
+ <div className="text-center">
195
+ <Badge variant="outline" className="mb-4">Features</Badge>
196
+ <h2 className="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white">
197
+ Everything you need to go digital
198
+ </h2>
199
+ <p className="mx-auto mt-4 max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
200
+ From menu creation to order management, ScanMenu has you covered with powerful tools designed for modern restaurants.
201
+ </p>
202
+ </div>
203
+
204
+ <div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
205
+ {features.map((feature) => (
206
+ <div key={feature.title} className="group rounded-2xl border border-zinc-200/60 bg-white p-6 transition-all hover:border-zinc-300 hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700">
207
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600 transition-colors group-hover:bg-emerald-100 dark:bg-emerald-900/30 dark:group-hover:bg-emerald-900/50">
208
+ <feature.icon className="h-6 w-6" />
209
+ </div>
210
+ <h3 className="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">{feature.title}</h3>
211
+ <p className="mt-2 text-sm leading-relaxed text-zinc-500">{feature.description}</p>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ </div>
216
+ </section>
217
+
218
+ {/* How It Works */}
219
+ <section className="border-y border-zinc-200 bg-zinc-50 py-20 sm:py-28 dark:border-zinc-800 dark:bg-zinc-900/50">
220
+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
221
+ <div className="text-center">
222
+ <Badge variant="outline" className="mb-4">How It Works</Badge>
223
+ <h2 className="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white">
224
+ Up and running in minutes
225
+ </h2>
226
+ </div>
227
+
228
+ <div className="mt-16 grid gap-8 sm:grid-cols-3">
229
+ {[
230
+ { step: 1, icon: ChefHat, title: 'Create Your Menu', description: 'Add your restaurant, create categories, and upload your products with images, prices, and options.' },
231
+ { step: 2, icon: QrCode, title: 'Generate QR Codes', description: 'Create unique QR codes for each table or for takeaway/delivery. Print and display them in your restaurant.' },
232
+ { step: 3, icon: Zap, title: 'Start Receiving Orders', description: 'Customers scan, browse, and order instantly. Manage everything from your real-time dashboard.' },
233
+ ].map((item) => (
234
+ <div key={item.step} className="relative text-center">
235
+ <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-500 to-cyan-500 text-white shadow-lg shadow-emerald-500/30">
236
+ <item.icon className="h-7 w-7" />
237
+ </div>
238
+ <h3 className="mt-6 text-lg font-semibold text-zinc-900 dark:text-zinc-100">{item.title}</h3>
239
+ <p className="mt-2 text-sm text-zinc-500">{item.description}</p>
240
+ </div>
241
+ ))}
242
+ </div>
243
+ </div>
244
+ </section>
245
+
246
+ {/* Testimonials */}
247
+ <section className="py-20 sm:py-28">
248
+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
249
+ <div className="text-center">
250
+ <Badge variant="outline" className="mb-4">Testimonials</Badge>
251
+ <h2 className="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white">
252
+ Loved by restaurant owners
253
+ </h2>
254
+ </div>
255
+
256
+ <div className="mt-16 grid gap-6 sm:grid-cols-3">
257
+ {testimonials.map((t) => (
258
+ <div key={t.name} className="rounded-2xl border border-zinc-200/60 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900">
259
+ <div className="flex gap-0.5">
260
+ {[1, 2, 3, 4, 5].map((i) => (
261
+ <Star key={i} className="h-4 w-4 fill-amber-400 text-amber-400" />
262
+ ))}
263
+ </div>
264
+ <p className="mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">&ldquo;{t.text}&rdquo;</p>
265
+ <div className="mt-4 flex items-center gap-3">
266
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 text-xs font-bold text-white">
267
+ {t.name.split(' ').map((n) => n[0]).join('')}
268
+ </div>
269
+ <div>
270
+ <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{t.name}</p>
271
+ <p className="text-xs text-zinc-500">{t.role}</p>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ ))}
276
+ </div>
277
+ </div>
278
+ </section>
279
+
280
+ {/* CTA */}
281
+ <section className="border-t border-zinc-200 dark:border-zinc-800">
282
+ <div className="mx-auto max-w-6xl px-4 py-20 sm:px-6 sm:py-28">
283
+ <div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-900 px-8 py-16 text-center shadow-2xl sm:px-16 dark:from-zinc-800 dark:via-zinc-900 dark:to-zinc-800">
284
+ <div className="relative">
285
+ <h2 className="text-3xl font-bold text-white sm:text-4xl">
286
+ Ready to transform your restaurant?
287
+ </h2>
288
+ <p className="mx-auto mt-4 max-w-xl text-lg text-zinc-400">
289
+ Join thousands of restaurants using ScanMenu to increase revenue, reduce wait times, and delight customers.
290
+ </p>
291
+ <div className="mt-8 flex flex-col items-center justify-center gap-4 sm:flex-row">
292
+ <Link href="/register">
293
+ <Button variant="primary" size="xl" className="w-full sm:w-auto">
294
+ Start Free Trial <ArrowRight className="h-4 w-4" />
295
+ </Button>
296
+ </Link>
297
+ <Link href="/restaurant/the-garden-kitchen">
298
+ <Button variant="outline" size="xl" className="w-full border-zinc-700 text-white hover:bg-zinc-800 hover:text-white sm:w-auto">
299
+ View Live Demo
300
+ </Button>
301
+ </Link>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </section>
307
+
308
+ {/* Footer */}
309
+ <footer className="border-t border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900/50">
310
+ <div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
311
+ <div className="grid gap-8 sm:grid-cols-4">
312
+ <div>
313
+ <Link href="/" className="flex items-center gap-2">
314
+ <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500">
315
+ <Sparkles className="h-3.5 w-3.5 text-white" />
316
+ </div>
317
+ <span className="text-base font-bold">Scan<span className="text-emerald-600">Menu</span></span>
318
+ </Link>
319
+ <p className="mt-3 text-sm text-zinc-500">Digital menus and QR ordering for modern restaurants.</p>
320
+ </div>
321
+ <div>
322
+ <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Product</h4>
323
+ <div className="mt-3 space-y-2 text-sm text-zinc-500">
324
+ <p className="hover:text-zinc-700 cursor-pointer">Features</p>
325
+ <p className="hover:text-zinc-700 cursor-pointer">Pricing</p>
326
+ <p className="hover:text-zinc-700 cursor-pointer">Demo</p>
327
+ <p className="hover:text-zinc-700 cursor-pointer">Changelog</p>
328
+ </div>
329
+ </div>
330
+ <div>
331
+ <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Company</h4>
332
+ <div className="mt-3 space-y-2 text-sm text-zinc-500">
333
+ <p className="hover:text-zinc-700 cursor-pointer">About</p>
334
+ <p className="hover:text-zinc-700 cursor-pointer">Blog</p>
335
+ <p className="hover:text-zinc-700 cursor-pointer">Careers</p>
336
+ <p className="hover:text-zinc-700 cursor-pointer">Contact</p>
337
+ </div>
338
+ </div>
339
+ <div>
340
+ <h4 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Legal</h4>
341
+ <div className="mt-3 space-y-2 text-sm text-zinc-500">
342
+ <p className="hover:text-zinc-700 cursor-pointer">Privacy</p>
343
+ <p className="hover:text-zinc-700 cursor-pointer">Terms</p>
344
+ <p className="hover:text-zinc-700 cursor-pointer">Security</p>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ <div className="mt-8 border-t border-zinc-200 pt-8 text-center text-sm text-zinc-400 dark:border-zinc-800">
349
+ &copy; 2025 ScanMenu. All rights reserved.
350
+ </div>
351
+ </div>
352
+ </footer>
353
+ </div>
354
+ );
355
+ }
src/app/pricing/page.tsx ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Badge } from '@/components/ui/badge';
6
+ import {
7
+ Sparkles,
8
+ ArrowRight,
9
+ ArrowLeft,
10
+ Check,
11
+ Zap,
12
+ Crown,
13
+ Building2,
14
+ HelpCircle,
15
+ } from 'lucide-react';
16
+ import { useState } from 'react';
17
+ import { cn } from '@/lib/utils';
18
+
19
+ const plans = [
20
+ {
21
+ name: 'Free',
22
+ price: 0,
23
+ yearlyPrice: 0,
24
+ description: 'Try ScanMenu for free with basic features.',
25
+ icon: Sparkles,
26
+ features: [
27
+ '1 restaurant',
28
+ 'Up to 15 menu items',
29
+ '2 QR codes',
30
+ 'Basic ordering',
31
+ '50 orders/month',
32
+ 'Community support',
33
+ ],
34
+ cta: 'Get Started',
35
+ popular: false,
36
+ },
37
+ {
38
+ name: 'Starter',
39
+ price: 29,
40
+ yearlyPrice: 24,
41
+ description: 'Perfect for small restaurants just getting started.',
42
+ icon: Zap,
43
+ features: [
44
+ '1 restaurant',
45
+ 'Up to 50 menu items',
46
+ '10 QR codes',
47
+ 'Dine-in + Takeaway',
48
+ 'Basic analytics',
49
+ 'Email support',
50
+ '500 orders/month',
51
+ 'Custom branding',
52
+ ],
53
+ cta: 'Start Trial',
54
+ popular: false,
55
+ },
56
+ {
57
+ name: 'Pro',
58
+ price: 79,
59
+ yearlyPrice: 66,
60
+ description: 'For growing restaurants with advanced needs.',
61
+ icon: Crown,
62
+ features: [
63
+ 'Unlimited restaurants',
64
+ 'Unlimited menu items',
65
+ 'Unlimited QR codes',
66
+ 'Dine-in + Takeaway + Delivery',
67
+ 'Advanced analytics',
68
+ 'Priority support',
69
+ 'Unlimited orders',
70
+ 'Custom branding',
71
+ 'API access',
72
+ 'Team members (up to 10)',
73
+ 'Real-time order tracking',
74
+ 'Product options & extras',
75
+ ],
76
+ cta: 'Start Trial',
77
+ popular: true,
78
+ },
79
+ {
80
+ name: 'Enterprise',
81
+ price: 199,
82
+ yearlyPrice: 166,
83
+ description: 'For multi-location chains and franchises.',
84
+ icon: Building2,
85
+ features: [
86
+ 'Everything in Pro',
87
+ 'Multi-location dashboard',
88
+ 'Unlimited team members',
89
+ 'Dedicated account manager',
90
+ 'Custom integrations',
91
+ '99.9% SLA guarantee',
92
+ 'White-label solution',
93
+ 'Onboarding training',
94
+ 'Advanced security',
95
+ 'Custom API limits',
96
+ 'Phone support',
97
+ 'Invoice billing',
98
+ ],
99
+ cta: 'Contact Sales',
100
+ popular: false,
101
+ },
102
+ ];
103
+
104
+ const faq = [
105
+ { q: 'Can I try ScanMenu for free?', a: 'Yes! Our Free plan lets you try ScanMenu with basic features at no cost. We also offer a 14-day free trial on all paid plans — no credit card required.' },
106
+ { q: 'How do QR codes work?', a: 'Each QR code is linked to a specific table or order type (takeaway/delivery). Customers scan with their phone camera and instantly see your menu — no app download needed.' },
107
+ { q: 'Can I switch plans later?', a: 'Absolutely! You can upgrade or downgrade your plan at any time. Changes take effect at the start of your next billing cycle, and we prorate any differences.' },
108
+ { q: 'Do customers need to install an app?', a: 'No! That\'s the beauty of ScanMenu. Customers simply scan a QR code and your menu opens in their phone\'s browser. Zero friction, zero downloads.' },
109
+ { q: 'What payment methods do you accept?', a: 'We accept all major credit cards (Visa, Mastercard, Amex, Discover) through Stripe. Enterprise customers can also pay via invoice.' },
110
+ { q: 'Is my data secure?', a: 'Yes. We use Supabase (built on PostgreSQL) with row-level security, SSL encryption, and regular backups. Your data is safe and compliant.' },
111
+ ];
112
+
113
+ export default function PricingPage() {
114
+ const [annual, setAnnual] = useState(false);
115
+ const [openFaq, setOpenFaq] = useState<number | null>(0);
116
+
117
+ return (
118
+ <div className="min-h-screen bg-white dark:bg-zinc-950">
119
+ {/* Nav */}
120
+ <nav className="border-b border-zinc-200/50 bg-white/80 backdrop-blur-xl dark:border-zinc-800/50 dark:bg-zinc-950/80">
121
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6">
122
+ <Link href="/" className="flex items-center gap-2.5">
123
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500 shadow-sm">
124
+ <Sparkles className="h-4 w-4 text-white" />
125
+ </div>
126
+ <span className="text-lg font-bold tracking-tight">Scan<span className="text-emerald-600">Menu</span></span>
127
+ </Link>
128
+ <div className="flex items-center gap-3">
129
+ <Link href="/login"><Button variant="ghost" size="sm">Sign In</Button></Link>
130
+ <Link href="/register"><Button variant="primary" size="sm">Get Started</Button></Link>
131
+ </div>
132
+ </div>
133
+ </nav>
134
+
135
+ {/* Header */}
136
+ <section className="pt-16 pb-8 text-center">
137
+ <div className="mx-auto max-w-3xl px-4">
138
+ <Badge variant="outline" className="mb-4">Pricing</Badge>
139
+ <h1 className="text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-white">
140
+ Simple, transparent pricing
141
+ </h1>
142
+ <p className="mt-4 text-lg text-zinc-600 dark:text-zinc-400">
143
+ Choose the plan that fits your restaurant. Upgrade, downgrade, or cancel anytime.
144
+ </p>
145
+ </div>
146
+ </section>
147
+
148
+ {/* Toggle */}
149
+ <div className="flex items-center justify-center gap-3 pb-12">
150
+ <span className={cn('text-sm font-medium transition-colors', !annual ? 'text-zinc-900' : 'text-zinc-500')}>Monthly</span>
151
+ <button
152
+ onClick={() => setAnnual(!annual)}
153
+ className={cn('relative h-6 w-11 rounded-full transition-colors', annual ? 'bg-emerald-500' : 'bg-zinc-300')}
154
+ >
155
+ <span className={cn('absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform', annual && 'translate-x-5')} />
156
+ </button>
157
+ <span className={cn('text-sm font-medium transition-colors', annual ? 'text-zinc-900' : 'text-zinc-500')}>
158
+ Annual <Badge variant="success" className="ml-1 text-[10px]">Save 17%</Badge>
159
+ </span>
160
+ </div>
161
+
162
+ {/* Plans */}
163
+ <section className="pb-20">
164
+ <div className="mx-auto max-w-6xl px-4 sm:px-6">
165
+ <div className="grid gap-6 lg:grid-cols-4">
166
+ {plans.map((plan) => (
167
+ <div
168
+ key={plan.name}
169
+ className={cn(
170
+ 'relative flex flex-col rounded-2xl border bg-white p-6 transition-all hover:shadow-lg dark:bg-zinc-900',
171
+ plan.popular
172
+ ? 'border-emerald-500 shadow-lg ring-1 ring-emerald-500/20'
173
+ : 'border-zinc-200 dark:border-zinc-800'
174
+ )}
175
+ >
176
+ {plan.popular && (
177
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2">
178
+ <Badge className="bg-emerald-500 text-white border-0 px-3 py-1">Most Popular</Badge>
179
+ </div>
180
+ )}
181
+
182
+ <div className="mb-6">
183
+ <div className="flex items-center gap-2 mb-3">
184
+ <div className={cn(
185
+ 'flex h-9 w-9 items-center justify-center rounded-xl',
186
+ plan.popular ? 'bg-emerald-100 text-emerald-600' : 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800'
187
+ )}>
188
+ <plan.icon className="h-4 w-4" />
189
+ </div>
190
+ <h3 className="text-lg font-bold">{plan.name}</h3>
191
+ </div>
192
+ <p className="text-sm text-zinc-500 mb-4">{plan.description}</p>
193
+ <div className="flex items-baseline gap-1">
194
+ <span className="text-4xl font-bold text-zinc-900 dark:text-white">
195
+ ${annual ? plan.yearlyPrice : plan.price}
196
+ </span>
197
+ {plan.price > 0 && <span className="text-zinc-500">/month</span>}
198
+ </div>
199
+ {annual && plan.price > 0 && (
200
+ <p className="mt-1 text-xs text-emerald-600">
201
+ ${plan.yearlyPrice * 12}/year (save ${(plan.price - plan.yearlyPrice) * 12})
202
+ </p>
203
+ )}
204
+ </div>
205
+
206
+ <Link href="/register" className="block mb-6">
207
+ <Button
208
+ variant={plan.popular ? 'primary' : 'outline'}
209
+ className="w-full"
210
+ >
211
+ {plan.cta} {plan.price > 0 && <ArrowRight className="h-4 w-4" />}
212
+ </Button>
213
+ </Link>
214
+
215
+ <div className="flex-1 space-y-3">
216
+ {plan.features.map((feature) => (
217
+ <div key={feature} className="flex items-start gap-2 text-sm">
218
+ <Check className={cn('h-4 w-4 shrink-0 mt-0.5', plan.popular ? 'text-emerald-500' : 'text-zinc-400')} />
219
+ <span className="text-zinc-600 dark:text-zinc-400">{feature}</span>
220
+ </div>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ ))}
225
+ </div>
226
+ </div>
227
+ </section>
228
+
229
+ {/* FAQ */}
230
+ <section className="border-t border-zinc-200 bg-zinc-50 py-20 dark:border-zinc-800 dark:bg-zinc-900/50">
231
+ <div className="mx-auto max-w-3xl px-4 sm:px-6">
232
+ <div className="text-center mb-12">
233
+ <Badge variant="outline" className="mb-4">FAQ</Badge>
234
+ <h2 className="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">Frequently asked questions</h2>
235
+ </div>
236
+
237
+ <div className="space-y-3">
238
+ {faq.map((item, i) => (
239
+ <div key={i} className="rounded-2xl border border-zinc-200 bg-white overflow-hidden dark:border-zinc-800 dark:bg-zinc-900">
240
+ <button
241
+ onClick={() => setOpenFaq(openFaq === i ? null : i)}
242
+ className="flex w-full items-center justify-between p-5 text-left"
243
+ >
244
+ <span className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{item.q}</span>
245
+ <HelpCircle className={cn('h-4 w-4 shrink-0 text-zinc-400 transition-transform', openFaq === i && 'rotate-180')} />
246
+ </button>
247
+ {openFaq === i && (
248
+ <div className="px-5 pb-5 pt-0">
249
+ <p className="text-sm leading-relaxed text-zinc-500">{item.a}</p>
250
+ </div>
251
+ )}
252
+ </div>
253
+ ))}
254
+ </div>
255
+ </div>
256
+ </section>
257
+
258
+ {/* Footer CTA */}
259
+ <section className="py-16 text-center">
260
+ <div className="mx-auto max-w-2xl px-4">
261
+ <h2 className="text-2xl font-bold text-zinc-900 dark:text-white">Still have questions?</h2>
262
+ <p className="mt-2 text-zinc-500">We&apos;re here to help. Contact our team for a personalized demo.</p>
263
+ <div className="mt-6 flex justify-center gap-3">
264
+ <Link href="/register"><Button variant="primary">Start Free Trial</Button></Link>
265
+ <Button variant="outline">Contact Sales</Button>
266
+ </div>
267
+ </div>
268
+ </section>
269
+ </div>
270
+ );
271
+ }
src/components/layout/dashboard-header.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { Bell, Search, ChevronDown } from 'lucide-react';
4
+ import { getInitials } from '@/lib/utils';
5
+ import { demoUser, demoRestaurant } from '@/lib/demo-data';
6
+
7
+ export function DashboardHeader() {
8
+ return (
9
+ <header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-zinc-200/60 bg-white/80 px-4 backdrop-blur-xl sm:px-6 dark:border-zinc-800 dark:bg-zinc-950/80">
10
+ {/* Search */}
11
+ <div className="flex items-center gap-3">
12
+ <div className="relative hidden sm:block">
13
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
14
+ <input
15
+ type="text"
16
+ placeholder="Search menus, orders, products..."
17
+ className="h-9 w-72 rounded-xl border border-zinc-200 bg-zinc-50 pl-9 pr-4 text-sm placeholder:text-zinc-400 focus:border-emerald-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-emerald-500/20 transition-all dark:border-zinc-700 dark:bg-zinc-900"
18
+ />
19
+ <kbd className="absolute right-3 top-1/2 -translate-y-1/2 hidden rounded-md border border-zinc-200 bg-white px-1.5 py-0.5 text-[10px] font-medium text-zinc-400 sm:inline-block dark:border-zinc-700 dark:bg-zinc-800">
20
+ ⌘K
21
+ </kbd>
22
+ </div>
23
+ </div>
24
+
25
+ {/* Right side */}
26
+ <div className="flex items-center gap-2">
27
+ {/* Restaurant switcher */}
28
+ <button className="hidden items-center gap-2 rounded-xl border border-zinc-200 px-3 py-2 text-sm font-medium text-zinc-700 transition-all hover:bg-zinc-50 sm:flex dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800">
29
+ <div className="h-5 w-5 rounded-md bg-gradient-to-br from-emerald-500 to-cyan-500" />
30
+ <span className="max-w-[120px] truncate">{demoRestaurant.name}</span>
31
+ <ChevronDown className="h-3.5 w-3.5 text-zinc-400" />
32
+ </button>
33
+
34
+ {/* Notifications */}
35
+ <button className="relative rounded-xl p-2.5 text-zinc-500 transition-all hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800">
36
+ <Bell className="h-[18px] w-[18px]" />
37
+ <span className="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-emerald-500 ring-2 ring-white dark:ring-zinc-950" />
38
+ </button>
39
+
40
+ {/* Avatar */}
41
+ <button className="flex items-center gap-2 rounded-xl p-1.5 transition-all hover:bg-zinc-100 dark:hover:bg-zinc-800">
42
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500 text-xs font-bold text-white shadow-sm">
43
+ {getInitials(demoUser.full_name)}
44
+ </div>
45
+ </button>
46
+ </div>
47
+ </header>
48
+ );
49
+ }
src/components/layout/dashboard-layout.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { DashboardSidebar } from './dashboard-sidebar';
4
+ import { DashboardHeader } from './dashboard-header';
5
+
6
+ export function DashboardLayout({ children }: { children: React.ReactNode }) {
7
+ return (
8
+ <div className="min-h-screen bg-zinc-50 dark:bg-zinc-950">
9
+ <DashboardSidebar />
10
+ <div className="lg:pl-[260px] transition-all duration-300">
11
+ <DashboardHeader />
12
+ <main className="p-4 sm:p-6 lg:p-8">{children}</main>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
src/components/layout/dashboard-sidebar.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { cn } from '@/lib/utils';
6
+ import {
7
+ LayoutDashboard,
8
+ UtensilsCrossed,
9
+ QrCode,
10
+ ShoppingBag,
11
+ BarChart3,
12
+ CreditCard,
13
+ Settings,
14
+ Store,
15
+ ChevronLeft,
16
+ Menu,
17
+ LogOut,
18
+ Sparkles,
19
+ } from 'lucide-react';
20
+ import { useState } from 'react';
21
+
22
+ const navigation = [
23
+ { name: 'Overview', href: '/dashboard/overview', icon: LayoutDashboard },
24
+ { name: 'Menu Builder', href: '/dashboard/menu-builder', icon: UtensilsCrossed },
25
+ { name: 'QR Codes', href: '/dashboard/qr-manager', icon: QrCode },
26
+ { name: 'Orders', href: '/dashboard/orders', icon: ShoppingBag },
27
+ { name: 'Analytics', href: '/dashboard/analytics', icon: BarChart3 },
28
+ { name: 'Billing', href: '/dashboard/billing', icon: CreditCard },
29
+ { name: 'Restaurant', href: '/dashboard/restaurant-setup', icon: Store },
30
+ { name: 'Settings', href: '/dashboard/settings', icon: Settings },
31
+ ];
32
+
33
+ export function DashboardSidebar() {
34
+ const pathname = usePathname();
35
+ const [collapsed, setCollapsed] = useState(false);
36
+ const [mobileOpen, setMobileOpen] = useState(false);
37
+
38
+ return (
39
+ <>
40
+ {/* Mobile menu button */}
41
+ <button
42
+ onClick={() => setMobileOpen(true)}
43
+ className="fixed left-4 top-4 z-50 rounded-xl border border-zinc-200 bg-white p-2.5 shadow-sm lg:hidden dark:border-zinc-700 dark:bg-zinc-900"
44
+ >
45
+ <Menu className="h-5 w-5 text-zinc-600" />
46
+ </button>
47
+
48
+ {/* Mobile overlay */}
49
+ {mobileOpen && (
50
+ <div
51
+ className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm lg:hidden"
52
+ onClick={() => setMobileOpen(false)}
53
+ />
54
+ )}
55
+
56
+ {/* Sidebar */}
57
+ <aside
58
+ className={cn(
59
+ 'fixed inset-y-0 left-0 z-50 flex flex-col border-r border-zinc-200/60 bg-white transition-all duration-300 dark:border-zinc-800 dark:bg-zinc-950',
60
+ collapsed ? 'w-[72px]' : 'w-[260px]',
61
+ mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
62
+ )}
63
+ >
64
+ {/* Logo */}
65
+ <div className={cn('flex h-16 items-center border-b border-zinc-200/60 px-4 dark:border-zinc-800', collapsed && 'justify-center')}>
66
+ <Link href="/dashboard/overview" className="flex items-center gap-2.5">
67
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500 shadow-sm">
68
+ <Sparkles className="h-4 w-4 text-white" />
69
+ </div>
70
+ {!collapsed && (
71
+ <span className="text-lg font-bold tracking-tight text-zinc-900 dark:text-white">
72
+ Scan<span className="text-emerald-600">Menu</span>
73
+ </span>
74
+ )}
75
+ </Link>
76
+ </div>
77
+
78
+ {/* Navigation */}
79
+ <nav className="flex-1 space-y-1 overflow-y-auto px-3 py-4">
80
+ {navigation.map((item) => {
81
+ const isActive = pathname === item.href || pathname?.startsWith(item.href + '/');
82
+ return (
83
+ <Link
84
+ key={item.name}
85
+ href={item.href}
86
+ onClick={() => setMobileOpen(false)}
87
+ className={cn(
88
+ 'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-150',
89
+ isActive
90
+ ? 'bg-zinc-900 text-white shadow-sm dark:bg-white dark:text-zinc-900'
91
+ : 'text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-white',
92
+ collapsed && 'justify-center px-2'
93
+ )}
94
+ >
95
+ <item.icon className={cn('h-[18px] w-[18px] shrink-0', isActive ? 'text-current' : 'text-zinc-400 group-hover:text-zinc-600 dark:group-hover:text-zinc-300')} />
96
+ {!collapsed && <span>{item.name}</span>}
97
+ </Link>
98
+ );
99
+ })}
100
+ </nav>
101
+
102
+ {/* Footer */}
103
+ <div className="border-t border-zinc-200/60 p-3 dark:border-zinc-800">
104
+ {!collapsed && (
105
+ <div className="mb-3 rounded-xl bg-gradient-to-br from-emerald-50 to-cyan-50 p-3 dark:from-emerald-950/30 dark:to-cyan-950/30">
106
+ <p className="text-xs font-semibold text-emerald-700 dark:text-emerald-400">Pro Plan</p>
107
+ <p className="mt-0.5 text-[11px] text-emerald-600/70 dark:text-emerald-500/70">Unlimited menus & orders</p>
108
+ </div>
109
+ )}
110
+ <button
111
+ className={cn(
112
+ 'flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium text-zinc-500 transition-all hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800',
113
+ collapsed && 'justify-center px-2'
114
+ )}
115
+ >
116
+ <LogOut className="h-[18px] w-[18px]" />
117
+ {!collapsed && <span>Sign Out</span>}
118
+ </button>
119
+
120
+ {/* Collapse toggle — desktop only */}
121
+ <button
122
+ onClick={() => setCollapsed(!collapsed)}
123
+ className="mt-2 hidden w-full items-center justify-center rounded-xl p-2 text-zinc-400 transition-all hover:bg-zinc-100 hover:text-zinc-600 lg:flex dark:hover:bg-zinc-800"
124
+ >
125
+ <ChevronLeft className={cn('h-4 w-4 transition-transform', collapsed && 'rotate-180')} />
126
+ </button>
127
+ </div>
128
+ </aside>
129
+ </>
130
+ );
131
+ }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'border-transparent bg-zinc-900 text-white dark:bg-zinc-50 dark:text-zinc-900',
11
+ secondary: 'border-transparent bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100',
12
+ success: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-400',
13
+ warning: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400',
14
+ destructive: 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-400',
15
+ info: 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-400',
16
+ outline: 'border-zinc-200 text-zinc-700 dark:border-zinc-700 dark:text-zinc-300',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: 'default',
21
+ },
22
+ }
23
+ );
24
+
25
+ export interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLDivElement>,
27
+ VariantProps<typeof badgeVariants> {}
28
+
29
+ function Badge({ className, variant, ...props }: BadgeProps) {
30
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
31
+ }
32
+
33
+ export { Badge, badgeVariants };
src/components/ui/button.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const buttonVariants = cva(
7
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-zinc-900 text-white shadow-sm hover:bg-zinc-800 focus-visible:ring-zinc-500 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-100',
12
+ primary: 'bg-emerald-600 text-white shadow-sm hover:bg-emerald-500 focus-visible:ring-emerald-500',
13
+ destructive: 'bg-red-600 text-white shadow-sm hover:bg-red-500 focus-visible:ring-red-500',
14
+ outline: 'border border-zinc-200 bg-white text-zinc-900 shadow-sm hover:bg-zinc-50 hover:border-zinc-300 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800',
15
+ secondary: 'bg-zinc-100 text-zinc-900 shadow-sm hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700',
16
+ ghost: 'text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100',
17
+ link: 'text-emerald-600 underline-offset-4 hover:underline dark:text-emerald-400',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-8 rounded-lg px-3 text-xs',
22
+ lg: 'h-12 rounded-xl px-6 text-base',
23
+ xl: 'h-14 rounded-2xl px-8 text-base',
24
+ icon: 'h-10 w-10',
25
+ 'icon-sm': 'h-8 w-8 rounded-lg',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: 'default',
30
+ size: 'default',
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface ButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ asChild?: boolean;
39
+ }
40
+
41
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
43
+ const Comp = asChild ? Slot : 'button';
44
+ return (
45
+ <Comp
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+ Button.displayName = 'Button';
54
+
55
+ export { Button, buttonVariants };
src/components/ui/card.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn(
9
+ 'rounded-2xl border border-zinc-200/60 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900',
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ )
15
+ );
16
+ Card.displayName = 'Card';
17
+
18
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
19
+ ({ className, ...props }, ref) => (
20
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
21
+ )
22
+ );
23
+ CardHeader.displayName = 'CardHeader';
24
+
25
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
26
+ ({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
28
+ )
29
+ );
30
+ CardTitle.displayName = 'CardTitle';
31
+
32
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
33
+ ({ className, ...props }, ref) => (
34
+ <div ref={ref} className={cn('text-sm text-zinc-500 dark:text-zinc-400', className)} {...props} />
35
+ )
36
+ );
37
+ CardDescription.displayName = 'CardDescription';
38
+
39
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
40
+ ({ className, ...props }, ref) => (
41
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
42
+ )
43
+ );
44
+ CardContent.displayName = 'CardContent';
45
+
46
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
47
+ ({ className, ...props }, ref) => (
48
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
49
+ )
50
+ );
51
+ CardFooter.displayName = 'CardFooter';
52
+
53
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
src/components/ui/empty-state.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LucideIcon } from 'lucide-react';
2
+ import { Button } from './button';
3
+
4
+ interface EmptyStateProps {
5
+ icon: LucideIcon;
6
+ title: string;
7
+ description: string;
8
+ action?: {
9
+ label: string;
10
+ onClick: () => void;
11
+ };
12
+ }
13
+
14
+ export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
15
+ return (
16
+ <div className="flex flex-col items-center justify-center py-16 px-4">
17
+ <div className="rounded-2xl bg-zinc-100 p-4 dark:bg-zinc-800">
18
+ <Icon className="h-8 w-8 text-zinc-400" />
19
+ </div>
20
+ <h3 className="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">{title}</h3>
21
+ <p className="mt-2 max-w-sm text-center text-sm text-zinc-500 dark:text-zinc-400">{description}</p>
22
+ {action && (
23
+ <Button variant="primary" className="mt-6" onClick={action.onClick}>
24
+ {action.label}
25
+ </Button>
26
+ )}
27
+ </div>
28
+ );
29
+ }
src/components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => {
6
+ return (
7
+ <input
8
+ type={type}
9
+ className={cn(
10
+ 'flex h-10 w-full rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-0 focus-visible:border-emerald-500 disabled:cursor-not-allowed disabled:opacity-50 transition-all dark:border-zinc-700 dark:bg-zinc-900 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-500',
11
+ className
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+ );
19
+ Input.displayName = 'Input';
20
+
21
+ export { Input };
src/components/ui/stat-card.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { LucideIcon } from 'lucide-react';
5
+
6
+ interface StatCardProps {
7
+ title: string;
8
+ value: string;
9
+ change?: string;
10
+ changeType?: 'positive' | 'negative' | 'neutral';
11
+ icon: LucideIcon;
12
+ iconColor?: string;
13
+ }
14
+
15
+ export function StatCard({ title, value, change, changeType = 'neutral', icon: Icon, iconColor = 'text-emerald-600' }: StatCardProps) {
16
+ return (
17
+ <div className="group relative overflow-hidden rounded-2xl border border-zinc-200/60 bg-white p-6 shadow-sm transition-all hover:shadow-md hover:border-zinc-300/60 dark:border-zinc-800 dark:bg-zinc-900">
18
+ <div className="flex items-start justify-between">
19
+ <div className="space-y-2">
20
+ <p className="text-sm font-medium text-zinc-500 dark:text-zinc-400">{title}</p>
21
+ <p className="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">{value}</p>
22
+ {change && (
23
+ <p className={cn(
24
+ 'text-sm font-medium',
25
+ changeType === 'positive' && 'text-emerald-600',
26
+ changeType === 'negative' && 'text-red-600',
27
+ changeType === 'neutral' && 'text-zinc-500'
28
+ )}>
29
+ {change}
30
+ </p>
31
+ )}
32
+ </div>
33
+ <div className={cn('rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800', iconColor)}>
34
+ <Icon className="h-5 w-5" />
35
+ </div>
36
+ </div>
37
+ <div className="absolute inset-x-0 bottom-0 h-0.5 bg-gradient-to-r from-emerald-500 to-cyan-500 opacity-0 transition-opacity group-hover:opacity-100" />
38
+ </div>
39
+ );
40
+ }
src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
5
+ ({ className, ...props }, ref) => {
6
+ return (
7
+ <textarea
8
+ className={cn(
9
+ 'flex min-h-[80px] w-full rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-0 focus-visible:border-emerald-500 disabled:cursor-not-allowed disabled:opacity-50 transition-all dark:border-zinc-700 dark:bg-zinc-900',
10
+ className
11
+ )}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+ );
18
+ Textarea.displayName = 'Textarea';
19
+
20
+ export { Textarea };
src/lib/demo-data.ts ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // ScanMenu – Demo Data Store
3
+ // Simulates Supabase backend for the demo
4
+ // ============================================================
5
+
6
+ import { Restaurant, Menu, Category, Product, ProductOption, ProductOptionItem, Table, QRCode, Order, OrderItem, Subscription, User } from '@/types/database';
7
+
8
+ export const demoUser: User = {
9
+ id: 'user-1',
10
+ email: 'demo@scanmenu.app',
11
+ full_name: 'Alex Rivera',
12
+ avatar_url: undefined,
13
+ role: 'owner',
14
+ created_at: '2025-01-15T10:00:00Z',
15
+ updated_at: '2025-03-20T14:30:00Z',
16
+ };
17
+
18
+ export const demoRestaurant: Restaurant = {
19
+ id: 'rest-1',
20
+ owner_id: 'user-1',
21
+ name: 'The Garden Kitchen',
22
+ slug: 'the-garden-kitchen',
23
+ description: 'Farm-to-table dining with seasonal menus, craft cocktails, and a lush garden terrace. Experience the freshest ingredients in every dish.',
24
+ logo_url: undefined,
25
+ cover_image_url: undefined,
26
+ address: '742 Evergreen Terrace, San Francisco, CA 94102',
27
+ phone: '+1 (415) 555-0123',
28
+ email: 'hello@gardenkitchen.com',
29
+ website: 'https://gardenkitchen.com',
30
+ currency: 'USD',
31
+ timezone: 'America/Los_Angeles',
32
+ is_active: true,
33
+ opening_hours: {
34
+ monday: { open: '11:00', close: '22:00' },
35
+ tuesday: { open: '11:00', close: '22:00' },
36
+ wednesday: { open: '11:00', close: '22:00' },
37
+ thursday: { open: '11:00', close: '23:00' },
38
+ friday: { open: '11:00', close: '23:30' },
39
+ saturday: { open: '10:00', close: '23:30' },
40
+ sunday: { open: '10:00', close: '21:00' },
41
+ },
42
+ theme_color: '#10b981',
43
+ created_at: '2025-01-15T10:00:00Z',
44
+ updated_at: '2025-03-20T14:30:00Z',
45
+ };
46
+
47
+ export const demoMenu: Menu = {
48
+ id: 'menu-1',
49
+ restaurant_id: 'rest-1',
50
+ name: 'Main Menu',
51
+ description: 'Our full dining menu',
52
+ is_active: true,
53
+ sort_order: 0,
54
+ created_at: '2025-01-15T10:00:00Z',
55
+ updated_at: '2025-03-20T14:30:00Z',
56
+ };
57
+
58
+ export const demoCategories: Category[] = [
59
+ { id: 'cat-1', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Starters', description: 'Light bites to begin your meal', image_url: undefined, is_active: true, sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
60
+ { id: 'cat-2', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Mains', description: 'Hearty entrees and signature dishes', image_url: undefined, is_active: true, sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
61
+ { id: 'cat-3', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Pasta & Risotto', description: 'Fresh handmade pasta and creamy risottos', image_url: undefined, is_active: true, sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
62
+ { id: 'cat-4', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Burgers & Sandwiches', description: 'Gourmet burgers and artisan sandwiches', image_url: undefined, is_active: true, sort_order: 3, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
63
+ { id: 'cat-5', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Desserts', description: 'Sweet endings to your meal', image_url: undefined, is_active: true, sort_order: 4, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
64
+ { id: 'cat-6', menu_id: 'menu-1', restaurant_id: 'rest-1', name: 'Drinks', description: 'Cocktails, wines, and refreshments', image_url: undefined, is_active: true, sort_order: 5, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
65
+ ];
66
+
67
+ export const demoProducts: Product[] = [
68
+ // Starters
69
+ { id: 'prod-1', category_id: 'cat-1', restaurant_id: 'rest-1', name: 'Truffle Burrata', description: 'Creamy burrata with black truffle, heirloom tomatoes, basil oil & sourdough crostini', price: 16.50, image_url: undefined, is_available: true, is_featured: true, preparation_time: 10, calories: 320, allergens: ['dairy', 'gluten'], tags: ['vegetarian', 'popular'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
70
+ { id: 'prod-2', category_id: 'cat-1', restaurant_id: 'rest-1', name: 'Tuna Tartare', description: 'Sashimi-grade tuna, avocado mousse, yuzu ponzu, sesame tuile', price: 18.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 12, calories: 280, allergens: ['fish', 'sesame'], tags: ['gluten-free'], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
71
+ { id: 'prod-3', category_id: 'cat-1', restaurant_id: 'rest-1', name: 'Crispy Calamari', description: 'Lightly breaded calamari, lemon aioli, marinara, fresh herbs', price: 14.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 8, calories: 410, allergens: ['shellfish', 'gluten'], tags: [], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
72
+ { id: 'prod-4', category_id: 'cat-1', restaurant_id: 'rest-1', name: 'Garden Soup', description: 'Seasonal vegetable soup with herbs from our garden, served with warm bread', price: 9.50, image_url: undefined, is_available: true, is_featured: false, preparation_time: 5, calories: 180, allergens: ['gluten'], tags: ['vegan', 'healthy'], sort_order: 3, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
73
+ // Mains
74
+ { id: 'prod-5', category_id: 'cat-2', restaurant_id: 'rest-1', name: 'Grilled Ribeye Steak', description: '12oz prime ribeye, roasted garlic butter, truffle fries, grilled asparagus', price: 42.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 25, calories: 780, allergens: ['dairy'], tags: ['signature', 'popular'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
75
+ { id: 'prod-6', category_id: 'cat-2', restaurant_id: 'rest-1', name: 'Pan-Seared Salmon', description: 'Atlantic salmon, lemon dill sauce, quinoa pilaf, roasted vegetables', price: 32.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 20, calories: 520, allergens: ['fish'], tags: ['healthy', 'gluten-free'], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
76
+ { id: 'prod-7', category_id: 'cat-2', restaurant_id: 'rest-1', name: 'Herb-Roasted Chicken', description: 'Free-range chicken, herb jus, mashed potatoes, seasonal greens', price: 28.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 22, calories: 620, allergens: ['dairy'], tags: [], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
77
+ { id: 'prod-8', category_id: 'cat-2', restaurant_id: 'rest-1', name: 'Mushroom Wellington', description: 'Wild mushroom & spinach Wellington, red wine reduction, roasted roots', price: 26.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 25, calories: 480, allergens: ['gluten', 'dairy'], tags: ['vegetarian'], sort_order: 3, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
78
+ // Pasta & Risotto
79
+ { id: 'prod-9', category_id: 'cat-3', restaurant_id: 'rest-1', name: 'Lobster Linguine', description: 'Fresh linguine, butter-poached lobster, cherry tomatoes, white wine & chili', price: 36.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 18, calories: 580, allergens: ['shellfish', 'gluten', 'dairy'], tags: ['signature'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
80
+ { id: 'prod-10', category_id: 'cat-3', restaurant_id: 'rest-1', name: 'Wild Mushroom Risotto', description: 'Arborio rice, porcini & chanterelle mushrooms, truffle oil, parmesan', price: 24.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 20, calories: 460, allergens: ['dairy'], tags: ['vegetarian', 'gluten-free'], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
81
+ { id: 'prod-11', category_id: 'cat-3', restaurant_id: 'rest-1', name: 'Carbonara', description: 'Spaghetti, guanciale, pecorino romano, black pepper, egg yolk', price: 20.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 15, calories: 520, allergens: ['gluten', 'dairy', 'egg'], tags: ['classic'], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
82
+ // Burgers & Sandwiches
83
+ { id: 'prod-12', category_id: 'cat-4', restaurant_id: 'rest-1', name: 'Garden Smash Burger', description: 'Double smashed patty, aged cheddar, caramelized onions, secret sauce, brioche bun', price: 19.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 15, calories: 720, allergens: ['gluten', 'dairy'], tags: ['popular', 'signature'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
84
+ { id: 'prod-13', category_id: 'cat-4', restaurant_id: 'rest-1', name: 'Crispy Chicken Sandwich', description: 'Buttermilk fried chicken, slaw, pickles, spicy mayo, potato bun', price: 17.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 12, calories: 650, allergens: ['gluten', 'dairy', 'egg'], tags: [], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
85
+ { id: 'prod-14', category_id: 'cat-4', restaurant_id: 'rest-1', name: 'Grilled Veggie Wrap', description: 'Roasted vegetables, hummus, feta, arugula, sun-dried tomato wrap', price: 15.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 10, calories: 380, allergens: ['gluten', 'dairy'], tags: ['vegetarian'], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
86
+ // Desserts
87
+ { id: 'prod-15', category_id: 'cat-5', restaurant_id: 'rest-1', name: 'Chocolate Lava Cake', description: 'Warm molten chocolate cake, vanilla bean ice cream, raspberry coulis', price: 14.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 15, calories: 480, allergens: ['gluten', 'dairy', 'egg'], tags: ['popular'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
88
+ { id: 'prod-16', category_id: 'cat-5', restaurant_id: 'rest-1', name: 'Crème Brûlée', description: 'Classic vanilla custard, caramelized sugar, fresh berries', price: 12.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 5, calories: 340, allergens: ['dairy', 'egg'], tags: ['classic'], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
89
+ { id: 'prod-17', category_id: 'cat-5', restaurant_id: 'rest-1', name: 'Tiramisu', description: 'Espresso-soaked ladyfingers, mascarpone cream, cocoa dusting', price: 13.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 5, calories: 380, allergens: ['gluten', 'dairy', 'egg'], tags: [], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
90
+ // Drinks
91
+ { id: 'prod-18', category_id: 'cat-6', restaurant_id: 'rest-1', name: 'Garden Spritz', description: 'Aperol, elderflower, prosecco, fresh herbs, soda', price: 14.00, image_url: undefined, is_available: true, is_featured: true, preparation_time: 3, calories: 180, allergens: [], tags: ['signature', 'cocktail'], sort_order: 0, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
92
+ { id: 'prod-19', category_id: 'cat-6', restaurant_id: 'rest-1', name: 'Espresso Martini', description: 'Vodka, Kahlúa, fresh espresso, coffee beans', price: 15.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 3, calories: 220, allergens: [], tags: ['cocktail'], sort_order: 1, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
93
+ { id: 'prod-20', category_id: 'cat-6', restaurant_id: 'rest-1', name: 'Fresh Lemonade', description: 'House-made lemonade with mint, ginger, and a hint of lavender', price: 6.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 2, calories: 120, allergens: [], tags: ['non-alcoholic'], sort_order: 2, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
94
+ { id: 'prod-21', category_id: 'cat-6', restaurant_id: 'rest-1', name: 'House Red Wine', description: 'Cabernet Sauvignon, Napa Valley — rich, full-bodied with dark fruit notes', price: 12.00, image_url: undefined, is_available: true, is_featured: false, preparation_time: 1, calories: 125, allergens: ['sulfites'], tags: ['wine'], sort_order: 3, created_at: '2025-01-15T10:00:00Z', updated_at: '2025-01-15T10:00:00Z' },
95
+ ];
96
+
97
+ export const demoProductOptions: ProductOption[] = [
98
+ { id: 'opt-1', product_id: 'prod-5', name: 'Doneness', type: 'single', required: true, sort_order: 0 },
99
+ { id: 'opt-2', product_id: 'prod-12', name: 'Side', type: 'single', required: true, sort_order: 0 },
100
+ { id: 'opt-3', product_id: 'prod-12', name: 'Add-ons', type: 'multiple', required: false, max_selections: 3, sort_order: 1 },
101
+ ];
102
+
103
+ export const demoOptionItems: ProductOptionItem[] = [
104
+ // Steak doneness
105
+ { id: 'optitem-1', option_id: 'opt-1', name: 'Rare', price_modifier: 0, is_default: false, sort_order: 0 },
106
+ { id: 'optitem-2', option_id: 'opt-1', name: 'Medium Rare', price_modifier: 0, is_default: true, sort_order: 1 },
107
+ { id: 'optitem-3', option_id: 'opt-1', name: 'Medium', price_modifier: 0, is_default: false, sort_order: 2 },
108
+ { id: 'optitem-4', option_id: 'opt-1', name: 'Medium Well', price_modifier: 0, is_default: false, sort_order: 3 },
109
+ { id: 'optitem-5', option_id: 'opt-1', name: 'Well Done', price_modifier: 0, is_default: false, sort_order: 4 },
110
+ // Burger sides
111
+ { id: 'optitem-6', option_id: 'opt-2', name: 'Regular Fries', price_modifier: 0, is_default: true, sort_order: 0 },
112
+ { id: 'optitem-7', option_id: 'opt-2', name: 'Sweet Potato Fries', price_modifier: 2.00, is_default: false, sort_order: 1 },
113
+ { id: 'optitem-8', option_id: 'opt-2', name: 'Side Salad', price_modifier: 1.50, is_default: false, sort_order: 2 },
114
+ { id: 'optitem-9', option_id: 'opt-2', name: 'Onion Rings', price_modifier: 2.50, is_default: false, sort_order: 3 },
115
+ // Burger add-ons
116
+ { id: 'optitem-10', option_id: 'opt-3', name: 'Extra Patty', price_modifier: 5.00, is_default: false, sort_order: 0 },
117
+ { id: 'optitem-11', option_id: 'opt-3', name: 'Bacon', price_modifier: 3.00, is_default: false, sort_order: 1 },
118
+ { id: 'optitem-12', option_id: 'opt-3', name: 'Avocado', price_modifier: 2.50, is_default: false, sort_order: 2 },
119
+ { id: 'optitem-13', option_id: 'opt-3', name: 'Fried Egg', price_modifier: 2.00, is_default: false, sort_order: 3 },
120
+ ];
121
+
122
+ export const demoTables: Table[] = [
123
+ { id: 'table-1', restaurant_id: 'rest-1', number: '1', name: 'Window Seat', capacity: 2, is_active: true, created_at: '2025-01-15T10:00:00Z' },
124
+ { id: 'table-2', restaurant_id: 'rest-1', number: '2', name: 'Corner Booth', capacity: 4, is_active: true, created_at: '2025-01-15T10:00:00Z' },
125
+ { id: 'table-3', restaurant_id: 'rest-1', number: '3', name: 'Garden Table', capacity: 6, is_active: true, created_at: '2025-01-15T10:00:00Z' },
126
+ { id: 'table-4', restaurant_id: 'rest-1', number: '4', name: 'Bar Counter', capacity: 2, is_active: true, created_at: '2025-01-15T10:00:00Z' },
127
+ { id: 'table-5', restaurant_id: 'rest-1', number: '5', name: 'Private Room', capacity: 8, is_active: true, created_at: '2025-01-15T10:00:00Z' },
128
+ { id: 'table-6', restaurant_id: 'rest-1', number: '6', name: 'Terrace A', capacity: 4, is_active: true, created_at: '2025-01-15T10:00:00Z' },
129
+ { id: 'table-7', restaurant_id: 'rest-1', number: '7', name: 'Terrace B', capacity: 4, is_active: true, created_at: '2025-01-15T10:00:00Z' },
130
+ { id: 'table-8', restaurant_id: 'rest-1', number: '8', name: 'High Top', capacity: 2, is_active: false, created_at: '2025-01-15T10:00:00Z' },
131
+ ];
132
+
133
+ export const demoQRCodes: QRCode[] = [
134
+ { id: 'qr-1', restaurant_id: 'rest-1', table_id: 'table-1', label: 'Table 1 - Window Seat', url: '/restaurant/the-garden-kitchen?table=1', scans: 142, is_active: true, created_at: '2025-01-15T10:00:00Z' },
135
+ { id: 'qr-2', restaurant_id: 'rest-1', table_id: 'table-2', label: 'Table 2 - Corner Booth', url: '/restaurant/the-garden-kitchen?table=2', scans: 98, is_active: true, created_at: '2025-01-15T10:00:00Z' },
136
+ { id: 'qr-3', restaurant_id: 'rest-1', table_id: 'table-3', label: 'Table 3 - Garden Table', url: '/restaurant/the-garden-kitchen?table=3', scans: 256, is_active: true, created_at: '2025-01-15T10:00:00Z' },
137
+ { id: 'qr-4', restaurant_id: 'rest-1', table_id: 'table-4', label: 'Table 4 - Bar Counter', url: '/restaurant/the-garden-kitchen?table=4', scans: 67, is_active: true, created_at: '2025-01-15T10:00:00Z' },
138
+ { id: 'qr-5', restaurant_id: 'rest-1', table_id: 'table-5', label: 'Table 5 - Private Room', url: '/restaurant/the-garden-kitchen?table=5', scans: 34, is_active: true, created_at: '2025-01-15T10:00:00Z' },
139
+ { id: 'qr-6', restaurant_id: 'rest-1', label: 'General - Takeaway', url: '/restaurant/the-garden-kitchen?type=takeaway', scans: 523, is_active: true, created_at: '2025-01-15T10:00:00Z' },
140
+ { id: 'qr-7', restaurant_id: 'rest-1', label: 'General - Delivery', url: '/restaurant/the-garden-kitchen?type=delivery', scans: 891, is_active: true, created_at: '2025-01-15T10:00:00Z' },
141
+ ];
142
+
143
+ export const demoOrders: Order[] = [
144
+ { id: 'ord-1', restaurant_id: 'rest-1', customer_name: 'Sarah Chen', customer_phone: '+1 555-0101', order_type: 'dine_in', table_number: '3', status: 'preparing', subtotal: 68.50, tax: 5.48, total: 73.98, notes: 'No peanuts please', created_at: new Date(Date.now() - 15 * 60000).toISOString(), updated_at: new Date(Date.now() - 5 * 60000).toISOString() },
145
+ { id: 'ord-2', restaurant_id: 'rest-1', customer_name: 'Mike Johnson', customer_phone: '+1 555-0102', order_type: 'takeaway', status: 'ready', subtotal: 38.00, tax: 3.04, total: 41.04, created_at: new Date(Date.now() - 30 * 60000).toISOString(), updated_at: new Date(Date.now() - 2 * 60000).toISOString() },
146
+ { id: 'ord-3', restaurant_id: 'rest-1', customer_name: 'Emily Davis', customer_phone: '+1 555-0103', customer_email: 'emily@email.com', order_type: 'delivery', status: 'confirmed', subtotal: 52.00, tax: 4.16, total: 56.16, delivery_address: '123 Oak Street, Apt 4B', created_at: new Date(Date.now() - 8 * 60000).toISOString(), updated_at: new Date(Date.now() - 6 * 60000).toISOString() },
147
+ { id: 'ord-4', restaurant_id: 'rest-1', customer_name: 'James Wilson', order_type: 'dine_in', table_number: '1', status: 'pending', subtotal: 94.00, tax: 7.52, total: 101.52, created_at: new Date(Date.now() - 2 * 60000).toISOString(), updated_at: new Date(Date.now() - 2 * 60000).toISOString() },
148
+ { id: 'ord-5', restaurant_id: 'rest-1', customer_name: 'Lisa Park', order_type: 'dine_in', table_number: '5', status: 'delivered', subtotal: 126.00, tax: 10.08, total: 136.08, created_at: new Date(Date.now() - 90 * 60000).toISOString(), updated_at: new Date(Date.now() - 45 * 60000).toISOString() },
149
+ { id: 'ord-6', restaurant_id: 'rest-1', customer_name: 'Tom Baker', order_type: 'takeaway', status: 'delivered', subtotal: 32.00, tax: 2.56, total: 34.56, created_at: new Date(Date.now() - 120 * 60000).toISOString(), updated_at: new Date(Date.now() - 100 * 60000).toISOString() },
150
+ ];
151
+
152
+ export const demoOrderItems: OrderItem[] = [
153
+ { id: 'oi-1', order_id: 'ord-1', product_id: 'prod-1', product_name: 'Truffle Burrata', quantity: 1, unit_price: 16.50, total_price: 16.50 },
154
+ { id: 'oi-2', order_id: 'ord-1', product_id: 'prod-5', product_name: 'Grilled Ribeye Steak', quantity: 1, unit_price: 42.00, total_price: 42.00, options: { Doneness: 'Medium Rare' } },
155
+ { id: 'oi-3', order_id: 'ord-1', product_id: 'prod-20', product_name: 'Fresh Lemonade', quantity: 2, unit_price: 6.00, total_price: 12.00 },
156
+ { id: 'oi-4', order_id: 'ord-2', product_id: 'prod-12', product_name: 'Garden Smash Burger', quantity: 2, unit_price: 19.00, total_price: 38.00, options: { Side: 'Regular Fries' } },
157
+ { id: 'oi-5', order_id: 'ord-3', product_id: 'prod-6', product_name: 'Pan-Seared Salmon', quantity: 1, unit_price: 32.00, total_price: 32.00 },
158
+ { id: 'oi-6', order_id: 'ord-3', product_id: 'prod-11', product_name: 'Carbonara', quantity: 1, unit_price: 20.00, total_price: 20.00 },
159
+ { id: 'oi-7', order_id: 'ord-4', product_id: 'prod-5', product_name: 'Grilled Ribeye Steak', quantity: 2, unit_price: 42.00, total_price: 84.00 },
160
+ { id: 'oi-8', order_id: 'ord-4', product_id: 'prod-20', product_name: 'Fresh Lemonade', quantity: 1, unit_price: 6.00, total_price: 6.00 },
161
+ { id: 'oi-9', order_id: 'ord-4', product_id: 'prod-16', product_name: 'Crème Brûlée', quantity: 1, unit_price: 12.00, total_price: 12.00 },
162
+ ];
163
+
164
+ export const demoSubscription: Subscription = {
165
+ id: 'sub-1',
166
+ restaurant_id: 'rest-1',
167
+ stripe_customer_id: 'cus_demo123',
168
+ stripe_subscription_id: 'sub_demo123',
169
+ tier: 'pro',
170
+ status: 'active',
171
+ current_period_start: '2025-03-01T00:00:00Z',
172
+ current_period_end: '2025-04-01T00:00:00Z',
173
+ created_at: '2025-01-15T10:00:00Z',
174
+ updated_at: '2025-03-01T00:00:00Z',
175
+ };
176
+
177
+ // Analytics data generators
178
+ export function generateDailyRevenue(days: number = 30) {
179
+ const data = [];
180
+ for (let i = days - 1; i >= 0; i--) {
181
+ const date = new Date();
182
+ date.setDate(date.getDate() - i);
183
+ const base = 800 + Math.random() * 1200;
184
+ const dayOfWeek = date.getDay();
185
+ const weekendBoost = (dayOfWeek === 5 || dayOfWeek === 6) ? 1.4 : 1;
186
+ data.push({
187
+ date: date.toISOString().split('T')[0],
188
+ revenue: Math.round(base * weekendBoost * 100) / 100,
189
+ orders: Math.round((base * weekendBoost) / 35),
190
+ });
191
+ }
192
+ return data;
193
+ }
194
+
195
+ export function generatePopularItems() {
196
+ return [
197
+ { name: 'Garden Smash Burger', orders: 186, revenue: 3534 },
198
+ { name: 'Grilled Ribeye Steak', orders: 142, revenue: 5964 },
199
+ { name: 'Lobster Linguine', orders: 128, revenue: 4608 },
200
+ { name: 'Pan-Seared Salmon', orders: 119, revenue: 3808 },
201
+ { name: 'Chocolate Lava Cake', orders: 98, revenue: 1372 },
202
+ { name: 'Truffle Burrata', orders: 94, revenue: 1551 },
203
+ { name: 'Garden Spritz', orders: 87, revenue: 1218 },
204
+ { name: 'Carbonara', orders: 82, revenue: 1640 },
205
+ ];
206
+ }
207
+
208
+ export function generateOrdersByType() {
209
+ return [
210
+ { type: 'Dine In', count: 456, percentage: 52 },
211
+ { type: 'Takeaway', count: 234, percentage: 27 },
212
+ { type: 'Delivery', count: 183, percentage: 21 },
213
+ ];
214
+ }
215
+
216
+ export function generateHourlyOrders() {
217
+ const hours = [];
218
+ for (let h = 10; h <= 22; h++) {
219
+ const lunchPeak = h >= 12 && h <= 14 ? 2.2 : 1;
220
+ const dinnerPeak = h >= 18 && h <= 21 ? 2.8 : 1;
221
+ hours.push({
222
+ hour: `${h}:00`,
223
+ orders: Math.round((3 + Math.random() * 5) * Math.max(lunchPeak, dinnerPeak)),
224
+ });
225
+ }
226
+ return hours;
227
+ }
src/lib/utils.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function formatCurrency(amount: number, currency = 'USD') {
9
+ return new Intl.NumberFormat('en-US', {
10
+ style: 'currency',
11
+ currency,
12
+ }).format(amount);
13
+ }
14
+
15
+ export function formatDate(date: string | Date) {
16
+ return new Intl.DateTimeFormat('en-US', {
17
+ month: 'short',
18
+ day: 'numeric',
19
+ year: 'numeric',
20
+ }).format(new Date(date));
21
+ }
22
+
23
+ export function formatTime(date: string | Date) {
24
+ return new Intl.DateTimeFormat('en-US', {
25
+ hour: 'numeric',
26
+ minute: '2-digit',
27
+ hour12: true,
28
+ }).format(new Date(date));
29
+ }
30
+
31
+ export function formatRelativeTime(date: string | Date) {
32
+ const now = new Date();
33
+ const then = new Date(date);
34
+ const diffMs = now.getTime() - then.getTime();
35
+ const diffMins = Math.floor(diffMs / 60000);
36
+ if (diffMins < 1) return 'Just now';
37
+ if (diffMins < 60) return `${diffMins}m ago`;
38
+ const diffHours = Math.floor(diffMins / 60);
39
+ if (diffHours < 24) return `${diffHours}h ago`;
40
+ const diffDays = Math.floor(diffHours / 24);
41
+ return `${diffDays}d ago`;
42
+ }
43
+
44
+ export function generateSlug(name: string) {
45
+ return name
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-')
48
+ .replace(/^-|-$/g, '');
49
+ }
50
+
51
+ export function generateId() {
52
+ return crypto.randomUUID();
53
+ }
54
+
55
+ export function truncate(str: string, length: number) {
56
+ if (str.length <= length) return str;
57
+ return str.slice(0, length) + '...';
58
+ }
59
+
60
+ export function getInitials(name: string) {
61
+ return name
62
+ .split(' ')
63
+ .map((n) => n[0])
64
+ .join('')
65
+ .toUpperCase()
66
+ .slice(0, 2);
67
+ }
68
+
69
+ export function getOrderStatusColor(status: string) {
70
+ const colors: Record<string, string> = {
71
+ pending: 'bg-amber-100 text-amber-700 border-amber-200',
72
+ confirmed: 'bg-blue-100 text-blue-700 border-blue-200',
73
+ preparing: 'bg-purple-100 text-purple-700 border-purple-200',
74
+ ready: 'bg-emerald-100 text-emerald-700 border-emerald-200',
75
+ delivered: 'bg-gray-100 text-gray-700 border-gray-200',
76
+ cancelled: 'bg-red-100 text-red-700 border-red-200',
77
+ };
78
+ return colors[status] || 'bg-gray-100 text-gray-700 border-gray-200';
79
+ }
80
+
81
+ export function getOrderTypeLabel(type: string) {
82
+ const labels: Record<string, string> = {
83
+ dine_in: 'Dine In',
84
+ takeaway: 'Takeaway',
85
+ delivery: 'Delivery',
86
+ };
87
+ return labels[type] || type;
88
+ }
src/stores/cart-store.ts ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import { persist } from 'zustand/middleware';
5
+ import { CartItem, Product, OrderType } from '@/types/database';
6
+ import { generateId } from '@/lib/utils';
7
+
8
+ interface CartStore {
9
+ items: CartItem[];
10
+ restaurantId: string | null;
11
+ restaurantName: string | null;
12
+ orderType: OrderType;
13
+ tableNumber: string | null;
14
+ customerName: string;
15
+ customerPhone: string;
16
+ customerEmail: string;
17
+ deliveryAddress: string;
18
+ notes: string;
19
+
20
+ addItem: (product: Product, quantity: number, options?: Record<string, string | string[]>, notes?: string) => void;
21
+ removeItem: (itemId: string) => void;
22
+ updateQuantity: (itemId: string, quantity: number) => void;
23
+ clearCart: () => void;
24
+ setOrderType: (type: OrderType) => void;
25
+ setTableNumber: (number: string | null) => void;
26
+ setRestaurant: (id: string, name: string) => void;
27
+ setCustomerName: (name: string) => void;
28
+ setCustomerPhone: (phone: string) => void;
29
+ setCustomerEmail: (email: string) => void;
30
+ setDeliveryAddress: (address: string) => void;
31
+ setNotes: (notes: string) => void;
32
+ getSubtotal: () => number;
33
+ getTax: () => number;
34
+ getTotal: () => number;
35
+ getItemCount: () => number;
36
+ }
37
+
38
+ export const useCartStore = create<CartStore>()(
39
+ persist(
40
+ (set, get) => ({
41
+ items: [],
42
+ restaurantId: null,
43
+ restaurantName: null,
44
+ orderType: 'dine_in',
45
+ tableNumber: null,
46
+ customerName: '',
47
+ customerPhone: '',
48
+ customerEmail: '',
49
+ deliveryAddress: '',
50
+ notes: '',
51
+
52
+ addItem: (product, quantity, options = {}, notes = '') => {
53
+ const total = product.price * quantity;
54
+ const newItem: CartItem = {
55
+ id: generateId(),
56
+ product,
57
+ quantity,
58
+ options,
59
+ notes,
60
+ total,
61
+ };
62
+ set((state) => ({ items: [...state.items, newItem] }));
63
+ },
64
+
65
+ removeItem: (itemId) => {
66
+ set((state) => ({ items: state.items.filter((i) => i.id !== itemId) }));
67
+ },
68
+
69
+ updateQuantity: (itemId, quantity) => {
70
+ if (quantity <= 0) {
71
+ get().removeItem(itemId);
72
+ return;
73
+ }
74
+ set((state) => ({
75
+ items: state.items.map((item) =>
76
+ item.id === itemId
77
+ ? { ...item, quantity, total: item.product.price * quantity }
78
+ : item
79
+ ),
80
+ }));
81
+ },
82
+
83
+ clearCart: () => {
84
+ set({ items: [], notes: '' });
85
+ },
86
+
87
+ setOrderType: (type) => set({ orderType: type }),
88
+ setTableNumber: (number) => set({ tableNumber: number }),
89
+ setRestaurant: (id, name) => set({ restaurantId: id, restaurantName: name }),
90
+ setCustomerName: (name) => set({ customerName: name }),
91
+ setCustomerPhone: (phone) => set({ customerPhone: phone }),
92
+ setCustomerEmail: (email) => set({ customerEmail: email }),
93
+ setDeliveryAddress: (address) => set({ deliveryAddress: address }),
94
+ setNotes: (notes) => set({ notes }),
95
+
96
+ getSubtotal: () => {
97
+ return get().items.reduce((sum, item) => sum + item.total, 0);
98
+ },
99
+
100
+ getTax: () => {
101
+ return get().getSubtotal() * 0.08;
102
+ },
103
+
104
+ getTotal: () => {
105
+ return get().getSubtotal() + get().getTax();
106
+ },
107
+
108
+ getItemCount: () => {
109
+ return get().items.reduce((sum, item) => sum + item.quantity, 0);
110
+ },
111
+ }),
112
+ {
113
+ name: 'scanmenu-cart',
114
+ }
115
+ )
116
+ );
src/types/database.ts ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // ScanMenu – Complete Database Types
3
+ // ============================================================
4
+
5
+ export type UserRole = 'admin' | 'owner' | 'staff' | 'customer';
6
+ export type OrderStatus = 'pending' | 'confirmed' | 'preparing' | 'ready' | 'delivered' | 'cancelled';
7
+ export type OrderType = 'dine_in' | 'takeaway' | 'delivery';
8
+ export type SubscriptionTier = 'free' | 'starter' | 'pro' | 'enterprise';
9
+ export type SubscriptionStatus = 'active' | 'past_due' | 'cancelled' | 'trialing';
10
+
11
+ export interface User {
12
+ id: string;
13
+ email: string;
14
+ full_name: string;
15
+ avatar_url?: string;
16
+ role: UserRole;
17
+ created_at: string;
18
+ updated_at: string;
19
+ }
20
+
21
+ export interface Restaurant {
22
+ id: string;
23
+ owner_id: string;
24
+ name: string;
25
+ slug: string;
26
+ description?: string;
27
+ logo_url?: string;
28
+ cover_image_url?: string;
29
+ address?: string;
30
+ phone?: string;
31
+ email?: string;
32
+ website?: string;
33
+ currency: string;
34
+ timezone: string;
35
+ is_active: boolean;
36
+ opening_hours?: Record<string, { open: string; close: string; closed?: boolean }>;
37
+ theme_color: string;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+
42
+ export interface Menu {
43
+ id: string;
44
+ restaurant_id: string;
45
+ name: string;
46
+ description?: string;
47
+ is_active: boolean;
48
+ sort_order: number;
49
+ created_at: string;
50
+ updated_at: string;
51
+ }
52
+
53
+ export interface Category {
54
+ id: string;
55
+ menu_id: string;
56
+ restaurant_id: string;
57
+ name: string;
58
+ description?: string;
59
+ image_url?: string;
60
+ is_active: boolean;
61
+ sort_order: number;
62
+ created_at: string;
63
+ updated_at: string;
64
+ }
65
+
66
+ export interface Product {
67
+ id: string;
68
+ category_id: string;
69
+ restaurant_id: string;
70
+ name: string;
71
+ description?: string;
72
+ price: number;
73
+ image_url?: string;
74
+ is_available: boolean;
75
+ is_featured: boolean;
76
+ preparation_time?: number; // minutes
77
+ calories?: number;
78
+ allergens?: string[];
79
+ tags?: string[];
80
+ sort_order: number;
81
+ created_at: string;
82
+ updated_at: string;
83
+ }
84
+
85
+ export interface ProductOption {
86
+ id: string;
87
+ product_id: string;
88
+ name: string; // e.g. "Size", "Spice Level"
89
+ type: 'single' | 'multiple';
90
+ required: boolean;
91
+ min_selections?: number;
92
+ max_selections?: number;
93
+ sort_order: number;
94
+ }
95
+
96
+ export interface ProductOptionItem {
97
+ id: string;
98
+ option_id: string;
99
+ name: string; // e.g. "Small", "Medium", "Large"
100
+ price_modifier: number;
101
+ is_default: boolean;
102
+ sort_order: number;
103
+ }
104
+
105
+ export interface Table {
106
+ id: string;
107
+ restaurant_id: string;
108
+ number: string;
109
+ name?: string;
110
+ capacity?: number;
111
+ is_active: boolean;
112
+ qr_code_id?: string;
113
+ created_at: string;
114
+ }
115
+
116
+ export interface QRCode {
117
+ id: string;
118
+ restaurant_id: string;
119
+ table_id?: string;
120
+ label: string;
121
+ url: string;
122
+ scans: number;
123
+ is_active: boolean;
124
+ created_at: string;
125
+ }
126
+
127
+ export interface Order {
128
+ id: string;
129
+ restaurant_id: string;
130
+ customer_name?: string;
131
+ customer_phone?: string;
132
+ customer_email?: string;
133
+ order_type: OrderType;
134
+ table_number?: string;
135
+ status: OrderStatus;
136
+ subtotal: number;
137
+ tax: number;
138
+ total: number;
139
+ notes?: string;
140
+ delivery_address?: string;
141
+ estimated_time?: number;
142
+ created_at: string;
143
+ updated_at: string;
144
+ }
145
+
146
+ export interface OrderItem {
147
+ id: string;
148
+ order_id: string;
149
+ product_id: string;
150
+ product_name: string;
151
+ quantity: number;
152
+ unit_price: number;
153
+ total_price: number;
154
+ options?: Record<string, string | string[]>;
155
+ notes?: string;
156
+ }
157
+
158
+ export interface Subscription {
159
+ id: string;
160
+ restaurant_id: string;
161
+ stripe_customer_id?: string;
162
+ stripe_subscription_id?: string;
163
+ tier: SubscriptionTier;
164
+ status: SubscriptionStatus;
165
+ current_period_start?: string;
166
+ current_period_end?: string;
167
+ created_at: string;
168
+ updated_at: string;
169
+ }
170
+
171
+ export interface AnalyticsEvent {
172
+ id: string;
173
+ restaurant_id: string;
174
+ event_type: string;
175
+ event_data?: Record<string, unknown>;
176
+ created_at: string;
177
+ }
178
+
179
+ // ============================================================
180
+ // Cart Types (Client-side)
181
+ // ============================================================
182
+
183
+ export interface CartItem {
184
+ id: string;
185
+ product: Product;
186
+ quantity: number;
187
+ options: Record<string, string | string[]>;
188
+ notes?: string;
189
+ total: number;
190
+ }
191
+
192
+ export interface Cart {
193
+ restaurant_id: string;
194
+ restaurant_name: string;
195
+ items: CartItem[];
196
+ order_type: OrderType;
197
+ table_number?: string;
198
+ subtotal: number;
199
+ tax: number;
200
+ total: number;
201
+ }
tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }