feat: add journals

This commit is contained in:
gosangam
2026-05-03 21:02:41 +05:30
parent 89106efc98
commit 35d1d14f37
47 changed files with 11365 additions and 37 deletions
+249
View File
@@ -0,0 +1,249 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getBkContent,
getBkSidebarItems,
type BkChapter,
type BkSidebarItem,
} from "@/lib/bk";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export const dynamic = "force-dynamic";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function BkChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getBkContent(slug);
if (!content) notFound();
const sidebarItems = await getBkSidebarItems();
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 chapterNum = (content as BkChapter).chapter ?? null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/bk"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Bhagavāner Kathā
</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 */}
<BkSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<BkNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
{/* 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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<BkNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function BkSidebar({
items,
currentSlug,
}: {
items: BkSidebarItem[];
currentSlug: string;
}) {
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">
{/* 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: BkSidebarItem;
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/bk/${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 BkNav({
prev,
next,
sidebarItems,
currentSlug,
}: {
prev: BkSidebarItem | null;
next: BkSidebarItem | null;
sidebarItems: BkSidebarItem[];
currentSlug: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
const chapterItems = sidebarItems.filter((i) => i.type === "chapter");
if (chapterItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: chapterItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/bk/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle="Bhagavāner Kathā"
/>
{prev ? (
<Link
href={`/books/bk/${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 className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/books/bk/${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 { getBkMeta } from "@/lib/bk";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function BkPage() {
const meta = await getBkMeta();
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/bk/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 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>
);
}
+249
View File
@@ -0,0 +1,249 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getCbhcContent,
getCbhcSidebarItems,
type CbhcChapter,
type CbhcSidebarItem,
} from "@/lib/cbhc";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export const dynamic = "force-dynamic";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function CbhcChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getCbhcContent(slug);
if (!content) notFound();
const sidebarItems = await getCbhcSidebarItems();
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 chapterNum = (content as CbhcChapter).chapter ?? null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/cbhc"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Śrī Caitanya-bhāgavata Commentary
</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 */}
<CbhcSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<CbhcNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
{/* 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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<CbhcNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function CbhcSidebar({
items,
currentSlug,
}: {
items: CbhcSidebarItem[];
currentSlug: string;
}) {
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">
{/* 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: CbhcSidebarItem;
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/cbhc/${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 CbhcNav({
prev,
next,
sidebarItems,
currentSlug,
}: {
prev: CbhcSidebarItem | null;
next: CbhcSidebarItem | null;
sidebarItems: CbhcSidebarItem[];
currentSlug: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
const chapterItems = sidebarItems.filter((i) => i.type === "chapter");
if (chapterItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: chapterItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/cbhc/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle="Śrī Caitanya-bhāgavata Commentary"
/>
{prev ? (
<Link
href={`/books/cbhc/${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 className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/books/cbhc/${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 { getCbhcMeta } from "@/lib/cbhc";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function CbhcPage() {
const meta = await getCbhcMeta();
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/cbhc/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 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>
);
}
+276
View File
@@ -0,0 +1,276 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getMogContent,
getMogSidebarItems,
type MogChapter,
type MogSidebarItem,
} from "@/lib/mog";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export const dynamic = "force-dynamic";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function MogChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getMogContent(slug);
if (!content) notFound();
const sidebarItems = await getMogSidebarItems();
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 MogChapter).chapter === "number" &&
(content as MogChapter).chapter !== null;
const chapterNum = isChapter ? (content as MogChapter).chapter : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/mog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Message of Godhead
</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 */}
<MogSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<MogNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
{/* 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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<MogNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function MogSidebar({
items,
currentSlug,
}: {
items: MogSidebarItem[];
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: MogSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/mog/${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 MogNav({
prev,
next,
sidebarItems,
currentSlug,
}: {
prev: MogSidebarItem | null;
next: MogSidebarItem | null;
sidebarItems: MogSidebarItem[];
currentSlug: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
const introItems = sidebarItems.filter((i) => i.type === "intro-segment");
if (introItems.length > 0) {
mobileNavGroups.push({
title: "Front Matter",
items: introItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/mog/${i.slug}`,
type: i.type,
})),
});
}
const chapterItems = sidebarItems.filter((i) => i.type === "chapter");
if (chapterItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: chapterItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/mog/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle="Message of Godhead"
/>
{prev ? (
<Link
href={`/books/mog/${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 className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/books/mog/${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 { getMogMeta, getMogIntroSegments } from "@/lib/mog";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function MogPage() {
const meta = await getMogMeta();
if (!meta) notFound();
const introSegments = await getMogIntroSegments();
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/mog/${seg.slug.startsWith("intro-") ? seg.slug : `intro-${seg.slug}`}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{seg.title}
</p>
</div>
<span className="text-sm text-muted-foreground sm:shrink-0 sm: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/mog/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 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>
);
}
+239
View File
@@ -0,0 +1,239 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getPkContent,
getPkSidebarItems,
type PkChapter,
type PkSidebarItem,
} from "@/lib/pk";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export const dynamic = "force-dynamic";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function PkChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getPkContent(slug);
if (!content) notFound();
const sidebarItems = await getPkSidebarItems();
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;
const chapterNum = (content as PkChapter).chapter;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/pk"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Prayers of King Kulaśekhara
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{chapterNum != null && (
<span className="text-primary">Verse {chapterNum}: </span>
)}
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<PkSidebar items={sidebarItems} currentSlug={slug} />
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<PkNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
{/* 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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<PkNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function PkSidebar({
items,
currentSlug,
}: {
items: PkSidebarItem[];
currentSlug: string;
}) {
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">
{/* Chapters (Prayers) */}
{chapterItems.length > 0 && (
<>
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
Prayers
</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: PkSidebarItem;
isActive: boolean;
}) {
return (
<Link
href={`/books/pk/${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 PkNav({
prev,
next,
sidebarItems,
currentSlug,
}: {
prev: PkSidebarItem | null;
next: PkSidebarItem | null;
sidebarItems: PkSidebarItem[];
currentSlug: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
const chapterItems = sidebarItems.filter((i) => i.type === "chapter");
if (chapterItems.length > 0) {
mobileNavGroups.push({
title: "Prayers",
items: chapterItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/pk/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle="Prayers of King Kulaśekhara"
/>
{prev ? (
<Link
href={`/books/pk/${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.chapter != null
? `Prayer ${prev.chapter}`
: prev.label}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/books/pk/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
{next.chapter != null
? `Prayer ${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 { getPkMeta } from "@/lib/pk";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function PkPage() {
const meta = await getPkMeta();
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 (Prayers) */}
<div>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
Prayers
</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/pk/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 hover:bg-muted transition-colors"
>
<div>
<span className="text-sm text-primary font-medium">
Verse {ch.number}
</span>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+192
View File
@@ -0,0 +1,192 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getVaContent,
getVaSidebarItems,
type VaChapter,
type VaSidebarItem,
} from "@/lib/va";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export default async function VaChapterPage({
params,
}: {
params: Promise<{ chapter: string }>;
}) {
const { chapter: slug } = await params;
const content = await getVaContent(slug);
if (!content) notFound();
const sidebarItems = await getVaSidebarItems();
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;
const chapterNum = (content as VaChapter).chapter;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href="/books/va"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Vaiśiṣṭyāṣṭakam
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
<VaSidebar items={sidebarItems} currentSlug={slug} />
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
<VaNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
<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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
<VaNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={slug}
/>
</main>
</div>
</div>
);
}
function VaSidebar({
items,
currentSlug,
}: {
items: VaSidebarItem[];
currentSlug: string;
}) {
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">
<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">
{items.map((item) => (
<Link
key={item.slug}
href={`/books/va/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
item.slug === currentSlug
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
))}
</nav>
</aside>
);
}
function VaNav({
prev,
next,
sidebarItems,
currentSlug,
}: {
prev: VaSidebarItem | null;
next: VaSidebarItem | null;
sidebarItems: VaSidebarItem[];
currentSlug: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
if (sidebarItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: sidebarItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/books/va/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle="Vaiśiṣṭyāṣṭakam"
/>
{prev ? (
<Link
href={`/books/va/${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 className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/books/va/${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>
);
}
+61
View File
@@ -0,0 +1,61 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getVaMeta } from "@/lib/va";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function VaPage() {
const meta = await getVaMeta();
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">
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
<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/va/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{ch.title}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+144
View File
@@ -0,0 +1,144 @@
"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import {
Card,
CardHeader,
CardTitle,
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";
export type EssayInfo = {
slug: string;
title: string;
year: number | null;
group: string | null;
href: string;
};
/**
* Strip diacritics / accents so that "Krsna" matches "Kṛṣṇa",
* "Gita" matches "Gītā", "Srimad" matches "Śrīmad", etc.
*/
function normalize(text: string): string {
return (
text
.toLowerCase()
.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")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
);
}
/** Human-readable group labels */
const GROUP_LABELS: Record<string, string> = {
"cc-essays": "Śrī Caitanya-caritāmṛta Essays",
"pre-1977-essays": "Pre-1977 Essays",
"two-essays": "Two Essays (1966)",
reviews: "Reviews",
standalone: "Standalone",
"typed-transcripts": "Typed Transcripts",
};
function groupLabel(group: string | null): string {
if (!group) return "Other";
return GROUP_LABELS[group] ?? group;
}
export function EssaysGrid({ essays }: { essays: EssayInfo[] }) {
const [query, setQuery] = useState("");
const normalizedEssays = useMemo(
() =>
essays.map((essay) => ({
essay,
searchText: normalize(
`${essay.title} ${essay.group ?? ""} ${essay.slug}`,
),
})),
[essays],
);
const filtered = useMemo(() => {
if (!query.trim()) return essays;
const q = normalize(query.trim());
return normalizedEssays
.filter((entry) => entry.searchText.includes(q))
.map((entry) => entry.essay);
}, [essays, normalizedEssays, 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 essays..."
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 essays found"
: `${filtered.length} essay${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((essay) => (
<Link key={essay.slug} href={essay.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">
{essay.title}
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
{essay.year && (
<Badge variant="secondary" className="text-xs">
{essay.year}
</Badge>
)}
{essay.group && (
<span className="text-muted-foreground text-xs">
{groupLabel(essay.group)}
</span>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}
+12 -27
View File
@@ -2,19 +2,27 @@ export const dynamic = "force-dynamic";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
import { prisma } from "@/lib/db";
import { EssaysGrid } from "./essays-grid";
async function getAllEssays() {
return prisma.essay.findMany({
orderBy: [{ group: "asc" }, { order: "asc" }],
select: { slug: true, title: true, author: true, year: true, group: true },
select: { slug: true, title: true, year: true, group: true },
});
}
export default async function EssaysPage() {
const essays = await getAllEssays();
const items = essays.map((essay) => ({
slug: essay.slug,
title: essay.title,
year: essay.year,
group: essay.group,
href: `/essays/${essay.slug}`,
}));
return (
<div className="min-h-screen bg-background">
<PageHeader>
@@ -30,31 +38,8 @@ export default async function EssaysPage() {
</p>
</PageHeader>
<main className="max-w-3xl mx-auto px-6 py-8">
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{essays.map((essay) => (
<li key={essay.slug}>
<Link
href={`/essays/${essay.slug}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 hover:bg-muted transition-colors"
>
<div>
<p className="font-medium text-foreground">
{essay.title}
</p>
<p className="text-sm text-muted-foreground">
{essay.author}
{essay.year ? ` · ${essay.year}` : ""}
</p>
</div>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
<main className="max-w-6xl mx-auto px-6 py-8">
<EssaysGrid essays={items} />
</main>
</div>
);
+223
View File
@@ -0,0 +1,223 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getJournalContent,
getJournalMeta,
getJournalSidebarItems,
slugToBookCode,
type JournalSidebarItem,
} from "@/lib/journals";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
export default async function JournalChapterPage({
params,
}: {
params: Promise<{ slug: string; chapter: string }>;
}) {
const { slug, chapter: chapterSlug } = await params;
const bookCode = slugToBookCode(slug);
const [meta, content, sidebarItems] = await Promise.all([
getJournalMeta(bookCode),
getJournalContent(bookCode, chapterSlug),
getJournalSidebarItems(bookCode),
]);
if (!meta || !content) notFound();
// Filter out heading items for prev/next navigation
const navItems = sidebarItems.filter((item) => item.type !== "heading");
const currentIdx = navItems.findIndex((item) => item.slug === chapterSlug);
const prev = currentIdx > 0 ? navItems[currentIdx - 1] : null;
const next =
currentIdx < navItems.length - 1 ? navItems[currentIdx + 1] : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href={`/journals/${slug}`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; {meta.title}
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
<ChapterSidebar
items={sidebarItems}
currentSlug={chapterSlug}
docSlug={slug}
/>
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
<ChapterNav
prev={prev}
next={next}
navItems={navItems}
currentSlug={chapterSlug}
docSlug={slug}
docTitle={meta.title}
showMobileNav
/>
<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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
<ChapterNav
prev={prev}
next={next}
navItems={navItems}
currentSlug={chapterSlug}
docSlug={slug}
docTitle={meta.title}
/>
</main>
</div>
</div>
);
}
function ChapterSidebar({
items,
currentSlug,
docSlug,
}: {
items: JournalSidebarItem[];
currentSlug: string;
docSlug: string;
}) {
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">
<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">
{items.map((item, i) =>
item.type === "heading" ? (
<h3
key={`item-${i}`}
className="text-xs font-bold uppercase tracking-wider text-muted-foreground mt-4 mb-1 px-2"
>
{item.label}
</h3>
) : (
<Link
key={`item-${i}`}
href={`/journals/${docSlug}/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
item.slug === currentSlug
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
),
)}
</nav>
</aside>
);
}
function ChapterNav({
prev,
next,
navItems,
currentSlug,
docSlug,
docTitle,
showMobileNav = false,
}: {
prev: JournalSidebarItem | null;
next: JournalSidebarItem | null;
navItems: JournalSidebarItem[];
currentSlug: string;
docSlug: string;
docTitle: string;
showMobileNav?: boolean;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
if (showMobileNav && navItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: navItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/journals/${docSlug}/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
{showMobileNav && (
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle={docTitle}
/>
)}
{prev ? (
<Link
href={`/journals/${docSlug}/${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 className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/journals/${docSlug}/${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>
);
}
+138
View File
@@ -0,0 +1,138 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getJournalMeta,
slugToBookCode,
type JournalChapterEntry,
} from "@/lib/journals";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function JournalIndexPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const bookCode = slugToBookCode(slug);
const meta = await getJournalMeta(bookCode);
if (!meta) notFound();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/journals"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Journals
</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">
{meta.description && (
<p className="text-muted-foreground">{meta.description}</p>
)}
{meta.chapters.map((ch: JournalChapterEntry, idx: number) => {
// Top-level chapter with a slug (Preface, Introduction, etc.)
if ("slug" in ch && !("subChapters" in ch)) {
return (
<div key={ch.slug}>
<Card>
<CardContent className="p-0">
<Link
href={`/journals/${slug}/${ch.slug}`}
className="flex items-center px-5 py-4 hover:bg-muted transition-colors"
>
<p className="font-medium text-foreground">{ch.title}</p>
</Link>
</CardContent>
</Card>
</div>
);
}
// Group with sub-chapters (may be 2 or 3 levels deep)
if ("subChapters" in ch) {
const group = ch as {
title: string;
subChapters: {
slug?: string;
title: string;
subChapters?: { slug: string; title: string }[];
}[];
};
return (
<div key={`group-${idx}`}>
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
{group.title}
</h2>
<div className="space-y-4">
{group.subChapters.map((sc, scIdx) => {
// Sub-chapter with its own nested sub-chapters (e.g., month → days)
if ("subChapters" in sc && sc.subChapters) {
return (
<div key={`sc-${idx}-${scIdx}`}>
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2 ml-1">
{sc.title}
</h3>
<Card className="divide-y divide-border">
<CardContent className="p-0">
<ul className="divide-y divide-border">
{sc.subChapters.map((day) => (
<li key={day.slug}>
<Link
href={`/journals/${slug}/${day.slug}`}
className="flex items-center px-5 py-3 hover:bg-muted transition-colors"
>
<p className="text-sm font-medium text-foreground">
{day.title}
</p>
</Link>
</li>
))}
</ul>
</CardContent>
</Card>
</div>
);
}
// Direct sub-chapter with a slug (e.g., Summary, Notice)
if ("slug" in sc && sc.slug) {
return (
<Card key={`sc-${idx}-${scIdx}`}>
<CardContent className="p-0">
<Link
href={`/journals/${slug}/${sc.slug}`}
className="flex items-center px-5 py-3 hover:bg-muted transition-colors"
>
<p className="text-sm font-medium text-foreground">
{sc.title}
</p>
</Link>
</CardContent>
</Card>
);
}
return null;
})}
</div>
</div>
);
}
return null;
})}
</main>
</div>
);
}
+52
View File
@@ -0,0 +1,52 @@
export const dynamic = "force-dynamic";
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 { getAllJournals, bookCodeToSlug } from "@/lib/journals";
export default async function JournalsPage() {
const journals = await getAllJournals();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link href="/" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
&larr; Home
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">Journals</h1>
<p className="mt-1 text-muted-foreground">
Personal diaries and journals of Śrīla Prabhupāda
</p>
</PageHeader>
<main className="max-w-6xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{journals.map((journal) => (
<Link key={journal.book} href={`/journals/${bookCodeToSlug(journal.book)}`} className="block h-full">
<Card className="h-full transition-all hover:shadow-md hover:ring-foreground/20">
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold leading-snug">{journal.title}</CardTitle>
{journal.description && (
<CardDescription className="text-xs line-clamp-2">{journal.description}</CardDescription>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
{journal.year && <Badge variant="secondary" className="text-xs">{journal.year}</Badge>}
{journal.totalChapters > 0 && (
<span className="text-muted-foreground text-xs">
{journal.totalChapters} {journal.totalChapters === 1 ? "chapter" : "chapters"}
</span>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</main>
</div>
);
}
@@ -0,0 +1,229 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getOtherDocumentContent,
getOtherDocumentMeta,
getOtherDocumentSidebarItems,
slugToBookCode,
type OtherDocumentSidebarItem,
} from "@/lib/other-documents";
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";
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function OtherDocumentChapterPage({
params,
}: {
params: Promise<{ slug: string; chapter: string }>;
}) {
const { slug, chapter: chapterSlug } = await params;
const bookCode = slugToBookCode(slug);
const [meta, content, sidebarItems] = await Promise.all([
getOtherDocumentMeta(bookCode),
getOtherDocumentContent(bookCode, chapterSlug),
getOtherDocumentSidebarItems(bookCode),
]);
if (!meta || !content) notFound();
const currentIdx = sidebarItems.findIndex(
(item) => item.slug === chapterSlug,
);
const prev = currentIdx > 0 ? sidebarItems[currentIdx - 1] : null;
const next =
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
return (
<div className="min-h-screen bg-background">
<PageHeader sticky wide>
<Link
href={`/other-documents/${slug}`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; {meta.title}
</Link>
<h1 className="mt-3 text-2xl font-bold text-foreground">
<span className="text-primary">Chapter {content.chapter}: </span>
{content.title}
</h1>
</PageHeader>
<div className="w-full md:max-w-6xl mx-auto flex">
{/* Left sidebar */}
<ChapterSidebar
items={sidebarItems}
currentSlug={chapterSlug}
docSlug={slug}
/>
{/* Main content */}
<main className="flex-1 min-w-0 px-6 py-8 space-y-8">
{/* Top navigation */}
<ChapterNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={chapterSlug}
docSlug={slug}
docTitle={meta.title}
/>
{/* 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-base text-foreground leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraph }}
/>
))}
</div>
</CardContent>
</Card>
{/* Bottom navigation */}
<ChapterNav
prev={prev}
next={next}
sidebarItems={sidebarItems}
currentSlug={chapterSlug}
docSlug={slug}
docTitle={meta.title}
/>
</main>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Sidebar
// ---------------------------------------------------------------------------
function ChapterSidebar({
items,
currentSlug,
docSlug,
}: {
items: OtherDocumentSidebarItem[];
currentSlug: string;
docSlug: string;
}) {
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">
<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">
{items.map((item) => (
<Link
key={item.slug}
href={`/other-documents/${docSlug}/${item.slug}`}
className={cn(
"px-2 py-1.5 rounded-md text-sm transition-colors truncate",
item.slug === currentSlug
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={item.label}
>
{item.label}
</Link>
))}
</nav>
</aside>
);
}
// ---------------------------------------------------------------------------
// Prev / Next navigation
// ---------------------------------------------------------------------------
function ChapterNav({
prev,
next,
sidebarItems,
currentSlug,
docSlug,
docTitle,
}: {
prev: OtherDocumentSidebarItem | null;
next: OtherDocumentSidebarItem | null;
sidebarItems: OtherDocumentSidebarItem[];
currentSlug: string;
docSlug: string;
docTitle: string;
}) {
const mobileNavGroups: MobileNavGroup[] = [];
if (sidebarItems.length > 0) {
mobileNavGroups.push({
title: "Chapters",
items: sidebarItems.map((i) => ({
slug: i.slug,
label: i.label,
href: `/other-documents/${docSlug}/${i.slug}`,
type: i.type,
})),
});
}
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<MobileNav
groups={mobileNavGroups}
currentSlug={currentSlug}
bookTitle={docTitle}
/>
{prev ? (
<Link
href={`/other-documents/${docSlug}/${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]">
Ch. {prev.chapter}
</span>
<span className="sm:hidden">Previous</span>
</Link>
) : (
<div className="hidden lg:block" />
)}
</div>
{next ? (
<Link
href={`/other-documents/${docSlug}/${next.slug}`}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"gap-1.5",
)}
>
<span className="hidden sm:inline truncate max-w-[180px]">
Ch. {next.chapter}
</span>
<span className="sm:hidden">Next</span>
<ChevronRight className="size-4" />
</Link>
) : (
<div />
)}
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getOtherDocumentMeta, slugToBookCode } from "@/lib/other-documents";
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
export default async function OtherDocumentIndexPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const bookCode = slugToBookCode(slug);
const meta = await getOtherDocumentMeta(bookCode);
if (!meta) notFound();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/other-documents"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Other Documents
</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 */}
{meta.chapters.length > 0 && (
<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={`/other-documents/${slug}/${ch.number}`}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between px-5 py-4 gap-1 sm:gap-0 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>
);
}
@@ -0,0 +1,141 @@
"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";
export type OtherDocumentInfo = {
slug: string;
title: string;
description: string;
year: number | null;
totalChapters: number;
href: string;
};
/**
* Strip diacritics / accents so that "Krsna" matches "Kṛṣṇa",
* "Gita" matches "Gītā", "Srimad" matches "Śrīmad", etc.
*/
function normalize(text: string): string {
return (
text
.toLowerCase()
.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")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
);
}
export function OtherDocumentsGrid({
items,
}: {
items: OtherDocumentInfo[];
}) {
const [query, setQuery] = useState("");
const normalizedItems = useMemo(
() =>
items.map((item) => ({
item,
searchText: normalize(
`${item.title} ${item.description} ${item.slug}`,
),
})),
[items],
);
const filtered = useMemo(() => {
if (!query.trim()) return items;
const q = normalize(query.trim());
return normalizedItems
.filter((entry) => entry.searchText.includes(q))
.map((entry) => entry.item);
}, [items, normalizedItems, 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 documents..."
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 documents found"
: `${filtered.length} document${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((item) => (
<Link key={item.slug} href={item.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">
{item.title}
</CardTitle>
{item.description && (
<CardDescription className="text-xs line-clamp-2">
{item.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
{item.year && (
<Badge variant="secondary" className="text-xs">
{item.year}
</Badge>
)}
{item.totalChapters > 0 && (
<span className="text-muted-foreground text-xs">
{item.totalChapters}{" "}
{item.totalChapters === 1 ? "chapter" : "chapters"}
</span>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
export const dynamic = "force-dynamic";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { getAllOtherDocuments, bookCodeToSlug } from "@/lib/other-documents";
import { OtherDocumentsGrid } from "./other-documents-grid";
export default async function OtherDocumentsPage() {
const documents = await getAllOtherDocuments();
const items = documents.map((doc) => ({
slug: bookCodeToSlug(doc.book),
title: doc.title,
description: doc.description ?? "",
year: doc.year,
totalChapters: doc.totalChapters,
href: `/other-documents/${bookCodeToSlug(doc.book)}`,
}));
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link
href="/"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Home
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">
Other Documents
</h1>
<p className="mt-1 text-muted-foreground">
Miscellaneous documents, analyses, and writings by Śrīla Prabhupāda
</p>
</PageHeader>
<main className="max-w-6xl mx-auto px-6 py-8">
<OtherDocumentsGrid items={items} />
</main>
</div>
);
}
+66 -1
View File
@@ -9,7 +9,14 @@ import {
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
import { Logo } from "@/components/logo";
import { BookOpen, FileText, BookA, PenLine } from "lucide-react";
import {
BookOpen,
FileText,
BookA,
PenLine,
FileStack,
NotebookPen,
} from "lucide-react";
const allowRtf = process.env.ALLOW_RTF === "true";
@@ -76,6 +83,35 @@ export default function Home() {
</Card>
</Link>
{/* Journals */}
<Link href="/journals" className="block group">
<Card className="transition-all group-hover:shadow-md group-hover:ring-foreground/20">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center size-10 rounded-lg bg-primary/10 text-primary">
<NotebookPen className="size-5" />
</div>
<div className="flex-1 min-w-0">
<CardTitle className="text-xl font-bold">
Journals
</CardTitle>
<CardDescription>
Personal diaries &amp; journals
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Śrīla Prabhupāda&apos;s personal diaries — including the
historic Jaladuta Diary from his 1965 voyage to America and
The Beginning, his 1966 New York journal documenting the
founding of the Hare Kṛṣṇa movement.
</p>
</CardContent>
</Card>
</Link>
{/* Glossary */}
<Link href="/glossary" className="block group">
<Card className="transition-all group-hover:shadow-md group-hover:ring-foreground/20">
@@ -104,6 +140,35 @@ export default function Home() {
</Card>
</Link>
{/* Other Documents */}
<Link href="/other-documents" className="block group">
<Card className="transition-all group-hover:shadow-md group-hover:ring-foreground/20">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center size-10 rounded-lg bg-primary/10 text-primary">
<FileStack className="size-5" />
</div>
<div className="flex-1 min-w-0">
<CardTitle className="text-xl font-bold">
Other Documents
</CardTitle>
<CardDescription>
Miscellaneous analyses &amp; writings
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Miscellaneous documents, analyses, and writings by Śrīla
Prabhupāda that don&apos;t fit neatly into the books or essays
collections — including structured analyses with verse
references.
</p>
</CardContent>
</Card>
</Link>
{/* RTF Document Viewer — only visible when ALLOW_RTF=true */}
{allowRtf && (
<Link href="/rtfs" className="block group">