Fase 3a ADR 0006 Etapa 2: buscador del navbar via Meilisearch
PredictiveSearch pasa de filtrar en memoria el arreglo completo de productos (prop products, substring match) a un fetch debounced (250ms, con AbortController) contra la nueva ruta /api/search -- mismo componente visual, solo cambia de donde salen los datos, tal como pide el ADR 0006 seccion 5. /api/search (nueva, server-side) es el puente same-origin: el componente es cliente y no puede pegarle directo al provider server-side (MEDUSA_BACKEND_URL es DNS interno del cluster, no resuelve desde el navegador). CatalogProvider gana un metodo OPCIONAL searchWithFacets? (no cambia la firma de getProducts/searchProducts) -- mock.ts y shopify.ts compilan sin tocarlos, siguen sin implementarlo. medusa.ts lo implementa contra /store/search (Fase 2), reutilizando mapProduct() ya que la ruta devuelve el mismo shape de producto que /store/products. searchProducts tambien se repunta a /store/search, dejando de usar el ?q= + re-filtro client-side de fetchProducts/matchesFilters para busqueda (que sigue sirviendo para navegacion sin busqueda, sin cambios). Navbar deja de pasarle products a PredictiveSearch (ya no los necesita) pero los sigue recibiendo el mismo -- collections todavia se deriva de ahi. Limpieza: borrado patches/frontend-medusa-env.yaml, confirmado huerfano (ningun kustomization lo referencia; el patch que si esta vivo es frontend/patches/deployment.yaml). Validado: build completo de Docker (target builder) -- npm ci, next build con typecheck, /api/search aparece en la tabla de rutas generada. Cero errores nuevos (los warnings de ESLint son preexistentes en catalog-client.tsx, no tocado en esta fase).
This commit is contained in:
1 parent
72ae63f2e3
commit
902350b58b
7 files changed
+159
-32
No files matched your search
@@ -56,7 +56,7 @@ export function Navbar({ products, categories }: { products: Product[]; categori
|
||||
<Link href="/" className="relative size-16 overflow-hidden rounded-full border-2 border-slate-900 bg-black shadow-lg lg:size-20">
|
||||
<Image src="/images/brand/logo-ari.png" alt="ARI Shopping" fill priority sizes="80px" className="object-cover" />
|
||||
</Link>
|
||||
<PredictiveSearch products={products} />
|
||||
<PredictiveSearch />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" aria-label="Mi cuenta" className="hidden border-slate-300 bg-white text-slate-900 hover:border-violet-400 hover:bg-violet-50 sm:inline-flex" onClick={() => setAccountOpen(true)}><UserRound /></Button>
|
||||
<Button variant="outline" size="icon" aria-label={`Favoritos: ${wishlistCount}`} className="relative hidden border-slate-300 bg-white text-slate-900 hover:border-fuchsia-400 hover:bg-fuchsia-50 sm:inline-flex" onClick={openWishlist}>
|
||||
|
||||
@@ -4,20 +4,45 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { FormEvent, useMemo, useState } from "react";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Product } from "@/lib/headless/types";
|
||||
|
||||
export function PredictiveSearch({ products }: { products: Product[] }) {
|
||||
export function PredictiveSearch() {
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState("");
|
||||
const normalized = query.trim().toLocaleLowerCase("es");
|
||||
const matches = useMemo(
|
||||
() => normalized.length < 2
|
||||
? []
|
||||
: products.filter((product) => [product.name, product.category, product.collection, product.brand].join(" ").toLocaleLowerCase("es").includes(normalized)).slice(0, 6),
|
||||
[normalized, products],
|
||||
);
|
||||
const [matches, setMatches] = useState<Product[]>([]);
|
||||
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<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -36,6 +61,11 @@ export function PredictiveSearch({ products }: { products: Product[] }) {
|
||||
{query && <button type="button" aria-label="Limpiar búsqueda" className="absolute right-4 top-1/2 -translate-y-1/2 rounded-full p-1 text-slate-400 hover:bg-slate-100" onClick={() => setQuery("")}><X className="size-4" /></button>}
|
||||
</div>
|
||||
</div>
|
||||
{loading && normalized.length >= 2 && matches.length === 0 && (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-4 text-center text-sm text-slate-500 shadow-2xl">
|
||||
Buscando…
|
||||
</div>
|
||||
)}
|
||||
{matches.length > 0 && (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl">
|
||||
{matches.map((product) => (
|
||||
|
||||
Reference in new issue
Block a user