"use client"; import { useState, useMemo, useRef } from "react"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { ChevronLeft, ChevronRight, Menu, Calendar, Clock, MapPin } from "lucide-react"; import { PlayAudioButton } from "@/app/(app)/(reader)/transcriptions/[slug]/play-audio-button"; import { parseChapter, parseVerse } from "@/lib/book-reference"; import { Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type TranscriptionItem = { slug: string; name: string; type: string; date: string | null; year: number | null; month: number | null; day: number | null; place: string | null; context: string | null; reference: string | null; durationMinutes: number | null; participants: string[]; audioUrl: string | null; }; type ChapterGroup = { key: string; label: string; transcriptions: TranscriptionItem[]; }; // A section is a canto (SB) or līlā (CC). Books without that level produce a // single section with a null label (rendered without a heading). type SectionGroup = { key: string; label: string | null; chapters: ChapterGroup[]; }; // --------------------------------------------------------------------------- // Grouping // --------------------------------------------------------------------------- // Group all of a book's transcripts into sections (canto/līlā) → chapters, // ordered by the parsed sort tuple. Books without a canto/līlā level collapse // into a single section with a null label. Only sections/chapters that have // transcripts appear. Transcripts arrive already sorted chronologically. function buildSections( transcriptions: TranscriptionItem[], code: string, ): SectionGroup[] { const sections = new Map< string, { label: string | null; order: number; chapters: Map< string, { label: string; sort: [number, number]; items: TranscriptionItem[] } >; } >(); for (const t of transcriptions) { const ch = parseChapter(code, t.reference); const secKey = ch.section ? ch.section.key : "__flat__"; if (!sections.has(secKey)) { sections.set(secKey, { label: ch.section ? ch.section.label : null, order: ch.section ? ch.section.order : 0, chapters: new Map(), }); } const sec = sections.get(secKey)!; if (!sec.chapters.has(ch.key)) { sec.chapters.set(ch.key, { label: ch.label, sort: ch.sort, items: [] }); } sec.chapters.get(ch.key)!.items.push(t); } return Array.from(sections.entries()) .sort((a, b) => a[1].order - b[1].order) .map(([key, s]) => ({ key, label: s.label, chapters: Array.from(s.chapters.entries()) .sort( (a, b) => a[1].sort[0] - b[1].sort[0] || a[1].sort[1] - b[1].sort[1], ) .map(([k, v]) => ({ key: k, label: v.label, // Order lectures by verse ascending (e.g. 2.1, 2.3, 2.4…), // irrespective of year. Stable sort keeps year order within a verse. transcriptions: v.items .slice() .sort((a, b) => parseVerse(a.reference) - parseVerse(b.reference)), })), })); } // Flatten sections into a linear chapter list for prev/next navigation and // active-chapter lookup, carrying each chapter's section label for display. type FlatChapter = { sectionLabel: string | null; chapter: ChapterGroup }; function flattenChapters(sections: SectionGroup[]): FlatChapter[] { return sections.flatMap((s) => s.chapters.map((chapter) => ({ sectionLabel: s.label, chapter })), ); } // --------------------------------------------------------------------------- // Chapter nav list (shared by sidebar + mobile drawer) // --------------------------------------------------------------------------- function ChapterNavList({ sections, activeKey, onSelect, }: { sections: SectionGroup[]; activeKey: string; onSelect: (key: string) => void; }) { return ( ); } function ChapterSidebar(props: { sections: SectionGroup[]; activeKey: string; onSelect: (key: string) => void; }) { return ( ); } function ChapterMobileNav({ sections, activeKey, onSelect, title, }: { sections: SectionGroup[]; activeKey: string; onSelect: (key: string) => void; title: string; }) { const [open, setOpen] = useState(false); return ( Chapters {title}
{ onSelect(key); setOpen(false); }} />
); } // --------------------------------------------------------------------------- // Prev / Next nav // --------------------------------------------------------------------------- function ChapterNav({ prev, next, sections, activeKey, onSelect, drawerTitle, }: { prev: FlatChapter | null; next: FlatChapter | null; sections: SectionGroup[]; activeKey: string; onSelect: (key: string) => void; drawerTitle: string; }) { return (
{prev ? ( ) : (
)}
{next ? ( ) : (
)}
); } // --------------------------------------------------------------------------- // Lecture row // --------------------------------------------------------------------------- function LectureRow({ t }: { t: TranscriptionItem }) { return (

{t.name}

{t.type === "lecture" ? "Lecture" : "Conversation"}
{t.date && ( {t.date} )} {t.place && ( {t.place} )} {t.reference && ( {t.reference} )} {t.durationMinutes && t.durationMinutes > 0 && ( {t.durationMinutes} min )}
{t.audioUrl && ( )} ); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export function TranscriptionBookBrowser({ transcriptions, code, title, totalCount, }: { transcriptions: TranscriptionItem[]; code: string; title: string; totalCount: number; }) { const sections = useMemo( () => buildSections(transcriptions, code), [transcriptions, code], ); const flat = useMemo(() => flattenChapters(sections), [sections]); // The active chapter lives in the URL (?chapter=) so it's bookmarkable // and survives back-navigation: opening a lecture then pressing Back returns // to the chapter you were on rather than resetting to the first one. We seed // local state from the URL on mount and keep the URL in sync on selection. const pathname = usePathname(); const searchParams = useSearchParams(); const [activeKeyRaw, setActiveKey] = useState(() => { const fromUrl = searchParams.get("chapter"); if (fromUrl && flat.find((f) => f.chapter.key === fromUrl)) return fromUrl; return flat.length > 0 ? flat[0].chapter.key : ""; }); // Derive a safe active chapter key (the data could change underneath us). const activeKey = useMemo(() => { if (flat.length === 0) return activeKeyRaw; if (flat.find((f) => f.chapter.key === activeKeyRaw)) return activeKeyRaw; return flat[0].chapter.key; }, [flat, activeKeyRaw]); const contentRef = useRef(null); const handleSelect = (key: string) => { setActiveKey(key); // Reflect the choice in the URL without a new history entry (so Back leaves // the page rather than cycling chapters) or a server round-trip. const params = new URLSearchParams(Array.from(searchParams.entries())); params.set("chapter", key); window.history.replaceState(null, "", `${pathname}?${params.toString()}`); contentRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); }; const activeIdx = flat.findIndex((f) => f.chapter.key === activeKey); const active = activeIdx >= 0 ? flat[activeIdx] : null; const prev = activeIdx > 0 ? flat[activeIdx - 1] : null; const next = activeIdx >= 0 && activeIdx < flat.length - 1 ? flat[activeIdx + 1] : null; const activeFullLabel = active ? active.sectionLabel ? `${active.sectionLabel} · ${active.chapter.label}` : active.chapter.label : ""; const activeCount = active?.chapter.transcriptions.length ?? 0; return ( <> {/* Header */}
← Transcriptions by Book

{title} {activeFullLabel ? `: ${activeFullLabel}` : ""}

{activeCount} {activeCount === 1 ? "lecture" : "lectures"} in this chapter · {totalCount} total

{/* Sidebar + Main */}
{/* Left sidebar — chapters */} {/* Main content */}
{/* Top navigation */} {/* Content — every lecture of the active chapter (all years/places) */} {!active || active.chapter.transcriptions.length === 0 ? (

No transcriptions for this chapter

) : ( {active.chapter.transcriptions.map((t) => ( ))} )} {/* Bottom navigation */}
); }