From 6bf79ab392419d7e73c9e2407d3eacc99e11ccfd Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 2 Mar 2026 21:39:11 +0500 Subject: [PATCH] feat(ui): add chapter reader page with SSR markdown rendering from MinIO --- ui/package-lock.json | 13 +++ ui/package.json | 1 + ui/src/app.css | 39 +++++++++ .../books/[slug]/chapters/[n]/+page.server.ts | 45 ++++++++++ .../books/[slug]/chapters/[n]/+page.svelte | 85 +++++++++++++++++++ 5 files changed, 183 insertions(+) create mode 100644 ui/src/routes/books/[slug]/chapters/[n]/+page.server.ts create mode 100644 ui/src/routes/books/[slug]/chapters/[n]/+page.svelte diff --git a/ui/package-lock.json b/ui/package-lock.json index 2ae568b..af9008e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,6 +8,7 @@ "name": "ui", "version": "0.0.1", "dependencies": { + "marked": "^17.0.3", "pocketbase": "^0.26.8" }, "devDependencies": { @@ -1979,6 +1980,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz", + "integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/ui/package.json b/ui/package.json index bb5ab5b..01bca59 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,6 +24,7 @@ "vite": "^7.3.1" }, "dependencies": { + "marked": "^17.0.3", "pocketbase": "^0.26.8" } } diff --git a/ui/src/app.css b/ui/src/app.css index 7bc294a..e7bc911 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -14,3 +14,42 @@ html { background-color: var(--color-surface); color: var(--color-text); } + +/* ── Chapter prose ─────────────────────────────────────────────────── */ +.prose-chapter { + max-width: 72ch; + line-height: 1.85; + font-size: 1.05rem; + color: #d4d4d8; /* zinc-300 */ +} + +.prose-chapter h1, +.prose-chapter h2, +.prose-chapter h3 { + color: #f4f4f5; + font-weight: 700; + margin-top: 1.5em; + margin-bottom: 0.5em; +} + +.prose-chapter h1 { font-size: 1.4rem; } +.prose-chapter h2 { font-size: 1.2rem; } +.prose-chapter h3 { font-size: 1.05rem; } + +.prose-chapter p { + margin-bottom: 1.2em; +} + +.prose-chapter em { + color: #a1a1aa; +} + +.prose-chapter strong { + color: #f4f4f5; +} + +.prose-chapter hr { + border-color: #3f3f46; + margin: 2em 0; +} + diff --git a/ui/src/routes/books/[slug]/chapters/[n]/+page.server.ts b/ui/src/routes/books/[slug]/chapters/[n]/+page.server.ts new file mode 100644 index 0000000..3df478b --- /dev/null +++ b/ui/src/routes/books/[slug]/chapters/[n]/+page.server.ts @@ -0,0 +1,45 @@ +import { error } from '@sveltejs/kit'; +import { marked } from 'marked'; +import type { PageServerLoad } from './$types'; +import { getBook, listChapterIdx } from '$lib/server/pocketbase'; +import { presignChapter } from '$lib/server/minio'; + +export const load: PageServerLoad = async ({ params, locals }) => { + const { slug } = params; + const n = parseInt(params.n, 10); + + if (!n || n < 1) error(400, 'Invalid chapter number'); + + // Fetch book metadata and chapter index in parallel + const [book, chapters] = await Promise.all([getBook(slug), listChapterIdx(slug)]); + + if (!book) error(404, `Book "${slug}" not found`); + + const chapterIdx = chapters.find((c) => c.number === n); + if (!chapterIdx) error(404, `Chapter ${n} not found`); + + // Get presigned URL and fetch chapter markdown server-side + let html = ''; + try { + const url = await presignChapter(slug, n); + const res = await fetch(url); + if (!res.ok) throw new Error(`MinIO returned ${res.status}`); + const markdown = await res.text(); + html = await marked(markdown, { async: true }); + } catch (e) { + // Don't hard-fail — show empty content with error message + console.error('Failed to fetch chapter content:', e); + } + + const prevChapter = chapters.find((c) => c.number === n - 1) ?? null; + const nextChapter = chapters.find((c) => c.number === n + 1) ?? null; + + return { + book: { slug: book.slug, title: book.title }, + chapter: chapterIdx, + html, + prev: prevChapter ? prevChapter.number : null, + next: nextChapter ? nextChapter.number : null, + sessionId: locals.sessionId + }; +}; diff --git a/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte new file mode 100644 index 0000000..e4706fb --- /dev/null +++ b/ui/src/routes/books/[slug]/chapters/[n]/+page.svelte @@ -0,0 +1,85 @@ + + + + {data.chapter.title || `Chapter ${data.chapter.number}`} — {data.book.title} — libnovel + + + +
+ + + + + {data.book.title} + + +
+ {#if data.prev} + + ← Ch.{data.prev} + + {/if} + {#if data.next} + + Ch.{data.next} → + + {/if} +
+
+ + +
+

Chapter {data.chapter.number}

+

+ {data.chapter.title || `Chapter ${data.chapter.number}`} +

+ {#if data.chapter.date_label} +

{data.chapter.date_label}

+ {/if} +
+ + +{#if !data.html} +
+

Chapter content not available.

+
+{:else} +
+ {@html data.html} +
+{/if} + + +
+ {#if data.prev} + + ← Previous chapter + + {:else} +
+ {/if} + {#if data.next} + + Next chapter → + + {/if} +