"use client"; import { useCallback, useMemo, useState } from "react"; import type { SanityImageSource } from "@sanity/image-url"; import { IoCloseOutline } from "react-icons/io5"; import { HiOutlineChevronDoubleLeft, HiOutlineChevronLeft, HiOutlineChevronRight, HiOutlineChevronDoubleRight, } from "react-icons/hi2"; import { ReleaseCard } from "@/components/release/ReleaseCard"; import { SortDropdown, type SortOption } from "@/components/SortDropdown"; import { FilterDropdown, type FilterGroup } from "@/components/FilterDropdown"; import { IconButton } from "@/components/IconButton"; import { CARD_GRID_CLASSES_3 } from "@/lib/constants"; export type ArtistRelease = { _id?: string; name?: string; albumArtist?: string; catalogNo?: string; releaseDate?: string; slug?: string; albumCover?: SanityImageSource; genre?: string[]; instrumentation?: string[]; }; const PAGE_SIZE = 12; type SortMode = | "releaseDateDesc" | "releaseDateAsc" | "titleAsc" | "titleDesc" | "catalogNoAsc" | "catalogNoDesc"; 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" }, ]; 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 }, ]; function compareStr(a?: string, b?: string): number { return (a ?? "").localeCompare(b ?? "", undefined, { sensitivity: "base" }); } function compareDate(a?: string, b?: string, asc = true): number { const da = a ?? (asc ? "9999-12-31" : "0000-01-01"); const db = b ?? (asc ? "9999-12-31" : "0000-01-01"); return da < db ? -1 : da > db ? 1 : 0; } function sortReleases(list: ArtistRelease[], mode: SortMode): ArtistRelease[] { return [...list].sort((a, b) => { switch (mode) { case "titleAsc": return compareStr(a.name, b.name); case "titleDesc": return compareStr(b.name, a.name); case "catalogNoAsc": return compareStr(a.catalogNo, b.catalogNo) || compareStr(a.name, b.name); case "catalogNoDesc": return compareStr(b.catalogNo, a.catalogNo) || compareStr(a.name, b.name); case "releaseDateAsc": return compareDate(a.releaseDate, b.releaseDate, true) || compareStr(a.name, b.name); case "releaseDateDesc": return compareDate(b.releaseDate, a.releaseDate, false) || compareStr(a.name, b.name); } }); } function matchesSearch(r: ArtistRelease, lower: string): boolean { return ( (r.name?.toLowerCase().includes(lower) ?? false) || (r.albumArtist?.toLowerCase().includes(lower) ?? false) || (r.catalogNo?.toLowerCase().includes(lower) ?? false) ); } function matchesFilters(r: ArtistRelease, filters: Record): boolean { const genres = filters.genre ?? []; const instrumentations = filters.instrumentation ?? []; if (genres.length > 0 && !genres.some((g) => r.genre?.includes(g))) return false; if (instrumentations.length > 0 && !instrumentations.some((i) => r.instrumentation?.includes(i))) return false; return true; } type Props = { releases: ArtistRelease[]; }; export function ArtistReleasesTab({ releases }: Props) { const [q, setQ] = useState(""); const [sort, setSort] = useState("releaseDateDesc"); const [filters, setFilters] = useState>({}); const [page, setPage] = useState(1); const handleFilterChange = useCallback((param: string, selected: string[]) => { setFilters((prev) => ({ ...prev, [param]: selected })); setPage(1); }, []); const handleSortChange = useCallback((value: SortMode) => { setSort(value); setPage(1); }, []); const handleSearchChange = useCallback((e: React.ChangeEvent) => { setQ(e.target.value); setPage(1); }, []); const filtered = useMemo(() => { const lower = q.trim().toLowerCase(); let result = releases; if (lower) result = result.filter((r) => matchesSearch(r, lower)); result = result.filter((r) => matchesFilters(r, filters)); return sortReleases(result, sort); }, [releases, q, sort, filters]); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const start = (safePage - 1) * PAGE_SIZE; const paginated = filtered.slice(start, start + PAGE_SIZE); return (

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

{q ? ( { setQ(""); setPage(1); }} className="text-lg" aria-label="Clear search" > ) : null}
{paginated.length > 0 ? (
{paginated.map((r) => ( ))}
) : null} {totalPages > 1 ? ( ) : null}
); } function Pagination({ page, totalPages, onPageChange, }: { page: number; totalPages: number; onPageChange: (p: number) => void; }) { const hasPrev = page > 1; const hasNext = page < totalPages; return ( ); }