feat: add policy pages
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
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/mobile-nav";
|
||||
import type { Metadata } from "next";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
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: "Mukunda-mālā-stotra",
|
||||
detail: `Chapter ${chapter}, Verse ${verse}`,
|
||||
quote,
|
||||
});
|
||||
}
|
||||
return ogForVerse("mm", {
|
||||
chapter: parseInt(chapter),
|
||||
verse: parseInt(verse),
|
||||
});
|
||||
}
|
||||
|
||||
type MmMeta = {
|
||||
book: string;
|
||||
title: string;
|
||||
author: string;
|
||||
totalChapters: number;
|
||||
chapters: { number: number; title: string; totalVerses: number }[];
|
||||
};
|
||||
|
||||
const MM_JSON_DIR = path.join(process.cwd(), "lib", "jsons", "books", "mm");
|
||||
|
||||
// Fallback functions that read from local JSON when DB is empty
|
||||
async function getMetaWithFallback() {
|
||||
const meta = await getBookMeta("mm");
|
||||
if (meta) return meta as unknown as MmMeta;
|
||||
|
||||
const introPath = path.join(MM_JSON_DIR, "intro.json");
|
||||
if (!fs.existsSync(introPath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(introPath, "utf-8")) as MmMeta;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerseIndexWithFallback(
|
||||
chapter: number,
|
||||
): Promise<VerseIndexEntry[]> {
|
||||
const index = await getChapterVerseIndex("mm", null, chapter);
|
||||
if (index.length > 0) return index;
|
||||
|
||||
// Fallback: read verse files from local JSON
|
||||
const versesDir = path.join(
|
||||
MM_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("mm", null, chapter, verse);
|
||||
if (v) return v;
|
||||
|
||||
// Fallback: read from local JSON
|
||||
const versesDir = path.join(
|
||||
MM_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: `mm-${chapter}-${verse}`,
|
||||
book: "mm",
|
||||
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 default async function VersePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ chapter: string; verse: string }>;
|
||||
}) {
|
||||
const { chapter, verse: verseParam } = await params;
|
||||
|
||||
const mmMeta = await getMetaWithFallback();
|
||||
if (!mmMeta) notFound();
|
||||
|
||||
const chapterInfo = mmMeta.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/mm/${chapter}/${v.verse}`,
|
||||
type: "verse",
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/mm"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
← Mukunda-mālā-stotra
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
</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="mm"
|
||||
division={null}
|
||||
chapter={parseInt(chapter)}
|
||||
basePath={`/books/mm/${chapter}`}
|
||||
showBengali={false}
|
||||
mobileNavGroups={mobileNavGroups}
|
||||
mobileNavBookTitle={`Chapter ${chapter}: ${title}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getChapterVerseIndex } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import Link from "next/link";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const MM_JSON_DIR = path.join(process.cwd(), "lib", "jsons", "books", "mm");
|
||||
|
||||
type IntroSegment = {
|
||||
slug: string;
|
||||
title: string;
|
||||
paragraphs: string[];
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ chapter: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { chapter } = await params;
|
||||
return buildOgMeta({ title: BOOK_NAMES["mm"], detail: `Chapter ${chapter}` });
|
||||
}
|
||||
|
||||
export default async function MmChapterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ chapter: string }>;
|
||||
}) {
|
||||
const { chapter } = await params;
|
||||
|
||||
// Handle intro segments (intro-introduction)
|
||||
if (chapter.startsWith("intro-")) {
|
||||
const rawSlug = chapter.replace(/^intro-/, "");
|
||||
const filePath = path.join(MM_JSON_DIR, "intro", `${rawSlug}.json`);
|
||||
|
||||
if (!fs.existsSync(filePath)) notFound();
|
||||
|
||||
let segment: IntroSegment;
|
||||
try {
|
||||
segment = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/mm"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Mukunda-mālā-stotra
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card>
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph: string, i: number) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle numeric chapters — redirect to first verse
|
||||
const chapterNum = parseInt(chapter);
|
||||
if (isNaN(chapterNum)) notFound();
|
||||
|
||||
// Try DB first
|
||||
const verseIndex = await getChapterVerseIndex("mm", null, chapterNum);
|
||||
|
||||
// Fallback to local JSON
|
||||
if (verseIndex.length === 0) {
|
||||
const versesDir = path.join(
|
||||
MM_JSON_DIR,
|
||||
"chapters",
|
||||
String(chapterNum).padStart(2, "0"),
|
||||
"verses",
|
||||
);
|
||||
if (fs.existsSync(versesDir)) {
|
||||
const files = fs
|
||||
.readdirSync(versesDir)
|
||||
.filter((f: string) => f.endsWith(".json"))
|
||||
.sort();
|
||||
if (files.length > 0) {
|
||||
const first = JSON.parse(
|
||||
fs.readFileSync(path.join(versesDir, files[0]), "utf-8"),
|
||||
);
|
||||
redirect(`/books/mm/${chapter}/${first.verse}`);
|
||||
}
|
||||
}
|
||||
notFound();
|
||||
}
|
||||
|
||||
redirect(`/books/mm/${chapter}/${verseIndex[0].verse}`);
|
||||
}
|
||||
Reference in New Issue
Block a user