feat: add journals

This commit is contained in:
gosangam
2026-05-03 21:02:41 +05:30
parent 89106efc98
commit 35d1d14f37
47 changed files with 11365 additions and 37 deletions
+167
View File
@@ -0,0 +1,167 @@
/**
* Data access layer for Vaiśiṣṭyāṣṭakam (VA).
*
* Reads from MongoDB via Prisma.
* VA is a poetic work with 8 chapters (Vaiśiṣṭyas) and no intro segments.
*
* Collections used:
* - book_meta (via Prisma BookMeta model)
* - prose_chapters (via Prisma ProseChapter model)
*/
import { prisma } from "@/lib/db";
import type { JsonValue } from "@prisma/client/runtime/library";
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const BOOK = "va";
/** Volume sentinel for books without volumes. */
const NO_VOLUME = 0;
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type VaMeta = {
book: string;
title: string;
author: string;
year: number;
description: string;
totalChapters: number;
chapters: VaChapterEntry[];
};
export type VaChapterEntry = {
number: number;
title: string;
};
export type VaChapter = {
book: string;
chapter: number | null;
title: string;
type: string;
slug?: string;
paragraphs: string[];
};
/** Used in sidebar / table-of-contents navigation. */
export type VaSidebarItem = {
slug: string;
label: string;
type: string;
chapter: number | null;
};
// ---------------------------------------------------------------------------
// HTML sanitisation
// ---------------------------------------------------------------------------
function sanitizeHtml(html: string): string {
let clean = html.replace(/<script[\s>][\s\S]*?<\/script>/gi, "");
clean = clean.replace(/<style[\s>][\s\S]*?<\/style>/gi, "");
clean = clean.replace(/<iframe[\s>][\s\S]*?<\/iframe>/gi, "");
clean = clean.replace(
/<\/?(?:script|style|iframe|object|embed|form|input|textarea|select|button)[^>]*>/gi,
"",
);
clean = clean.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
clean = clean.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href=""');
clean = clean.replace(/href\s*=\s*'javascript:[^']*'/gi, "href=''");
return clean;
}
function normalizeParagraphs(paragraphs: string[] | JsonValue): string[] {
let result: string[];
if (typeof paragraphs === "string") {
try {
const parsed = JSON.parse(paragraphs);
result = Array.isArray(parsed) ? parsed : [paragraphs];
} catch {
result = [paragraphs];
}
} else if (Array.isArray(paragraphs)) {
result = paragraphs as string[];
} else {
result = [];
}
return result.map(sanitizeHtml);
}
// ---------------------------------------------------------------------------
// Book meta
// ---------------------------------------------------------------------------
export async function getVaMeta(): Promise<VaMeta | null> {
const meta = await prisma.bookMeta.findUnique({ where: { book: BOOK } });
if (!meta) return null;
return meta.data as unknown as VaMeta;
}
// ---------------------------------------------------------------------------
// Chapters
// ---------------------------------------------------------------------------
export async function getVaChapter(
chapterNum: number,
): Promise<VaChapter | null> {
if (chapterNum < 1 || chapterNum > 8 || !Number.isInteger(chapterNum)) {
return null;
}
const doc = await prisma.proseChapter.findUnique({
where: {
book_volume_slug: {
book: BOOK,
volume: NO_VOLUME,
slug: String(chapterNum),
},
},
});
if (!doc) return null;
return {
book: doc.book,
chapter: doc.chapter,
title: doc.title,
type: doc.type,
slug: doc.slug,
paragraphs: normalizeParagraphs(doc.paragraphs),
};
}
// ---------------------------------------------------------------------------
// Unified content accessor
// ---------------------------------------------------------------------------
export async function getVaContent(
slug: string,
): Promise<VaChapter | null> {
const num = Number(slug);
if (!Number.isNaN(num) && Number.isInteger(num) && num >= 1 && num <= 8) {
return getVaChapter(num);
}
return null;
}
// ---------------------------------------------------------------------------
// Sidebar navigation
// ---------------------------------------------------------------------------
export async function getVaSidebarItems(): Promise<VaSidebarItem[]> {
const meta = await getVaMeta();
if (!meta) return [];
return meta.chapters.map((ch) => ({
slug: String(ch.number),
label: `${ch.number}. ${ch.title}`,
type: "chapter",
chapter: ch.number,
}));
}