Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
2 changed files with 204 additions and 264 deletions
Showing only changes of commit 1050372634 - Show all commits
@@ -4,39 +4,26 @@ import { createApiClient } from '$lib/server/api';
const PAGE_SIZE = 24; const PAGE_SIZE = 24;
type Source = 'all' | 'local' | 'yt';
function parseSource(value: string | null): Source {
return value === 'local' || value === 'yt' ? value : 'all';
}
export const load: PageServerLoad = async ({ url, locals, fetch }) => { export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const query = url.searchParams.get('q')?.trim() ?? ''; const query = url.searchParams.get('q')?.trim() ?? '';
const src = parseSource(url.searchParams.get('src'));
const page = Math.max(1, Number(url.searchParams.get('page')) || 1); const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const wantsLocal = src !== 'yt'; const tracksPromise = api.GET('/tracks', {
const wantsYouTube = src !== 'local'; params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
});
const tracksPromise = wantsLocal
? api.GET('/tracks', {
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
})
: Promise.resolve(null);
const searchPromise = const searchPromise =
wantsYouTube && query && locals.accessToken query && locals.accessToken
? api.GET('/youtube/search', { params: { query: { query } } }) ? api.GET('/youtube/search', { params: { query: { query } } })
: Promise.resolve(null); : Promise.resolve(null);
const playlistsPromise = const playlistsPromise = locals.user
wantsYouTube && locals.user ? api.GET('/playlists/users/{userId}', {
? api.GET('/playlists/users/{userId}', { params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } }
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } } })
}) : Promise.resolve(null);
: Promise.resolve(null);
const [tracksRes, searchRes, playlistsRes] = await Promise.all([ const [tracksRes, searchRes, playlistsRes] = await Promise.all([
tracksPromise, tracksPromise,
@@ -44,18 +31,17 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
playlistsPromise playlistsPromise
]); ]);
if (wantsLocal && (tracksRes?.error || !tracksRes?.data)) { if (tracksRes.error || !tracksRes.data) {
error(502, 'No se pudieron cargar las canciones.'); error(502, 'No se pudieron cargar las canciones.');
} }
return { return {
src,
query, query,
tracks: tracksRes?.data ?? null, tracks: tracksRes.data,
ytResults: searchRes?.data ?? null, ytResults: searchRes?.data ?? null,
ytError: Boolean(searchRes?.error), ytError: Boolean(searchRes?.error),
playlists: playlistsRes?.data?.items ?? [], playlists: playlistsRes?.data?.items ?? [],
needsAuth: wantsYouTube && !locals.user needsAuth: !locals.user
}; };
}; };
+192 -238
View File
@@ -20,8 +20,6 @@
isExplicit: boolean; isExplicit: boolean;
} }
type Source = 'all' | 'local' | 'yt';
let { data, form } = $props(); let { data, form } = $props();
let searchTimeout: ReturnType<typeof setTimeout> | undefined; let searchTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -37,14 +35,14 @@
addedVideoIds = new Set(); addedVideoIds = new Set();
}); });
const showLocal = $derived(data.tracks !== null); const showHeadings = $derived(!!data.query);
const showYouTube = $derived(data.src !== 'local' && !!data.query); const tracks = $derived(data.tracks);
const showHeadings = $derived(data.src === 'all' && !!data.query); const currentPage = $derived(Number(data.tracks.pageNumber));
const total = $derived(Number(data.tracks.totalItemCount));
function buildHref(src: Source, page = 1, query = data.query) { function buildHref(page = 1, query = data.query) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (query) params.set('q', query); if (query) params.set('q', query);
if (src !== 'all') params.set('src', src);
if (page > 1) params.set('page', String(page)); if (page > 1) params.set('page', String(page));
const qs = params.toString(); const qs = params.toString();
return qs ? `/explore?${qs}` : '/explore'; return qs ? `/explore?${qs}` : '/explore';
@@ -54,7 +52,7 @@
const value = (event.currentTarget as HTMLInputElement).value; const value = (event.currentTarget as HTMLInputElement).value;
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
goto(buildHref(data.src, 1, value.trim()), { goto(buildHref(1, value.trim()), {
keepFocus: true, keepFocus: true,
replaceState: true, replaceState: true,
noScroll: true noScroll: true
@@ -69,7 +67,6 @@
} }
function togglePlayLocal(index: number) { function togglePlayLocal(index: number) {
if (!data.tracks) return;
const track = data.tracks.items[index]; const track = data.tracks.items[index];
if (player.current?.id === queueIdForTrack(track)) player.toggle(); if (player.current?.id === queueIdForTrack(track)) player.toggle();
else player.playQueue(toQueueItems(data.tracks.items), index); else player.playQueue(toQueueItems(data.tracks.items), index);
@@ -109,18 +106,6 @@
ytLoadingMore = false; ytLoadingMore = false;
} }
} }
const tabs: { src: Source; label: string }[] = [
{ src: 'all', label: 'Todo' },
{ src: 'local', label: 'Mi música' },
{ src: 'yt', label: 'YouTube Music' }
];
const placeholders: Record<Source, string> = {
all: 'Buscar en tu música y en YouTube Music...',
local: 'Buscar por nombre...',
yt: 'Buscar en YouTube Music...'
};
</script> </script>
<svelte:head> <svelte:head>
@@ -130,23 +115,9 @@
<section class="px-8 pt-12 pb-8"> <section class="px-8 pt-12 pb-8">
<h1 class="font-display text-3xl font-bold tracking-tight sm:text-4xl">Explorar</h1> <h1 class="font-display text-3xl font-bold tracking-tight sm:text-4xl">Explorar</h1>
<p class="mt-2 text-neutral-400">Busca a la vez en tu música y en YouTube Music, o filtra por fuente.</p> <p class="mt-2 text-neutral-400">Busca a la vez en tu música y en YouTube Music.</p>
<div class="mt-8 flex items-center gap-2"> <div class="relative mt-8">
{#each tabs as tab (tab.src)}
<a
href={buildHref(tab.src)}
data-sveltekit-noscroll
class="rounded-full px-4 py-1.5 text-sm transition {data.src === tab.src
? 'bg-emerald-500 font-semibold text-neutral-950'
: 'border border-white/15 text-neutral-300 hover:bg-white/5'}"
>
{tab.label}
</a>
{/each}
</div>
<div class="relative mt-6">
<span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-neutral-500"> <span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-neutral-500">
<Search class="h-4 w-4" /> <Search class="h-4 w-4" />
</span> </span>
@@ -154,7 +125,7 @@
type="search" type="search"
value={data.query} value={data.query}
oninput={onSearchInput} oninput={onSearchInput}
placeholder={placeholders[data.src]} placeholder="Buscar en tu música y en YouTube Music..."
autocomplete="off" autocomplete="off"
class="w-full rounded-lg border border-white/15 bg-neutral-950 py-3 pl-11 pr-4 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-[var(--mf-accent)]/50 focus:outline-none" class="w-full rounded-lg border border-white/15 bg-neutral-950 py-3 pl-11 pr-4 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-[var(--mf-accent)]/50 focus:outline-none"
/> />
@@ -166,54 +137,131 @@
</p> </p>
{/if} {/if}
{#if showLocal && data.tracks} {#if showHeadings}
{@const tracks = data.tracks} <h2 class="mt-10 font-display text-lg font-bold tracking-tight">Mi música</h2>
{@const currentPage = Number(tracks.pageNumber)} {/if}
{@const total = Number(tracks.totalItemCount)}
<p class="{showHeadings ? 'mt-1' : 'mt-6'} text-sm text-neutral-500">
{#if data.query}
{total}
{total === 1 ? 'resultado' : 'resultados'} para «{data.query}»
{:else}
{total}
{total === 1 ? 'canción' : 'canciones'}
{/if}
</p>
{#if tracks.items.length === 0}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300">
{#if data.query}
No hay canciones tuyas que coincidan con «{data.query}».
{:else}
Todavía no hay canciones. ¡Sé el primero en subir una!
{/if}
</p>
</div>
{:else}
<ul class="mt-6 grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4">
{#each tracks.items as track, i (track.id)}
<li class="group animate-enter" style="animation-delay:{i * 40}ms">
<button type="button" onclick={() => togglePlayLocal(i)} class="block w-full text-left">
<Cover
trackId={track.id}
hue={hueFor(track.id)}
size="large"
alt={track.title}
class="aspect-square w-full rounded-xl shadow-[0_12px_28px_-10px_rgba(0,0,0,0.6)]"
>
<span
class="absolute right-2.5 bottom-2.5 grid h-11 w-11 translate-y-2 place-items-center rounded-full bg-[var(--mf-accent)] text-neutral-950 opacity-0 shadow-lg transition-all group-hover:translate-y-0 group-hover:opacity-100"
class:!opacity-100={player.current?.id === queueIdForTrack(track)}
class:!translate-y-0={player.current?.id === queueIdForTrack(track)}
>
{#if player.current?.id === queueIdForTrack(track) && player.isPlaying}
<svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path d="M8 5v14l11-7z" />
</svg>
{/if}
</span>
</Cover>
<div class="mt-2.5 truncate text-sm font-semibold text-[var(--mf-text)]" title={track.title}>
{track.title}
</div>
{#if track.artist}
<div class="mt-0.5 truncate text-xs text-neutral-500">{track.artist}</div>
{/if}
</button>
</li>
{/each}
</ul>
{#if tracks.hasPreviousPage || tracks.hasNextPage}
<nav class="mt-8 flex items-center justify-center gap-4">
{#if tracks.hasPreviousPage}
<a
href={buildHref(currentPage - 1)}
class="rounded-lg border border-white/10 px-5 py-2 text-sm text-white/65 transition hover:bg-white/2.5"
>
Anterior
</a>
{/if}
<span class="text-sm text-neutral-500">Página {currentPage}</span>
{#if tracks.hasNextPage}
<a
href={buildHref(currentPage + 1)}
class="rounded-lg border border-white/10 px-5 py-2 text-sm text-white/65 transition hover:bg-white/2.5"
>
Siguiente
</a>
{/if}
</nav>
{/if}
{/if}
{#if data.query}
{#if showHeadings} {#if showHeadings}
<h2 class="mt-10 font-display text-lg font-bold tracking-tight">Mi música</h2> <h2 class="mt-12 font-display text-lg font-bold tracking-tight">YouTube Music</h2>
{/if} {/if}
<p class="{showHeadings ? 'mt-1' : 'mt-6'} text-sm text-neutral-500"> {#if data.needsAuth}
{#if data.query}
{total}
{total === 1 ? 'resultado' : 'resultados'} para «{data.query}»
{:else}
{total}
{total === 1 ? 'canción' : 'canciones'}
{/if}
</p>
{#if tracks.items.length === 0}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md"> <div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" /> <Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300"> <p class="mt-3 text-sm text-neutral-300">Inicia sesión para buscar en YouTube Music.</p>
{#if data.query} </div>
No hay canciones tuyas que coincidan con «{data.query}». {:else if data.ytError}
{:else} <div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
Todavía no hay canciones. ¡Sé el primero en subir una! <Music class="mx-auto h-8 w-8 text-neutral-500" />
{/if} <p class="mt-3 text-sm text-neutral-300">YouTube Music no está disponible ahora mismo. Inténtalo de nuevo.</p>
</p> </div>
{:else if ytItems.length === 0}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300">No hay resultados en YouTube Music para «{data.query}».</p>
</div> </div>
{:else} {:else}
<ul class="mt-6 grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4"> <ul class="mt-6 grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4">
{#each tracks.items as track, i (track.id)} {#each ytItems as song, i (song.videoId)}
<li class="group animate-enter" style="animation-delay:{i * 40}ms"> <li class="group animate-enter relative" style="animation-delay:{i * 40}ms">
<button type="button" onclick={() => togglePlayLocal(i)} class="block w-full text-left"> <button type="button" onclick={() => togglePlayYouTube(i)} class="block w-full text-left">
<Cover <Cover
trackId={track.id} trackId={song.videoId}
hue={hueFor(track.id)} src={song.thumbnailUrl}
size="large" hue={hueFor(song.videoId)}
alt={track.title} alt={song.title}
class="aspect-square w-full rounded-xl shadow-[0_12px_28px_-10px_rgba(0,0,0,0.6)]" class="aspect-square w-full rounded-xl shadow-[0_12px_28px_-10px_rgba(0,0,0,0.6)]"
> >
<span <span
class="absolute right-2.5 bottom-2.5 grid h-11 w-11 translate-y-2 place-items-center rounded-full bg-[var(--mf-accent)] text-neutral-950 opacity-0 shadow-lg transition-all group-hover:translate-y-0 group-hover:opacity-100" class="absolute right-2.5 bottom-2.5 grid h-11 w-11 translate-y-2 place-items-center rounded-full bg-[var(--mf-accent)] text-neutral-950 opacity-0 shadow-lg transition-all group-hover:translate-y-0 group-hover:opacity-100"
class:!opacity-100={player.current?.id === queueIdForTrack(track)} class:!opacity-100={player.current?.id === song.videoId}
class:!translate-y-0={player.current?.id === queueIdForTrack(track)} class:!translate-y-0={player.current?.id === song.videoId}
> >
{#if player.current?.id === queueIdForTrack(track) && player.isPlaying} {#if player.current?.id === song.videoId && player.isPlaying}
<svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"> <svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" /> <path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg> </svg>
@@ -224,181 +272,87 @@
{/if} {/if}
</span> </span>
</Cover> </Cover>
<div class="mt-2.5 truncate text-sm font-semibold text-[var(--mf-text)]" title={track.title}> <div class="mt-2.5 truncate text-sm font-semibold text-[var(--mf-text)]" title={song.title}>
{track.title} {song.title}
</div>
<div class="mt-0.5 flex min-w-0 items-center gap-1.5">
{#if song.isExplicit}
<span
class="grid h-3.5 w-3.5 shrink-0 place-items-center rounded-[3px] bg-white/20 text-[9px] font-bold leading-none text-neutral-200"
title="Contenido explícito"
>
E
</span>
{/if}
<span class="truncate text-xs text-neutral-500" title={song.artist}>{song.artist}</span>
</div> </div>
{#if track.artist}
<div class="mt-0.5 truncate text-xs text-neutral-500">{track.artist}</div>
{/if}
</button> </button>
{#if data.playlists.length > 0}
<details class="absolute right-2.5 top-2.5 z-10">
<summary
class="grid h-9 w-9 cursor-pointer list-none place-items-center rounded-full bg-neutral-950/70 text-white opacity-0 shadow-lg backdrop-blur-sm transition group-hover:opacity-100 [&::-webkit-details-marker]:hidden"
class:!opacity-100={addedVideoIds.has(song.videoId)}
title="Añadir a playlist"
>
{#if addedVideoIds.has(song.videoId)}
<Check class="h-4 w-4 text-emerald-400" />
{:else}
<ListPlus class="h-4 w-4" />
{/if}
</summary>
<div
class="absolute right-0 z-20 mt-2 w-56 rounded-2xl border border-white/10 bg-neutral-900 p-2 shadow-[0_12px_28px_-10px_rgba(0,0,0,0.8)]"
>
<p class="px-3 py-1.5 text-xs font-semibold text-neutral-500">Añadir a playlist</p>
{#each data.playlists as playlist (playlist.id)}
<form
method="POST"
action="?/addYouTubeToPlaylist"
use:enhance={() =>
({ result, update }) => {
if (result.type === 'success') {
addedVideoIds = new Set([...addedVideoIds, song.videoId]);
}
return update({ reset: false });
}}
>
<input type="hidden" name="playlistId" value={playlist.id} />
<input type="hidden" name="videoId" value={song.videoId} />
<input type="hidden" name="title" value={song.title} />
<input type="hidden" name="artist" value={song.artist} />
<input type="hidden" name="durationSeconds" value={song.durationSeconds} />
<input type="hidden" name="thumbnailUrl" value={song.thumbnailUrl} />
<button
type="submit"
class="w-full truncate rounded-lg px-3 py-2 text-left text-sm text-neutral-200 transition hover:bg-white/5"
>
{playlist.name}
</button>
</form>
{/each}
</div>
</details>
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
{#if tracks.hasPreviousPage || tracks.hasNextPage} {#if ytContinuation}
<nav class="mt-8 flex items-center justify-center gap-4"> <div class="mt-8 flex justify-center">
{#if tracks.hasPreviousPage} <button
<a type="button"
href={buildHref(data.src, currentPage - 1)} onclick={loadMoreYouTube}
class="rounded-lg border border-white/10 px-5 py-2 text-sm text-white/65 transition hover:bg-white/2.5" disabled={ytLoadingMore}
> class="flex items-center gap-2 rounded-full border border-white/15 px-6 py-3 text-sm font-semibold text-white transition hover:bg-white/5 disabled:opacity-50"
Anterior >
</a> {#if ytLoadingMore}
{/if} <LoaderCircle class="h-4 w-4 animate-spin" />
<span class="text-sm text-neutral-500">Página {currentPage}</span> {/if}
{#if tracks.hasNextPage} Cargar más
<a </button>
href={buildHref(data.src, currentPage + 1)}
class="rounded-lg border border-white/10 px-5 py-2 text-sm text-white/65 transition hover:bg-white/2.5"
>
Siguiente
</a>
{/if}
</nav>
{/if}
{/if}
{/if}
{#if data.src !== 'local'}
{#if showHeadings}
<h2 class="mt-12 font-display text-lg font-bold tracking-tight">YouTube Music</h2>
{/if}
{#if data.needsAuth}
{#if data.query || data.src === 'yt'}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300">Inicia sesión para buscar en YouTube Music.</p>
</div> </div>
{/if} {/if}
{:else if !data.query}
{#if data.src === 'yt'}
<div class="mt-16 rounded-2xl border border-white/10 bg-neutral-900/40 p-12 text-center backdrop-blur-md">
<Search class="mx-auto h-10 w-10 text-neutral-500" />
<p class="mt-4 text-neutral-300">Escribe algo para buscar canciones en YouTube Music.</p>
</div>
{/if}
{:else if data.ytError}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300">YouTube Music no está disponible ahora mismo. Inténtalo de nuevo.</p>
</div>
{:else if showYouTube}
{#if ytItems.length === 0}
<div class="mt-6 rounded-2xl border border-white/10 bg-neutral-900/40 p-10 text-center backdrop-blur-md">
<Music class="mx-auto h-8 w-8 text-neutral-500" />
<p class="mt-3 text-sm text-neutral-300">No hay resultados en YouTube Music para «{data.query}».</p>
</div>
{:else}
<ul class="mt-6 grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4">
{#each ytItems as song, i (song.videoId)}
<li class="group animate-enter relative" style="animation-delay:{i * 40}ms">
<button type="button" onclick={() => togglePlayYouTube(i)} class="block w-full text-left">
<Cover
trackId={song.videoId}
src={song.thumbnailUrl}
hue={hueFor(song.videoId)}
alt={song.title}
class="aspect-square w-full rounded-xl shadow-[0_12px_28px_-10px_rgba(0,0,0,0.6)]"
>
<span
class="absolute right-2.5 bottom-2.5 grid h-11 w-11 translate-y-2 place-items-center rounded-full bg-[var(--mf-accent)] text-neutral-950 opacity-0 shadow-lg transition-all group-hover:translate-y-0 group-hover:opacity-100"
class:!opacity-100={player.current?.id === song.videoId}
class:!translate-y-0={player.current?.id === song.videoId}
>
{#if player.current?.id === song.videoId && player.isPlaying}
<svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5">
<path d="M8 5v14l11-7z" />
</svg>
{/if}
</span>
</Cover>
<div class="mt-2.5 truncate text-sm font-semibold text-[var(--mf-text)]" title={song.title}>
{song.title}
</div>
<div class="mt-0.5 flex min-w-0 items-center gap-1.5">
{#if song.isExplicit}
<span
class="grid h-3.5 w-3.5 shrink-0 place-items-center rounded-[3px] bg-white/20 text-[9px] font-bold leading-none text-neutral-200"
title="Contenido explícito"
>
E
</span>
{/if}
<span class="truncate text-xs text-neutral-500" title={song.artist}>{song.artist}</span>
</div>
</button>
{#if data.playlists.length > 0}
<details class="absolute right-2.5 top-2.5 z-10">
<summary
class="grid h-9 w-9 cursor-pointer list-none place-items-center rounded-full bg-neutral-950/70 text-white opacity-0 shadow-lg backdrop-blur-sm transition group-hover:opacity-100 [&::-webkit-details-marker]:hidden"
class:!opacity-100={addedVideoIds.has(song.videoId)}
title="Añadir a playlist"
>
{#if addedVideoIds.has(song.videoId)}
<Check class="h-4 w-4 text-emerald-400" />
{:else}
<ListPlus class="h-4 w-4" />
{/if}
</summary>
<div
class="absolute right-0 z-20 mt-2 w-56 rounded-2xl border border-white/10 bg-neutral-900 p-2 shadow-[0_12px_28px_-10px_rgba(0,0,0,0.8)]"
>
<p class="px-3 py-1.5 text-xs font-semibold text-neutral-500">Añadir a playlist</p>
{#each data.playlists as playlist (playlist.id)}
<form
method="POST"
action="?/addYouTubeToPlaylist"
use:enhance={() =>
({ result, update }) => {
if (result.type === 'success') {
addedVideoIds = new Set([...addedVideoIds, song.videoId]);
}
return update({ reset: false });
}}
>
<input type="hidden" name="playlistId" value={playlist.id} />
<input type="hidden" name="videoId" value={song.videoId} />
<input type="hidden" name="title" value={song.title} />
<input type="hidden" name="artist" value={song.artist} />
<input type="hidden" name="durationSeconds" value={song.durationSeconds} />
<input type="hidden" name="thumbnailUrl" value={song.thumbnailUrl} />
<button
type="submit"
class="w-full truncate rounded-lg px-3 py-2 text-left text-sm text-neutral-200 transition hover:bg-white/5"
>
{playlist.name}
</button>
</form>
{/each}
</div>
</details>
{/if}
</li>
{/each}
</ul>
{#if ytContinuation}
<div class="mt-8 flex justify-center">
<button
type="button"
onclick={loadMoreYouTube}
disabled={ytLoadingMore}
class="flex items-center gap-2 rounded-full border border-white/15 px-6 py-3 text-sm font-semibold text-white transition hover:bg-white/5 disabled:opacity-50"
>
{#if ytLoadingMore}
<LoaderCircle class="h-4 w-4 animate-spin" />
{/if}
Cargar más
</button>
</div>
{/if}
{/if}
{/if} {/if}
{/if} {/if}
</section> </section>