src/features/reviews/workspace-household.tsx
Linked repository file from this build.
"use client";
import { MAX_HOUSEHOLD_SIZE } from "@/programs/snap-negative/input-limits";
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { HouseholdMember, SnapReview } from "@/programs/snap-negative/types";
export function WorkspaceHousehold({
review,
editable,
onMembersChange,
onClear,
onReset,
}: {
review: SnapReview;
editable: boolean;
onClear(): void;
onReset?(): void;
onMembersChange(members: HouseholdMember[]): void;
}) {
const members = review.facts.members;
function updateMember(index: number, patch: Partial<HouseholdMember>) {
onMembersChange(members.map((member, i) => (i === index ? { ...member, ...patch } : member)));
}
function removeMember(index: number) {
onMembersChange(members.filter((_, i) => i !== index));
}
function addMember() {
if (members.length >= MAX_HOUSEHOLD_SIZE) return;
onMembersChange([
...members,
{
id: crypto.randomUUID(),
line: String(members.length + 1),
name: "",
relationship: "",
},
]);
}
return (
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden">
<section
data-section="household"
aria-label="Household members"
className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden"
>
{editable && (
<div className="flex shrink-0 items-center justify-end gap-2">
<Button
type="button"
size="sm"
variant="ghost"
disabled={!members.length}
onClick={onClear}
>
Clear
</Button>
{onReset && (
<Button type="button" size="sm" variant="ghost" onClick={onReset}>
Reset
</Button>
)}
<Button
type="button"
size="sm"
variant="outline"
onClick={addMember}
disabled={members.length >= MAX_HOUSEHOLD_SIZE}
title={
members.length >= MAX_HOUSEHOLD_SIZE ? "Maximum 20 household members." : undefined
}
>
<Plus className="size-3.5" aria-hidden />
Add member
</Button>
</div>
)}
<div className="min-h-0 flex-1 overflow-auto">
<table className="w-full min-w-[28rem] table-fixed border-collapse text-sm">
<caption className="sr-only">Editable household member list</caption>
<colgroup>
<col className="w-16" />
<col />
<col className="w-[11rem]" />
{editable && <col className="w-10" />}
</colgroup>
<thead className="sticky top-0 z-10 bg-background">
<tr className="text-left text-muted-foreground">
<th scope="col" className="px-2 py-2 text-center font-medium">
Line
</th>
<th scope="col" className="px-2 py-2 font-medium">
Name
</th>
<th scope="col" className="px-2 py-2 font-medium">
Relationship
</th>
{editable && (
<th scope="col" className="w-10 px-2 py-2">
<span className="sr-only">Remove</span>
</th>
)}
</tr>
</thead>
<tbody>
{members.map((member, index) => (
<tr key={member.id}>
<td className="px-2 py-3">
<Label htmlFor={`member-line-${member.id}`} className="sr-only">
Line for member {index + 1}
</Label>
<Input
id={`member-line-${member.id}`}
value={member.line}
readOnly={!editable}
className="px-1.5 text-center"
onChange={(event) => updateMember(index, { line: event.target.value })}
/>
</td>
<td className="px-2 py-3">
<Label htmlFor={`member-name-${member.id}`} className="sr-only">
Name for member {index + 1}
</Label>
<Input
id={`member-name-${member.id}`}
value={member.name}
readOnly={!editable}
onChange={(event) => updateMember(index, { name: event.target.value })}
/>
</td>
<td className="px-2 py-3">
<Label htmlFor={`member-rel-${member.id}`} className="sr-only">
Relationship for member {index + 1}
</Label>
<Input
id={`member-rel-${member.id}`}
value={member.relationship}
readOnly={!editable}
onChange={(event) =>
updateMember(index, { relationship: event.target.value })
}
/>
</td>
{editable && (
<td className="px-2 py-3">
<Button
type="button"
size="icon"
variant="ghost"
className="size-8"
disabled={members.length <= 1}
aria-label={`Remove member ${member.name || index + 1}`}
onClick={() => removeMember(index)}
>
<Trash2 className="size-3.5" />
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
}