You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/src/routes/canciones/[slug]/+page.svelte

438 lines
11 KiB

<script lang="ts">
import { resolve } from '$app/paths';
import BotonCarrito from '$lib/components/BotonCarrito.svelte';
import BotonFavorito from '$lib/components/BotonFavorito.svelte';
import BotonReproducir from '$lib/components/BotonReproducir.svelte';
import EnlacesPlataformas from '$lib/components/EnlacesPlataformas.svelte';
import FichaDeCancion from '$lib/components/FichaDeCancion.svelte';
import Meta from '$lib/components/Meta.svelte';
import Pestanas, { type Pestana } from '$lib/components/Pestanas.svelte';
import VideoYoutube from '$lib/components/VideoYoutube.svelte';
import { aPista, aPistaDeVersion, aPistas } from '$lib/player/pista';
import { CANCIONES_POR_PACK, formatearPrecio, PRECIO_PACK_CENTIMOS } from '$lib/tienda';
import Migas from '$lib/components/Migas.svelte';
import { listaDeNombres, segundosADuracion } from '$lib/utils/format';
let { data } = $props();
const cancion = $derived(data.cancion);
const Cuerpo = $derived(data.Cuerpo);
/** El contexto es el álbum entero; el índice, la posición del tema en él. */
const pistas = $derived(aPistas(data.hermanas));
const indice = $derived(data.hermanas.findIndex((c) => c.slug === cancion.slug));
/**
* Las alternativas musicales forman su propio contexto, con la versión
* principal a la cabeza: así se pueden comparar arreglos seguidos sin que
* el reproductor salte al tema siguiente del disco.
*/
const pistasDeVersiones = $derived([
aPista(cancion),
...cancion.versiones.map((v) => aPistaDeVersion(cancion, v))
]);
/**
* Las pestañas del cuerpo. La letra va primero y abierta: es el contenido
* principal de la ficha. Las otras dos solo aparecen si el tema las tiene.
*/
const pestanas: Pestana[] = $derived([
{ id: 'letra', etiqueta: cancion.instrumental ? 'Sobre la pieza' : 'Letra' },
...(cancion.inspiracion.length > 0 ? [{ id: 'inspiracion', etiqueta: 'De dónde sale' }] : []),
...(cancion.versiones.length > 0
? [{ id: 'versiones', etiqueta: 'Alternativas musicales' }]
: []),
...(cancion.videos.length > 0
? [{ id: 'videos', etiqueta: cancion.videos.length === 1 ? 'Vídeo' : 'Vídeos' }]
: [])
]);
/**
* La letra va a dos columnas cuando no está en español y hay traducción:
* original a la izquierda, castellano a la derecha. Con una sola de las dos
* cosas no hay nada que enfrentar, y una columna vacía sería peor que
* ninguna.
*/
const enEspanol = $derived(cancion.idioma.split('-')[0] === 'es');
const aDosColumnas = $derived(!enEspanol && Boolean(cancion.traduccion));
/**
* El nombre del idioma en español, sin tabla que mantener. Si el navegador o
* el servidor no saben traducirlo, se queda el código, que sigue diciendo
* algo.
*/
const nombreIdioma = $derived.by(() => {
try {
return (
new Intl.DisplayNames(['es'], { type: 'language' }).of(cancion.idioma) ?? cancion.idioma
);
} catch {
return cancion.idioma;
}
});
const descripcion = $derived(
`${cancion.titulo}. Letra de ${listaDeNombres(cancion.letra)}, música de ${listaDeNombres(cancion.musica)}.`
);
const migas = $derived([
{ texto: 'Música', href: resolve('/musica') },
...(data.estilo
? [
{
texto: data.estilo.nombre,
href: resolve('/musica/[estilo]', { estilo: data.estilo.slug })
}
]
: []),
...(data.album
? [{ texto: data.album.titulo, href: resolve('/albumes/[slug]', { slug: data.album.slug }) }]
: []),
{ texto: cancion.titulo }
]);
</script>
<Meta titulo={cancion.titulo} {descripcion} imagen={data.portada} />
<div class="container">
<Migas {migas} />
<header class="ficha">
<img
class="portada"
src={data.portada}
alt="Portada de {cancion.titulo}"
width="280"
height="280"
/>
<div class="titulo">
<h1>{cancion.titulo}</h1>
<p class="meta muted">
<!--
El album de singles es sintetico y se titula «Singles», asi que
nombrarlo aqui diria «Single del estilo Singles». Lo que situa un
tema suelto es su estilo; a la coleccion se llega por las migas.
-->
{#if data.album?.sintetico && data.estilo}
Single del estilo
<a href={resolve('/musica/[estilo]', { estilo: data.estilo.slug })}>
{data.estilo.nombre}
</a>
{:else if data.album}
Del disco
<a href={resolve('/albumes/[slug]', { slug: data.album.slug })}>{data.album.titulo}</a>
{/if}
{#if cancion.ano}
<span aria-hidden="true">&middot;</span> {cancion.ano}
{/if}
{#if cancion.duracion}
<span aria-hidden="true">&middot;</span>
{segundosADuracion(cancion.duracion)}
{/if}
</p>
<div class="acciones">
<BotonReproducir
{pistas}
indice={Math.max(0, indice)}
texto="Escuchar la muestra"
variante="solido"
tamano={18}
/>
<BotonCarrito {cancion} />
<BotonFavorito cancionSlug={cancion.slug} />
</div>
{#if cancion.alaVenta}
<p class="venta muted">
En la web suena un fragmento. La descarga completa en MP3, con todas sus versiones, va en
packs de {CANCIONES_POR_PACK} canciones por {formatearPrecio(PRECIO_PACK_CENTIMOS)}.
</p>
{/if}
<EnlacesPlataformas enlaces={cancion.enlaces} etiqueta="Escuchar {cancion.titulo} en" />
</div>
</header>
<div class="cuerpo">
<div class="principal">
<Pestanas {pestanas} etiqueta="Contenido del tema">
{#snippet panel(id)}
{#if id === 'letra'}
{#if Cuerpo}
<div class="letra" class:pareada={aDosColumnas}>
<div class="texto" class:versos={!cancion.instrumental} lang={cancion.idioma}>
{#if aDosColumnas}
<p class="rotulo-columna muted">Letra original · {nombreIdioma}</p>
{/if}
<Cuerpo />
</div>
{#if aDosColumnas}
<!--
`pre-line` conserva el corte de los versos sin necesidad de
compilar Markdown: una traducción que pierde los cortes deja
de servir para seguir la original línea a línea.
-->
<div class="texto versos traduccion" lang="es">
<p class="rotulo-columna muted">Traducción al español</p>
<p class="verso-a-verso">{cancion.traduccion}</p>
</div>
{/if}
</div>
{:else}
<p class="muted">
{cancion.instrumental
? 'Todavía no hay notas publicadas de esta pieza.'
: 'La letra de este tema aún no está publicada.'}
</p>
{/if}
{:else if id === 'inspiracion'}
<div class="texto">
{#each cancion.inspiracion as parrafo, i (i)}
<p>{parrafo}</p>
{/each}
</div>
{:else if id === 'videos'}
<div class="videos">
{#each cancion.videos as video (video.youtube)}
<VideoYoutube youtube={video.youtube} titulo={video.titulo} poster={data.portada} />
{/each}
</div>
<p class="muted aviso">
El vídeo se carga solo al pulsarlo: hasta entonces esta página no pide nada a YouTube.
</p>
{:else if id === 'versiones'}
<p class="muted">
El mismo tema en otros arreglos. Quien compra la canción se lleva todas sus versiones.
</p>
<ul class="versiones">
{#each cancion.versiones as version, i (version.id)}
<li class="version">
<BotonReproducir pistas={pistasDeVersiones} indice={i + 1} />
<div class="datos">
<p class="nombre">{version.nombre}</p>
{#if version.nota}
<p class="nota muted">{version.nota}</p>
{/if}
</div>
<span class="duracion muted">
{version.duracion ? segundosADuracion(version.duracion) : '--:--'}
</span>
</li>
{/each}
</ul>
{/if}
{/snippet}
</Pestanas>
</div>
<FichaDeCancion
{cancion}
album={data.album}
estilo={data.estilo}
valoracion={data.valoracion}
miValoracion={data.miValoracion}
/>
</div>
{#if data.anterior || data.siguiente}
<nav class="vecinos" aria-label="Otros temas del disco">
{#if data.anterior}
<a class="vecino" href={resolve('/canciones/[slug]', { slug: data.anterior.slug })}>
<span class="muted">Anterior</span>
<span>{data.anterior.titulo}</span>
</a>
{:else}
<span></span>
{/if}
{#if data.siguiente}
<a
class="vecino derecha"
href={resolve('/canciones/[slug]', { slug: data.siguiente.slug })}
>
<span class="muted">Siguiente</span>
<span>{data.siguiente.titulo}</span>
</a>
{/if}
</nav>
{/if}
</div>
<style>
/*
* A dos columnas se leen en paralelo, que es para lo que sirve tener la
* traducción al lado. Al estrecharse caen una debajo de otra: partir un
* verso para que quepan dos columnas sería peor que apilarlas.
*/
.letra.pareada {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--sp-5) var(--sp-6);
align-items: start;
}
.rotulo-columna {
margin: 0 0 var(--sp-3);
padding-bottom: var(--sp-2);
border-bottom: 1px solid var(--c-border);
font-family: var(--font-body);
font-size: var(--fs-xs);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.verso-a-verso {
margin: 0;
white-space: pre-line;
}
@media (max-width: 52rem) {
.letra.pareada {
grid-template-columns: minmax(0, 1fr);
}
}
.ficha {
display: grid;
grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
gap: var(--sp-6);
align-items: start;
padding-bottom: var(--sp-5);
border-bottom: 1px solid var(--c-border);
}
.portada {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border: 1px solid var(--c-border);
border-radius: var(--radius);
background: var(--c-bg-sunken);
}
.meta {
font-size: var(--fs-sm);
}
.acciones {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--sp-3);
margin-bottom: var(--sp-3);
}
.venta {
max-width: 46ch;
font-size: var(--fs-xs);
}
.cuerpo {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 16rem);
gap: var(--sp-6);
padding-block: var(--sp-6);
}
/*
* La letra se compone en bloque estrecho y con los saltos de línea del
* original: en una canción, dónde termina cada verso es parte del texto.
*/
.texto {
max-width: 46ch;
}
.versos {
font-family: var(--font-versos);
font-size: var(--fs-lg);
}
.versos :global(p) {
margin-bottom: var(--sp-5);
line-height: 1.75;
}
.videos {
display: grid;
gap: var(--sp-4);
max-width: 44rem;
}
.aviso {
margin-top: var(--sp-3);
font-size: var(--fs-xs);
max-width: 44rem;
}
.versiones {
list-style: none;
margin: var(--sp-4) 0 0;
padding: 0;
border-top: 1px solid var(--c-border);
}
.version {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-1);
border-bottom: 1px solid var(--c-border);
}
.version .datos p {
margin: 0;
}
.nombre {
font-size: var(--fs-sm);
font-weight: 600;
}
.nota {
font-size: var(--fs-xs);
}
.duracion {
font-size: var(--fs-sm);
font-variant-numeric: tabular-nums;
}
.vecinos {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sp-4);
padding-block: var(--sp-5);
border-top: 1px solid var(--c-border);
}
.vecino {
display: flex;
flex-direction: column;
font-size: var(--fs-sm);
text-decoration: none;
}
.vecino:hover span:last-child {
text-decoration: underline;
}
.vecino.derecha {
text-align: right;
}
@media (max-width: 48rem) {
.ficha,
.cuerpo {
grid-template-columns: minmax(0, 1fr);
}
.portada {
max-width: 14rem;
}
}
</style>

Powered by TurnKey Linux.