Files
HytaleMMORPG/src/components/site/CustomWaffleCard.tsx
T
gpatruno e45e9f154e update
2026-06-11 17:30:13 +02:00

246 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Image from "next/image";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
formatEuro,
type CustomWaffleConfig,
type IngredientPickerItem,
} from "@/lib/custom-waffle";
import { isUploadAssetPath } from "@/lib/image-url";
import { WaffleMenuImage } from "./WaffleMenuImage";
function IngredientThumb({ src, alt }: { src: string; alt: string }) {
if (src.startsWith("http")) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt} className="h-full w-full object-cover" />
);
}
return (
<Image
src={src}
alt={alt}
fill
unoptimized={isUploadAssetPath(src)}
className="object-cover"
sizes="64px"
/>
);
}
export function CustomWaffleCard({
config,
ingredients,
}: {
config: CustomWaffleConfig;
ingredients: IngredientPickerItem[];
}) {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<Set<string>>(new Set());
const selectedList = useMemo(
() => ingredients.filter((ing) => selected.has(ing.id)),
[ingredients, selected]
);
const extrasTotal = useMemo(
() => selectedList.reduce((sum, ing) => sum + ing.price, 0),
[selectedList]
);
const total = config.basePrice + extrasTotal;
const close = useCallback(() => setOpen(false), []);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") close();
};
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open, close]);
function toggle(id: string) {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}
return (
<>
<li className="overflow-hidden rounded-3xl border border-gaufre/30 bg-white shadow-[0_8px_30px_-8px_rgba(61,43,31,0.12)] ring-1 ring-gaufre/20 transition hover:shadow-[0_12px_40px_-8px_rgba(61,43,31,0.18)]">
<button
type="button"
onClick={() => setOpen(true)}
className="flex w-full flex-col text-left"
>
<div className="relative aspect-[16/10] w-full bg-chocolat/5">
<WaffleMenuImage src={config.imageUrl} alt={config.title} />
<span className="absolute left-4 top-4 rounded-full bg-gaufre px-3 py-1 text-xs font-bold uppercase tracking-wide text-chocolat shadow-sm">
Sur mesure
</span>
</div>
<div className="p-6">
<div className="mb-2">
<span className="font-semibold text-chocolat">
{config.title}
</span>
</div>
<p className="text-sm leading-relaxed text-muted">
{config.description}
</p>
{selectedList.length > 0 ? (
<p className="mt-2 text-xs font-medium text-chocolat">
{selectedList.map((i) => i.name).join(", ")}
</p>
) : (
<p className="mt-3 text-sm font-semibold text-gaufre">
Cliquez pour composer
</p>
)}
</div>
</button>
</li>
{open ? (
<div
className="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center"
role="presentation"
onClick={close}
>
<div
className="absolute inset-0 bg-chocolat/50 backdrop-blur-[2px]"
aria-hidden
/>
<div
role="dialog"
aria-modal="true"
aria-labelledby="custom-waffle-title"
className="relative flex max-h-[min(90vh,720px)] w-full max-w-lg flex-col overflow-hidden rounded-3xl border border-chocolat/10 bg-cream shadow-[0_24px_60px_-12px_rgba(61,43,31,0.35)]"
onClick={(e) => e.stopPropagation()}
>
<div className="border-b border-chocolat/10 bg-white px-5 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<h4
id="custom-waffle-title"
className="text-lg font-bold text-chocolat"
>
{config.title}
</h4>
<p className="mt-1 text-sm text-muted">
Base {formatEuro(config.basePrice)} ajoutez vos
ingrédients
</p>
</div>
<button
type="button"
onClick={close}
className="rounded-full p-2 text-chocolat/70 hover:bg-chocolat/5 hover:text-chocolat"
aria-label="Fermer"
>
<span className="text-xl leading-none">×</span>
</button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
{ingredients.length === 0 ? (
<p className="rounded-2xl border border-dashed border-chocolat/20 p-6 text-center text-sm text-muted">
Aucun ingrédient disponible pour le moment.
</p>
) : (
<ul className="space-y-2">
{ingredients.map((ing) => {
const isOn = selected.has(ing.id);
return (
<li key={ing.id}>
<button
type="button"
onClick={() => toggle(ing.id)}
className={`flex w-full items-center gap-3 rounded-2xl border p-3 text-left transition ${
isOn
? "border-gaufre bg-gaufre/15 ring-1 ring-gaufre/40"
: "border-chocolat/10 bg-white hover:border-chocolat/20"
}`}
>
<span
className={`relative h-14 w-14 shrink-0 overflow-hidden rounded-xl border bg-chocolat/5 ${
isOn
? "border-gaufre/50"
: "border-chocolat/10"
}`}
>
{ing.imageUrl ? (
<IngredientThumb
src={ing.imageUrl}
alt={ing.name}
/>
) : (
<span className="flex h-full w-full items-center justify-center text-lg text-chocolat/25">
</span>
)}
</span>
<span className="min-w-0 flex-1">
<span className="block font-medium text-chocolat">
{ing.name}
</span>
<span className="text-sm font-semibold text-gaufre">
+{formatEuro(ing.price)}
</span>
</span>
<span
className={`flex size-6 shrink-0 items-center justify-center rounded-full border-2 text-xs font-bold ${
isOn
? "border-gaufre bg-gaufre text-chocolat"
: "border-chocolat/20 text-transparent"
}`}
aria-hidden
>
</span>
</button>
</li>
);
})}
</ul>
)}
</div>
<div className="border-t border-chocolat/10 bg-white px-5 py-4">
<div className="mb-3 flex items-center justify-between gap-2">
<span className="text-sm text-muted">
{selectedList.length === 0
? "Aucun ingrédient sélectionné"
: `${selectedList.length} ingrédient${selectedList.length > 1 ? "s" : ""}`}
</span>
<span className="text-xl font-bold text-gaufre">
{formatEuro(total)}
</span>
</div>
<button
type="button"
onClick={close}
className="w-full rounded-full bg-gaufre py-3 text-sm font-semibold text-chocolat shadow-md hover:opacity-95"
>
Valider ma composition
</button>
</div>
</div>
</div>
) : null}
</>
);
}