diff --git a/workloads/ecommerce/app/api/search/route.ts b/workloads/ecommerce/app/api/search/route.ts new file mode 100644 index 0000000..c55a0e8 --- /dev/null +++ b/workloads/ecommerce/app/api/search/route.ts @@ -0,0 +1,43 @@ +import { searchWithFacets } from "@/lib/headless/client"; +import type { ProductFilters } from "@/lib/headless/types"; + +// PredictiveSearch es un componente cliente y no puede pegarle directo +// al provider server-side (MEDUSA_BACKEND_URL es un nombre DNS interno +// del cluster, no resuelve desde el navegador) -- esta ruta es el +// puente same-origin. +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const filters: ProductFilters = { + query: searchParams.get("q") ?? undefined, + category: searchParams.get("category") ?? undefined, + collection: searchParams.get("collection") ?? undefined, + brand: searchParams.get("brand") ?? undefined, + ageRange: searchParams.get("ageRange") ?? undefined, + }; + + const minPrice = searchParams.get("minPrice"); + if (minPrice) filters.minPrice = Number(minPrice); + const maxPrice = searchParams.get("maxPrice"); + if (maxPrice) filters.maxPrice = Number(maxPrice); + + try { + const result = await searchWithFacets(filters); + // provider actual no implementa searchWithFacets (mock/shopify) -- + // degradar a vacío, no a 500; el componente cliente cae de nuevo a + // su propio filtrado si esta ruta no trae nada útil. + return Response.json( + result ?? { products: [], facetDistribution: {}, count: 0 }, + ); + } catch (error) { + return Response.json( + { + products: [], + facetDistribution: {}, + count: 0, + error: (error as Error).message, + }, + { status: 502 }, + ); + } +} diff --git a/workloads/ecommerce/components/layout/navbar.tsx b/workloads/ecommerce/components/layout/navbar.tsx index 1ae0ecc..f8d9335 100644 --- a/workloads/ecommerce/components/layout/navbar.tsx +++ b/workloads/ecommerce/components/layout/navbar.tsx @@ -56,7 +56,7 @@ export function Navbar({ products, categories }: { products: Product[]; categori ARI Shopping - +
}
+ {loading && normalized.length >= 2 && matches.length === 0 && ( +
+ Buscando… +
+ )} {matches.length > 0 && (
{matches.map((product) => ( diff --git a/workloads/ecommerce/lib/headless/client.ts b/workloads/ecommerce/lib/headless/client.ts index fe291da..f60db80 100644 --- a/workloads/ecommerce/lib/headless/client.ts +++ b/workloads/ecommerce/lib/headless/client.ts @@ -26,3 +26,7 @@ export async function searchProducts(query: string, limit?: number) { export async function getCategories() { return provider().getCategories(); } + +export async function searchWithFacets(filters?: ProductFilters) { + return provider().searchWithFacets?.(filters); +} diff --git a/workloads/ecommerce/lib/headless/providers/medusa.ts b/workloads/ecommerce/lib/headless/providers/medusa.ts index 0b1f1b0..211f60b 100644 --- a/workloads/ecommerce/lib/headless/providers/medusa.ts +++ b/workloads/ecommerce/lib/headless/providers/medusa.ts @@ -1,6 +1,8 @@ import type { CatalogProvider, Category, + FacetDistribution, + FacetedSearchResult, Product, ProductFilters, ProductVariant, @@ -385,6 +387,56 @@ async function fetchCategories(): Promise { })); } +type MedusaSearchResponse = { + products: MedusaProduct[]; + facetDistribution?: FacetDistribution; + count?: number; +}; + +// Pega contra /store/search (ADR 0006 Etapa 2) en vez de /store/products +// -- Meilisearch resuelve texto tolerante a errores de tipeo y facetas +// reales, la ruta del backend re-hidrata precio/inventario vía +// query.graph antes de responder, así que el shape de MedusaProduct es +// exactamente el mismo que ya mapea mapProduct(). +async function fetchWithFacets( + filters?: ProductFilters, + limit = 24, +): Promise { + const params = new URLSearchParams({ + limit: String(limit), + region_id: regionId(), + }); + + if (filters?.query) params.set("q", filters.query); + if (filters?.category && filters.category !== "Todos") { + params.set("category", filters.category); + } + if (filters?.collection) params.set("collection", filters.collection); + if (filters?.brand && filters.brand !== "Todas") { + params.set("brand", filters.brand); + } + if (filters?.ageRange && filters.ageRange !== "Todas") { + params.set("ageRange", filters.ageRange); + } + if (typeof filters?.minPrice === "number") { + params.set("minPrice", String(filters.minPrice)); + } + if (typeof filters?.maxPrice === "number") { + params.set("maxPrice", String(filters.maxPrice)); + } + + const response = await medusaFetch( + "/store/search", + params, + ); + + return { + products: response.products.map(mapProduct), + facetDistribution: response.facetDistribution ?? {}, + count: response.count ?? response.products.length, + }; +} + export const medusaCatalogProvider: CatalogProvider = { async getProducts(filters) { return fetchProducts(filters); @@ -416,6 +468,11 @@ export const medusaCatalogProvider: CatalogProvider = { }, async searchProducts(query, limit = 8) { - return fetchProducts({ query }, limit); + const { products } = await fetchWithFacets({ query }, limit); + return products; + }, + + async searchWithFacets(filters) { + return fetchWithFacets(filters); }, }; diff --git a/workloads/ecommerce/lib/headless/types.ts b/workloads/ecommerce/lib/headless/types.ts index 5f9cced..f92783b 100644 --- a/workloads/ecommerce/lib/headless/types.ts +++ b/workloads/ecommerce/lib/headless/types.ts @@ -47,9 +47,23 @@ export interface Category { parentId: string | null; } +// facet -> valor -> cuantos productos del resultado filtrado tienen ese +// valor (ej. { brand: { Marvel: 5, Disney: 10 } }). +export type FacetDistribution = Record>; + +export interface FacetedSearchResult { + products: Product[]; + facetDistribution: FacetDistribution; + count: number; +} + export interface CatalogProvider { getProducts(filters?: ProductFilters): Promise; getProductBySlug(slug: string): Promise; searchProducts(query: string, limit?: number): Promise; getCategories(): Promise; + // Opcional a propósito: mock.ts y shopify.ts no lo implementan y + // siguen compilando igual -- los llamadores caen de nuevo al + // filtrado/conteo 100% client-side que ya existe cuando no está. + searchWithFacets?(filters?: ProductFilters): Promise; } diff --git a/workloads/ecommerce/patches/frontend-medusa-env.yaml b/workloads/ecommerce/patches/frontend-medusa-env.yaml deleted file mode 100644 index 45c9810..0000000 --- a/workloads/ecommerce/patches/frontend-medusa-env.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: frontend-deploy -spec: - template: - spec: - containers: - - name: web - env: - - name: HEADLESS_PROVIDER - value: medusa - - - name: MEDUSA_BACKEND_URL - value: http://medusa-svc:9000 - - - name: MEDUSA_PUBLISHABLE_KEY - valueFrom: - secretKeyRef: - name: commerce-storefront - key: MEDUSA_PUBLISHABLE_KEY