Add instant-search overlay (products + blog posts)
Separate commit on purpose so this can be reverted independently if needed. Search icon in the Navbar (hidden below sm:, same reasoning as WishlistLink — Account+Cart are the only always-visible icons on true mobile) opens a debounced (250ms) overlay searching both collections at once via a new /api/search route. Plain Payload `where[...][contains]` queries (Postgres ILIKE), not a real search index (Meilisearch/Algolia) — matches the catalog's current small size, see [[project-ecommerce-sota-gaps]]'s own "search becomes necessary past ~20 products" note. Worth upgrading later without touching the overlay component itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
|
||||
const TENANT_SLUG = "einfach-produktiv";
|
||||
|
||||
export type SearchResult = {
|
||||
type: "product" | "post";
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
thumbnail: string | null;
|
||||
};
|
||||
|
||||
// Lightweight instant search — plain `where[...][contains]` queries
|
||||
// against Payload (Postgres ILIKE under the hood) rather than a real
|
||||
// search index (Meilisearch/Algolia). Fine at this catalog size (a
|
||||
// handful of products + blog posts, see [[project-ecommerce-sota-gaps]]'s
|
||||
// own "search becomes necessary past ~20 products" note) — worth
|
||||
// upgrading only once the catalog actually grows into that range, this
|
||||
// route can be swapped out later without touching the frontend overlay.
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const q = searchParams.get("q")?.trim() ?? "";
|
||||
if (q.length < 2) return NextResponse.json({ results: [] });
|
||||
|
||||
const productParams = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[active][equals]": "true",
|
||||
"where[name][contains]": q,
|
||||
depth: "1",
|
||||
limit: "6",
|
||||
});
|
||||
const postParams = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[status][equals]": "published",
|
||||
"where[title][contains]": q,
|
||||
depth: "1",
|
||||
limit: "6",
|
||||
});
|
||||
|
||||
const [productsRes, postsRes] = await Promise.all([
|
||||
fetch(`${PAYLOAD_URL}/api/products?${productParams}`, { next: { revalidate: 30 } }),
|
||||
fetch(`${PAYLOAD_URL}/api/posts?${postParams}`, { next: { revalidate: 30 } }),
|
||||
]);
|
||||
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
if (productsRes.ok) {
|
||||
const data: { docs?: { id: number; slug: string; name: string; detailHref: string | null; image: { url: string } | number | null }[] } =
|
||||
await productsRes.json();
|
||||
for (const doc of data.docs ?? []) {
|
||||
results.push({
|
||||
type: "product",
|
||||
id: `product-${doc.id}`,
|
||||
title: doc.name,
|
||||
href: doc.detailHref || "/shop",
|
||||
thumbnail: typeof doc.image === "object" && doc.image ? doc.image.url : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (postsRes.ok) {
|
||||
const data: { docs?: { id: number; slug: string; title: string; thumbnail: { url: string } | number | null }[] } = await postsRes.json();
|
||||
for (const doc of data.docs ?? []) {
|
||||
results.push({
|
||||
type: "post",
|
||||
id: `post-${doc.id}`,
|
||||
title: doc.title,
|
||||
href: `/blog/${doc.slug}`,
|
||||
thumbnail: typeof doc.thumbnail === "object" && doc.thumbnail ? doc.thumbnail.url : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ results });
|
||||
}
|
||||
Reference in New Issue
Block a user