trptk/app/api/search/route.ts
2026-02-24 17:14:07 +01:00

109 lines
4.1 KiB
TypeScript

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<RawSearchData> {
const now = Date.now();
if (cachedData && now - cacheTime < CACHE_TTL) return cachedData;
cachedData = await sanity.fetch<RawSearchData>(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 });
}