Compare commits
147
Commits
feat/go-bff
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1984797ad3 | ||
|
|
1582df0ed5 | ||
|
|
1683abc2e9 | ||
|
|
12a5c15121 | ||
|
|
8bc27bb8a1 | ||
|
|
89391279bb | ||
|
|
7f716397a0 | ||
|
|
4586f1c22c | ||
|
|
b6471f9430 | ||
|
|
380c23e469 | ||
|
|
0b67a1142f | ||
|
|
be28585294 | ||
|
|
1462844e95 | ||
|
|
9916810d18 | ||
|
|
7b81bbc791 | ||
|
|
f92b41384e | ||
|
|
5cd75c27f9 | ||
|
|
5679f7a639 | ||
|
|
25db9d0eee | ||
|
|
0178f89f2b | ||
|
|
b6dc762984 | ||
|
|
1fe10adb1e | ||
|
|
84cb273941 | ||
|
|
071eb4d58a | ||
|
|
bc1798b31b | ||
|
|
fc2aef0c3d | ||
|
|
89f94c1a6e | ||
|
|
f5abe33c09 | ||
|
|
f96e7dd2f1 | ||
|
|
0cf6df5942 | ||
|
|
5c72416ae8 | ||
|
|
197e13c0ef | ||
|
|
0608e2b6f6 | ||
|
|
cb376f8497 | ||
|
|
eac56b7974 | ||
|
|
be4c2f17fe | ||
|
|
6cc9285bad | ||
|
|
ab35767374 | ||
|
|
c551d2f7a8 | ||
|
|
a4fd63695a | ||
|
|
91621693a6 | ||
|
|
79f0bd9223 | ||
|
|
7585e6cb31 | ||
|
|
c8de98c8de | ||
|
|
72a092b3c9 | ||
|
|
752ad20fed | ||
|
|
0280a81777 | ||
|
|
69e5f2c45c | ||
|
|
45c7cf047e | ||
|
|
a4baf6d215 | ||
|
|
455ed0f7cb | ||
|
|
7c3cf656b5 | ||
|
|
23d06a3db5 | ||
|
|
3accc753d0 | ||
|
|
b04ed1d3aa | ||
|
|
9808dd3033 | ||
|
|
b248f962a1 | ||
|
|
61581b67a7 | ||
|
|
eb0c981d3c | ||
|
|
37305201bd | ||
|
|
4471ef10aa | ||
|
|
397a09e814 | ||
|
|
9e68184750 | ||
|
|
439664a97d | ||
|
|
cc5633c37a | ||
|
|
6a0559e608 | ||
|
|
25a729959f | ||
|
|
af5ab9cf6b | ||
|
|
884a984e50 | ||
|
|
ead8ad02e8 | ||
|
|
4b159cb4d9 | ||
|
|
3903423d06 | ||
|
|
a46e29d998 | ||
|
|
922d76e21f | ||
|
|
909ae67320 | ||
|
|
482a845f37 | ||
|
|
647a803b96 | ||
|
|
e36afdd1a5 | ||
|
|
ef6e280524 | ||
|
|
cf733062f8 | ||
|
|
421b41cade | ||
|
|
deeda40c9d | ||
|
|
90df5bc784 | ||
|
|
0334e03c3c | ||
|
|
ecb32d2e85 | ||
|
|
d5fb350652 | ||
|
|
bcffe9684f | ||
|
|
0db339254d | ||
|
|
684ea2ae97 | ||
|
|
0f433c5708 | ||
|
|
48dd450a15 | ||
|
|
ddc6d3c7bf | ||
|
|
fd333159f6 | ||
|
|
0bcda5a791 | ||
|
|
c89cc8b779 | ||
|
|
ec67ddf852 | ||
|
|
086f1dd8aa | ||
|
|
b61c3df473 | ||
|
|
cb70a84dd5 | ||
|
|
dc01d09f09 | ||
|
|
84d86c45b5 | ||
|
|
4d45e20cb1 | ||
|
|
e72679d1b6 | ||
|
|
91d5d272d2 | ||
|
|
0352cb2003 | ||
|
|
8a8ce24479 | ||
|
|
7d76afe102 | ||
|
|
8c1f3a1b7b | ||
|
|
3624c19d2d | ||
|
|
5988d02373 | ||
|
|
17fb83087f | ||
|
|
489adb0eeb | ||
|
|
d8de0a18e5 | ||
|
|
c20d6bffe8 | ||
|
|
01bcc057fa | ||
|
|
8bd82a0e8a | ||
|
|
5506afa758 | ||
|
|
88f1b08675 | ||
|
|
336f478f8b | ||
|
|
03b8f85e06 | ||
|
|
0ff8005df2 | ||
|
|
0ae3334d98 | ||
|
|
ec20eb996b | ||
|
|
dcdd2383c6 | ||
|
|
7bd2952caa | ||
|
|
6b6ce0813e | ||
|
|
3767a3a9b6 | ||
|
|
a22fcf360d | ||
|
|
804cf1ad9a | ||
|
|
30d08c665d | ||
|
|
cc94296b46 | ||
|
|
8cd1d2e724 | ||
|
|
179b7d8862 | ||
|
|
7294d9e15b | ||
|
|
e3df40559b | ||
|
|
9a3056fbd8 | ||
|
|
db8deb232b | ||
|
|
730efb1bb3 | ||
|
|
0106d8cee2 | ||
|
|
d917b3ea7d | ||
|
|
7c07b33e4b | ||
|
|
96a83fe1cb | ||
|
|
befcc44980 | ||
|
|
e45d5eb02c | ||
|
|
09fbcf93ed | ||
|
|
4cbb146bc1 | ||
|
|
bf1ff47d3e |
@@ -0,0 +1,20 @@
|
||||
NEXT_PUBLIC_SITE_URL=https://prabhupadasays.org
|
||||
|
||||
# BFF API Server (server-side fetches)
|
||||
BFF_URL=http://localhost:8080
|
||||
|
||||
# BFF API Server (browser-side fetches; must be publicly reachable in prod)
|
||||
NEXT_PUBLIC_BFF_URL=http://localhost:8080
|
||||
|
||||
# RTF Viewer (development only)
|
||||
ALLOW_RTF=true
|
||||
NEXT_PUBLIC_OAUTH_ENABLED=true
|
||||
|
||||
# SOKETI
|
||||
NEXT_PUBLIC_SOKETI_KEY=spsays
|
||||
NEXT_PUBLIC_SOKETI_HOST=ws.prabhupadasays.org
|
||||
NEXT_PUBLIC_SOKETI_PORT=443
|
||||
NEXT_PUBLIC_SOKETI_FORCE_TLS=true
|
||||
|
||||
# Analytics
|
||||
NEXT_PUBLIC_RYBBIT_HOST=https://analytics.krispire.cloud
|
||||
+1
-1
@@ -31,7 +31,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
+152
-252
@@ -15,10 +15,10 @@ Content is sourced from the pre-1980s Vedabase, extracted and indexed into Mongo
|
||||
- **shadcn/ui** (base-nova style, lucide icons) — components: Button, Card, Badge, Tooltip, Input, Drawer
|
||||
- **next-themes** for dark/light/system mode
|
||||
- **vaul** for mobile drawer navigation
|
||||
- **Prisma v6** with MongoDB (provider `prisma-client-js`)
|
||||
- **MongoDB** native driver (`mongodb` package) for seeding (Prisma requires replica set for writes)
|
||||
- **Go BFF API** — all data fetched via `lib/bff.ts` HTTP client calling the Go BFF server (no direct database access)
|
||||
- **framer-motion** for audio player animations (spring enter/exit, minimize/expand transitions, audio wave bars)
|
||||
- **Typesense** (via BFF) — typo-tolerant full-text search across all content
|
||||
- **Python 3** with BeautifulSoup4 + `indic-transliteration` for HTML extraction scripts
|
||||
- **Database**: MongoDB at `DATABASE_URL` env variable with `?authSource=admin`
|
||||
|
||||
---
|
||||
|
||||
@@ -70,9 +70,13 @@ knowledgebase/
|
||||
│ │ ├── place/[place]/page.tsx → Place listing
|
||||
│ │ ├── type/[type]/page.tsx → Type listing (lecture/conversation), year sidebar
|
||||
│ │ └── [slug]/page.tsx → Individual transcription reading page
|
||||
│ ├── search/ → Search page (server + client components)
|
||||
│ │ ├── page.tsx → Server component (URL-driven state)
|
||||
│ │ └── search-content.tsx → Client component (input, filters, results)
|
||||
│ └── api/
|
||||
│ ├── og/route.tsx → Dynamic OG image generator (Noto Sans, Prabhupāda image)
|
||||
│ ├── books/ → Book/verse API routes
|
||||
│ ├── search/route.ts → BFF search proxy (Typesense via /api/v1/search)
|
||||
│ └── glossary/route.ts → Glossary API
|
||||
├── components/
|
||||
│ ├── ui/ → shadcn components (button, card, badge, tooltip, input, drawer)
|
||||
@@ -87,6 +91,7 @@ knowledgebase/
|
||||
│ ├── page-header.tsx → Shared header with ReadingSettings (max-w-5xl)
|
||||
│ ├── reading-settings.tsx → Theme/font/size/justify accessibility panel
|
||||
│ ├── mobile-nav.tsx → Left-slide drawer for mobile sidebar navigation
|
||||
│ ├── header-search.tsx → Header search button with framer-motion expand animation
|
||||
│ ├── site-footer.tsx → Global footer (About + Home links)
|
||||
│ ├── audio-player/ → Site-wide floating audio player
|
||||
│ │ ├── audio-context.tsx → React context + provider, manages <audio> element
|
||||
@@ -96,23 +101,21 @@ knowledgebase/
|
||||
│ ├── logo.tsx → Theme-aware logo component
|
||||
│ └── theme-provider.tsx → next-themes wrapper
|
||||
├── lib/
|
||||
│ ├── db.ts → Prisma client singleton
|
||||
│ ├── api.ts → Core data access (books, verses, prose chapters, glossary)
|
||||
│ ├── bff.ts → BFF API client (typed fetch wrapper: bff<T>(), bffList<T>(), bffOrThrow<T>())
|
||||
│ ├── api.ts → Core data access (books, verses, prose chapters, glossary) — calls BFF API
|
||||
│ ├── books.ts → Generic book data layer (replaces 29 individual book files)
|
||||
│ ├── constants.ts → Shared constants (BOOK_NAMES, CC_LILAS)
|
||||
│ ├── og.ts → OG image metadata builders (ogForVerse, ogForLetter, etc.)
|
||||
│ ├── text-highlight.ts → Text fragment encode/decode, highlight matching
|
||||
│ ├── source-reference.ts → Source reference from URL (for copy attribution)
|
||||
│ ├── disciples.ts → Disciples data access (by letter, place, year, search)
|
||||
│ ├── letters.ts → Letters data access (by year, recipient, location)
|
||||
│ ├── transcriptions.ts → Transcriptions data access (reads from Prisma/MongoDB)
|
||||
│ ├── disciples.ts → Disciples data access — calls BFF API
|
||||
│ ├── letters.ts → Letters data access (by year, recipient, location) — calls BFF API
|
||||
│ ├── transcriptions.ts → Transcriptions data access — calls BFF API
|
||||
│ ├── utils.ts → cn() helper
|
||||
│ ├── [book].ts → Per-book data access layers (nod, tlc, iso, noi, ej, ek, etc.)
|
||||
│ ├── other-documents.ts → Other Documents data access
|
||||
│ ├── journals.ts → Journals data access
|
||||
│ └── magazines.ts → Magazines data access
|
||||
├── prisma/
|
||||
│ └── schema.prisma → All models (Verse, BookMeta, ProseChapter, Essay, Letter,
|
||||
│ GlossaryEntry, Disciple, Transcription, etc.)
|
||||
│ ├── [book].ts → Per-book data access layers (nod, tlc, iso, noi, ej, ek, etc.) — calls BFF API
|
||||
│ ├── other-documents.ts → Other Documents data access — calls BFF API
|
||||
│ ├── journals.ts → Journals data access — calls BFF API
|
||||
│ └── magazines.ts → Magazines data access — calls BFF API
|
||||
└── public/
|
||||
├── fonts/Noto_Sans/static/ → NotoSans-Regular.ttf + NotoSans-Bold.ttf (OG images)
|
||||
├── images/ → OG card image (Prabhupāda)
|
||||
@@ -132,232 +135,50 @@ extractor/ → Extraction & seeding tools (not deployed)
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (Prisma + MongoDB)
|
||||
## Data Architecture
|
||||
|
||||
```prisma
|
||||
type Synonym {
|
||||
word String
|
||||
meanings String[]
|
||||
}
|
||||
The knowledgebase **never connects to MongoDB directly**. All data is served by the **Go BFF API** server.
|
||||
|
||||
model Verse {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
book String // "bg" | "sb" | "cc" | "sb1962" | "iso" | "noi"
|
||||
division String? // null for BG/SB1962/ISO/NOI; canto for SB; lila for CC
|
||||
chapter Int
|
||||
verse Int
|
||||
verseEnd Int?
|
||||
reference String
|
||||
bookTitle String
|
||||
devanagari String
|
||||
bengali String? // only CC has Bengali
|
||||
transliteration String
|
||||
synonyms Synonym[]
|
||||
translation String
|
||||
purport String[]
|
||||
### BFF Server
|
||||
|
||||
@@unique([book, division, chapter, verse])
|
||||
@@index([book])
|
||||
@@index([book, division])
|
||||
@@index([book, division, chapter])
|
||||
@@map("verses")
|
||||
}
|
||||
- **URL**: Configured via `BFF_URL` environment variable (default: `http://localhost:8080`)
|
||||
- The BFF reads from MongoDB — the knowledgebase frontend has no database driver or connection string
|
||||
- All API endpoints live under `/api/v1/...` on the BFF server
|
||||
|
||||
model BookMeta {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
book String @unique
|
||||
data Json
|
||||
### BFF Client (`lib/bff.ts`)
|
||||
|
||||
@@map("book_meta")
|
||||
}
|
||||
The typed fetch wrapper provides three helpers:
|
||||
|
||||
model ProseChapter {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
book String
|
||||
volume Int // 0 = no volumes (sentinel for non-volume books)
|
||||
chapter Int? // null for intro segments
|
||||
title String
|
||||
type String // "chapter" | "intro-segment" | "conclusion"
|
||||
slug String
|
||||
order Int
|
||||
paragraphs String[]
|
||||
- **`bff<T>(path, options?)`** — Fetch JSON from the BFF. Returns `null` on 404, throws `BffError` on other failures.
|
||||
- **`bffList<T>(path, options?)`** — Fetch a list. Returns `[]` on 404 (instead of null).
|
||||
- **`bffOrThrow<T>(path, options?)`** — Fetch JSON, throwing on 404. Use when a missing resource should be an error.
|
||||
|
||||
@@unique([book, volume, slug])
|
||||
@@index([book])
|
||||
@@index([book, volume])
|
||||
@@index([book, volume, type])
|
||||
@@map("prose_chapters")
|
||||
}
|
||||
Options include `params` (query parameters), `revalidate` (Next.js ISR seconds, default 60), and `tags` (cache tags).
|
||||
|
||||
model Essay {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
slug String @unique
|
||||
title String
|
||||
author String
|
||||
year Int?
|
||||
group String? // e.g., "two-essays"
|
||||
order Int
|
||||
paragraphs String[]
|
||||
### Response Envelope
|
||||
|
||||
@@index([group])
|
||||
@@map("essays")
|
||||
}
|
||||
BFF responses use a `{ success: true, data: ... }` envelope. The `bff()` client auto-unwraps this — callers receive the inner `data` value directly.
|
||||
|
||||
model OtherDocumentMeta {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
book String @unique
|
||||
data Json
|
||||
### Case Mapping
|
||||
|
||||
@@map("other_document_meta")
|
||||
}
|
||||
The BFF uses **snake_case** JSON field names (Go convention). Each frontend data layer file (`lib/letters.ts`, `lib/transcriptions.ts`, `lib/books.ts`, etc.) maps BFF responses to **camelCase** TypeScript types used by React components.
|
||||
|
||||
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[]
|
||||
### Data Layer Files
|
||||
|
||||
@@unique([book, slug])
|
||||
@@index([book])
|
||||
@@map("other_document_chapters")
|
||||
}
|
||||
Individual `lib/*.ts` files are thin wrappers that:
|
||||
1. Call a BFF endpoint via `bff()` / `bffList()` / `bffOrThrow()`
|
||||
2. Map the snake_case response to camelCase frontend types
|
||||
3. Export typed functions consumed by Next.js server components
|
||||
|
||||
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 Letter {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
slug String @unique
|
||||
recipients String[] // array of individual recipient names
|
||||
recipientDisplay String // original combined string for display
|
||||
date String?
|
||||
dateRef String?
|
||||
year Int?
|
||||
month Int?
|
||||
day Int?
|
||||
location String?
|
||||
salutation String?
|
||||
paragraphs String[]
|
||||
|
||||
@@index([year])
|
||||
@@index([recipients])
|
||||
@@index([location])
|
||||
@@index([year, month])
|
||||
@@map("letters")
|
||||
}
|
||||
|
||||
model GlossaryEntry {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
term String
|
||||
definition String
|
||||
definitionHtml String
|
||||
|
||||
@@unique([term])
|
||||
@@map("glossary")
|
||||
}
|
||||
|
||||
model Disciple {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
initiatedName String
|
||||
suffix String // "Dasa" | "Devi Dasi" | "Sw"
|
||||
initiationDate DateTime? // 1st of the month (exact day unknown unless verified)
|
||||
initiationPlace String?
|
||||
karmiName String? // pre-initiation (birth) name
|
||||
avatar String? // URL to profile photo
|
||||
about String? // biographical notes
|
||||
tags String[]
|
||||
isVerified Boolean @default(false)
|
||||
|
||||
@@index([initiatedName])
|
||||
@@index([initiationPlace])
|
||||
@@index([initiationDate])
|
||||
@@index([suffix])
|
||||
@@index([tags])
|
||||
@@map("disciples")
|
||||
}
|
||||
|
||||
type ContentBlock {
|
||||
text String
|
||||
format String @default("paragraph")
|
||||
alignment String @default("left")
|
||||
speaker String?
|
||||
}
|
||||
|
||||
model Transcription {
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
slug String @unique
|
||||
name String
|
||||
type String // "lecture" | "conversation"
|
||||
date String?
|
||||
year Int?
|
||||
month Int?
|
||||
day Int?
|
||||
place String?
|
||||
context String?
|
||||
reference String?
|
||||
summary String?
|
||||
content ContentBlock[]
|
||||
tags String[]
|
||||
participants String[]
|
||||
language String @default("en")
|
||||
source String?
|
||||
durationMinutes Int?
|
||||
audioUrl String?
|
||||
isPublished Boolean @default(false)
|
||||
notes String?
|
||||
|
||||
@@map("transcriptions")
|
||||
}
|
||||
```
|
||||
Key data layers and their BFF endpoint prefixes:
|
||||
- `lib/api.ts` → `/api/v1/books/*`, `/api/v1/glossary/*` — Books, verses, prose chapters, glossary
|
||||
- `lib/books.ts` → `/api/v1/books/{code}/*` — Generic book data layer (replaces 29 individual per-book files)
|
||||
- `lib/letters.ts` → `/api/v1/letters/*` — Letters (by year, recipient, location)
|
||||
- `lib/transcriptions.ts` → `/api/v1/transcriptions/*` — Transcriptions (by year, place, type)
|
||||
- `lib/disciples.ts` → `/api/v1/disciples/*` — Disciples directory
|
||||
- `lib/other-documents.ts` → `/api/v1/other-documents/*` — Other Documents
|
||||
- `lib/journals.ts` → `/api/v1/journals/*` — Journals
|
||||
- `lib/magazines.ts` → `/api/v1/magazines/*` — Magazines
|
||||
|
||||
---
|
||||
|
||||
@@ -503,7 +324,7 @@ Seed script: `npx tsx scripts/seed-disciples.ts`
|
||||
**Schema notes**:
|
||||
- `recipients` is an array of individual names (e.g., `["Mukunda", "Janaki"]`)
|
||||
- `recipientDisplay` is the original combined string (e.g., `"Mukunda, Janaki"`)
|
||||
- Searching for a recipient uses Prisma `{ has: name }` to match any letter where that person appears
|
||||
- Searching for a recipient filters via the BFF endpoint (matches any letter where that person appears)
|
||||
- Multi-recipient groups are split by `,`, `;`, ` and `, ` & `
|
||||
- Trailing punctuation (`:`, `,`, `.`) cleaned from names
|
||||
|
||||
@@ -518,9 +339,7 @@ Seed script: `npx tsx scripts/seed-disciples.ts`
|
||||
- **Audio**: 99.1% have linked audio recordings from Cloudflare R2 (`audioUrl` field)
|
||||
- **Empty content**: 30 transcriptions (Hindi/Bengali lectures) have no English text but may have audio
|
||||
|
||||
Data layer: `lib/transcriptions.ts` (reads from Prisma/MongoDB, NOT local JSON files)
|
||||
|
||||
Seed script: `cd knowledgebase && npx tsx scripts/seed-transcriptions.ts`
|
||||
Data layer: `lib/transcriptions.ts` (calls BFF API, NOT local JSON files or direct MongoDB)
|
||||
|
||||
### Other
|
||||
|
||||
@@ -716,11 +535,17 @@ SSR and DS have parent chapters with sub-chapters stored as individual `prose_ch
|
||||
|
||||
### Reading Settings (accessibility)
|
||||
- **Theme**: Light / Dark / System toggle
|
||||
- **Palette**: 3 light + 3 dark colour palettes, each selectable from a swatch grid in the settings panel
|
||||
- Light: **Aged Parchment** (warm cream/saffron — default), **Tulasī Garden** (sage green/emerald), **Yamunā Mist** (sky-blue/cerulean)
|
||||
- Dark: **Ancient Manuscript** (warm brown/saffron — default), **Nīlamaṇi** (deep navy/sky-blue — Kṛṣṇa's "blue jewel"), **Vraja Twilight** (deep wine/rose-red)
|
||||
- Swatches in the settings panel show only the palettes for the currently resolved theme (uses `resolvedTheme` from next-themes, so System mode resolves correctly)
|
||||
- Each palette defines a full set of CSS variables (background, foreground, primary, card, muted, accent, border, etc.) so the entire UI adapts cohesively
|
||||
- Palette CSS lives in `app/globals.css` under `:root:not(.dark)[data-light-palette="..."]` and `.dark[data-dark-palette="..."]` selectors. The `:not(.dark)` scoping prevents light-palette attribute rules from overriding `.dark` (an attribute selector has higher specificity than a plain class selector)
|
||||
- **Serif Font**: Toggle to switch reading content to Georgia/serif
|
||||
- **Font Size**: Slider (XS → XXL) using CSS `zoom` on `main` and `aside`
|
||||
- **Justify Text**: Toggle for justified text alignment on reading content
|
||||
- Settings persist in `localStorage` under `spsays_reading_prefs`
|
||||
- Applied via CSS classes on `<html>` element
|
||||
- Settings persist in `localStorage` under `spsays_reading_prefs` (now includes `lightPalette` and `darkPalette` fields)
|
||||
- Applied via CSS classes (`reading-serif`, `reading-fs-*`, `reading-justified`) AND data attributes (`data-light-palette`, `data-dark-palette`) on `<html>` element
|
||||
|
||||
### Mobile Navigation
|
||||
- Left-slide drawer (vaul) triggered by a menu button next to the Previous button
|
||||
@@ -728,14 +553,80 @@ SSR and DS have parent chapters with sub-chapters stored as individual `prose_ch
|
||||
- Shows the full sidebar navigation grouped by section
|
||||
- Implemented in all 24+ reader pages via shared `MobileNav` component
|
||||
|
||||
### Page Header (`components/page-header.tsx`)
|
||||
- Shared client component used on every page — wraps the title/breadcrumb area and the right-side actions (search button + reading settings)
|
||||
- **Mobile layout** uses CSS `float-right` on the actions block so the title text genuinely wraps around them on the first line and uses the **full container width** on every subsequent line. (CSS `padding-right` was previously used but applies the same gap to every line, leaving an empty column down the right edge.)
|
||||
- When the header search is expanded on mobile, the actions block drops the float, the logo/title area is hidden via `max-md:hidden`, and the search bar fills the full header width edge-to-edge (no `ml-2` margin in the expanded state)
|
||||
- **Desktop layout** is unchanged — standard `md:flex md:items-start md:justify-between md:gap-4` with the actions block on the right (`md:order-1 md:shrink-0`)
|
||||
- Reads a sessionStorage flag (`spsays_from_search`) on first render via a lazy `useState` initialiser; if set, renders a “← Back to search results” button that calls `router.back()` and **hides the page's own first-child `<Link>` breadcrumb** via `[&>a:first-child]:hidden` so there's never a double-arrow stack
|
||||
- The lazy initialiser also clears the flag on read (one-shot), so navigating from the result page to a subsequent page (e.g. next verse) doesn't show the search return button again
|
||||
|
||||
### Search
|
||||
|
||||
**Search** (`/search` page) — Typo-tolerant, diacritic-insensitive full-text search across all content.
|
||||
|
||||
- **Powered by Typesense** via the BFF (`/api/v1/search` proxied through `/api/search`)
|
||||
- **9 indexed content types** combined into 8 user-facing filters: Books (verses + prose), Letters, Transcriptions, Glossary, Essays, Journals, Magazines, Other Documents
|
||||
- **Books filter** combines verses and prose into one tab (sends `type=verses,prose` to the BFF)
|
||||
- **Multi-select filters** — clicking multiple type tabs combines them (e.g., Books + Letters); clicking "All" clears all filters
|
||||
- **URL state** — `q`, `type`, and `page` are synced to the URL (e.g. `/search?q=foo&type=books,letters&page=2`), so any search is shareable and bookmarkable
|
||||
- **Pagination** — appears both above the results (compact: prev/next + page count) and below them
|
||||
- **Result cards** — the entire card is a clickable `Link` that navigates to the source page
|
||||
- **Header search** — `HeaderSearchButton` on every page header expands into a search input on click; the `/` keyboard shortcut opens the search input from any page
|
||||
- **300ms debounce** on input
|
||||
- **Highlighted snippets** — matched text wrapped in `<mark>` tags
|
||||
- **Typo tolerance** — "Krishna", "Krsna", "Krishn" all find Kṛṣṇa results
|
||||
- **Diacritic insensitivity** — `_ascii` shadow fields indexed alongside originals (e.g., "Yashodanandana" matches "Yaśodanandana")
|
||||
- **Prefix matching** — partial words like "sarv dharman parit" match `sarva-dharmān parityajya`
|
||||
- **Hyphenated terms** — every collection (not just verses) has `token_separators: ["-"]`, so `Dīkṣā-guru` indexes as two tokens (`dīkṣā` + `guru`) and a query for "diksa guru" matches it cleanly
|
||||
- **Stopwords** — a server-side Typesense stopwords set (`common`) strips conversational tokens like `letter`, `to`, `of`, `essay`, `magazine`, etc. from queries so they don't drown out specific intent. e.g. "letter to tusta krsna" effectively searches for "tusta krsna" and surfaces actual letters to that recipient instead of any letter whose body contains "letter" + "to"
|
||||
- **Exact > prefix > typo ranking** — `prioritize_exact_match=true` + `prioritize_token_position=true` so "karma" outranks "karmic", and exact recipient matches outrank typo matches
|
||||
- **Glossary results** — glossary pages read `?term=` from the URL so a search result lands on the correct term (with diacritic-insensitive matching)
|
||||
- **Defensive null handling** — the frontend always treats a null `results` payload as an empty array
|
||||
- Files: `app/search/page.tsx`, `app/search/search-content.tsx`, `app/api/search/route.ts`, `components/header-search.tsx`
|
||||
|
||||
**Header search behaviour.** When the user submits a search from the inline header input, `HeaderSearchButton` automatically appends a `type` query param matching the section they're currently in (via `usePathname()`):
|
||||
|
||||
| Current path | Search URL gets | Active tab |
|
||||
|---|---|---|
|
||||
| `/books/**` | `type=books` | Books |
|
||||
| `/letters/**` | `type=letters` | Letters |
|
||||
| `/transcriptions/**` | `type=transcriptions` | Transcriptions |
|
||||
| `/essays/**` | `type=essays` | Essays |
|
||||
| `/glossary/**` | `type=glossary` | Glossary |
|
||||
| `/journals/**` | `type=journals` | Journals |
|
||||
| `/magazines/**` | `type=magazines` | Magazines |
|
||||
| `/other-documents/**` | `type=other_documents` | Other Documents |
|
||||
| `/`, `/search`, `/about` | no `type` param | All |
|
||||
|
||||
**Header search expand UX.** The collapsed icon expands into a 400px-wide bar on desktop (capped via `md:max-w-100`, animated to `width: 100%` of its container by framer-motion) and a full-width bar on mobile. The bar has a “Close” X on the left, the text input in the middle, and a primary-coloured “Search ⏎” submit button on the right — the `CornerDownLeft` icon hints that Enter also submits. Pressing Esc collapses. Spring animation on enter, instant collapse (no exit animation, no AnimatePresence — the previous use of `AnimatePresence mode="popLayout"` caused a backwards-shrink flash because `popLayout` pulled the exiting element out of flow and ran a layout-level FLIP).
|
||||
|
||||
**Back to search results.** When a user clicks a result card, `ResultCard`'s `onClick` writes a one-shot `spsays_from_search` flag to sessionStorage. On the destination page, `PageHeader` reads (and consumes) this flag in a lazy `useState` initialiser; if present, it replaces the page's own breadcrumb link with a “← Back to search results” button that calls `router.back()`. Client-side navigation preserves `AudioPlayerProvider` so audio playback continues uninterrupted.
|
||||
|
||||
**Search-result highlight.** Each result card builds its destination URL through `buildSearchResultUrl(result.url, result.snippet)` from `lib/text-highlight.ts`. This extracts the text inside the snippet's `<mark>` tags (from first mark to last mark, HTML stripped), encodes it via the existing `encodeTextFragment()` helper, and appends it as `?hl=...`. The destination page's existing `TextHighlighter` then finds and highlights the matched phrase — reusing the exact same `?hl=` infrastructure as share links, with no changes to `TextHighlighter` itself.
|
||||
|
||||
**Search relevance.** Search is tuned to find verses where the verse text itself contains the query, not commentaries that cite it. Verse purports are intentionally excluded from the default search so results stay focused on primary sources. In practice this means searching for `sarva dharman parityajya` returns BG 18.66 first (the actual verse) rather than the many commentaries that quote it.
|
||||
|
||||
### Per-Page Filters
|
||||
- Books page: diacritic-aware search (typing "Krsna" matches "Kṛṣṇa")
|
||||
- Glossary page: full-text search across terms and definitions
|
||||
- Uses Unicode NFD normalization + manual IAST character stripping
|
||||
|
||||
### Color Palette
|
||||
- **Light mode ("Aged Parchment")**: Warm cream backgrounds, deep sepia text, saffron/ochre primary
|
||||
- **Dark mode ("Ancient Manuscript")**: Deep warm brown backgrounds, parchment text, golden saffron primary
|
||||
### Color Palettes
|
||||
|
||||
Three themes per mode, all named after concepts from the Vedic tradition. Each defines a complete set of CSS variables (background, foreground, primary, card, muted, accent, border, ring, sidebar variants, charts) for cohesive styling site-wide.
|
||||
|
||||
**Light palettes**
|
||||
- **Aged Parchment** (default) — warm cream backgrounds, deep sepia text, saffron/ochre primary
|
||||
- **Tulasī Garden** — sage-green backgrounds, deep forest text, emerald primary; inspired by the sacred Tulasī plant
|
||||
- **Yamunā Mist** — soft blue backgrounds, deep teal text, cerulean primary; inspired by the sacred Yamunā river
|
||||
|
||||
**Dark palettes**
|
||||
- **Ancient Manuscript** (default) — deep warm brown backgrounds, parchment text, golden saffron primary
|
||||
- **Nīlamaṇi** — deep navy backgrounds, soft moonlight text, sky-blue primary; “blue jewel,” one of Kṛṣṇa's epithets
|
||||
- **Vraja Twilight** — deep wine backgrounds, warm cream text, rose-red primary; the crimson dusk over Vraja
|
||||
|
||||
Palettes are switched independently of theme: the user can pick their preferred light palette AND their preferred dark palette — the right one takes effect automatically when next-themes toggles between modes. Selection happens in `components/reading-settings.tsx` via a 3-up grid of swatch buttons (each swatch is a paint-chip-style square with the palette's background colour plus a small primary-coloured dot in the corner).
|
||||
|
||||
### Text Selection Toolbar
|
||||
- Global floating toolbar appears on text selection across all reading pages (except RTFs)
|
||||
@@ -757,6 +648,8 @@ SSR and DS have parent chapters with sub-chapters stored as individual `prose_ch
|
||||
- Server-side `extractMatchedText()` searches paragraphs for OG image quote extraction
|
||||
- Case-insensitive matching for both DOM highlighting and server-side text extraction
|
||||
- Cross-paragraph matching via position mapping (spaces inserted at element boundaries)
|
||||
- **HTML-entity regex must be strict.** `extractMatchedText()` strips HTML entities from paragraph strings before searching. The regex matches only real entity shapes — named (`&`), decimal (`'`), and hex (`'`) — not the loose `&[^;]+;` pattern, which would pair a stray `&` (e.g. "24 & 25 Oct") with a far-away `;` (e.g. "my beloved son;") and eat everything between them, breaking OG quote extraction for any selection that spans such a stray ampersand. The browser-side `findAndHighlightText` is unaffected because it walks DOM text nodes that the browser has already decoded.
|
||||
- `buildSearchResultUrl(resultUrl, snippetHtml)` extracts the matched text from a search snippet's `<mark>` tags and appends `?hl=...` to the result URL so the destination page highlights the match — same infrastructure as share links.
|
||||
|
||||
### Dynamic OG Images
|
||||
- `/api/og` route generates 1200×630 PNG images using `next/og` (Satori)
|
||||
@@ -771,7 +664,10 @@ SSR and DS have parent chapters with sub-chapters stored as individual `prose_ch
|
||||
A site-wide floating audio player for transcription recordings (and future music library). Built with framer-motion for animations.
|
||||
|
||||
- **Desktop**: Centered floating bar (max-width 680px) with cover image, track info, transport controls (prev/play/next), volume slider, source link, close button
|
||||
- **Mobile**: Auto-minimizes to a floating pill (cover image with circular progress ring and audio wave animation) after 8 seconds of inactivity. Tap to expand. Full player shows two rows: track info row + centered transport controls
|
||||
- **Mobile**: Full player shows two rows: track info row + centered transport controls
|
||||
- **Auto-minimize**: After 8 seconds of inactivity on **both mobile and desktop**, the player auto-minimizes to a floating pill. The minimized pill shows cover image with a circular progress ring and audio wave animation. Tap/click to expand.
|
||||
- **X button behavior**: Minimizes the player if audio is playing; stops playback entirely if audio is paused
|
||||
- **Clicking outside the player**: Minimizes it (does not stop playback)
|
||||
- **Z-index**: `z-40` — below text selection toolbar (z-80), drawers (z-90), modals (z-100)
|
||||
- **Hidden when selection toolbar is active** on mobile to avoid overlap
|
||||
- **Forced dark surface** (`bg-[#1c1c1e]`) for accessibility contrast regardless of theme
|
||||
@@ -799,30 +695,32 @@ The `AudioPlayerProvider` wraps the entire app in `layout.tsx`. The `FloatingPla
|
||||
## Environment Variables
|
||||
|
||||
```
|
||||
DATABASE_URL=mongodb://user:pass@host:port/spsays?authSource=admin
|
||||
NEXT_PUBLIC_SITE_URL=https://prabhupadasays.org
|
||||
BFF_URL=http://localhost:8080 # Go BFF API server URL (default: http://localhost:8080)
|
||||
ALLOW_RTF=true # Enable RTF viewer (development only)
|
||||
NEXT_PUBLIC_SITE_URL=https://prabhupadasays.org # Public site URL for OG images
|
||||
```
|
||||
|
||||
> No new vars are needed for the frontend search feature — search goes through the BFF, which has `TYPESENSE_URL` and `TYPESENSE_API_KEY` configured server-side.
|
||||
|
||||
---
|
||||
|
||||
## How to Run
|
||||
|
||||
```bash
|
||||
# Development (from knowledgebase/)
|
||||
npm run dev
|
||||
# 1. Start the Go BFF server (required — all data comes from the BFF)
|
||||
cd spsays-bff && go run cmd/api/main.go # Starts on :8080 by default
|
||||
|
||||
# Prisma
|
||||
npx prisma generate # Regenerate Prisma client after schema changes
|
||||
# 2. Development (from knowledgebase/)
|
||||
npm run dev # Next.js dev server with Turbopack
|
||||
|
||||
# Build
|
||||
# 3. Build
|
||||
npx next build
|
||||
|
||||
# Extraction & seeding scripts are in extractor/ — see extractor/scripts/
|
||||
|
||||
# BFF API server (from ../spsays-bff/)
|
||||
make run # Build + start Go API on :8080
|
||||
```
|
||||
|
||||
> **Note**: No `npx prisma generate`, `npx prisma db push`, or MongoDB connection string needed in the frontend. The BFF server must be running for any page to load data.
|
||||
|
||||
---
|
||||
|
||||
## Verse JSON Schema (BG/SB/CC/SB1962/ISO/NOI)
|
||||
@@ -946,7 +844,7 @@ Content block formats: `"paragraph"` (body text), `"verse"` (centered italic), `
|
||||
|
||||
## Known Issues
|
||||
|
||||
1. **MongoDB BSON wire corruption**: The MongoDB server occasionally corrupts large BSON documents. Workaround: seed using `insertOne` + `$push` for paragraphs one at a time.
|
||||
1. **BFF dependency** — The knowledgebase requires the Go BFF server to be running; all pages will fail to load data if the BFF is down.
|
||||
|
||||
2. **SB verse numbering**: 5 duplicate verse reference tags in the HTML. Fixed by using the TEXT N header as authoritative.
|
||||
|
||||
@@ -956,10 +854,12 @@ Content block formats: `"paragraph"` (body text), `"verse"` (centered italic), `
|
||||
|
||||
5. **CSS zoom for font scaling**: `zoom` property works in Chrome/Safari/Edge but may not work in older Firefox (< 126). A `transform: scale()` fallback is provided via `@supports not (zoom: 1.1)`.
|
||||
|
||||
6. **Password protection requires ACCESS_KEY at runtime**: The `PasswordGate` component checks the server at runtime via `/api/verify-access`. The env var must be available when the server is running, not just at build time.
|
||||
6. **Transcription BSON fallback** — 1 document stores content as `contentJson` string due to BSON wire protocol limits; `getTranscription()` transparently parses it
|
||||
|
||||
7. **Transcription BSON fallback** — 1 document stores content as `contentJson` string due to BSON wire protocol limits; `getTranscription()` transparently parses it
|
||||
7. **Empty transcriptions** — 30 entries have no English text content (Hindi/Bengali recordings); the UI shows "No transcription available" with the audio player still accessible
|
||||
|
||||
8. **Empty transcriptions** — 30 entries have no English text content (Hindi/Bengali recordings); the UI shows "No transcription available" with the audio player still accessible
|
||||
8. **Audio player z-index** — Player is hidden when text selection toolbar is active on mobile to avoid overlap
|
||||
|
||||
9. **Audio player z-index** — Player is hidden when text selection toolbar is active on mobile to avoid overlap
|
||||
9. **Search depends on Typesense** — Search depends on Typesense being available via the BFF; if Typesense is down, search will return empty results but the rest of the site is unaffected.
|
||||
|
||||
10. **Palette FOUC on first load** — Palette data attributes are applied via the `applyClasses` effect after first paint. A user with a non-default palette will see a brief flash of the default palette before their preference kicks in. Could be fixed by injecting an inline script in `layout.tsx` that reads `localStorage` before hydration (next-themes does this for the `.dark` class). Not yet implemented.
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
||||
import { auth, AuthError } from "@/lib/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function onSubmit(e: React.SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await auth.forgotPassword(email);
|
||||
setSubmitted(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof AuthError ? err.message : "Something went wrong.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<AuthCard
|
||||
title="Check your inbox"
|
||||
description="If an account exists for that email, we've sent a reset link. It will expire in 1 hour."
|
||||
footer={<AuthFooterLink prefix="Back to" href="/login" label="log in" />}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Didn't get an email? Check your spam folder or try again in a minute.
|
||||
</p>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title="Reset your password"
|
||||
description="Enter your email and we'll send you a reset link."
|
||||
footer={<AuthFooterLink prefix="Remembered it?" href="/login" label="Log in" />}
|
||||
>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||
<Button type="submit" disabled={submitting} className="w-full h-11 text-base">
|
||||
{submitting ? "Sending…" : "Send reset link"}
|
||||
</Button>
|
||||
</form>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Shared layout for the auth pages (login, signup, forgot-password,
|
||||
* reset-password). Provides the centered card frame so each page only
|
||||
* has to render its own form.
|
||||
*
|
||||
* Also gates the routes: a signed-in user landing on any /login, /signup,
|
||||
* etc. URL is bounced to /u/me (or the `next` query param if present).
|
||||
* The check happens client-side because auth state lives in
|
||||
* AuthProvider — there's no server-side cookie inspection here yet, and
|
||||
* the redirect is purely cosmetic (logged-in users don't gain anything
|
||||
* from re-seeing these forms, and signing in over an existing session
|
||||
* would just no-op).
|
||||
*/
|
||||
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/components/auth/auth-provider";
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// useSearchParams forces a CSR bailout during prerender, so Next.js
|
||||
// requires a Suspense boundary anywhere it's read. The gate is the
|
||||
// only consumer here — isolate it so the rest of the layout can still
|
||||
// SSR normally.
|
||||
return (
|
||||
<main className="flex items-center justify-center px-4 py-12 min-h-[90vh]">
|
||||
<Suspense fallback={<AuthShell>{children}</AuthShell>}>
|
||||
<AuthGate>{children}</AuthGate>
|
||||
</Suspense>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
return <div className="w-full max-w-md">{children}</div>;
|
||||
}
|
||||
|
||||
function AuthGate({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !user) return;
|
||||
const next = params.get("next");
|
||||
router.replace(next && next.startsWith("/") ? next : "/u/me");
|
||||
}, [loading, user, router, params]);
|
||||
|
||||
// Don't flash the form for an instant before the redirect fires.
|
||||
if (!loading && user) {
|
||||
return <p className="text-sm text-muted-foreground">Redirecting…</p>;
|
||||
}
|
||||
|
||||
return <AuthShell>{children}</AuthShell>;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
||||
import { LoginForm } from "@/components/auth/login-form";
|
||||
|
||||
function LoginContent() {
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
const next = params.get("next") ?? "/u/me";
|
||||
const oauthError = params.get("oauth_error");
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title="Welcome back"
|
||||
description="Log in to access your bookmarks, notes, and collections."
|
||||
footer={
|
||||
<AuthFooterLink prefix="No account?" href="/signup" label="Create one" />
|
||||
}
|
||||
>
|
||||
{oauthError ? (
|
||||
<p className="text-sm text-destructive">
|
||||
{oauthErrorMessage(oauthError)}
|
||||
</p>
|
||||
) : null}
|
||||
<LoginForm
|
||||
oauthNext={next}
|
||||
onSuccess={() => {
|
||||
router.push(next);
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
function oauthErrorMessage(code: string): string {
|
||||
switch (code) {
|
||||
case "invalid_state":
|
||||
case "missing_state":
|
||||
return "Your sign-in link expired. Please try again.";
|
||||
case "exchange_failed":
|
||||
case "profile_failed":
|
||||
return "Sign-in failed while talking to the provider. Please try again.";
|
||||
case "unknown_provider":
|
||||
return "That sign-in provider isn't configured. Please use email or another method.";
|
||||
case "unavailable":
|
||||
return "Social sign-in is temporarily unavailable. Please use email/password.";
|
||||
case "server":
|
||||
return "Something went wrong on our side. Please try again in a moment.";
|
||||
default:
|
||||
return "Sign-in failed. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<LoginContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useState } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
||||
import { auth, AuthError } from "@/lib/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
function ResetPasswordForm() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const token = params.get("token") ?? "";
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<AuthCard title="Invalid reset link" description="The link is missing a token. Please request a new one.">
|
||||
<AuthFooterLink prefix="Need a new link?" href="/forgot-password" label="Request one" />
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
async function onSubmit(e: React.SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (password !== confirm) {
|
||||
setError("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await auth.resetPassword(token, password);
|
||||
setDone(true);
|
||||
setTimeout(() => router.push("/login"), 1500);
|
||||
} catch (err) {
|
||||
setError(err instanceof AuthError ? err.message : "Reset failed.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<AuthCard
|
||||
title="Password updated"
|
||||
description="Redirecting you to the login page…"
|
||||
>
|
||||
<AuthFooterLink prefix="Take me there now —" href="/login" label="Log in" />
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title="Set a new password"
|
||||
description="Choose a strong password you haven't used before."
|
||||
>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="password">New password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="confirm">Confirm password</Label>
|
||||
<Input
|
||||
id="confirm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
required
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||
<Button type="submit" disabled={submitting} className="w-full h-11 text-base">
|
||||
{submitting ? "Updating…" : "Update password"}
|
||||
</Button>
|
||||
</form>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
// useSearchParams must be inside Suspense for Next.js streaming.
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ResetPasswordForm />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
||||
import { SignUpForm } from "@/components/auth/signup-form";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<AuthCard
|
||||
title="Create your account"
|
||||
description="Save bookmarks, build collections, and follow Śrīla Prabhupāda's words across devices."
|
||||
footer={
|
||||
<AuthFooterLink prefix="Already registered?" href="/login" label="Log in" />
|
||||
}
|
||||
>
|
||||
<SignUpForm
|
||||
oauthNext="/u/me"
|
||||
onSuccess={() => {
|
||||
router.push("/u/me");
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function BbdChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/bbd/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Beyond Birth and Death" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/bbd"
|
||||
@@ -77,12 +81,15 @@ export default async function BbdChapterPage({
|
||||
>
|
||||
← Beyond Birth and Death
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function BbdChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBbdMeta, getBbdIntroSegments } from "@/lib/bbd";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function BbdPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/bbd", name: "Beyond Birth and Death" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+8
-4
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -101,9 +102,12 @@ export default async function VersePage({
|
||||
>
|
||||
← Bhagavad-gītā
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -0,0 +1,86 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { getChapterVerseIndex, getProseChapter } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ chapter: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { chapter } = await params;
|
||||
// Non-numeric segments are front matter (dedication, foreword, …).
|
||||
if (isNaN(parseInt(chapter))) {
|
||||
const segment = await getProseChapter("bg", 0, chapter);
|
||||
return buildOgMeta({
|
||||
title: BOOK_NAMES["bg"],
|
||||
detail: segment?.title ?? "Front Matter",
|
||||
});
|
||||
}
|
||||
return buildOgMeta({ title: BOOK_NAMES["bg"], detail: `Chapter ${chapter}` });
|
||||
}
|
||||
|
||||
export default async function ChapterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ chapter: string }>;
|
||||
}) {
|
||||
const { chapter } = await params;
|
||||
|
||||
// Front matter — non-numeric slugs (dedication, foreword, preface,
|
||||
// introduction, disciplic-succession) resolve to prose intro-segments.
|
||||
if (isNaN(parseInt(chapter))) {
|
||||
const segment = await getProseChapter("bg", 0, chapter);
|
||||
if (!segment) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/bg"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← {BOOK_NAMES["bg"]}
|
||||
</Link>
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Numeric chapters — redirect to the first verse.
|
||||
const verseIndex = await getChapterVerseIndex("bg", null, parseInt(chapter));
|
||||
if (verseIndex.length === 0) {
|
||||
notFound();
|
||||
}
|
||||
redirect(`/books/bg/${chapter}/${verseIndex[0].verse}`);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getProseChapters } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return ogForBook("bg");
|
||||
}
|
||||
|
||||
type ChapterInfo = {
|
||||
number: number;
|
||||
title: string;
|
||||
totalVerses: number;
|
||||
};
|
||||
|
||||
export default async function BgPage() {
|
||||
const [meta, introSegments] = await Promise.all([
|
||||
getBookMeta("bg"),
|
||||
getProseChapters("bg", 0, "intro-segment"),
|
||||
]);
|
||||
if (!meta) notFound();
|
||||
|
||||
const intro = meta as unknown as {
|
||||
book: string;
|
||||
title: string;
|
||||
author: string;
|
||||
totalChapters: number;
|
||||
chapters: ChapterInfo[];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/bg", name: "Bhagavad-gītā As It Is" })} />
|
||||
<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">
|
||||
{intro.title}
|
||||
</h1>
|
||||
<p className="mt-1 text-muted-foreground">{intro.author}</p>
|
||||
</PageHeader>
|
||||
<main className="max-w-5xl mx-auto px-6 py-8 space-y-8">
|
||||
{/* Front Matter */}
|
||||
{introSegments.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Front Matter
|
||||
</h2>
|
||||
<Card className="divide-y divide-border py-0">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
{introSegments.map((seg) => (
|
||||
<li key={seg.slug}>
|
||||
<Link
|
||||
href={`/books/bg/${seg.slug}`}
|
||||
className="flex px-5 py-4 hover:bg-muted transition-colors"
|
||||
>
|
||||
<p className="font-medium text-foreground">
|
||||
{seg.title}
|
||||
</p>
|
||||
</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 py-0">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
{intro.chapters.map((ch) => (
|
||||
<li key={ch.number}>
|
||||
<Link
|
||||
href={`/books/bg/${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>
|
||||
<span className="text-sm text-muted-foreground sm:shrink-0 sm:ml-4">
|
||||
<ExtractedVerseCount count={ch.totalVerses} />
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -62,6 +65,7 @@ export default async function BkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/bk/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Bhagavāner Kathā" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/bk"
|
||||
@@ -69,12 +73,15 @@ export default async function BkChapterPage({
|
||||
>
|
||||
← Bhagavāner Kathā
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -92,7 +99,7 @@ export default async function BkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBkMeta } from "@/lib/bk";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -18,6 +20,7 @@ export default async function BkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/bk", name: "Bhagavāner Kathā" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -72,11 +72,16 @@ export function BooksGrid({ books }: { books: BookInfo[] }) {
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!query.trim()) return books;
|
||||
const q = normalize(query.trim());
|
||||
return normalizedBooks
|
||||
.filter((entry) => entry.searchText.includes(q))
|
||||
.map((entry) => entry.book);
|
||||
const base = query.trim()
|
||||
? normalizedBooks
|
||||
.filter((entry) =>
|
||||
entry.searchText.includes(normalize(query.trim())),
|
||||
)
|
||||
.map((entry) => entry.book)
|
||||
: books;
|
||||
return [...base].sort((a, b) =>
|
||||
normalize(a.title).localeCompare(normalize(b.title)),
|
||||
);
|
||||
}, [books, normalizedBooks, query]);
|
||||
|
||||
return (
|
||||
@@ -86,7 +91,7 @@ export function BooksGrid({ books }: { books: BookInfo[] }) {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search books..."
|
||||
placeholder="Search books by name..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
+8
-4
@@ -2,7 +2,8 @@ import type { Metadata } from "next";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
getBookMeta,
|
||||
@@ -11,8 +12,8 @@ import {
|
||||
type VerseIndexEntry,
|
||||
type VerseRecord,
|
||||
} from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -187,9 +188,12 @@ export default async function VersePage({
|
||||
>
|
||||
← Śrī Brahma-saṁhitā
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -7,7 +7,8 @@ import { getChapterVerseIndex } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import Link from "next/link";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -59,13 +60,16 @@ export default async function BsChapterPage({
|
||||
>
|
||||
← Śrī Brahma-saṁhitā
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph: string, i: number) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
import fs from "fs";
|
||||
@@ -80,6 +82,7 @@ export default async function BsPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/bs", name: "Śrī Brahma-saṁhitā" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+11
-4
@@ -10,12 +10,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -62,6 +65,7 @@ export default async function CbhcChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/cbhc/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Śrī Caitanya-bhāgavata Commentary" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/cbhc"
|
||||
@@ -69,12 +73,15 @@ export default async function CbhcChapterPage({
|
||||
>
|
||||
← Śrī Caitanya-bhāgavata Commentary
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -92,7 +99,7 @@ export default async function CbhcChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getCbhcMeta } from "@/lib/cbhc";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -18,6 +20,7 @@ export default async function CbhcPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/cbhc", name: "Śrī Caitanya-bhāgavata Commentary" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+8
-4
@@ -2,11 +2,12 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
type ChapterInfo = {
|
||||
@@ -111,9 +112,12 @@ export default async function CcVersePage({
|
||||
>
|
||||
← {lilaInfo.name}
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {chapterInfo.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -0,0 +1,227 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { getBookMeta, getProseChapter, getChapterVerseIndex } from "@/lib/api";
|
||||
import { CC_LILAS } from "@/lib/constants";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ChapterInfo = { number: number; title: string; totalVerses: number };
|
||||
type LilaInfo = {
|
||||
key: string;
|
||||
name: string;
|
||||
totalChapters: number;
|
||||
totalVerses: number;
|
||||
chapters: ChapterInfo[];
|
||||
};
|
||||
type VerseEntry = { verse: number; verseEnd?: number | null; reference: string };
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lila: string; chapter: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { lila, chapter } = await params;
|
||||
const lilaName = CC_LILAS[lila] || lila;
|
||||
return buildOgMeta({
|
||||
title: "Śrī Caitanya-caritāmṛta",
|
||||
detail: `${lilaName}, Chapter ${chapter}`,
|
||||
});
|
||||
}
|
||||
|
||||
function verseLabel(v: VerseEntry): string {
|
||||
return v.verseEnd ? `${v.verse}–${v.verseEnd}` : String(v.verse);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top/bottom navigation — mirrors the verse pages' VerseNav: a Contents
|
||||
// toggle (mobile) listing this chapter's verses, Previous/Next (chapter), and
|
||||
// a "Chapter X of N" label.
|
||||
// ---------------------------------------------------------------------------
|
||||
function ChapterNav({
|
||||
lila,
|
||||
chapters,
|
||||
current,
|
||||
chapterTitle,
|
||||
verseIndex,
|
||||
}: {
|
||||
lila: string;
|
||||
chapters: ChapterInfo[];
|
||||
current: number;
|
||||
chapterTitle: string;
|
||||
verseIndex: VerseEntry[];
|
||||
}) {
|
||||
const idx = chapters.findIndex((c) => c.number === current);
|
||||
const prev = idx > 0 ? chapters[idx - 1] : null;
|
||||
const next = idx < chapters.length - 1 ? chapters[idx + 1] : null;
|
||||
|
||||
// The contents drawer lists the active chapter's verses (same as the
|
||||
// verse-page sidebar) so the reader can jump straight in.
|
||||
const groups: MobileNavGroup[] = [
|
||||
{
|
||||
title: "Verses",
|
||||
items: verseIndex.map((v) => ({
|
||||
slug: String(v.verse),
|
||||
label: `Verse ${verseLabel(v)}`,
|
||||
href: `/books/cc/${lila}/${current}/${v.verse}`,
|
||||
type: "verse",
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<MobileNav
|
||||
groups={groups}
|
||||
currentSlug=""
|
||||
bookTitle={`Chapter ${current}: ${chapterTitle}`}
|
||||
/>
|
||||
{prev ? (
|
||||
<Link
|
||||
href={`/books/cc/${lila}/${prev.number}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "gap-1.5")}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
<span className="hidden sm:inline">Previous</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden lg:block" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Chapter {current} of {chapters.length}
|
||||
</span>
|
||||
|
||||
{next ? (
|
||||
<Link
|
||||
href={`/books/cc/${lila}/${next.number}`}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "gap-1.5")}
|
||||
>
|
||||
<span className="hidden sm:inline">Next</span>
|
||||
<ChevronRight className="size-4" />
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function CcChapterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ lila: string; chapter: string }>;
|
||||
}) {
|
||||
const { lila, chapter } = await params;
|
||||
const chapterNum = parseInt(chapter);
|
||||
|
||||
const [meta, summary, verseIndex] = await Promise.all([
|
||||
getBookMeta("cc"),
|
||||
getProseChapter("cc", 0, `${lila}-${chapterNum}`),
|
||||
getChapterVerseIndex("cc", lila, chapterNum),
|
||||
]);
|
||||
|
||||
if (!meta) notFound();
|
||||
const lilaInfo = (meta.lilas as LilaInfo[] | undefined)?.find(
|
||||
(l) => l.key === lila,
|
||||
);
|
||||
if (!lilaInfo) notFound();
|
||||
const chapterInfo = lilaInfo.chapters.find((ch) => ch.number === chapterNum);
|
||||
if (!chapterInfo) notFound();
|
||||
if (verseIndex.length === 0) notFound();
|
||||
|
||||
const firstVerse = verseIndex[0].verse;
|
||||
|
||||
// Chapters without a summary (e.g. Ādi 7) go straight to the verses.
|
||||
if (!summary || summary.paragraphs.length === 0) {
|
||||
redirect(`/books/cc/${lila}/${chapterNum}/${firstVerse}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href={`/books/cc/${lila}`}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← {lilaInfo.name}
|
||||
</Link>
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapterNum}: {chapterInfo.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
{/* Desktop sidebar — verses of the active chapter (matches the
|
||||
verse-page sidebar) so the reader can jump to any verse. */}
|
||||
<aside className="hidden lg:block w-64 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-165px)] overflow-y-auto py-8 pr-4">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Verses
|
||||
</h2>
|
||||
<nav className="grid grid-cols-3 gap-1">
|
||||
{verseIndex.map((v) => (
|
||||
<Link
|
||||
key={v.verse}
|
||||
href={`/books/cc/${lila}/${chapterNum}/${v.verse}`}
|
||||
className="px-2 py-1 rounded-md text-sm tabular-nums text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{verseLabel(v)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main — chapter summary */}
|
||||
<main className="flex-1 min-w-0 px-6 py-8 space-y-6">
|
||||
{/* Top navigation */}
|
||||
<ChapterNav
|
||||
lila={lila}
|
||||
chapters={lilaInfo.chapters}
|
||||
current={chapterNum}
|
||||
chapterTitle={chapterInfo.title}
|
||||
verseIndex={verseIndex}
|
||||
/>
|
||||
|
||||
<p className="text-sm font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Chapter Summary
|
||||
</p>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{summary.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bottom navigation */}
|
||||
<ChapterNav
|
||||
lila={lila}
|
||||
chapters={lilaInfo.chapters}
|
||||
current={chapterNum}
|
||||
chapterTitle={chapterInfo.title}
|
||||
verseIndex={verseIndex}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { ExtractedVerseCount } from "../../extracted-verse-count";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { getBookMeta, getProseChapter } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES, CC_LILAS } from "@/lib/constants";
|
||||
|
||||
@@ -14,12 +15,15 @@ export async function generateMetadata({
|
||||
params: Promise<{ lila: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { lila } = await params;
|
||||
const lilaName = CC_LILAS[lila] || lila;
|
||||
const bookTitle = BOOK_NAMES["cc"] || "Śrī Caitanya-caritāmṛta";
|
||||
return buildOgMeta({
|
||||
title: bookTitle,
|
||||
detail: lilaName,
|
||||
});
|
||||
// A non-līlā segment (front-flap, preface, introduction) is front matter.
|
||||
if (!CC_LILAS[lila]) {
|
||||
const segment = await getProseChapter("cc", 0, lila);
|
||||
if (segment) {
|
||||
return buildOgMeta({ title: bookTitle, detail: segment.title });
|
||||
}
|
||||
}
|
||||
return buildOgMeta({ title: bookTitle, detail: CC_LILAS[lila] || lila });
|
||||
}
|
||||
|
||||
type ChapterInfo = {
|
||||
@@ -47,7 +51,48 @@ export default async function LilaPage({
|
||||
|
||||
const lilas = meta.lilas as LilaInfo[];
|
||||
const lilaInfo = lilas.find((l) => l.key === lila);
|
||||
if (!lilaInfo) notFound();
|
||||
|
||||
// Front matter — the param is not a līlā key, so resolve it as a prose
|
||||
// intro-segment (front-flap, preface, introduction).
|
||||
if (!lilaInfo) {
|
||||
const segment = await getProseChapter("cc", 0, lila);
|
||||
if (!segment) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/cc"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Śrī Caitanya-caritāmṛta
|
||||
</Link>
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalVerses = lilaInfo.chapters.reduce(
|
||||
(sum, ch) => sum + ch.totalVerses,
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { getBookMeta, getProseChapters } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
@@ -26,7 +28,10 @@ export function generateMetadata(): Metadata {
|
||||
}
|
||||
|
||||
export default async function CcPage() {
|
||||
const meta = await getBookMeta("cc");
|
||||
const [meta, introSegments] = await Promise.all([
|
||||
getBookMeta("cc"),
|
||||
getProseChapters("cc", 0, "intro-segment"),
|
||||
]);
|
||||
if (!meta) {
|
||||
return (
|
||||
<div className="p-8 text-center text-muted-foreground">
|
||||
@@ -41,6 +46,7 @@ export default async function CcPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/cc", name: "Śrī Caitanya-caritāmṛta" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -51,7 +57,33 @@ export default async function CcPage() {
|
||||
<h1 className="mt-3 text-3xl font-bold text-foreground">{title}</h1>
|
||||
<p className="mt-1 text-muted-foreground">{author}</p>
|
||||
</PageHeader>
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<main className="max-w-5xl mx-auto px-6 py-8 space-y-8">
|
||||
{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 py-0">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
{introSegments.map((seg) => (
|
||||
<li key={seg.slug}>
|
||||
<Link
|
||||
href={`/books/cc/${seg.slug}`}
|
||||
className="flex px-5 py-4 hover:bg-muted transition-colors"
|
||||
>
|
||||
<p className="font-medium text-foreground">
|
||||
{seg.title}
|
||||
</p>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="divide-y divide-border">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
@@ -6,7 +6,10 @@ import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -216,9 +219,17 @@ function VerseCard({
|
||||
Purport
|
||||
</h3>
|
||||
<div className="text-base text-foreground leading-relaxed space-y-3">
|
||||
{verse.purport.map((p, i) => (
|
||||
{verse.purport.map((p, i) =>
|
||||
// Cited ślokas come wrapped in <blockquote class="shloka"> — a
|
||||
// block element that can't live inside a <p>, so render those
|
||||
// in a <div> (styled centered + accent via globals.css); plain
|
||||
// prose paragraphs stay as <p>.
|
||||
/^\s*<blockquote/i.test(p) ? (
|
||||
<div key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
||||
) : (
|
||||
<p key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -389,8 +400,25 @@ export function ChapterViewer({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Structured data for the currently-shown verse (SSR'd for the initial
|
||||
verse, updates on client navigation). */}
|
||||
{currentVerse ? (
|
||||
<JsonLd
|
||||
data={articleJsonLd({
|
||||
path: `${basePath}/${currentVerseNum}`,
|
||||
headline: `${BOOK_NAMES[book] ?? book} ${currentVerse.reference}`,
|
||||
description: currentVerse.translation,
|
||||
isPartOf: BOOK_NAMES[book] ?? book,
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* ---- Sidebar: Verse Index ---- */}
|
||||
<aside className="hidden lg:block w-64 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
||||
{/* Sticky sidebar. The max-h leaves 76px at the viewport bottom for
|
||||
the site footer so the sidebar doesn't slide behind it when the
|
||||
page is scrolled to the bottom. (89 sticky-top + 76 bottom gap
|
||||
= 165, hence calc(100vh - 165px).) */}
|
||||
<aside className="hidden lg:block w-64 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-165px)] overflow-y-auto py-8 pr-4">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Verses
|
||||
</h2>
|
||||
@@ -436,7 +464,8 @@ export function ChapterViewer({
|
||||
mobileNavBookTitle={mobileNavBookTitle}
|
||||
/>
|
||||
|
||||
{/* Verse content */}
|
||||
{/* Verse content — id targeted by each page header's FindInPage. */}
|
||||
<div id="book-content">
|
||||
{loading && !currentVerse ? (
|
||||
<div className="flex items-center justify-center py-24 text-muted-foreground gap-2">
|
||||
<Loader2 className="size-5 animate-spin" />
|
||||
@@ -449,6 +478,7 @@ export function ChapterViewer({
|
||||
Verse not found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom navigation */}
|
||||
<VerseNav
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -76,6 +79,7 @@ export default async function DsChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ds/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Dialectic Spiritualism" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ds"
|
||||
@@ -83,12 +87,15 @@ export default async function DsChapterPage({
|
||||
>
|
||||
← Dialectic Spiritualism
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{parentChapterNum != null && (
|
||||
<span className="text-primary">Chapter {parentChapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -106,7 +113,7 @@ export default async function DsChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getDsMeta, getDsIntroSegments } from "@/lib/ds";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function DsPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ds", name: "Dialectic Spiritualism" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function EjChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ej/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Easy Journey to Other Planets" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ej"
|
||||
@@ -77,12 +81,15 @@ export default async function EjChapterPage({
|
||||
>
|
||||
← Easy Journey to Other Planets
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function EjChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEjMeta, getEjIntroSegments } from "@/lib/ej";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function EjPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ej", name: "Easy Journey to Other Planets" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function EkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ek/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Elevation to Kṛṣṇa Consciousness" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ek"
|
||||
@@ -77,12 +81,15 @@ export default async function EkChapterPage({
|
||||
>
|
||||
← Elevation to Kṛṣṇa Consciousness
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function EkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEkMeta, getEkIntroSegments } from "@/lib/ek";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function EkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ek", name: "Elevation to Kṛṣṇa Consciousness" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -10,12 +10,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -66,6 +69,7 @@ export default async function GgChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/gg/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Gītār Gān" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/gg"
|
||||
@@ -73,12 +77,15 @@ export default async function GgChapterPage({
|
||||
>
|
||||
← Gītār Gān
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -96,7 +103,7 @@ export default async function GgChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getGgMeta, getGgIntroSegments } from "@/lib/gg";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function GgPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/gg", name: "Gītār Gān" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function HkhChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/hkh/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "The Kṛṣṇa Consciousness Handbook" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/hkh"
|
||||
@@ -77,12 +81,15 @@ export default async function HkhChapterPage({
|
||||
>
|
||||
← The Kṛṣṇa Consciousness Handbook
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function HkhChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getHkhMeta, getHkhIntroSegments } from "@/lib/hkh";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function HkhPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/hkh", name: "The Kṛṣṇa Consciousness Handbook" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
type IsoSidebarItem,
|
||||
} from "@/lib/iso";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type guards
|
||||
@@ -102,6 +105,7 @@ export default async function IsoMantraPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/iso/${slug}`, headline: content.title ?? "Śrī Īśopaniṣad", isPartOf: "Śrī Īśopaniṣad" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/iso"
|
||||
@@ -109,7 +113,8 @@ export default async function IsoMantraPage({
|
||||
>
|
||||
← Śrī Īśopaniṣad
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{verse && (
|
||||
<>
|
||||
<span className="text-primary">{verse.reference}: </span>
|
||||
@@ -118,6 +123,8 @@ export default async function IsoMantraPage({
|
||||
)}
|
||||
{intro && intro.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -157,7 +164,7 @@ export default async function IsoMantraPage({
|
||||
|
||||
function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
return (
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8 space-y-8">
|
||||
{/* Devanagari */}
|
||||
<section>
|
||||
@@ -233,7 +240,7 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
|
||||
function IntroContent({ intro }: { intro: IsoIntroSegment }) {
|
||||
return (
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{intro.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getIsoMeta, getIsoIntroSegments } from "@/lib/iso";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -28,6 +30,7 @@ export default async function IsoPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/iso", name: "Śrī Īśopaniṣad" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+11
-4
@@ -6,12 +6,15 @@ import { notFound } from "next/navigation";
|
||||
import { getProseChapter, getProseChapters } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -65,6 +68,7 @@ export default async function KbChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/kb/${vol}/${slug}`, headline: chapter.title, description: chapter.paragraphs?.[0], isPartOf: "Kṛṣṇa, The Supreme Personality of Godhead" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href={`/books/kb/${vol}`}
|
||||
@@ -72,12 +76,15 @@ export default async function KbChapterPage({
|
||||
>
|
||||
← Volume {vol}
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterLabel && (
|
||||
<span className="text-primary">{chapterLabel}: </span>
|
||||
)}
|
||||
{chapter.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function KbChapterPage({
|
||||
/>
|
||||
|
||||
{/* Chapter content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{chapter.paragraphs.map((paragraph, i) => (
|
||||
@@ -4,7 +4,7 @@ import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getProseChapters } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export async function generateMetadata({
|
||||
@@ -3,7 +3,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
@@ -27,6 +29,7 @@ export default async function KbPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/kb", name: "Kṛṣṇa, The Supreme Personality of Godhead" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function LclChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/lcl/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Life Comes from Life" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/lcl"
|
||||
@@ -77,12 +81,15 @@ export default async function LclChapterPage({
|
||||
>
|
||||
← Life Comes from Life
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function LclChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getLclMeta, getLclIntroSegments } from "@/lib/lcl";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function LclPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/lcl", name: "Life Comes from Life" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -66,6 +69,7 @@ export default async function LobChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/lob/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Light of the Bhāgavata" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/lob"
|
||||
@@ -73,12 +77,15 @@ export default async function LobChapterPage({
|
||||
>
|
||||
← Light of the Bhāgavata
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Text {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -96,7 +103,7 @@ export default async function LobChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) =>
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getLobMeta, getLobIntroSegments } from "@/lib/lob";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function LobPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/lob", name: "Light of the Bhāgavata" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -66,6 +69,7 @@ export default async function MgChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/mg/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Kṛṣṇa Consciousness: The Matchless Gift" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/mg"
|
||||
@@ -73,12 +77,15 @@ export default async function MgChapterPage({
|
||||
>
|
||||
← Kṛṣṇa Consciousness: The Matchless Gift
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -96,7 +103,7 @@ export default async function MgChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getMgMeta, getMgIntroSegments } from "@/lib/mg";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function MgPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/mg", name: "Kṛṣṇa Consciousness: The Matchless Gift" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+8
-4
@@ -1,7 +1,8 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
getBookMeta,
|
||||
@@ -10,8 +11,8 @@ import {
|
||||
type VerseIndexEntry,
|
||||
type VerseRecord,
|
||||
} from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import type { Metadata } from "next";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
@@ -187,9 +188,12 @@ export default async function VersePage({
|
||||
>
|
||||
← Mukunda-mālā-stotra
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -7,7 +7,8 @@ import { getChapterVerseIndex } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import Link from "next/link";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -59,13 +60,16 @@ export default async function MmChapterPage({
|
||||
>
|
||||
← Mukunda-mālā-stotra
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph: string, i: number) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
import fs from "fs";
|
||||
@@ -79,6 +81,7 @@ export default async function MmPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/mm", name: "Mukunda-mālā-stotra" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function MogChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/mog/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Message of Godhead" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/mog"
|
||||
@@ -77,12 +81,15 @@ export default async function MogChapterPage({
|
||||
>
|
||||
← Message of Godhead
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function MogChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getMogMeta, getMogIntroSegments } from "@/lib/mog";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function MogPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/mog", name: "Message of Godhead" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+8
-4
@@ -4,7 +4,8 @@ import type { Metadata } from "next";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
getBookMeta,
|
||||
@@ -14,8 +15,8 @@ import {
|
||||
type VerseRecord,
|
||||
} from "@/lib/api";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
|
||||
type NbsMeta = {
|
||||
book: string;
|
||||
@@ -192,9 +193,12 @@ export default async function VersePage({
|
||||
>
|
||||
← Nārada-bhakti-sūtra
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -7,7 +7,8 @@ import { getChapterVerseIndex } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import Link from "next/link";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -62,13 +63,16 @@ export default async function NbsChapterPage({
|
||||
>
|
||||
← Nārada-bhakti-sūtra
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph: string, i: number) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
import fs from "fs";
|
||||
@@ -79,6 +81,7 @@ export default async function NbsPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/nbs", name: "Nārada-bhakti-sūtra" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function NodChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/nod/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "The Nectar of Devotion" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/nod"
|
||||
@@ -77,12 +81,15 @@ export default async function NodChapterPage({
|
||||
>
|
||||
← The Nectar of Devotion
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function NodChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getNodMeta, getNodIntroSegments } from "@/lib/nod";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -27,6 +29,7 @@ export default async function NodPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/nod", name: "The Nectar of Devotion" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
type NoiSidebarItem,
|
||||
} from "@/lib/noi";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type guards
|
||||
@@ -93,6 +96,7 @@ export default async function NoiTextPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/noi/${slug}`, headline: content.title ?? "The Nectar of Instruction", isPartOf: "The Nectar of Instruction" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/noi"
|
||||
@@ -100,7 +104,8 @@ export default async function NoiTextPage({
|
||||
>
|
||||
← The Nectar of Instruction
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{verse && (
|
||||
<>
|
||||
<span className="text-primary">Text {verse.text}: </span>
|
||||
@@ -109,6 +114,8 @@ export default async function NoiTextPage({
|
||||
)}
|
||||
{intro && intro.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -148,7 +155,7 @@ export default async function NoiTextPage({
|
||||
|
||||
function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
return (
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8 space-y-8">
|
||||
{/* Devanagari */}
|
||||
<section>
|
||||
@@ -224,7 +231,7 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
|
||||
function IntroContent({ intro }: { intro: NoiIntroSegment }) {
|
||||
return (
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{intro.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getNoiMeta, getNoiIntroSegments } from "@/lib/noi";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function NoiPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/noi", name: "The Nectar of Instruction" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+11
-4
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function OchkmChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ochkm/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "On Chanting Hare Kṛṣṇa" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ochkm"
|
||||
@@ -77,12 +81,15 @@ export default async function OchkmChapterPage({
|
||||
>
|
||||
← On Chanting the Hare Kṛṣṇa Mantra
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function OchkmChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getOchkmMeta, getOchkmIntroSegments } from "@/lib/ochkm";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function OchkmPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ochkm", name: "On Chanting Hare Kṛṣṇa" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function OwkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/owk/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "On the Way to Kṛṣṇa" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/owk"
|
||||
@@ -77,12 +81,15 @@ export default async function OwkChapterPage({
|
||||
>
|
||||
← On the Way to Kṛṣṇa
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function OwkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getOwkMeta, getOwkIntroSegments } from "@/lib/owk";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function OwkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/owk", name: "On the Way to Kṛṣṇa" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { getAllBooks, type BookMeta } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BooksGrid, type BookInfo } from "./books-grid";
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -65,6 +68,7 @@ export default async function PkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/pk/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Prabhupāda-Kṛpā" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/pk"
|
||||
@@ -72,12 +76,15 @@ export default async function PkChapterPage({
|
||||
>
|
||||
← Prayers of King Kulaśekhara
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Verse {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -95,7 +102,7 @@ export default async function PkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPkMeta } from "@/lib/pk";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -18,6 +20,7 @@ export default async function PkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/pk", name: "Prabhupāda-Kṛpā" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function PoyChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/poy/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "The Perfection of Yoga" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/poy"
|
||||
@@ -77,12 +81,15 @@ export default async function PoyChapterPage({
|
||||
>
|
||||
← The Perfection of Yoga
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function PoyChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPoyMeta, getPoyIntroSegments } from "@/lib/poy";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function PoyPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/poy", name: "The Perfection of Yoga" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+11
-4
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function PqpaChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/pqpa/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Perfect Questions, Perfect Answers" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/pqpa"
|
||||
@@ -77,12 +81,15 @@ export default async function PqpaChapterPage({
|
||||
>
|
||||
← Perfect Questions Perfect Answers
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function PqpaChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPqpaMeta, getPqpaIntroSegments } from "@/lib/pqpa";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function PqpaPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/pqpa", name: "Perfect Questions, Perfect Answers" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function RvChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/rv/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Rāja-vidyā: The King of Knowledge" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/rv"
|
||||
@@ -77,12 +81,15 @@ export default async function RvChapterPage({
|
||||
>
|
||||
← Rāja-vidyā: The King of Knowledge
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function RvChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRvMeta, getRvIntroSegments } from "@/lib/rv";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -20,6 +22,7 @@ export default async function RvPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/rv", name: "Rāja-vidyā: The King of Knowledge" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
+8
-4
@@ -2,11 +2,12 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
type ChapterInfo = {
|
||||
@@ -107,9 +108,12 @@ export default async function SbVersePage({
|
||||
>
|
||||
← Canto {cantoInfo.number}: {cantoInfo.title}
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {chapterInfo.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { ExtractedVerseCount } from "../../extracted-verse-count";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { getBookMeta, getProseChapter } from "@/lib/api";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
|
||||
@@ -15,6 +16,11 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { canto } = await params;
|
||||
const title = BOOK_NAMES["sb"] || "Śrīmad-Bhāgavatam";
|
||||
// A non-numeric segment (preface, introduction) is front matter.
|
||||
if (isNaN(parseInt(canto))) {
|
||||
const segment = await getProseChapter("sb", 0, canto);
|
||||
if (segment) return buildOgMeta({ title, detail: segment.title });
|
||||
}
|
||||
return buildOgMeta({ title, detail: `Canto ${canto}` });
|
||||
}
|
||||
|
||||
@@ -38,6 +44,49 @@ export default async function CantoPage({
|
||||
}) {
|
||||
const { canto } = await params;
|
||||
const cantoNum = parseInt(canto);
|
||||
|
||||
// Front matter — a non-numeric param (preface, introduction) resolves to a
|
||||
// prose intro-segment.
|
||||
if (isNaN(cantoNum)) {
|
||||
const segment = await getProseChapter("sb", 0, canto);
|
||||
if (!segment) notFound();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/sb"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
← Śrīmad-Bhāgavatam
|
||||
</Link>
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{segment.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{segment.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = await getBookMeta("sb");
|
||||
if (!meta) notFound();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { getBookMeta, getProseChapters } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -26,7 +28,10 @@ type CantoInfo = {
|
||||
};
|
||||
|
||||
export default async function SbPage() {
|
||||
const meta = await getBookMeta("sb");
|
||||
const [meta, introSegments] = await Promise.all([
|
||||
getBookMeta("sb"),
|
||||
getProseChapters("sb", 0, "intro-segment"),
|
||||
]);
|
||||
if (!meta) notFound();
|
||||
|
||||
const intro = meta as {
|
||||
@@ -39,6 +44,7 @@ export default async function SbPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/sb", name: "Śrīmad-Bhāgavatam" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -51,7 +57,33 @@ export default async function SbPage() {
|
||||
</h1>
|
||||
<p className="mt-1 text-muted-foreground">{intro.author}</p>
|
||||
</PageHeader>
|
||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||
<main className="max-w-5xl mx-auto px-6 py-8 space-y-8">
|
||||
{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 py-0">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
{introSegments.map((seg) => (
|
||||
<li key={seg.slug}>
|
||||
<Link
|
||||
href={`/books/sb/${seg.slug}`}
|
||||
className="flex px-5 py-4 hover:bg-muted transition-colors"
|
||||
>
|
||||
<p className="font-medium text-foreground">
|
||||
{seg.title}
|
||||
</p>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="divide-y divide-border">
|
||||
<CardContent className="p-0">
|
||||
<ul className="divide-y divide-border">
|
||||
+8
-4
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta, getSingleVerse, getChapterVerseIndex } from "@/lib/api";
|
||||
import { ChapterViewer } from "@/app/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/mobile-nav";
|
||||
import { ChapterViewer } from "@/app/(app)/(reader)/books/chapter-viewer";
|
||||
import type { MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { ogForVerse, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -104,9 +105,12 @@ export default async function VersePage({
|
||||
>
|
||||
← Śrīmad-Bhāgavatam (1962)
|
||||
</Link>
|
||||
<h1 className="mt-2 text-2xl font-bold text-foreground">
|
||||
<div className="mt-2 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
Chapter {chapter}: {title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -3,7 +3,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getBookMeta } from "@/lib/api";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ExtractedVerseCount } from "../extracted-verse-count";
|
||||
|
||||
@@ -32,6 +34,7 @@ export default async function Sb1962Page() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/sb1962", name: "Śrīmad-Bhāgavatam (1962 Edition)" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -76,6 +79,7 @@ export default async function SsrChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ssr/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "The Science of Self-Realization" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ssr"
|
||||
@@ -83,12 +87,15 @@ export default async function SsrChapterPage({
|
||||
>
|
||||
← The Science of Self-Realization
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{parentChapterNum != null && (
|
||||
<span className="text-primary">Chapter {parentChapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -106,7 +113,7 @@ export default async function SsrChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getSsrMeta, getSsrIntroSegments } from "@/lib/ssr";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function SsrPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ssr", name: "The Science of Self-Realization" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -13,12 +13,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -77,6 +80,7 @@ export default async function SvaChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/sva/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Songs of the Vaiṣṇava Ācāryas" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/sva"
|
||||
@@ -84,12 +88,15 @@ export default async function SvaChapterPage({
|
||||
>
|
||||
← Songs of the Vaiṣṇava Ācāryas
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{parentChapterNum != null && (
|
||||
<span className="text-primary">Part {parentChapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -107,7 +114,7 @@ export default async function SvaChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getSvaMeta, getSvaIntroSegments } from "@/lib/sva";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function SvaPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/sva", name: "Songs of the Vaiṣṇava Ācāryas" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function TlcChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/tlc/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Teachings of Lord Caitanya" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/tlc"
|
||||
@@ -77,12 +81,15 @@ export default async function TlcChapterPage({
|
||||
>
|
||||
← Teachings of Lord Caitanya
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function TlcChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTlcMeta, getTlcIntroSegments } from "@/lib/tlc";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -25,6 +27,7 @@ export default async function TlcPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/tlc", name: "Teachings of Lord Caitanya" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function TlkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/tlk/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Teachings of Lord Kapila" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/tlk"
|
||||
@@ -77,12 +81,15 @@ export default async function TlkChapterPage({
|
||||
>
|
||||
← Teachings of Lord Kapila
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function TlkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTlkMeta, getTlkIntroSegments } from "@/lib/tlk";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function TlkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/tlk", name: "Teachings of Lord Kapila" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function TqkChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/tqk/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Teachings of Queen Kuntī" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/tqk"
|
||||
@@ -77,12 +81,15 @@ export default async function TqkChapterPage({
|
||||
>
|
||||
← Teachings of Queen Kuntī
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function TqkChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTqkMeta, getTqkIntroSegments } from "@/lib/tqk";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -24,6 +26,7 @@ export default async function TqkPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/tqk", name: "Teachings of Queen Kuntī" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -70,6 +73,7 @@ export default async function TtpChapterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/ttp/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Teachings of Lord Kapila" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/ttp"
|
||||
@@ -77,12 +81,15 @@ export default async function TtpChapterPage({
|
||||
>
|
||||
← Transcendental Teachings of Prahlāda Mahārāja
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{chapterNum != null && (
|
||||
<span className="text-primary">Chapter {chapterNum}: </span>
|
||||
)}
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -100,7 +107,7 @@ export default async function TtpChapterPage({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTtpMeta } from "@/lib/ttp";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -18,6 +20,7 @@ export default async function TtpPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/ttp", name: "Teachings of Lord Kapila" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -6,18 +6,20 @@ import { notFound } from "next/navigation";
|
||||
import {
|
||||
getVaContent,
|
||||
getVaSidebarItems,
|
||||
type VaChapter,
|
||||
type VaSidebarItem,
|
||||
} from "@/lib/va";
|
||||
import { getProseChapter } from "@/lib/api";
|
||||
import { ogForChapter, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { BOOK_NAMES } from "@/lib/constants";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { FindInPage } from "@/components/layout/find-in-page";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metadata
|
||||
@@ -61,10 +63,9 @@ export default async function VaChapterPage({
|
||||
const next =
|
||||
currentIdx < sidebarItems.length - 1 ? sidebarItems[currentIdx + 1] : null;
|
||||
|
||||
const chapterNum = (content as VaChapter).chapter;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/books/va/${slug}`, headline: content.title, description: content.paragraphs?.[0], isPartOf: "Vedānta-sūtra Transcript" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/books/va"
|
||||
@@ -72,9 +73,12 @@ export default async function VaChapterPage({
|
||||
>
|
||||
← Vaiśiṣṭyāṣṭakam
|
||||
</Link>
|
||||
<h1 className="mt-3 text-2xl font-bold text-foreground">
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{content.title}
|
||||
</h1>
|
||||
<FindInPage targetId="book-content" className="shrink-0 mt-0.5" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div className="max-w-5xl mx-auto flex">
|
||||
@@ -88,7 +92,7 @@ export default async function VaChapterPage({
|
||||
currentSlug={slug}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Card id="book-content">
|
||||
<CardContent className="py-8 px-6 sm:px-8">
|
||||
<div className="space-y-4">
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
@@ -5,7 +5,9 @@ import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getVaMeta } from "@/lib/va";
|
||||
import { ogForBook } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { bookJsonLd } from "@/lib/jsonld";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
@@ -18,6 +20,7 @@ export default async function VaPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={bookJsonLd({ path: "/books/va", name: "Vedānta-sūtra Transcript" })} />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/books"
|
||||
@@ -2,7 +2,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { ogForDisciples } from "@/lib/og";
|
||||
import {
|
||||
getTotalDiscipleCount,
|
||||
@@ -4,12 +4,14 @@ import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ogForEssay, extractQuoteFromHl, buildOgMeta } from "@/lib/og";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
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";
|
||||
import { MobileNav, type MobileNavGroup } from "@/components/layout/mobile-nav";
|
||||
import { bff, bffList } from "@/lib/bff";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -119,6 +121,7 @@ export default async function EssayPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd data={articleJsonLd({ path: `/essays/${slug}`, headline: essay.title, description: essay.paragraphs?.[0], isPartOf: "Essays & Talks" })} />
|
||||
<PageHeader sticky>
|
||||
<Link
|
||||
href="/essays"
|
||||
@@ -91,7 +91,7 @@ export function EssaysGrid({ essays }: { essays: EssayInfo[] }) {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search essays..."
|
||||
placeholder="Search essays by title..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
@@ -2,7 +2,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { buildOgMeta } from "@/lib/og";
|
||||
import { bffList } from "@/lib/bff";
|
||||
import { EssaysGrid } from "./essays-grid";
|
||||
@@ -5,6 +5,8 @@ import { notFound } from "next/navigation";
|
||||
import { getAllGlossaryEntries } from "@/lib/api";
|
||||
import { buildOgMeta, extractQuoteFromHl } from "@/lib/og";
|
||||
import { GlossaryClient } from "../glossary-client";
|
||||
import { JsonLd } from "@/components/seo/json-ld";
|
||||
import { articleJsonLd } from "@/lib/jsonld";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Diacritic → base letter mapping
|
||||
@@ -145,6 +147,13 @@ export default async function GlossaryLetterPage({
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<JsonLd
|
||||
data={articleJsonLd({
|
||||
path: `/glossary/${rawLetter}`,
|
||||
headline: `Glossary — ${activeLetter}`,
|
||||
isPartOf: "Glossary of Sanskrit & Vedic Terms",
|
||||
})}
|
||||
/>
|
||||
<GlossaryClient
|
||||
entries={clientEntries}
|
||||
activeLetter={activeLetter}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user