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
-
+
+ {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