feat(10-01): wire pros/cons through client hooks, form, and card indicator

- CandidateResponse: add pros/cons string|null fields
- CandidateForm: add pros/cons to FormData, INITIAL_FORM, pre-fill, payload
- CandidateForm: add Pros/Cons textarea inputs (after Notes, before Product Link)
- CandidateCard: add pros/cons props, render purple +/- Notes badge when present
- Thread detail route: pass pros/cons props to CandidateCard
This commit is contained in:
2026-03-16 21:36:10 +01:00
parent 7a64a1887d
commit 4f2aefe7a4
4 changed files with 57 additions and 0 deletions

View File

@@ -18,6 +18,8 @@ interface CandidateCardProps {
isActive: boolean;
status: "researching" | "ordered" | "arrived";
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
pros?: string | null;
cons?: string | null;
}
export function CandidateCard({
@@ -33,6 +35,8 @@ export function CandidateCard({
isActive,
status,
onStatusChange,
pros,
cons,
}: CandidateCardProps) {
const unit = useWeightUnit();
const currency = useCurrency();
@@ -174,6 +178,11 @@ export function CandidateCard({
{categoryName}
</span>
<StatusBadge status={status} onStatusChange={onStatusChange} />
{(pros || cons) && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-700">
+/- Notes
</span>
)}
</div>
</div>
</button>

View File

@@ -19,6 +19,8 @@ interface FormData {
notes: string;
productUrl: string;
imageFilename: string | null;
pros: string;
cons: string;
}
const INITIAL_FORM: FormData = {
@@ -29,6 +31,8 @@ const INITIAL_FORM: FormData = {
notes: "",
productUrl: "",
imageFilename: null,
pros: "",
cons: "",
};
export function CandidateForm({
@@ -61,6 +65,8 @@ export function CandidateForm({
notes: candidate.notes ?? "",
productUrl: candidate.productUrl ?? "",
imageFilename: candidate.imageFilename,
pros: candidate.pros ?? "",
cons: candidate.cons ?? "",
});
}
} else if (mode === "add") {
@@ -110,6 +116,8 @@ export function CandidateForm({
notes: form.notes.trim() || undefined,
productUrl: form.productUrl.trim() || undefined,
imageFilename: form.imageFilename ?? undefined,
pros: form.pros.trim() || undefined,
cons: form.cons.trim() || undefined,
};
if (mode === "add") {
@@ -239,6 +247,42 @@ export function CandidateForm({
/>
</div>
{/* Pros */}
<div>
<label
htmlFor="candidate-pros"
className="block text-sm font-medium text-gray-700 mb-1"
>
Pros
</label>
<textarea
id="candidate-pros"
value={form.pros}
onChange={(e) => setForm((f) => ({ ...f, pros: e.target.value }))}
rows={3}
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 resize-none"
placeholder="One pro per line..."
/>
</div>
{/* Cons */}
<div>
<label
htmlFor="candidate-cons"
className="block text-sm font-medium text-gray-700 mb-1"
>
Cons
</label>
<textarea
id="candidate-cons"
value={form.cons}
onChange={(e) => setForm((f) => ({ ...f, cons: e.target.value }))}
rows={3}
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 resize-none"
placeholder="One con per line..."
/>
</div>
{/* Product Link */}
<div>
<label

View File

@@ -13,6 +13,8 @@ interface CandidateResponse {
productUrl: string | null;
imageFilename: string | null;
status: "researching" | "ordered" | "arrived";
pros: string | null;
cons: string | null;
createdAt: string;
updatedAt: string;
}

View File

@@ -153,6 +153,8 @@ function ThreadDetailPage() {
status: newStatus,
})
}
pros={candidate.pros}
cons={candidate.cons}
/>
))}
</div>