"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { IoCloseOutline } from "react-icons/io5"; import { HiOutlineChevronDoubleLeft, HiOutlineChevronLeft, HiOutlineChevronRight, HiOutlineChevronDoubleRight, } from "react-icons/hi2"; 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"; import { DownloadCard, UpcomingCard, type DownloadCardData, type UpcomingCardData, } from "./DownloadCard"; const PAGE_SIZE = 12; // ── Sort ──────────────────────────────────────────────────────────── type SortMode = | "titleAsc" | "titleDesc" | "catalogNoAsc" | "catalogNoDesc" | "releaseDateAsc" | "releaseDateDesc"; 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" }, ]; // ── Filters ───────────────────────────────────────────────────────── 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 }, ]; // ── Helpers ───────────────────────────────────────────────────────── type Sortable = { product_title: string; catalogNo?: string | null; releaseDate?: string | null; }; function compareStr(a?: string | null, b?: string | null): number { return (a ?? "").localeCompare(b ?? "", undefined, { sensitivity: "base" }); } function compareDate(a?: string | null, b?: string | null, 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 sortItems(list: T[], mode: SortMode): T[] { return [...list].sort((a, b) => { switch (mode) { case "titleAsc": return compareStr(a.product_title, b.product_title); case "titleDesc": return compareStr(b.product_title, a.product_title); case "catalogNoAsc": return compareStr(a.catalogNo, b.catalogNo) || compareStr(a.product_title, b.product_title); case "catalogNoDesc": return compareStr(b.catalogNo, a.catalogNo) || compareStr(a.product_title, b.product_title); case "releaseDateAsc": return ( compareDate(a.releaseDate, b.releaseDate, true) || compareStr(a.product_title, b.product_title) ); case "releaseDateDesc": return ( compareDate(b.releaseDate, a.releaseDate, false) || compareStr(a.product_title, b.product_title) ); } }); } type Searchable = { product_title: string; albumArtist?: string | null; catalogNo?: string | null; }; function matchesSearch(item: Searchable, lower: string): boolean { return ( item.product_title.toLowerCase().includes(lower) || (item.albumArtist?.toLowerCase().includes(lower) ?? false) || (item.catalogNo?.toLowerCase().includes(lower) ?? false) ); } type Filterable = { genre?: string[]; instrumentation?: string[]; }; function matchesFilters(item: Filterable, filters: Record): boolean { const genres = filters.genre ?? []; const instrumentations = filters.instrumentation ?? []; if (genres.length > 0 && !genres.some((g) => item.genre?.includes(g))) return false; if ( instrumentations.length > 0 && !instrumentations.some((i) => item.instrumentation?.includes(i)) ) return false; return true; } // ── Component ─────────────────────────────────────────────────────── export function DownloadsTab() { const [downloads, setDownloads] = useState([]); const [upcoming, setUpcoming] = useState([]); const [loading, setLoading] = useState(true); const [q, setQ] = useState(""); const [sort, setSort] = useState("releaseDateDesc"); const [filters, setFilters] = useState>({}); const [dlPage, setDlPage] = useState(1); const [upPage, setUpPage] = useState(1); useEffect(() => { async function fetchDownloads() { try { const res = await fetch("/api/account/downloads"); if (res.ok) { const data = await res.json(); if (data.downloads?.length) setDownloads(data.downloads); if (data.upcoming?.length) setUpcoming(data.upcoming); } } catch { // Silently fail } finally { setLoading(false); } } fetchDownloads(); }, []); const resetPages = useCallback(() => { setDlPage(1); setUpPage(1); }, []); const handleFilterChange = useCallback((param: string, selected: string[]) => { setFilters((prev) => ({ ...prev, [param]: selected })); setDlPage(1); setUpPage(1); }, []); const handleSortChange = useCallback( (value: SortMode) => { setSort(value); resetPages(); }, [resetPages], ); const handleSearchChange = useCallback( (e: React.ChangeEvent) => { setQ(e.target.value); resetPages(); }, [resetPages], ); const clearSearch = useCallback(() => { setQ(""); resetPages(); }, [resetPages]); const filteredDownloads = useMemo(() => { const lower = q.trim().toLowerCase(); let result = downloads; if (lower) result = result.filter((d) => matchesSearch(d, lower)); result = result.filter((d) => matchesFilters(d, filters)); return sortItems(result, sort); }, [downloads, q, sort, filters]); const filteredUpcoming = useMemo(() => { const lower = q.trim().toLowerCase(); let result = upcoming; if (lower) result = result.filter((u) => matchesSearch(u, lower)); result = result.filter((u) => matchesFilters(u, filters)); return sortItems(result, sort); }, [upcoming, q, sort, filters]); const dlTotalPages = Math.max(1, Math.ceil(filteredDownloads.length / PAGE_SIZE)); const dlSafePage = Math.min(dlPage, dlTotalPages); const dlStart = (dlSafePage - 1) * PAGE_SIZE; const paginatedDownloads = filteredDownloads.slice(dlStart, dlStart + PAGE_SIZE); const upTotalPages = Math.max(1, Math.ceil(filteredUpcoming.length / PAGE_SIZE)); const upSafePage = Math.min(upPage, upTotalPages); const upStart = (upSafePage - 1) * PAGE_SIZE; const paginatedUpcoming = filteredUpcoming.slice(upStart, upStart + PAGE_SIZE); if (loading) { return

Loading downloads…

; } if (downloads.length === 0 && upcoming.length === 0) { return (

No downloads available. Your digital purchases will appear here.

); } const totalFiltered = filteredDownloads.length + filteredUpcoming.length; return (
{/* Count */}

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

{/* Search + Filter + Sort bar */}
{q ? ( ) : null}
{/* Available Downloads */} {paginatedDownloads.length > 0 && (
{paginatedDownloads.map((dl) => ( ))}
{dlTotalPages > 1 && ( )}
)} {/* Upcoming Releases */} {paginatedUpcoming.length > 0 && (
0 ? "mt-16" : ""}>

Upcoming Releases

You'll receive download links by email when these become available.

{paginatedUpcoming.map((up) => ( ))}
{upTotalPages > 1 && ( )}
)} {/* No results after filtering */} {totalFiltered === 0 && (q || Object.values(filters).some((v) => v.length > 0)) && (

No releases found.

)}
); } // ── Pagination ────────────────────────────────────────────────────── function Pagination({ page, totalPages, onPageChange, }: { page: number; totalPages: number; onPageChange: (p: number) => void; }) { const hasPrev = page > 1; const hasNext = page < totalPages; return ( ); }