180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { defineQuery } from "next-sanity";
|
|
import { sanity } from "@/lib/sanity";
|
|
import { ArtistCard } from "@/components/artist/ArtistCard";
|
|
import { formatYears, type ComposerCardData } from "@/components/artist/types";
|
|
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";
|
|
|
|
export const revalidate = 86400;
|
|
|
|
type SortMode = "name" | "sortKey" | "birthYearAsc" | "birthYearDesc";
|
|
|
|
const SORT_OPTIONS: SortOption<SortMode>[] = [
|
|
{ value: "name", label: "Sort by first name" },
|
|
{ value: "sortKey", label: "Sort by last name" },
|
|
{ value: "birthYearAsc", label: "Sort by birth year", iconDirection: "asc" },
|
|
{ value: "birthYearDesc", label: "Sort by birth year", iconDirection: "desc" },
|
|
];
|
|
|
|
function normalizeSort(s: string | undefined): SortMode {
|
|
switch (s) {
|
|
case "name":
|
|
case "sortKey":
|
|
case "birthYearAsc":
|
|
case "birthYearDesc":
|
|
return s;
|
|
default:
|
|
return "sortKey";
|
|
}
|
|
}
|
|
|
|
const COMPOSER_FILTER_CLAUSE = `
|
|
_type == "composer" &&
|
|
(
|
|
$q == "" ||
|
|
name match $qPattern ||
|
|
role match $qPattern
|
|
)
|
|
`;
|
|
|
|
const COMPOSER_PROJECTION = `{
|
|
_id,
|
|
name,
|
|
birthYear,
|
|
deathYear,
|
|
"slug": slug.current,
|
|
image
|
|
}`;
|
|
|
|
const COMPOSERS_BY_NAME_QUERY = defineQuery(`
|
|
*[
|
|
${COMPOSER_FILTER_CLAUSE}
|
|
]
|
|
| order(lower(name) asc)
|
|
[$start...$end]${COMPOSER_PROJECTION}
|
|
`);
|
|
|
|
const COMPOSERS_BY_SORT_KEY_QUERY = defineQuery(`
|
|
*[
|
|
${COMPOSER_FILTER_CLAUSE}
|
|
]
|
|
| order(lower(coalesce(sortKey, name)) asc, lower(name) asc)
|
|
[$start...$end]${COMPOSER_PROJECTION}
|
|
`);
|
|
|
|
const COMPOSERS_BY_BIRTH_YEAR_ASC_QUERY = defineQuery(`
|
|
*[
|
|
${COMPOSER_FILTER_CLAUSE}
|
|
]
|
|
| order(coalesce(birthYear, 999999) asc, lower(name) asc)
|
|
[$start...$end]${COMPOSER_PROJECTION}
|
|
`);
|
|
|
|
const COMPOSERS_BY_BIRTH_YEAR_DESC_QUERY = defineQuery(`
|
|
*[
|
|
${COMPOSER_FILTER_CLAUSE}
|
|
]
|
|
| order(coalesce(birthYear, -999999) desc, lower(name) asc)
|
|
[$start...$end]${COMPOSER_PROJECTION}
|
|
`);
|
|
|
|
const COMPOSERS_COUNT_QUERY = defineQuery(`
|
|
count(*[
|
|
${COMPOSER_FILTER_CLAUSE}
|
|
])
|
|
`);
|
|
|
|
export const metadata: Metadata = {
|
|
title: "Composers",
|
|
description:
|
|
"Discover composers featured on TRPTK recordings, from early music to contemporary works.",
|
|
};
|
|
|
|
export default async function ComposersPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ page?: string; q?: string; sort?: string }>;
|
|
}) {
|
|
const sp = await searchParams;
|
|
|
|
const q = normalizeQuery(sp.q);
|
|
const sort = normalizeSort(sp.sort);
|
|
const page = clampInt(sp.page, 1, 1, 9999);
|
|
|
|
const start = (page - 1) * PAGE_SIZE;
|
|
const end = start + PAGE_SIZE;
|
|
|
|
const qPattern = q ? `${groqLikeParam(q)}*` : "";
|
|
|
|
const listQuery =
|
|
sort === "sortKey"
|
|
? COMPOSERS_BY_SORT_KEY_QUERY
|
|
: sort === "birthYearAsc"
|
|
? COMPOSERS_BY_BIRTH_YEAR_ASC_QUERY
|
|
: sort === "birthYearDesc"
|
|
? COMPOSERS_BY_BIRTH_YEAR_DESC_QUERY
|
|
: COMPOSERS_BY_NAME_QUERY;
|
|
|
|
const [composers, total] = await Promise.all([
|
|
sanity.fetch<ComposerCardData[]>(listQuery, { start, end, q, qPattern }),
|
|
sanity.fetch<number>(COMPOSERS_COUNT_QUERY, { q, qPattern }),
|
|
]);
|
|
|
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
const safePage = Math.min(page, totalPages);
|
|
|
|
const buildHref = (nextPage: number) => {
|
|
const params = new URLSearchParams();
|
|
if (q) params.set("q", q);
|
|
if (sort !== "sortKey") params.set("sort", sort);
|
|
if (nextPage > 1) params.set("page", String(nextPage));
|
|
const qs = params.toString();
|
|
return qs ? `/composers?${qs}` : "/composers";
|
|
};
|
|
|
|
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="Composers" as="h1" className="font-argesta text-3xl" />
|
|
<p className="mt-2 text-base text-lightsec dark:text-darksec">
|
|
{total ? `${total} composer(s)` : "No composers found."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mb-12">
|
|
<SearchBar
|
|
initialQuery={q}
|
|
initialSort={sort}
|
|
initialPage={safePage}
|
|
defaultSort="sortKey"
|
|
placeholder="Search composers…"
|
|
sortOptions={SORT_OPTIONS}
|
|
sortAriaLabel="Sort composers"
|
|
sortMenuClassName="absolute right-0 z-20 mt-4 min-w-47 overflow-hidden rounded-2xl bg-lightbg shadow-lg ring-1 ring-lightline transition-colors duration-300 ease-in-out hover:ring-lightline-hover dark:bg-darkbg dark:ring-darkline dark:hover:ring-darkline-hover"
|
|
/>
|
|
</div>
|
|
|
|
<section className={CARD_GRID_CLASSES_4}>
|
|
{composers.map((composer) => (
|
|
<ArtistCard
|
|
key={composer._id}
|
|
name={composer.name}
|
|
subtitle={formatYears(composer.birthYear, composer.deathYear)}
|
|
image={composer.image}
|
|
href={composer.slug ? `/composer/${composer.slug}` : "/composers/"}
|
|
label="Composer"
|
|
/>
|
|
))}
|
|
</section>
|
|
|
|
{totalPages > 1 ? (
|
|
<PaginationNav page={safePage} totalPages={totalPages} buildHref={buildHref} />
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|