"use client"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useMemo, useEffect, useState } from "react"; import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type Synonym = { word: string; meanings: string[]; }; type Verse = { id: string; book: string; division: string | null; chapter: number; verse: number; verseEnd: number | null; reference: string; bookTitle: string; devanagari: string; bengali: string | null; transliteration: string; synonyms: Synonym[]; translation: string; purport: string[]; }; type VerseIndexEntry = { verse: number; verseEnd?: number; reference: string; }; // --------------------------------------------------------------------------- // Props // --------------------------------------------------------------------------- type ChapterViewerProps = { /** The first verse (SSR'd), shown before any client-side fetch */ initialVerse: Verse; /** The verse number of the initial verse */ initialVerseNum: number; /** Lightweight index of ALL verses in the chapter (for the sidebar) */ verseIndex: VerseIndexEntry[]; /** Book key */ book: string; /** Division: null for BG, canto string for SB, lila key for CC */ division: string | null; /** Chapter number */ chapter: number; /** Whether to show the Bengali script field (CC only) */ showBengali?: boolean; }; // --------------------------------------------------------------------------- // Navigation buttons (reused at top and bottom) // --------------------------------------------------------------------------- function VerseNav({ currentIdx, total, onPrev, onNext, verseIndex, }: { currentIdx: number; total: number; onPrev: () => void; onNext: () => void; verseIndex: VerseIndexEntry[]; }) { const hasPrev = currentIdx > 0; const hasNext = currentIdx < total - 1; const current = verseIndex[currentIdx]; const label = current ? current.verseEnd ? `${current.verse}–${current.verseEnd}` : String(current.verse) : ""; return (
Verse {label} of {total}
); } // --------------------------------------------------------------------------- // Verse card // --------------------------------------------------------------------------- function VerseCard({ verse, showBengali, }: { verse: Verse; showBengali: boolean; }) { return ( {verse.reference} {/* Bengali (CC only) */} {showBengali && verse.bengali && (

{verse.bengali}

)} {/* Devanagari */}

{verse.devanagari}

{/* Transliteration */}

{verse.transliteration}

{/* Synonyms */} {verse.synonyms.length > 0 && (

Synonyms

{verse.synonyms.map((s, i) => ( {s.word}— {s.meanings.join(", ")} {i < verse.synonyms.length - 1 ? "; " : "."} ))}

)} {/* Translation */}

Translation

{/* Purport */} {verse.purport.length > 0 && (

Purport

{verse.purport.map((p, i) => (

))}

)}
); } // --------------------------------------------------------------------------- // ChapterViewer // --------------------------------------------------------------------------- export function ChapterViewer({ initialVerse, initialVerseNum, verseIndex, book, division, chapter, showBengali = false, }: ChapterViewerProps) { const router = useRouter(); const searchParams = useSearchParams(); // Verse cache: verseNum → Verse data const [cache, setCache] = useState>(() => { const map = new Map(); map.set(initialVerseNum, initialVerse); return map; }); const [loading, setLoading] = useState(false); // Current verse number from URL (or default to initial) const currentVerseNum = useMemo(() => { const param = searchParams.get("verse"); if (param) { const num = parseInt(param); if (!isNaN(num) && verseIndex.some((v) => v.verse === num)) { return num; } } return initialVerseNum; }, [searchParams, initialVerseNum, verseIndex]); // Current index in the verseIndex array const currentIdx = useMemo( () => verseIndex.findIndex((v) => v.verse === currentVerseNum), [verseIndex, currentVerseNum], ); // The verse data to display const currentVerse = cache.get(currentVerseNum) ?? null; // Reset cache when route changes to a different chapter useEffect(() => { const map = new Map(); map.set(initialVerseNum, initialVerse); setCache(map); }, [initialVerse, initialVerseNum]); // Fetch a verse if it's not in the cache const fetchVerse = useCallback( async (verseNum: number) => { if (cache.has(verseNum)) return cache.get(verseNum)!; setLoading(true); const params = new URLSearchParams({ chapter: String(chapter), verse: String(verseNum), }); if (division !== null) { params.set("division", division); } try { const res = await fetch(`/api/books/${book}/verses?${params}`); if (!res.ok) throw new Error(`API error: ${res.status}`); const data = await res.json(); // The API returns the verse in a paginated wrapper or an array const verse: Verse | undefined = Array.isArray(data.verses) ? data.verses[0] : Array.isArray(data) ? data[0] : undefined; if (verse) { setCache((prev) => { const next = new Map(prev); next.set(verseNum, verse); return next; }); return verse; } } catch (err) { console.error("Failed to fetch verse:", err); } finally { setLoading(false); } return null; }, [cache, book, division, chapter], ); // Navigate to a verse — update the URL and fetch if needed const navigateTo = useCallback( async (verseNum: number) => { // Update URL with shallow navigation const params = new URLSearchParams(searchParams.toString()); params.set("verse", String(verseNum)); router.push(`?${params.toString()}`, { scroll: false }); // Fetch if not cached if (!cache.has(verseNum)) { await fetchVerse(verseNum); } // Scroll to top of the verse window.scrollTo({ top: 0, behavior: "smooth" }); }, [searchParams, router, cache, fetchVerse], ); // Fetch current verse on mount/URL change if not cached useEffect(() => { if (!cache.has(currentVerseNum)) { fetchVerse(currentVerseNum); } }, [currentVerseNum, cache, fetchVerse]); // Prefetch adjacent verses useEffect(() => { if (currentIdx > 0) { const prevNum = verseIndex[currentIdx - 1].verse; if (!cache.has(prevNum)) fetchVerse(prevNum); } if (currentIdx < verseIndex.length - 1) { const nextNum = verseIndex[currentIdx + 1].verse; if (!cache.has(nextNum)) fetchVerse(nextNum); } }, [currentIdx, verseIndex, cache, fetchVerse]); // Keyboard navigation useEffect(() => { const handleKey = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) return; if (e.key === "ArrowLeft" && currentIdx > 0) { e.preventDefault(); navigateTo(verseIndex[currentIdx - 1].verse); } else if (e.key === "ArrowRight" && currentIdx < verseIndex.length - 1) { e.preventDefault(); navigateTo(verseIndex[currentIdx + 1].verse); } }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [currentIdx, verseIndex, navigateTo]); const goPrev = useCallback(() => { if (currentIdx > 0) navigateTo(verseIndex[currentIdx - 1].verse); }, [currentIdx, verseIndex, navigateTo]); const goNext = useCallback(() => { if (currentIdx < verseIndex.length - 1) navigateTo(verseIndex[currentIdx + 1].verse); }, [currentIdx, verseIndex, navigateTo]); return ( <> {/* ---- Sidebar: Verse Index ---- */} {/* ---- Main: Single Verse ---- */}
{/* Top navigation */} {/* Verse content */} {loading && !currentVerse ? (
Loading verse…
) : currentVerse ? ( ) : (
Verse not found.
)} {/* Bottom navigation */}
); }