fix(wizard): cache geo singleton, feedback carga, debounce draft
geo_paraguay_full.json se cargaba dos veces sin cache compartido. Módulo lib/geo-cache.ts singleton: un fetch para toda la sesión. Feedback visual mientras carga (disabled + placeholder) y si falla (mensaje + texto libre). debounce 500ms en saveDraft para no escribir en localStorage en cada keystroke. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||
|
||||
type GeoData = Record<string, Record<string, string[]>>
|
||||
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||
|
||||
interface BarrioAutocompleteFieldProps {
|
||||
name: string
|
||||
@@ -25,16 +24,16 @@ export function BarrioAutocompleteField({
|
||||
error,
|
||||
}: BarrioAutocompleteFieldProps) {
|
||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeIdx, setActiveIdx] = useState(-1)
|
||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/geo_paraguay_full.json')
|
||||
.then((r) => r.json())
|
||||
.then(setGeoData)
|
||||
.catch(() => {})
|
||||
getGeoData()
|
||||
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||
.catch(() => setGeoStatus('error'))
|
||||
}, [])
|
||||
|
||||
// Close dropdown on scroll so the fixed-position list doesn't drift from the input
|
||||
@@ -126,6 +125,12 @@ export function BarrioAutocompleteField({
|
||||
autoComplete="off"
|
||||
className="text-short-input"
|
||||
ref={inputRef}
|
||||
disabled={geoStatus === 'loading'}
|
||||
placeholder={
|
||||
geoStatus === 'loading' ? 'Cargando opciones...' :
|
||||
geoStatus === 'error' ? 'No se pudieron cargar las opciones. Podés escribir el nombre manualmente.' :
|
||||
undefined
|
||||
}
|
||||
onChange={(e) => handleInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||
|
||||
type GeoData = Record<string, Record<string, string[]>>
|
||||
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||
|
||||
interface CiudadSelectFieldProps {
|
||||
name: string
|
||||
@@ -23,6 +22,7 @@ export function CiudadSelectField({
|
||||
error,
|
||||
}: CiudadSelectFieldProps) {
|
||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeIdx, setActiveIdx] = useState(-1)
|
||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||
@@ -35,10 +35,9 @@ export function CiudadSelectField({
|
||||
const prevDeptoRef = useRef(departamento)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/geo_paraguay_full.json')
|
||||
.then((r) => r.json())
|
||||
.then(setGeoData)
|
||||
.catch(() => {})
|
||||
getGeoData()
|
||||
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||
.catch(() => setGeoStatus('error'))
|
||||
}, [])
|
||||
|
||||
// Clear city when departamento changes so no orphan value remains
|
||||
@@ -130,6 +129,12 @@ export function CiudadSelectField({
|
||||
autoComplete="off"
|
||||
className="text-short-input"
|
||||
ref={inputRef}
|
||||
disabled={geoStatus === 'loading'}
|
||||
placeholder={
|
||||
geoStatus === 'loading' ? 'Cargando opciones...' :
|
||||
geoStatus === 'error' ? 'No se pudieron cargar las opciones. Podés escribir el nombre manualmente.' :
|
||||
undefined
|
||||
}
|
||||
onChange={(e) => handleInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||
|
||||
@@ -70,7 +70,8 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return
|
||||
saveDraft(survey.id, state.answers)
|
||||
const timer = setTimeout(() => saveDraft(survey.id, state.answers), 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [state.answers, hydrated, survey.id])
|
||||
|
||||
const screens = resolveScreens(schema, state.answers)
|
||||
@@ -192,10 +193,12 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
||||
setErrors((e) => ({ ...e, [screen.question!.code]: 'Este campo es obligatorio.' }))
|
||||
return
|
||||
}
|
||||
saveDraft(survey.id, state.answers)
|
||||
setState((s) => next(s, schema))
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
saveDraft(survey.id, state.answers)
|
||||
setState((s) => back(s, schema))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user