"use client"; import { useEffect, useId, useRef, useState, type FormEvent, } from "react"; import ConfirmDialog from "@/components/ConfirmDialog"; import ImageCropDialog from "@/components/ImageCropDialog"; import WebsiteLinkGenerator from "@/components/WebsiteLinkGenerator"; import { getWebsiteHomeMedia, newMediaItem, saveWebsiteHomeMedia, uploadWebsiteMedia, type WebsiteHomeMedia, type WebsiteMediaItem, } from "@/services/website-media"; import styles from "./WebsiteImagesPage.module.css"; type SectionKey = | "slider" | "standingBanners" | "categories" | "menuImages" | "aboutUs" | "banners"; type PixelSize = { width: number; height: number }; type SlotGroup = { size: PixelSize; indexes: number[]; }; type SectionDef = { key: SectionKey; label: string; kind: "list" | "fixed"; itemCount?: number; /** Default upload / preview size */ size: PixelSize; /** Per-index size overrides when not using groups */ sizes?: PixelSize[]; /** Same-row size groups (e.g. homepage banners) */ groups?: SlotGroup[]; fit: "contain" | "cover"; }; type EditTarget = { key: SectionKey; index: number; }; const SECTIONS: SectionDef[] = [ { key: "slider", label: "Slider", kind: "list", size: { width: 450, height: 450 }, fit: "contain", }, { key: "standingBanners", label: "Standing banners", kind: "fixed", itemCount: 2, size: { width: 600, height: 600 }, fit: "contain", }, { key: "categories", label: "Categories", kind: "list", size: { width: 600, height: 600 }, fit: "contain", }, { key: "menuImages", label: "Menu images", kind: "fixed", itemCount: 4, size: { width: 600, height: 600 }, fit: "contain", }, { key: "aboutUs", label: "About us", kind: "fixed", itemCount: 1, size: { width: 979, height: 800 }, fit: "contain", }, { key: "banners", label: "Banners", kind: "fixed", itemCount: 5, size: { width: 650, height: 650 }, // Matches homepage: wide featured · 3 small cards · export strip groups: [ { size: { width: 650, height: 650 }, indexes: [0] }, { size: { width: 900, height: 900 }, indexes: [1, 2, 3] }, { size: { width: 593, height: 640 }, indexes: [4] }, ], fit: "contain", }, ]; function formatResolution(size: PixelSize) { return `${size.width} × ${size.height}`; } function sizeForItem(section: SectionDef, index: number): PixelSize { const fromGroup = section.groups?.find((group) => group.indexes.includes(index), ); if (fromGroup) return fromGroup.size; return section.sizes?.[index] ?? section.size; } function previewWidth(size: PixelSize) { const aspect = size.width / size.height; if (aspect >= 1.4) return 220; if (aspect >= 0.9) return 140; return 110; } function getItems(media: WebsiteHomeMedia, key: SectionKey): WebsiteMediaItem[] { if (key === "aboutUs") return [media.aboutUs]; return media[key]; } function withItems( media: WebsiteHomeMedia, key: SectionKey, items: WebsiteMediaItem[], ): WebsiteHomeMedia { if (key === "aboutUs") { return { ...media, aboutUs: items[0] ?? media.aboutUs }; } return { ...media, [key]: items }; } function emptySlot(partial?: Partial): WebsiteMediaItem { return newMediaItem({ imageUrl: "", linkUrl: "/", title: "", subtitle: "", cta: "", ...partial, }); } function padFixed(items: WebsiteMediaItem[], count: number) { const next = items.slice(0, count); while (next.length < count) next.push(emptySlot()); return next; } export default function WebsiteImagesPage() { const [media, setMedia] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(""); const [error, setError] = useState(""); const [editTarget, setEditTarget] = useState(null); const [removeTarget, setRemoveTarget] = useState(null); useEffect(() => { let cancelled = false; void (async () => { setLoading(true); setError(""); try { const data = await getWebsiteHomeMedia(); if (!cancelled) setMedia(data); } catch (err) { if (!cancelled) { setError( err instanceof Error ? err.message : "Could not load website images.", ); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, []); async function persist(next: WebsiteHomeMedia) { setSaving(true); setStatus(""); setError(""); try { const saved = await saveWebsiteHomeMedia(next); setMedia(saved); setStatus("Saved"); return saved; } catch (err) { setError(err instanceof Error ? err.message : "Could not save."); throw err; } finally { setSaving(false); } } async function handleSaveItem( target: EditTarget, values: { imageUrl: string; title: string; subtitle: string; cta: string; linkUrl: string; }, ) { if (!media) return; const section = SECTIONS.find((row) => row.key === target.key); if (!section) return; let items = [...getItems(media, target.key)]; if (section.kind === "fixed" && section.itemCount) { items = padFixed(items, section.itemCount); } const existing = items[target.index]; const nextItem: WebsiteMediaItem = { ...(existing ?? emptySlot()), imageUrl: values.imageUrl, title: values.title, subtitle: values.subtitle, cta: values.cta, linkUrl: values.linkUrl || "/", }; if (section.kind === "list" && !existing) { items.push(nextItem); } else { items[target.index] = nextItem; } if (section.kind === "fixed" && section.itemCount) { items = padFixed(items, section.itemCount); } const next = withItems(media, target.key, items); setMedia(next); await persist(next); setEditTarget(null); } async function handleRemove(target: EditTarget) { if (!media) return; const section = SECTIONS.find((row) => row.key === target.key); if (!section) return; let items = [...getItems(media, target.key)]; if (section.kind === "list") { items = items.filter((_, index) => index !== target.index); if (target.key === "slider" && items.length === 0) { items = [emptySlot({ title: "New slide", cta: "browse producers" })]; } } else { if (section.itemCount) items = padFixed(items, section.itemCount); items[target.index] = emptySlot({ id: items[target.index]?.id }); } const next = withItems(media, target.key, items); setMedia(next); setRemoveTarget(null); await persist(next); } if (loading) { return
Loading website images…
; } if (!media) { return (
{error || "Could not load website images."}
); } const editingSection = editTarget ? SECTIONS.find((row) => row.key === editTarget.key) : null; const editingItems = editTarget ? getItems(media, editTarget.key) : []; const editingItem = editTarget ? editingItems[editTarget.index] : null; return (

Website images

Homepage assets at their website pixel sizes — add or replace via cropper.

{status ? ( {status} ) : null} {error ? ( {error} ) : null}
{SECTIONS.map((section) => { const raw = getItems(media, section.key); const items = section.kind === "fixed" && section.itemCount ? padFixed(raw, section.itemCount) : raw; const addSize = sizeForItem(section, items.length); const addWidth = previewWidth(addSize); const showListPlus = section.kind === "list"; const compactPlus = showListPlus && items.length > 0; const thumbFitClass = `${styles.thumbTransparent} ${ section.fit === "contain" ? styles.thumbContain : styles.thumbCover }`; const groups = section.groups; const hasGroups = Boolean(groups?.length); function renderSlot(index: number, item: WebsiteMediaItem) { const filled = Boolean(item.imageUrl?.trim()); const size = sizeForItem(section, index); const width = previewWidth(size); const aspect = `${size.width} / ${size.height}`; return (
{filled ? ( <>

{index + 1}

) : ( )}
); } return (

{section.label}

{section.key} {" · "} {section.kind === "list" ? "Duplicatable" : `Fixed · ${section.itemCount}`} {!hasGroups ? ` · ${formatResolution(section.size)}` : null}

{showListPlus ? (
) : null} {hasGroups ? groups!.map((group, groupIndex) => (
{groupIndex > 0 ? (
) : null}

{formatResolution(group.size)}

{group.indexes.map((index) => { const item = items[index] ?? emptySlot(); return renderSlot(index, item); })}
)) : items.map((item, index) => renderSlot(index, item))}
); })} { if (!saving) setEditTarget(null); }} onSave={async (values) => { if (!editTarget) return; await handleSaveItem(editTarget, values); }} /> { if (!saving) setRemoveTarget(null); }} onConfirm={() => { if (removeTarget) void handleRemove(removeTarget); }} />
); } function EditIcon() { return ( ); } function RemoveIcon() { return ( ); } function WebsiteMediaEditModal({ open, sectionLabel, size, fit, initial, busy, onClose, onSave, }: { open: boolean; sectionLabel: string; size: PixelSize; fit: "contain" | "cover"; initial: WebsiteMediaItem | null | undefined; busy: boolean; onClose: () => void; onSave: (values: { imageUrl: string; title: string; subtitle: string; cta: string; linkUrl: string; }) => Promise; }) { const resolution = formatResolution(size); const aspect = size.width / size.height; const titleId = useId(); const fileRef = useRef(null); const [title, setTitle] = useState(""); const [subtitle, setSubtitle] = useState(""); const [cta, setCta] = useState(""); const [linkUrl, setLinkUrl] = useState(""); const [imageUrl, setImageUrl] = useState(""); const [pendingFile, setPendingFile] = useState(null); const [cropSrc, setCropSrc] = useState(null); const [cropOpen, setCropOpen] = useState(false); const [localError, setLocalError] = useState(""); const [uploading, setUploading] = useState(false); useEffect(() => { if (!open) return; setTitle(initial?.title ?? ""); setSubtitle(initial?.subtitle ?? ""); setCta(initial?.cta ?? ""); setLinkUrl(initial?.linkUrl ?? ""); setImageUrl(initial?.imageUrl ?? ""); setPendingFile(null); setLocalError(""); setCropSrc(null); setCropOpen(false); }, [open, initial]); useEffect(() => { if (!open) return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape" && !busy && !cropOpen) onClose(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = previousOverflow; window.removeEventListener("keydown", onKey); }; }, [open, busy, cropOpen, onClose]); if (!open) return null; async function submit(event: FormEvent) { event.preventDefault(); if (!imageUrl && !pendingFile) { setLocalError("Add an image first."); return; } setLocalError(""); setUploading(true); try { let nextUrl = imageUrl; if (pendingFile) { nextUrl = await uploadWebsiteMedia(pendingFile); } await onSave({ imageUrl: nextUrl, title: title.trim(), subtitle: subtitle.trim(), cta: cta.trim(), linkUrl: linkUrl.trim() || "/", }); } catch (err) { setLocalError(err instanceof Error ? err.message : "Could not save."); } finally { setUploading(false); } } const working = busy || uploading; return ( <>
event.stopPropagation()} >

Website image

{initial?.imageUrl ? "Edit image" : "Add image"}

{sectionLabel} · {resolution}

void submit(event)}>
Image {resolution}
{imageUrl ? (
) : ( )} { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; const url = URL.createObjectURL(file); setCropSrc(url); setCropOpen(true); }} />