Lo que se escribe de un género: resumen, lugar y año de origen, tempo, rima, las notas de métrica y compás, los rasgos de escritura, el estado, en qué países se canta y toda la prosa repartida en secciones. Todo en una pantalla y un solo guardado, porque es una sola cosa. Repartido en tres formularios habría que guardar tres veces para arreglar un párrafo, y en una transacción, porque si al reemplazar los países fallara algo no puede quedar el texto guardado y el mapa a medias. Los países son lo que pinta el género en el mapa, así que la pantalla lo dice y la validación lo defiende: una sola cuna —un género nace en un sitio; si nació en dos orillas, la cuna es la región y el otro país va como adoptado— y ningún género publicado sin país, que se quedaría fuera del mapa y sin manera de llegar a él. Las secciones se editan, se reordenan y se borran marcando una casilla, que se puede desmarcar antes de guardar. Y se puede añadir una nueva con su clave. De las fichas importadas por un modelo sale una casilla para darlas por revisadas. Es la diferencia entre lo que propuso una máquina y lo que ha leído una persona, y el modelo de datos la guarda a propósito: esto se publica con el nombre del titular. Comprobado de punta a punta: editar en el panel, guardar, y el cambio sale en la web pública. Dos fallos que se vieron mirando las capturas y no las pruebas: un `td` en flex se descolgaba de su fila y descuadraba las tres columnas de la lista, y el «cuna» de cada país acababa a diez centímetros de su país, alineado con el del de al lado, de modo que no se sabía de quién era cuál. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
4e53ba64f5
commit
3c09592e11
@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Los géneros, para el panel.
|
||||
*
|
||||
* Una ficha de género no es una fila: es la fila más los países donde vive y la
|
||||
* prosa repartida en secciones. Se edita todo en la misma pantalla porque es
|
||||
* una sola cosa —la ficha—, y repartirla en tres formularios obligaría a
|
||||
* guardar tres veces para arreglar un párrafo.
|
||||
*
|
||||
* Lo que todavía no se edita aquí —alias, temática, instrumentación, linaje,
|
||||
* personas, obras y versos— se carga por script. Se irá trayendo; lo de aquí es
|
||||
* lo que tiene texto que escribir.
|
||||
*/
|
||||
|
||||
import { and, asc, eq, inArray, sql } from 'drizzle-orm';
|
||||
import { db } from './db';
|
||||
import { capitulo, genero, generoPais, generoSeccion, pais } from './db/schema';
|
||||
import { ESTADOS } from './db/schema/comun';
|
||||
|
||||
export type Estado = (typeof ESTADOS)[number];
|
||||
export type Relacion = 'cuna' | 'adoptado';
|
||||
|
||||
export interface GeneroEnLista {
|
||||
slug: string;
|
||||
nombre: string;
|
||||
estado: Estado;
|
||||
origenLugar: string | null;
|
||||
origenAno: number | null;
|
||||
/** Cuántas secciones de prosa tiene escritas. */
|
||||
secciones: number;
|
||||
/** Si existe el capítulo que lo publica. Sin él, la ficha no sale en la web. */
|
||||
tieneFicha: boolean;
|
||||
/** Importado por un modelo y sin repasar por una persona. */
|
||||
sinRevisar: boolean;
|
||||
}
|
||||
|
||||
export async function listaDeGeneros(): Promise<GeneroEnLista[]> {
|
||||
const filas = await db
|
||||
.select({
|
||||
slug: genero.slug,
|
||||
nombre: genero.nombre,
|
||||
estado: genero.estado,
|
||||
origenLugar: genero.origenLugar,
|
||||
origenAno: genero.origenAno,
|
||||
origen: genero.origen,
|
||||
revisadoEn: genero.revisadoEn,
|
||||
capituloId: capitulo.id,
|
||||
secciones: sql<number>`(
|
||||
select count(*)::int from genero_seccion s where s.genero_id = ${genero.id}
|
||||
)`
|
||||
})
|
||||
.from(genero)
|
||||
.leftJoin(capitulo, eq(capitulo.generoId, genero.id))
|
||||
.orderBy(asc(genero.nombre));
|
||||
|
||||
return filas.map(({ origen, revisadoEn, capituloId, ...resto }) => ({
|
||||
...resto,
|
||||
tieneFicha: capituloId !== null,
|
||||
sinRevisar: origen === 'importado' && revisadoEn === null
|
||||
}));
|
||||
}
|
||||
|
||||
export interface SeccionDelGenero {
|
||||
id: string;
|
||||
clave: string;
|
||||
titulo: string;
|
||||
cuerpo: string;
|
||||
orden: number;
|
||||
}
|
||||
|
||||
export interface PaisDelGenero {
|
||||
codigo: string;
|
||||
nombre: string;
|
||||
relacion: Relacion | null;
|
||||
}
|
||||
|
||||
export interface GeneroParaEditar {
|
||||
slug: string;
|
||||
nombre: string;
|
||||
resumen: string;
|
||||
origenLugar: string;
|
||||
origenAno: number | null;
|
||||
tempo: string;
|
||||
rima: string;
|
||||
metricaNota: string;
|
||||
compasNota: string;
|
||||
/** Frases sueltas: «Tratamiento de usted, nunca coloquial». */
|
||||
rasgos: string[];
|
||||
estado: Estado;
|
||||
origen: string;
|
||||
modeloOrigen: string | null;
|
||||
revisadoEn: Date | null;
|
||||
secciones: SeccionDelGenero[];
|
||||
/** Los 22 del mapa, con su relación si la tiene. */
|
||||
paises: PaisDelGenero[];
|
||||
/** El capítulo que lo publica, si existe. */
|
||||
fichaSlug: string | null;
|
||||
}
|
||||
|
||||
export async function generoParaEditar(slug: string): Promise<GeneroParaEditar | null> {
|
||||
const [fila] = await db.select().from(genero).where(eq(genero.slug, slug)).limit(1);
|
||||
if (!fila) return null;
|
||||
|
||||
const [secciones, relaciones, todosLosPaises, ficha] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(generoSeccion)
|
||||
.where(eq(generoSeccion.generoId, fila.id))
|
||||
.orderBy(asc(generoSeccion.orden)),
|
||||
db.select().from(generoPais).where(eq(generoPais.generoId, fila.id)),
|
||||
db.select().from(pais).orderBy(asc(pais.nombre)),
|
||||
db.select({ slug: capitulo.slug }).from(capitulo).where(eq(capitulo.generoId, fila.id)).limit(1)
|
||||
]);
|
||||
|
||||
const porCodigo = new Map(relaciones.map((r) => [r.paisCodigo, r.relacion]));
|
||||
|
||||
return {
|
||||
slug: fila.slug,
|
||||
nombre: fila.nombre,
|
||||
resumen: fila.resumen,
|
||||
origenLugar: fila.origenLugar ?? '',
|
||||
origenAno: fila.origenAno,
|
||||
tempo: fila.tempo ?? '',
|
||||
rima: fila.rima ?? '',
|
||||
metricaNota: fila.metricaNota ?? '',
|
||||
compasNota: fila.compasNota ?? '',
|
||||
rasgos: fila.rasgos ?? [],
|
||||
estado: fila.estado,
|
||||
origen: fila.origen,
|
||||
modeloOrigen: fila.modeloOrigen,
|
||||
revisadoEn: fila.revisadoEn,
|
||||
secciones: secciones.map((s) => ({
|
||||
id: s.id,
|
||||
clave: s.clave,
|
||||
titulo: s.titulo,
|
||||
cuerpo: s.cuerpo,
|
||||
orden: s.orden
|
||||
})),
|
||||
paises: todosLosPaises.map((p) => ({
|
||||
codigo: p.codigo,
|
||||
nombre: p.nombre,
|
||||
relacion: porCodigo.get(p.codigo) ?? null
|
||||
})),
|
||||
fichaSlug: ficha[0]?.slug ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export interface CambiosDelGenero {
|
||||
nombre: string;
|
||||
resumen: string;
|
||||
origenLugar: string;
|
||||
origenAno: number | null;
|
||||
tempo: string;
|
||||
rima: string;
|
||||
metricaNota: string;
|
||||
compasNota: string;
|
||||
rasgos: string[];
|
||||
estado: Estado;
|
||||
/** Qué países y con qué relación. Reemplaza lo que hubiera. */
|
||||
paises: { codigo: string; relacion: Relacion }[];
|
||||
/** Secciones existentes, con lo que se haya cambiado o marcado para borrar. */
|
||||
secciones: { id: string; titulo: string; cuerpo: string; orden: number; borrar: boolean }[];
|
||||
/** Una sección nueva, si se ha escrito algo en ella. */
|
||||
nueva: { clave: string; titulo: string; cuerpo: string } | null;
|
||||
/** Marcar la ficha como repasada por una persona. */
|
||||
revisar: boolean;
|
||||
}
|
||||
|
||||
export type ErroresDelGenero = Partial<
|
||||
Record<'nombre' | 'origenAno' | 'paises' | 'nueva' | 'secciones', string>
|
||||
>;
|
||||
|
||||
/** El año actual, para no aceptar un origen en el futuro. */
|
||||
const ESTE_ANO = () => new Date().getFullYear();
|
||||
|
||||
export function revisarGenero(cambios: CambiosDelGenero): ErroresDelGenero {
|
||||
const errores: ErroresDelGenero = {};
|
||||
|
||||
if (cambios.nombre.trim().length < 2) errores.nombre = 'El nombre no puede quedar vacío.';
|
||||
|
||||
if (cambios.origenAno !== null) {
|
||||
if (!Number.isInteger(cambios.origenAno)) {
|
||||
errores.origenAno = 'El año es un número entero.';
|
||||
} else if (cambios.origenAno < 1500 || cambios.origenAno > ESTE_ANO()) {
|
||||
errores.origenAno = `El año tiene que estar entre 1500 y ${ESTE_ANO()}.`;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Dos cunas es un dato falso, no un matiz. Un género nace en un sitio y se
|
||||
* adopta en varios; si de verdad nació en dos orillas —el tango— la cuna es
|
||||
* la región y el otro país va como adoptado, con su nota.
|
||||
*/
|
||||
if (cambios.paises.filter((p) => p.relacion === 'cuna').length > 1) {
|
||||
errores.paises = 'Solo puede haber un país como cuna. Los demás van como adoptado.';
|
||||
}
|
||||
|
||||
/*
|
||||
* Publicar un género que no está en ningún país lo deja fuera del mapa, que
|
||||
* es por donde se llega a las fichas.
|
||||
*/
|
||||
if (cambios.estado === 'publicado' && cambios.paises.length === 0) {
|
||||
errores.paises =
|
||||
'Un género publicado tiene que estar en algún país: si no, no sale en el mapa.';
|
||||
}
|
||||
|
||||
if (cambios.nueva) {
|
||||
if (!/^[a-z][a-z0-9-]{1,40}$/.test(cambios.nueva.clave)) {
|
||||
errores.nueva =
|
||||
'La clave va en minúsculas, sin espacios ni acentos: «historia», «estructura».';
|
||||
} else if (cambios.nueva.titulo.trim() === '') {
|
||||
errores.nueva = 'La sección nueva necesita un título.';
|
||||
}
|
||||
}
|
||||
|
||||
if (cambios.secciones.some((s) => !s.borrar && s.titulo.trim() === '')) {
|
||||
errores.secciones = 'Una sección no puede quedarse sin título.';
|
||||
}
|
||||
|
||||
return errores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda la ficha entera en una transacción.
|
||||
*
|
||||
* Todo junto porque es una sola cosa: si al reemplazar los países falla algo,
|
||||
* no puede quedar el texto guardado y el mapa a medias. Devuelve `false` si el
|
||||
* género ha dejado de existir.
|
||||
*/
|
||||
export async function guardarGenero(slug: string, cambios: CambiosDelGenero): Promise<boolean> {
|
||||
const [fila] = await db
|
||||
.select({ id: genero.id })
|
||||
.from(genero)
|
||||
.where(eq(genero.slug, slug))
|
||||
.limit(1);
|
||||
if (!fila) return false;
|
||||
|
||||
const ahora = new Date();
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(genero)
|
||||
.set({
|
||||
nombre: cambios.nombre.trim(),
|
||||
resumen: cambios.resumen.trim(),
|
||||
// Los opcionales vacíos se guardan como nulo y no como cadena vacía:
|
||||
// «no se sabe» y «es una cadena de cero letras» no son lo mismo, y la
|
||||
// web pregunta por nulo para decidir si pinta el dato.
|
||||
origenLugar: cambios.origenLugar.trim() || null,
|
||||
origenAno: cambios.origenAno,
|
||||
tempo: cambios.tempo.trim() || null,
|
||||
rima: cambios.rima.trim() || null,
|
||||
metricaNota: cambios.metricaNota.trim() || null,
|
||||
compasNota: cambios.compasNota.trim() || null,
|
||||
rasgos: cambios.rasgos.length > 0 ? cambios.rasgos : null,
|
||||
estado: cambios.estado,
|
||||
actualizadoEn: ahora,
|
||||
/*
|
||||
* Repasar es una firma, no una casilla que se pueda desmarcar sola: si
|
||||
* ya estaba revisada se deja la fecha que tenía.
|
||||
*/
|
||||
...(cambios.revisar ? { revisadoEn: ahora } : {})
|
||||
})
|
||||
.where(eq(genero.id, fila.id));
|
||||
|
||||
/*
|
||||
* Los países se reemplazan enteros en vez de ir comparando: son cuatro
|
||||
* filas y el formulario manda el conjunto completo, así que calcular
|
||||
* altas y bajas sería trabajo para llegar al mismo sitio con más formas de
|
||||
* equivocarse. Se pierde la nota de cada país, que hoy no se edita aquí.
|
||||
*/
|
||||
await tx.delete(generoPais).where(eq(generoPais.generoId, fila.id));
|
||||
if (cambios.paises.length > 0) {
|
||||
await tx.insert(generoPais).values(
|
||||
cambios.paises.map((p) => ({
|
||||
generoId: fila.id,
|
||||
paisCodigo: p.codigo,
|
||||
relacion: p.relacion
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const aBorrar = cambios.secciones.filter((s) => s.borrar).map((s) => s.id);
|
||||
if (aBorrar.length > 0) {
|
||||
await tx
|
||||
.delete(generoSeccion)
|
||||
.where(and(eq(generoSeccion.generoId, fila.id), inArray(generoSeccion.id, aBorrar)));
|
||||
}
|
||||
|
||||
for (const s of cambios.secciones) {
|
||||
if (s.borrar) continue;
|
||||
await tx
|
||||
.update(generoSeccion)
|
||||
.set({ titulo: s.titulo.trim(), cuerpo: s.cuerpo.trim(), orden: s.orden })
|
||||
.where(and(eq(generoSeccion.id, s.id), eq(generoSeccion.generoId, fila.id)));
|
||||
}
|
||||
|
||||
if (cambios.nueva) {
|
||||
await tx.insert(generoSeccion).values({
|
||||
generoId: fila.id,
|
||||
clave: cambios.nueva.clave,
|
||||
titulo: cambios.nueva.titulo.trim(),
|
||||
cuerpo: cambios.nueva.cuerpo.trim(),
|
||||
orden: cambios.secciones.filter((s) => !s.borrar).length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { listaDeGeneros } from '$lib/server/panel-generos';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return { generos: await listaDeGeneros() };
|
||||
};
|
||||
@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Los géneros.
|
||||
*
|
||||
* Cada fila dice lo único que hace falta saber desde fuera: si está publicado,
|
||||
* de dónde es, cuánta prosa tiene escrita y si existe el capítulo que lo saca
|
||||
* en la web. Un género sin ficha está en la base y no está en el sitio, y eso
|
||||
* no se ve por ningún otro sitio.
|
||||
*/
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let consulta = $state('');
|
||||
|
||||
const plano = (t: string) => t.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
|
||||
|
||||
const encontrados = $derived.by(() => {
|
||||
const q = plano(consulta.trim());
|
||||
if (!q) return data.generos;
|
||||
return data.generos.filter((g) =>
|
||||
plano(`${g.nombre} ${g.slug} ${g.origenLugar ?? ''}`).includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const ESTADO: Record<string, string> = {
|
||||
borrador: 'Borrador',
|
||||
publicado: 'Publicado',
|
||||
retirado: 'Retirado'
|
||||
};
|
||||
|
||||
/**
|
||||
* «Santiago de Cuba, 1883» y no «Santiago de Cuba, 1883, 1883».
|
||||
*
|
||||
* El lugar de origen es texto libre y a menudo ya trae la fecha dentro
|
||||
* —«México, tras la Revolución (años veinte y treinta)»—. Añadirle el año
|
||||
* siempre repetía el dato o lo contradecía.
|
||||
*/
|
||||
function procedencia(lugar: string | null, ano: number | null): string {
|
||||
if (!lugar) return ano ? String(ano) : 'sin lugar de origen';
|
||||
if (!ano || lugar.includes(String(ano))) return lugar;
|
||||
return `${lugar}, ${ano}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Géneros · Panel</title>
|
||||
</svelte:head>
|
||||
|
||||
<header>
|
||||
<h1>Géneros</h1>
|
||||
<p class="entradilla">
|
||||
De dónde viene cada estilo, en qué países se canta y toda la prosa de su ficha.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="buscador">
|
||||
<label class="visually-hidden" for="buscar">Buscar un género</label>
|
||||
<input
|
||||
id="buscar"
|
||||
type="search"
|
||||
bind:value={consulta}
|
||||
placeholder="Buscar por nombre o por lugar"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="cuenta muted" role="status">{encontrados.length} de {data.generos.length}</p>
|
||||
</div>
|
||||
|
||||
{#if encontrados.length === 0}
|
||||
<p class="vacio muted">Nada con «{consulta.trim()}».</p>
|
||||
{:else}
|
||||
<table>
|
||||
<tbody>
|
||||
{#each encontrados as g (g.slug)}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<a href={resolve('/(dentro)/generos/[slug]', { slug: g.slug })}>{g.nombre}</a>
|
||||
<span class="donde muted">{procedencia(g.origenLugar, g.origenAno)}</span>
|
||||
</th>
|
||||
|
||||
<td>
|
||||
<!--
|
||||
Las pastillas en su propia caja y no en el `td`: un `td` en flex
|
||||
deja de comportarse como celda y su borde inferior se descuelga
|
||||
del de la fila. Se vio en la captura, con las tres columnas
|
||||
cortadas a distinta altura.
|
||||
-->
|
||||
<div class="marcas">
|
||||
<span class="pastilla" data-estado={g.estado}>{ESTADO[g.estado] ?? g.estado}</span>
|
||||
{#if !g.tieneFicha}
|
||||
<span class="pastilla" data-aviso="si">Sin ficha</span>
|
||||
{/if}
|
||||
{#if g.sinRevisar}
|
||||
<span class="pastilla" data-aviso="si">Sin revisar</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="secciones muted">
|
||||
{g.secciones}
|
||||
{g.secciones === 1 ? 'sección' : 'secciones'}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
max-width: 62ch;
|
||||
padding-bottom: var(--sp-5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 var(--sp-2);
|
||||
font-size: var(--fs-2xl);
|
||||
}
|
||||
|
||||
.entradilla {
|
||||
margin: 0;
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.buscador {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-4);
|
||||
padding-bottom: var(--sp-5);
|
||||
}
|
||||
|
||||
input[type='search'] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: var(--boton-relleno);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-boton);
|
||||
background: var(--c-bg-sunken);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: var(--boton-texto);
|
||||
}
|
||||
|
||||
input[type='search']:focus-visible {
|
||||
outline: 2px solid var(--c-accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.cuenta {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
font-size: var(--fs-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
text-align: left;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.donde {
|
||||
display: block;
|
||||
margin-top: 0.15rem;
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.marcas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* El estado en pastilla, y los avisos en el color del dato. Publicado va en
|
||||
* gris: es lo normal, y si los tres llevaran color ninguno destacaría.
|
||||
*/
|
||||
.pastilla {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 999px;
|
||||
font-size: var(--fs-xs);
|
||||
white-space: nowrap;
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
.pastilla[data-estado='borrador'],
|
||||
.pastilla[data-aviso='si'] {
|
||||
border-color: color-mix(in oklab, var(--c-dato) 45%, transparent);
|
||||
color: var(--c-dato);
|
||||
}
|
||||
|
||||
.pastilla[data-estado='retirado'] {
|
||||
border-color: color-mix(in oklab, var(--c-danger) 45%, transparent);
|
||||
color: var(--c-danger);
|
||||
}
|
||||
|
||||
.secciones {
|
||||
width: 1%;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-size: var(--fs-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.vacio {
|
||||
padding-block: var(--sp-6);
|
||||
}
|
||||
|
||||
@media (max-width: 44rem) {
|
||||
th,
|
||||
td {
|
||||
padding: var(--sp-3) var(--sp-2);
|
||||
}
|
||||
|
||||
.secciones {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,97 @@
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
import {
|
||||
generoParaEditar,
|
||||
guardarGenero,
|
||||
revisarGenero,
|
||||
type CambiosDelGenero,
|
||||
type Estado,
|
||||
type Relacion
|
||||
} from '$lib/server/panel-generos';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const genero = await generoParaEditar(params.slug);
|
||||
if (!genero) error(404, 'No hay ningún género con ese slug.');
|
||||
|
||||
return { genero };
|
||||
};
|
||||
|
||||
/** Un año vacío es «no se sabe», no un cero. */
|
||||
function anoDe(valor: FormDataEntryValue | null): number | null {
|
||||
const texto = String(valor ?? '').trim();
|
||||
if (texto === '') return null;
|
||||
return Number(texto);
|
||||
}
|
||||
|
||||
/** Una frase por línea, sin las vacías: es como se escribe una lista a mano. */
|
||||
function lineasDe(valor: FormDataEntryValue | null): string[] {
|
||||
return String(valor ?? '')
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l !== '');
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, params }) => {
|
||||
const datos = await request.formData();
|
||||
|
||||
/*
|
||||
* Los países vienen en dos campos: qué casillas están marcadas y, de esas,
|
||||
* cuál es la cuna. Se cruzan aquí en vez de mandar un solo campo por país
|
||||
* porque una casilla sin marcar no se envía, y así el conjunto que llega
|
||||
* es exactamente el que se ve en pantalla.
|
||||
*/
|
||||
const cuna = String(datos.get('cuna') ?? '');
|
||||
const paises = datos.getAll('pais').map((codigo) => ({
|
||||
codigo: String(codigo),
|
||||
relacion: (String(codigo) === cuna ? 'cuna' : 'adoptado') as Relacion
|
||||
}));
|
||||
|
||||
/*
|
||||
* Las secciones viajan con su identificador en el nombre del campo. Se
|
||||
* recorren las que el formulario declara en `seccion`, y no las de la base,
|
||||
* para que borrar una en pantalla la borre de verdad.
|
||||
*/
|
||||
const secciones = datos.getAll('seccion').map((valor) => {
|
||||
const id = String(valor);
|
||||
return {
|
||||
id,
|
||||
titulo: String(datos.get(`titulo:${id}`) ?? ''),
|
||||
cuerpo: String(datos.get(`cuerpo:${id}`) ?? ''),
|
||||
orden: Number(datos.get(`orden:${id}`) ?? 0),
|
||||
borrar: datos.get(`borrar:${id}`) !== null
|
||||
};
|
||||
});
|
||||
|
||||
const claveNueva = String(datos.get('nueva:clave') ?? '').trim();
|
||||
const tituloNuevo = String(datos.get('nueva:titulo') ?? '').trim();
|
||||
const cuerpoNuevo = String(datos.get('nueva:cuerpo') ?? '').trim();
|
||||
// Solo cuenta como sección nueva si se ha escrito algo en ella.
|
||||
const hayNueva = claveNueva !== '' || tituloNuevo !== '' || cuerpoNuevo !== '';
|
||||
|
||||
const cambios: CambiosDelGenero = {
|
||||
nombre: String(datos.get('nombre') ?? ''),
|
||||
resumen: String(datos.get('resumen') ?? ''),
|
||||
origenLugar: String(datos.get('origenLugar') ?? ''),
|
||||
origenAno: anoDe(datos.get('origenAno')),
|
||||
tempo: String(datos.get('tempo') ?? ''),
|
||||
rima: String(datos.get('rima') ?? ''),
|
||||
metricaNota: String(datos.get('metricaNota') ?? ''),
|
||||
compasNota: String(datos.get('compasNota') ?? ''),
|
||||
rasgos: lineasDe(datos.get('rasgos')),
|
||||
estado: String(datos.get('estado') ?? '') as Estado,
|
||||
paises,
|
||||
secciones,
|
||||
nueva: hayNueva ? { clave: claveNueva, titulo: tituloNuevo, cuerpo: cuerpoNuevo } : null,
|
||||
revisar: datos.get('revisar') !== null
|
||||
};
|
||||
|
||||
const errores = revisarGenero(cambios);
|
||||
if (Object.keys(errores).length > 0) return fail(400, { errores });
|
||||
|
||||
const guardado = await guardarGenero(params.slug, cambios);
|
||||
if (!guardado) error(404, 'Ese género ha dejado de existir mientras lo editabas.');
|
||||
|
||||
return { guardado: true };
|
||||
}
|
||||
};
|
||||
Loading…
Reference in new issue