Files
Musify/web-player/src/routes/(app)/u/[id]/+page.svelte
T
Admin a0ba57b569 web-player: P9 — tarjetas y grids (MediaCard)
MediaCard pasa a construir su propio arte (src/trackIds vía Artwork
size="fill", o un snippet art a medida) y su botón de reproducir
(onPlay, con scrim y PlayButton glass en hover) en vez de depender de
que cada wrapper le pase un snippet. Sustituye a AlbumCard,
PlaylistCard y MixTile, que se borran junto con MediaGrid.svelte (sus
grids pasan a los @utility grid-cards/grid-cards-lg/grid-tiles/
grid-wide de P1).

format.ts gana albumMeta() y playlistMeta() (antes cálculos ad-hoc
dentro de cada tarjeta). El tile de género en Explorar pasa de un
style= con el gradiente inline a la receta @utility genre-tile
+ --tile-hue. De paso, corrige la pluralización "seguidor/es" del
perfil de usuario que quedó suelta.

Delta documentado: el hover-zoom del arte y el scrim+PlayButton (antes
exclusivos de MixTile) pasan a ser comportamiento de MediaCard
condicionado a la prop onPlay — se aplicarían a cualquier tarjeta
futura que la use, por consistencia.

Verificado con npm run check/build; sin QA visual autenticada (Docker
no disponible en esta sesión).
2026-09-21 00:30:30 +02:00

102 lines
2.8 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte';
import { enhance } from '$app/forms';
import ListMusic from '@lucide/svelte/icons/list-music';
import Page from '$lib/components/ui/Page.svelte';
import MediaCard from '$lib/components/ui/MediaCard.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Avatar from '$lib/components/ui/Avatar.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
import { plural, playlistMeta } from '$lib/format';
let { data, form } = $props();
const profile = $derived(data.profile);
const playlists = $derived(data.playlists);
const canFollow = $derived(!data.isOwnProfile && !data.isAnonymous);
let following = $state(untrack(() => data.isFollowing));
let submitting = $state(false);
$effect(() => {
if (form && 'following' in form && typeof form.following === 'boolean')
following = form.following;
});
</script>
<svelte:head>
<title>{profile.name}</title>
<meta name="description" content="Perfil de {profile.name} en Musify." />
</svelte:head>
{#snippet followAction()}
<form
method="POST"
action={following ? '?/unfollow' : '?/follow'}
use:enhance={() => {
submitting = true;
return async ({ update }) => {
await update();
submitting = false;
};
}}
>
<Button
type="submit"
size="sm"
variant={following ? 'secondary' : 'primary'}
disabled={submitting}
>
{following ? 'Dejar de seguir' : 'Seguir'}
</Button>
</form>
{/snippet}
<Page>
<CollectionHeader
eyebrow="Perfil"
title={profile.name}
meta="{plural(
Number(profile.followersCount),
'seguidor',
'seguidores'
)} · {profile.followingCount} siguiendo"
align="center"
actions={canFollow ? followAction : undefined}
>
{#snippet cover()}
<Avatar name={profile.name} src={profile.profilePictureUrl} size="hero" />
{/snippet}
</CollectionHeader>
<div class="mt-9">
<SectionHeading title="Playlists públicas" subtitle="Creadas y compartidas por este usuario" />
{#if playlists.length > 0}
<div class="mt-6 grid-cards-lg">
{#each playlists as playlist, i (playlist.id)}
<MediaCard
href="/playlists/{playlist.id}"
title={playlist.name}
subtitle={playlistMeta(Number(playlist.trackCount))}
trackIds={playlist.coverTrackIds}
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
playlist.updatedAt
)}"
index={i}
/>
{/each}
</div>
{:else}
<EmptyState
icon={ListMusic}
title="Sin playlists públicas"
description="Este usuario todavía no ha compartido ninguna playlist pública."
class="mt-6"
/>
{/if}
</div>
</Page>