Files

154 lines
4.6 KiB
TypeScript

'use client';
import { useEffect, useCallback } from 'react';
import { X } from 'lucide-react';
import type { MenuItem } from '@/domain/menu/entities';
import { getDishIngredients } from '@/domain/menu/dish-details';
import {
applyNextImageFallback,
dishImageUrl,
getMenuPosterCandidates,
getMenuVideoSrc,
} from '@/lib/assets';
interface DishDetailsModalProps {
item: MenuItem;
displayName: string;
description: string;
isOpen: boolean;
onClose: () => void;
labels: {
ingredients: string;
close: string;
};
}
export default function DishDetailsModal({
item,
displayName,
description,
isOpen,
onClose,
labels,
}: DishDetailsModalProps) {
const videoSrc = getMenuVideoSrc(item);
const imageSrc = item.image ? dishImageUrl(item.image) : null;
const ingredients = getDishIngredients(item.id);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
},
[onClose],
);
useEffect(() => {
if (!isOpen) return;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, handleKeyDown]);
if (!isOpen) return null;
return (
<>
<div
className="fixed inset-0 z-[1000] bg-black/45 backdrop-blur-[2px]"
onClick={onClose}
aria-hidden
/>
<div
className="fixed inset-0 z-[1010] flex items-end sm:items-center justify-center p-0 sm:p-6 pointer-events-none"
role="dialog"
aria-modal="true"
aria-labelledby="dish-detail-title"
>
<div
className="pointer-events-auto w-full sm:max-w-lg max-h-[92dvh] sm:max-h-[88dvh] overflow-y-auto rounded-t-3xl sm:rounded-2xl bg-[#FFFCF7] border border-[#EDE6D9] shadow-2xl shadow-[#101724]/15"
onClick={(e) => e.stopPropagation()}
>
<div className="relative bg-[#F2EDE4]">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
loop
muted
playsInline
className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]"
/>
) : imageSrc ? (
<img
src={imageSrc}
alt={displayName}
className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(item),
(e.target as HTMLImageElement).src,
);
}}
/>
) : (
<div className="w-full aspect-[4/3] flex items-center justify-center text-sm text-[#8A8478]">
No media available
</div>
)}
<button
type="button"
onClick={onClose}
aria-label={labels.close}
className="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 text-[#2C2A26] shadow-md border border-[#EDE6D9] hover:bg-white hover:text-[#0f5a4a] transition-colors"
>
<X className="h-4 w-4" aria-hidden />
</button>
</div>
<div className="p-6 sm:p-7 space-y-5">
<h2
id="dish-detail-title"
className="font-serif text-2xl sm:text-[26px] tracking-[-0.4px] text-[#2C2A26]"
>
{displayName}
</h2>
{description && (
<p className="text-[#6B665F] text-[15px] leading-relaxed">
{description}
</p>
)}
{ingredients.length > 0 && (
<div>
<h3 className="text-xs font-semibold uppercase tracking-[0.2em] text-[#c99a2e] mb-3">
{labels.ingredients}
</h3>
<ul className="space-y-2">
{ingredients.map((ingredient) => (
<li
key={ingredient}
className="flex items-start gap-2.5 text-[15px] text-[#2C2A26] leading-snug"
>
<span
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-[#B38B4D]"
aria-hidden
/>
{ingredient}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
</>
);
}