Files
minstrel/web/src/routes/+page.svelte
T
bvandeusen 4e47b2e7f5 feat(web): pair scroll arrows beside section title; bleed scroller to viewport edge
Restructures HorizontalScrollRow so paired arrows sit in a flex header
beside an optional section title (instead of absolute-positioned at the
row's left/right edges). Adds margin-right: -1rem to the row so the
scroller's items extend to the viewport's right edge, escaping Shell's
p-4 padding. The header bar keeps its right gutter so titles align with
the rest of the page content.

Home page passes 'title' to the first row of each section; subsequent
rows in multi-row sections render arrow-only headers.
2026-05-01 21:05:16 -04:00

133 lines
4.9 KiB
Svelte

<script lang="ts">
import { createHomeQuery } from '$lib/api/home';
import HorizontalScrollRow from '$lib/components/HorizontalScrollRow.svelte';
import AlbumCard from '$lib/components/AlbumCard.svelte';
import ArtistCard from '$lib/components/ArtistCard.svelte';
import CompactTrackCard from '$lib/components/CompactTrackCard.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
import { useDelayed } from '$lib/utils/useDelayed.svelte';
const queryStore = createHomeQuery();
const query = $derived($queryStore);
const data = $derived(query.data);
const showSkeleton = useDelayed(() => query.isPending);
// Each "Most played" row gets 25 of the 75 tracks (server returns up
// to 75; rows 0/1/2 slice [0..25), [25..50), [50..75)).
function chunk<T>(items: T[], size: number): T[][] {
const out: T[][] = [];
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
return out;
}
</script>
<div class="space-y-8">
{#if query.isError}
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
{:else if showSkeleton.value && !data}
<p class="text-text-secondary">Loading…</p>
{:else if data}
<!-- Recently added: 50 albums in 2 rows of 25 -->
<section class="space-y-3">
{#if data.recently_added_albums.length === 0}
<header>
<h2 class="font-display text-2xl font-medium text-text-primary">Recently added</h2>
</header>
<p class="text-text-secondary">Nothing added yet. Scan a folder via the server's config.</p>
{:else}
{#each chunk(data.recently_added_albums, 25) as row, i (i)}
<HorizontalScrollRow
items={row}
title={i === 0 ? 'Recently added' : undefined}
ariaLabel="Recently added row {i + 1}"
>
{#snippet item(album: import('$lib/api/types').AlbumRef)}
<div class="w-44"><AlbumCard {album} /></div>
{/snippet}
</HorizontalScrollRow>
{/each}
{/if}
</section>
<!-- Rediscover: albums + artists -->
<section class="space-y-3">
{#if data.rediscover_albums.length === 0 && data.rediscover_artists.length === 0}
<header>
<h2 class="font-display text-2xl font-medium text-text-primary">Rediscover</h2>
</header>
<p class="text-text-secondary">No forgotten favourites yet. Like some albums or artists to fill this in.</p>
{:else}
{#if data.rediscover_albums.length > 0}
<HorizontalScrollRow
items={data.rediscover_albums}
title="Rediscover"
ariaLabel="Rediscover albums"
>
{#snippet item(album: import('$lib/api/types').AlbumRef)}
<div class="w-44"><AlbumCard {album} /></div>
{/snippet}
</HorizontalScrollRow>
{/if}
{#if data.rediscover_artists.length > 0}
<HorizontalScrollRow
items={data.rediscover_artists}
title={data.rediscover_albums.length === 0 ? 'Rediscover' : undefined}
ariaLabel="Rediscover artists"
>
{#snippet item(artist: import('$lib/api/types').ArtistRef)}
<div class="w-36"><ArtistCard {artist} /></div>
{/snippet}
</HorizontalScrollRow>
{/if}
{/if}
</section>
<!-- Most played: 75 tracks in 3 rows of 25 -->
<section class="space-y-3">
{#if data.most_played_tracks.length === 0}
<header>
<h2 class="font-display text-2xl font-medium text-text-primary">Most played</h2>
</header>
<p class="text-text-secondary">No plays to draw from. Listen to something.</p>
{:else}
{#each chunk(data.most_played_tracks, 25) as row, i (i)}
<HorizontalScrollRow
items={row}
title={i === 0 ? 'Most played' : undefined}
ariaLabel="Most played row {i + 1}"
>
{#snippet item(track: import('$lib/api/types').TrackRef, idx: number)}
<CompactTrackCard
{track}
sectionTracks={row}
index={idx}
/>
{/snippet}
</HorizontalScrollRow>
{/each}
{/if}
</section>
<!-- Last played artists -->
<section class="space-y-3">
{#if data.last_played_artists.length === 0}
<header>
<h2 class="font-display text-2xl font-medium text-text-primary">Last played</h2>
</header>
<p class="text-text-secondary">No recent plays.</p>
{:else}
<HorizontalScrollRow
items={data.last_played_artists}
title="Last played"
ariaLabel="Last played artists"
>
{#snippet item(artist: import('$lib/api/types').ArtistRef)}
<div class="w-36"><ArtistCard {artist} /></div>
{/snippet}
</HorizontalScrollRow>
{/if}
</section>
{/if}
</div>