"use client"; import NextImage from "next/image"; import Link from "next/link"; import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import { uploadProductImage } from "@/actions/products/upload-image"; import { createProduct } from "@/actions/products/create-product"; import { AdminInput, AdminTextInput, AdminTextarea, AdminSelect } from "./design-system"; type Props = { defaultBrandId?: string; brands?: { id: string; name: string }[]; lockBrand?: boolean; }; export default function NewProductForm({ defaultBrandId = "", brands = [], lockBrand = false }: Props) { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [imagePreview, setImagePreview] = useState(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); const [pendingImageUrl, setPendingImageUrl] = useState(""); // Form state const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [price, setPrice] = useState(""); const [type, setType] = useState("Pickup"); const [brandId, setBrandId] = useState(defaultBrandId); const [isTaxable, setIsTaxable] = useState("true"); const [pickupType, setPickupType] = useState("scheduled_stop"); const [active, setActive] = useState("true"); // Build the brand options. If the server provided a list, use it; otherwise // fall back to the historical hardcoded list so the form still works in // environments where getBrands() failed or returned empty. const brandOptions = brands.length > 0 ? brands : [ { id: "64294306-5f42-463d-a5e8-2ad6c81a96de", name: "Tuxedo Corn" }, { id: "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28", name: "Indian River Direct" }, ]; const brandSelectOptions = brandOptions.map((b) => ({ value: b.id, label: b.name })); 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("__NEW__", resizedFile); setUploading(false); if (result.success) { setPendingImageUrl(result.imageUrl); setImagePreview(result.imageUrl); } else { setUploadError(result.error ?? "Upload failed."); } } 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 handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); // Use pendingImageUrl if image was uploaded, otherwise null const imageUrl = pendingImageUrl || null; const result = await createProduct(brandId, { name, description, price: parseFloat(price), type, active: active === "true", image_url: imageUrl, is_taxable: isTaxable === "true", pickup_type: pickupType, }); if (!result.success) { setError(result.error ?? "Failed to create product"); setLoading(false); return; } router.push("/admin/products"); router.refresh(); } return (
{error && (
{error}
)} setName(e.target.value)} placeholder="e.g. Dozen Sweet Corn" autoComplete="off" /> setDescription(e.target.value)} rows={3} placeholder="e.g. Fresh-picked Olathe sweet corn." />
setPrice(e.target.value)} placeholder="12.00" /> setType(e.target.value)} options={[ { value: "Pickup", label: "Pickup" }, { value: "Shipping", label: "Shipping" }, { value: "Pickup & Shipping", label: "Pickup & Shipping" }, ]} />
{lockBrand ? ( // brand_admin / store_employee — brand is fixed by their admin_users record
{brandOptions.find((b) => b.id === brandId)?.name ?? brandId}
) : ( setBrandId(e.target.value)} options={[ { value: "", label: brands.length > 0 ? "Select brand..." : "Loading brands..." }, ...brandSelectOptions, ]} disabled={brands.length === 0 && defaultBrandId === ""} /> )} {!lockBrand && brands.length === 0 && (

Loading available brands — if this persists, check the admin Brands settings.

)}
setIsTaxable(e.target.value)} options={[ { value: "true", label: "Yes — taxable (default)" }, { value: "false", label: "No — non-taxable" }, ]} /> setPickupType(e.target.value)} options={[ { value: "scheduled_stop", label: "Scheduled Stop — requires stop selection at checkout" }, { value: "shed", label: "Shed Pickup — uses description as location" }, ]} /> setActive(e.target.value)} options={[ { value: "true", label: "Yes — show on storefront" }, { value: "false", label: "No — hide from storefront" }, ]} />

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 ${uploadError ? "border-[var(--admin-danger)]" : "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
); }