feat: add journals
This commit is contained in:
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← Ś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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
← {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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
← 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"
|
||||
>
|
||||
← {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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
>
|
||||
← 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
@@ -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 & journals
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Śrīla Prabhupāda'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 & 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'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">
|
||||
|
||||
Reference in New Issue
Block a user