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'
|
'use client'
|
||||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||||
|
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||||
type GeoData = Record<string, Record<string, string[]>>
|
|
||||||
|
|
||||||
interface BarrioAutocompleteFieldProps {
|
interface BarrioAutocompleteFieldProps {
|
||||||
name: string
|
name: string
|
||||||
@@ -25,16 +24,16 @@ export function BarrioAutocompleteField({
|
|||||||
error,
|
error,
|
||||||
}: BarrioAutocompleteFieldProps) {
|
}: BarrioAutocompleteFieldProps) {
|
||||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||||
|
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [activeIdx, setActiveIdx] = useState(-1)
|
const [activeIdx, setActiveIdx] = useState(-1)
|
||||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/data/geo_paraguay_full.json')
|
getGeoData()
|
||||||
.then((r) => r.json())
|
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||||
.then(setGeoData)
|
.catch(() => setGeoStatus('error'))
|
||||||
.catch(() => {})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Close dropdown on scroll so the fixed-position list doesn't drift from the input
|
// Close dropdown on scroll so the fixed-position list doesn't drift from the input
|
||||||
@@ -126,6 +125,12 @@ export function BarrioAutocompleteField({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="text-short-input"
|
className="text-short-input"
|
||||||
ref={inputRef}
|
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)}
|
onChange={(e) => handleInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react'
|
||||||
|
import { getGeoData, type GeoData } from '@/lib/geo-cache'
|
||||||
type GeoData = Record<string, Record<string, string[]>>
|
|
||||||
|
|
||||||
interface CiudadSelectFieldProps {
|
interface CiudadSelectFieldProps {
|
||||||
name: string
|
name: string
|
||||||
@@ -23,6 +22,7 @@ export function CiudadSelectField({
|
|||||||
error,
|
error,
|
||||||
}: CiudadSelectFieldProps) {
|
}: CiudadSelectFieldProps) {
|
||||||
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
const [geoData, setGeoData] = useState<GeoData | null>(null)
|
||||||
|
const [geoStatus, setGeoStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [activeIdx, setActiveIdx] = useState(-1)
|
const [activeIdx, setActiveIdx] = useState(-1)
|
||||||
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
|
||||||
@@ -35,10 +35,9 @@ export function CiudadSelectField({
|
|||||||
const prevDeptoRef = useRef(departamento)
|
const prevDeptoRef = useRef(departamento)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/data/geo_paraguay_full.json')
|
getGeoData()
|
||||||
.then((r) => r.json())
|
.then((data) => { setGeoData(data); setGeoStatus('ready') })
|
||||||
.then(setGeoData)
|
.catch(() => setGeoStatus('error'))
|
||||||
.catch(() => {})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Clear city when departamento changes so no orphan value remains
|
// Clear city when departamento changes so no orphan value remains
|
||||||
@@ -130,6 +129,12 @@ export function CiudadSelectField({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="text-short-input"
|
className="text-short-input"
|
||||||
ref={inputRef}
|
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)}
|
onChange={(e) => handleInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export function WizardForm({ survey, schema }: WizardFormProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hydrated) return
|
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])
|
}, [state.answers, hydrated, survey.id])
|
||||||
|
|
||||||
const screens = resolveScreens(schema, state.answers)
|
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.' }))
|
setErrors((e) => ({ ...e, [screen.question!.code]: 'Este campo es obligatorio.' }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveDraft(survey.id, state.answers)
|
||||||
setState((s) => next(s, schema))
|
setState((s) => next(s, schema))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
|
saveDraft(survey.id, state.answers)
|
||||||
setState((s) => back(s, schema))
|
setState((s) => back(s, schema))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
lib/geo-cache.ts
Normal file
16
lib/geo-cache.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user