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:
@@ -18,6 +18,8 @@ interface CandidateCardProps {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
status: "researching" | "ordered" | "arrived";
|
status: "researching" | "ordered" | "arrived";
|
||||||
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
|
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
|
||||||
|
pros?: string | null;
|
||||||
|
cons?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CandidateCard({
|
export function CandidateCard({
|
||||||
@@ -33,6 +35,8 @@ export function CandidateCard({
|
|||||||
isActive,
|
isActive,
|
||||||
status,
|
status,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
|
pros,
|
||||||
|
cons,
|
||||||
}: CandidateCardProps) {
|
}: CandidateCardProps) {
|
||||||
const unit = useWeightUnit();
|
const unit = useWeightUnit();
|
||||||
const currency = useCurrency();
|
const currency = useCurrency();
|
||||||
@@ -174,6 +178,11 @@ export function CandidateCard({
|
|||||||
{categoryName}
|
{categoryName}
|
||||||
</span>
|
</span>
|
||||||
<StatusBadge status={status} onStatusChange={onStatusChange} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ interface FormData {
|
|||||||
notes: string;
|
notes: string;
|
||||||
productUrl: string;
|
productUrl: string;
|
||||||
imageFilename: string | null;
|
imageFilename: string | null;
|
||||||
|
pros: string;
|
||||||
|
cons: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_FORM: FormData = {
|
const INITIAL_FORM: FormData = {
|
||||||
@@ -29,6 +31,8 @@ const INITIAL_FORM: FormData = {
|
|||||||
notes: "",
|
notes: "",
|
||||||
productUrl: "",
|
productUrl: "",
|
||||||
imageFilename: null,
|
imageFilename: null,
|
||||||
|
pros: "",
|
||||||
|
cons: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CandidateForm({
|
export function CandidateForm({
|
||||||
@@ -61,6 +65,8 @@ export function CandidateForm({
|
|||||||
notes: candidate.notes ?? "",
|
notes: candidate.notes ?? "",
|
||||||
productUrl: candidate.productUrl ?? "",
|
productUrl: candidate.productUrl ?? "",
|
||||||
imageFilename: candidate.imageFilename,
|
imageFilename: candidate.imageFilename,
|
||||||
|
pros: candidate.pros ?? "",
|
||||||
|
cons: candidate.cons ?? "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (mode === "add") {
|
} else if (mode === "add") {
|
||||||
@@ -110,6 +116,8 @@ export function CandidateForm({
|
|||||||
notes: form.notes.trim() || undefined,
|
notes: form.notes.trim() || undefined,
|
||||||
productUrl: form.productUrl.trim() || undefined,
|
productUrl: form.productUrl.trim() || undefined,
|
||||||
imageFilename: form.imageFilename ?? undefined,
|
imageFilename: form.imageFilename ?? undefined,
|
||||||
|
pros: form.pros.trim() || undefined,
|
||||||
|
cons: form.cons.trim() || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
@@ -239,6 +247,42 @@ export function CandidateForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Product Link */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface CandidateResponse {
|
|||||||
productUrl: string | null;
|
productUrl: string | null;
|
||||||
imageFilename: string | null;
|
imageFilename: string | null;
|
||||||
status: "researching" | "ordered" | "arrived";
|
status: "researching" | "ordered" | "arrived";
|
||||||
|
pros: string | null;
|
||||||
|
cons: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ function ThreadDetailPage() {
|
|||||||
status: newStatus,
|
status: newStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
pros={candidate.pros}
|
||||||
|
cons={candidate.cons}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user