feat(web): replace / placeholder with real artists list
Uses createArtistsQuery (infinite), URL-driven sort dropdown, Load more pagination, delayed skeleton, and shared error banner. Also introduces the test-utils/query.ts helpers for subsequent page tests.
This commit is contained in:
@@ -1,2 +1,83 @@
|
||||
<h1 class="text-2xl font-semibold">Library</h1>
|
||||
<p class="mt-2 text-text-secondary">Library views land in a subsequent plan.</p>
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { createArtistsQuery, type ArtistSort } from '$lib/api/queries';
|
||||
import ArtistRow from '$lib/components/ArtistRow.svelte';
|
||||
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
|
||||
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
|
||||
import { useDelayed } from '$lib/utils/useDelayed.svelte';
|
||||
|
||||
const sort: ArtistSort = $derived(
|
||||
(page.url.searchParams.get('sort') as ArtistSort) === 'newest' ? 'newest' : 'alpha'
|
||||
);
|
||||
|
||||
const queryStore = $derived(createArtistsQuery(sort));
|
||||
const query = $derived($queryStore);
|
||||
|
||||
// Flattened artist list from all loaded pages.
|
||||
const artists = $derived(
|
||||
query.data?.pages?.flatMap((p) => p.items) ?? []
|
||||
);
|
||||
|
||||
const total = $derived(query.data?.pages?.[0]?.total ?? 0);
|
||||
|
||||
const showSkeleton = useDelayed(() => query.isPending);
|
||||
|
||||
function onSortChange(e: Event) {
|
||||
const value = (e.currentTarget as HTMLSelectElement).value as ArtistSort;
|
||||
goto(`?sort=${value}`, { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Library</h1>
|
||||
{#if !query.isPending && !query.isError}
|
||||
<p class="text-sm text-text-secondary">
|
||||
{total} {total === 1 ? 'artist' : 'artists'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<span class="text-text-secondary">Sort</span>
|
||||
<select
|
||||
value={sort}
|
||||
onchange={onSortChange}
|
||||
class="rounded border border-border bg-surface px-2 py-1"
|
||||
>
|
||||
<option value="alpha">A–Z</option>
|
||||
<option value="newest">Newest</option>
|
||||
</select>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
{#if query.isError}
|
||||
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
|
||||
{:else if showSkeleton.value && artists.length === 0}
|
||||
<LibrarySkeleton variant="list" count={12} />
|
||||
{:else if !query.isPending && total === 0}
|
||||
<p class="text-text-secondary">
|
||||
No artists yet — scan a library folder via the server's config.
|
||||
</p>
|
||||
{:else}
|
||||
<div>
|
||||
{#each artists as a (a.id)}
|
||||
<ArtistRow artist={a} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if query.hasNextPage}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded bg-surface py-2 text-sm hover:bg-surface-hover disabled:opacity-60"
|
||||
disabled={query.isFetchingNextPage}
|
||||
onclick={() => query.fetchNextPage()}
|
||||
>
|
||||
{query.isFetchingNextPage ? 'Loading…' : 'Load more'}
|
||||
</button>
|
||||
{:else if artists.length > 0}
|
||||
<p class="py-2 text-center text-sm text-text-secondary">End of library</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user