feat: add ssr

This commit is contained in:
gosangam
2026-04-17 22:51:58 +05:30
parent d88ababd76
commit 0aab1d23ec
97 changed files with 29891 additions and 68 deletions
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getBbdContent,
getBbdSidebarItems,
type BbdChapter,
type BbdSidebarItem,
} from "@/lib/bbd";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function BbdChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getBbdContent(slug);
if (!content) notFound();
const sidebarItems = await getBbdSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as BbdChapter).chapter === "number" &&
(content as BbdChapter).chapter !== null;
const chapterNum = isChapter ? (content as BbdChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/bbd"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Beyond Birth and Death
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<BbdSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<BbdNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<BbdNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function BbdSidebar({
items,
currentSlug,
}: {
items: BbdSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: BbdSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/bbd/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function BbdNav({
prev,
next,
}: {
prev: BbdSidebarItem | null;
next: BbdSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/bbd/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/bbd/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getBbdMeta, getBbdIntroSegments } from "@/lib/bbd";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function BbdPage() {
const meta = await getBbdMeta();
if (!meta) notFound();
const introSegments = await getBbdIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/bbd/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/bbd/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+146
View File
@@ -0,0 +1,146 @@
"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { Search } from "lucide-react";
import { ExtractedVerseCount } from "./extracted-verse-count";
export type BookInfo = {
slug: string;
title: string;
author: string;
description: string;
contentDescription: string;
totalVerses: number;
href: string;
};
/**
* Strip diacritics / accents so that "Krsna" matches "Kṛṣṇa",
* "Gita" matches "Gītā", "Srimad" matches "Śrīmad", etc.
*
* Uses Unicode NFD decomposition to split base characters from
* combining marks, then removes the marks. Also handles special
* IAST characters that don't decompose cleanly via NFD.
*/
function normalize(text: string): string {
return (
text
.toLowerCase()
// Handle IAST characters that NFD doesn't decompose into ASCII base
.replace(/ṛ/g, "r")
.replace(/ṝ/g, "r")
.replace(/ṣ/g, "s")
.replace(/ś/g, "s")
.replace(/ṭ/g, "t")
.replace(/ḍ/g, "d")
.replace(/ṇ/g, "n")
.replace(/ñ/g, "n")
.replace(/ṅ/g, "n")
.replace(/ḥ/g, "h")
.replace(/ṁ/g, "m")
.replace(/ṃ/g, "m")
.replace(/ḷ/g, "l")
// NFD decompose + strip combining marks (handles ā→a, ī→i, ū→u, etc.)
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
);
}
export function BooksGrid({ books }: { books: BookInfo[] }) {
const [query, setQuery] = useState("");
const normalizedBooks = useMemo(
() =>
books.map((book) => ({
book,
searchText: normalize(
`${book.title} ${book.contentDescription} ${book.description} ${book.slug}`,
),
})),
[books],
);
const filtered = useMemo(() => {
if (!query.trim()) return books;
const q = normalize(query.trim());
return normalizedBooks
.filter((entry) => entry.searchText.includes(q))
.map((entry) => entry.book);
}, [books, normalizedBooks, query]);
return (
<div className="space-y-6">
{/* Search input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search books..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* Results count when searching */}
{query.trim() && (
<p className="text-sm text-muted-foreground">
{filtered.length === 0
? "No books found"
: `${filtered.length} book${filtered.length === 1 ? "" : "s"} found`}
</p>
)}
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filtered.map((book) => (
<Link key={book.slug} href={book.href} className="block h-full">
<Card
className={cn(
"h-full transition-all hover:shadow-md hover:ring-foreground/20",
)}
>
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold leading-snug">
{book.title}
</CardTitle>
{book.contentDescription && (
<CardDescription className="text-xs line-clamp-2">
{book.contentDescription}
</CardDescription>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
{book.description && (
<Badge variant="secondary" className="text-xs">
{book.description}
</Badge>
)}
{book.totalVerses > 0 && (
<ExtractedVerseCount
count={book.totalVerses}
label="verses"
className="text-muted-foreground text-xs"
/>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}
+1 -1
View File
@@ -148,7 +148,7 @@ function VerseCard({
{/* Devanagari */}
<div className="text-center">
<p className="text-xl leading-relaxed text-red-800 dark:text-red-400 whitespace-pre-line font-serif">
<p className="text-xl leading-relaxed text-red-800 dark:text-white whitespace-pre-line font-serif">
{verse.devanagari}
</p>
</div>
+250
View File
@@ -0,0 +1,250 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getDsContent,
getDsSidebarItems,
type DsChapter,
type DsSidebarItem,
} from "@/lib/ds";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function DsChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getDsContent(slug);
if (!content) notFound();
const sidebarItems = await getDsSidebarItems();
// Build a flat list of navigable items (skip "parent" type — they're labels, not pages)
const navigableItems = sidebarItems.filter((item) => item.type !== "parent");
const currentIdx = navigableItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? navigableItems[currentIdx - 1] : null;
const next =
currentIdx < navigableItems.length - 1
? navigableItems[currentIdx + 1]
: null;
// Determine if this is a sub-chapter (has parentChapter and is type "chapter"/"sub-chapter")
const isSubChapter =
"parentChapter" in content &&
typeof (content as DsChapter).parentChapter === "number";
const parentChapterNum = isSubChapter
? (content as DsChapter).parentChapter
: null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ds"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Dialectic Spiritualism
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{parentChapterNum != null && (
<span className="text-primary">Chapter {parentChapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<DsSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<DsNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<DsNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function DsSidebar({
items,
currentSlug,
}: {
items: DsSidebarItem[];
currentSlug: string;
}) {
// Split items by section
const introItems = items.filter((i) => i.type === "intro-segment");
const parents = items.filter((i) => i.type === "parent");
const subChapters = items.filter((i) => i.type === "sub-chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters — grouped by parent */}
{parents.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{parents.map((parent) => (
<div key={parent.slug}>
{/* Parent heading — not a link, just a bold label */}
<div className="px-2 py-1.5 text-xs font-bold text-muted-foreground mt-3 first:mt-0">
{parent.label}
</div>
{/* Sub-chapters indented */}
{subChapters
.filter((sub) => sub.parentChapter === parent.parentChapter)
.map((sub) => {
const isActive = sub.slug === currentSlug;
return (
<Link
key={sub.slug}
href={`/books/ds/${sub.slug}`}
className={cn(
"pl-4 px-2 py-1.5 rounded-md text-sm transition-colors truncate block",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={sub.label}
>
{sub.label}
</Link>
);
})}
</div>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: DsSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ds/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function DsNav({
prev,
next,
}: {
prev: DsSidebarItem | null;
next: DsSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ds/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ds/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getDsMeta, getDsIntroSegments } from "@/lib/ds";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function DsPage() {
const meta = await getDsMeta();
if (!meta) notFound();
const introSegments = await getDsIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
{meta.subtitle && (
<p className="mt-1 text-lg text-muted-foreground">{meta.subtitle}</p>
)}
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ds/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters — hierarchical parent → sub-chapter structure */}
{meta.chapters.map((ch) => (
<div key={ch.number}>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
{ch.number}. {ch.title}
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{ch.subChapters.map((sub) => (
<li key={sub.slug}>
<Link
href={`/books/ds/${sub.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<p className="font-medium text-foreground">
{sub.title}
</p>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
))}
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getEjContent,
getEjSidebarItems,
type EjChapter,
type EjSidebarItem,
} from "@/lib/ej";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function EjChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getEjContent(slug);
if (!content) notFound();
const sidebarItems = await getEjSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as EjChapter).chapter === "number" &&
(content as EjChapter).chapter !== null;
const chapterNum = isChapter ? (content as EjChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ej"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Easy Journey to Other Planets
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<EjSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<EjNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<EjNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function EjSidebar({
items,
currentSlug,
}: {
items: EjSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: EjSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ej/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function EjNav({
prev,
next,
}: {
prev: EjSidebarItem | null;
next: EjSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ej/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ej/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+103
View File
@@ -0,0 +1,103 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getEjMeta, getEjIntroSegments } from "@/lib/ej";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function EjPage() {
const meta = await getEjMeta();
if (!meta) notFound();
const introSegments = await getEjIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-sm text-muted-foreground italic">
{meta.subtitle}
</p>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ej/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/ej/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getEkContent,
getEkSidebarItems,
type EkChapter,
type EkSidebarItem,
} from "@/lib/ek";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function EkChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getEkContent(slug);
if (!content) notFound();
const sidebarItems = await getEkSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as EkChapter).chapter === "number" &&
(content as EkChapter).chapter !== null;
const chapterNum = isChapter ? (content as EkChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ek"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Elevation to Kṛṣṇa Consciousness
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<EkSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<EkNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<EkNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function EkSidebar({
items,
currentSlug,
}: {
items: EkSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: EkSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ek/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function EkNav({
prev,
next,
}: {
prev: EkSidebarItem | null;
next: EkSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ek/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ek/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getEkMeta, getEkIntroSegments } from "@/lib/ek";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function EkPage() {
const meta = await getEkMeta();
if (!meta) notFound();
const introSegments = await getEkIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ek/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/ek/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getHkhContent,
getHkhSidebarItems,
type HkhChapter,
type HkhSidebarItem,
} from "@/lib/hkh";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function HkhChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getHkhContent(slug);
if (!content) notFound();
const sidebarItems = await getHkhSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as HkhChapter).chapter === "number" &&
(content as HkhChapter).chapter !== null;
const chapterNum = isChapter ? (content as HkhChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/hkh"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; The Kṛṣṇa Consciousness Handbook
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<HkhSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<HkhNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<HkhNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function HkhSidebar({
items,
currentSlug,
}: {
items: HkhSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: HkhSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/hkh/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function HkhNav({
prev,
next,
}: {
prev: HkhSidebarItem | null;
next: HkhSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/hkh/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/hkh/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getHkhMeta, getHkhIntroSegments } from "@/lib/hkh";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function HkhPage() {
const meta = await getHkhMeta();
if (!meta) notFound();
const introSegments = await getHkhIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/hkh/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/hkh/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+345
View File
@@ -0,0 +1,345 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getIsoContent,
getIsoSidebarItems,
type IsoVerse,
type IsoIntroSegment,
type IsoSidebarItem,
} from "@/lib/iso";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Type guards
// ---------------------------------------------------------------------------
function isVerse(content: IsoVerse | IsoIntroSegment): content is IsoVerse {
return "mantra" in content && "devanagari" in content;
}
function isIntroSegment(
content: IsoVerse | IsoIntroSegment,
): content is IsoIntroSegment {
return "paragraphs" in content && "order" in content;
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function IsoMantraPage({
params,
}: {
params: Promise<{ mantra: string }>;
}) {
const { mantra: slug } = await params;
const content = await getIsoContent(slug);
if (!content) notFound();
const sidebarItems = await getIsoSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title for the header
const verse = isVerse(content) ? content : null;
const intro = isIntroSegment(content) ? content : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/iso"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Śrī Īśopaniṣad
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{verse && (
<>
<span className="text-primary">{verse.reference}: </span>
{verse.title}
</>
)}
{intro && intro.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<IsoSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<IsoNav prev={prev} next={next} />
{/* Content */}
{verse && <VerseContent verse={verse} />}
{intro && <IntroContent intro={intro} />}
{/* Bottom navigation */}
<IsoNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Verse content (Devanagari, transliteration, synonyms, translation, purport)
// ---------------------------------------------------------------------------
function VerseContent({ verse }: { verse: IsoVerse }) {
return (
<Card>
<CardContent className="py-8 px-6 sm:px-8 space-y-8">
{/* Devanagari */}
<section>
<div className="text-xl leading-relaxed text-foreground whitespace-pre-line font-serif text-center">
{verse.devanagari}
</div>
</section>
{/* Transliteration */}
<section>
<div className="text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
{verse.transliteration}
</div>
</section>
{/* Synonyms */}
{verse.synonyms.length > 0 && (
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Synonyms
</h2>
<div className="text-sm text-foreground/70 leading-relaxed">
{verse.synonyms.map((s, i) => (
<span key={i}>
{i > 0 && "; "}
<span className="font-medium text-foreground/90">
{s.word}
</span>
{"—"}
{s.meanings.join(", ")}
</span>
))}
</div>
</section>
)}
{/* Translation */}
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Translation
</h2>
<div
className="text-foreground font-medium leading-relaxed"
dangerouslySetInnerHTML={{ __html: verse.translation }}
/>
</section>
{/* Purport */}
{verse.purport.length > 0 && (
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Purport
</h2>
<div className="space-y-4">
{verse.purport.map((p, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: p }}
/>
))}
</div>
</section>
)}
</CardContent>
</Card>
);
}
// ---------------------------------------------------------------------------
// Intro segment content (paragraphs)
// ---------------------------------------------------------------------------
function IntroContent({ intro }: { intro: IsoIntroSegment }) {
return (
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{intro.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function IsoSidebar({
items,
currentSlug,
}: {
items: IsoSidebarItem[];
currentSlug: string;
}) {
// Front matter items (front-pages + introduction)
const frontMatterItems = items.filter(
(item) =>
item.slug === "intro-front-pages" || item.slug === "intro-introduction",
);
// Mantras = invocation + 1-18
const mantraItems = items.filter((item) => item.type === "verse");
// about-the-author comes last
const aboutAuthorItem = items.find(
(item) => item.slug === "intro-about-the-author",
);
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{frontMatterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{frontMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Mantras */}
{mantraItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Mantras
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{mantraItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* About the Author */}
{aboutAuthorItem && (
<nav className="flex flex-col gap-0.5">
<SidebarLink
item={aboutAuthorItem}
isActive={aboutAuthorItem.slug === currentSlug}
/>
</nav>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: IsoSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/iso/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function IsoNav({
prev,
next,
}: {
prev: IsoSidebarItem | null;
next: IsoSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/iso/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "verse" && prev.mantra != null && prev.mantra > 0
? `Mantra ${prev.mantra}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/iso/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "verse" && next.mantra != null && next.mantra > 0
? `Mantra ${next.mantra}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+173
View File
@@ -0,0 +1,173 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getIsoMeta, getIsoIntroSegments } from "@/lib/iso";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function IsoPage() {
const meta = await getIsoMeta();
if (!meta) notFound();
const introSegments = await getIsoIntroSegments();
const frontPages = introSegments.find((s) => s.slug === "front-pages");
const introduction = introSegments.find((s) => s.slug === "introduction");
const aboutAuthor = introSegments.find((s) => s.slug === "about-the-author");
// Invocation is mantra 0, mantras are 1-18
const invocation = meta.mantras.find((m) => m.number === 0);
const numberedMantras = meta.mantras.filter((m) => m.number > 0);
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{(frontPages || introduction) && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{frontPages && (
<li>
<Link
href="/books/iso/intro-front-pages"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{frontPages.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{frontPages.paragraphs.length} paragraphs
</span>
</Link>
</li>
)}
{introduction && (
<li>
<Link
href="/books/iso/intro-introduction"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{introduction.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{introduction.paragraphs.length} paragraphs
</span>
</Link>
</li>
)}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Mantras */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Mantras
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{/* Invocation */}
{invocation && (
<li>
<Link
href="/books/iso/invocation"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Invocation
</span>
<p className="font-medium text-foreground">
{invocation.title}
</p>
</div>
</Link>
</li>
)}
{/* Mantras 1-18 */}
{numberedMantras.map((m) => (
<li key={m.number}>
<Link
href={`/books/iso/${m.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Mantra {m.number}
</span>
<p className="font-medium text-foreground">{m.title}</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
{/* About the Author */}
{aboutAuthor && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
About the Author
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
<li>
<Link
href="/books/iso/intro-about-the-author"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{aboutAuthor.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{aboutAuthor.paragraphs.length} paragraphs
</span>
</Link>
</li>
</ul>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getMgContent,
getMgSidebarItems,
type MgChapter,
type MgSidebarItem,
} from "@/lib/mg";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function MgChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getMgContent(slug);
if (!content) notFound();
const sidebarItems = await getMgSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as MgChapter).chapter === "number" &&
(content as MgChapter).chapter !== null;
const chapterNum = isChapter ? (content as MgChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/mg"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Kṛṣṇa Consciousness: The Matchless Gift
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<MgSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<MgNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<MgNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function MgSidebar({
items,
currentSlug,
}: {
items: MgSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: MgSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/mg/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function MgNav({
prev,
next,
}: {
prev: MgSidebarItem | null;
next: MgSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/mg/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/mg/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getMgMeta, getMgIntroSegments } from "@/lib/mg";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function MgPage() {
const meta = await getMgMeta();
if (!meta) notFound();
const introSegments = await getMgIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/mg/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/mg/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+247
View File
@@ -0,0 +1,247 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getNodContent,
getNodSidebarItems,
type NodChapter,
type NodSidebarItem,
} from "@/lib/nod";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function NodChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getNodContent(slug);
if (!content) notFound();
const sidebarItems = await getNodSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as NodChapter).chapter === "number" &&
(content as NodChapter).chapter !== null;
const chapterNum = isChapter ? (content as NodChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/nod"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; The Nectar of Devotion
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<NodSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<NodNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<NodNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function NodSidebar({
items,
currentSlug,
}: {
items: NodSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
const conclusionItems = items.filter((item) => item.type === "conclusion");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Concluding Words */}
{conclusionItems.length > 0 && (
<nav className="flex flex-col gap-0.5">
{conclusionItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: NodSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
return (
<Link
href={`/books/nod/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={label}
>
{label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function NodNav({
prev,
next,
}: {
prev: NodSidebarItem | null;
next: NodSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/nod/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/nod/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+143
View File
@@ -0,0 +1,143 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getNodMeta, getNodIntroSegments } from "@/lib/nod";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
const PART_LABELS: Record<number, string> = {
1: "Part One",
2: "Part Two",
3: "Part Three",
4: "Part Four",
};
export default async function NodPage() {
const meta = await getNodMeta();
if (!meta) notFound();
const introSegments = await getNodIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-sm text-muted-foreground italic">
{meta.subtitle}
</p>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/nod/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Parts & Chapters */}
{meta.parts.map((part) => {
const partChapters = part.chapters
.map((num) => meta.chapters.find((ch) => ch.number === num))
.filter(Boolean) as { number: number; title: string }[];
return (
<div key={part.number}>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
{PART_LABELS[part.number] ?? `Part ${part.number}`}
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{partChapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/nod/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
);
})}
{/* Concluding Words */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Conclusion
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
<li>
<Link
href="/books/nod/concluding-words"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
Concluding Words
</p>
</div>
</Link>
</li>
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+345
View File
@@ -0,0 +1,345 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getNoiContent,
getNoiSidebarItems,
type NoiVerse,
type NoiIntroSegment,
type NoiSidebarItem,
} from "@/lib/noi";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Type guards
// ---------------------------------------------------------------------------
function isVerse(content: NoiVerse | NoiIntroSegment): content is NoiVerse {
return "text" in content && "devanagari" in content;
}
function isIntroSegment(
content: NoiVerse | NoiIntroSegment,
): content is NoiIntroSegment {
return "paragraphs" in content && "order" in content;
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function NoiTextPage({
params,
}: {
params: Promise<{ text: string }>;
}) {
const { text: slug } = await params;
const content = await getNoiContent(slug);
if (!content) notFound();
const sidebarItems = await getNoiSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title for the header
const verse = isVerse(content) ? content : null;
const intro = isIntroSegment(content) ? content : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/noi"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; The Nectar of Instruction
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{verse && (
<>
<span className="text-primary">Text {verse.text}: </span>
{verse.title}
</>
)}
{intro && intro.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<NoiSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<NoiNav prev={prev} next={next} />
{/* Content */}
{verse && <VerseContent verse={verse} />}
{intro && <IntroContent intro={intro} />}
{/* Bottom navigation */}
<NoiNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Verse content (Devanagari, transliteration, synonyms, translation, purport)
// ---------------------------------------------------------------------------
function VerseContent({ verse }: { verse: NoiVerse }) {
return (
<Card>
<CardContent className="py-8 px-6 sm:px-8 space-y-8">
{/* Devanagari */}
<section>
<div className="text-xl leading-relaxed text-foreground whitespace-pre-line font-serif text-center">
{verse.devanagari}
</div>
</section>
{/* Transliteration */}
<section>
<div className="text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
{verse.transliteration}
</div>
</section>
{/* Synonyms */}
{verse.synonyms.length > 0 && (
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Synonyms
</h2>
<div className="text-sm text-foreground/70 leading-relaxed">
{verse.synonyms.map((s, i) => (
<span key={i}>
{i > 0 && "; "}
<span className="font-medium text-foreground/90">
{s.word}
</span>
{"—"}
{s.meanings.join(", ")}
</span>
))}
</div>
</section>
)}
{/* Translation */}
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Translation
</h2>
<div
className="text-foreground font-medium leading-relaxed"
dangerouslySetInnerHTML={{ __html: verse.translation }}
/>
</section>
{/* Purport */}
{verse.purport.length > 0 && (
<section>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
Purport
</h2>
<div className="space-y-4">
{verse.purport.map((p, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: p }}
/>
))}
</div>
</section>
)}
</CardContent>
</Card>
);
}
// ---------------------------------------------------------------------------
// Intro segment content (paragraphs)
// ---------------------------------------------------------------------------
function IntroContent({ intro }: { intro: NoiIntroSegment }) {
return (
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{intro.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function NoiSidebar({
items,
currentSlug,
}: {
items: NoiSidebarItem[];
currentSlug: string;
}) {
// Front matter items (front-pages + preface)
const frontMatterItems = items.filter(
(item) =>
item.slug === "intro-front-pages" || item.slug === "intro-preface",
);
// Texts = 1-11
const textItems = items.filter((item) => item.type === "verse");
// Appendixes comes last
const appendixesItem = items.find(
(item) => item.slug === "intro-appendixes",
);
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{frontMatterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{frontMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Texts */}
{textItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Texts
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{textItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Appendixes */}
{appendixesItem && (
<nav className="flex flex-col gap-0.5">
<SidebarLink
item={appendixesItem}
isActive={appendixesItem.slug === currentSlug}
/>
</nav>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: NoiSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/noi/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function NoiNav({
prev,
next,
}: {
prev: NoiSidebarItem | null;
next: NoiSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/noi/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "verse" && prev.text != null
? `Text ${prev.text}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/noi/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "verse" && next.text != null
? `Text ${next.text}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+150
View File
@@ -0,0 +1,150 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getNoiMeta, getNoiIntroSegments } from "@/lib/noi";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function NoiPage() {
const meta = await getNoiMeta();
if (!meta) notFound();
const introSegments = await getNoiIntroSegments();
const frontPages = introSegments.find((s) => s.slug === "front-pages");
const preface = introSegments.find((s) => s.slug === "preface");
const appendixes = introSegments.find((s) => s.slug === "appendixes");
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 italic text-muted-foreground">{meta.subtitle}</p>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{(frontPages || preface) && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{frontPages && (
<li>
<Link
href="/books/noi/intro-front-pages"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{frontPages.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{frontPages.paragraphs.length} paragraphs
</span>
</Link>
</li>
)}
{preface && (
<li>
<Link
href="/books/noi/intro-preface"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{preface.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{preface.paragraphs.length} paragraphs
</span>
</Link>
</li>
)}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Texts */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Texts
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.texts.map((t) => (
<li key={t.number}>
<Link
href={`/books/noi/${t.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Text {t.number}
</span>
<p className="font-medium text-foreground">{t.title}</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
{/* Appendixes */}
{appendixes && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Appendixes
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
<li>
<Link
href="/books/noi/intro-appendixes"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{appendixes.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{appendixes.paragraphs.length} paragraphs
</span>
</Link>
</li>
</ul>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getOchkmContent,
getOchkmSidebarItems,
type OchkmChapter,
type OchkmSidebarItem,
} from "@/lib/ochkm";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function OchkmChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getOchkmContent(slug);
if (!content) notFound();
const sidebarItems = await getOchkmSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as OchkmChapter).chapter === "number" &&
(content as OchkmChapter).chapter !== null;
const chapterNum = isChapter ? (content as OchkmChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ochkm"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; On Chanting the Hare Kṛṣṇa Mantra
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<OchkmSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<OchkmNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<OchkmNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function OchkmSidebar({
items,
currentSlug,
}: {
items: OchkmSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: OchkmSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ochkm/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function OchkmNav({
prev,
next,
}: {
prev: OchkmSidebarItem | null;
next: OchkmSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ochkm/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ochkm/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getOchkmMeta, getOchkmIntroSegments } from "@/lib/ochkm";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function OchkmPage() {
const meta = await getOchkmMeta();
if (!meta) notFound();
const introSegments = await getOchkmIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ochkm/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/ochkm/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getOwkContent,
getOwkSidebarItems,
type OwkChapter,
type OwkSidebarItem,
} from "@/lib/owk";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function OwkChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getOwkContent(slug);
if (!content) notFound();
const sidebarItems = await getOwkSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as OwkChapter).chapter === "number" &&
(content as OwkChapter).chapter !== null;
const chapterNum = isChapter ? (content as OwkChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/owk"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; On the Way to Kṛṣṇa
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<OwkSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<OwkNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<OwkNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function OwkSidebar({
items,
currentSlug,
}: {
items: OwkSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: OwkSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/owk/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function OwkNav({
prev,
next,
}: {
prev: OwkSidebarItem | null;
next: OwkSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/owk/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/owk/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getOwkMeta, getOwkIntroSegments } from "@/lib/owk";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function OwkPage() {
const meta = await getOwkMeta();
if (!meta) notFound();
const introSegments = await getOwkIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/owk/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/owk/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+34 -62
View File
@@ -1,16 +1,7 @@
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ExtractedVerseCount } from "./extracted-verse-count";
import { getAllBooks, type BookMeta } from "@/lib/api";
import { BooksGrid, type BookInfo } from "./books-grid";
type ChapterEntry = { totalVerses: number };
type CantoEntry = { totalChapters: number; chapters: ChapterEntry[] };
@@ -21,12 +12,6 @@ interface ChapterBookMeta extends BookMeta {
chapters: ChapterEntry[];
}
interface KbMeta extends BookMeta {
totalChapters: number;
description: string;
volumes: { number: number; totalChapters: number }[];
}
interface SbMeta extends BookMeta {
totalCantos: number;
cantos: CantoEntry[];
@@ -37,15 +22,6 @@ interface CcMeta extends BookMeta {
lilas: LilaEntry[];
}
type BookInfo = {
slug: string;
title: string;
author: string;
description: string;
totalVerses: number;
href: string;
};
function buildBookList(allBooks: BookMeta[]): BookInfo[] {
const books: BookInfo[] = [];
@@ -65,6 +41,8 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] {
title,
author,
description: `${bg.totalChapters} chapters`,
contentDescription:
"The essence of Vedic knowledge — Lord Kṛṣṇa's teachings to Arjuna on the battlefield of Kurukṣetra.",
totalVerses,
href: "/books/bg",
});
@@ -79,6 +57,9 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] {
title,
author,
description: `${sb62.totalChapters} chapters · First Canto`,
contentDescription:
(data as { description?: string }).description ||
"The original 1962 Indian edition of the First Canto.",
totalVerses,
href: "/books/sb1962",
});
@@ -97,6 +78,8 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] {
title,
author,
description: `${sb.totalCantos} cantos, ${totalChapters} chapters`,
contentDescription:
"The ripened fruit of Vedic literature — the science of God in 18,000 verses.",
totalVerses,
href: "/books/sb",
});
@@ -115,18 +98,37 @@ function buildBookList(allBooks: BookMeta[]): BookInfo[] {
title,
author,
description: `${cc.totalLilas} līlās, ${totalChapters} chapters`,
contentDescription:
"The life and teachings of Lord Caitanya Mahāprabhu — the most complete theistic science.",
totalVerses,
href: "/books/cc",
});
} else if (slug === "kb") {
const kb = data as KbMeta;
} else {
// Generic handler for all other books — pulls description from metadata
const metaDesc = (data as { description?: string }).description || "";
const totalChapters = (data as { totalChapters?: number }).totalChapters;
const totalMantras = (data as { totalMantras?: number }).totalMantras;
const totalTexts = (data as { totalTexts?: number }).totalTexts;
const parts = (data as { parts?: unknown[] }).parts;
const volumes = (data as { volumes?: unknown[] }).volumes;
let badge = "";
if (totalMantras) badge = `${totalMantras} mantras`;
else if (totalTexts) badge = `${totalTexts} texts`;
else if (totalChapters && volumes)
badge = `${totalChapters} chapters · ${volumes.length} volumes`;
else if (totalChapters && parts)
badge = `${totalChapters} chapters · ${parts.length} parts`;
else if (totalChapters) badge = `${totalChapters} chapters`;
books.push({
slug,
title,
author,
description: `${kb.totalChapters} chapters · 2 volumes`,
description: badge,
contentDescription: metaDesc,
totalVerses: 0,
href: "/books/kb",
href: `/books/${slug}`,
});
}
}
@@ -149,42 +151,12 @@ export default async function BooksPage() {
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">Books</h1>
<p className="mt-1 text-muted-foreground">
Structured verse-by-verse editions with Devanagari, transliteration,
synonyms, translation &amp; purports
Browse the books and teachings of Śrīla Prabhupāda
</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8">
<div className="grid gap-4">
{books.map((book) => (
<Link key={book.slug} href={book.href} className="block">
<Card
className={cn(
"transition-all hover:shadow-md hover:ring-foreground/20",
)}
>
<CardHeader>
<CardTitle className="text-xl font-bold">
{book.title}
</CardTitle>
<CardDescription>{book.author}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3 text-sm">
<Badge variant="secondary">{book.description}</Badge>
{book.totalVerses > 0 && (
<ExtractedVerseCount
count={book.totalVerses}
label="extracted verses"
className="text-muted-foreground"
/>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
<main className="max-w-6xl mx-auto px-6 py-8">
<BooksGrid books={books} />
</main>
</div>
);
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getPoyContent,
getPoySidebarItems,
type PoyChapter,
type PoySidebarItem,
} from "@/lib/poy";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function PoyChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getPoyContent(slug);
if (!content) notFound();
const sidebarItems = await getPoySidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as PoyChapter).chapter === "number" &&
(content as PoyChapter).chapter !== null;
const chapterNum = isChapter ? (content as PoyChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/poy"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; The Perfection of Yoga
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<PoySidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<PoyNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<PoyNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function PoySidebar({
items,
currentSlug,
}: {
items: PoySidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: PoySidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/poy/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function PoyNav({
prev,
next,
}: {
prev: PoySidebarItem | null;
next: PoySidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/poy/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/poy/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getPoyMeta, getPoyIntroSegments } from "@/lib/poy";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function PoyPage() {
const meta = await getPoyMeta();
if (!meta) notFound();
const introSegments = await getPoyIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/poy/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/poy/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+283
View File
@@ -0,0 +1,283 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getPqpaContent,
getPqpaSidebarItems,
type PqpaChapter,
type PqpaSidebarItem,
} from "@/lib/pqpa";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function PqpaChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getPqpaContent(slug);
if (!content) notFound();
const sidebarItems = await getPqpaSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as PqpaChapter).chapter === "number" &&
(content as PqpaChapter).chapter !== null;
const chapterNum = isChapter ? (content as PqpaChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/pqpa"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Perfect Questions Perfect Answers
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<PqpaSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<PqpaNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<PqpaNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function PqpaSidebar({
items,
currentSlug,
}: {
items: PqpaSidebarItem[];
currentSlug: string;
}) {
// Split items into groups for visual sections
// Front matter: intro-segment items before chapters
// Chapters: chapter items
// Conclusion: conclusion items
// Back matter: intro-segment items after conclusion (about-the-author)
const frontMatterItems: PqpaSidebarItem[] = [];
const chapterItems: PqpaSidebarItem[] = [];
const conclusionItems: PqpaSidebarItem[] = [];
const backMatterItems: PqpaSidebarItem[] = [];
let pastChapters = false;
for (const item of items) {
if (item.type === "chapter") {
pastChapters = true;
chapterItems.push(item);
} else if (item.type === "conclusion") {
pastChapters = true;
conclusionItems.push(item);
} else if (!pastChapters) {
frontMatterItems.push(item);
} else {
backMatterItems.push(item);
}
}
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{frontMatterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{frontMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Concluding Words */}
{conclusionItems.length > 0 && (
<nav className="flex flex-col gap-0.5 mb-4">
{conclusionItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
)}
{/* About the Author */}
{backMatterItems.length > 0 && (
<nav className="flex flex-col gap-0.5">
{backMatterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: PqpaSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
return (
<Link
href={`/books/pqpa/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={label}
>
{label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function PqpaNav({
prev,
next,
}: {
prev: PqpaSidebarItem | null;
next: PqpaSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/pqpa/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/pqpa/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+161
View File
@@ -0,0 +1,161 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getPqpaMeta, getPqpaIntroSegments } from "@/lib/pqpa";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function PqpaPage() {
const meta = await getPqpaMeta();
if (!meta) notFound();
const introSegments = await getPqpaIntroSegments();
// Split intro segments into front matter (order < 99) and back matter (order >= 99)
const frontMatter = introSegments.filter((seg) => seg.order < 99);
const backMatter = introSegments.filter((seg) => seg.order >= 99);
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{frontMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{frontMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/pqpa/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/pqpa/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
{/* Concluding Words */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Conclusion
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
<li>
<Link
href="/books/pqpa/concluding-words"
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
Concluding Words
</p>
</div>
</Link>
</li>
</ul>
</CardContent>
</Card>
</div>
{/* About the Author */}
{backMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Back Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{backMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/pqpa/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getRvContent,
getRvSidebarItems,
type RvChapter,
type RvSidebarItem,
} from "@/lib/rv";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function RvChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getRvContent(slug);
if (!content) notFound();
const sidebarItems = await getRvSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as RvChapter).chapter === "number" &&
(content as RvChapter).chapter !== null;
const chapterNum = isChapter ? (content as RvChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/rv"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Rāja-vidyā: The King of Knowledge
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<RvSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<RvNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<RvNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function RvSidebar({
items,
currentSlug,
}: {
items: RvSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: RvSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/rv/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function RvNav({
prev,
next,
}: {
prev: RvSidebarItem | null;
next: RvSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/rv/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/rv/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getRvMeta, getRvIntroSegments } from "@/lib/rv";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function RvPage() {
const meta = await getRvMeta();
if (!meta) notFound();
const introSegments = await getRvIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/rv/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/rv/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+271
View File
@@ -0,0 +1,271 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getSsrContent,
getSsrSidebarItems,
type SsrChapter,
type SsrSidebarItem,
} from "@/lib/ssr";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function SsrChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getSsrContent(slug);
if (!content) notFound();
const sidebarItems = await getSsrSidebarItems();
// Build a flat list of navigable items (skip "parent" type — they're labels, not pages)
const navigableItems = sidebarItems.filter((item) => item.type !== "parent");
const currentIdx = navigableItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? navigableItems[currentIdx - 1] : null;
const next =
currentIdx < navigableItems.length - 1
? navigableItems[currentIdx + 1]
: null;
// Determine if this is a sub-chapter (has parentChapter and is type "chapter"/"sub-chapter")
const isSubChapter =
"parentChapter" in content &&
typeof (content as SsrChapter).parentChapter === "number";
const parentChapterNum = isSubChapter
? (content as SsrChapter).parentChapter
: null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ssr"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; The Science of Self-Realization
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{parentChapterNum != null && (
<span className="text-primary">Chapter {parentChapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<SsrSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<SsrNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<SsrNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function SsrSidebar({
items,
currentSlug,
}: {
items: SsrSidebarItem[];
currentSlug: string;
}) {
// Split items by section
const frontMatter = items.filter(
(i) => i.type === "intro-segment" && i.slug !== "intro-about-the-author",
);
const parents = items.filter((i) => i.type === "parent");
const subChapters = items.filter((i) => i.type === "sub-chapter");
const backMatter = items.filter((i) => i.slug === "intro-about-the-author");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{frontMatter.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{frontMatter.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters — grouped by parent */}
{parents.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{parents.map((parent) => (
<div key={parent.slug}>
{/* Parent heading — not a link, just a bold label */}
<div className="px-2 py-1.5 text-xs font-bold text-muted-foreground mt-3 first:mt-0">
{parent.label}
</div>
{/* Sub-chapters indented */}
{subChapters
.filter((sub) => sub.parentChapter === parent.parentChapter)
.map((sub) => {
const isActive = sub.slug === currentSlug;
return (
<Link
key={sub.slug}
href={`/books/ssr/${sub.slug}`}
className={cn(
"pl-4 px-2 py-1.5 rounded-md text-sm transition-colors truncate block",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={sub.label}
>
{sub.label}
</Link>
);
})}
</div>
))}
</nav>
</>
)}
{/* Back Matter */}
{backMatter.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Back Matter
</h2>
<nav className="flex flex-col gap-0.5">
{backMatter.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: SsrSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ssr/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function SsrNav({
prev,
next,
}: {
prev: SsrSidebarItem | null;
next: SsrSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ssr/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ssr/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+133
View File
@@ -0,0 +1,133 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getSsrMeta, getSsrIntroSegments } from "@/lib/ssr";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function SsrPage() {
const meta = await getSsrMeta();
if (!meta) notFound();
const introSegments = await getSsrIntroSegments();
// Split intro segments into front matter (before chapters) and back matter (after)
const frontMatter = introSegments.filter((seg) => seg.order < 99);
const backMatter = introSegments.filter((seg) => seg.order >= 99);
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{frontMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{frontMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ssr/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Chapters — hierarchical parent → sub-chapter structure */}
{meta.chapters.map((ch) => (
<div key={ch.number}>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
{ch.number}. {ch.title}
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{ch.subChapters.map((sub) => (
<li key={sub.slug}>
<Link
href={`/books/ssr/${sub.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<p className="font-medium text-foreground">
{sub.title}
</p>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
))}
{/* Back Matter (About the Author) */}
{backMatter.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Back Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{backMatter.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/ssr/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}
+233
View File
@@ -0,0 +1,233 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getTlcContent,
getTlcSidebarItems,
type TlcChapter,
type TlcSidebarItem,
} from "@/lib/tlc";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function TlcChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getTlcContent(slug);
if (!content) notFound();
const sidebarItems = await getTlcSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as TlcChapter).chapter === "number" &&
(content as TlcChapter).chapter !== null;
const chapterNum = isChapter ? (content as TlcChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/tlc"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Teachings of Lord Caitanya
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<TlcSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<TlcNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<TlcNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function TlcSidebar({
items,
currentSlug,
}: {
items: TlcSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: TlcSidebarItem;
isActive: boolean;
}) {
let label: string;
if (item.type === "chapter" && item.chapter != null) {
// Extract just the title portion after "Chapter N: "
const titlePart = item.label.replace(/^Chapter \d+:\s*/, "");
label = `${item.chapter}. ${titlePart}`;
} else {
label = item.label;
}
return (
<Link
href={`/books/tlc/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={label}
>
{label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function TlcNav({
prev,
next,
}: {
prev: TlcSidebarItem | null;
next: TlcSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/tlc/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/tlc/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+116
View File
@@ -0,0 +1,116 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTlcMeta, getTlcIntroSegments } from "@/lib/tlc";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
const PART_LABELS: Record<number, string> = {
1: "Part I",
2: "Part II",
};
export default async function TlcPage() {
const meta = await getTlcMeta();
if (!meta) notFound();
const introSegments = await getTlcIntroSegments();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-sm text-muted-foreground italic">
{meta.subtitle}
</p>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Front Matter */}
{introSegments && introSegments.length > 0 && (
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Front Matter
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{introSegments.map((seg) => (
<li key={seg.slug}>
<Link
href={`/books/tlc/${seg.slug}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground shrink-0 ml-4">
{seg.paragraphs.length} paragraphs
</span>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
)}
{/* Parts & Chapters */}
{meta.parts.map((part) => {
const partChapters = part.chapters
.map((num) => meta.chapters.find((ch) => ch.number === num))
.filter(Boolean) as { number: number; title: string }[];
return (
<div key={part.number}>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
{PART_LABELS[part.number] ?? `Part ${part.number}`}
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{partChapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/tlc/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
);
})}
</main>
</div>
);
}
+224
View File
@@ -0,0 +1,224 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getTtpContent,
getTtpSidebarItems,
type TtpChapter,
type TtpSidebarItem,
} from "@/lib/ttp";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function TtpChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getTtpContent(slug);
if (!content) notFound();
const sidebarItems = await getTtpSidebarItems();
const currentIdx = sidebarItems.findIndex((item) => item.slug === slug);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
// Build the page title
const isChapter =
"chapter" in content &&
typeof (content as TtpChapter).chapter === "number" &&
(content as TtpChapter).chapter !== null;
const chapterNum = isChapter ? (content as TtpChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/ttp"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Transcendental Teachings of Prahlāda Mahārāja
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Chapter {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<TtpSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<TtpNav prev={prev} next={next} />
{/* Content */}
<Card>
<CardContent className="py-8 px-6 sm:px-8">
<div className="space-y-4">
{content.paragraphs.map((paragraph, i) => (
<div
key={i}
className="text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<TtpNav prev={prev} next={next} />
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function TtpSidebar({
items,
currentSlug,
}: {
items: TtpSidebarItem[];
currentSlug: string;
}) {
const introItems = items.filter((item) => item.type === "intro-segment");
const chapterItems = items.filter((item) => item.type === "chapter");
return (
<aside className="hidden lg:block w-56 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
{/* Front Matter */}
{introItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Front Matter
</h2>
<nav className="flex flex-col gap-0.5 mb-4">
{introItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
{/* Chapters */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Chapters
</h2>
<nav className="flex flex-col gap-0.5">
{chapterItems.map((item) => (
<SidebarLink
key={item.slug}
item={item}
isActive={item.slug === currentSlug}
/>
))}
</nav>
</>
)}
</aside>
);
}
function SidebarLink({
item,
isActive,
}: {
item: TtpSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/ttp/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function TtpNav({
prev,
next,
}: {
prev: TtpSidebarItem | null;
next: TtpSidebarItem | null;
}) {
return (
<div className="flex items-center justify-between gap-3">
{prev ? (
<Link
href={`/books/ttp/${prev.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<ChevronLeft className="size-4" />
<span className="hidden sm:inline truncate max-w-[180px]">
{prev.type === "chapter" && prev.chapter != null
? `Ch. ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div />
)}
{next ? (
<Link
href={`/books/ttp/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.type === "chapter" && next.chapter != null
? `Ch. ${next.chapter}`
: next.label}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTtpMeta } from "@/lib/ttp";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function TtpPage() {
const meta = await getTtpMeta();
if (!meta) notFound();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/books"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Books
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
{meta.title}
</h1>
<p className="mt-1 text-muted-foreground">{meta.author}</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8 space-y-8">
{/* Description */}
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{/* Chapters */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Chapters
</h2>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{meta.chapters.map((ch) => (
<li key={ch.number}>
<Link
href={`/books/ttp/${ch.number}`}
className="flex items-center justify-between px-5 py-4 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Chapter {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}