236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { api, resolveMediaUrl } from '../../api';
|
|
import { Clock, MapPin, Minus, Plus, ShoppingBag, Sparkles, Store, X } from 'lucide-react';
|
|
|
|
interface MenuItem {
|
|
id: string;
|
|
restaurant_id: string;
|
|
name: string;
|
|
description?: string;
|
|
category?: string;
|
|
ingredients?: string;
|
|
price: number;
|
|
image_url?: string;
|
|
is_available: number | boolean;
|
|
}
|
|
|
|
interface Restaurant {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
address: string;
|
|
image_url?: string;
|
|
status: string;
|
|
}
|
|
|
|
interface StoreDetailModalProps {
|
|
restaurantId: string | null;
|
|
onClose: () => void;
|
|
cartItems: { item: MenuItem; quantity: number }[];
|
|
onAddToCart: (item: MenuItem, quantity: number) => void;
|
|
onOpenCart?: () => void;
|
|
}
|
|
|
|
export default function StoreDetailModal({
|
|
restaurantId,
|
|
onClose,
|
|
cartItems,
|
|
onAddToCart,
|
|
onOpenCart,
|
|
}: StoreDetailModalProps) {
|
|
const [activeCategory, setActiveCategory] = useState<string>('all');
|
|
|
|
const { data: restaurantRes, isLoading: isRestLoading } = useQuery({
|
|
queryKey: ['restaurant', restaurantId],
|
|
queryFn: () => api.get(`/api/restaurants/${restaurantId}`),
|
|
enabled: Boolean(restaurantId),
|
|
});
|
|
|
|
const { data: menuRes, isLoading: isMenuLoading } = useQuery({
|
|
queryKey: ['menu-items', restaurantId],
|
|
queryFn: () => api.get(`/api/restaurants/${restaurantId}/menu-items`),
|
|
enabled: Boolean(restaurantId),
|
|
});
|
|
|
|
if (!restaurantId) return null;
|
|
|
|
const restaurant: Restaurant | null = restaurantRes?.data?.restaurant ?? null;
|
|
const menuItems: MenuItem[] = Array.isArray(menuRes?.data?.menu_items)
|
|
? menuRes.data.menu_items
|
|
: [];
|
|
|
|
const categories = ['all', ...Array.from(new Set(menuItems.map((i) => i.category || 'General').filter(Boolean)))];
|
|
|
|
const filteredItems = activeCategory === 'all'
|
|
? menuItems
|
|
: menuItems.filter((i) => (i.category || 'General') === activeCategory);
|
|
|
|
const totalCartCount = cartItems.reduce((acc, i) => acc + i.quantity, 0);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm">
|
|
<div className="relative flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-2xl bg-surface shadow-2xl border border-border">
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-surface/80 text-foreground shadow-md transition hover:bg-surface border border-border"
|
|
aria-label="Close details"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
|
|
{isRestLoading ? (
|
|
<div className="flex h-64 items-center justify-center text-mutedForeground">
|
|
<Clock className="h-6 w-6 animate-spin mr-2 text-accent" /> Loading store details...
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="relative h-48 w-full bg-slate-900 md:h-64">
|
|
{restaurant?.image_url ? (
|
|
<img
|
|
src={resolveMediaUrl(restaurant.image_url)}
|
|
alt={restaurant.name}
|
|
className="h-full w-full object-cover brightness-75"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center bg-slate-800 text-slate-400">
|
|
<Store className="h-16 w-16 opacity-40" />
|
|
</div>
|
|
)}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
|
|
<div className="absolute bottom-4 left-6 right-6 text-white">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`inline-block h-2.5 w-2.5 rounded-full ${restaurant?.status === 'active' ? 'bg-emerald-500' : 'bg-rose-500'}`} />
|
|
<span className="text-xs uppercase tracking-wider font-semibold opacity-90">
|
|
{restaurant?.status === 'active' ? 'Open for Orders' : 'Currently Closed'}
|
|
</span>
|
|
</div>
|
|
<h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">{restaurant?.name}</h2>
|
|
<p className="flex items-center text-sm text-slate-300 mt-1">
|
|
<MapPin className="h-4 w-4 mr-1 text-accent shrink-0" />
|
|
{restaurant?.address}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{categories.length > 1 && (
|
|
<div className="flex items-center gap-2 overflow-x-auto border-b border-border bg-surfaceSecondary p-3 px-6">
|
|
{categories.map((cat) => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => setActiveCategory(cat)}
|
|
className={`rounded-full px-4 py-1.5 text-xs font-semibold uppercase tracking-wider transition ${activeCategory === cat ? 'bg-primary text-primaryForeground shadow' : 'bg-surface text-mutedForeground hover:text-foreground'}`}
|
|
>
|
|
{cat}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
{isMenuLoading ? (
|
|
<div className="py-12 text-center text-mutedForeground">Loading menu items...</div>
|
|
) : filteredItems.length === 0 ? (
|
|
<div className="py-12 text-center text-mutedForeground">No dishes found in this category.</div>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
{filteredItems.map((dish) => {
|
|
const cartItem = cartItems.find((ci) => ci.item.id === dish.id);
|
|
const qty = cartItem ? cartItem.quantity : 0;
|
|
const isAvailable = Boolean(dish.is_available);
|
|
|
|
return (
|
|
<div
|
|
key={dish.id}
|
|
className="flex flex-col justify-between rounded-xl border border-border bg-surface p-4 shadow-sm transition hover:shadow-md"
|
|
>
|
|
<div className="flex gap-3">
|
|
{dish.image_url && (
|
|
<img
|
|
src={resolveMediaUrl(dish.image_url)}
|
|
alt={dish.name}
|
|
className="h-20 w-20 rounded-lg object-cover border border-border shrink-0"
|
|
/>
|
|
)}
|
|
<div className="flex-1">
|
|
<div className="flex items-start justify-between">
|
|
<h4 className="font-display font-semibold text-foreground text-base">{dish.name}</h4>
|
|
<span className="font-mono font-bold text-accent text-sm ml-2">
|
|
${Number(dish.price).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
{dish.description && (
|
|
<p className="mt-1 text-xs text-mutedForeground line-clamp-2">{dish.description}</p>
|
|
)}
|
|
{dish.ingredients && (
|
|
<p className="mt-1 text-[11px] text-slate-500 italic flex items-center gap-1">
|
|
<Sparkles className="h-3 w-3 text-amber-500 inline" /> {dish.ingredients}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 flex items-center justify-between border-t border-border/50 pt-3">
|
|
<span className={`text-xs font-medium ${isAvailable ? 'text-emerald-600' : 'text-rose-500'}`}>
|
|
{isAvailable ? 'Available' : 'Sold Out'}
|
|
</span>
|
|
{isAvailable && (
|
|
<div className="flex items-center gap-2">
|
|
{qty > 0 ? (
|
|
<div className="flex items-center rounded-lg border border-border bg-surfaceSecondary p-0.5">
|
|
<button
|
|
onClick={() => onAddToCart(dish, -1)}
|
|
className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-surface text-foreground"
|
|
>
|
|
<Minus className="h-3.5 w-3.5" />
|
|
</button>
|
|
<span className="w-6 text-center font-mono text-xs font-semibold">{qty}</span>
|
|
<button
|
|
onClick={() => onAddToCart(dish, 1)}
|
|
className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-surface text-foreground"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => onAddToCart(dish, 1)}
|
|
className="flex items-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-xs font-semibold text-accentForeground shadow hover:opacity-90 transition"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" /> Add to Order
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{totalCartCount > 0 && onOpenCart && (
|
|
<div className="border-t border-border bg-surface p-4 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-mutedForeground">Items in Cart</p>
|
|
<p className="font-mono font-bold text-foreground text-sm">{totalCartCount} item(s) selected</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
onClose();
|
|
onOpenCart();
|
|
}}
|
|
className="flex items-center gap-2 rounded-xl bg-accent px-5 py-2.5 text-sm font-semibold text-accentForeground shadow-md hover:opacity-90 transition"
|
|
>
|
|
<ShoppingBag className="h-4 w-4" /> View Cart & Checkout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|