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:
markosbenitez
2026-06-28 22:04:12 -03:00
parent 5472043a33
commit 704d2407e8
4 changed files with 42 additions and 13 deletions

View File

@@ -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) } }}

View File

@@ -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) } }}

View File

@@ -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))
}

16
lib/geo-cache.ts Normal file
View File

@@ -0,0 +1,16 @@
// departamento → ciudad → barrios[]
export type GeoData = Record<string, Record<string, string[]>>
let geoData: GeoData | null = null
let geoPromise: Promise<GeoData> | null = null
export async function getGeoData(): Promise<GeoData> {
if (geoData) return geoData
if (!geoPromise) {
geoPromise = fetch('/data/geo_paraguay_full.json')
.then((r) => r.json())
.then((data: GeoData) => { geoData = data; return data })
.catch((err) => { geoPromise = null; throw err })
}
return geoPromise
}