Phase 1 members now entered as inline text rows with a '+ Add member' affordance and per-row remove, per the pre-deployed-devices onboarding rework. Parent store still receives only trimmed, non-empty names. Caps at 5. MemberChips retained (unused) for now. Tests updated. Co-Authored-By: Claude Opus 4.8 <[email protected]>
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
import { useState } from 'react'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Button } from '@/components/ui/button'
|
||
|
||
const MAX_MEMBERS = 5
|
||
|
||
export interface MemberFieldsProps {
|
||
members: string[]
|
||
onChange: (next: string[]) => void
|
||
}
|
||
|
||
/**
|
||
* Inline member entry: one text field per member with a "+" to append another
|
||
* row below, and a "×" to remove a row. The parent store only ever sees the
|
||
* trimmed, non-empty names; empty rows are a local editing affordance.
|
||
*/
|
||
export function MemberFields({ members, onChange }: MemberFieldsProps) {
|
||
// Seed local rows from the parent (always at least one row to type into).
|
||
const [rows, setRows] = useState<string[]>(members.length ? members : [''])
|
||
|
||
const commit = (next: string[]) => {
|
||
setRows(next)
|
||
onChange(next.map((r) => r.trim()).filter(Boolean))
|
||
}
|
||
|
||
const setRow = (i: number, value: string) => {
|
||
const next = rows.slice()
|
||
next[i] = value
|
||
commit(next)
|
||
}
|
||
|
||
const addRow = () => {
|
||
if (rows.length >= MAX_MEMBERS) return
|
||
setRows([...rows, '']) // don't commit — empty row adds nothing to the store
|
||
}
|
||
|
||
const removeRow = (i: number) => {
|
||
const next = rows.length > 1 ? rows.filter((_, idx) => idx !== i) : ['']
|
||
commit(next)
|
||
}
|
||
|
||
const full = rows.length >= MAX_MEMBERS
|
||
const filled = rows.filter((r) => r.trim()).length
|
||
|
||
return (
|
||
<div className="space-y-2" data-testid="member-fields">
|
||
{rows.map((row, i) => (
|
||
<div key={i} className="flex gap-2 items-center">
|
||
<span className="font-mono text-[10px] text-muted-foreground w-4 shrink-0 text-right">
|
||
{i + 1}
|
||
</span>
|
||
<Input
|
||
aria-label={`Member ${i + 1}`}
|
||
placeholder="e.g. A. Rossi"
|
||
value={row}
|
||
onChange={(e) => setRow(i, e.target.value)}
|
||
className="font-mono text-sm"
|
||
/>
|
||
<button
|
||
type="button"
|
||
aria-label={`Remove member ${i + 1}`}
|
||
onClick={() => removeRow(i)}
|
||
className="text-muted-foreground hover:text-destructive transition leading-none px-1.5 text-lg shrink-0"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
<div className="flex items-center justify-between pl-6">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={addRow}
|
||
disabled={full}
|
||
className="font-mono text-[11px] tracking-wider uppercase h-7 px-2"
|
||
>
|
||
+ Add member
|
||
</Button>
|
||
<span className="font-mono text-[10px] text-muted-foreground tracking-wider uppercase">
|
||
{filled} / {MAX_MEMBERS}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|