import { NextRequest, NextResponse } from "next/server"; import { sanity } from "@/lib/sanity"; import { urlFor } from "@/lib/sanityImage"; import { foldDiacritics } from "@/lib/diacritics"; import type { SanityImageSource } from "@sanity/image-url"; const SEARCH_QUERY = `{ "releases": *[_type == "release" && defined(slug.current)]{ name, albumArtist, "slug": slug.current, albumCover }, "artists": *[_type == "artist" && defined(slug.current)]{ name, role, "slug": slug.current, image }, "composers": *[_type == "composer" && defined(slug.current)]{ name, birthYear, deathYear, "slug": slug.current, image }, "works": *[_type == "work" && defined(slug.current)]{ title, "composerName": composer->name, "composerSlug": composer->slug.current, "composerImage": composer->image, "arrangerName": arranger->name, "slug": slug.current }, "blog": *[_type == "blog" && defined(slug.current)]{ title, category, "slug": slug.current, featuredImage } }`; type RawSearchData = { releases: Array<{ name: string; albumArtist?: string; slug: string; albumCover?: SanityImageSource }>; artists: Array<{ name: string; role?: string; slug: string; image?: SanityImageSource }>; composers: Array<{ name: string; birthYear?: number; deathYear?: number; slug: string; image?: SanityImageSource }>; works: Array<{ title: string; composerName?: string; arrangerName?: string; composerSlug?: string; composerImage?: SanityImageSource; slug: string }>; blog: Array<{ title: string; category?: string; slug: string; featuredImage?: SanityImageSource }>; }; let cachedData: RawSearchData | null = null; let cacheTime = 0; const CACHE_TTL = 86_400_000; // 24 hours in ms async function getData(): Promise { const now = Date.now(); if (cachedData && now - cacheTime < CACHE_TTL) return cachedData; cachedData = await sanity.fetch(SEARCH_QUERY); cacheTime = now; return cachedData; } const MAX_PER_TYPE = 5; const THUMB_SIZE = 96; function imageUrl(source?: SanityImageSource): string | undefined { if (!source) return undefined; try { return urlFor(source).width(THUMB_SIZE).height(THUMB_SIZE).url(); } catch { return undefined; } } function formatYears(birthYear?: number, deathYear?: number): string | undefined { if (birthYear && deathYear) return `${birthYear}\u2013${deathYear}`; if (birthYear) return `${birthYear}`; return undefined; } function matches(text: string | undefined, folded: string): boolean { if (!text) return false; return foldDiacritics(text.toLowerCase()).includes(folded); } export async function GET(request: NextRequest) { const q = request.nextUrl.searchParams.get("q")?.trim(); if (!q || q.length < 2) { return NextResponse.json({ releases: [], artists: [], composers: [], works: [], blog: [], }); } const data = await getData(); const folded = foldDiacritics(q.toLowerCase()); const releases = data.releases .filter((r) => matches(r.name, folded) || matches(r.albumArtist, folded)) .slice(0, MAX_PER_TYPE) .map((r) => ({ name: r.name, albumArtist: r.albumArtist, slug: r.slug, imageUrl: imageUrl(r.albumCover) })); const artists = data.artists .filter((a) => matches(a.name, folded)) .slice(0, MAX_PER_TYPE) .map((a) => ({ name: a.name, role: a.role, slug: a.slug, imageUrl: imageUrl(a.image) })); const composers = data.composers .filter((c) => matches(c.name, folded)) .slice(0, MAX_PER_TYPE) .map((c) => ({ name: c.name, years: formatYears(c.birthYear, c.deathYear), slug: c.slug, imageUrl: imageUrl(c.image) })); const works = data.works .filter((w) => matches(w.title, folded) || matches(w.composerName, folded)) .slice(0, MAX_PER_TYPE) .map((w) => ({ title: w.title, composerName: w.composerName, arrangerName: w.arrangerName, slug: w.slug, imageUrl: imageUrl(w.composerImage) })); const blog = data.blog .filter((b) => matches(b.title, folded)) .slice(0, MAX_PER_TYPE) .map((b) => ({ title: b.title, category: b.category, slug: b.slug, imageUrl: imageUrl(b.featuredImage) })); return NextResponse.json({ releases, artists, composers, works, blog }); }