import { useEffect, useState } from "react"; import { useCreateCandidate, useUpdateCandidate } from "../hooks/useCandidates"; import { useThread } from "../hooks/useThreads"; import { useUIStore } from "../stores/uiStore"; import { CategoryPicker } from "./CategoryPicker"; import { ImageUpload } from "./ImageUpload"; interface CandidateFormProps { mode: "add" | "edit"; threadId: number; candidateId?: number | null; } interface FormData { name: string; weightGrams: string; priceDollars: string; categoryId: number; notes: string; productUrl: string; imageFilename: string | null; } const INITIAL_FORM: FormData = { name: "", weightGrams: "", priceDollars: "", categoryId: 1, notes: "", productUrl: "", imageFilename: null, }; export function CandidateForm({ mode, threadId, candidateId, }: CandidateFormProps) { const { data: thread } = useThread(threadId); const createCandidate = useCreateCandidate(threadId); const updateCandidate = useUpdateCandidate(threadId); const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel); const [form, setForm] = useState(INITIAL_FORM); const [errors, setErrors] = useState>({}); // Pre-fill form when editing useEffect(() => { if (mode === "edit" && candidateId != null && thread?.candidates) { const candidate = thread.candidates.find((c) => c.id === candidateId); if (candidate) { setForm({ name: candidate.name, weightGrams: candidate.weightGrams != null ? String(candidate.weightGrams) : "", priceDollars: candidate.priceCents != null ? (candidate.priceCents / 100).toFixed(2) : "", categoryId: candidate.categoryId, notes: candidate.notes ?? "", productUrl: candidate.productUrl ?? "", imageFilename: candidate.imageFilename, }); } } else if (mode === "add") { setForm(INITIAL_FORM); } }, [mode, candidateId, thread?.candidates]); function validate(): boolean { const newErrors: Record = {}; if (!form.name.trim()) { newErrors.name = "Name is required"; } if ( form.weightGrams && (Number.isNaN(Number(form.weightGrams)) || Number(form.weightGrams) < 0) ) { newErrors.weightGrams = "Must be a positive number"; } if ( form.priceDollars && (Number.isNaN(Number(form.priceDollars)) || Number(form.priceDollars) < 0) ) { newErrors.priceDollars = "Must be a positive number"; } if ( form.productUrl && form.productUrl.trim() !== "" && !form.productUrl.match(/^https?:\/\//) ) { newErrors.productUrl = "Must be a valid URL (https://...)"; } setErrors(newErrors); return Object.keys(newErrors).length === 0; } function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!validate()) return; const payload = { name: form.name.trim(), weightGrams: form.weightGrams ? Number(form.weightGrams) : undefined, priceCents: form.priceDollars ? Math.round(Number(form.priceDollars) * 100) : undefined, categoryId: form.categoryId, notes: form.notes.trim() || undefined, productUrl: form.productUrl.trim() || undefined, imageFilename: form.imageFilename ?? undefined, }; if (mode === "add") { createCandidate.mutate(payload, { onSuccess: () => { setForm(INITIAL_FORM); closeCandidatePanel(); }, }); } else if (candidateId != null) { updateCandidate.mutate( { candidateId, ...payload }, { onSuccess: () => closeCandidatePanel() }, ); } } const isPending = createCandidate.isPending || updateCandidate.isPending; return (
{/* Image */} setForm((f) => ({ ...f, imageFilename: filename })) } /> {/* Name */}
setForm((f) => ({ ...f, name: e.target.value }))} className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" placeholder="e.g. Osprey Talon 22" /> {errors.name && (

{errors.name}

)}
{/* Weight */}
setForm((f) => ({ ...f, weightGrams: e.target.value })) } className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" placeholder="e.g. 680" /> {errors.weightGrams && (

{errors.weightGrams}

)}
{/* Price */}
setForm((f) => ({ ...f, priceDollars: e.target.value })) } className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" placeholder="e.g. 129.99" /> {errors.priceDollars && (

{errors.priceDollars}

)}
{/* Category */}
setForm((f) => ({ ...f, categoryId: id }))} />
{/* Notes */}