- 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
101 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|