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;
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user