Home carousel cards route a tap to the item's detail / Episode Focus View and a ~500ms long-press to a confirm-then-play flow. MediaCard gains an onLongPress prop with pointer-based detection (cancelled on >10px move so carousel scroll is unaffected, trailing click suppressed). Episode taps route to /library/<seriesId>?episode=<id>; the bare-episode detail page links back to its parent series/season. TRACES: UR-058 | DR-087
105 lines
3.0 KiB
Svelte
105 lines
3.0 KiB
Svelte
<!-- TRACES: UR-034 | DR-039 -->
|
|
<script lang="ts">
|
|
import type { MediaItem } from "$lib/api/types";
|
|
import MediaCard from "$lib/components/library/MediaCard.svelte";
|
|
|
|
interface Props {
|
|
title: string;
|
|
items: MediaItem[];
|
|
onItemClick?: (item: MediaItem) => void;
|
|
onItemLongPress?: (item: MediaItem) => void;
|
|
showAll?: () => void;
|
|
}
|
|
|
|
let { title, items, onItemClick, onItemLongPress, showAll }: Props = $props();
|
|
|
|
let scrollContainer: HTMLDivElement | null = $state(null);
|
|
let showLeftArrow = $state(false);
|
|
let showRightArrow = $state(true);
|
|
|
|
function handleScroll() {
|
|
if (!scrollContainer) return;
|
|
showLeftArrow = scrollContainer.scrollLeft > 0;
|
|
showRightArrow =
|
|
scrollContainer.scrollLeft <
|
|
scrollContainer.scrollWidth - scrollContainer.clientWidth - 10;
|
|
}
|
|
|
|
function scrollLeft() {
|
|
scrollContainer?.scrollBy({ left: -600, behavior: "smooth" });
|
|
}
|
|
|
|
function scrollRight() {
|
|
scrollContainer?.scrollBy({ left: 600, behavior: "smooth" });
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-3">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-4">
|
|
<h2 class="text-2xl font-semibold text-white">{title}</h2>
|
|
{#if showAll}
|
|
<button
|
|
onclick={showAll}
|
|
class="text-sm text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
See all
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Scrollable row -->
|
|
<div class="relative group">
|
|
<div
|
|
bind:this={scrollContainer}
|
|
onscroll={handleScroll}
|
|
class="flex gap-4 overflow-x-auto scrollbar-hide scroll-smooth px-4 pb-4"
|
|
>
|
|
{#each items as item (item.id)}
|
|
<MediaCard
|
|
{item}
|
|
size="medium"
|
|
showProgress={true}
|
|
onclick={() => onItemClick?.(item)}
|
|
onLongPress={onItemLongPress ? () => onItemLongPress(item) : undefined}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- Navigation arrows (Spotify-style - only show on hover) -->
|
|
{#if showLeftArrow}
|
|
<button
|
|
onclick={scrollLeft}
|
|
class="absolute left-0 top-1/2 -translate-y-1/2 p-2 bg-black/80 hover:bg-black rounded-full opacity-0 group-hover:opacity-100 transition-opacity z-10 ml-2"
|
|
aria-label="Scroll left"
|
|
>
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
|
|
{#if showRightArrow}
|
|
<button
|
|
onclick={scrollRight}
|
|
class="absolute right-0 top-1/2 -translate-y-1/2 p-2 bg-black/80 hover:bg-black rounded-full opacity-0 group-hover:opacity-100 transition-opacity z-10 mr-2"
|
|
aria-label="Scroll right"
|
|
>
|
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.scrollbar-hide {
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
</style>
|