"use client"; import { useState, useMemo, useRef, useEffect } from "react"; import Link from "next/link"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { Search, ChevronLeft, ChevronRight, ArrowUpDown, ArrowDownAZ, ArrowUpAZ, Hash, Check, } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type YearGroup = { year: number; count: number }; export type RecipientGroup = { name: string; count: number }; export type LocationGroup = { name: string; count: number }; type SortMode = "count" | "asc" | "desc"; // --------------------------------------------------------------------------- // Diacritic-aware normalize (matches books-grid pattern) // --------------------------------------------------------------------------- function normalize(text: string): string { return text .toLowerCase() .replace(/ṛ/g, "r") .replace(/ṝ/g, "r") .replace(/ṣ/g, "s") .replace(/ś/g, "s") .replace(/ṭ/g, "t") .replace(/ḍ/g, "d") .replace(/ṇ/g, "n") .replace(/ñ/g, "n") .replace(/ṅ/g, "n") .replace(/ḥ/g, "h") .replace(/ṁ/g, "m") .replace(/ṃ/g, "m") .replace(/ḷ/g, "l") .replace(/ā/g, "a") .replace(/ī/g, "i") .replace(/ū/g, "u") .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); } // --------------------------------------------------------------------------- // Sort Popover // --------------------------------------------------------------------------- const SORT_OPTIONS: { key: SortMode; label: string; icon: typeof Hash }[] = [ { key: "count", label: "Most letters first", icon: Hash }, { key: "asc", label: "A → Z", icon: ArrowDownAZ }, { key: "desc", label: "Z → A", icon: ArrowUpAZ }, ]; const SORT_OPTIONS_YEAR: { key: SortMode; label: string; icon: typeof Hash }[] = [ { key: "count", label: "Most letters first", icon: Hash }, { key: "asc", label: "Oldest first", icon: ArrowDownAZ }, { key: "desc", label: "Newest first", icon: ArrowUpAZ }, ]; function SortPopover({ sort, onSortChange, isYear, }: { sort: SortMode; onSortChange: (s: SortMode) => void; isYear?: boolean; }) { const [open, setOpen] = useState(false); const ref = useRef(null); // Close on outside click useEffect(() => { if (!open) return; function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [open]); const options = isYear ? SORT_OPTIONS_YEAR : SORT_OPTIONS; return (
{open && (
{options.map((opt) => { const Icon = opt.icon; const isActive = sort === opt.key; return ( ); })}
)}
); } // --------------------------------------------------------------------------- // Pagination // --------------------------------------------------------------------------- const PAGE_SIZE_RECIPIENTS = 50; const PAGE_SIZE_LOCATIONS = 60; const PAGE_SIZE_YEARS = 100; function Pagination({ page, totalPages, onPageChange, }: { page: number; totalPages: number; onPageChange: (p: number) => void; }) { if (totalPages <= 1) return null; return (
Page {page} of {totalPages}
); } // --------------------------------------------------------------------------- // Sorting helpers // --------------------------------------------------------------------------- function sortYears(data: YearGroup[], mode: SortMode): YearGroup[] { const sorted = [...data]; switch (mode) { case "count": return sorted.sort((a, b) => b.count - a.count); case "asc": return sorted.sort((a, b) => a.year - b.year); case "desc": return sorted.sort((a, b) => b.year - a.year); } } function sortRecipients( data: RecipientGroup[], mode: SortMode, ): RecipientGroup[] { const sorted = [...data]; switch (mode) { case "count": return sorted.sort((a, b) => b.count - a.count); case "asc": return sorted.sort((a, b) => a.name.localeCompare(b.name)); case "desc": return sorted.sort((a, b) => b.name.localeCompare(a.name)); } } function sortLocations(data: LocationGroup[], mode: SortMode): LocationGroup[] { const sorted = [...data]; switch (mode) { case "count": return sorted.sort((a, b) => b.count - a.count); case "asc": return sorted.sort((a, b) => a.name.localeCompare(b.name)); case "desc": return sorted.sort((a, b) => b.name.localeCompare(a.name)); } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function LettersBrowser({ currentView, years, recipients, locations, }: { currentView: string; years: YearGroup[]; recipients: RecipientGroup[]; locations: LocationGroup[]; }) { const [query, setQuery] = useState(""); const [page, setPage] = useState(1); const [sortYear, setSortYear] = useState("asc"); const [sortRecipient, setSortRecipient] = useState("count"); const [sortLocation, setSortLocation] = useState("count"); const handleQueryChange = (q: string) => { setQuery(q); setPage(1); }; const handleSortChange = (mode: SortMode) => { if (currentView === "year") setSortYear(mode); else if (currentView === "recipient") setSortRecipient(mode); else setSortLocation(mode); setPage(1); }; const currentSort = currentView === "year" ? sortYear : currentView === "recipient" ? sortRecipient : sortLocation; // --- Filtered + sorted data --- const filteredYears = useMemo(() => { let data = years; if (query.trim()) { const q = normalize(query.trim()); data = data.filter((yg) => String(yg.year).includes(q)); } return sortYears(data, sortYear); }, [years, query, sortYear]); const filteredRecipients = useMemo(() => { let data = recipients; if (query.trim()) { const q = normalize(query.trim()); data = data.filter((rg) => normalize(rg.name).includes(q)); } return sortRecipients(data, sortRecipient); }, [recipients, query, sortRecipient]); const filteredLocations = useMemo(() => { let data = locations; if (query.trim()) { const q = normalize(query.trim()); data = data.filter((lg) => normalize(lg.name).includes(q)); } return sortLocations(data, sortLocation); }, [locations, query, sortLocation]); // --- Paginated data --- const activeData = currentView === "year" ? filteredYears : currentView === "recipient" ? filteredRecipients : filteredLocations; const pageSize = currentView === "year" ? PAGE_SIZE_YEARS : currentView === "recipient" ? PAGE_SIZE_RECIPIENTS : PAGE_SIZE_LOCATIONS; const totalPages = Math.max(1, Math.ceil(activeData.length / pageSize)); const safePage = Math.min(page, totalPages); const startIdx = (safePage - 1) * pageSize; const endIdx = startIdx + pageSize; const placeholder = currentView === "year" ? "Search years..." : currentView === "recipient" ? "Search recipients..." : "Search locations..."; const resultCount = activeData.length; const totalCount = currentView === "year" ? years.length : currentView === "recipient" ? recipients.length : locations.length; return (
{/* Search + Sort */}
handleQueryChange(e.target.value)} className="pl-9" />
{/* Result count */} {query.trim() && (

{resultCount === 0 ? "No results found" : `${resultCount} of ${totalCount} ${currentView === "year" ? "years" : currentView === "recipient" ? "recipients" : "locations"}`}

)} {/* Year view */} {currentView === "year" && (
{(filteredYears.slice(startIdx, endIdx) as YearGroup[]).map((yg) => (

{yg.year}

{yg.count} {yg.count === 1 ? "letter" : "letters"}

))}
)} {/* Recipient view */} {currentView === "recipient" && (
{(filteredRecipients.slice(startIdx, endIdx) as RecipientGroup[]).map( (rg) => (

{rg.name}

{rg.count} {rg.count === 1 ? "letter" : "letters"}

), )}
)} {/* Location view */} {currentView === "location" && (
{(filteredLocations.slice(startIdx, endIdx) as LocationGroup[]).map( (lg) => (

{lg.name}

{lg.count} {lg.count === 1 ? "letter" : "letters"}

), )}
)} {/* Pagination */}
); }