feat(web): add /library/liked page with three sections

Each section is its own infinite query against /api/likes/{type}.
Empty sections show 'No liked X yet' (gated on total === 0). Reuses
ArtistRow / AlbumCard / TrackRow for rendering — same components as
the search overflow pages.
This commit is contained in:
2026-04-26 17:08:06 -04:00
parent de5320a7b4
commit 457b94005f
2 changed files with 201 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
<script lang="ts">
import {
createLikedTracksInfiniteQuery,
createLikedAlbumsInfiniteQuery,
createLikedArtistsInfiniteQuery
} from '$lib/api/likes';
import ArtistRow from '$lib/components/ArtistRow.svelte';
import AlbumCard from '$lib/components/AlbumCard.svelte';
import TrackRow from '$lib/components/TrackRow.svelte';
const artistsStore = $derived(createLikedArtistsInfiniteQuery());
const albumsStore = $derived(createLikedAlbumsInfiniteQuery());
const tracksStore = $derived(createLikedTracksInfiniteQuery());
const artistsQuery = $derived($artistsStore);
const albumsQuery = $derived($albumsStore);
const tracksQuery = $derived($tracksStore);
const artists = $derived(artistsQuery?.data?.pages?.flatMap((p) => p.items) ?? []);
const albums = $derived(albumsQuery?.data?.pages?.flatMap((p) => p.items) ?? []);
const tracks = $derived(tracksQuery?.data?.pages?.flatMap((p) => p.items) ?? []);
const artistsTotal = $derived(artistsQuery?.data?.pages?.[0]?.total ?? 0);
const albumsTotal = $derived(albumsQuery?.data?.pages?.[0]?.total ?? 0);
const tracksTotal = $derived(tracksQuery?.data?.pages?.[0]?.total ?? 0);
</script>
<div class="space-y-6">
<h1 class="text-2xl font-semibold">Liked</h1>
<section>
<h2 class="mb-2 text-lg font-semibold">Artists</h2>
{#if artistsTotal === 0}
<p class="text-text-secondary">No liked artists yet.</p>
{:else}
<div>
{#each artists as a (a.id)}
<ArtistRow artist={a} />
{/each}
</div>
{#if artistsQuery?.hasNextPage}
<button
type="button"
class="mt-2 w-full rounded bg-surface py-2 text-sm hover:bg-surface-hover disabled:opacity-60"
disabled={artistsQuery.isFetchingNextPage}
onclick={() => artistsQuery.fetchNextPage()}
>
{artistsQuery.isFetchingNextPage ? 'Loading…' : 'Load more'}
</button>
{/if}
{/if}
</section>
<section>
<h2 class="mb-2 text-lg font-semibold">Albums</h2>
{#if albumsTotal === 0}
<p class="text-text-secondary">No liked albums yet.</p>
{:else}
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each albums as al (al.id)}
<AlbumCard album={al} />
{/each}
</div>
{#if albumsQuery?.hasNextPage}
<button
type="button"
class="mt-2 w-full rounded bg-surface py-2 text-sm hover:bg-surface-hover disabled:opacity-60"
disabled={albumsQuery.isFetchingNextPage}
onclick={() => albumsQuery.fetchNextPage()}
>
{albumsQuery.isFetchingNextPage ? 'Loading…' : 'Load more'}
</button>
{/if}
{/if}
</section>
<section>
<h2 class="mb-2 text-lg font-semibold">Tracks</h2>
{#if tracksTotal === 0}
<p class="text-text-secondary">No liked tracks yet.</p>
{:else}
<div class="overflow-hidden rounded border border-border">
{#each tracks as t, i (t.id)}
<TrackRow tracks={tracks} index={i} />
{/each}
</div>
{#if tracksQuery?.hasNextPage}
<button
type="button"
class="mt-2 w-full rounded bg-surface py-2 text-sm hover:bg-surface-hover disabled:opacity-60"
disabled={tracksQuery.isFetchingNextPage}
onclick={() => tracksQuery.fetchNextPage()}
>
{tracksQuery.isFetchingNextPage ? 'Loading…' : 'Load more'}
</button>
{/if}
{/if}
</section>
</div>