chore: refactor settings route

This commit is contained in:
gosangam
2026-07-14 18:16:32 +05:30
parent 1683abc2e9
commit 1582df0ed5
134 changed files with 677 additions and 463 deletions
@@ -0,0 +1,143 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
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 } from "@/lib/api";
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
type ChapterInfo = {
number: number;
title: string;
totalVerses: number;
};
type LilaInfo = {
key: string;
name: string;
totalChapters: number;
totalVerses: number;
chapters: ChapterInfo[];
};
export async function generateMetadata({
params,
searchParams,
}: {
params: Promise<{ lila: string; chapter: string; verse: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}): Promise<Metadata> {
const { lila, chapter, verse } = await params;
const sp = await searchParams;
const v = await getSingleVerse(
"cc",
lila,
parseInt(chapter),
parseInt(verse),
);
const paragraphs = v ? [v.translation, ...v.purport] : undefined;
const quote = extractQuoteFromHl(sp, paragraphs);
if (quote) {
const bookTitle = "Śrī Caitanya-caritāmṛta";
return buildOgMeta({
title: bookTitle,
detail: `${lila.charAt(0).toUpperCase() + lila.slice(1)}, Chapter ${chapter}, Verse ${verse}`,
quote,
});
}
return ogForVerse("cc", {
division: lila,
chapter: parseInt(chapter),
verse: parseInt(verse),
});
}
export default async function CcVersePage({
params,
}: {
params: Promise<{ lila: string; chapter: string; verse: string }>;
}) {
const { lila, chapter, verse } = await params;
const meta = await getBookMeta("cc");
if (!meta) notFound();
const lilas = meta.lilas as LilaInfo[] | undefined;
const lilaInfo = lilas?.find((l) => l.key === lila);
if (!lilaInfo) notFound();
const chapterInfo = lilaInfo.chapters.find(
(ch) => ch.number === parseInt(chapter),
);
if (!chapterInfo) notFound();
const verseIndex = await getChapterVerseIndex("cc", lila, parseInt(chapter));
if (verseIndex.length === 0) notFound();
const requestedVerse = parseInt(verse);
const verseData = await getSingleVerse(
"cc",
lila,
parseInt(chapter),
requestedVerse,
);
if (!verseData) notFound();
// Build mobile nav groups from verse index
const mobileNavGroups: MobileNavGroup[] = [];
if (verseIndex.length > 0) {
mobileNavGroups.push({
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/cc/${lila}/${chapter}/${v.verse}`,
type: "verse",
})),
});
}
return (
<div className="min-h-screen bg-background">
<PageHeader sticky>
<Link
href={`/books/cc/${lila}`}
className="text-sm text-muted-foreground hover:text-foreground"
>
&larr; {lilaInfo.name}
</Link>
<div className="mt-2 flex items-start justify-between gap-3">
<h1 className="text-2xl font-bold text-foreground">
Chapter {chapter}: {chapterInfo.title}
</h1>
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
</div>
</PageHeader>
<div className="max-w-5xl mx-auto flex">
<ChapterViewer
initialVerse={verseData}
initialVerseNum={requestedVerse}
verseIndex={verseIndex.map((v) => ({
verse: v.verse,
verseEnd: v.verseEnd ?? undefined,
reference: v.reference,
}))}
book="cc"
division={lila}
chapter={parseInt(chapter)}
showBengali={true}
basePath={`/books/cc/${lila}/${chapter}`}
mobileNavGroups={mobileNavGroups}
mobileNavBookTitle={`Chapter ${chapter}: ${chapterInfo.title}`}
/>
</div>
</div>
);
}
@@ -0,0 +1,227 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect, notFound } from "next/navigation";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { getBookMeta, getProseChapter, getChapterVerseIndex } from "@/lib/api";
import { CC_LILAS } from "@/lib/constants";
import { buildOgMeta } from "@/lib/og";
import { PageHeader } from "@/components/layout/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { FindInPage } from "@/components/layout/find-in-page";
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
import { cn } from "@/lib/utils";
type ChapterInfo = { number: number; title: string; totalVerses: number };
type LilaInfo = {
key: string;
name: string;
totalChapters: number;
totalVerses: number;
chapters: ChapterInfo[];
};
type VerseEntry = { verse: number; verseEnd?: number | null; reference: string };
export async function generateMetadata({
params,
}: {
params: Promise<{ lila: string; chapter: string }>;
}): Promise<Metadata> {
const { lila, chapter } = await params;
const lilaName = CC_LILAS[lila] || lila;
return buildOgMeta({
title: "Śrī Caitanya-caritāmṛta",
detail: `${lilaName}, Chapter ${chapter}`,
});
}
function verseLabel(v: VerseEntry): string {
return v.verseEnd ? `${v.verse}–${v.verseEnd}` : String(v.verse);
}
// ---------------------------------------------------------------------------
// Top/bottom navigation — mirrors the verse pages' VerseNav: a Contents
// toggle (mobile) listing this chapter's verses, Previous/Next (chapter), and
// a "Chapter X of N" label.
// ---------------------------------------------------------------------------
function ChapterNav({
lila,
chapters,
current,
chapterTitle,
verseIndex,
}: {
lila: string;
chapters: ChapterInfo[];
current: number;
chapterTitle: string;
verseIndex: VerseEntry[];
}) {
const idx = chapters.findIndex((c) => c.number === current);
const prev = idx > 0 ? chapters[idx - 1] : null;
const next = idx < chapters.length - 1 ? chapters[idx + 1] : null;
// The contents drawer lists the active chapter's verses (same as the
// verse-page sidebar) so the reader can jump straight in.
const groups: MobileNavGroup[] = [
{
title: "Verses",
items: verseIndex.map((v) => ({
slug: String(v.verse),
label: `Verse ${verseLabel(v)}`,
href: `/books/cc/${lila}/${current}/${v.verse}`,
type: "verse",
})),
},
];
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={groups}
currentSlug=""
bookTitle={`Chapter ${current}: ${chapterTitle}`}
/>
{prev ? (
<Link
href={`/books/cc/${lila}/${prev.number}`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "gap-1.5")}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline">Previous</span>
</Link>
) : (
<div className="hidden lg:block" />
)}
</div>
<span className="text-sm text-muted-foreground">
Chapter {current} of {chapters.length}
</span>
{next ? (
<Link
href={`/books/cc/${lila}/${next.number}`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "gap-1.5")}
>
<span className="hidden sm:inline">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
export default async function CcChapterPage({
params,
}: {
params: Promise<{ lila: string; chapter: string }>;
}) {
const { lila, chapter } = await params;
const chapterNum = parseInt(chapter);
const [meta, summary, verseIndex] = await Promise.all([
getBookMeta("cc"),
getProseChapter("cc", 0, `${lila}-${chapterNum}`),
getChapterVerseIndex("cc", lila, chapterNum),
]);
if (!meta) notFound();
const lilaInfo = (meta.lilas as LilaInfo[] | undefined)?.find(
(l) => l.key === lila,
);
if (!lilaInfo) notFound();
const chapterInfo = lilaInfo.chapters.find((ch) => ch.number === chapterNum);
if (!chapterInfo) notFound();
if (verseIndex.length === 0) notFound();
const firstVerse = verseIndex[0].verse;
// Chapters without a summary (e.g. Ādi 7) go straight to the verses.
if (!summary || summary.paragraphs.length === 0) {
redirect(`/books/cc/${lila}/${chapterNum}/${firstVerse}`);
}
return (
<div className="min-h-screen bg-background">
<PageHeader sticky>
<Link
href={`/books/cc/${lila}`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; {lilaInfo.name}
</Link>
<div className="mt-3 flex items-start justify-between gap-3">
<h1 className="text-2xl font-bold text-foreground">
Chapter {chapterNum}: {chapterInfo.title}
</h1>
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
</div>
</PageHeader>
<div className="max-w-5xl mx-auto flex">
{/* Desktop sidebar — verses of the active chapter (matches the
verse-page sidebar) so the reader can jump to any verse. */}
<aside className="hidden lg:block w-64 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-165px)] overflow-y-auto py-8 pr-4">
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Verses
</h2>
<nav className="grid grid-cols-3 gap-1">
{verseIndex.map((v) => (
<Link
key={v.verse}
href={`/books/cc/${lila}/${chapterNum}/${v.verse}`}
className="px-2 py-1 rounded-md text-sm tabular-nums text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
>
{verseLabel(v)}
</Link>
))}
</nav>
</aside>
{/* Main — chapter summary */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-6">
{/* Top navigation */}
<ChapterNav
lila={lila}
chapters={lilaInfo.chapters}
current={chapterNum}
chapterTitle={chapterInfo.title}
verseIndex={verseIndex}
/>
<p className="text-sm font-bold uppercase tracking-wider text-muted-foreground">
Chapter Summary
</p>
<Card id="book-content">
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{summary.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<ChapterNav
lila={lila}
chapters={lilaInfo.chapters}
current={chapterNum}
chapterTitle={chapterInfo.title}
verseIndex={verseIndex}
/>
</main>
</div>
</div>
);
}