"use client"; import NextImage from "next/image"; 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"; 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); const [name, setName] = useState(product.name); const [description, setDescription] = useState(product.description); const [price, setPrice] = useState(product.price); const [type, setType] = useState(product.type); const [active, setActive] = useState(product.active); const [brand_id, setBrand_id] = useState(product.brand_id); const [image_url, setImage_url] = useState(product.image_url ?? ""); const [is_taxable, setIs_taxable] = useState(product.is_taxable ?? true); const [pickup_type, setPickup_type] = useState(product.pickup_type ?? "scheduled_stop"); 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 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); }); } 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)

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-green-500 bg-green-900/30" : "border-zinc-600 hover:border-slate-400 hover:bg-zinc-900"} ${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 && ( )}
); }