"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Image from "next/image"; import { createPortal } from "react-dom"; export type ProductFormModalProps = { open: boolean; mode: "add" | "edit"; onClose: () => void; onSubmit: (values: ProductFormValues) => Promise<{ success: boolean; error?: string }>; /** Defaults / initial values (edit mode passes the current product) */ initial?: Partial; /** List of selectable brands (only shown to platform admins) */ brands?: { id: string; name: string }[]; /** When true the brand picker is hidden and the admin's brand is used */ lockBrand?: boolean; /** Hard-locked brand id (for brand_admin / store_employee) */ lockedBrandId?: string; /** Pre-uploaded image URL (edit mode) */ initialImageUrl?: string | null; /** Server action: upload a file and return the public URL */ onUploadImage: (file: File) => Promise<{ success: boolean; imageUrl?: string; error?: string }>; }; export type ProductFormValues = { name: string; description: string; price: string; type: "pickup" | "wholesale" | "subscription"; brand_id: string; active: boolean; is_taxable: boolean; image_url: string | null; available_from?: string | null; available_until?: string | null; }; const TYPE_OPTIONS: Array<{ value: ProductFormValues["type"]; label: string; italic: string; icon: React.ReactNode; }> = [ { value: "pickup", label: "Pickup", italic: "Farm & stops", icon: ( ), }, { value: "wholesale", label: "Wholesale", italic: "B2B portal", icon: ( ), }, { value: "subscription", label: "Subscription", italic: "Recurring", icon: ( ), }, ]; export default function ProductFormModal({ open, mode, onClose, onSubmit, initial, brands = [], lockBrand = false, lockedBrandId = "", initialImageUrl = null, onUploadImage, }: ProductFormModalProps) { // Form state const [name, setName] = useState(initial?.name ?? ""); const [description, setDescription] = useState(initial?.description ?? ""); const [price, setPrice] = useState(initial?.price ?? ""); const [type, setType] = useState(initial?.type ?? "pickup"); const [brandId, setBrandId] = useState( initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? "" ); const [active, setActive] = useState(initial?.active ?? true); const [isTaxable, setIsTaxable] = useState(initial?.is_taxable ?? true); const [availableFrom, setAvailableFrom] = useState(initial?.available_from ? initial.available_from.split('T')[0] : ""); const [availableUntil, setAvailableUntil] = useState(initial?.available_until ? initial.available_until.split('T')[0] : ""); // Image state const [imagePreview, setImagePreview] = useState(initialImageUrl); const [imageUrl, setImageUrl] = useState(initialImageUrl); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [isDrag, setIsDrag] = useState(false); const fileInputRef = useRef(null); // Submit state const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [mounted, setMounted] = useState(false); useEffect(() => { const init = async () => { setMounted(true); }; init(); }, []); // Reset state when opening with new initial values useEffect(() => { const init = async () => { if (!open) return; setName(initial?.name ?? ""); setDescription(initial?.description ?? ""); setPrice(initial?.price ?? ""); setType((initial?.type as ProductFormValues["type"]) ?? "pickup"); setBrandId(initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? ""); setActive(initial?.active ?? true); setIsTaxable(initial?.is_taxable ?? true); setAvailableFrom(initial?.available_from ? initial.available_from.split('T')[0] : ""); setAvailableUntil(initial?.available_until ? initial.available_until.split('T')[0] : ""); setImagePreview(initialImageUrl); setImageUrl(initialImageUrl); setUploading(false); setUploadError(null); setError(null); setSaving(false); setIsDrag(false); }; init(); }, [open, initial, initialImageUrl, lockedBrandId, brands]); // Body scroll lock + escape key useEffect(() => { if (!open) return; const original = document.body.style.overflow; document.body.style.overflow = "hidden"; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = original; window.removeEventListener("keydown", onKey); }; }, [open, onClose]); const handleFile = useCallback( async (file: File) => { const validTypes = ["image/png", "image/jpeg", "image/webp"]; if (!validTypes.includes(file.type)) { setUploadError("PNG, JPEG, or WebP only."); return; } if (file.size > 5 * 1024 * 1024) { setUploadError("File must be under 5MB."); return; } setUploadError(null); setUploading(true); // Local preview first const localUrl = URL.createObjectURL(file); setImagePreview(localUrl); const result = await onUploadImage(file); setUploading(false); if (result.success && result.imageUrl) { setImageUrl(result.imageUrl); setImagePreview(result.imageUrl); URL.revokeObjectURL(localUrl); } else { setUploadError(result.error ?? "Upload failed."); setImagePreview(imageUrl); // revert } }, [onUploadImage, imageUrl] ); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); if (!name.trim()) { setError("Product name is required."); return; } if (!price || isNaN(parseFloat(price)) || parseFloat(price) < 0) { setError("Valid price is required."); return; } if (!lockBrand && !brandId) { setError("Please select a brand."); return; } setSaving(true); const res = await onSubmit({ name: name.trim(), description: description.trim(), price, type, brand_id: lockBrand ? lockedBrandId : brandId, active, is_taxable: isTaxable, image_url: imageUrl, available_from: availableFrom ? new Date(availableFrom).toISOString() : null, available_until: availableUntil ? new Date(availableUntil).toISOString() : null, }); setSaving(false); if (!res.success) { setError(res.error ?? "Something went wrong."); return; } onClose(); } if (!mounted || !open) return null; const showBrandPicker = !lockBrand; const lockedBrandName = brands.find((b) => b.id === lockedBrandId)?.name ?? lockedBrandId; return createPortal(
{ if (e.target === e.currentTarget) onClose(); }} style={{ backgroundColor: "color-mix(in srgb, var(--admin-text-primary) 55%, transparent)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)", }} >
{/* grain overlay */}
{/* HEADER */}
{mode === "add" ? "Nouveau" : "Mise à jour"} · MMXXVI

{mode === "add" ? ( <> Add a product ) : ( <> Edit {initial?.name || "product"} )}

{mode === "add" ? "Compose a new entry for the harvest catalog — every detail, like a recipe." : "Refine the details of this entry. The catalog is a living record."}


{/* BODY — two columns */}
{ e.preventDefault(); setIsDrag(true); }} >
{/* LEFT — Image hero */}
01 · Media
{ e.preventDefault(); setIsDrag(true); }} onDragLeave={(e) => { if (e.currentTarget === e.target) setIsDrag(false); }} onDrop={(e) => { e.preventDefault(); setIsDrag(false); const file = e.dataTransfer.files?.[0]; if (file) handleFile(file); }} onClick={() => !uploading && fileInputRef.current?.click()} className={[ "atelier-drop", isDrag ? "is-drag" : "", uploadError ? "is-error" : "", imagePreview ? "has-image" : "", ].join(" ")} > {/* status pill */}
{imagePreview ? uploading ? "Uploading" : "Image set" : "No image yet"}
{uploading ? (

Pouring into the cellar…

) : imagePreview ? (
Product preview
) : ( <> {/* ornamental drop zone content */}

Drag & drop

or click to choose
a harvest portrait

PNG · JPEG · WebP · 5MB max

)} { const file = e.target.files?.[0]; if (file) handleFile(file); e.target.value = ""; }} />
{uploadError && (

{uploadError}

)}
{/* RIGHT — Form fields */}
{error && (
{error}
)} {/* 02 IDENTITY */}
02 · Identity
setName(e.target.value)} placeholder="e.g. Dozen Sweet Corn" className="atelier-input atelier-input--display" autoComplete="off" />