feat(web): hero row at top of Home (For You + most recently added)
test-web / test (push) Failing after 36s
test-web / test (push) Failing after 36s
#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.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { Play } from 'lucide-svelte';
|
||||
import { FALLBACK_COVER } from '$lib/media/covers';
|
||||
|
||||
// Shared shape for the two hero tiles at the top of Home (the
|
||||
// "Today's pick" + "Just added" anchors). Wider than the regular
|
||||
// grid tiles, cover on the left + meta + play button on the right,
|
||||
// accent-tinted background so the eye lands here first.
|
||||
|
||||
let {
|
||||
href,
|
||||
coverUrl,
|
||||
eyebrow,
|
||||
title,
|
||||
subtitle,
|
||||
onPlay,
|
||||
playDisabled = false
|
||||
}: {
|
||||
href: string;
|
||||
coverUrl: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onPlay: (e: MouseEvent) => void;
|
||||
playDisabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
function onImgError(e: Event) {
|
||||
(e.currentTarget as HTMLImageElement).src = FALLBACK_COVER;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
class="group flex items-stretch gap-4 overflow-hidden rounded-lg border border-border
|
||||
bg-gradient-to-br from-accent/15 via-surface to-surface
|
||||
shadow-sm transition-all duration-200
|
||||
hover:from-accent/25 hover:shadow-lg hover:ring-1 hover:ring-accent/40
|
||||
focus-visible:ring-2 focus-visible:ring-accent"
|
||||
>
|
||||
<div class="relative h-32 w-32 flex-shrink-0 overflow-hidden md:h-40 md:w-40">
|
||||
<img
|
||||
src={coverUrl}
|
||||
alt=""
|
||||
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
onerror={onImgError}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col justify-between gap-2 py-3 pr-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-accent">{eyebrow}</div>
|
||||
<div class="font-display text-xl font-medium leading-tight text-text-primary
|
||||
line-clamp-2 mt-1">
|
||||
{title}
|
||||
</div>
|
||||
{#if subtitle}
|
||||
<div class="mt-1 truncate text-sm text-text-secondary">{subtitle}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Play ${title}`}
|
||||
onclick={onPlay}
|
||||
disabled={playDisabled}
|
||||
class="self-start inline-flex items-center gap-1.5 rounded-full
|
||||
bg-accent px-4 py-1.5 text-sm font-medium text-text-primary
|
||||
hover:opacity-90 focus-visible:ring-2 focus-visible:ring-accent
|
||||
disabled:opacity-50"
|
||||
>
|
||||
<Play size={14} strokeWidth={1.5} fill="currentColor" />
|
||||
Play
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
Reference in New Issue
Block a user