Files
Mangalord/frontend/src/routes/authors/[id]/+page.svelte
MechaCat02 cee1e73f98 feat: grid skeletons on the author and collection detail pages
Both loaders now stream their manga grid instead of blocking on it: the cheap
header renders immediately (and still handles 404/401 in the loader) while a
MangaGridSkeleton stands in until the grid resolves, then swaps in without a
layout shift. Collection detail seeds its optimistic-removal state from the
stream via a guarded effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:01:53 +02:00

116 lines
3.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import MangaCard from '$lib/components/MangaCard.svelte';
import MangaGridSkeleton from '$lib/components/MangaGridSkeleton.svelte';
import Pager from '$lib/components/Pager.svelte';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
let { data } = $props();
const author = $derived(data.author);
const currentPage = $derived(data.currentPage);
const pageSize = $derived(data.pageSize);
function goToPage(p: number) {
if (p === currentPage) return;
const url = new URL($page.url);
if (p === 1) url.searchParams.delete('page');
else url.searchParams.set('page', String(p));
goto(url.pathname + url.search, { noScroll: false });
}
</script>
<svelte:head>
<title>Mangalord | {author.name}</title>
</svelte:head>
<nav class="back">
<a href="/" class="back-link">
<ArrowLeft size={16} aria-hidden="true" />
<span>Back to search</span>
</a>
</nav>
<header class="overview">
<h1 data-testid="author-name">{author.name}</h1>
<p class="count" data-testid="author-manga-count">
{author.manga_count}
{author.manga_count === 1 ? 'work' : 'works'}
</p>
</header>
{#await data.mangas}
<MangaGridSkeleton />
{:then result}
{@const mangas = result.items}
{@const total = result.total}
{@const totalPages = total != null && total > 0 ? Math.ceil(total / pageSize) : 1}
{@const rangeStart = mangas.length === 0 ? 0 : (currentPage - 1) * pageSize + 1}
{@const rangeEnd = (currentPage - 1) * pageSize + mangas.length}
{#if mangas.length === 0}
<p class="status" data-testid="author-no-mangas">
No mangas attributed to this author.
</p>
{:else}
{#if total != null}
<p class="meta" data-testid="author-shown-of-total">
Showing {rangeStart}{rangeEnd} of {total}
</p>
{/if}
<ul class="manga-grid" data-testid="author-manga-list">
{#each mangas as m (m.id)}
<MangaCard manga={m} testid={`author-manga-${m.id}`} />
{/each}
</ul>
<Pager page={currentPage} {totalPages} onChange={goToPage} testid="author-pager" />
{/if}
{:catch}
<p class="status" role="alert" data-testid="author-error">
Could not load this author's mangas.
</p>
{/await}
<style>
.back {
margin-bottom: var(--space-3);
}
.back-link {
display: inline-flex;
align-items: center;
gap: var(--space-1);
color: var(--text-muted);
font-size: var(--font-sm);
}
.back-link:hover {
color: var(--primary);
text-decoration: none;
}
.overview {
margin-bottom: var(--space-5);
}
.overview h1 {
margin: 0 0 var(--space-1);
}
.count {
color: var(--text-muted);
margin: 0 0 var(--space-2);
}
.meta {
color: var(--text-muted);
font-size: var(--font-sm);
margin: 0 0 var(--space-3);
}
.status {
color: var(--text-muted);
}
/* `.manga-grid` is a shared global (see lib/styles/tokens.css). */
</style>