From 17b276b5f52211cab1cec765bb79d5d484395988 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 1 Jun 2026 20:10:47 -0400 Subject: [PATCH] feat(web): hero row at top of Home (For You + most recently added) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 — Adds a 2-up grid above the Playlists carousel: - Today's pick: For-You playlist card with a Play button that invokes the system-shuffle endpoint (same path as PlaylistCard uses for the play overlay). - Just added: most recently added album card with a Play button that fetches the album detail and queues the tracks. Both cards share HomeHeroCard.svelte — cover left, eyebrow + title + subtitle + Play right, on a from-accent/15 → surface gradient so the eye lands here before scrolling into the carousels. Each card links to its detail page; the Play button is event-stopped so it plays in-place. md:grid-cols-2 stacks on narrow viewports. Only renders when at least one of the two pieces of data is available so the page degrades cleanly on fresh installs. --- web/src/lib/components/HomeHeroCard.svelte | 75 ++++++++++++++++++++ web/src/routes/+page.svelte | 81 ++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 web/src/lib/components/HomeHeroCard.svelte diff --git a/web/src/lib/components/HomeHeroCard.svelte b/web/src/lib/components/HomeHeroCard.svelte new file mode 100644 index 00000000..4c4e6445 --- /dev/null +++ b/web/src/lib/components/HomeHeroCard.svelte @@ -0,0 +1,75 @@ + + + +
+ +
+
+
+
{eyebrow}
+
+ {title} +
+ {#if subtitle} +
{subtitle}
+ {/if} +
+ +
+
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 80b8d6db..60cf8d5a 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -3,6 +3,13 @@ import { createHomeQuery } from '$lib/api/home'; import HorizontalScrollRow from '$lib/components/HorizontalScrollRow.svelte'; import AlbumCard from '$lib/components/AlbumCard.svelte'; + import HomeHeroCard from '$lib/components/HomeHeroCard.svelte'; + import { systemShuffle, getPlaylist } from '$lib/api/playlists'; + import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef'; + import { playQueue } from '$lib/player/store.svelte'; + import { api } from '$lib/api/client'; + import { coverUrl as albumCoverUrl } from '$lib/media/covers'; + import type { AlbumDetail, TrackRef as TrackRefType } from '$lib/api/types'; import ArtistCard from '$lib/components/ArtistCard.svelte'; import CompactTrackCard from '$lib/components/CompactTrackCard.svelte'; import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte'; @@ -75,6 +82,46 @@ const userPlaylists = $derived(userPlaylistsQ.data?.owned ?? []); + // Hero row: For-You + most-recently-added album. Anchors the page + // so the eye lands on these instead of cascading down a wall of + // identical tiles. + const latestAlbum = $derived(data?.recently_added_albums?.[0] ?? null); + + function albumCoverFor(albumId: string | undefined | null): string { + return albumId ? albumCoverUrl(albumId) : ''; + } + + let heroStarting = $state(false); + + async function playForYou(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (heroStarting || !forYouPlaylist?.system_variant) return; + heroStarting = true; + try { + const detail = await systemShuffle(forYouPlaylist.system_variant); + const refs = detail.tracks + .map((r) => playlistTrackToRef(r)) + .filter((t): t is TrackRefType => t !== null); + if (refs.length > 0) playQueue(refs, 0, { source: forYouPlaylist.system_variant }); + } finally { + heroStarting = false; + } + } + + async function playLatestAlbum(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (heroStarting || !latestAlbum) return; + heroStarting = true; + try { + const detail = await api.get(`/api/albums/${latestAlbum.id}`); + if (detail.tracks.length > 0) playQueue(detail.tracks, 0); + } finally { + heroStarting = false; + } + } + type PlaceholderVariant = 'building' | 'failed' | 'pending' | 'seed-needed'; function placeholderVariant(slot: 'for-you' | 'discover' | 'songs-like'): PlaceholderVariant { const s = systemStatusQ.data; @@ -134,6 +181,40 @@ {pageTitle('Home')}
+ + {#if forYouPlaylist || latestAlbum} +
+ {#if forYouPlaylist} + 0 + ? `${forYouPlaylist.track_count} tracks for you` + : 'Building your mix'} + onPlay={playForYou} + playDisabled={heroStarting || forYouPlaylist.track_count === 0} + /> + {/if} + {#if latestAlbum} + + {/if} +
+ {/if} +