import type { Metadata } from "next"; import { defineQuery } from "next-sanity"; import { sanity } from "@/lib/sanity"; import { ReleaseCard, type ReleaseCardData } from "@/components/release/ReleaseCard"; import { AnimatedText } from "@/components/AnimatedText"; import { SearchBar } from "@/components/SearchBar"; import { PaginationNav } from "@/components/PaginationNav"; import { CARD_GRID_CLASSES_4 } from "@/lib/constants"; import { PAGE_SIZE, clampInt, normalizeQuery, groqLikeParam } from "@/lib/listHelpers"; import type { SortOption } from "@/components/SortDropdown"; import type { FilterGroup } from "@/components/FilterDropdown"; export const revalidate = 86400; type SortMode = | "titleAsc" | "titleDesc" | "catalogNoAsc" | "catalogNoDesc" | "releaseDateAsc" | "releaseDateDesc"; const GENRE_OPTIONS = [ { value: "earlyMusic", label: "Early Music" }, { value: "baroque", label: "Baroque" }, { value: "classical", label: "Classical" }, { value: "romantic", label: "Romantic" }, { value: "contemporary", label: "Contemporary" }, { value: "worldMusic", label: "World Music" }, { value: "jazz", label: "Jazz" }, { value: "crossover", label: "Crossover" }, { value: "electronic", label: "Electronic" }, { value: "minimal", label: "Minimal" }, { value: "popRock", label: "Pop / Rock" }, ]; const INSTRUMENTATION_OPTIONS = [ { value: "solo", label: "Solo" }, { value: "chamber", label: "Chamber" }, { value: "ensemble", label: "Ensemble" }, { value: "orchestra", label: "Orchestral" }, { value: "vocalChoral", label: "Vocal / Choral" }, ]; const FILTER_GROUPS: FilterGroup[] = [ { label: "Genre", param: "genre", options: GENRE_OPTIONS }, { label: "Instrumentation", param: "instrumentation", options: INSTRUMENTATION_OPTIONS }, ]; const VALID_GENRES = new Set(GENRE_OPTIONS.map((o) => o.value)); const VALID_INSTRUMENTATIONS = new Set(INSTRUMENTATION_OPTIONS.map((o) => o.value)); function parseFilterParam(raw: string | undefined, valid: Set): string[] { if (!raw) return []; return raw.split(",").filter((v) => valid.has(v)); } const SORT_OPTIONS: SortOption[] = [ { value: "titleAsc", label: "Title", iconDirection: "asc" }, { value: "titleDesc", label: "Title", iconDirection: "desc" }, { value: "catalogNoAsc", label: "Cat. No.", iconDirection: "asc" }, { value: "catalogNoDesc", label: "Cat. No.", iconDirection: "desc" }, { value: "releaseDateAsc", label: "Release date", iconDirection: "asc" }, { value: "releaseDateDesc", label: "Release date", iconDirection: "desc" }, ]; function normalizeSort(s: string | undefined): SortMode { switch (s) { case "titleAsc": case "titleDesc": case "catalogNoAsc": case "catalogNoDesc": case "releaseDateAsc": case "releaseDateDesc": return s; default: return "releaseDateDesc"; } } const RELEASE_FILTER_CLAUSE = ` _type == "release" && ( $q == "" || name match $qPattern || albumArtist match $qPattern || catalogNo match $qPattern ) && (count($genres) == 0 || count(genre[@ in $genres]) > 0) && (count($instrumentations) == 0 || count(instrumentation[@ in $instrumentations]) > 0) `; const RELEASE_PROJECTION = `{ _id, name, albumArtist, catalogNo, releaseDate, "slug": slug.current, albumCover }`; const RELEASES_BY_TITLE_ASC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(lower(name) asc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_BY_TITLE_DESC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(lower(name) desc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_BY_CATALOG_NO_ASC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(lower(catalogNo) asc, lower(name) asc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_BY_CATALOG_NO_DESC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(lower(catalogNo) desc, lower(name) asc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_BY_DATE_ASC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(coalesce(releaseDate, "9999-12-31") asc, lower(name) asc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_BY_DATE_DESC_QUERY = defineQuery(` *[ ${RELEASE_FILTER_CLAUSE} ] | order(coalesce(releaseDate, "0000-01-01") desc, lower(name) asc) [$start...$end]${RELEASE_PROJECTION} `); const RELEASES_COUNT_QUERY = defineQuery(` count(*[ ${RELEASE_FILTER_CLAUSE} ]) `); export const metadata: Metadata = { title: "Releases", description: "Explore the full TRPTK catalogue. Filter by genre and instrumentation to find your next favourite recording.", }; export default async function ReleasesPage({ searchParams, }: { searchParams: Promise<{ page?: string; q?: string; sort?: string; genre?: string; instrumentation?: string; }>; }) { const sp = await searchParams; const q = normalizeQuery(sp.q); const sort = normalizeSort(sp.sort); const page = clampInt(sp.page, 1, 1, 9999); const genres = parseFilterParam(sp.genre, VALID_GENRES); const instrumentations = parseFilterParam(sp.instrumentation, VALID_INSTRUMENTATIONS); const start = (page - 1) * PAGE_SIZE; const end = start + PAGE_SIZE; const qPattern = q ? `${groqLikeParam(q)}*` : ""; const listQuery = sort === "titleDesc" ? RELEASES_BY_TITLE_DESC_QUERY : sort === "catalogNoAsc" ? RELEASES_BY_CATALOG_NO_ASC_QUERY : sort === "catalogNoDesc" ? RELEASES_BY_CATALOG_NO_DESC_QUERY : sort === "releaseDateAsc" ? RELEASES_BY_DATE_ASC_QUERY : sort === "releaseDateDesc" ? RELEASES_BY_DATE_DESC_QUERY : RELEASES_BY_TITLE_ASC_QUERY; const queryParams = { start, end, q, qPattern, genres, instrumentations }; const [releases, total] = await Promise.all([ sanity.fetch(listQuery, queryParams), sanity.fetch(RELEASES_COUNT_QUERY, queryParams), ]); const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const initialFilters: Record = {}; if (genres.length) initialFilters.genre = genres; if (instrumentations.length) initialFilters.instrumentation = instrumentations; const buildHref = (nextPage: number) => { const params = new URLSearchParams(); if (q) params.set("q", q); if (sort !== "releaseDateDesc") params.set("sort", sort); if (genres.length) params.set("genre", genres.join(",")); if (instrumentations.length) params.set("instrumentation", instrumentations.join(",")); if (nextPage > 1) params.set("page", String(nextPage)); const qs = params.toString(); return qs ? `/releases?${qs}` : "/releases"; }; return (

{total ? `${total} release(s)` : "No releases found."}

{releases.map((release) => ( ))}
{totalPages > 1 ? ( ) : null}
); }