"use client"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Search, X } from "lucide-react"; import { FormEvent, useEffect, useState } from "react"; import type { Product } from "@/lib/headless/types"; export function PredictiveSearch() { const router = useRouter(); const [query, setQuery] = useState(""); const [matches, setMatches] = useState([]); const [loading, setLoading] = useState(false); const normalized = query.trim(); useEffect(() => { if (normalized.length < 2) { setMatches([]); setLoading(false); return; } const controller = new AbortController(); setLoading(true); const timer = setTimeout(() => { fetch(`/api/search?q=${encodeURIComponent(normalized)}`, { signal: controller.signal, }) .then((response) => response.json()) .then((data: { products?: Product[] }) => { setMatches((data.products ?? []).slice(0, 6)); }) .catch((error) => { if ((error as Error).name !== "AbortError") setMatches([]); }) .finally(() => setLoading(false)); }, 250); return () => { clearTimeout(timer); controller.abort(); }; }, [normalized]); const submit = (event: FormEvent) => { event.preventDefault(); const value = query.trim(); if (!value) return; router.push(`/catalog?q=${encodeURIComponent(value)}`); setQuery(""); }; return (
{/* input propio (no el compartido) -- ese componente trae su propio focus:ring-4 focus:ring-cyan-300/30 hardcodeado que no se anulaba de forma confiable via tailwind-merge dentro de este contenedor compuesto (input + boton circular), causando un segundo ring visible al hacer foco. Mas simple y robusto no heredar clases de foco que hay que pelear despues. focus-visible:outline-none (no outline-none a secas): globals.css define un outline de accesibilidad global por teclado (input:focus-visible, azul de marca) que le gana en especificidad a un outline-none plano. El wrapper de este componente ya tiene su propio indicador de foco visible (focus-within:border-ari-marino + ring amarillo, mismo patron que button.tsx) que cumple el mismo proposito de accesibilidad sin duplicarlo -- variante focus-visible: en vez de la base iguala la especificidad de la regla global (mismo truco que ya usa button.tsx) y evita el doble indicador de foco. */} setQuery(event.target.value)} placeholder="Busca juguetes, mochilas, personajes…" className="h-[48px] w-full border-0 bg-transparent p-0 text-sm text-slate-950 focus-visible:outline-none" /> {query && }
{loading && normalized.length >= 2 && matches.length === 0 && (
Buscando…
)} {/* Encontrado probando en vivo con navegador real: sin este bloque, una busqueda de 2+ caracteres sin coincidencias no mostraba nada (ni "buscando" ni un mensaje) -- el usuario se quedaba sin ningun feedback de que su busqueda de verdad corrio. Se ofrece igual el boton de buscar en todo el catalogo porque esa busqueda completa puede usar criterios mas amplios que el autocompletado. */} {!loading && normalized.length >= 2 && matches.length === 0 && (

No encontramos resultados para “{query.trim()}”.

)} {matches.length > 0 && (
{matches.map((product) => ( setQuery("")}>
{product.name}{product.category} · {product.collection}
))}
)}
); }