Files
apps-registry/workloads/ecommerce/components/layout/location-picker.tsx
T
devopsandClaude Opus 5.5 e6f031a366 feat(storefront): barra de navegación desktop con categorías, personajes, ayuda y ubicación
- Desktop xl+: las 6 categorías raíz directo en la barra con desplegable
  de subcategorías; lg-xl mantiene "Departamentos"; "Personajes"
  (colecciones) y "Ayuda" a la derecha; hamburguesa solo en mobile.
- "Enviar a <nombre> · <ciudad>" (location-store persistido) que precarga
  departamento/ciudad en el checkout; "Hola X / Mi cuenta" y "Cambios y
  devoluciones" en xl+.
- Header compacto al hacer scroll en lg+ (histéresis 160/40px).
- Fix de menús (también en DepartmentsMegaMenu, ya presente en prod): el
  click de mouse ya no cierra el menú recién abierto por hover
  (keepOpenAfterHover) y el hover solo reacciona a pointerType "mouse",
  así el mouseleave sintético de touch no autocierra en tablets.

Revisado por el subagente frontend-critic (3 hallazgos, todos corregidos y
re-verificados) + Playwright contra build de producción local: sin solapes
a 1024/1280/1440/1920, click real, touch, mobile 390/320 sin cambios.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
2026-09-27 05:22:21 +00:00

101 lines
4.4 KiB
TypeScript

"use client";
import { MapPin } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { ciudades, ciudadesPorDepartamento, departamentos } from "@/lib/data/colombia";
import { useLocationStore } from "@/store/location-store";
const selectClass =
"h-11 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-950 outline-none focus:border-ari-coral focus:ring-4 focus:ring-ari-coral/15";
// "Enviar a <nombre> · <ciudad>" del header (inspirado en el bloque de
// ubicacion de Amazon). La ciudad elegida se reusa en el checkout.
export function LocationPicker({ firstName }: { firstName: string | null }) {
const departmentId = useLocationStore((state) => state.departmentId);
const cityId = useLocationStore((state) => state.cityId);
const hydrated = useLocationStore((state) => state.hasHydrated);
const setLocation = useLocationStore((state) => state.setLocation);
const [open, setOpen] = useState(false);
const [draftDepartment, setDraftDepartment] = useState<number | null>(null);
const [draftCity, setDraftCity] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const city = hydrated && cityId ? ciudades.find((item) => item.id === cityId)?.name : undefined;
useEffect(() => {
if (!open) return;
setDraftDepartment(departmentId);
setDraftCity(cityId);
function handlePointerDown(event: PointerEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) setOpen(false);
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") setOpen(false);
}
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [open, departmentId, cityId]);
const draftCities = draftDepartment ? ciudadesPorDepartamento(draftDepartment) : [];
return (
<div ref={containerRef} className="relative">
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-label={city ? `Enviar a ${city}. Cambiar ciudad` : "Elegir ciudad de envío"}
className="flex items-center gap-1.5 rounded-xl px-2 py-1.5 text-left transition hover:bg-slate-100"
>
<MapPin className="size-5 shrink-0 text-ari-coral" />
<span className="leading-tight">
<span className="block text-xs text-slate-500">Enviar a {firstName || "Colombia"}</span>
<span className="block max-w-36 truncate text-sm font-black text-ari-marino">{city ?? "Elige tu ciudad"}</span>
</span>
</button>
{open && (
<div className="absolute left-0 top-full z-50 mt-2 w-80 rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl">
<p className="font-black text-ari-marino">¿A dónde enviamos?</p>
<p className="mt-1 text-xs text-slate-500">Hacemos envíos a toda Colombia. Tu ciudad queda lista en el checkout.</p>
<div className="mt-4 grid gap-3">
<select
aria-label="Departamento"
className={selectClass}
value={draftDepartment ?? ""}
onChange={(event) => { setDraftDepartment(Number(event.target.value) || null); setDraftCity(null); }}
>
<option value="">Departamento</option>
{departamentos.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
<select
aria-label="Ciudad"
className={selectClass}
value={draftCity ?? ""}
disabled={!draftDepartment}
onChange={(event) => setDraftCity(Number(event.target.value) || null)}
>
<option value="">Ciudad o municipio</option>
{draftCities.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
<Button
variant="primary"
disabled={!draftDepartment || !draftCity}
onClick={() => {
if (draftDepartment && draftCity) setLocation(draftDepartment, draftCity);
setOpen(false);
}}
>
Guardar ciudad
</Button>
</div>
</div>
)}
</div>
);
}