app-uau39m/frontend/src/components/customer/StoreDetailModal.tsx
2026-09-28 23:38:56 +05:30

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>
);
}