Files
minstrel/web/src/routes/library/artists/+page.svelte
T
bvandeusen 859aff8d30
test-web / test (push) Successful in 32s
feat(web): smart empty-states with action affordances (Tier B6)
Replace dead-end empty copy with EmptyState cards that include a
clear next step.

- EmptyState.svelte: reusable card with `block` (whole-tab) and
  `inline` (sub-section) variants, an actions snippet for buttons.
- Artists / Albums: when the whole library is empty, link to
  /settings (the operator can scan a folder there).
- Liked: when all three sub-sections are empty, show a single
  whole-tab card ("No likes yet" with Explore Home + Browse albums).
  When only one sub-section is empty, an inline hint with a link
  to the corresponding library tab.
- History: "Listen to something" button → Home.
- Playlists: "Create a playlist" button calls the existing create
  flow; renamed from "New playlist" to avoid colliding with the
  header's button-by-name lookup in tests.

Liked tab tests updated to match: the previous "three 'no liked X
yet'" assertion is now a single onboarding card test + a sibling
test for the mixed populated/empty case.
2026-06-01 13:53:38 -04:00

93 lines
3.3 KiB
Svelte

<script lang="ts">
import { pageTitle } from '$lib/branding';
import { createArtistsQuery } from '$lib/api/queries';
import ArtistCard from '$lib/components/ArtistCard.svelte';
import AlphabeticalGrid from '$lib/components/AlphabeticalGrid.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
import InfiniteScrollSentinel from '$lib/components/InfiniteScrollSentinel.svelte';
import QuickFilter from '$lib/components/QuickFilter.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import { useDelayed } from '$lib/utils/useDelayed.svelte';
import type { ArtistRef } from '$lib/api/types';
const queryStore = $derived(createArtistsQuery('alpha'));
const query = $derived($queryStore);
const artists = $derived(query.data?.pages?.flatMap((p) => p.items) ?? []);
const total = $derived(query.data?.pages?.[0]?.total ?? 0);
const showSkeleton = useDelayed(() => query.isPending);
let filter = $state('');
const filtered = $derived.by(() => {
const q = filter.trim().toLowerCase();
if (!q) return artists;
return artists.filter((a) => a.name.toLowerCase().includes(q));
});
</script>
<svelte:head><title>{pageTitle('Library · Artists')}</title></svelte:head>
<div class="space-y-4">
<header class="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 class="font-display text-2xl font-medium text-text-primary">Artists</h1>
{#if !query.isPending && !query.isError}
<p class="text-sm text-text-secondary">
{total} {total === 1 ? 'artist' : 'artists'}
</p>
{/if}
</div>
{#if artists.length > 0}
<QuickFilter bind:value={filter} placeholder="Filter artists" />
{/if}
</header>
{#if query.isError}
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
{:else if showSkeleton.value && artists.length === 0}
<p class="text-text-secondary">Loading…</p>
{:else if !query.isPending && total === 0}
<EmptyState
title="No artists yet"
hint="Once a library folder has been scanned, artists will appear here."
>
{#snippet actions()}
<a
href="/settings"
class="inline-flex items-center rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90"
>
Open settings
</a>
{/snippet}
</EmptyState>
{:else if filter.trim() && filtered.length === 0}
<p class="text-text-secondary">
No artists in your loaded library match
<span class="font-medium">'{filter.trim()}'</span>.
<a href={`/search/artists?q=${encodeURIComponent(filter.trim())}`} class="text-accent hover:underline">
Search the full library →
</a>
</p>
{:else}
<AlphabeticalGrid
items={filtered}
getKey={(a: ArtistRef) => a.sort_name || a.name}
>
{#snippet item(a: ArtistRef)}
<ArtistCard artist={a} />
{/snippet}
</AlphabeticalGrid>
{#if query.hasNextPage}
<InfiniteScrollSentinel
enabled={!query.isFetchingNextPage}
onIntersect={() => query.fetchNextPage()}
/>
{#if query.isFetchingNextPage}
<p class="py-2 text-center text-sm text-text-secondary">Loading more…</p>
{/if}
{:else if artists.length > 0}
<p class="py-2 text-center text-sm text-text-secondary">End of library</p>
{/if}
{/if}
</div>