teqo/src/components/campaign/TseZoneInput.tsx
Francisco Ignácio Fontoura Solla bcac665cb5 Ship /campanha nuclei MVP with RBAC, invites, and vote estimates.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.
2026-07-18 15:01:56 -03:00

100 lines
3.3 KiB
TypeScript

'use client'
import { useState } from 'react'
import { XIcon } from 'lucide-react'
import { Badge } from '@/components/ui/Badge'
import { FieldDescription, FieldError } from '@/components/ui/field'
import { InputGroup, InputGroupInput } from '@/components/ui/input-group'
import { parseTseZoneNumbers, TseZoneParseError } from '@/utilities/tseZone'
const sortedUnique = (values: number[]): number[] =>
[...new Set(values)].sort((left, right) => left - right)
export const TseZoneInput = ({
defaultValues = [],
error,
}: {
defaultValues?: number[]
error?: string
}) => {
const [zones, setZones] = useState(() => sortedUnique(defaultValues))
const [draft, setDraft] = useState('')
const [localError, setLocalError] = useState<string>()
const visibleError = localError ?? error
const commit = (value: string) => {
if (!value.trim()) {
setDraft('')
return
}
try {
const parsed = parseTseZoneNumbers(value)
setZones((current) => sortedUnique([...current, ...parsed]))
setDraft('')
setLocalError(undefined)
} catch (parseError) {
setLocalError(
parseError instanceof TseZoneParseError ? parseError.message : 'Zona TSE inválida.',
)
}
}
return (
<>
<input type="hidden" name="tseZones" value={zones.join(',')} />
<InputGroup className="min-h-11 h-auto flex-wrap gap-1 p-1">
{zones.map((zoneNumber) => (
<Badge
key={zoneNumber}
asChild
variant="tse"
className="h-8 gap-1 rounded-sm px-2.5 py-1"
>
<button
type="button"
onClick={() => setZones((current) => current.filter((zone) => zone !== zoneNumber))}
aria-label={`Remover Zona TSE ${zoneNumber}`}
>
<span aria-hidden="true">{zoneNumber}</span>
<XIcon aria-hidden="true" className="size-3" />
</button>
</Badge>
))}
<InputGroupInput
id="tseZoneDraft"
value={draft}
inputMode="numeric"
placeholder="Digite um número e pressione Enter"
aria-label="Adicionar Zona TSE"
aria-invalid={Boolean(visibleError)}
aria-describedby={visibleError ? 'tseZones-error' : 'tseZones-description'}
className="min-w-48"
onChange={(event) => {
const nextValue = event.target.value
if (!/^[\d,\s]*$/.test(nextValue)) {
setLocalError('Use apenas números, vírgulas e espaços.')
return
}
setLocalError(undefined)
if (/[, \t\n]$/.test(nextValue)) commit(nextValue)
else setDraft(nextValue)
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
commit(draft)
} else if (event.key === 'Backspace' && !draft && zones.length > 0) {
setZones((current) => current.slice(0, -1))
}
}}
onBlur={() => commit(draft)}
/>
</InputGroup>
<FieldDescription id="tseZones-description">
Use vírgula, espaço ou Enter para adicionar. As Zonas TSE ficam em ordem crescente.
</FieldDescription>
{visibleError ? <FieldError id="tseZones-error">{visibleError}</FieldError> : null}
</>
)
}