feat: loading skeleton on the library page

The library loader streams its one-shot bundle (also the auth gate), so the
active sub-tab shows a skeleton while it loads — a grid skeleton for the
Collections tab, a row skeleton for Bookmarks/History/Page-tags — instead of
only the global nav bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 07:21:20 +02:00
parent 16cdec051a
commit 3c8e264f48
5 changed files with 68 additions and 55 deletions

2
backend/Cargo.lock generated
View File

@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mangalord" name = "mangalord"
version = "0.120.0" version = "0.121.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "mangalord" name = "mangalord"
version = "0.120.0" version = "0.121.0"
edition = "2021" edition = "2021"
default-run = "mangalord" default-run = "mangalord"

View File

@@ -1,6 +1,6 @@
{ {
"name": "mangalord-frontend", "name": "mangalord-frontend",
"version": "0.120.0", "version": "0.121.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -7,6 +7,8 @@
import CollectionsGrid from '$lib/components/CollectionsGrid.svelte'; import CollectionsGrid from '$lib/components/CollectionsGrid.svelte';
import PageTagsList from '$lib/components/PageTagsList.svelte'; import PageTagsList from '$lib/components/PageTagsList.svelte';
import HistoryList from '$lib/components/HistoryList.svelte'; import HistoryList from '$lib/components/HistoryList.svelte';
import ListRowSkeleton from '$lib/components/ListRowSkeleton.svelte';
import MangaGridSkeleton from '$lib/components/MangaGridSkeleton.svelte';
let { data } = $props(); let { data } = $props();
@@ -66,37 +68,42 @@
/> />
</div> </div>
{#if !data.authenticated} {#await data.result}
<p class="hint" data-testid="library-signin"> {#if activeTab === 'collections'}
<a href="/login?next=/library">Sign in</a> to see your library. <MangaGridSkeleton count={6} />
</p>
{:else if data.error}
<p class="error" role="alert" data-testid="library-error">
Couldn't load library: {data.error}
</p>
{:else if activeTab === 'bookmarks'}
{#if data.bookmarks.length === 0}
<p class="hint" data-testid="library-bookmarks-empty">No bookmarks yet.</p>
{:else} {:else}
<BookmarkList bookmarks={data.bookmarks} testid="library-bookmark-list" /> <ListRowSkeleton />
{/if} {/if}
{:else if activeTab === 'collections'} {:then r}
{#if data.collections.length === 0} {#if !r.authenticated}
<p class="hint" data-testid="library-collections-empty"> <p class="hint" data-testid="library-signin">
You don't have any collections yet. Open any manga and use <a href="/login?next=/library">Sign in</a> to see your library.
<strong>Add to collection</strong> to start one.
</p> </p>
{:else if r.error}
<p class="error" role="alert" data-testid="library-error">
Couldn't load library: {r.error}
</p>
{:else if activeTab === 'bookmarks'}
{#if r.bookmarks.length === 0}
<p class="hint" data-testid="library-bookmarks-empty">No bookmarks yet.</p>
{:else}
<BookmarkList bookmarks={r.bookmarks} testid="library-bookmark-list" />
{/if}
{:else if activeTab === 'collections'}
{#if r.collections.length === 0}
<p class="hint" data-testid="library-collections-empty">
You don't have any collections yet. Open any manga and use
<strong>Add to collection</strong> to start one.
</p>
{:else}
<CollectionsGrid collections={r.collections} />
{/if}
{:else if activeTab === 'page-tags'}
<PageTagsList initialItems={r.pageTags} initialDistinct={r.distinctPageTags} />
{:else} {:else}
<CollectionsGrid collections={data.collections} /> <HistoryList entries={r.history} onClear={clearOne} testid="library-history" />
{/if} {/if}
{:else if activeTab === 'page-tags'} {/await}
<PageTagsList
initialItems={data.pageTags}
initialDistinct={data.distinctPageTags}
/>
{:else}
<HistoryList entries={data.history} onClear={clearOne} testid="library-history" />
{/if}
<style> <style>
.library-heading { .library-heading {

View File

@@ -26,30 +26,36 @@ export const load: PageLoad = async () => {
distinctPageTags: [] as Awaited<ReturnType<typeof listMyDistinctPageTags>>, distinctPageTags: [] as Awaited<ReturnType<typeof listMyDistinctPageTags>>,
error: null as string | null error: null as string | null
}; };
try { // Streamed so the active sub-tab shows a skeleton while the one-shot fetch
const [bookmarks, collections, history, pageTags, distinctPageTags] = // (also the auth gate) is in flight.
await Promise.all([ return {
listMyBookmarks(), result: (async () => {
listMyCollections({ limit: 200 }), try {
listMyReadProgress({ limit: 100 }), const [bookmarks, collections, history, pageTags, distinctPageTags] =
listMyPageTags({ limit: 100 }), await Promise.all([
listMyDistinctPageTags(undefined, 100) listMyBookmarks(),
]); listMyCollections({ limit: 200 }),
return { listMyReadProgress({ limit: 100 }),
...empty, listMyPageTags({ limit: 100 }),
bookmarks: bookmarks.items, listMyDistinctPageTags(undefined, 100)
collections: collections.items, ]);
history: history.items, return {
pageTags: pageTags.items, ...empty,
distinctPageTags bookmarks: bookmarks.items,
}; collections: collections.items,
} catch (e) { history: history.items,
if (e instanceof ApiError && e.status === 401) { pageTags: pageTags.items,
return { ...empty, authenticated: false }; distinctPageTags
} };
if (e instanceof ApiError) { } catch (e) {
return { ...empty, error: e.message }; if (e instanceof ApiError && e.status === 401) {
} return { ...empty, authenticated: false };
throw e; }
} if (e instanceof ApiError) {
return { ...empty, error: e.message };
}
throw e;
}
})()
};
}; };