feat(web): add artist detail page at /artists/:id

Renders the artist name + album count and a responsive AlbumCard grid
from the nested ArtistDetail response. 404 surfaces a distinct
non-retryable 'not found' state; other errors share the retry banner.
This commit is contained in:
2026-04-23 20:12:57 -04:00
parent 25d44a180c
commit 2e49335aab
2 changed files with 144 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
<script lang="ts">
import { page } from '$app/state';
import { createArtistQuery } from '$lib/api/queries';
import AlbumCard from '$lib/components/AlbumCard.svelte';
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
import { useDelayed } from '$lib/utils/useDelayed.svelte';
import type { ApiError } from '$lib/api/client';
const id = $derived(page.params.id ?? '');
const queryStore = $derived(createArtistQuery(id));
const query = $derived($queryStore);
const showSkeleton = useDelayed(() => query.isPending);
const notFound = $derived(
query.isError && (query.error as unknown as ApiError | undefined)?.code === 'not_found'
);
</script>
<div class="space-y-4">
<a href="/" class="text-sm text-text-secondary hover:text-text-primary">← Library</a>
{#if notFound}
<div role="alert" class="rounded border border-border bg-surface p-4">
<p>Artist not found.</p>
<a href="/" class="mt-2 inline-block text-sm text-accent">Back to Library</a>
</div>
{:else if query.isError}
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
{:else if showSkeleton.value && !query.data}
<LibrarySkeleton variant="grid" count={12} />
{:else if query.data}
{@const detail = query.data}
<header>
<h1 class="text-2xl font-semibold">{detail.name}</h1>
<p class="text-sm text-text-secondary">
{detail.album_count} {detail.album_count === 1 ? 'album' : 'albums'}
</p>
</header>
{#if detail.albums.length === 0}
<p class="text-text-secondary">This artist has no albums in the library.</p>
{:else}
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each detail.albums as album (album.id)}
<AlbumCard {album} />
{/each}
</div>
{/if}
{/if}
</div>