diff --git a/components/question-renderer.tsx b/components/question-renderer.tsx index 4343e7c..566d727 100644 --- a/components/question-renderer.tsx +++ b/components/question-renderer.tsx @@ -5,6 +5,7 @@ import { CheckboxGroup } from './widgets/checkbox-group' import { TextAreaField } from './widgets/text-area-field' import { TextShortField } from './widgets/text-short-field' import { BarrioAutocompleteField } from './widgets/barrio-autocomplete-field' +import { CiudadSelectField } from './widgets/ciudad-select-field' interface QuestionRendererProps { question: Question @@ -28,6 +29,19 @@ export function QuestionRenderer({ question, value, onChange, error, answers }: ) if (type === 'single_choice' && options) { + if (code === '2.2') { + return ( + + ) + } return ( > + +interface CiudadSelectFieldProps { + name: string + label: ReactNode + value: string + required: boolean + departamento?: string | null + onChange: (value: string) => void + error?: string +} + +export function CiudadSelectField({ + name, + label, + value, + required, + departamento, + onChange, + error, +}: CiudadSelectFieldProps) { + const [geoData, setGeoData] = useState(null) + const [open, setOpen] = useState(false) + const [activeIdx, setActiveIdx] = useState(-1) + const [dropdownStyle, setDropdownStyle] = useState({}) + const inputRef = useRef(null) + + // Stable ref to always call the latest onChange without it in effect deps + const onChangeRef = useRef(onChange) + onChangeRef.current = onChange + + const prevDeptoRef = useRef(departamento) + + useEffect(() => { + fetch('/data/geo_paraguay_full.json') + .then((r) => r.json()) + .then(setGeoData) + .catch(() => {}) + }, []) + + // Clear city when departamento changes so no orphan value remains + useEffect(() => { + if (prevDeptoRef.current !== departamento) { + onChangeRef.current('') + setOpen(false) + } + prevDeptoRef.current = departamento + }, [departamento]) + + // Close fixed dropdown on any scroll (input moves, list stays) + useEffect(() => { + if (!open) return + const close = () => setOpen(false) + window.addEventListener('scroll', close, { passive: true, capture: true }) + return () => window.removeEventListener('scroll', close, { capture: true }) + }, [open]) + + function computeDropdownStyle() { + if (!inputRef.current) return + const rect = inputRef.current.getBoundingClientRect() + setDropdownStyle({ top: rect.bottom + 4, left: rect.left, width: rect.width }) + } + + const ciudades: string[] = (() => { + if (!geoData) return [] + if (departamento) return Object.keys(geoData[departamento] ?? {}) + return Object.values(geoData).flatMap((d) => Object.keys(d)) + })() + + const query = value.trim().toLowerCase() + const suggestions = + query.length >= 2 + ? ciudades.filter((c) => c.toLowerCase().includes(query)).slice(0, 10) + : [] + + function handleInput(v: string) { + onChange(v) + computeDropdownStyle() + setOpen(true) + setActiveIdx(-1) + } + + function select(ciudad: string) { + onChange(ciudad) + setOpen(false) + setActiveIdx(-1) + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (!open || suggestions.length === 0) return + if (e.key === 'ArrowDown') { + e.preventDefault() + setActiveIdx((i) => Math.min(i + 1, suggestions.length - 1)) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setActiveIdx((i) => Math.max(i - 1, -1)) + } else if (e.key === 'Enter' && activeIdx >= 0) { + e.preventDefault() + select(suggestions[activeIdx]) + } else if (e.key === 'Escape') { + setOpen(false) + } + } + + const listId = `${name}-suggestions` + + return ( +
+ +
+ 0 ? listId : undefined} + aria-activedescendant={activeIdx >= 0 ? `${name}-opt-${activeIdx}` : undefined} + autoComplete="off" + className="text-short-input" + ref={inputRef} + onChange={(e) => handleInput(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }} + onBlur={() => setTimeout(() => setOpen(false), 150)} + /> + {open && suggestions.length > 0 && ( +
    + {suggestions.map((ciudad, i) => ( +
  • select(ciudad)} + > + {ciudad} +
  • + ))} +
+ )} +
+ {error && ( + + )} +
+ ) +}