| 'use client'; |
|
|
| import { DashboardLayout } from '@/components/layout/dashboard-layout'; |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
| import { Button } from '@/components/ui/button'; |
| import { Badge } from '@/components/ui/badge'; |
| import { Input } from '@/components/ui/input'; |
| import { Textarea } from '@/components/ui/textarea'; |
| import { |
| Plus, |
| Search, |
| MoreHorizontal, |
| GripVertical, |
| Pencil, |
| Trash2, |
| Eye, |
| EyeOff, |
| Star, |
| Clock, |
| Flame, |
| ChevronDown, |
| ChevronRight, |
| X, |
| Image as ImageIcon, |
| DollarSign, |
| Tag, |
| AlertCircle, |
| } from 'lucide-react'; |
| import { useState } from 'react'; |
| import { demoCategories, demoProducts } from '@/lib/demo-data'; |
| import { formatCurrency, cn } from '@/lib/utils'; |
| import { Product, Category } from '@/types/database'; |
|
|
| function ProductCard({ product, onEdit }: { product: Product; onEdit: () => void }) { |
| return ( |
| <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"> |
| <button className="mt-1 cursor-grab text-zinc-300 opacity-0 transition-opacity group-hover:opacity-100 active:cursor-grabbing"> |
| <GripVertical className="h-4 w-4" /> |
| </button> |
| |
| {/* Image placeholder */} |
| <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"> |
| <ImageIcon className="h-6 w-6 text-zinc-300 dark:text-zinc-500" /> |
| </div> |
| |
| <div className="flex-1 min-w-0"> |
| <div className="flex items-start justify-between gap-2"> |
| <div className="min-w-0"> |
| <div className="flex items-center gap-2"> |
| <h4 className="truncate text-sm font-semibold text-zinc-900 dark:text-zinc-100">{product.name}</h4> |
| {product.is_featured && <Star className="h-3.5 w-3.5 shrink-0 fill-amber-400 text-amber-400" />} |
| </div> |
| <p className="mt-0.5 line-clamp-1 text-xs text-zinc-500">{product.description}</p> |
| </div> |
| <div className="flex items-center gap-1 shrink-0"> |
| <span className="text-sm font-bold text-zinc-900 dark:text-zinc-100">{formatCurrency(product.price)}</span> |
| </div> |
| </div> |
| |
| <div className="mt-2 flex flex-wrap items-center gap-1.5"> |
| {product.is_available ? ( |
| <Badge variant="success" className="text-[10px]"><Eye className="h-3 w-3 mr-0.5" />Available</Badge> |
| ) : ( |
| <Badge variant="destructive" className="text-[10px]"><EyeOff className="h-3 w-3 mr-0.5" />Hidden</Badge> |
| )} |
| {product.preparation_time && ( |
| <Badge variant="secondary" className="text-[10px]"><Clock className="h-3 w-3 mr-0.5" />{product.preparation_time}m</Badge> |
| )} |
| {product.calories && ( |
| <Badge variant="secondary" className="text-[10px]"><Flame className="h-3 w-3 mr-0.5" />{product.calories} cal</Badge> |
| )} |
| {product.tags?.map((tag) => ( |
| <Badge key={tag} variant="outline" className="text-[10px]">{tag}</Badge> |
| ))} |
| </div> |
| </div> |
| |
| <div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100"> |
| <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"> |
| <Pencil className="h-3.5 w-3.5" /> |
| </button> |
| <button className="rounded-lg p-1.5 text-zinc-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950/30"> |
| <Trash2 className="h-3.5 w-3.5" /> |
| </button> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function ProductModal({ product, onClose }: { product?: Product; onClose: () => void }) { |
| return ( |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"> |
| <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"> |
| <div className="flex items-center justify-between border-b border-zinc-200 p-5 dark:border-zinc-800"> |
| <h3 className="text-lg font-semibold">{product ? 'Edit Product' : 'Add Product'}</h3> |
| <button onClick={onClose} className="rounded-lg p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800"> |
| <X className="h-4 w-4" /> |
| </button> |
| </div> |
| <div className="space-y-4 p-5"> |
| {/* Image upload */} |
| <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"> |
| <div className="text-center"> |
| <ImageIcon className="mx-auto h-8 w-8 text-zinc-300" /> |
| <p className="mt-2 text-sm font-medium text-zinc-600">Upload product image</p> |
| <p className="text-xs text-zinc-400">PNG, JPG up to 5MB</p> |
| <Button variant="outline" size="sm" className="mt-3">Choose File</Button> |
| </div> |
| </div> |
| |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Product Name</label> |
| <Input placeholder="e.g., Truffle Burrata" defaultValue={product?.name} /> |
| </div> |
| |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Description</label> |
| <Textarea placeholder="Describe your product..." defaultValue={product?.description} /> |
| </div> |
| |
| <div className="grid grid-cols-2 gap-3"> |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Price</label> |
| <div className="relative"> |
| <DollarSign className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input type="number" step="0.01" placeholder="0.00" defaultValue={product?.price} className="pl-10" /> |
| </div> |
| </div> |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Prep Time (min)</label> |
| <div className="relative"> |
| <Clock className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input type="number" placeholder="15" defaultValue={product?.preparation_time ?? undefined} className="pl-10" /> |
| </div> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-2 gap-3"> |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Calories</label> |
| <div className="relative"> |
| <Flame className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input type="number" placeholder="350" defaultValue={product?.calories ?? undefined} className="pl-10" /> |
| </div> |
| </div> |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Tags</label> |
| <div className="relative"> |
| <Tag className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input placeholder="vegetarian, popular" defaultValue={product?.tags?.join(', ')} className="pl-10" /> |
| </div> |
| </div> |
| </div> |
| |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Allergens</label> |
| <div className="relative"> |
| <AlertCircle className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input placeholder="dairy, gluten, nuts" defaultValue={product?.allergens?.join(', ')} className="pl-10" /> |
| </div> |
| </div> |
| |
| <div className="flex items-center gap-6 rounded-xl bg-zinc-50 p-4 dark:bg-zinc-800/50"> |
| <label className="flex items-center gap-2 text-sm"> |
| <input type="checkbox" defaultChecked={product?.is_available ?? true} className="h-4 w-4 rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500" /> |
| <span className="font-medium text-zinc-700 dark:text-zinc-300">Available</span> |
| </label> |
| <label className="flex items-center gap-2 text-sm"> |
| <input type="checkbox" defaultChecked={product?.is_featured ?? false} className="h-4 w-4 rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500" /> |
| <span className="font-medium text-zinc-700 dark:text-zinc-300">Featured</span> |
| </label> |
| </div> |
| </div> |
| <div className="flex justify-end gap-2 border-t border-zinc-200 p-5 dark:border-zinc-800"> |
| <Button variant="outline" onClick={onClose}>Cancel</Button> |
| <Button variant="primary" onClick={onClose}>{product ? 'Save Changes' : 'Add Product'}</Button> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| export default function MenuBuilderPage() { |
| const [search, setSearch] = useState(''); |
| const [expandedCategories, setExpandedCategories] = useState<string[]>(demoCategories.map((c) => c.id)); |
| const [editingProduct, setEditingProduct] = useState<Product | null>(null); |
| const [showAddProduct, setShowAddProduct] = useState(false); |
|
|
| const toggleCategory = (id: string) => { |
| setExpandedCategories((prev) => |
| prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id] |
| ); |
| }; |
|
|
| const filteredProducts = search |
| ? demoProducts.filter((p) => p.name.toLowerCase().includes(search.toLowerCase()) || p.description?.toLowerCase().includes(search.toLowerCase())) |
| : demoProducts; |
|
|
| return ( |
| <DashboardLayout> |
| <div className="space-y-6"> |
| {/* Header */} |
| <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> |
| <div> |
| <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Menu Builder</h1> |
| <p className="mt-1 text-sm text-zinc-500">Manage your menus, categories, and products.</p> |
| </div> |
| <div className="flex gap-2"> |
| <Button variant="outline" size="sm"> |
| <Plus className="h-4 w-4" /> Add Category |
| </Button> |
| <Button variant="primary" size="sm" onClick={() => setShowAddProduct(true)}> |
| <Plus className="h-4 w-4" /> Add Product |
| </Button> |
| </div> |
| </div> |
| |
| {/* Search */} |
| <div className="relative"> |
| <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input |
| placeholder="Search products..." |
| value={search} |
| onChange={(e) => setSearch(e.target.value)} |
| className="pl-10 max-w-sm" |
| /> |
| </div> |
| |
| {/* Categories & Products */} |
| <div className="space-y-4"> |
| {demoCategories.map((category) => { |
| const categoryProducts = filteredProducts.filter((p) => p.category_id === category.id); |
| const isExpanded = expandedCategories.includes(category.id); |
| |
| return ( |
| <Card key={category.id}> |
| <div |
| className="flex cursor-pointer items-center justify-between p-4" |
| onClick={() => toggleCategory(category.id)} |
| > |
| <div className="flex items-center gap-3"> |
| <button className="cursor-grab text-zinc-300 active:cursor-grabbing"> |
| <GripVertical className="h-4 w-4" /> |
| </button> |
| {isExpanded ? ( |
| <ChevronDown className="h-4 w-4 text-zinc-400" /> |
| ) : ( |
| <ChevronRight className="h-4 w-4 text-zinc-400" /> |
| )} |
| <div> |
| <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{category.name}</h3> |
| <p className="text-xs text-zinc-500">{categoryProducts.length} items{category.description && ` • ${category.description}`}</p> |
| </div> |
| </div> |
| <div className="flex items-center gap-2"> |
| <Badge variant={category.is_active ? 'success' : 'secondary'} className="text-[10px]"> |
| {category.is_active ? 'Active' : 'Hidden'} |
| </Badge> |
| <button className="rounded-lg p-1.5 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800" onClick={(e) => e.stopPropagation()}> |
| <MoreHorizontal className="h-4 w-4" /> |
| </button> |
| </div> |
| </div> |
| |
| {isExpanded && ( |
| <CardContent className="border-t border-zinc-100 pt-3 dark:border-zinc-800"> |
| {categoryProducts.length > 0 ? ( |
| <div className="space-y-2"> |
| {categoryProducts.map((product) => ( |
| <ProductCard |
| key={product.id} |
| product={product} |
| onEdit={() => setEditingProduct(product)} |
| /> |
| ))} |
| </div> |
| ) : ( |
| <div className="py-8 text-center text-sm text-zinc-400"> |
| No products in this category |
| </div> |
| )} |
| <button |
| onClick={() => setShowAddProduct(true)} |
| 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" |
| > |
| <Plus className="h-4 w-4" /> Add product |
| </button> |
| </CardContent> |
| )} |
| </Card> |
| ); |
| })} |
| </div> |
| </div> |
| |
| {/* Modals */} |
| {(editingProduct || showAddProduct) && ( |
| <ProductModal |
| product={editingProduct ?? undefined} |
| onClose={() => { |
| setEditingProduct(null); |
| setShowAddProduct(false); |
| }} |
| /> |
| )} |
| </DashboardLayout> |
| ); |
| } |
|
|