109 lines
4.1 KiB
TypeScript
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 });
|
|
}
|