diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index 2731b12f..3aa1c85a 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -27,7 +27,7 @@ The original RTF files use **custom font encodings** (Indevr, ScaGoudy, Inbenr/I
```
spsays-training/
├── app/
-│ ├── page.tsx → Landing page with Logo, links to Books/Essays/Glossary/RTFs
+│ ├── page.tsx → Landing page with Logo, links to Books/Essays/Journals/Glossary/Other Documents/RTFs
│ ├── layout.tsx → ThemeProvider + TooltipProvider + PasswordGate wrapper
│ ├── globals.css → Vintage color palette + reading settings CSS
│ ├── books/
@@ -56,10 +56,28 @@ spsays-training/
│ │ ├── ochkm/ → On Chanting the Hare Kṛṣṇa Mantra (4 chapters)
│ │ ├── hkh/ → The Kṛṣṇa Consciousness Handbook (19 chapters)
│ │ ├── ssr/ → The Science of Self-Realization (8 chapters, 50 sub-chapters)
-│ │ └── ds/ → Dialectic Spiritualism (13 chapters, 36 sub-chapters)
+│ │ ├── ds/ → Dialectic Spiritualism (13 chapters, 36 sub-chapters)
+│ │ └── va/ → Vaiśiṣṭyāṣṭakam (8 chapters)
+│ ├── magazines/
+│ │ ├── page.tsx → Magazines index page (grouped by flag: "Pre 1978")
+│ │ └── [slug]/
+│ │ ├── page.tsx → Magazine issue listing (grouped by volume)
+│ │ └── [issue]/page.tsx → Individual issue reader with sidebar
│ ├── essays/
-│ │ ├── page.tsx → Essays listing page
+│ │ ├── page.tsx → Essays grid page (searchable)
+│ │ ├── essays-grid.tsx → Client component for searchable essay grid
│ │ └── [slug]/page.tsx → Individual essay reader
+│ ├── journals/
+│ │ ├── page.tsx → Journals index page (grid)
+│ │ └── [slug]/
+│ │ ├── page.tsx → Journal chapter listing (hierarchical: months → days)
+│ │ └── [chapter]/page.tsx → Individual journal entry reader
+│ ├── other-documents/
+│ │ ├── page.tsx → Other documents grid page (searchable)
+│ │ ├── other-documents-grid.tsx → Client component for searchable grid
+│ │ └── [slug]/
+│ │ ├── page.tsx → Document chapter listing
+│ │ └── [chapter]/page.tsx → Individual chapter reader
│ ├── glossary/
│ │ ├── page.tsx → Server component wrapper
│ │ └── glossary-client.tsx → Searchable client component (2749 entries)
@@ -104,6 +122,10 @@ spsays-training/
│ ├── ssr.ts → Science of Self-Realization data access (Prisma, hierarchical)
│ ├── ds.ts → Dialectic Spiritualism data access (Prisma, hierarchical)
│ ├── te.ts → Two Essays data access (Prisma, uses essays collection)
+│ ├── va.ts → Vaiśiṣṭyāṣṭakam data access (Prisma)
+│ ├── other-documents.ts → Other Documents data access (Prisma, dedicated collections)
+│ ├── journals.ts → Journals data access (Prisma, dedicated collections)
+│ ├── magazines.ts → Magazines data access (Prisma, dedicated collections)
│ └── jsons/books/ → Extracted JSON data (all books)
│ ├── bg/, sb/, cc/, sb-1962/, kb/ → Original verse/prose books
│ ├── nod/, tlc/, iso/, noi/ → Extracted from other.html
@@ -111,9 +133,16 @@ spsays-training/
│ ├── rv/, poy/, pqpa/, ttp/, ochkm/ → More prose books
│ ├── hkh/, ssr/, ds/ → Large structured books
│ ├── te/ → Two Essays
-│ └── glossary/index.json → 2,749 glossary entries
+│ ├── va/ → Vaiśiṣṭyāṣṭakam
+│ ├── glossary/index.json → 2,749 glossary entries
+│ ├── essays/ → Essay JSON files (pre-1977, cc-essays, standalone, etc.)
+│ ├── other-documents/ → Other document JSON files (12 documents)
+│ ├── journals/ → Journal JSON files (2 journals, 300 chapters)
+│ └── magazines/ → Magazine JSON files (2 magazines, 59 issues)
+│ ├── btg/ → Back to Godhead (1944–1960), 29 issues
+│ └── btg-1978/ → Back to Godhead (1966–1973), 30 issues
├── prisma/
-│ └── schema.prisma → Verse, BookMeta, ProseChapter, Essay, GlossaryEntry models
+│ └── schema.prisma → Verse, BookMeta, ProseChapter, Essay, GlossaryEntry, OtherDocumentMeta, OtherDocumentChapter, JournalMeta, JournalChapter, MagazineMeta, MagazineIssue models
├── scripts/
│ ├── bg_extract.py → BG extraction from bg.html
│ ├── sb_extract.py → SB extraction from sb.html
@@ -141,6 +170,15 @@ spsays-training/
│ ├── seed-db.ts → Main MongoDB seeder (BG, SB, CC, SB1962, KB, glossary)
│ ├── seed-nod.ts … seed-ds.ts → Individual book seeders
│ ├── seed-te.ts → Two Essays seeder (uses essays collection)
+│ ├── seed-va.ts → Vaiśiṣṭyāṣṭakam seeder
+│ ├── seed-pre-1977-essays.ts → Pre-1977 essays seeder (13 essays)
+│ ├── seed-other-documents.ts → Other documents seeder (dedicated collections)
+│ ├── seed-journals.ts → Journals seeder (dedicated collections)
+│ ├── jaladuta_diary_extract.py → Jaladuta Diary extraction (83 day entries)
+│ ├── the_beginning_extract.py → The Beginning extraction (189 day entries + 20 excerpts)
+│ ├── btg_extract.py → BTG (1944–1960) extraction from btg-1960.html
+│ ├── btg_1978_extract.py → BTG (1966–1973) extraction from btg-1978.html
+│ ├── seed-magazines.ts → Magazines seeder (dedicated collections)
│ ├── kb_extract_*.py → KB extraction scripts (intro, vol1, vol2)
│ ├── inject-fonts.py → Inject @font-face rules into converted HTML
│ └── fix-bengali-encoding.py → Fix Bengali font encoding in cc.html
@@ -226,6 +264,77 @@ model Essay {
@@map("essays")
}
+model OtherDocumentMeta {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String @unique
+ data Json
+
+ @@map("other_document_meta")
+}
+
+model OtherDocumentChapter {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String
+ chapter Int
+ title String
+ type String
+ slug String
+ order Int
+ paragraphs String[]
+
+ @@unique([book, slug])
+ @@index([book])
+ @@map("other_document_chapters")
+}
+
+model JournalMeta {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String @unique
+ data Json
+
+ @@map("journal_meta")
+}
+
+model JournalChapter {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String
+ chapter Int
+ title String
+ type String
+ slug String
+ order Int
+ paragraphs String[]
+
+ @@unique([book, slug])
+ @@index([book])
+ @@map("journal_chapters")
+}
+
+model MagazineMeta {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String @unique
+ data Json
+
+ @@map("magazine_meta")
+}
+
+model MagazineIssue {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ book String
+ volume Int
+ part Int
+ title String
+ slug String
+ order Int
+ paragraphs String[]
+ paragraphsJson String? // JSON string fallback for large issues (BSON wire protocol workaround)
+
+ @@unique([book, slug])
+ @@index([book])
+ @@index([book, volume])
+ @@map("magazine_issues")
+}
+
model GlossaryEntry {
id String @id @default(auto()) @map("_id") @db.ObjectId
term String
@@ -285,6 +394,7 @@ model GlossaryEntry {
| Śrī Caitanya-bhāgavata Commentary | `cbhc` | 4 | others-before-1977.html |
| Bhagavāner Kathā | `bk` | 2 | others-before-1977.html (ScaGoudy) |
| Prayers of King Kulaśekhara (1958) | `pk` | 6 | others-before-1977.html |
+| Vaiśiṣṭyāṣṭakam | `va` | 8 | others-before-1977.html |
### Essays (stored in `essays` collection)
@@ -297,18 +407,53 @@ model GlossaryEntry {
| The Causes of Lord Caitanya's Incarnation | `causes-of-lord-caitanyas-incarnation` | `cc-essays` | 1957 |
| The Confidential Reasons for Lord Caitanya's Appearance | `confidential-reasons-for-lord-caitanyas-appearance` | `cc-essays` | 1957 |
| The Glories of Lord Nityānanda | `glories-of-lord-nityananda` | `cc-essays` | 1957 |
+| Man Is the Architect of His Own Fortune | `man-is-the-architect-of-his-own-fortune` | `pre-1977-essays` | — |
+| What Is the Matter with the World? | `what-is-the-matter-with-the-world` | `pre-1977-essays` | 1957 |
+| Religion Is Godly Life | `religion-is-godly-life` | `pre-1977-essays` | — |
+| Truth and Beauty | `truth-and-beauty` | `pre-1977-essays` | 1958 |
+| Misuse of Public Fund in the Name of Religion | `misuse-of-public-fund-in-the-name-of-religion` | `pre-1977-essays` | 1958 |
+| Perfection at Home | `perfection-at-home` | `pre-1977-essays` | — |
+| How Should One Cultivate Human Spirit | `how-should-one-cultivate-human-spirit` | `pre-1977-essays` | 1961 |
+| Viswa Dharma | `viswa-dharma` | `pre-1977-essays` | — |
+| Glorification of Śrīla Bhaktisiddhānta Sarasvatī Ṭhākura | `glorification-of-srila-bhaktisiddhanta-sarasvati-thakura` | `pre-1977-essays` | 1952 |
+| Who Is Crazy? (Back to Godhead #66) | `who-is-crazy-btg66` | `pre-1977-essays` | — |
+| Manifestation of Dharma in Human Being | `manifestation-of-dharma-in-human-being` | `pre-1977-essays` | 1972 |
+| Message of Krishna Consciousness | `message-of-krishna-consciousness` | `pre-1977-essays` | 1972 |
+| A Wrong Civilization | `a-wrong-civilization` | `pre-1977-essays` | 1936 |
| Vedānta-sūtra (Typed Transcript) | `vedanta-sutra-typed` | `typed-transcripts` | 1969 |
| Shaktipujah | `shaktipujah` | `reviews` | — |
| Devotee and the Divinity | `devotee-and-the-divinity` | `standalone` | — |
-### Other Documents (stored in `book_meta` + `prose_chapters` with `od-` prefix)
+### Other Documents (stored in `other_document_meta` + `other_document_chapters` collections)
| Document | Code | Chapters |
|----------|------|----------|
-| An Analysis of Bhagwat Geeta | `od-analysis-bg` | 3 |
-| Solution of Present Crisis by Bhagwat Geeta | `od-solution-crisis` | 4 |
-| Śrī Caitanya Mahāprabhu | `od-sri-caitanya` | 3 |
-| Conception of Geeta Nagari on the Principles of Gandhi | `od-geeta-nagari` | 1 |
+| An Analysis of Bhagwat Geeta | `analysis-bg` | 3 |
+| Solution of Present Crisis by Bhagwat Geeta | `solution-crisis` | 4 |
+| Śrī Caitanya Mahāprabhu | `sri-caitanya` | 3 |
+| Conception of Geeta Nagari on the Principles of Gandhi | `geeta-nagari` | 1 |
+| The Real Peace Formula | `real-peace-formula` | 3 |
+| International Institution for God Consciousness | `intl-institution-god-consciousness` | 1 |
+| "Srimad Bhawatam" India's Message of Peace and Goodwill | `srimad-bhawatam-message` | 1 |
+| Viraha Aṣṭaka | `viraha-astaka` | 8 |
+| Vṛndāvana Bhajana | `vrndavana-bhajana` | 1 |
+| Prayer unto the Lotus Feet of Kṛṣṇa | `prayer-unto-lotus-feet-of-krsna` | 1 |
+| Märkine Bhägavata-dharma | `markine-bhagavata-dharma` | 1 |
+| Jaladuta Pamphlet | `jaladuta-pamphlet` | 1 |
+
+### Journals (stored in `journal_meta` + `journal_chapters` collections)
+
+| Journal | Code | Chapters | Structure |
+|---------|------|----------|-----------|
+| The Jaladuta Diary, 1965 | `jaladuta-diary` | 85 | Preface, Introduction, 83 day entries grouped by month (Apr–Nov 1965) |
+| The Beginning — The 1966 New York Journal | `the-beginning` | 215 | Introduction, 3 place-based chapters with day entries, Memoranda, Epilogue, 20 Excerpts |
+
+### Magazines (stored in `magazine_meta` + `magazine_issues` collections)
+
+| Magazine | Code | Issues | Articles | Flag | Source |
+|----------|------|--------|----------|------|--------|
+| Back to Godhead (1944–1960) | `btg` | 29 | 89 | Pre 1978 | btg-1960.html |
+| Back to Godhead (1966–1973) | `btg-1978` | 30 | 1,035 | Pre 1978 | btg-1978.html |
### Other
@@ -389,17 +534,33 @@ IAST transliteration → Devanagari via `indic-transliteration` library. Key fix
| Route | Content |
|-------|---------|
-| `/essays` | All essays listing (10 essays across 4 groups) |
+| `/essays` | Searchable essay grid (23 essays across 6 groups) |
| `/essays/[slug]` | Individual essay reader |
### Other Documents
| Route | Content |
|-------|---------|
-| `/other-documents` | All other documents listing |
+| `/other-documents` | Searchable other documents grid (12 documents) |
| `/other-documents/[slug]` | Document index (chapter listing) |
| `/other-documents/[slug]/[chapter]` | Individual chapter reader |
+### Journals
+
+| Route | Content |
+|-------|---------|
+| `/journals` | Journals index (grid) |
+| `/journals/[slug]` | Journal chapter listing (hierarchical: place → month → day) |
+| `/journals/[slug]/[chapter]` | Individual journal entry reader (slug-based, e.g., `apr-3`, `sep-17`, `excerpt-...`) |
+
+### Magazines
+
+| Route | Content |
+|-------|---------|
+| `/magazines` | Magazines index (grouped by flag: "Pre 1978 Editions") |
+| `/magazines/[slug]` | Magazine issue listing (grouped by volume) |
+| `/magazines/[slug]/[issue]` | Individual issue reader (slug-based, e.g., `v1-p1`, `no-17`) |
+
### Other routes
| Route | Content |
@@ -407,6 +568,8 @@ IAST transliteration → Devanagari via `indic-transliteration` library. Key fix
| `/` | Landing page |
| `/books` | Searchable book grid |
| `/glossary` | Searchable glossary (2,749 entries) |
+| `/journals` | Journals (2 journals, 300 chapters) |
+| `/magazines` | Magazines (2 magazines, 59 issues, Pre 1978) |
| `/rtfs` | RTF viewer (gated by ALLOW_RTF=true) |
| `/api/verify-access` | Password protection endpoint |
diff --git a/app/letters/[slug]/page.tsx b/app/letters/[slug]/page.tsx
new file mode 100644
index 00000000..3b55b82b
--- /dev/null
+++ b/app/letters/[slug]/page.tsx
@@ -0,0 +1,117 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getLetter, getAdjacentLetters } from "@/lib/letters";
+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";
+
+export default async function LetterPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+
+ const letter = await getLetter(slug);
+ if (!letter) notFound();
+
+ const { prev, next } = await getAdjacentLetters(slug, letter.year);
+
+ const backHref = letter.year ? `/letters/year/${letter.year}` : "/letters";
+ const backLabel = letter.year ? `Letters from ${letter.year}` : "Letters";
+
+ return (
+
+
+
+ ← {backLabel}
+
+
+ Letter to {letter.recipientDisplay}
+
+
+ {letter.date}
+ {letter.location ? ` · ${letter.location}` : ""}
+
+
+
+
+ {/* Top nav */}
+
+
+ {/* Letter content */}
+
+
+
+ {letter.paragraphs.map((paragraph, i) => (
+
+ ))}
+
+
+
+
+ {/* Bottom nav */}
+
+
+
+ );
+}
+
+function LetterNav({
+ prev,
+ next,
+}: {
+ prev: { slug: string; recipientDisplay: string } | null;
+ next: { slug: string; recipientDisplay: string } | null;
+}) {
+ return (
+
+ {prev ? (
+
+
+
+ {prev.recipientDisplay}
+
+
Previous
+
+ ) : (
+
+ )}
+
+ {next ? (
+
+
+ {next.recipientDisplay}
+
+
Next
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/app/letters/letters-browser.tsx b/app/letters/letters-browser.tsx
new file mode 100644
index 00000000..71846dac
--- /dev/null
+++ b/app/letters/letters-browser.tsx
@@ -0,0 +1,285 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import Link from "next/link";
+import {
+ Card,
+ CardContent,
+} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+import { Search, ChevronLeft, ChevronRight } from "lucide-react";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+export type YearGroup = { year: number; count: number };
+export type RecipientGroup = { name: string; count: number };
+export type LocationGroup = { name: string; count: number };
+
+// ---------------------------------------------------------------------------
+// Diacritic-aware normalize (matches books-grid pattern)
+// ---------------------------------------------------------------------------
+
+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")
+ .replace(/ā/g, "a")
+ .replace(/ī/g, "i")
+ .replace(/ū/g, "u")
+ .normalize("NFD")
+ .replace(/[\u0300-\u036f]/g, "");
+}
+
+// ---------------------------------------------------------------------------
+// Pagination
+// ---------------------------------------------------------------------------
+
+const PAGE_SIZE_RECIPIENTS = 50;
+const PAGE_SIZE_LOCATIONS = 60;
+const PAGE_SIZE_YEARS = 100; // years are small cards, show all usually
+
+function Pagination({
+ page,
+ totalPages,
+ onPageChange,
+}: {
+ page: number;
+ totalPages: number;
+ onPageChange: (p: number) => void;
+}) {
+ if (totalPages <= 1) return null;
+
+ return (
+
+ onPageChange(page - 1)}
+ disabled={page <= 1}
+ className={cn(
+ "flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
+ page <= 1
+ ? "text-muted-foreground/40 cursor-not-allowed"
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
+ )}
+ >
+
+ Previous
+
+
+
+ Page {page} of {totalPages}
+
+
+ onPageChange(page + 1)}
+ disabled={page >= totalPages}
+ className={cn(
+ "flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
+ page >= totalPages
+ ? "text-muted-foreground/40 cursor-not-allowed"
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
+ )}
+ >
+ Next
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Component
+// ---------------------------------------------------------------------------
+
+export function LettersBrowser({
+ currentView,
+ years,
+ recipients,
+ locations,
+}: {
+ currentView: string;
+ years: YearGroup[];
+ recipients: RecipientGroup[];
+ locations: LocationGroup[];
+}) {
+ const [query, setQuery] = useState("");
+ const [page, setPage] = useState(1);
+
+ // Reset page when query or view changes
+ const handleQueryChange = (q: string) => {
+ setQuery(q);
+ setPage(1);
+ };
+
+ // --- Filtered data ---
+
+ const filteredYears = useMemo(() => {
+ if (!query.trim()) return years;
+ const q = normalize(query.trim());
+ return years.filter((yg) => String(yg.year).includes(q));
+ }, [years, query]);
+
+ const filteredRecipients = useMemo(() => {
+ if (!query.trim()) return recipients;
+ const q = normalize(query.trim());
+ return recipients.filter((rg) => normalize(rg.name).includes(q));
+ }, [recipients, query]);
+
+ const filteredLocations = useMemo(() => {
+ if (!query.trim()) return locations;
+ const q = normalize(query.trim());
+ return locations.filter((lg) => normalize(lg.name).includes(q));
+ }, [locations, query]);
+
+ // --- Paginated data ---
+
+ const activeData =
+ currentView === "year"
+ ? filteredYears
+ : currentView === "recipient"
+ ? filteredRecipients
+ : filteredLocations;
+
+ const pageSize =
+ currentView === "year"
+ ? PAGE_SIZE_YEARS
+ : currentView === "recipient"
+ ? PAGE_SIZE_RECIPIENTS
+ : PAGE_SIZE_LOCATIONS;
+
+ const totalPages = Math.max(1, Math.ceil(activeData.length / pageSize));
+ const safePage = Math.min(page, totalPages);
+ const startIdx = (safePage - 1) * pageSize;
+ const endIdx = startIdx + pageSize;
+
+ const placeholder =
+ currentView === "year"
+ ? "Search years..."
+ : currentView === "recipient"
+ ? "Search recipients..."
+ : "Search locations...";
+
+ const resultCount = activeData.length;
+ const totalCount =
+ currentView === "year"
+ ? years.length
+ : currentView === "recipient"
+ ? recipients.length
+ : locations.length;
+
+ return (
+
+ {/* Search */}
+
+
+ handleQueryChange(e.target.value)}
+ className="pl-9"
+ />
+
+
+ {/* Result count */}
+ {query.trim() && (
+
+ {resultCount === 0
+ ? "No results found"
+ : `${resultCount} of ${totalCount} ${currentView === "year" ? "years" : currentView === "recipient" ? "recipients" : "locations"}`}
+
+ )}
+
+ {/* Year view */}
+ {currentView === "year" && (
+
+ {(filteredYears.slice(startIdx, endIdx) as YearGroup[]).map((yg) => (
+
+
+
+
+ {yg.year}
+
+
+ {yg.count} {yg.count === 1 ? "letter" : "letters"}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Recipient view */}
+ {currentView === "recipient" && (
+
+ {(filteredRecipients.slice(startIdx, endIdx) as RecipientGroup[]).map(
+ (rg) => (
+
+
+
+
+ {rg.name}
+
+
+ {rg.count}
+
+
+
+
+ ),
+ )}
+
+ )}
+
+ {/* Location view */}
+ {currentView === "location" && (
+
+ {(filteredLocations.slice(startIdx, endIdx) as LocationGroup[]).map(
+ (lg) => (
+
+
+
+ {lg.name}
+
+ {lg.count} {lg.count === 1 ? "letter" : "letters"}
+
+
+
+
+ ),
+ )}
+
+ )}
+
+ {/* Pagination */}
+
+
+ );
+}
diff --git a/app/letters/letters-nav.tsx b/app/letters/letters-nav.tsx
new file mode 100644
index 00000000..5e826883
--- /dev/null
+++ b/app/letters/letters-nav.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import { Calendar, User, MapPin } from "lucide-react";
+
+const views = [
+ { key: "year", label: "By Year", icon: Calendar },
+ { key: "recipient", label: "By Recipient", icon: User },
+ { key: "location", label: "By Location", icon: MapPin },
+];
+
+export function LettersNav({ currentView }: { currentView: string }) {
+ return (
+
+ {views.map((v) => {
+ const Icon = v.icon;
+ const isActive = currentView === v.key;
+ return (
+
+
+ {v.label}
+
+ );
+ })}
+
+ );
+}
diff --git a/app/letters/location/[name]/page.tsx b/app/letters/location/[name]/page.tsx
new file mode 100644
index 00000000..ca8baede
--- /dev/null
+++ b/app/letters/location/[name]/page.tsx
@@ -0,0 +1,64 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getLettersByLocation } from "@/lib/letters";
+import { PageHeader } from "@/components/page-header";
+import { Card, CardContent } from "@/components/ui/card";
+
+export default async function LettersByLocationPage({
+ params,
+}: {
+ params: Promise<{ name: string }>;
+}) {
+ const { name: encodedName } = await params;
+ const name = decodeURIComponent(encodedName);
+
+ const letters = await getLettersByLocation(name);
+ if (letters.length === 0) notFound();
+
+ return (
+
+
+
+ ← Letters by Location
+
+
+ Letters from {name}
+
+
+ {letters.length} {letters.length === 1 ? "letter" : "letters"}
+
+
+
+
+
+
+
+ {letters.map((letter) => (
+
+
+
+
+ To: {letter.recipientDisplay}
+
+
+ {letter.date || "Undated"}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/letters/page.tsx b/app/letters/page.tsx
new file mode 100644
index 00000000..1de0d238
--- /dev/null
+++ b/app/letters/page.tsx
@@ -0,0 +1,55 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { PageHeader } from "@/components/page-header";
+import {
+ getLetterYears,
+ getLetterRecipients,
+ getLetterLocations,
+ getTotalLetterCount,
+} from "@/lib/letters";
+import { LettersNav } from "./letters-nav";
+import { LettersBrowser } from "./letters-browser";
+
+export default async function LettersPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ view?: string }>;
+}) {
+ const { view = "year" } = await searchParams;
+
+ const [years, recipients, locations, totalCount] = await Promise.all([
+ getLetterYears(),
+ getLetterRecipients(),
+ getLetterLocations(),
+ getTotalLetterCount(),
+ ]);
+
+ return (
+
+
+
+ ← Home
+
+ Letters
+
+ {totalCount.toLocaleString()} letters by Śrīla Prabhupāda (1947–1977)
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/letters/recipient/[name]/page.tsx b/app/letters/recipient/[name]/page.tsx
new file mode 100644
index 00000000..a95cc89c
--- /dev/null
+++ b/app/letters/recipient/[name]/page.tsx
@@ -0,0 +1,65 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getLettersByRecipient } from "@/lib/letters";
+import { PageHeader } from "@/components/page-header";
+import { Card, CardContent } from "@/components/ui/card";
+
+export default async function LettersByRecipientPage({
+ params,
+}: {
+ params: Promise<{ name: string }>;
+}) {
+ const { name: encodedName } = await params;
+ const name = decodeURIComponent(encodedName);
+
+ const letters = await getLettersByRecipient(name);
+ if (letters.length === 0) notFound();
+
+ return (
+
+
+
+ ← Letters by Recipient
+
+
+ Letters to or including {name}
+
+
+ {letters.length} {letters.length === 1 ? "letter" : "letters"}
+
+
+
+
+
+
+
+ {letters.map((letter) => (
+
+
+
+
+ To: {letter.recipientDisplay}
+
+
+ {letter.date || "Undated"}
+ {letter.location ? ` · ${letter.location}` : ""}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/letters/year/[year]/page.tsx b/app/letters/year/[year]/page.tsx
new file mode 100644
index 00000000..40af1aaf
--- /dev/null
+++ b/app/letters/year/[year]/page.tsx
@@ -0,0 +1,66 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { getLettersByYear } from "@/lib/letters";
+import { PageHeader } from "@/components/page-header";
+import { Card, CardContent } from "@/components/ui/card";
+
+export default async function LettersByYearPage({
+ params,
+}: {
+ params: Promise<{ year: string }>;
+}) {
+ const { year: yearStr } = await params;
+ const year = parseInt(yearStr, 10);
+ if (isNaN(year)) notFound();
+
+ const letters = await getLettersByYear(year);
+ if (letters.length === 0) notFound();
+
+ return (
+
+
+
+ ← Letters by Year
+
+
+ Letters from {year}
+
+
+ {letters.length} {letters.length === 1 ? "letter" : "letters"}
+
+
+
+
+
+
+
+ {letters.map((letter) => (
+
+
+
+
+ To: {letter.recipientDisplay}
+
+
+ {letter.date}
+ {letter.location ? ` · ${letter.location}` : ""}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/app/magazines/[slug]/[issue]/page.tsx b/app/magazines/[slug]/[issue]/page.tsx
new file mode 100644
index 00000000..bbc36385
--- /dev/null
+++ b/app/magazines/[slug]/[issue]/page.tsx
@@ -0,0 +1,223 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import {
+ getMagazineContent,
+ getMagazineMeta,
+ getMagazineSidebarItems,
+ slugToBookCode,
+ type MagazineSidebarItem,
+} from "@/lib/magazines";
+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 MagazineIssuePage({
+ params,
+}: {
+ params: Promise<{ slug: string; issue: string }>;
+}) {
+ const { slug, issue: issueSlug } = await params;
+ const bookCode = slugToBookCode(slug);
+
+ const [meta, content, sidebarItems] = await Promise.all([
+ getMagazineMeta(bookCode),
+ getMagazineContent(bookCode, issueSlug),
+ getMagazineSidebarItems(bookCode),
+ ]);
+
+ if (!meta || !content) notFound();
+
+ const navItems = sidebarItems.filter((item) => item.type !== "heading");
+ const currentIdx = navItems.findIndex((item) => item.slug === issueSlug);
+ const prev = currentIdx > 0 ? navItems[currentIdx - 1] : null;
+ const next =
+ currentIdx < navItems.length - 1 ? navItems[currentIdx + 1] : null;
+
+ return (
+
+
+
+ ← {meta.title}
+
+
+ {content.title}
+
+
+
+
+
+
+
+
+
+
+
+
+ {content.paragraphs.map((paragraph, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+function IssueSidebar({
+ items,
+ currentSlug,
+ magSlug,
+}: {
+ items: MagazineSidebarItem[];
+ currentSlug: string;
+ magSlug: string;
+}) {
+ return (
+
+
+ Issues
+
+
+ {items.map((item, i) =>
+ item.type === "heading" ? (
+
+ {item.label}
+
+ ) : (
+
+ {item.label}
+
+ ),
+ )}
+
+
+ );
+}
+
+function IssueNav({
+ prev,
+ next,
+ navItems,
+ currentSlug,
+ magSlug,
+ magTitle,
+ showMobileNav = false,
+}: {
+ prev: MagazineSidebarItem | null;
+ next: MagazineSidebarItem | null;
+ navItems: MagazineSidebarItem[];
+ currentSlug: string;
+ magSlug: string;
+ magTitle: string;
+ showMobileNav?: boolean;
+}) {
+ const mobileNavGroups: MobileNavGroup[] = [];
+ if (showMobileNav && navItems.length > 0) {
+ mobileNavGroups.push({
+ title: "Issues",
+ items: navItems.map((i) => ({
+ slug: i.slug,
+ label: i.label,
+ href: `/magazines/${magSlug}/${i.slug}`,
+ type: i.type,
+ })),
+ });
+ }
+
+ return (
+
+
+ {showMobileNav && (
+
+ )}
+ {prev ? (
+
+
+
+ {prev.label}
+
+
Previous
+
+ ) : (
+
+ )}
+
+
+ {next ? (
+
+
+ {next.label}
+
+
Next
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/app/magazines/[slug]/page.tsx b/app/magazines/[slug]/page.tsx
new file mode 100644
index 00000000..43017a21
--- /dev/null
+++ b/app/magazines/[slug]/page.tsx
@@ -0,0 +1,96 @@
+export const dynamic = "force-dynamic";
+
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import {
+ getMagazineMeta,
+ slugToBookCode,
+ type MagazineChapterEntry,
+} from "@/lib/magazines";
+import { PageHeader } from "@/components/page-header";
+import { Card, CardContent } from "@/components/ui/card";
+
+export default async function MagazineIndexPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+ const bookCode = slugToBookCode(slug);
+ const meta = await getMagazineMeta(bookCode);
+ if (!meta) notFound();
+
+ return (
+
+
+
+ ← Magazines
+
+
+ {meta.title}
+
+ {meta.author}
+
+
+
+ {meta.description && (
+ {meta.description}
+ )}
+
+ {meta.chapters.map((ch: MagazineChapterEntry, idx: number) => {
+ if ("slug" in ch && !("subChapters" in ch)) {
+ return (
+
+
+
+ {ch.title}
+
+
+
+ );
+ }
+
+ if ("subChapters" in ch) {
+ const group = ch as {
+ title: string;
+ subChapters: { slug: string; title: string }[];
+ };
+ return (
+
+
+ {group.title}
+
+
+
+
+ {group.subChapters.map((sc) => (
+
+
+
+ {sc.title}
+
+
+
+ ))}
+
+
+
+
+ );
+ }
+
+ return null;
+ })}
+
+
+ );
+}
diff --git a/app/magazines/page.tsx b/app/magazines/page.tsx
new file mode 100644
index 00000000..60d9f85e
--- /dev/null
+++ b/app/magazines/page.tsx
@@ -0,0 +1,110 @@
+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 { getAllMagazines, bookCodeToSlug } from "@/lib/magazines";
+
+export default async function MagazinesPage() {
+ const magazines = await getAllMagazines();
+
+ // Group magazines by flag
+ const grouped: Record = {};
+ for (const mag of magazines) {
+ const key = mag.flag ?? "Other";
+ if (!grouped[key]) grouped[key] = [];
+ grouped[key].push(mag);
+ }
+
+ // Define display order for groups
+ const groupOrder = ["Pre 1978", "Other"];
+ const groupLabels: Record = {
+ "Pre 1978": "Pre 1978 Editions",
+ Other: "Other Editions",
+ };
+
+ const sortedGroups = Object.keys(grouped).sort(
+ (a, b) => groupOrder.indexOf(a) - groupOrder.indexOf(b),
+ );
+
+ // If there's only one group, don't show group headings
+ const showGroupHeadings = sortedGroups.length > 1;
+
+ return (
+
+
+
+ ← Home
+
+ Magazines
+
+ Back to Godhead and other periodicals by Śrīla Prabhupāda
+
+
+
+
+ {sortedGroups.map((group) => (
+
+ {showGroupHeadings && (
+
+ {groupLabels[group] ?? group}
+
+ )}
+
+
+ {grouped[group].map((mag) => (
+
+
+
+
+ {mag.title}
+
+ {mag.description && (
+
+ {mag.description}
+
+ )}
+
+
+
+ {mag.flag && (
+
+ {mag.flag}
+
+ )}
+ {mag.year && (
+
+ Since {mag.year}
+
+ )}
+ {mag.totalIssues > 0 && (
+
+ {mag.totalIssues} issues
+
+ )}
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index b70c7b22..4ef5d7cd 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -16,6 +16,8 @@ import {
PenLine,
FileStack,
NotebookPen,
+ Newspaper,
+ Mail,
} from "lucide-react";
const allowRtf = process.env.ALLOW_RTF === "true";
@@ -27,11 +29,11 @@ export default function Home() {
-
-
+
+
{/* Structured Books (JSON) */}
-
-
+
+
@@ -58,8 +60,8 @@ export default function Home() {
{/* Essays */}
-
-
+
+
@@ -83,9 +85,64 @@ export default function Home() {
+ {/* Magazines */}
+
+
+
+
+
+
+
+
+
+ Magazines
+
+
+ Back to Godhead & other periodicals
+
+
+
+
+
+
+ The original Back to Godhead magazine founded by Śrīla
+ Prabhupāda in 1944 — 29 issues across 3 volumes with 89
+ articles on Kṛṣṇa consciousness, Vedic philosophy, and
+ spiritual life.
+
+
+
+
+
+ {/* Letters */}
+
+
+
+
+
+
+
+
+ Letters
+
+ 6,800+ letters (1947–1977)
+
+
+
+
+
+
+ Śrīla Prabhupāda's personal correspondence — over 6,800
+ letters to disciples, Godbrothers, government officials, and
+ well-wishers. Browse by year, recipient, or location.
+
+
+
+
+
{/* Journals */}
-
-
+
+
@@ -113,8 +170,8 @@ export default function Home() {
{/* Glossary */}
-
-
+
+
@@ -141,8 +198,8 @@ export default function Home() {
{/* Other Documents */}
-
-
+
+
@@ -171,8 +228,8 @@ export default function Home() {
{/* RTF Document Viewer — only visible when ALLOW_RTF=true */}
{allowRtf && (
-
-
+
+
diff --git a/lib/letters.ts b/lib/letters.ts
new file mode 100644
index 00000000..034719f7
--- /dev/null
+++ b/lib/letters.ts
@@ -0,0 +1,249 @@
+/**
+ * Data access layer for Letters.
+ *
+ * Provides queries for browsing letters by year, recipient, and location.
+ * Uses the Prisma Letter model (collection: letters).
+ *
+ * Recipients are stored as an array (e.g., ["Mukunda", "Janaki"]).
+ * The recipient list aggregates individual names across all letters,
+ * and searching for "Mukunda" finds all letters where Mukunda is
+ * ANY recipient (individual or group).
+ */
+
+import { prisma } from "@/lib/db";
+
+// Types
+export type LetterSummary = {
+ slug: string;
+ recipients: string[];
+ recipientDisplay: string;
+ date: string | null;
+ year: number | null;
+ month: number | null;
+ location: string | null;
+};
+
+export type LetterFull = {
+ slug: string;
+ recipients: string[];
+ recipientDisplay: string;
+ date: string | null;
+ dateRef: string | null;
+ year: number | null;
+ month: number | null;
+ day: number | null;
+ location: string | null;
+ salutation: string | null;
+ paragraphs: string[];
+};
+
+export type YearGroup = {
+ year: number;
+ count: number;
+};
+
+export type RecipientGroup = {
+ name: string;
+ count: number;
+};
+
+export type LocationGroup = {
+ name: string;
+ count: number;
+};
+
+// Sanitize
+function sanitizeHtml(html: string): string {
+ let clean = html.replace(/