259 lines
7.7 KiB
TypeScript
259 lines
7.7 KiB
TypeScript
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>): string[] {
|
|
if (!raw) return [];
|
|
return raw.split(",").filter((v) => valid.has(v));
|
|
}
|
|
|
|
const SORT_OPTIONS: SortOption<SortMode>[] = [
|
|
{ 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<ReleaseCardData[]>(listQuery, queryParams),
|
|
sanity.fetch<number>(RELEASES_COUNT_QUERY, queryParams),
|
|
]);
|
|
|
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
const safePage = Math.min(page, totalPages);
|
|
|
|
const initialFilters: Record<string, string[]> = {};
|
|
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 (
|
|
<main className="mx-auto my-auto max-w-300 px-6 py-12 font-silka md:px-8 md:py-16 2xl:max-w-400">
|
|
<div className="mb-10">
|
|
<AnimatedText text="Releases" as="h1" className="font-argesta text-3xl" />
|
|
<p className="mt-2 text-base text-lightsec dark:text-darksec">
|
|
{total ? `${total} release(s)` : "No releases found."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-12">
|
|
<SearchBar
|
|
initialQuery={q}
|
|
initialSort={sort}
|
|
initialPage={safePage}
|
|
defaultSort="releaseDateDesc"
|
|
placeholder="Search releases…"
|
|
sortOptions={SORT_OPTIONS}
|
|
sortAriaLabel="Sort releases"
|
|
filterGroups={FILTER_GROUPS}
|
|
initialFilters={initialFilters}
|
|
/>
|
|
</div>
|
|
|
|
<section className={CARD_GRID_CLASSES_4}>
|
|
{releases.map((release) => (
|
|
<ReleaseCard key={release._id} release={release} />
|
|
))}
|
|
</section>
|
|
|
|
{totalPages > 1 ? (
|
|
<PaginationNav page={safePage} totalPages={totalPages} buildHref={buildHref} />
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|