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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mangalord-frontend",
|
||||
"version": "0.120.0",
|
||||
"version": "0.121.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import CollectionsGrid from '$lib/components/CollectionsGrid.svelte';
|
||||
import PageTagsList from '$lib/components/PageTagsList.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();
|
||||
|
||||
@@ -66,37 +68,42 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if !data.authenticated}
|
||||
<p class="hint" data-testid="library-signin">
|
||||
<a href="/login?next=/library">Sign in</a> to see your library.
|
||||
</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>
|
||||
{#await data.result}
|
||||
{#if activeTab === 'collections'}
|
||||
<MangaGridSkeleton count={6} />
|
||||
{:else}
|
||||
<BookmarkList bookmarks={data.bookmarks} testid="library-bookmark-list" />
|
||||
<ListRowSkeleton />
|
||||
{/if}
|
||||
{:else if activeTab === 'collections'}
|
||||
{#if data.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.
|
||||
{:then r}
|
||||
{#if !r.authenticated}
|
||||
<p class="hint" data-testid="library-signin">
|
||||
<a href="/login?next=/library">Sign in</a> to see your library.
|
||||
</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}
|
||||
<CollectionsGrid collections={data.collections} />
|
||||
<HistoryList entries={r.history} onClear={clearOne} testid="library-history" />
|
||||
{/if}
|
||||
{:else if activeTab === 'page-tags'}
|
||||
<PageTagsList
|
||||
initialItems={data.pageTags}
|
||||
initialDistinct={data.distinctPageTags}
|
||||
/>
|
||||
{:else}
|
||||
<HistoryList entries={data.history} onClear={clearOne} testid="library-history" />
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
.library-heading {
|
||||
|
||||
@@ -26,30 +26,36 @@ export const load: PageLoad = async () => {
|
||||
distinctPageTags: [] as Awaited<ReturnType<typeof listMyDistinctPageTags>>,
|
||||
error: null as string | null
|
||||
};
|
||||
try {
|
||||
const [bookmarks, collections, history, pageTags, distinctPageTags] =
|
||||
await Promise.all([
|
||||
listMyBookmarks(),
|
||||
listMyCollections({ limit: 200 }),
|
||||
listMyReadProgress({ limit: 100 }),
|
||||
listMyPageTags({ limit: 100 }),
|
||||
listMyDistinctPageTags(undefined, 100)
|
||||
]);
|
||||
return {
|
||||
...empty,
|
||||
bookmarks: bookmarks.items,
|
||||
collections: collections.items,
|
||||
history: history.items,
|
||||
pageTags: pageTags.items,
|
||||
distinctPageTags
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
return { ...empty, authenticated: false };
|
||||
}
|
||||
if (e instanceof ApiError) {
|
||||
return { ...empty, error: e.message };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
// Streamed so the active sub-tab shows a skeleton while the one-shot fetch
|
||||
// (also the auth gate) is in flight.
|
||||
return {
|
||||
result: (async () => {
|
||||
try {
|
||||
const [bookmarks, collections, history, pageTags, distinctPageTags] =
|
||||
await Promise.all([
|
||||
listMyBookmarks(),
|
||||
listMyCollections({ limit: 200 }),
|
||||
listMyReadProgress({ limit: 100 }),
|
||||
listMyPageTags({ limit: 100 }),
|
||||
listMyDistinctPageTags(undefined, 100)
|
||||
]);
|
||||
return {
|
||||
...empty,
|
||||
bookmarks: bookmarks.items,
|
||||
collections: collections.items,
|
||||
history: history.items,
|
||||
pageTags: pageTags.items,
|
||||
distinctPageTags
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
return { ...empty, authenticated: false };
|
||||
}
|
||||
if (e instanceof ApiError) {
|
||||
return { ...empty, error: e.message };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})()
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user