"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 { UserAvatar } from "@/components/ui/user-avatar"; import { cn } from "@/lib/utils"; import { Search, ArrowUpDown, ArrowDownAZ, ArrowUpAZ, Calendar, CalendarArrowDown, MapPin, Check, ChevronLeft, ChevronRight, } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type DiscipleSummary = { id: string; initiatedName: string; suffix: string; initiationDate: string | null; initiationPlace: string | null; isVerified: boolean; }; /** A disciple who has a platform account (linked by an admin). */ export type DiscipleLink = { discipleId: string; handle: string; name: string; avatarURL: string | null; }; export type LetterCount = { letter: string; count: number }; export type PlaceGroup = { name: string; count: number }; export type YearGroup = { year: number; count: number }; export type SuffixCount = { suffix: string; count: number }; type SuffixFilter = "all" | "Dasa" | "Devi Dasi"; type Props = { initialDisciples: DiscipleSummary[]; initialLetter: string; activeSuffix: SuffixFilter; letterCounts: LetterCount[]; suffixCounts: SuffixCount[]; places: PlaceGroup[]; years: YearGroup[]; totalCount: number; filteredCount: number; /** Disciples who have joined the platform — their cards are highlighted and * link through to the profile. Small set (dozens at most). */ links: DiscipleLink[]; }; type SortMode = "name-asc" | "name-desc" | "date-asc" | "date-desc" | "place"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXY".split(""); const PAGE_SIZE = 60; const SUFFIX_TABS: { key: SuffixFilter; label: string }[] = [ { key: "all", label: "All" }, { key: "Dasa", label: "Dasa" }, { key: "Devi Dasi", label: "Devi Dasi" }, ]; // --------------------------------------------------------------------------- // Diacritic-aware normalize // --------------------------------------------------------------------------- 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, ""); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function extractYear(isoDate: string | null): number | null { if (!isoDate) return null; const year = parseInt(isoDate.slice(0, 4), 10); return isNaN(year) ? null : year; } function formatLocationYear( place: string | null, date: string | null, ): string | null { const year = extractYear(date); const parts: string[] = []; if (place) parts.push(place); if (year) parts.push(String(year)); return parts.length > 0 ? parts.join(" · ") : null; } /** * Build the href for letter + suffix navigation. * Always preserves both params so the URL stays consistent. */ function buildHref(letter: string, suffix: SuffixFilter): string { const params = new URLSearchParams(); params.set("letter", letter); if (suffix !== "all") { params.set("suffix", suffix); } return `/disciples?${params.toString()}`; } // --------------------------------------------------------------------------- // Sort options // --------------------------------------------------------------------------- const SORT_OPTIONS: { key: SortMode; label: string; icon: typeof ArrowDownAZ; }[] = [ { key: "name-asc", label: "Name A → Z", icon: ArrowDownAZ }, { key: "name-desc", label: "Name Z → A", icon: ArrowUpAZ }, { key: "date-asc", label: "Earliest first", icon: Calendar }, { key: "date-desc", label: "Latest first", icon: CalendarArrowDown }, { key: "place", label: "By place", icon: MapPin }, ]; function sortDisciples( data: DiscipleSummary[], mode: SortMode, ): DiscipleSummary[] { const sorted = [...data]; switch (mode) { case "name-asc": return sorted.sort((a, b) => a.initiatedName.localeCompare(b.initiatedName), ); case "name-desc": return sorted.sort((a, b) => b.initiatedName.localeCompare(a.initiatedName), ); case "date-asc": return sorted.sort((a, b) => { if (!a.initiationDate && !b.initiationDate) return 0; if (!a.initiationDate) return 1; if (!b.initiationDate) return -1; return a.initiationDate.localeCompare(b.initiationDate); }); case "date-desc": return sorted.sort((a, b) => { if (!a.initiationDate && !b.initiationDate) return 0; if (!a.initiationDate) return 1; if (!b.initiationDate) return -1; return b.initiationDate.localeCompare(a.initiationDate); }); case "place": return sorted.sort((a, b) => { if (!a.initiationPlace && !b.initiationPlace) return 0; if (!a.initiationPlace) return 1; if (!b.initiationPlace) return -1; const cmp = a.initiationPlace.localeCompare(b.initiationPlace); return cmp !== 0 ? cmp : a.initiatedName.localeCompare(b.initiatedName); }); } } // --------------------------------------------------------------------------- // Sort Popover // --------------------------------------------------------------------------- function SortPopover({ sort, onSortChange, }: { sort: SortMode; onSortChange: (s: SortMode) => void; }) { const [open, setOpen] = useState(false); const ref = useRef(null); 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]); return (
{open && (
{SORT_OPTIONS.map((opt) => { const Icon = opt.icon; const isActive = sort === opt.key; return ( ); })}
)}
); } // --------------------------------------------------------------------------- // Pagination // --------------------------------------------------------------------------- function Pagination({ page, totalPages, onPageChange, }: { page: number; totalPages: number; onPageChange: (p: number) => void; }) { if (totalPages <= 1) return null; return (
Page {page} of {totalPages}
); } // --------------------------------------------------------------------------- // Disciple card // --------------------------------------------------------------------------- /** * One disciple in the grid. * * When `link` is set the disciple has joined the platform, and the card is * highlighted and becomes a link to their profile. The highlight is drawn * entirely from theme tokens (`--primary`, `--ring`) rather than fixed colours, * so it picks up whichever palette the reader has chosen — peacock green under * Tulasī Garden, deep indigo under Yamunā Mist, and so on. */ function DiscipleCard({ disciple, link, }: { disciple: DiscipleSummary; link?: DiscipleLink; }) { const locationYear = formatLocationYear( disciple.initiationPlace, disciple.initiationDate, ); const card = (

