v2 #1

Open
kamil wants to merge 231 commits from v2 into main
5 changed files with 183 additions and 0 deletions
Showing only changes of commit 6bf79ab392 - Show all commits

13
ui/package-lock.json generated
View File

@@ -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",

View File

@@ -24,6 +24,7 @@
"vite": "^7.3.1"
},
"dependencies": {
"marked": "^17.0.3",
"pocketbase": "^0.26.8"
}
}

View File

@@ -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;
}

View File

@@ -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
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<svelte:head>
<title>{data.chapter.title || `Chapter ${data.chapter.number}`} — {data.book.title} — libnovel</title>
</svelte:head>
<!-- Top nav -->
<div class="flex items-center justify-between mb-6 gap-4">
<a
href="/books/{data.book.slug}"
class="text-zinc-400 hover:text-zinc-100 text-sm flex items-center gap-1 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
{data.book.title}
</a>
<div class="flex gap-2">
{#if data.prev}
<a
href="/books/{data.book.slug}/chapters/{data.prev}"
class="px-3 py-1.5 rounded bg-zinc-700 text-zinc-300 text-sm hover:bg-zinc-600 transition-colors"
>
&larr; Ch.{data.prev}
</a>
{/if}
{#if data.next}
<a
href="/books/{data.book.slug}/chapters/{data.next}"
class="px-3 py-1.5 rounded bg-amber-400 text-zinc-900 text-sm font-semibold hover:bg-amber-300 transition-colors"
>
Ch.{data.next} &rarr;
</a>
{/if}
</div>
</div>
<!-- Chapter heading -->
<div class="mb-8">
<p class="text-zinc-500 text-sm mb-1">Chapter {data.chapter.number}</p>
<h1 class="text-xl font-bold text-zinc-100">
{data.chapter.title || `Chapter ${data.chapter.number}`}
</h1>
{#if data.chapter.date_label}
<p class="text-zinc-600 text-xs mt-1">{data.chapter.date_label}</p>
{/if}
</div>
<!-- Chapter content -->
{#if !data.html}
<div class="text-zinc-500 text-center py-16">
<p>Chapter content not available.</p>
</div>
{:else}
<div class="prose-chapter">
{@html data.html}
</div>
{/if}
<!-- Bottom nav -->
<div class="flex justify-between mt-12 pt-6 border-t border-zinc-800 gap-4">
{#if data.prev}
<a
href="/books/{data.book.slug}/chapters/{data.prev}"
class="px-4 py-2 rounded bg-zinc-700 text-zinc-300 text-sm hover:bg-zinc-600 transition-colors"
>
&larr; Previous chapter
</a>
{:else}
<div></div>
{/if}
{#if data.next}
<a
href="/books/{data.book.slug}/chapters/{data.next}"
class="px-4 py-2 rounded bg-amber-400 text-zinc-900 text-sm font-semibold hover:bg-amber-300 transition-colors"
>
Next chapter &rarr;
</a>
{/if}
</div>