"use client"; import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; import { useFamily } from "@/app/FamilyProvider"; import { Button } from "@/components/ui"; import { GARMENT_CATEGORIES, GARMENT_SIZE_ORDER } from "@/db/schema/wardrobe"; type Step = "capture" | "tagging" | "form"; interface VisionResult { name: string; category: string; colors: string[]; seasons: string[]; occasion_tags: string[]; } const SEASON_OPTIONS = ["summer", "monsoon", "winter"]; const OCCASION_OPTIONS = ["everyday", "daycare", "festive", "photoshoot"]; const ACQUIRED_OPTIONS = ["bought", "gift", "handmedown"]; const CATEGORY_LABELS: Record = { onesie: "Onesie", top: "Top", bottom: "Bottom", dress: "Dress", outerwear: "Jacket", sleepwear: "Sleepwear", accessory: "Accessory", }; const CATEGORY_COLORS: Record = { onesie: "bg-pink-100 text-pink-700", top: "bg-blue-100 text-blue-700", bottom: "bg-indigo-100 text-indigo-700", dress: "bg-purple-100 text-purple-700", outerwear: "bg-teal-100 text-teal-700", sleepwear: "bg-amber-100 text-amber-700", accessory: "bg-rose-100 text-rose-700", }; function ChipRow({ label, options, selected, onChange, required, optionLabel, }: { label: string; options: readonly T[]; selected: T[]; onChange: (v: T[]) => void; required?: boolean; optionLabel?: (v: T) => string; }) { const toggle = (v: T) => { if (selected.includes(v)) onChange(selected.filter(x => x !== v)); else onChange([...selected, v]); }; return (

{label}{required && *}

{options.map(o => ( ))}
); } function SingleChipRow({ label, options, selected, onChange, required, optionLabel, }: { label: string; options: readonly T[]; selected: T | null; onChange: (v: T) => void; required?: boolean; optionLabel?: (v: T) => string; }) { return (

{label}{required && *}

{options.map(o => ( ))}
); } export default function AddGarmentPage() { const { childId } = useFamily(); const router = useRouter(); const fileRef = useRef(null); const [step, setStep] = useState("capture"); const [preview, setPreview] = useState(null); const [thumbUrl, setThumbUrl] = useState(null); const [imageKey, setImageKey] = useState(""); const [thumbKey, setThumbKey] = useState(""); const [error, setError] = useState(""); const [saving, setSaving] = useState(false); // Form fields const [name, setName] = useState(""); const [category, setCategory] = useState(null); const [sizeLabel, setSizeLabel] = useState(null); const [colors, setColors] = useState([]); const [colorInput, setColorInput] = useState(""); const [seasons, setSeasons] = useState([]); const [occasionTags, setOccasionTags] = useState([]); const [acquiredVia, setAcquiredVia] = useState(null); const [giftFrom, setGiftFrom] = useState(""); const [visionMetadata, setVisionMetadata] = useState(null); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setError(""); setStep("tagging"); // Show local preview immediately const reader = new FileReader(); reader.onload = ev => setPreview(ev.target?.result as string); reader.readAsDataURL(file); // Upload to R2 const form = new FormData(); form.append("file", file); let uploadRes; try { uploadRes = await fetch("/api/garments/upload", { method: "POST", body: form }); if (!uploadRes.ok) throw new Error((await uploadRes.json()).error); } catch (err) { setError(`Upload failed: ${err}`); setStep("capture"); return; } const { imageKey: ik, thumbKey: tk, thumbUrl: tu } = await uploadRes.json(); setImageKey(ik); setThumbKey(tk); setThumbUrl(tu); // Vision tagging let vision: VisionResult = { name: "", category: "top", colors: [], seasons: [], occasion_tags: [] }; try { const tagRes = await fetch("/api/garments/tag", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageKey: ik }), }); if (tagRes.ok) { const tagData = await tagRes.json(); vision = tagData; } } catch { // Vision failure is non-fatal — proceed with empty defaults } setVisionMetadata(vision); setName(vision.name || ""); setCategory(vision.category || null); setColors(vision.colors || []); setSeasons(vision.seasons || []); setOccasionTags(vision.occasion_tags || []); setStep("form"); }; const addColor = () => { const c = colorInput.trim().toLowerCase(); if (c && !colors.includes(c)) setColors([...colors, c]); setColorInput(""); }; const handleSave = async () => { if (!childId || !category || !sizeLabel) return; setSaving(true); setError(""); try { const res = await fetch("/api/garments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ childId, name: name || null, category, sizeLabel, colors, seasons, occasionTags, imageKey, thumbKey, acquiredVia: acquiredVia || null, giftFrom: giftFrom || null, visionMetadata, }), }); if (!res.ok) throw new Error((await res.json()).error); handleAddAnother(); } catch (err) { setError(`Save failed: ${err}`); } finally { setSaving(false); } }; const handleAddAnother = () => { setStep("capture"); setPreview(null); setThumbUrl(null); setImageKey(""); setThumbKey(""); setName(""); setCategory(null); setSizeLabel(null); setColors([]); setColorInput(""); setSeasons([]); setOccasionTags([]); setAcquiredVia(null); setGiftFrom(""); setVisionMetadata(null); setError(""); if (fileRef.current) fileRef.current.value = ""; }; return (
{/* Header */}

Add Garment

{step === "form" && ( ✨ Vision pre-filled )}
{error && (
{error}
)} {/* Step 1: Capture */} {step === "capture" && (
fileRef.current?.click()} className="aspect-square rounded-3xl border-4 border-dashed border-rose-200 dark:border-gray-600 flex flex-col items-center justify-center gap-4 cursor-pointer hover:border-rose-400 transition-colors bg-white dark:bg-gray-800" > 👚

Take or upload a photo

Tip: flat-lay on a light surface for best tagging

📷 Add Photo
)} {/* Step 2: Tagging spinner */} {step === "tagging" && (
{preview && (
preview
)}
{[0, 150, 300].map(d => ( ))}

Analysing garment…

Vision is detecting category & colors

)} {/* Step 3: Form */} {step === "form" && (
{/* Thumbnail */}
{(thumbUrl || preview) && (
garment
)}
setName(e.target.value)} placeholder="e.g. Striped blue onesie" className="mt-1 w-full px-3 py-2 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 text-sm focus:outline-none focus:ring-2 focus:ring-rose-300" />
{/* Category */}
setCategory(v)} required optionLabel={v => CATEGORY_LABELS[v] || v} />
{/* Size — required, not pre-filled */}
setSizeLabel(v)} required /> {!sizeLabel && (

⚠ Size must be set manually — vision cannot guess it

)}
{/* Colors */}

Colors

{colors.map(c => ( ))}
setColorInput(e.target.value)} onKeyDown={e => e.key === "Enter" && addColor()} placeholder="Add color…" className="flex-1 px-3 py-1.5 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-sm focus:outline-none" />
{/* Seasons */}
{/* Occasions */}
{/* Optional fields */}

Optional

Acquired via

{ACQUIRED_OPTIONS.map(o => ( ))}
{acquiredVia === "gift" && (

Gift from

setGiftFrom(e.target.value)} placeholder="e.g. Nani" className="w-full px-3 py-2 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-sm focus:outline-none" />
)}
{/* Action buttons */}
{!sizeLabel && (

Please select a size to save

)}
)}
); }