From 213eea8e949e97ad2ef3a5d8f241d0811810618f Mon Sep 17 00:00:00 2001 From: gosangam Date: Tue, 14 Apr 2026 23:15:26 +0530 Subject: [PATCH] feat: add pagination --- .gitignore | 1 + app/api/books/[book]/verses/route.ts | 23 +- app/books/bg/[chapter]/page.tsx | 36 +- app/books/cc/[lila]/[chapter]/page.tsx | 36 +- app/books/chapter-viewer.tsx | 682 +++++++++++------------- app/books/sb/[canto]/[chapter]/page.tsx | 36 +- lib/api.ts | 20 + 7 files changed, 428 insertions(+), 406 deletions(-) diff --git a/.gitignore b/.gitignore index 5fbbaa9d..a3c20187 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ next-env.d.ts public/rtfs /lib/generated/prisma +app/lib diff --git a/app/api/books/[book]/verses/route.ts b/app/api/books/[book]/verses/route.ts index b4fc6704..11eecd8a 100644 --- a/app/api/books/[book]/verses/route.ts +++ b/app/api/books/[book]/verses/route.ts @@ -1,8 +1,6 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -const DEFAULT_PAGE_SIZE = 20; - export async function GET( req: Request, { params }: { params: Promise<{ book: string }> }, @@ -11,6 +9,7 @@ export async function GET( const url = new URL(req.url); const division = url.searchParams.get("division"); const chapterStr = url.searchParams.get("chapter"); + const verseStr = url.searchParams.get("verse"); const indexOnly = url.searchParams.get("indexOnly") === "true"; const pageStr = url.searchParams.get("page"); const limitStr = url.searchParams.get("limit"); @@ -31,8 +30,21 @@ export async function GET( where.division = division; } + // --- Single verse mode: ?verse=N --- + if (verseStr) { + const verseNum = parseInt(verseStr); + const verse = await prisma.verse.findFirst({ + where: { ...where, verse: verseNum }, + }); + + if (!verse) { + return NextResponse.json({ error: "Verse not found" }, { status: 404 }); + } + + return NextResponse.json(verse); + } + // --- Index-only mode: return lightweight verse list for the sidebar --- - // Returns all verses but only the fields needed for navigation if (indexOnly) { const verses = await prisma.verse.findMany({ where, @@ -49,10 +61,7 @@ export async function GET( // --- Paginated mode: return full verse data in pages --- const page = Math.max(1, parseInt(pageStr || "1")); - const limit = Math.min( - 100, - Math.max(1, parseInt(limitStr || String(DEFAULT_PAGE_SIZE))), - ); + const limit = Math.min(100, Math.max(1, parseInt(limitStr || "20"))); const skip = (page - 1) * limit; const [verses, total] = await Promise.all([ diff --git a/app/books/bg/[chapter]/page.tsx b/app/books/bg/[chapter]/page.tsx index 76710a60..4e2e9d1e 100644 --- a/app/books/bg/[chapter]/page.tsx +++ b/app/books/bg/[chapter]/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { PageHeader } from "@/components/page-header"; import { notFound } from "next/navigation"; -import { getBookMeta, getChapterVerses, getChapterVerseIndex } from "@/lib/api"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; import { ChapterViewer } from "@/app/books/chapter-viewer"; type BgMeta = { @@ -14,11 +14,13 @@ type BgMeta = { export default async function ChapterPage({ params, + searchParams, }: { params: Promise<{ chapter: string }>; + searchParams: Promise<{ verse?: string }>; }) { const { chapter } = await params; - const PAGE_SIZE = 20; + const { verse: verseParam } = await searchParams; const meta = await getBookMeta("bg"); if (!meta) notFound(); @@ -31,12 +33,20 @@ export default async function ChapterPage({ const title: string = chapterInfo.title; - const [verseIndex, firstPage] = await Promise.all([ - getChapterVerseIndex("bg", null, parseInt(chapter)), - getChapterVerses("bg", null, parseInt(chapter), 1, PAGE_SIZE), - ]); + const verseIndex = await getChapterVerseIndex("bg", null, parseInt(chapter)); if (verseIndex.length === 0) notFound(); + const requestedVerse = verseParam + ? parseInt(verseParam) + : verseIndex[0].verse; + const verse = await getSingleVerse( + "bg", + null, + parseInt(chapter), + requestedVerse, + ); + if (!verse) notFound(); + return (
@@ -53,19 +63,17 @@ export default async function ChapterPage({
({ verse: v.verse, verseEnd: v.verseEnd ?? undefined, reference: v.reference, }))} + book="bg" + division={null} + chapter={parseInt(chapter)} + showBengali={false} />
diff --git a/app/books/cc/[lila]/[chapter]/page.tsx b/app/books/cc/[lila]/[chapter]/page.tsx index 57cc980d..5b545c7f 100644 --- a/app/books/cc/[lila]/[chapter]/page.tsx +++ b/app/books/cc/[lila]/[chapter]/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { PageHeader } from "@/components/page-header"; import { notFound } from "next/navigation"; -import { getBookMeta, getChapterVerses, getChapterVerseIndex } from "@/lib/api"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; import { ChapterViewer } from "@/app/books/chapter-viewer"; type ChapterInfo = { @@ -20,11 +20,13 @@ type LilaInfo = { export default async function CcChapterPage({ params, + searchParams, }: { params: Promise<{ lila: string; chapter: string }>; + searchParams: Promise<{ verse?: string }>; }) { const { lila, chapter } = await params; - const PAGE_SIZE = 20; + const { verse: verseParam } = await searchParams; const meta = await getBookMeta("cc"); if (!meta) notFound(); @@ -38,12 +40,20 @@ export default async function CcChapterPage({ ); if (!chapterInfo) notFound(); - const [verseIndex, firstPage] = await Promise.all([ - getChapterVerseIndex("cc", lila, parseInt(chapter)), - getChapterVerses("cc", lila, parseInt(chapter), 1, PAGE_SIZE), - ]); + const verseIndex = await getChapterVerseIndex("cc", lila, parseInt(chapter)); if (verseIndex.length === 0) notFound(); + const requestedVerse = verseParam + ? parseInt(verseParam) + : verseIndex[0].verse; + const verse = await getSingleVerse( + "cc", + lila, + parseInt(chapter), + requestedVerse, + ); + if (!verse) notFound(); + return (
@@ -60,19 +70,17 @@ export default async function CcChapterPage({
({ verse: v.verse, verseEnd: v.verseEnd ?? undefined, reference: v.reference, }))} + book="cc" + division={lila} + chapter={parseInt(chapter)} + showBengali={true} />
diff --git a/app/books/chapter-viewer.tsx b/app/books/chapter-viewer.tsx index f821e534..23fb6bd3 100644 --- a/app/books/chapter-viewer.tsx +++ b/app/books/chapter-viewer.tsx @@ -1,9 +1,11 @@ "use client"; -import { useEffect, useRef, useState, useCallback } from "react"; +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 { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; // --------------------------------------------------------------------------- // Types @@ -31,15 +33,6 @@ type Verse = { purport: string[]; }; -type PaginatedResponse = { - verses: Verse[]; - total: number; - page: number; - limit: number; - totalPages: number; - hasMore: boolean; -}; - type VerseIndexEntry = { verse: number; verseEnd?: number; @@ -51,92 +44,230 @@ type VerseIndexEntry = { // --------------------------------------------------------------------------- type ChapterViewerProps = { - initialVerses: Verse[]; - hasMore: boolean; - totalPages: number; - book: string; - division: string | null; - chapter: number; - limit: number; - showBengali?: boolean; + /** 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 // --------------------------------------------------------------------------- -/** - * ChapterViewer — a single "use client" component that owns both the sidebar - * verse index and the main verse list with infinite scroll + jump-to-verse. - * - * This avoids the server→client boundary issue of passing functions as props - * (render props / callbacks can't cross that boundary). - * - * The server component only passes serialisable data: - * - initialVerses (page 1 of full verse data, SSR'd) - * - verseIndex (lightweight list of ALL verse numbers/references) - * - pagination metadata (hasMore, totalPages, book, division, chapter, limit) - */ export function ChapterViewer({ - initialVerses, - hasMore: initialHasMore, - totalPages: initialTotalPages, + initialVerse, + initialVerseNum, + verseIndex, book, division, chapter, - limit, showBengali = false, - verseIndex, }: ChapterViewerProps) { - // ------ Verse data state ------ - const [verses, setVerses] = useState(initialVerses); - const [loading, setLoading] = useState(false); - const [hasMorePages, setHasMorePages] = useState(initialHasMore); - const [highestPageFetched, setHighestPageFetched] = useState(1); - const [totalPagesKnown, setTotalPagesKnown] = useState(initialTotalPages); - const fetchingRef = useRef(false); - const sentinelRef = useRef(null); + const router = useRouter(); + const searchParams = useSearchParams(); - // ------ Sidebar state ------ - const [activeVerse, setActiveVerse] = useState(null); - const [loadedVerses, setLoadedVerses] = useState>(() => { - const set = new Set(); - for (const v of initialVerses) set.add(v.verse); - return set; + // Verse cache: verseNum → Verse data + const [cache, setCache] = useState>(() => { + const map = new Map(); + map.set(initialVerseNum, initialVerse); + return map; }); - const [pendingVerse, setPendingVerse] = useState(null); - // Reset when route changes (different chapter) + 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(() => { - setVerses(initialVerses); - setHasMorePages(initialHasMore); - setHighestPageFetched(1); - setTotalPagesKnown(initialTotalPages); - const set = new Set(); - for (const v of initialVerses) set.add(v.verse); - setLoadedVerses(set); - setPendingVerse(null); - setActiveVerse(null); - }, [initialVerses, initialHasMore, initialTotalPages]); + const map = new Map(); + map.set(initialVerseNum, initialVerse); + setCache(map); + }, [initialVerse, initialVerseNum]); - // ---- Update loadedVerses whenever verses array changes ---- - useEffect(() => { - const set = new Set(); - for (const v of verses) set.add(v.verse); - setLoadedVerses(set); - }, [verses]); + // Fetch a verse if it's not in the cache + const fetchVerse = useCallback( + async (verseNum: number) => { + if (cache.has(verseNum)) return cache.get(verseNum)!; - // ===================================================================== - // Fetching - // ===================================================================== + setLoading(true); - const fetchPage = useCallback( - async (page: number): Promise => { const params = new URLSearchParams({ chapter: String(chapter), - page: String(page), - limit: String(limit), + verse: String(verseNum), }); if (division !== null) { params.set("division", division); @@ -145,203 +276,102 @@ export function ChapterViewer({ try { const res = await fetch(`/api/books/${book}/verses?${params}`); if (!res.ok) throw new Error(`API error: ${res.status}`); - return await res.json(); + 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 page ${page}:`, err); - return null; + console.error("Failed to fetch verse:", err); + } finally { + setLoading(false); } + + return null; }, - [book, division, chapter, limit], + [cache, book, division, chapter], ); - // Fetch the next single page (for infinite scroll) - const fetchNextPage = useCallback(async () => { - if (fetchingRef.current || !hasMorePages) return; - fetchingRef.current = true; - setLoading(true); + // 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 }); - const nextPage = highestPageFetched + 1; - const data = await fetchPage(nextPage); + // Fetch if not cached + if (!cache.has(verseNum)) { + await fetchVerse(verseNum); + } - if (data) { - setVerses((prev) => [...prev, ...data.verses]); - setHasMorePages(data.hasMore); - setHighestPageFetched(nextPage); - setTotalPagesKnown(data.totalPages); + // 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]); - setLoading(false); - fetchingRef.current = false; - }, [hasMorePages, highestPageFetched, fetchPage]); + // 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]); - // Fetch all pages up to the one containing targetVerse - const requestVerse = useCallback( - async (targetVerse: number) => { - // Already loaded? - if (verses.some((v) => v.verse === targetVerse)) { - // Just scroll to it - const el = document.getElementById(`verse-${targetVerse}`); - if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + // 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); } - - if (fetchingRef.current) return; - fetchingRef.current = true; - setLoading(true); - setPendingVerse(targetVerse); - - let currentHighest = highestPageFetched; - const allNewVerses: Verse[] = []; - let found = false; - let moreRemaining = hasMorePages; - - while (!found && currentHighest < totalPagesKnown) { - const nextPage = currentHighest + 1; - const data = await fetchPage(nextPage); - - if (!data || data.verses.length === 0) break; - - allNewVerses.push(...data.verses); - currentHighest = nextPage; - moreRemaining = data.hasMore; - setTotalPagesKnown(data.totalPages); - - if (data.verses.some((v) => v.verse === targetVerse)) { - found = true; - } - } - - if (allNewVerses.length > 0) { - setVerses((prev) => [...prev, ...allNewVerses]); - setHighestPageFetched(currentHighest); - setHasMorePages(moreRemaining); - } - - setLoading(false); - fetchingRef.current = false; - // pendingVerse will trigger scroll via the effect below - }, - [verses, highestPageFetched, hasMorePages, totalPagesKnown, fetchPage], - ); - - // ===================================================================== - // Scroll to pending verse once it appears in the DOM - // ===================================================================== - - useEffect(() => { - if (pendingVerse === null) return; - if (!loadedVerses.has(pendingVerse)) return; - - requestAnimationFrame(() => { - setTimeout(() => { - const el = document.getElementById(`verse-${pendingVerse}`); - if (el) { - el.scrollIntoView({ behavior: "smooth", block: "start" }); - } - setPendingVerse(null); - }, 50); - }); - }, [loadedVerses, pendingVerse]); - - // ===================================================================== - // Infinite scroll observer (bottom sentinel) - // ===================================================================== - - useEffect(() => { - const sentinel = sentinelRef.current; - if (!sentinel) return; - - const observer = new IntersectionObserver( - (entries) => { - if (entries[0]?.isIntersecting && hasMorePages && !fetchingRef.current) { - fetchNextPage(); - } - }, - { rootMargin: "600px" }, - ); - - observer.observe(sentinel); - return () => observer.disconnect(); - }, [fetchNextPage, hasMorePages]); - - // ===================================================================== - // Active verse scroll tracking (IntersectionObserver + MutationObserver) - // ===================================================================== - - useEffect(() => { - const intersectionObserver = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - const num = parseInt(entry.target.id.replace("verse-", "")); - if (!isNaN(num)) setActiveVerse(num); - } - } - }, - { rootMargin: "-20% 0px -60% 0px" }, - ); - - const observeAll = () => { - const els = document.querySelectorAll("[id^='verse-']"); - els.forEach((el) => intersectionObserver.observe(el)); }; - observeAll(); + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [currentIdx, verseIndex, navigateTo]); - // Watch for new verse cards from infinite scroll - const mutationObserver = new MutationObserver((mutations) => { - let hasNew = false; - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if ( - node instanceof HTMLElement && - (node.id?.startsWith("verse-") || - node.querySelector?.("[id^='verse-']")) - ) { - hasNew = true; - break; - } - } - if (hasNew) break; - } - if (hasNew) observeAll(); - }); + const goPrev = useCallback(() => { + if (currentIdx > 0) navigateTo(verseIndex[currentIdx - 1].verse); + }, [currentIdx, verseIndex, navigateTo]); - const main = document.querySelector("main"); - if (main) { - mutationObserver.observe(main, { childList: true, subtree: true }); - } - - return () => { - intersectionObserver.disconnect(); - mutationObserver.disconnect(); - }; - }, []); - - // ===================================================================== - // Sidebar click handler - // ===================================================================== - - const handleIndexClick = useCallback( - (e: React.MouseEvent, verseNum: number) => { - e.preventDefault(); - - const el = document.getElementById(`verse-${verseNum}`); - if (el) { - el.scrollIntoView({ behavior: "smooth", block: "start" }); - return; - } - - // Not loaded — request it - setPendingVerse(verseNum); - requestVerse(verseNum); - }, - [requestVerse], - ); - - // ===================================================================== - // Render - // ===================================================================== + const goNext = useCallback(() => { + if (currentIdx < verseIndex.length - 1) + navigateTo(verseIndex[currentIdx + 1].verse); + }, [currentIdx, verseIndex, navigateTo]); return ( <> @@ -352,23 +382,20 @@ export function ChapterViewer({ - {/* ---- Main: Verse Cards ---- */} -
- {verses.map((verse) => ( - - - {verse.reference} - + {/* ---- Main: Single Verse ---- */} +
+ {/* Top navigation */} + - - {/* 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) => ( -

- ))} -

-
- )} -
- - ))} - - {/* Sentinel for infinite scroll */} -
- - {/* Loading indicator */} - {loading && ( -
- - Loading more verses… + {/* Verse content */} + {loading && !currentVerse ? ( +
+ + Loading verse… +
+ ) : currentVerse ? ( + + ) : ( +
+ Verse not found.
)} + + {/* Bottom navigation */} +
); diff --git a/app/books/sb/[canto]/[chapter]/page.tsx b/app/books/sb/[canto]/[chapter]/page.tsx index abb2bbc9..f093656d 100644 --- a/app/books/sb/[canto]/[chapter]/page.tsx +++ b/app/books/sb/[canto]/[chapter]/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { PageHeader } from "@/components/page-header"; import { notFound } from "next/navigation"; -import { getBookMeta, getChapterVerses, getChapterVerseIndex } from "@/lib/api"; +import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api"; import { ChapterViewer } from "@/app/books/chapter-viewer"; type ChapterInfo = { @@ -19,11 +19,13 @@ type CantoInfo = { export default async function SbChapterPage({ params, + searchParams, }: { params: Promise<{ canto: string; chapter: string }>; + searchParams: Promise<{ verse?: string }>; }) { const { canto, chapter } = await params; - const PAGE_SIZE = 20; + const { verse: verseParam } = await searchParams; const meta = await getBookMeta("sb"); if (!meta) notFound(); @@ -37,12 +39,20 @@ export default async function SbChapterPage({ ); if (!chapterInfo) notFound(); - const [verseIndex, firstPage] = await Promise.all([ - getChapterVerseIndex("sb", canto, parseInt(chapter)), - getChapterVerses("sb", canto, parseInt(chapter), 1, PAGE_SIZE), - ]); + const verseIndex = await getChapterVerseIndex("sb", canto, parseInt(chapter)); if (verseIndex.length === 0) notFound(); + const requestedVerse = verseParam + ? parseInt(verseParam) + : verseIndex[0].verse; + const verse = await getSingleVerse( + "sb", + canto, + parseInt(chapter), + requestedVerse, + ); + if (!verse) notFound(); + return (
@@ -59,19 +69,17 @@ export default async function SbChapterPage({
({ verse: v.verse, verseEnd: v.verseEnd ?? undefined, reference: v.reference, }))} + book="sb" + division={canto} + chapter={parseInt(chapter)} + showBengali={false} />
diff --git a/lib/api.ts b/lib/api.ts index 633678d1..aa58efa2 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -154,6 +154,26 @@ export async function getChapterVerses( }; } +// --------------------------------------------------------------------------- +// Single verse +// --------------------------------------------------------------------------- + +/** + * Get a single verse by its exact coordinates. + */ +export async function getSingleVerse( + book: string, + division: string | null, + chapter: number, + verse: number, +): Promise { + const result = await prisma.verse.findFirst({ + where: { book, division, chapter, verse }, + }); + + return result as unknown as VerseRecord | null; +} + // --------------------------------------------------------------------------- // Glossary // ---------------------------------------------------------------------------