859aff8d30
test-web / test (push) Successful in 32s
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.
93 lines
3.3 KiB
Svelte
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>
|