Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
<script lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Vídeo de YouTube con carga diferida.
|
|
|
|
|
*
|
|
|
|
|
* Hasta que alguien pulsa, aquí no se carga NADA de YouTube: se ve la
|
|
|
|
|
* portada del propio tema con un botón encima. Solo al pulsar se inserta el
|
|
|
|
|
* reproductor incrustado.
|
|
|
|
|
*
|
|
|
|
|
* Es deliberado por tres motivos: un `<iframe>` de YouTube pesa más que el
|
|
|
|
|
* resto de la página junta, deja cookies de terceros antes de que nadie haya
|
|
|
|
|
* pedido ver nada —lo que en la UE exige consentimiento— y la miniatura
|
|
|
|
|
* oficial se sirve desde los servidores de Google, así que usar la portada
|
|
|
|
|
* propia evita también esa petición.
|
|
|
|
|
*
|
|
|
|
|
* Se usa `youtube-nocookie.com`, que es el mismo reproductor sin las cookies
|
|
|
|
|
* de seguimiento salvo que se llegue a reproducir.
|
|
|
|
|
*/
|
|
|
|
|
import Icono from './Icono.svelte';
|
|
|
|
|
import { reproductor } from '$lib/player/player.svelte';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
/** Id de YouTube, once caracteres. */
|
|
|
|
|
youtube: string;
|
|
|
|
|
titulo: string;
|
|
|
|
|
/** Imagen propia que hace de portada mientras no se carga el vídeo. */
|
|
|
|
|
poster: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let { youtube, titulo, poster }: Props = $props();
|
|
|
|
|
|
|
|
|
|
let cargado = $state(false);
|
|
|
|
|
|
|
|
|
|
function abrir() {
|
|
|
|
|
// Dos cosas sonando a la vez, no: manda lo que se acaba de pedir.
|
|
|
|
|
reproductor.pausar();
|
|
|
|
|
cargado = true;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class="marco">
|
|
|
|
|
{#if cargado}
|
|
|
|
|
<iframe
|
|
|
|
|
src="https://www.youtube-nocookie.com/embed/{youtube}?autoplay=1&rel=0"
|
|
|
|
|
title={titulo}
|
|
|
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
|
|
|
referrerpolicy="strict-origin-when-cross-origin"
|
|
|
|
|
allowfullscreen
|
|
|
|
|
></iframe>
|
|
|
|
|
{:else}
|
|
|
|
|
<button type="button" class="portada" onclick={abrir}>
|
|
|
|
|
<img src={poster} alt="" />
|
|
|
|
|
<span class="velo"></span>
|
|
|
|
|
<span class="play"><Icono nombre="play" tamano={28} /></span>
|
|
|
|
|
<span class="etiqueta">
|
|
|
|
|
<span class="titulo">{titulo}</span>
|
|
|
|
|
<span class="aviso">Ver en YouTube</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.marco {
|
|
|
|
|
position: relative;
|
|
|
|
|
aspect-ratio: 16 / 9;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
border: 1px solid var(--c-border);
|
Cantos menos redondos, tarjetas coherentes y portada a ras
Los botones dejan de ser cápsulas y las tarjetas pierden la mitad del canto. De
paso se ordena el sistema: había tres radios sueltos y una píldora de 999px
repetida en quince archivos, y quedan dos —uno para lo que se pulsa, otro para
las superficies—. Los botones de icono suelto siguen redondos, que ahí sí tiene
sentido.
Las fichas del taller no usaban la tarjeta compartida: tenían su propio borde,
su propio canto y su propio hover, así que se veían de otra familia que las de
música. Ahora usan `.tarjeta`, con el mismo reflejo bajo el puntero y el mismo
subrayado.
Y la portada arranca en el borde de la ventana, con la cabecera flotando encima.
Para eso la cabecera pasa a medir su propio alto y publicarlo: estaba escrito a
mano en 4,75rem cuando mide 68px, y todo lo que dependía de ese número —el salto
a un ancla, las barras laterales pegajosas— quedaba desplazado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
border-radius: var(--radius-tarjeta);
|
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5.
El catálogo es contenido, no base de datos: estilos, álbumes, canciones y
capítulos son Markdown en `src/content/`, validados al compilar y compilados
dentro del bundle, así que recorrer la música no hace ni una consulta. La base
de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos,
compras, valoraciones y favoritos.
Lo que hay:
- Música agrupada por estilos, con página propia para cada estilo, álbum y
tema, buscador y tabla ordenable.
- Reproductor persistente que sobrevive a la navegación. Suena un fragmento;
el archivo completo solo se obtiene comprando.
- Taller de letras: los principios del oficio y una ficha de creación por
estilo hispanoamericano.
- Blog con feed RSS, y etiquetas que cruzan blog y catálogo.
- Tienda con cuentas por código de correo, carrito, Stripe y descargas
protegidas.
- Panel de contenido en `/admin` que escribe los mismos Markdown.
- Tema oscuro de marca y su inversión clara, aplicado antes de pintar.
Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento
del IVA, las páginas de condiciones y privacidad, y el material real. Las
canciones de ejemplo son inventadas y están para sustituirse; el audio son
marcadores salvo un tema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 month ago
|
|
|
background: var(--c-bg-sunken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.marco iframe {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border: 0;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 0;
|
|
|
|
|
border: none;
|
|
|
|
|
background: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada img {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
transition: transform 0.4s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada:hover img,
|
|
|
|
|
.portada:focus-visible img {
|
|
|
|
|
transform: scale(1.03);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* El velo mantiene legible el texto sobre cualquier portada. */
|
|
|
|
|
.velo {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
background: linear-gradient(to top, rgb(0 0 0 / 0.6), rgb(0 0 0 / 0.05) 55%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.play {
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada:hover .play,
|
|
|
|
|
.portada:focus-visible .play {
|
|
|
|
|
transform: scale(1.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.etiqueta {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: var(--sp-4);
|
|
|
|
|
right: var(--sp-4);
|
|
|
|
|
bottom: var(--sp-3);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.titulo {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.aviso {
|
|
|
|
|
font-size: var(--fs-xs);
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.portada img,
|
|
|
|
|
.play {
|
|
|
|
|
transition: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.portada:hover img,
|
|
|
|
|
.portada:focus-visible img,
|
|
|
|
|
.portada:hover .play,
|
|
|
|
|
.portada:focus-visible .play {
|
|
|
|
|
transform: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|