diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..a9a56dc --- /dev/null +++ b/app/api/search/route.ts @@ -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 }); +} diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index d24292c..0640568 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -7,6 +7,7 @@ import { usePathname } from "next/navigation"; import { AnimatePresence, motion } from "motion/react"; import { useCartCount } from "../lib/cart"; import { useWishlist } from "../lib/useWishlist"; +import { SearchButton } from "./SearchOverlay"; import { AUTH_CHANGED_EVENT } from "../lib/auth"; import { NewsletterModal } from "./NewsletterModal"; import { useCartFly } from "./CartFly"; @@ -544,6 +545,7 @@ export function Navbar({ singleActiveProduct, wishlistEnabled }: { singleActiveP built-in padding read as too much space on mobile, where these two icons are the only always-visible controls. */}
Suche…
} + {!loading && query.trim().length >= 2 && results.length === 0 && ( +Keine Treffer für „{query}“.
+ )} + + {products.length > 0 && ( +Produkte
+ {products.map((r) => ( +Blog
+ {posts.map((r) => ( +