"use client"; import NextImage from "next/image"; import Link from "next/link"; import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import { uploadProductImage, deleteProductImage } from "@/actions/products/upload-image"; import { updateProduct } from "@/actions/products/update-product"; import { AdminInput, AdminTextInput, AdminTextarea, AdminSelect } from "./design-system/AdminFormElements"; async function resizeImage(file: File, maxWidth: number): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { let { width, height } = img; if (width > maxWidth) { height = Math.round(height * (maxWidth / width)); width = maxWidth; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (!blob) { reject(new Error("Failed to resize image")); return; } blob.arrayBuffer().then((buf) => resolve(buf)); }, "image/jpeg", 0.85); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } type Brand = { id: string; name: string; slug: string; }; type ProductEditFormProps = { product: { id: string; name: string; description: string; price: number; type: string; active: boolean; brand_id: string; image_url?: string | null; is_taxable?: boolean; pickup_type?: string; }; brands: Brand[]; }; export default function ProductEditForm({ product, brands }: ProductEditFormProps) { const router = useRouter(); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); // Holds user edits only; missing keys fall back to the current prop so we // never copy the prop into useState. When the prop changes, the fallback // value tracks the new prop automatically. const [draft, setDraft] = useState<{ name?: string; description?: string; price?: number; type?: string; active?: boolean; brand_id?: string; image_url?: string; is_taxable?: boolean; pickup_type?: string; }>({}); const name = draft.name ?? product.name; const description = draft.description ?? product.description; const price = draft.price ?? product.price; const type = draft.type ?? product.type; const active = draft.active ?? product.active; const brand_id = draft.brand_id ?? product.brand_id; const image_url = draft.image_url ?? product.image_url ?? ""; const is_taxable = draft.is_taxable ?? product.is_taxable ?? true; const pickup_type = draft.pickup_type ?? product.pickup_type ?? "scheduled_stop"; const setName = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, name: typeof v === "function" ? v(d.name ?? product.name) : v, })); const setDescription = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, description: typeof v === "function" ? v(d.description ?? product.description) : v, })); const setPrice = (v: number | ((prev: number) => number)) => setDraft((d) => ({ ...d, price: typeof v === "function" ? v(d.price ?? product.price) : v, })); const setType = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, type: typeof v === "function" ? v(d.type ?? product.type) : v, })); const setActive = (v: boolean | ((prev: boolean) => boolean)) => setDraft((d) => ({ ...d, active: typeof v === "function" ? v(d.active ?? product.active) : v, })); const setBrand_id = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, brand_id: typeof v === "function" ? v(d.brand_id ?? product.brand_id) : v, })); const setImage_url = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, image_url: typeof v === "function" ? v(d.image_url ?? product.image_url ?? "") : v, })); const setIs_taxable = (v: boolean | ((prev: boolean) => boolean)) => setDraft((d) => ({ ...d, is_taxable: typeof v === "function" ? v(d.is_taxable ?? product.is_taxable ?? true) : v, })); const setPickup_type = (v: string | ((prev: string) => string)) => setDraft((d) => ({ ...d, pickup_type: typeof v === "function" ? v(d.pickup_type ?? product.pickup_type ?? "scheduled_stop") : v, })); const [dragOver, setDragOver] = useState(false); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const [imagePreview, setImagePreview] = useState(product.image_url ?? null); const fileInputRef = useRef(null); async function handleFileSelect(file: File) { const validTypes = ["image/png", "image/jpeg", "image/webp"]; if (!validTypes.includes(file.type)) { setUploadError("Only PNG, JPEG, and WebP images are allowed."); return; } if (file.size > 5 * 1024 * 1024) { setUploadError("Image must be under 5MB."); return; } // Client-side resize to max 1200px width const resizedBuffer = await resizeImage(file, 1200); const resizedFile = new File([resizedBuffer], file.name, { type: "image/jpeg" }); setUploadError(null); setUploading(true); const result = await uploadProductImage(product.id, resizedFile); setUploading(false); if (result.success) { setImage_url(result.imageUrl); setImagePreview(result.imageUrl); } else { setUploadError(result.error ?? "Upload failed."); } } async function handleRemoveImage() { const result = await deleteProductImage(product.id); if (result.success) { setImage_url(""); setImagePreview(null); } } async function handleSave() { if (!name.trim()) { setError("Product name is required."); return; } setSaving(true); setError(null); setSaved(false); const result = await updateProduct(product.id, brand_id, { name, description, price: Number(price), type, active, image_url: image_url || null, is_taxable, pickup_type, }); if (!result.success) { setError(result.error ?? "Failed to save"); setSaving(false); return; } setSaved(true); setSaving(false); router.refresh(); } return (
{error && (
{error}
)} {saved && (
Product updated successfully.
)} setName(e.target.value)} placeholder="Product name" /> setDescription(e.target.value)} rows={3} placeholder="Product description" />
setPrice(Number(e.target.value))} placeholder="0.00" /> setType(e.target.value)} placeholder="e.g. Sweet Corn" />
setBrand_id(e.target.value)} options={brands.map((b) => ({ value: b.id, label: b.name }))} />

Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.

setPickup_type(e.target.value)} options={[ { value: "scheduled_stop", label: "Scheduled Stop — requires stop selection at checkout" }, { value: "shed", label: "Shed Pickup — uses description as location" }, ]} />

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

{ if (e.key === "Enter" || e.key === " ") { fileInputRef.current?.click(); } }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const file = e.dataTransfer.files[0]; if (file) handleFileSelect(file); }} onClick={() => fileInputRef.current?.click()} className={` flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-6 cursor-pointer transition-colors ${dragOver ? "border-[var(--admin-primary)] bg-[var(--admin-primary-soft)]" : "border-[var(--admin-border)] hover:border-[var(--admin-primary)] hover:bg-[var(--admin-bg)]"} ${uploading ? "opacity-50 pointer-events-none" : ""} `} > {uploading ? ( <>
Uploading... ) : imagePreview ? ( <> Click or drop to replace ) : ( <> Drag & drop or click to upload )} { const file = e.target.files?.[0]; if (file) handleFileSelect(file); }} />
{uploadError &&

{uploadError}

} {imagePreview && ( )}
{/* Save button bar — new design tokens */}
Cancel
); }