From 06feb91f4fc8163330caf2d8403ca6390b9b34e1 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 4 Mar 2026 12:55:03 +0500 Subject: [PATCH] feat(ui): add homepage with Continue Reading, Recently Updated, and stats Replace the single-button placeholder with a full homepage: - Stats bar: total books, total chapters, books in progress - Continue Reading grid (up to 6): links directly to last chapter read - Recently Updated grid (up to 6): recent books not already in progress - Empty state with Discover Novels CTA when library is empty New pocketbase.ts helpers: recentlyAddedBooks(), getHomeStats(), listN(), countCollection(). --- ui/src/lib/server/pocketbase.ts | 40 +++++++++ ui/src/routes/+page.server.ts | 49 ++++++++++ ui/src/routes/+page.svelte | 152 +++++++++++++++++++++++++++++++- 3 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 ui/src/routes/+page.server.ts diff --git a/ui/src/lib/server/pocketbase.ts b/ui/src/lib/server/pocketbase.ts index 945cd16..1719999 100644 --- a/ui/src/lib/server/pocketbase.ts +++ b/ui/src/lib/server/pocketbase.ts @@ -130,6 +130,25 @@ async function listAll(collection: string, filter = '', sort = ''): Promise(collection: string, n: number, filter = '', sort = ''): Promise { + const params = new URLSearchParams({ perPage: String(n) }); + if (filter) params.set('filter', filter); + if (sort) params.set('sort', sort); + const data = await pbGet>( + `/api/collections/${collection}/records?${params.toString()}` + ); + return data.items ?? []; +} + +async function countCollection(collection: string, filter = ''): Promise { + const params = new URLSearchParams({ perPage: '1' }); + if (filter) params.set('filter', filter); + const data = await pbGet>( + `/api/collections/${collection}/records?${params.toString()}` + ); + return (data as { totalItems: number }).totalItems ?? 0; +} + async function listOne(collection: string, filter: string): Promise { const params = new URLSearchParams({ perPage: '1', filter }); const data = await pbGet>( @@ -154,6 +173,27 @@ export async function getBook(slug: string): Promise { return listOne('books', `slug="${slug}"`); } +export async function recentlyAddedBooks(limit = 6): Promise { + return listN('books', limit, '', '-meta_updated'); +} + +export async function recentlyUpdatedBooks(limit = 6): Promise { + return listN('books', limit, '', '-meta_updated'); +} + +export interface HomeStats { + totalBooks: number; + totalChapters: number; +} + +export async function getHomeStats(): Promise { + const [totalBooks, totalChapters] = await Promise.all([ + countCollection('books'), + countCollection('chapters_idx') + ]); + return { totalBooks, totalChapters }; +} + // ─── Chapter index ──────────────────────────────────────────────────────────── export async function listChapterIdx(slug: string): Promise { diff --git a/ui/src/routes/+page.server.ts b/ui/src/routes/+page.server.ts new file mode 100644 index 0000000..d95ac80 --- /dev/null +++ b/ui/src/routes/+page.server.ts @@ -0,0 +1,49 @@ +import type { PageServerLoad } from './$types'; +import { + listBooks, + recentlyAddedBooks, + allProgress, + getHomeStats +} from '$lib/server/pocketbase'; +import { log } from '$lib/server/logger'; +import type { Book, Progress } from '$lib/server/pocketbase'; + +export const load: PageServerLoad = async ({ locals }) => { + let allBooks: Book[] = []; + let recentBooks: Book[] = []; + let progressList: Progress[] = []; + let stats = { totalBooks: 0, totalChapters: 0 }; + + try { + [allBooks, recentBooks, progressList, stats] = await Promise.all([ + listBooks(), + recentlyAddedBooks(8), + allProgress(locals.sessionId, locals.user?.id), + getHomeStats() + ]); + } catch (e) { + log.error('home', 'failed to load home data', { err: String(e) }); + } + + // Build slug → book lookup + const bookMap = new Map(allBooks.map((b) => [b.slug, b])); + + // Continue reading: progress entries joined with book data, most recent first + const continueReading = progressList + .filter((p) => bookMap.has(p.slug)) + .slice(0, 6) + .map((p) => ({ book: bookMap.get(p.slug)!, chapter: p.chapter })); + + // Recently updated: deduplicate against continueReading slugs + const inProgressSlugs = new Set(continueReading.map((c) => c.book.slug)); + const recentlyUpdated = recentBooks.filter((b) => !inProgressSlugs.has(b.slug)).slice(0, 6); + + return { + continueReading, + recentlyUpdated, + stats: { + ...stats, + booksInProgress: continueReading.length + } + }; +}; diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte index d3549a8..3916a35 100644 --- a/ui/src/routes/+page.svelte +++ b/ui/src/routes/+page.svelte @@ -1,3 +1,149 @@ - - Browse Library - + + + + libnovel + + + +
+
+

{data.stats.totalBooks}

+

Books

+
+
+

{data.stats.totalChapters.toLocaleString()}

+

Chapters

+
+
+

{data.stats.booksInProgress}

+

In progress

+
+
+ + +{#if data.continueReading.length > 0} +
+
+

Continue Reading

+ View all +
+ +
+{/if} + + +{#if data.recentlyUpdated.length > 0} +
+
+

Recently Updated

+ View all +
+ +
+{/if} + + +{#if data.continueReading.length === 0 && data.recentlyUpdated.length === 0} +
+

Your library is empty

+

Discover novels and scrape them into your library.

+ + Discover Novels + +
+{/if}