{disciple.initiatedName}

{disciple.suffix && ( {disciple.suffix} )}
{locationYear && (

{locationYear}

)} {link && (
@{link.handle}
)}
); if (!link) return card; return ( {card} ); } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function DisciplesBrowser({ initialDisciples, initialLetter, activeSuffix, letterCounts, suffixCounts, totalCount, filteredCount, links, }: Props) { const [query, setQuery] = useState(""); const [sort, setSort] = useState("name-asc"); const [page, setPage] = useState(1); // Reset page when search changes const handleQueryChange = (q: string) => { setQuery(q); setPage(1); }; const handleSortChange = (s: SortMode) => { setSort(s); setPage(1); }; // Disciple id → their platform account, for the highlight + profile link. const linkByDiscipleId = useMemo(() => { const map = new Map(); for (const l of links) map.set(l.discipleId, l); return map; }, [links]); // Build a lookup map for letter counts const countByLetter = useMemo(() => { const map = new Map(); for (const lc of letterCounts) { map.set(lc.letter.toUpperCase(), lc.count); } return map; }, [letterCounts]); // Build a lookup map for suffix counts const countBySuffix = useMemo(() => { const map = new Map(); let total = 0; for (const sc of suffixCounts) { map.set(sc.suffix, sc.count); total += sc.count; } map.set("all", total); return map; }, [suffixCounts]); // Client-side search filtering + sorting const filtered = useMemo(() => { let data = initialDisciples; if (query.trim()) { const q = normalize(query.trim()); data = data.filter((d) => normalize(d.initiatedName).includes(q)); } return sortDisciples(data, sort); }, [initialDisciples, query, sort]); // Pagination const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages); const startIdx = (safePage - 1) * PAGE_SIZE; const pageData = filtered.slice(startIdx, startIdx + PAGE_SIZE); // Total for letter display const totalLetterCount = Array.from(countByLetter.values()).reduce( (a, b) => a + b, 0, ); return (
{/* ---- Suffix Filter Tabs ---- */}
{SUFFIX_TABS.map((tab) => { const count = countBySuffix.get(tab.key) ?? 0; const isActive = activeSuffix === tab.key; return ( {tab.label} {count.toLocaleString()} ); })}
{/* ---- Letter Navigation Bar ---- */} {/* ---- Search Bar + Sort ---- */}
) => handleQueryChange(e.target.value) } className="pl-9" />
{/* ---- Summary Stats ---- */}

{query.trim() ? ( <> Found{" "} {filtered.length} {" "} of{" "} {initialDisciples.length} {" "} disciples ) : ( <> Showing{" "} {filteredCount.toLocaleString()} {" "} {filteredCount === 1 ? "disciple" : "disciples"} {filteredCount !== totalCount && ( <> {" "} of{" "} {totalCount.toLocaleString()} {" "} total )} )}

{/* ---- Results Grid ---- */} {pageData.length === 0 ? (

No disciples found

{query.trim() ? "Try a different search term" : `No disciples found for the current filter`}

) : (
{pageData.map((disciple) => ( ))}
)} {/* ---- Pagination ---- */}
); }