Files
spsays-training/app/(app)/books/bs/[chapter]/[verse]/page.tsx
T

220 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from "next";
import fs from "fs";
import path from "path";
import Link from "next/link";
import { PageHeader } from "@/components/layout/page-header";
import { FindInPage } from "@/components/layout/find-in-page";
import { notFound } from "next/navigation";
import {
getBookMeta,
getSingleVerse,
getChapterVerseIndex,
type VerseIndexEntry,
type VerseRecord,
} from "@/lib/api";
import { ChapterViewer } from "@/app/(app)/books/chapter-viewer";
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
export const dynamic = "force-dynamic";
type BsMeta = {
book: string;
title: string;
author: string;
totalChapters: number;
chapters: { number: number; title: string; totalVerses: number }[];
};
const BS_JSON_DIR = path.join(process.cwd(), "lib", "jsons", "books", "bs");
// Fallback functions that read from local JSON when DB is empty
async function getMetaWithFallback() {
const meta = await getBookMeta("bs");
if (meta) return meta as unknown as BsMeta;
const introPath = path.join(BS_JSON_DIR, "intro.json");
if (!fs.existsSync(introPath)) return null;
try {
return JSON.parse(fs.readFileSync(introPath, "utf-8")) as BsMeta;
} catch {
return null;
}
}
async function getVerseIndexWithFallback(
chapter: number,
): Promise<VerseIndexEntry[]> {
const index = await getChapterVerseIndex("bs", null, chapter);
if (index.length > 0) return index;
// Fallback: read verse files from local JSON
const versesDir = path.join(
BS_JSON_DIR,
"chapters",
String(chapter).padStart(2, "0"),
"verses",
);
if (!fs.existsSync(versesDir)) return [] as VerseIndexEntry[];
const files = fs
.readdirSync(versesDir)
.filter((f) => f.endsWith(".json"))
.sort();
return files.map((f): VerseIndexEntry => {
const v = JSON.parse(fs.readFileSync(path.join(versesDir, f), "utf-8"));
return {
verse: v.verse,
verseEnd: v.verse_end || null,
reference: v.reference,
};
});
}
async function getVerseWithFallback(
chapter: number,
verse: number,
): Promise<VerseRecord | null> {
const v = await getSingleVerse("bs", null, chapter, verse);
if (v) return v;
// Fallback: read from local JSON
const versesDir = path.join(
BS_JSON_DIR,
"chapters",
String(chapter).padStart(2, "0"),
"verses",
);
if (!fs.existsSync(versesDir)) return null;
const files = fs
.readdirSync(versesDir)
.filter((f) => f.endsWith(".json"))
.sort();
for (const f of files) {
const data = JSON.parse(fs.readFileSync(path.join(versesDir, f), "utf-8"));
if (data.verse === verse) {
return {
id: `bs-${chapter}-${verse}`,
book: "bs",
division: null,
chapter: data.chapter,
verse: data.verse,
verseEnd: data.verse_end || null,
reference: data.reference,
bookTitle: data.book_title,
devanagari: data.devanagari,
bengali: null,
transliteration: data.transliteration,
synonyms: data.synonyms,
translation: data.translation,
purport: data.purport,
};
}
}
return null;
}
export async function generateMetadata({
params,
searchParams,
}: {
params: Promise<{ chapter: string; verse: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}): Promise<Metadata> {
const { chapter, verse } = await params;
const sp = await searchParams;
const v = await getVerseWithFallback(parseInt(chapter), parseInt(verse));
const paragraphs = v ? [v.translation, ...v.purport] : undefined;
const quote = extractQuoteFromHl(sp, paragraphs);
if (quote) {
return buildOgMeta({
title: "Śrī Brahma-saṁhitā",
detail: `Chapter ${chapter}, Verse ${parseInt(verse)}`,
quote,
});
}
return ogForVerse("bs", {
chapter: parseInt(chapter),
verse: parseInt(verse),
});
}
export default async function VersePage({
params,
}: {
params: Promise<{ chapter: string; verse: string }>;
}) {
const { chapter, verse: verseParam } = await params;
const bsMeta = await getMetaWithFallback();
if (!bsMeta) notFound();
const chapterInfo = bsMeta.chapters?.find(
(c) => c.number === parseInt(chapter),
);
if (!chapterInfo?.title) notFound();
const title: string = chapterInfo.title;
const verseIndex = await getVerseIndexWithFallback(parseInt(chapter));
if (verseIndex.length === 0) notFound();
const requestedVerse = parseInt(verseParam);
const verse = await getVerseWithFallback(parseInt(chapter), requestedVerse);
if (!verse) notFound();
// Build mobile nav groups — verses for the current chapter
const mobileNavGroups: MobileNavGroup[] = [
{
title: "Verses",
items: verseIndex.map((v) => ({
slug: String(v.verse),
label: v.verseEnd
? `Verse ${chapter}.${v.verse}–${v.verseEnd}`
: `Verse ${chapter}.${v.verse}`,
href: `/books/bs/${chapter}/${v.verse}`,
type: "verse",
})),
},
];
return (
<div className="min-h-screen bg-background">
<PageHeader sticky>
<Link
href="/books/bs"
className="text-sm text-muted-foreground hover:text-foreground"
>
&larr; Śrī Brahma-saṁhitā
</Link>
<div className="mt-2 flex items-start justify-between gap-3">
<h1 className="text-2xl font-bold text-foreground">
Chapter {chapter}: {title}
</h1>
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
</div>
</PageHeader>
<div className="max-w-5xl mx-auto flex">
<ChapterViewer
initialVerse={verse}
initialVerseNum={requestedVerse}
verseIndex={verseIndex.map((v) => ({
verse: v.verse,
verseEnd: v.verseEnd ?? undefined,
reference: v.reference,
}))}
book="bs"
division={null}
chapter={parseInt(chapter)}
basePath={`/books/bs/${chapter}`}
showBengali={false}
mobileNavGroups={mobileNavGroups}
mobileNavBookTitle={`Chapter ${chapter}: ${title}`}
/>
</div>
</div>
);
}