Compare commits
5 Commits
c3be9ab9a2
...
e779907ff4
| Author | SHA1 | Date | |
|---|---|---|---|
| e779907ff4 | |||
| 2e0b742e49 | |||
| 868e7d3c23 | |||
| 00131f311a | |||
| 7d4433279d |
5
.env.example
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Credenciales del panel de administración
|
||||||
|
# Copia este archivo como .env y rellena con tus valores reales
|
||||||
|
|
||||||
|
ADMIN_USER=admin
|
||||||
|
ADMIN_PASS=tu_contrasena_segura_aqui
|
||||||
3
.gitignore
vendored
@@ -22,3 +22,6 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Claude context
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import node from '@astrojs/node';
|
import node from '@astrojs/node';
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
import keystatic from '@keystatic/astro';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -8,4 +10,5 @@ export default defineConfig({
|
|||||||
adapter: node({
|
adapter: node({
|
||||||
mode: 'standalone',
|
mode: 'standalone',
|
||||||
}),
|
}),
|
||||||
|
integrations: [react(), keystatic()],
|
||||||
});
|
});
|
||||||
|
|||||||
142
keystatic.config.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { config, collection, fields } from '@keystatic/core';
|
||||||
|
|
||||||
|
export default config({
|
||||||
|
storage: {
|
||||||
|
kind: 'local',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
brand: {
|
||||||
|
name: 'Admin Renacer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
programas: collection({
|
||||||
|
label: 'Programas',
|
||||||
|
slugField: 'nombre',
|
||||||
|
path: 'src/content/programas/*',
|
||||||
|
format: { data: 'yaml' },
|
||||||
|
schema: {
|
||||||
|
nombre: fields.slug({
|
||||||
|
name: { label: 'Nombre del programa' },
|
||||||
|
slug: { label: 'Slug (URL)' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ── Tarjeta en el landing ─────────────────────────────────────
|
||||||
|
estado: fields.select({
|
||||||
|
label: 'Estado',
|
||||||
|
options: [
|
||||||
|
{ label: 'Activo', value: 'Activo' },
|
||||||
|
{ label: 'Vacaciones', value: 'Vacaciones' },
|
||||||
|
{ label: 'Anual', value: 'Anual' },
|
||||||
|
{ label: 'Temporada', value: 'Temporada' },
|
||||||
|
{ label: 'En planificación', value: 'En planificación' },
|
||||||
|
],
|
||||||
|
defaultValue: 'Activo',
|
||||||
|
}),
|
||||||
|
bajada: fields.text({
|
||||||
|
label: 'Resumen corto (opcional)',
|
||||||
|
description: 'Campo editorial secundario. Ya no se muestra en la tarjeta pública.',
|
||||||
|
}),
|
||||||
|
detalle: fields.text({
|
||||||
|
label: 'Descripción breve',
|
||||||
|
description: 'Texto que sí aparece en la tarjeta del landing, bajo el título.',
|
||||||
|
multiline: true,
|
||||||
|
validation: { isRequired: true },
|
||||||
|
}),
|
||||||
|
icono: fields.select({
|
||||||
|
label: 'Icono de la tarjeta',
|
||||||
|
description: 'Se muestra dentro del círculo de la tarjeta del programa.',
|
||||||
|
options: [
|
||||||
|
{ label: 'Manos y apoyo', value: 'HeartHandshake' },
|
||||||
|
{ label: 'Alimentación', value: 'UtensilsCrossed' },
|
||||||
|
{ label: 'Canasta / apoyo', value: 'Package' },
|
||||||
|
{ label: 'Vestimenta', value: 'Shirt' },
|
||||||
|
{ label: 'Naturaleza', value: 'Sprout' },
|
||||||
|
{ label: 'Regalo / campaña', value: 'Gift' },
|
||||||
|
{ label: 'Juego e infancia', value: 'Puzzle' },
|
||||||
|
{ label: 'Comunidad', value: 'Users' },
|
||||||
|
{ label: 'Inclusión', value: 'HandHelping' },
|
||||||
|
],
|
||||||
|
defaultValue: 'HeartHandshake',
|
||||||
|
}),
|
||||||
|
orden: fields.integer({
|
||||||
|
label: 'Orden en landing',
|
||||||
|
description: 'Número de posición (menor = primero). Ej: 1, 2, 3…',
|
||||||
|
defaultValue: 99,
|
||||||
|
validation: { isRequired: true },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ── Configuración de página ───────────────────────────────────
|
||||||
|
tienePagina: fields.checkbox({
|
||||||
|
label: '¿Tiene página propia?',
|
||||||
|
description: 'Desmarcar para programas en planificación sin página',
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
|
externalHref: fields.text({
|
||||||
|
label: 'URL externa o personalizada (opcional)',
|
||||||
|
description: 'Déjalo vacío para usar automáticamente /programas/slug/.',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// ── Contenido de la página ────────────────────────────────────
|
||||||
|
kicker: fields.text({
|
||||||
|
label: 'Kicker (nombre corto para la página)',
|
||||||
|
description: 'Aparece sobre el título principal',
|
||||||
|
}),
|
||||||
|
titulo: fields.text({
|
||||||
|
label: 'Título de la página',
|
||||||
|
description: 'Título largo y descriptivo',
|
||||||
|
}),
|
||||||
|
lead: fields.text({
|
||||||
|
label: 'Párrafo introductorio (lead)',
|
||||||
|
description: 'Párrafo destacado que aparece bajo el título',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
parrafos: fields.array(
|
||||||
|
fields.text({ label: 'Párrafo', multiline: true }),
|
||||||
|
{
|
||||||
|
label: 'Párrafos del cuerpo',
|
||||||
|
description: 'Agrega los párrafos del contenido principal',
|
||||||
|
itemLabel: (props) => {
|
||||||
|
const val = props.value ?? '';
|
||||||
|
return val.length > 60 ? val.slice(0, 60) + '…' : val || 'Párrafo vacío';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
stats: fields.array(
|
||||||
|
fields.object({
|
||||||
|
valor: fields.text({ label: 'Valor o cifra' }),
|
||||||
|
etiqueta: fields.text({ label: 'Descripción' }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
label: 'Estadísticas (idealmente 4)',
|
||||||
|
description: 'Datos clave del programa en formato cifra + descripción',
|
||||||
|
itemLabel: (props) => props.fields.valor.value || 'Estadística',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
textoApoyo: fields.text({
|
||||||
|
label: 'Texto de apoyo / cómo colaborar',
|
||||||
|
description: 'Texto que aparece en la sección final de la página',
|
||||||
|
multiline: true,
|
||||||
|
}),
|
||||||
|
imagen: fields.image({
|
||||||
|
label: 'Imagen principal (opcional)',
|
||||||
|
description: 'Portada del hero para la página del programa',
|
||||||
|
directory: 'public/uploads/programas',
|
||||||
|
publicPath: '/uploads/programas/',
|
||||||
|
}),
|
||||||
|
galeria: fields.array(
|
||||||
|
fields.image({
|
||||||
|
label: 'Imagen de galería',
|
||||||
|
directory: 'public/uploads/programas',
|
||||||
|
publicPath: '/uploads/programas/',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
label: 'Galería (opcional)',
|
||||||
|
description: 'Imágenes secundarias para mostrar debajo del contenido',
|
||||||
|
itemLabel: (props) => props.value?.split('/').pop() ?? 'Imagen',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
4252
package-lock.json
generated
11
package.json
@@ -11,8 +11,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.1",
|
"@astrojs/node": "^9.5.1",
|
||||||
"astro": "^5.16.7"
|
"@astrojs/react": "^5.0.3",
|
||||||
},
|
"@keystatic/astro": "^5.0.6",
|
||||||
"devDependencies": {
|
"@keystatic/core": "^0.5.50",
|
||||||
|
"astro": "^5.16.7",
|
||||||
|
"image-size": "^2.0.2",
|
||||||
|
"lucide": "^1.8.0",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/uploads/branding/logo-renacer-sin-fondo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
0
public/uploads/programas/.gitkeep
Normal file
BIN
public/uploads/programas/canasta-familiar/entrega-canasta.jpeg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
public/uploads/programas/canasta-familiar/imagen-principal.jpg
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
public/uploads/programas/comedor-solidario/comedor-accion.jpeg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/uploads/programas/comedor-solidario/imagen1.jpg
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
public/uploads/programas/fiestas-patrias/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
public/uploads/programas/fiestas-patrias/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
public/uploads/programas/fiestas-patrias/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
public/uploads/programas/germinando-suenos/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
public/uploads/programas/germinando-suenos/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
public/uploads/programas/germinando-suenos/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
public/uploads/programas/juegotetra/imagen1.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/uploads/programas/juegotetra/imagen2.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/uploads/programas/juegotetra/imagen3.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/uploads/programas/ropero-solidario/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
public/uploads/programas/ropero-solidario/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
public/uploads/programas/ropero-solidario/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
public/uploads/programas/tardes-recreativas/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
public/uploads/programas/tardes-recreativas/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
public/uploads/programas/tardes-recreativas/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/uploads/programas/test/imagen.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/assets/canasta-familiar/entrega-canasta.jpeg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
src/assets/canasta-familiar/imagen-principal.jpg
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
src/assets/comedor-solidario/comedor-accion.jpeg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
src/assets/comedor-solidario/imagen1.jpg
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
src/assets/fiestas-patrias/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
src/assets/fiestas-patrias/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
src/assets/fiestas-patrias/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/germinando-suenos/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
src/assets/germinando-suenos/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
src/assets/germinando-suenos/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
src/assets/juegotetra/imagen1.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/assets/juegotetra/imagen2.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/assets/juegotetra/imagen3.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/assets/logo-renacer-sin-fondo.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/ropero-solidario/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
src/assets/ropero-solidario/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/assets/ropero-solidario/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
src/assets/tardes-recreativas/imagen1.jpeg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
src/assets/tardes-recreativas/imagen2.jpeg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
src/assets/tardes-recreativas/imagen3.jpeg
Normal file
|
After Width: | Height: | Size: 137 KiB |
@@ -1,232 +1,360 @@
|
|||||||
---
|
---
|
||||||
|
import LucideIcon from './LucideIcon.astro';
|
||||||
|
|
||||||
const donationLinks = [
|
const donationLinks = [
|
||||||
{
|
{
|
||||||
name: 'Gesto Solidario',
|
name: 'Gesto Solidario',
|
||||||
amount: 'CLP 1.000 a CLP 4.000',
|
amount: 'CLP 1.000 a CLP 4.000',
|
||||||
description: 'Un pequeño gesto que también ayuda a sembrar esperanza.',
|
description: 'Un pequeño gesto que también ayuda a sembrar esperanza.',
|
||||||
emoji: '🤍',
|
icon: 'HeartHandshake',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/tDRHQvX1T0JL1gdx8A7S0mM4RFqV25AR',
|
href: 'https://app.reveniu.com/checkout-custom-link/tDRHQvX1T0JL1gdx8A7S0mM4RFqV25AR',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Semilla Solidaria',
|
name: 'Semilla Solidaria',
|
||||||
amount: '$5.000',
|
amount: '$5.000',
|
||||||
description: 'Inicia la ayuda y permite sembrar nuevas oportunidades.',
|
description: 'Inicia la ayuda y permite sembrar nuevas oportunidades.',
|
||||||
emoji: '🌱',
|
icon: 'Sprout',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/95bH9li3iYcdJKxuugTd1NV2s7n5sglH',
|
href: 'https://app.reveniu.com/checkout-custom-link/95bH9li3iYcdJKxuugTd1NV2s7n5sglH',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Brote de Esperanza',
|
name: 'Brote de Esperanza',
|
||||||
amount: '$10.000',
|
amount: '$10.000',
|
||||||
description: 'Ayuda a que nuevos proyectos comiencen a crecer.',
|
description: 'Ayuda a que nuevos proyectos comiencen a crecer.',
|
||||||
emoji: '🌿',
|
icon: 'Leaf',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/jwtYOw0vQOk5QyWXg8Zps9t6HMBfksmY',
|
href: 'https://app.reveniu.com/checkout-custom-link/jwtYOw0vQOk5QyWXg8Zps9t6HMBfksmY',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Árbol de Apoyo',
|
name: 'Árbol de Apoyo',
|
||||||
amount: '$20.000',
|
amount: '$20.000',
|
||||||
description: 'Fortalece el acompañamiento a niños y familias.',
|
description: 'Fortalece el acompañamiento a niños y familias.',
|
||||||
emoji: '🌳',
|
icon: 'Users',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/dZEYJhCPUUQwOU8zqfYtsIIkGLpPPFsD',
|
href: 'https://app.reveniu.com/checkout-custom-link/dZEYJhCPUUQwOU8zqfYtsIIkGLpPPFsD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sol Solidario',
|
name: 'Sol Solidario',
|
||||||
amount: '$30.000',
|
amount: '$30.000',
|
||||||
description: 'Entrega la energía que permite que todo siga creciendo.',
|
description: 'Entrega la energía que permite que todo siga creciendo.',
|
||||||
emoji: '🌞',
|
icon: 'CircleDollarSign',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/ayAQljPMkfiwbl8FOEl9SIBXU1u8wRhK',
|
href: 'https://app.reveniu.com/checkout-custom-link/ayAQljPMkfiwbl8FOEl9SIBXU1u8wRhK',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Raíz Fundadora',
|
name: 'Raíz Fundadora',
|
||||||
amount: '$40.000',
|
amount: '$40.000',
|
||||||
description: 'Sostiene y da estabilidad al proyecto.',
|
description: 'Sostiene y da estabilidad al proyecto.',
|
||||||
emoji: '✨',
|
icon: 'BadgeCheck',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/kX0o2VEFugphajKBGUDhQEUew9YqYDtt',
|
href: 'https://app.reveniu.com/checkout-custom-link/kX0o2VEFugphajKBGUDhQEUew9YqYDtt',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Guardián de Renacer',
|
name: 'Guardián de Renacer',
|
||||||
amount: '$50.000 o más',
|
amount: '$50.000 o más',
|
||||||
description: 'Protege y cuida el futuro del centro y de quienes lo necesitan.',
|
description: 'Protege y cuida el futuro del centro y de quienes lo necesitan.',
|
||||||
emoji: '🛡️',
|
icon: 'ShieldCheck',
|
||||||
href: 'https://app.reveniu.com/checkout-custom-link/feN8IRS5PRiC2gC0XKqhwyBTus342V0d',
|
href: 'https://app.reveniu.com/checkout-custom-link/feN8IRS5PRiC2gC0XKqhwyBTus342V0d',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<main class="linktree-page">
|
<main class="donations-page">
|
||||||
<section class="linktree-card">
|
<div class="donations-shell">
|
||||||
<a href="/" class="back-link">← Volver a Renacer</a>
|
<aside class="donations-aside">
|
||||||
<div class="brand-badge">Renacer</div>
|
<a href="/" class="back-link">← Volver a Renacer</a>
|
||||||
<h1>Colaborador mensual</h1>
|
<div class="brand-badge">Renacer</div>
|
||||||
<p class="intro">
|
<h1>Colaborador mensual</h1>
|
||||||
Elige una forma de aportar mes a mes y ayúdanos a sostener el trabajo con niños, familias y
|
<p class="intro">
|
||||||
comunidad.
|
Elige un nivel de aporte mensual y abre el checkout seguro para sostener el trabajo con
|
||||||
</p>
|
niños, familias y comunidad.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="links-list">
|
<div class="aside-panel">
|
||||||
{donationLinks.map((item) => (
|
<div class="aside-stat">
|
||||||
<a
|
<LucideIcon name="CircleDollarSign" size={18} />
|
||||||
class="donation-link"
|
<div>
|
||||||
href={item.href}
|
<strong>Aporte recurrente</strong>
|
||||||
target="_blank"
|
<span>Ayuda a dar continuidad a programas y campañas.</span>
|
||||||
rel="nofollow sponsored noopener noreferrer"
|
</div>
|
||||||
>
|
</div>
|
||||||
<span class="link-emoji" aria-hidden="true">{item.emoji}</span>
|
<div class="aside-stat">
|
||||||
<span class="link-copy">
|
<LucideIcon name="ShieldCheck" size={18} />
|
||||||
<strong>
|
<div>
|
||||||
{item.name} <span>{item.amount}</span>
|
<strong>Checkout externo seguro</strong>
|
||||||
</strong>
|
<span>Los enlaces abren la plataforma protegida para completar tu aporte.</span>
|
||||||
<small>{item.description}</small>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span class="link-arrow" aria-hidden="true">↗</span>
|
<div class="aside-stat">
|
||||||
</a>
|
<LucideIcon name="HandHelping" size={18} />
|
||||||
))}
|
<div>
|
||||||
</div>
|
<strong>Impacto real</strong>
|
||||||
|
<span>Tu colaboración ayuda a sostener alimentación, infancia y apoyo familiar.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<p class="footnote">Los enlaces de apoyo abren una plataforma externa y segura para completar tu aporte.</p>
|
<section class="donations-content">
|
||||||
</section>
|
<div class="content-heading">
|
||||||
|
<p class="eyebrow">Formas de aportar</p>
|
||||||
|
<h2>Escoge la opción que mejor se ajuste a tu compromiso.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="links-grid">
|
||||||
|
{donationLinks.map((item) => (
|
||||||
|
<a
|
||||||
|
class="donation-card"
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="nofollow sponsored noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div class="card-icon">
|
||||||
|
<LucideIcon name={item.icon} size={20} />
|
||||||
|
</div>
|
||||||
|
<div class="card-copy">
|
||||||
|
<strong>{item.name}</strong>
|
||||||
|
<span class="card-amount">{item.amount}</span>
|
||||||
|
<small>{item.description}</small>
|
||||||
|
</div>
|
||||||
|
<span class="card-arrow">
|
||||||
|
<LucideIcon name="ArrowUpRight" size={18} />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="footnote">
|
||||||
|
Si tienes dudas sobre aportes, coordinación institucional o donaciones directas, escríbenos
|
||||||
|
a `contacto@familiarenacer.cl`.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.linktree-page {
|
.donations-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
padding: 1.5rem;
|
||||||
place-items: center;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top, rgba(29, 107, 87, 0.12), transparent 28%),
|
radial-gradient(circle at 10% 10%, rgba(8, 52, 183, 0.14), transparent 20%),
|
||||||
linear-gradient(180deg, #f8f4ec 0%, #f2ede3 100%);
|
radial-gradient(circle at 88% 18%, rgba(60, 118, 255, 0.12), transparent 18%),
|
||||||
|
linear-gradient(180deg, #eef4ff 0%, #f7faff 40%, #eef4ff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.linktree-card {
|
.donations-shell {
|
||||||
width: min(100%, 32rem);
|
width: min(100%, 1220px);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 0.88fr) minmax(0, 1.12fr);
|
||||||
|
gap: 1.25rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donations-aside,
|
||||||
|
.donations-content {
|
||||||
|
border-radius: 1.7rem;
|
||||||
|
border: 1px solid rgba(18, 63, 184, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.84);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
box-shadow: 0 18px 42px rgba(16, 46, 120, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.donations-aside {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border: 1px solid rgba(31, 39, 37, 0.1);
|
}
|
||||||
border-radius: 1.75rem;
|
|
||||||
background: rgba(255, 255, 255, 0.88);
|
.donations-content {
|
||||||
backdrop-filter: blur(12px);
|
padding: 1.45rem;
|
||||||
box-shadow: 0 18px 45px rgba(35, 44, 41, 0.09);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-link {
|
.back-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: #47625b;
|
color: #47649a;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-badge {
|
.brand-badge,
|
||||||
|
.eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: 0 auto 0.9rem;
|
|
||||||
padding: 0.45rem 0.8rem;
|
padding: 0.45rem 0.8rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(8, 52, 183, 0.08);
|
background: rgba(8, 52, 183, 0.08);
|
||||||
color: #0834b7;
|
color: #0834b7;
|
||||||
font-size: 0.78rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
strong {
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: #122756;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
margin: 1rem 0 0;
|
||||||
font-size: clamp(2rem, 5vw, 2.8rem);
|
font-size: clamp(2.2rem, 4vw, 3.4rem);
|
||||||
line-height: 1;
|
line-height: 0.96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro,
|
.intro,
|
||||||
.footnote {
|
.footnote,
|
||||||
margin: 0.85rem auto 0;
|
.aside-stat span,
|
||||||
max-width: 32ch;
|
.card-copy small {
|
||||||
text-align: center;
|
color: #5e6f8d;
|
||||||
color: #5f6966;
|
line-height: 1.65;
|
||||||
line-height: 1.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.links-list {
|
.intro {
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
max-width: 34ch;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
margin-top: 1.6rem;
|
margin-top: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-link {
|
.aside-stat {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: start;
|
||||||
|
padding: 0.95rem 1rem;
|
||||||
|
border-radius: 1.1rem;
|
||||||
|
background: linear-gradient(180deg, rgba(244, 248, 255, 0.95), rgba(236, 243, 255, 0.95));
|
||||||
|
border: 1px solid rgba(18, 63, 184, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-stat :global(svg) {
|
||||||
|
color: #0834b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-stat strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.96rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-stat span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-heading h2 {
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
font-size: clamp(1.55rem, 3vw, 2.3rem);
|
||||||
|
line-height: 1.02;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
margin-top: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
gap: 0.9rem;
|
gap: 0.85rem;
|
||||||
padding: 1rem 1rem 1rem 0.95rem;
|
padding: 1rem;
|
||||||
border: 1px solid rgba(31, 39, 37, 0.1);
|
|
||||||
border-radius: 1.2rem;
|
border-radius: 1.2rem;
|
||||||
background: #ffffff;
|
border: 1px solid rgba(18, 63, 184, 0.1);
|
||||||
box-shadow: 0 10px 24px rgba(35, 44, 41, 0.05);
|
background: linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
|
||||||
|
box-shadow: 0 12px 28px rgba(18, 54, 144, 0.06);
|
||||||
|
text-decoration: none;
|
||||||
transition:
|
transition:
|
||||||
transform 180ms ease,
|
transform 180ms ease,
|
||||||
border-color 180ms ease,
|
border-color 180ms ease,
|
||||||
box-shadow 180ms ease;
|
box-shadow 180ms ease,
|
||||||
|
background 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-link:hover {
|
.donation-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
border-color: rgba(8, 52, 183, 0.22);
|
border-color: rgba(8, 52, 183, 0.22);
|
||||||
box-shadow: 0 14px 30px rgba(8, 52, 183, 0.1);
|
background: linear-gradient(180deg, #ffffff 0%, #eaf1ff 100%);
|
||||||
|
box-shadow: 0 18px 34px rgba(8, 52, 183, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-emoji {
|
.card-icon {
|
||||||
display: inline-grid;
|
width: 2.8rem;
|
||||||
place-items: center;
|
height: 2.8rem;
|
||||||
width: 2.75rem;
|
display: inline-flex;
|
||||||
height: 2.75rem;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(8, 52, 183, 0.08);
|
background: rgba(8, 52, 183, 0.09);
|
||||||
font-size: 1.25rem;
|
color: #0834b7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-copy {
|
.card-copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.22rem;
|
gap: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-copy strong {
|
.card-copy strong {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.45rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.35;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-copy strong span,
|
.card-amount {
|
||||||
.link-arrow {
|
color: #0834b7;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-copy small {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
color: #0834b7;
|
color: #0834b7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-copy small {
|
|
||||||
color: #5f6966;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-arrow {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footnote {
|
.footnote {
|
||||||
font-size: 0.9rem;
|
margin: 1.2rem 0 0;
|
||||||
margin-top: 1.2rem;
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.donations-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donations-aside {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.linktree-page {
|
.donations-page {
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.linktree-card {
|
.donations-aside,
|
||||||
padding: 1.15rem;
|
.donations-content {
|
||||||
|
padding: 1.1rem;
|
||||||
border-radius: 1.35rem;
|
border-radius: 1.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-link {
|
.donation-card {
|
||||||
padding: 0.9rem;
|
grid-template-columns: auto 1fr;
|
||||||
gap: 0.75rem;
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
44
src/components/LucideIcon.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import { icons } from 'lucide';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
size = 20,
|
||||||
|
strokeWidth = 1.8,
|
||||||
|
class: className = '',
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const icon = icons[name];
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
throw new Error(`Lucide icon "${name}" is not available.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const markup = icon.map(([tag, attrs]) => {
|
||||||
|
const attrString = Object.entries(attrs)
|
||||||
|
.map(([key, value]) => `${key}="${String(value).replace(/"/g, '"')}"`)
|
||||||
|
.join(' ');
|
||||||
|
return `<${tag} ${attrString}></${tag}>`;
|
||||||
|
}).join('');
|
||||||
|
---
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class:list={['lucide-icon', className]}
|
||||||
|
set:html={markup}
|
||||||
|
/>
|
||||||
604
src/components/ProgramInfoPage.astro
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
interface Stat {
|
||||||
|
valor: string;
|
||||||
|
etiqueta: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgramImage {
|
||||||
|
src: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
backHref?: string;
|
||||||
|
kicker: string;
|
||||||
|
title: string;
|
||||||
|
lead: string;
|
||||||
|
paragraphs: string[];
|
||||||
|
stats: Stat[];
|
||||||
|
supportText: string;
|
||||||
|
images?: ProgramImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
backHref = '/',
|
||||||
|
kicker,
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
paragraphs,
|
||||||
|
stats,
|
||||||
|
supportText,
|
||||||
|
images = [],
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const hasImages = images.length > 0;
|
||||||
|
const heroImage = images[0];
|
||||||
|
const galleryImages = images.slice(1, 3);
|
||||||
|
---
|
||||||
|
|
||||||
|
<main class="program-page">
|
||||||
|
|
||||||
|
<!-- ── Hero ───────────────────────────────────────── -->
|
||||||
|
<section class={`program-hero ${hasImages ? 'has-image' : ''}`}>
|
||||||
|
<div class="hero-backdrop"></div>
|
||||||
|
<div class="hero-inner">
|
||||||
|
<div class="hero-text">
|
||||||
|
<a href={backHref} class="back-link">← Volver a Renacer</a>
|
||||||
|
<p class="kicker">{kicker}</p>
|
||||||
|
<h1 class="hero-title">{title}</h1>
|
||||||
|
<p class="hero-lead">{lead}</p>
|
||||||
|
</div>
|
||||||
|
{hasImages && heroImage && (
|
||||||
|
<div class="hero-visual">
|
||||||
|
<Image
|
||||||
|
src={heroImage.src}
|
||||||
|
alt={`Foto del programa ${kicker}`}
|
||||||
|
width={heroImage.width}
|
||||||
|
height={heroImage.height}
|
||||||
|
widths={[720, 1080]}
|
||||||
|
sizes="(max-width: 960px) 100vw, 46vw"
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Contenido + Stats ──────────────────────────── -->
|
||||||
|
<section class="content-section">
|
||||||
|
<div class="content-inner">
|
||||||
|
<div class="text-col">
|
||||||
|
{paragraphs.map((p) => (
|
||||||
|
<p data-reveal>{p}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<aside class="stats-col">
|
||||||
|
<div class="stats-grid">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<article class="stat-card" data-reveal>
|
||||||
|
<strong class="stat-valor" data-valor={stat.valor}>{stat.valor}</strong>
|
||||||
|
<span class="stat-label">{stat.etiqueta}</span>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Galería (si hay imágenes secundarias) ─────── -->
|
||||||
|
{galleryImages.length > 0 && (
|
||||||
|
<section class="gallery-section">
|
||||||
|
<div class="gallery-inner">
|
||||||
|
{galleryImages.map((img, i) => (
|
||||||
|
<div class="gallery-item" data-reveal>
|
||||||
|
<Image
|
||||||
|
src={img.src}
|
||||||
|
alt={`Foto del programa ${kicker} ${i + 2}`}
|
||||||
|
width={img.width}
|
||||||
|
height={img.height}
|
||||||
|
widths={[520, 800]}
|
||||||
|
sizes="(max-width: 960px) 100vw, 48vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- ── Apoyo ──────────────────────────────────────── -->
|
||||||
|
<section class="support-section" data-reveal>
|
||||||
|
<div class="support-card">
|
||||||
|
<div class="support-text-col">
|
||||||
|
<p class="support-kicker">Apoyo y comunidad</p>
|
||||||
|
<p class="support-text">{supportText}</p>
|
||||||
|
<div class="support-actions">
|
||||||
|
<a class="btn-primary" href="/colaboradormensual/">Colaborar mensualmente</a>
|
||||||
|
<a class="btn-secondary" href="mailto:contacto@familiarenacer.cl">Contactar a Renacer</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="support-deco" aria-hidden="true">
|
||||||
|
<div class="deco-ring deco-ring-1"></div>
|
||||||
|
<div class="deco-ring deco-ring-2"></div>
|
||||||
|
<span class="deco-icon">🤝</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ── Base ─────────────────────────────────────────── */
|
||||||
|
.program-page {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reveal ──────────────────────────────────────── */
|
||||||
|
[data-reveal] {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(22px);
|
||||||
|
transition:
|
||||||
|
opacity 600ms ease,
|
||||||
|
transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
[data-reveal].visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-reveal] { opacity: 1; transform: none; transition: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hero ─────────────────────────────────────────── */
|
||||||
|
.program-hero {
|
||||||
|
position: relative;
|
||||||
|
padding: clamp(2rem, 4svh, 3.5rem) 1.5rem clamp(2.5rem, 5svh, 4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 70% 60% at 15% 20%, rgba(8, 52, 183, 0.15), transparent),
|
||||||
|
radial-gradient(ellipse 50% 70% at 85% 80%, rgba(73, 121, 255, 0.1), transparent),
|
||||||
|
linear-gradient(160deg, #edf2ff 0%, #f0f5ff 40%, #e8f0fe 100%);
|
||||||
|
border-bottom-left-radius: 1.5rem;
|
||||||
|
border-bottom-right-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-image .hero-inner {
|
||||||
|
grid-template-columns: 0.95fr 1.05fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-text {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #52637a;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 150ms;
|
||||||
|
}
|
||||||
|
.back-link:hover { color: var(--brand); }
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4rem 0.85rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(8, 52, 183, 0.1);
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
font-size: clamp(2.2rem, 4.5vw, 3.6rem);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: #1b2740;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
max-width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lead {
|
||||||
|
font-size: clamp(0.97rem, 1.4vw, 1.1rem);
|
||||||
|
color: #4a5c73;
|
||||||
|
line-height: 1.7;
|
||||||
|
max-width: 52ch;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual {
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
box-shadow: 0 20px 50px rgba(8, 52, 183, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-visual img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Contenido + Stats ────────────────────────────── */
|
||||||
|
.content-section {
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2fr 0.8fr;
|
||||||
|
gap: 3rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-col p {
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Stats ────────────────────────────────────────── */
|
||||||
|
.stats-col {
|
||||||
|
position: sticky;
|
||||||
|
top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 1.25rem 1.1rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid rgba(29, 39, 56, 0.07);
|
||||||
|
box-shadow: 0 8px 28px rgba(8, 52, 183, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-valor {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
font-size: clamp(1.5rem, 2.4vw, 2.1rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
color: var(--brand);
|
||||||
|
line-height: 1;
|
||||||
|
transform-origin: left center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
color: #5b6a82;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Galería ──────────────────────────────────────── */
|
||||||
|
.gallery-section {
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 400ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover img {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Apoyo ────────────────────────────────────────── */
|
||||||
|
.support-section {
|
||||||
|
width: min(100%, 1120px);
|
||||||
|
margin: 0 auto 4rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 2.25rem 2.25rem;
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
background: linear-gradient(135deg, #101b33 0%, #15223c 100%);
|
||||||
|
box-shadow: 0 20px 50px rgba(17, 27, 51, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-kicker {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #7b96c8;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-text {
|
||||||
|
color: #c7d4e8;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
margin: 0 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.8rem 1.25rem;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-secondary:hover { transform: translateY(-2px); }
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(180deg, #2a5ae3 0%, var(--brand) 100%);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 20px rgba(8, 52, 183, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
color: #d0daf0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-deco {
|
||||||
|
position: relative;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deco-ring {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.deco-ring-1 { inset: 0; }
|
||||||
|
.deco-ring-2 { inset: 12px; border-color: rgba(255,255,255,0.07); }
|
||||||
|
|
||||||
|
.deco-icon {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 2.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Responsive ───────────────────────────────────── */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.has-image .hero-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.hero-visual {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
.content-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.stats-col {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.support-deco { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.hero-title { font-size: 2rem; }
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card { padding: 1rem 0.9rem; }
|
||||||
|
|
||||||
|
.support-card {
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-col p { font-size: 0.95rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── Reveal scroll ──────────────────────────────────────────────
|
||||||
|
const initReveal = () => {
|
||||||
|
const items = document.querySelectorAll('[data-reveal]');
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
items.forEach(el => el.classList.add('visible'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const siblings = [...(entry.target.parentElement?.children ?? [])];
|
||||||
|
const order = siblings.indexOf(entry.target);
|
||||||
|
const delay = order * 60;
|
||||||
|
setTimeout(() => entry.target.classList.add('visible'), delay);
|
||||||
|
io.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.08, rootMargin: '0px 0px -4% 0px' }
|
||||||
|
);
|
||||||
|
|
||||||
|
items.forEach(el => io.observe(el));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Contador de estadísticas ──────────────────────────────────
|
||||||
|
function parseValor(raw: string): { prefix: string; num: number; decimals: number; useDot: boolean; suffix: string } | null {
|
||||||
|
const m = raw.trim().match(/^([+\-]?)(\d[\d.,]*)(.*)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
|
||||||
|
const prefix = m[1];
|
||||||
|
const numStr = m[2];
|
||||||
|
const suffix = m[3];
|
||||||
|
|
||||||
|
const useDot = /\.\d{3}$/.test(numStr) || /\.\d{3}[.,]/.test(numStr);
|
||||||
|
const clean = numStr.replace(/\./g, '').replace(/,/g, '.');
|
||||||
|
const num = parseFloat(clean);
|
||||||
|
if (isNaN(num)) return null;
|
||||||
|
|
||||||
|
const decimals = (clean.split('.')[1] ?? '').length;
|
||||||
|
return { prefix, num, decimals, useDot, suffix };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNum(n: number, parsed: ReturnType<typeof parseValor>): string {
|
||||||
|
if (!parsed) return String(n);
|
||||||
|
const { prefix, decimals, useDot, suffix } = parsed;
|
||||||
|
let s: string;
|
||||||
|
|
||||||
|
if (decimals > 0) {
|
||||||
|
s = n.toFixed(decimals);
|
||||||
|
} else {
|
||||||
|
s = String(Math.round(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useDot && Math.round(n) >= 1000) {
|
||||||
|
s = Math.round(n).toLocaleString('de-DE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + s + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(t: number) { return 1 - Math.pow(1 - t, 3); }
|
||||||
|
|
||||||
|
function animateStat(el: Element) {
|
||||||
|
const raw = (el as HTMLElement).dataset.valor ?? el.textContent ?? '';
|
||||||
|
const parsed = parseValor(raw);
|
||||||
|
if (!parsed || parsed.num === 0) return;
|
||||||
|
|
||||||
|
const duration = 1400;
|
||||||
|
const start = performance.now();
|
||||||
|
const target = parsed.num;
|
||||||
|
|
||||||
|
el.textContent = formatNum(0, parsed);
|
||||||
|
|
||||||
|
function tick(now: number) {
|
||||||
|
const progress = Math.min((now - start) / duration, 1);
|
||||||
|
const eased = easeOutCubic(progress);
|
||||||
|
el.textContent = formatNum(eased * target, parsed);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
} else {
|
||||||
|
el.textContent = formatNum(target, parsed);
|
||||||
|
(el as HTMLElement).style.transition = 'transform 120ms ease-out';
|
||||||
|
(el as HTMLElement).style.transform = 'scale(1.1)';
|
||||||
|
setTimeout(() => {
|
||||||
|
(el as HTMLElement).style.transition = 'transform 380ms cubic-bezier(0.34, 1.56, 0.64, 1)';
|
||||||
|
(el as HTMLElement).style.transform = 'scale(1)';
|
||||||
|
}, 130);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initCounters = () => {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
|
||||||
|
const statCards = document.querySelectorAll('.stat-card');
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const valorEl = entry.target.querySelector('.stat-valor');
|
||||||
|
if (valorEl) animateStat(valorEl);
|
||||||
|
io.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.4 }
|
||||||
|
);
|
||||||
|
|
||||||
|
statCards.forEach(card => io.observe(card));
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
|
initReveal();
|
||||||
|
initCounters();
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
35
src/content.config.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const programas = defineCollection({
|
||||||
|
type: 'data',
|
||||||
|
schema: z.object({
|
||||||
|
nombre: z.string(),
|
||||||
|
estado: z.string(),
|
||||||
|
bajada: z.string(),
|
||||||
|
detalle: z.string(),
|
||||||
|
icono: z.string().optional().default('HeartHandshake'),
|
||||||
|
orden: z.number(),
|
||||||
|
tienePagina: z.boolean(),
|
||||||
|
externalHref: z.string().optional().default(''),
|
||||||
|
kicker: z.string().optional().default(''),
|
||||||
|
titulo: z.string().optional().default(''),
|
||||||
|
lead: z.string().optional().default(''),
|
||||||
|
parrafos: z.array(z.string()).optional().default([]),
|
||||||
|
stats: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
valor: z.string(),
|
||||||
|
etiqueta: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
textoApoyo: z.string().optional().default(''),
|
||||||
|
imagen: z.string().optional().default(''),
|
||||||
|
galeria: z.array(z.string()).optional().default([]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
programas,
|
||||||
|
};
|
||||||
51
src/content/programas/canasta-familiar.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
nombre: Canasta Familiar
|
||||||
|
estado: Activo
|
||||||
|
bajada: Apoyo alimentario y productos esenciales
|
||||||
|
detalle: >-
|
||||||
|
Apoyo directo con alimentos, frutas, verduras e insumos esenciales para sostener la
|
||||||
|
mesa y el hogar.
|
||||||
|
icono: Package
|
||||||
|
orden: 2
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Canasta Familiar
|
||||||
|
titulo: Apoyo directo para sostener la mesa y el hogar de muchas familias.
|
||||||
|
lead: >-
|
||||||
|
El Programa Canasta Familiar de Renacer entrega apoyo directo a familias que enfrentan
|
||||||
|
dificultades económicas, a través de alimentos y productos esenciales para el hogar.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Las canastas incluyen mercadería, frutas, verduras, artículos de higiene personal y
|
||||||
|
productos de aseo. En algunos casos también se incorporan elementos específicos,
|
||||||
|
como pañales, según las necesidades de cada familia.
|
||||||
|
- >-
|
||||||
|
Las entregas se realizan tanto a domicilio como con opción de retiro, adaptándose a
|
||||||
|
cada situación. Este programa se desarrolla desde el año 2005, acompañando
|
||||||
|
principalmente a familias vinculadas a niños, niñas y jóvenes que participan en los
|
||||||
|
espacios y talleres de Renacer.
|
||||||
|
- >-
|
||||||
|
En el contexto actual, donde el costo de la vida ha aumentado de forma sostenida,
|
||||||
|
muchas familias enfrentan dificultades para cubrir necesidades básicas. A través de
|
||||||
|
este programa, buscamos ser un apoyo concreto que alivie, en parte, esa carga
|
||||||
|
diaria.
|
||||||
|
- >-
|
||||||
|
Este trabajo es posible gracias a la colaboración de personas, aportes solidarios y
|
||||||
|
el apoyo del Ministerio Cristiano Renacer en Cristo. Más allá de la entrega de
|
||||||
|
alimentos, buscamos acompañar con respeto y cercanía la realidad que muchas familias
|
||||||
|
viven hoy.
|
||||||
|
stats:
|
||||||
|
- valor: '2005'
|
||||||
|
etiqueta: año desde el que se desarrolla este programa
|
||||||
|
- valor: Semanal
|
||||||
|
etiqueta: frecuencia actual de entregas a familias
|
||||||
|
- valor: Hogar
|
||||||
|
etiqueta: mercadería, frutas, verduras, higiene y aseo
|
||||||
|
- valor: Flexible
|
||||||
|
etiqueta: entrega a domicilio o con opción de retiro
|
||||||
|
textoApoyo: >-
|
||||||
|
Puedes colaborar de distintas maneras: aportando económicamente, difundiendo la
|
||||||
|
iniciativa o coordinando apoyo directo para familias que hoy necesitan un alivio real
|
||||||
|
en sus gastos básicos.
|
||||||
|
imagen: /uploads/programas/canasta-familiar/imagen-principal.jpg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/canasta-familiar/entrega-canasta.jpeg
|
||||||
48
src/content/programas/cena-navidena.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
nombre: Cena Navideña
|
||||||
|
estado: Anual
|
||||||
|
bajada: Encuentro y compañía en Noche Buena
|
||||||
|
detalle: >-
|
||||||
|
Cada 24 de diciembre compartimos una cena especial con personas y familias que viven
|
||||||
|
esta fecha con dificultad o soledad.
|
||||||
|
icono: UtensilsCrossed
|
||||||
|
orden: 5
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Cena Navideña
|
||||||
|
titulo: Una noche de encuentro, compañía y celebración compartida.
|
||||||
|
lead: >-
|
||||||
|
La Cena Navideña de Renacer se realiza cada 24 de diciembre y está pensada
|
||||||
|
especialmente para personas y familias que atraviesan dificultades económicas o que
|
||||||
|
viven la Navidad en soledad.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Sabemos que para muchas personas esta fecha puede ser compleja: a veces no alcanza
|
||||||
|
el dinero para una cena especial, y en otros casos, la falta de compañía hace que la
|
||||||
|
celebración pierda su sentido. Por eso abrimos este espacio como una oportunidad
|
||||||
|
para compartir, acompañar y vivir una noche distinta.
|
||||||
|
- >-
|
||||||
|
Desde el año 2006, este programa ha sido parte de nuestra comunidad, creciendo
|
||||||
|
gracias al apoyo de voluntarios y colaboradores. En su versión más reciente, durante
|
||||||
|
la Navidad 2025, se lograron entregar 100 cenas navideñas.
|
||||||
|
- >-
|
||||||
|
La cena consiste en un menú de tres tiempos preparado gracias a las colaboraciones
|
||||||
|
de personas que aportan con alimentos, recursos o tiempo. Además de la atención
|
||||||
|
presencial, el programa contempla la entrega a domicilio para personas con
|
||||||
|
enfermedades, movilidad reducida o adultos mayores.
|
||||||
|
- >-
|
||||||
|
Más que una cena, este programa busca generar un espacio donde las personas puedan
|
||||||
|
sentirse acompañadas, valoradas y parte de una comunidad en una fecha significativa.
|
||||||
|
stats:
|
||||||
|
- valor: 24 dic
|
||||||
|
etiqueta: fecha en que se realiza cada año
|
||||||
|
- valor: '100'
|
||||||
|
etiqueta: cenas entregadas en Navidad 2025
|
||||||
|
- valor: '2006'
|
||||||
|
etiqueta: año desde el que forma parte de Renacer
|
||||||
|
- valor: 3 tiempos
|
||||||
|
etiqueta: estructura del menú compartido
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas colaborar con este programa, puedes hacerlo a través de aportes,
|
||||||
|
voluntariado o difusión para que más personas puedan vivir una Noche Buena acompañada.
|
||||||
|
imagen: ''
|
||||||
|
galeria: []
|
||||||
51
src/content/programas/comedor-solidario.yaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
nombre: Comedor Solidario
|
||||||
|
estado: Activo
|
||||||
|
bajada: Almuerzos calientes y acompañamiento
|
||||||
|
detalle: >-
|
||||||
|
Almuerzos calientes, cercanía y apoyo concreto para personas y familias en situación
|
||||||
|
de vulnerabilidad en Quillota.
|
||||||
|
icono: UtensilsCrossed
|
||||||
|
orden: 1
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Comedor Solidario
|
||||||
|
titulo: Almuerzos calientes, cercanía y apoyo concreto en Quillota.
|
||||||
|
lead: >-
|
||||||
|
El Programa Comedor Solidario de Renacer entrega almuerzos calientes durante la semana
|
||||||
|
a personas y familias que atraviesan situaciones complejas, como cesantía,
|
||||||
|
dificultades de movilidad, discapacidad o falta de acceso regular a alimentación.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Actualmente, el programa se desarrolla en más de 10 sectores de la comuna, acercando
|
||||||
|
alimento preparado a quienes lo necesitan en su propio entorno. También llegamos a
|
||||||
|
personas mayores y a quienes se encuentran en situación de calle.
|
||||||
|
- >-
|
||||||
|
Durante el verano de 2026, el comedor entregó almuerzos calientes a niños y niñas
|
||||||
|
durante el período de vacaciones, considerando que en muchos casos no cuentan con la
|
||||||
|
presencia permanente de sus padres en el hogar. Esta iniciativa fue un apoyo
|
||||||
|
concreto tanto en la alimentación diaria como en la economía de las familias.
|
||||||
|
- >-
|
||||||
|
En la actualidad se preparan y distribuyen alrededor de 60 almuerzos, tres veces por
|
||||||
|
semana. Este trabajo comenzó el año 2017, inicialmente enfocado en acompañar a
|
||||||
|
personas migrantes, especialmente de la comunidad haitiana, y con el tiempo se
|
||||||
|
adaptó a las nuevas necesidades del territorio.
|
||||||
|
- >-
|
||||||
|
Más allá de la comida, este espacio busca transmitir dignidad, cercanía y
|
||||||
|
acompañamiento. Nuestro trabajo se inspira en valores cristianos, pero el apoyo se
|
||||||
|
entrega con respeto a cada persona, sin distinción de creencias.
|
||||||
|
stats:
|
||||||
|
- valor: '+10'
|
||||||
|
etiqueta: sectores de la comuna alcanzados
|
||||||
|
- valor: '60'
|
||||||
|
etiqueta: almuerzos preparados y distribuidos actualmente
|
||||||
|
- valor: '3'
|
||||||
|
etiqueta: jornadas de entrega cada semana
|
||||||
|
- valor: '2017'
|
||||||
|
etiqueta: año en que comenzó este trabajo territorial
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más o ser parte de este programa, puedes hacerlo a través de
|
||||||
|
nuestros canales. Toda colaboración permite que este apoyo continúe llegando a quienes
|
||||||
|
lo necesitan.
|
||||||
|
imagen: /uploads/programas/comedor-solidario/imagen1.jpg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/comedor-solidario/comedor-accion.jpeg
|
||||||
55
src/content/programas/fiestas-patrias.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
nombre: Fiestas Patrias
|
||||||
|
estado: Anual
|
||||||
|
bajada: Tradición, juego y comunidad
|
||||||
|
detalle: >-
|
||||||
|
Desde 2004, una celebración familiar con más de 20 juegos tradicionales, libre de
|
||||||
|
alcohol y orientada al encuentro comunitario.
|
||||||
|
icono: Users
|
||||||
|
orden: 10
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Fiestas Patrias
|
||||||
|
titulo: 'Tradición, juego y comunidad: una celebración familiar y libre de alcohol.'
|
||||||
|
lead: >-
|
||||||
|
Desde el año 2004, Renacer celebra Fiestas Patrias como una instancia orientada a
|
||||||
|
fortalecer la identidad, las tradiciones y el sentido de pertenencia en la comunidad,
|
||||||
|
ofreciendo un espacio familiar, seguro y libre de alcohol para niños, niñas y sus
|
||||||
|
familias.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
En los últimos años, muchas expresiones culturales propias del país — juegos
|
||||||
|
tradicionales, comida típica y símbolos patrios — han ido perdiendo presencia en la
|
||||||
|
vida cotidiana de niños y niñas. Frente a esto, Renacer promueve un espacio
|
||||||
|
participativo donde las familias puedan reencontrarse con estas prácticas.
|
||||||
|
- >-
|
||||||
|
La actividad ofrece más de 20 juegos, juguetes y competencias tradicionales: palo
|
||||||
|
encebado, tiro al blanco, la rana, trompos, emboque, carreras de saco, carrera de
|
||||||
|
tres pies y la carrera del huevo con cuchara, además de concursos, payas y bailes
|
||||||
|
típicos donde niños y familias participan activamente.
|
||||||
|
- >-
|
||||||
|
Durante la jornada se comparte en torno a la comida con preparaciones como helados,
|
||||||
|
choripanes y bebidas, generando un espacio de convivencia y comunidad. Renacer se
|
||||||
|
propone como alternativa a las celebraciones donde predomina el consumo de alcohol,
|
||||||
|
poniendo en el centro el encuentro, el juego y el vínculo familiar.
|
||||||
|
- >-
|
||||||
|
Esta actividad se sostiene gracias al apoyo de voluntarios que participan en la
|
||||||
|
implementación de los stands, acompañan a los niños, enseñan los juegos y promueven
|
||||||
|
la participación. Las colaboraciones también permiten costear premios, materiales y
|
||||||
|
alimentos.
|
||||||
|
stats:
|
||||||
|
- valor: '2004'
|
||||||
|
etiqueta: año desde el que se realiza esta celebración comunitaria
|
||||||
|
- valor: '+20'
|
||||||
|
etiqueta: juegos y competencias tradicionales disponibles
|
||||||
|
- valor: Alcohol free
|
||||||
|
etiqueta: espacio familiar y seguro sin consumo de alcohol
|
||||||
|
- valor: Voluntarios
|
||||||
|
etiqueta: participan activamente en los stands y actividades
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas ser parte de esta experiencia como voluntario en la organización,
|
||||||
|
acompañamiento en los juegos, o colaborar con premios y materiales, puedes
|
||||||
|
contactarnos a través de nuestros canales.
|
||||||
|
imagen: /uploads/programas/fiestas-patrias/imagen1.jpeg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/fiestas-patrias/imagen2.jpeg
|
||||||
|
- /uploads/programas/fiestas-patrias/imagen3.jpeg
|
||||||
54
src/content/programas/germinando-suenos.yaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
nombre: Germinando Sueños
|
||||||
|
estado: Activo
|
||||||
|
bajada: Invernaderos y agricultura sustentable
|
||||||
|
detalle: >-
|
||||||
|
Dos invernaderos que cultivan hortalizas y aromáticas para el Comedor Solidario, con
|
||||||
|
educación ambiental y siembra comunitaria.
|
||||||
|
icono: Sprout
|
||||||
|
orden: 8
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Germinando Sueños
|
||||||
|
titulo: Agricultura sustentable que alimenta la comunidad y cuida el entorno.
|
||||||
|
lead: >-
|
||||||
|
"Germinando Sueños" es una iniciativa de Renacer que promueve la agricultura
|
||||||
|
sustentable como una forma concreta de aportar a la alimentación y bienestar de la
|
||||||
|
comunidad. Los alimentos cultivados se destinan principalmente al Comedor Solidario,
|
||||||
|
mejorando la calidad, frescura y valor nutricional de las comidas entregadas.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
El programa cuenta con dos espacios complementarios: el Invernadero 1 (Almaciguera),
|
||||||
|
donde se siembran las semillas y se desarrollan las plántulas, y el Invernadero 2
|
||||||
|
(Huerta), donde las plantas son trasplantadas para su crecimiento y cosecha. Ambos
|
||||||
|
espacios están conectados por un sistema de sanitización que resguarda la salud de
|
||||||
|
los cultivos.
|
||||||
|
- >-
|
||||||
|
Trabajamos bajo principios de agricultura sustentable: reducción del uso de
|
||||||
|
químicos, promoción de la biodiversidad, uso eficiente del agua y recolección de
|
||||||
|
aguas lluvias para riego.
|
||||||
|
- >-
|
||||||
|
Entre los cultivos se encuentran hojas verdes como espinaca, rúcula, mizuna y
|
||||||
|
acelga; variedades de lechuga; hierbas aromáticas como cilantro, perejil y albahaca;
|
||||||
|
hortalizas como zanahoria, zapallo italiano, coliflor y pimentón; y otros cultivos
|
||||||
|
como arvejas, ají, choclo y tomate cherry.
|
||||||
|
- >-
|
||||||
|
El programa también tiene un fuerte componente educativo. Niños, niñas y comunidad
|
||||||
|
pueden visitar los invernaderos y aprender sobre siembra, repique, riego y cosecha,
|
||||||
|
promoviendo la conciencia ecológica y el valor del autosustento.
|
||||||
|
stats:
|
||||||
|
- valor: '2'
|
||||||
|
etiqueta: 'invernaderos complementarios: almaciguera y huerta'
|
||||||
|
- valor: '+10'
|
||||||
|
etiqueta: variedades de hortalizas y aromáticas cultivadas
|
||||||
|
- valor: Comedor
|
||||||
|
etiqueta: principal destino de los alimentos cosechados
|
||||||
|
- valor: Educativo
|
||||||
|
etiqueta: visitas y aprendizaje para niños y comunidad
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más o ser parte de esta iniciativa, puedes hacerlo a través de
|
||||||
|
nuestros canales. Toda colaboración permite que este programa continúe creciendo y
|
||||||
|
aportando a la comunidad.
|
||||||
|
imagen: /uploads/programas/germinando-suenos/imagen1.jpeg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/germinando-suenos/imagen2.jpeg
|
||||||
|
- /uploads/programas/germinando-suenos/imagen3.jpeg
|
||||||
53
src/content/programas/juegotetra.yaml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
nombre: Sala JuegoTetra
|
||||||
|
estado: Activo
|
||||||
|
bajada: Reciclaje y educación ambiental
|
||||||
|
detalle: >-
|
||||||
|
Un espacio construido en un 90% con materiales reutilizados, incluyendo más de 5.000
|
||||||
|
cajas tetra pak, que enseña las 3R de forma práctica.
|
||||||
|
icono: Puzzle
|
||||||
|
orden: 9
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Sala JuegoTetra
|
||||||
|
titulo: Un espacio construido con reciclaje que transforma la forma de entender los residuos.
|
||||||
|
lead: >-
|
||||||
|
La Sala JuegoTetra es un espacio educativo y experiencial que enseña, de manera
|
||||||
|
práctica, el valor del reciclaje, la reutilización y el cuidado del medio ambiente a
|
||||||
|
través de una experiencia guiada donde niños, niñas y adultos participan activamente.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
La experiencia comienza abordando la problemática de la contaminación y el impacto
|
||||||
|
de los residuos en el entorno, para luego introducir los conceptos de las 3R:
|
||||||
|
reducir, reutilizar y reciclar. A partir de ello, los participantes conocen procesos
|
||||||
|
reales de transformación de materiales y elaboran sus propios objetos utilizando
|
||||||
|
desechos.
|
||||||
|
- >-
|
||||||
|
La sala está construida en un 90% con materiales reutilizados: más de 5.000 cajas
|
||||||
|
tetra pak rellenas con residuos plásticos, cerámica reciclada en el suelo, botellas
|
||||||
|
reutilizadas, techos de envases y mobiliario fabricado a partir de desechos. La sala
|
||||||
|
no solo habla del reciclaje: lo materializa en cada uno de sus elementos.
|
||||||
|
- >-
|
||||||
|
Los participantes interactúan con el espacio, manipulan materiales, juegan y se
|
||||||
|
llevan un objeto creado durante la experiencia, reforzando el aprendizaje desde lo
|
||||||
|
vivencial.
|
||||||
|
- >-
|
||||||
|
La Sala JuegoTetra recibe visitas de establecimientos educacionales y estudiantes
|
||||||
|
universitarios, especialmente de carreras de pedagogía, quienes encuentran aquí una
|
||||||
|
instancia de aprendizaje práctico y observación de metodologías educativas
|
||||||
|
aplicadas.
|
||||||
|
stats:
|
||||||
|
- valor: '+5.000'
|
||||||
|
etiqueta: cajas tetra pak utilizadas en la construcción
|
||||||
|
- valor: 90%
|
||||||
|
etiqueta: de la sala construida con materiales reutilizados
|
||||||
|
- valor: 3R
|
||||||
|
etiqueta: reducir, reutilizar y reciclar como eje educativo
|
||||||
|
- valor: Escuelas
|
||||||
|
etiqueta: establecimientos y universidades que visitan el espacio
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas visitar la Sala JuegoTetra o coordinar una experiencia para tu
|
||||||
|
establecimiento educacional, puedes contactarnos a través de nuestros canales.
|
||||||
|
imagen: /uploads/programas/juegotetra/imagen1.jpg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/juegotetra/imagen2.jpg
|
||||||
|
- /uploads/programas/juegotetra/imagen3.jpg
|
||||||
39
src/content/programas/mi-rincon-seguro.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
nombre: Mi Rincón Seguro
|
||||||
|
estado: En planificación
|
||||||
|
bajada: Salas de Contención TEA
|
||||||
|
detalle: >-
|
||||||
|
Proyecto en alianza con SECPLAN para habilitar espacios adaptados sensorialmente para
|
||||||
|
niños y niñas del espectro autista.
|
||||||
|
icono: HandHelping
|
||||||
|
orden: 11
|
||||||
|
tienePagina: false
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Mi Rincón Seguro
|
||||||
|
titulo: Espacios sensorialmente adaptados para niños y niñas del espectro autista.
|
||||||
|
lead: >-
|
||||||
|
Mi Rincón Seguro es un proyecto en desarrollo que busca habilitar salas de contención
|
||||||
|
especialmente adaptadas para niños y niñas con Trastorno del Espectro Autista (TEA),
|
||||||
|
en alianza con SECPLAN y el municipio de Quillota.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Este proyecto surge de la necesidad concreta de contar con espacios diseñados
|
||||||
|
específicamente para acompañar a niños y niñas neurodivergentes en momentos de
|
||||||
|
desregulación sensorial o emocional.
|
||||||
|
- >-
|
||||||
|
Las salas de contención TEA están diseñadas con materiales, colores, texturas y
|
||||||
|
niveles de estimulación pensados para facilitar la autorregulación y el bienestar de
|
||||||
|
los niños y niñas.
|
||||||
|
stats:
|
||||||
|
- valor: TEA
|
||||||
|
etiqueta: diseño específico para el espectro autista
|
||||||
|
- valor: Sensorial
|
||||||
|
etiqueta: materiales y ambientes adaptados para autorregulación
|
||||||
|
- valor: SECPLAN
|
||||||
|
etiqueta: proyecto en alianza con planificación municipal
|
||||||
|
- valor: '2026'
|
||||||
|
etiqueta: año proyectado de implementación
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más sobre este proyecto o tienes interés en colaborar, contáctanos a
|
||||||
|
través de nuestros canales.
|
||||||
|
imagen: ''
|
||||||
|
galeria: []
|
||||||
48
src/content/programas/navidad-solidaria.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
nombre: Navidad Solidaria
|
||||||
|
estado: Temporada
|
||||||
|
bajada: Apadrinamiento y celebración comunitaria
|
||||||
|
detalle: >-
|
||||||
|
Una campaña de diciembre que acompaña a niños y niñas con regalos, dulces y una
|
||||||
|
experiencia significativa.
|
||||||
|
icono: Gift
|
||||||
|
orden: 4
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Navidad Solidaria
|
||||||
|
titulo: Una celebración comunitaria que acompaña a niños y niñas con esperanza y afecto.
|
||||||
|
lead: >-
|
||||||
|
Navidad Solidaria de Renacer se desarrolla durante diciembre para acompañar a niños y
|
||||||
|
niñas en una celebración significativa, entregando alegría y contención a través de
|
||||||
|
una experiencia comunitaria.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Está dirigida a niños y niñas desde 1 mes hasta los 15 años, pertenecientes a
|
||||||
|
familias vinculadas a los programas de Renacer.
|
||||||
|
- >-
|
||||||
|
A través de una campaña de apadrinamiento, personas de la comunidad pueden colaborar
|
||||||
|
entregando un regalo a un niño o niña, de forma presencial o a distancia. Cada
|
||||||
|
padrino o madrina recibe información básica de su ahijado o ahijada para que el
|
||||||
|
regalo tenga un sentido más personal.
|
||||||
|
- >-
|
||||||
|
Además del regalo, cada niño o niña recibe una bolsa de dulces y un presente
|
||||||
|
preparado por Renacer, buscando transmitir el sentido de la Navidad desde una mirada
|
||||||
|
de esperanza, amor y comunidad.
|
||||||
|
- >-
|
||||||
|
La entrega se realiza en una celebración organizada por Renacer, con actividades
|
||||||
|
recreativas, juegos y momentos compartidos. Más allá de lo material, este programa
|
||||||
|
busca generar una experiencia significativa y aliviar una carga económica adicional
|
||||||
|
para muchas familias.
|
||||||
|
stats:
|
||||||
|
- valor: '167'
|
||||||
|
etiqueta: niños y niñas acompañados en la celebración
|
||||||
|
- valor: '83'
|
||||||
|
etiqueta: madrinas participantes
|
||||||
|
- valor: '39'
|
||||||
|
etiqueta: padrinos participantes
|
||||||
|
- valor: Diciembre
|
||||||
|
etiqueta: mes en que se activa esta campaña comunitaria
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más o ser parte de esta iniciativa, puedes hacerlo a través de
|
||||||
|
nuestros canales y sumarte como padrino, madrina, colaborador o voluntario.
|
||||||
|
imagen: ''
|
||||||
|
galeria: []
|
||||||
52
src/content/programas/ropero-solidario.yaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
nombre: Ropero Solidario
|
||||||
|
estado: Activo
|
||||||
|
bajada: Vestimenta digna y economía circular
|
||||||
|
detalle: >-
|
||||||
|
Recibimos, seleccionamos y entregamos ropa en buen estado para responder con respeto a
|
||||||
|
necesidades reales.
|
||||||
|
icono: Shirt
|
||||||
|
orden: 3
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Ropero Solidario
|
||||||
|
titulo: Vestimenta digna, rescate textil y apoyo concreto para la comunidad.
|
||||||
|
lead: >-
|
||||||
|
El programa Ropero Solidario de Renacer apoya a familias que enfrentan dificultades
|
||||||
|
para acceder a vestimenta, entregando ropa en buen estado y artículos esenciales según
|
||||||
|
sus necesidades.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Este espacio funciona gracias a la donación de ropa por parte de la comunidad, la
|
||||||
|
cual es recibida, clasificada, seleccionada, lavada y organizada para luego ser
|
||||||
|
entregada de manera digna y respetuosa a quienes lo necesitan.
|
||||||
|
- >-
|
||||||
|
El programa se desarrolla durante todo el año, adaptándose a las distintas
|
||||||
|
temporadas. En invierno, se refuerza la entrega de abrigo para personas en situación
|
||||||
|
de calle, familias e inmigrantes, mientras que en verano se continúa apoyando con
|
||||||
|
vestimenta adecuada a la estación.
|
||||||
|
- >-
|
||||||
|
A través de este programa también promovemos una lógica de economía circular, donde
|
||||||
|
las familias no solo pueden recibir apoyo, sino también donar ropa que ya no
|
||||||
|
utilizan, generando una red de colaboración dentro de la misma comunidad y
|
||||||
|
fomentando la reutilización y el cuidado del entorno.
|
||||||
|
- >-
|
||||||
|
Más allá de la entrega de ropa, este espacio busca ser un apoyo concreto,
|
||||||
|
resguardando la dignidad de cada persona y ofreciendo un entorno de respeto y
|
||||||
|
cercanía.
|
||||||
|
stats:
|
||||||
|
- valor: Todo el año
|
||||||
|
etiqueta: funcionamiento continuo según temporada
|
||||||
|
- valor: Abrigo
|
||||||
|
etiqueta: refuerzo especial en invierno para calle, familias e inmigrantes
|
||||||
|
- valor: Circular
|
||||||
|
etiqueta: reutilización y rescate textil desde la comunidad
|
||||||
|
- valor: Digno
|
||||||
|
etiqueta: clasificación, lavado y entrega respetuosa
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más o ser parte de esta iniciativa, puedes hacerlo a través de
|
||||||
|
nuestros canales. Toda colaboración permite que este apoyo continúe llegando a quienes
|
||||||
|
lo necesitan.
|
||||||
|
imagen: /uploads/programas/ropero-solidario/imagen1.jpeg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/ropero-solidario/imagen2.jpeg
|
||||||
|
- /uploads/programas/ropero-solidario/imagen3.jpeg
|
||||||
48
src/content/programas/sala-del-juguete.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
nombre: Sala del Juguete
|
||||||
|
estado: Activo
|
||||||
|
bajada: Juego, infancia y reutilización
|
||||||
|
detalle: >-
|
||||||
|
Un espacio para rescatar el valor del juego y dar nueva vida a juguetes donados por la
|
||||||
|
comunidad.
|
||||||
|
icono: Puzzle
|
||||||
|
orden: 6
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Sala del Juguete
|
||||||
|
titulo: Un espacio para rescatar el juego, la imaginación y la infancia.
|
||||||
|
lead: >-
|
||||||
|
La Sala del Juguete de Renacer está pensada para devolver al juego el lugar que merece
|
||||||
|
en la infancia, ofreciendo a niños y niñas la oportunidad de explorar, imaginar y
|
||||||
|
compartir más allá de las pantallas.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Hoy sabemos que el juego no es solo una forma de entretención, sino una parte
|
||||||
|
fundamental del desarrollo. A través del juego, los niños fortalecen su creatividad,
|
||||||
|
habilidades sociales, expresión emocional y aprendizaje.
|
||||||
|
- >-
|
||||||
|
Este espacio busca recuperar ese tiempo de juego que muchas veces se ve limitado,
|
||||||
|
especialmente en contextos donde predominan las pantallas o donde no siempre existen
|
||||||
|
las condiciones para acceder a juguetes.
|
||||||
|
- >-
|
||||||
|
La Sala del Juguete funciona también como un espacio de economía circular, donde se
|
||||||
|
reciben donaciones de juguetes nuevos y usados en buen estado, los cuales son
|
||||||
|
seleccionados y puestos a disposición de los niños para su uso dentro del espacio o
|
||||||
|
para ser entregados en fechas especiales.
|
||||||
|
- >-
|
||||||
|
Más allá de los objetos, este programa promueve el juego, la reutilización, el
|
||||||
|
cuidado del entorno y la colaboración entre familias como parte del bienestar
|
||||||
|
comunitario.
|
||||||
|
stats:
|
||||||
|
- valor: '2.410'
|
||||||
|
etiqueta: juguetes disponibles para uso dentro del espacio
|
||||||
|
- valor: '+4.000'
|
||||||
|
etiqueta: juguetes entregados a niños y niñas, dándoles una segunda vida
|
||||||
|
- valor: Circular
|
||||||
|
etiqueta: revisión, clasificación y reutilización de cada juguete
|
||||||
|
- valor: Comunidad
|
||||||
|
etiqueta: apoyo a celebraciones y actividades durante el año
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas colaborar con la donación de juguetes o conocer más sobre este espacio,
|
||||||
|
puedes hacerlo a través de nuestros canales y campañas comunitarias.
|
||||||
|
imagen: ''
|
||||||
|
galeria: []
|
||||||
48
src/content/programas/tardes-recreativas.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
nombre: Tardes Recreativas
|
||||||
|
estado: Vacaciones
|
||||||
|
bajada: Cuidado y actividades en verano e invierno
|
||||||
|
detalle: >-
|
||||||
|
Jornadas seguras con recreación, alimentación y acompañamiento para niños y niñas
|
||||||
|
durante vacaciones.
|
||||||
|
icono: Users
|
||||||
|
orden: 7
|
||||||
|
tienePagina: true
|
||||||
|
externalHref: ''
|
||||||
|
kicker: Tardes Recreativas
|
||||||
|
titulo: Vacaciones con cuidado, juego y actividades seguras para niños y niñas.
|
||||||
|
lead: >-
|
||||||
|
El programa Tardes Recreativas de Renacer ofrece un espacio seguro, acompañado y
|
||||||
|
entretenido para niños y niñas durante las vacaciones de verano e invierno.
|
||||||
|
parrafos:
|
||||||
|
- >-
|
||||||
|
Está pensado especialmente para familias donde los adultos deben trabajar en estos
|
||||||
|
períodos y no siempre cuentan con redes de apoyo para el cuidado de sus hijos.
|
||||||
|
- >-
|
||||||
|
Durante estas jornadas, los niños participan en actividades recreativas, artísticas
|
||||||
|
y de convivencia en un entorno supervisado por monitores, donde pueden compartir,
|
||||||
|
aprender y disfrutar su tiempo de manera segura.
|
||||||
|
- >-
|
||||||
|
Además, el programa incluye almuerzo y colación, asegurando que los niños cuenten
|
||||||
|
con una alimentación adecuada durante su permanencia. En verano se realizan juegos
|
||||||
|
con agua, actividades al aire libre y talleres de cocina; en invierno, cine, juegos
|
||||||
|
grupales, manualidades y momentos de relajación.
|
||||||
|
- >-
|
||||||
|
Más allá de las actividades, este espacio busca entregar cuidado, contención y un
|
||||||
|
entorno donde los niños se sientan acompañados, especialmente en períodos donde
|
||||||
|
pueden pasar más tiempo solos.
|
||||||
|
stats:
|
||||||
|
- valor: '60'
|
||||||
|
etiqueta: niños recibidos en el programa
|
||||||
|
- valor: Verano
|
||||||
|
etiqueta: juegos con agua, aire libre y talleres de cocina
|
||||||
|
- valor: Invierno
|
||||||
|
etiqueta: cine, manualidades y juegos en espacios cerrados
|
||||||
|
- valor: Cuidado
|
||||||
|
etiqueta: incluye almuerzo y colación durante la jornada
|
||||||
|
textoApoyo: >-
|
||||||
|
Si deseas conocer más o ser parte de esta iniciativa, puedes colaborar con recursos,
|
||||||
|
voluntariado o difusión para fortalecer estos espacios de cuidado.
|
||||||
|
imagen: /uploads/programas/tardes-recreativas/imagen1.jpeg
|
||||||
|
galeria:
|
||||||
|
- /uploads/programas/tardes-recreativas/imagen2.jpeg
|
||||||
|
- /uploads/programas/tardes-recreativas/imagen3.jpeg
|
||||||
99
src/lib/programs.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { createReader } from '@keystatic/core/reader';
|
||||||
|
import { imageSize } from 'image-size';
|
||||||
|
import keystaticConfig from '../../keystatic.config';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const reader = createReader(process.cwd(), keystaticConfig);
|
||||||
|
|
||||||
|
type RawProgram = NonNullable<Awaited<ReturnType<typeof reader.collections.programas.read>>>;
|
||||||
|
type ProgramImage = { src: string; width: number; height: number };
|
||||||
|
|
||||||
|
function normalizeName(nombre: RawProgram['nombre']) {
|
||||||
|
return typeof nombre === 'string' ? nombre : nombre.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExternalHref(program: RawProgram) {
|
||||||
|
return typeof program.externalHref === 'string' ? program.externalHref.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageMetadata(src: string): ProgramImage | null {
|
||||||
|
if (!src.startsWith('/uploads/')) return null;
|
||||||
|
|
||||||
|
const filePath = join(process.cwd(), 'public', src.replace(/^\/+/, ''));
|
||||||
|
if (!existsSync(filePath)) return null;
|
||||||
|
|
||||||
|
const dimensions = imageSize(readFileSync(filePath));
|
||||||
|
if (!dimensions.width || !dimensions.height) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
src,
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImages(program: RawProgram): ProgramImage[] {
|
||||||
|
const primary =
|
||||||
|
typeof program.imagen === 'string' && program.imagen.trim().length > 0
|
||||||
|
? [program.imagen.trim()]
|
||||||
|
: [];
|
||||||
|
const gallery = Array.isArray(program.galeria)
|
||||||
|
? program.galeria.filter(
|
||||||
|
(image): image is string => typeof image === 'string' && image.trim().length > 0
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [...primary, ...gallery]
|
||||||
|
.map((image) => getImageMetadata(image))
|
||||||
|
.filter((image): image is ProgramImage => Boolean(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIcon(program: RawProgram) {
|
||||||
|
return typeof program.icono === 'string' && program.icono.trim().length > 0
|
||||||
|
? program.icono.trim()
|
||||||
|
: 'HeartHandshake';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPrograms() {
|
||||||
|
const slugs = await reader.collections.programas.list();
|
||||||
|
const programs = await Promise.all(
|
||||||
|
slugs.map(async (slug) => {
|
||||||
|
const program = await reader.collections.programas.read(slug);
|
||||||
|
if (!program) return null;
|
||||||
|
|
||||||
|
const dynamicHref = `/programas/${slug}/`;
|
||||||
|
const externalHref = getExternalHref(program);
|
||||||
|
const href = program.tienePagina ? externalHref || dynamicHref : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
...program,
|
||||||
|
icono: getIcon(program),
|
||||||
|
nombreNormalizado: normalizeName(program.nombre),
|
||||||
|
dynamicHref,
|
||||||
|
href,
|
||||||
|
imagenes: getImages(program),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return programs
|
||||||
|
.filter((program): program is NonNullable<typeof program> => Boolean(program))
|
||||||
|
.sort((a, b) => (a.orden ?? 99) - (b.orden ?? 99));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProgramBySlug(slug: string) {
|
||||||
|
const program = await reader.collections.programas.read(slug);
|
||||||
|
if (!program) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
...program,
|
||||||
|
icono: getIcon(program),
|
||||||
|
nombreNormalizado: normalizeName(program.nombre),
|
||||||
|
dynamicHref: `/programas/${slug}/`,
|
||||||
|
externalHrefNormalizado: getExternalHref(program),
|
||||||
|
imagenes: getImages(program),
|
||||||
|
};
|
||||||
|
}
|
||||||
35
src/middleware.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineMiddleware } from 'astro:middleware';
|
||||||
|
|
||||||
|
const SESSION_COOKIE = 'renacer_admin';
|
||||||
|
|
||||||
|
function getExpectedToken() {
|
||||||
|
const user = import.meta.env.ADMIN_USER ?? 'admin';
|
||||||
|
const pass = import.meta.env.ADMIN_PASS ?? '';
|
||||||
|
return `${user}::${pass}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onRequest = defineMiddleware(async (context, next) => {
|
||||||
|
const { url, cookies } = context;
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
// Solo protege rutas del panel admin y su API
|
||||||
|
// Excluye /admin-login y /admin-logout (páginas públicas de auth)
|
||||||
|
const isProtected =
|
||||||
|
path.startsWith('/keystatic') ||
|
||||||
|
path.startsWith('/api/keystatic') ||
|
||||||
|
(path.startsWith('/admin') && !path.startsWith('/admin-'));
|
||||||
|
|
||||||
|
if (!isProtected) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = cookies.get(SESSION_COOKIE);
|
||||||
|
if (session?.value === getExpectedToken()) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sin sesión válida → redirige al login
|
||||||
|
const loginUrl = new URL('/admin-login', url.origin);
|
||||||
|
loginUrl.searchParams.set('redirect', path);
|
||||||
|
return context.redirect(loginUrl.toString());
|
||||||
|
});
|
||||||
185
src/pages/admin-login.astro
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
const SESSION_COOKIE = 'renacer_admin';
|
||||||
|
const adminUser = import.meta.env.ADMIN_USER ?? 'admin';
|
||||||
|
const adminPass = import.meta.env.ADMIN_PASS ?? '';
|
||||||
|
|
||||||
|
const redirectTo = Astro.url.searchParams.get('redirect') ?? '/admin/';
|
||||||
|
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
if (Astro.request.method === 'POST') {
|
||||||
|
const form = await Astro.request.formData();
|
||||||
|
const user = form.get('user')?.toString() ?? '';
|
||||||
|
const pass = form.get('pass')?.toString() ?? '';
|
||||||
|
|
||||||
|
if (user === adminUser && pass === adminPass) {
|
||||||
|
Astro.cookies.set(SESSION_COOKIE, `${user}::${pass}`, {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: import.meta.env.PROD,
|
||||||
|
sameSite: 'strict',
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 días
|
||||||
|
});
|
||||||
|
return Astro.redirect(redirectTo);
|
||||||
|
} else {
|
||||||
|
error = 'Usuario o contraseña incorrectos.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Admin | Renacer</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Sora:wght@600;700;800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #eef4ff 0%, #f5f8ff 60%, #e8f0fe 100%);
|
||||||
|
min-height: 100svh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
box-shadow: 0 24px 64px rgba(8, 52, 183, 0.12);
|
||||||
|
padding: 2.75rem 2.5rem;
|
||||||
|
width: min(100%, 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #062b96;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo span { color: #7f95ad; }
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #53637b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #1d2738;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border: 1.5px solid rgba(29, 39, 56, 0.16);
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.96rem;
|
||||||
|
color: #1d2738;
|
||||||
|
background: #f9fbff;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 150ms ease;
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #0834b7;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
background: #fff0f0;
|
||||||
|
border: 1px solid #fca5a5;
|
||||||
|
color: #b91c1c;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(180deg, #1b49d6 0%, #0834b7 100%);
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 14px 28px rgba(8, 52, 183, 0.22);
|
||||||
|
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 18px 32px rgba(8, 52, 183, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
color: #53637b;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back:hover { color: #0834b7; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<a href="/" class="logo">Renacer<span>.</span></a>
|
||||||
|
<p class="subtitle">Panel de administración</p>
|
||||||
|
|
||||||
|
{error && <p class="error-msg">{error}</p>}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<label for="user">Usuario</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="user"
|
||||||
|
name="user"
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="pass">Contraseña</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="pass"
|
||||||
|
name="pass"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button type="submit">Ingresar</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="/" class="back">← Volver al sitio</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
src/pages/admin-logout.astro
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
Astro.cookies.delete('renacer_admin', { path: '/' });
|
||||||
|
return Astro.redirect('/admin-login');
|
||||||
|
---
|
||||||
131
src/pages/admin/index.astro
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
// Protegido por middleware
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Panel Admin | Renacer</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Sora:wght@600;700;800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #eef4ff 0%, #f5f8ff 100%);
|
||||||
|
min-height: 100svh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
h1, h2, h3 { font-family: 'Sora', sans-serif; letter-spacing: -.04em; }
|
||||||
|
|
||||||
|
.shell { width: min(100%, 680px); }
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-family: 'Sora', sans-serif; font-size: 2rem; font-weight: 800;
|
||||||
|
color: #062b96; letter-spacing: -.05em; text-decoration: none;
|
||||||
|
}
|
||||||
|
.logo span { color: #7f95ad; }
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
font-size: .84rem; font-weight: 700; color: #53627b;
|
||||||
|
text-decoration: none; padding: .5rem .9rem;
|
||||||
|
border: 1.5px solid rgba(29,39,56,.14); border-radius: .7rem;
|
||||||
|
transition: color 140ms, border-color 140ms;
|
||||||
|
}
|
||||||
|
.logout:hover { color: #0834b7; border-color: rgba(8,52,183,.3); }
|
||||||
|
|
||||||
|
h2 { font-size: 1.5rem; color: #1d2738; margin-bottom: .4rem; }
|
||||||
|
.sub { color: #53627b; font-size: .92rem; margin-bottom: 1.75rem; }
|
||||||
|
|
||||||
|
.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||||
|
|
||||||
|
@media (max-width: 520px) { .cards { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 12px 36px rgba(8,52,183,.08);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 160ms, box-shadow 160ms;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 18px 44px rgba(8,52,183,.13);
|
||||||
|
border-color: rgba(8,52,183,.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 2rem; margin-bottom: .85rem;
|
||||||
|
width: 3rem; height: 3rem;
|
||||||
|
display: grid; place-items: center;
|
||||||
|
background: #eef2ff; border-radius: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 { font-size: 1rem; color: #1b2740; margin-bottom: .35rem; }
|
||||||
|
.card p { font-size: .83rem; color: #53627b; line-height: 1.55; flex: 1; }
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
margin-top: 1rem; font-size: .82rem; font-weight: 800; color: #0834b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-primary { background: linear-gradient(135deg, #0834b7, #062b96); }
|
||||||
|
.card-primary h3 { color: white; }
|
||||||
|
.card-primary p { color: #c7d4f8; }
|
||||||
|
.card-primary .card-arrow { color: #a5b4fc; }
|
||||||
|
.card-primary .card-icon { background: rgba(255,255,255,.15); }
|
||||||
|
|
||||||
|
.site-link {
|
||||||
|
display: block; text-align: center; margin-top: 1.5rem;
|
||||||
|
color: #53627b; font-size: .84rem; text-decoration: none;
|
||||||
|
transition: color 140ms;
|
||||||
|
}
|
||||||
|
.site-link:hover { color: #0834b7; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<div class="header">
|
||||||
|
<a href="/" class="logo">Renacer<span>.</span></a>
|
||||||
|
<a href="/admin-logout" class="logout">Cerrar sesión</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Panel de administración</h2>
|
||||||
|
<p class="sub">¿Qué quieres hacer hoy?</p>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<a href="/admin/programa-builder" class="card card-primary">
|
||||||
|
<div class="card-icon">✏️</div>
|
||||||
|
<h3>Constructor de programa</h3>
|
||||||
|
<p>Crea una nueva página de programa con preview en vivo y genera el YAML automáticamente.</p>
|
||||||
|
<span class="card-arrow">Abrir →</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/keystatic/" class="card">
|
||||||
|
<div class="card-icon">🗂️</div>
|
||||||
|
<h3>Editar programas (CMS)</h3>
|
||||||
|
<p>Gestiona todos los programas existentes, edita contenido y sube imágenes.</p>
|
||||||
|
<span class="card-arrow">Abrir →</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="site-link">← Ver el sitio público</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
931
src/pages/admin/programa-builder.astro
Normal file
@@ -0,0 +1,931 @@
|
|||||||
|
---
|
||||||
|
// Protegido por middleware — requiere login
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Constructor de Programa | Admin Renacer</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&family=Sora:wght@600;700;800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
/* ── Reset & base ─────────────────────────────── */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--brand: #0834b7;
|
||||||
|
--brand-dark: #062b96;
|
||||||
|
--ink: #1d2738;
|
||||||
|
--muted: #53627b;
|
||||||
|
--line: rgba(29,39,56,.12);
|
||||||
|
--bg: #f5f8ff;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
min-height: 100svh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3,h4 { font-family: 'Sora', sans-serif; letter-spacing: -.04em; }
|
||||||
|
|
||||||
|
/* ── Topbar ───────────────────────────────────── */
|
||||||
|
.topbar {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; z-index: 50;
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: .9rem 1.5rem;
|
||||||
|
background: rgba(255,255,255,.95);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left { display: flex; align-items: center; gap: 1.2rem; }
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
font-family: 'Sora', sans-serif; font-size: 1.4rem; font-weight: 800;
|
||||||
|
color: var(--brand-dark); letter-spacing: -.05em; text-decoration: none;
|
||||||
|
}
|
||||||
|
.brand span { color: #7f95ad; }
|
||||||
|
|
||||||
|
.page-title { font-size: .88rem; font-weight: 700; color: var(--muted); }
|
||||||
|
|
||||||
|
.topbar-actions { display: flex; gap: .7rem; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; gap: .4rem;
|
||||||
|
padding: .6rem 1.1rem; border-radius: .75rem;
|
||||||
|
font-family: inherit; font-size: .88rem; font-weight: 800;
|
||||||
|
cursor: pointer; border: none; transition: transform 140ms, box-shadow 140ms;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn:hover { transform: translateY(-1px); }
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent; border: 1.5px solid var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(180deg,#1b49d6,var(--brand));
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 20px rgba(8,52,183,.22);
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(180deg,#16a34a,#15803d);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 20px rgba(22,163,74,.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main layout ──────────────────────────────── */
|
||||||
|
.builder {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 420px;
|
||||||
|
height: calc(100svh - 56px);
|
||||||
|
margin-top: 56px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Preview pane ─────────────────────────────── */
|
||||||
|
.preview-pane {
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #eef3ff;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-label {
|
||||||
|
font-size: .75rem; font-weight: 800; letter-spacing: .12em;
|
||||||
|
text-transform: uppercase; color: var(--brand);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex; align-items: center; gap: .5rem;
|
||||||
|
}
|
||||||
|
.preview-label::before {
|
||||||
|
content: ''; display: block; width: 8px; height: 8px;
|
||||||
|
border-radius: 50%; background: #22c55e;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,100% { opacity: 1; } 50% { opacity: .4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Preview: replica visual de ProgramInfoPage ── */
|
||||||
|
.prev-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 50px rgba(8,52,183,.1);
|
||||||
|
font-size: .88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-hero {
|
||||||
|
padding: 2rem 1.75rem 1.75rem;
|
||||||
|
background: linear-gradient(135deg,#f0f4ff 0%,#e8eeff 100%);
|
||||||
|
border-bottom: 1px solid rgba(8,52,183,.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-back {
|
||||||
|
font-size: .8rem; font-weight: 700; color: var(--brand);
|
||||||
|
margin-bottom: .75rem; display: flex; align-items: center; gap: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-kicker {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .35rem .8rem;
|
||||||
|
background: rgba(8,52,183,.1);
|
||||||
|
color: var(--brand);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: .75rem; font-weight: 800;
|
||||||
|
letter-spacing: .08em; text-transform: uppercase;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-titulo {
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
font-size: 1.35rem; font-weight: 800;
|
||||||
|
line-height: 1.15; letter-spacing: -.04em;
|
||||||
|
color: #1b2740;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-lead {
|
||||||
|
color: var(--muted); line-height: 1.65;
|
||||||
|
font-size: .85rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-info-pill {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
padding: .4rem .85rem;
|
||||||
|
background: rgba(8,52,183,.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: .75rem; font-weight: 700;
|
||||||
|
color: var(--brand);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-body { padding: 1.5rem 1.75rem; }
|
||||||
|
|
||||||
|
.prev-parrafos { display: flex; flex-direction: column; gap: .9rem; }
|
||||||
|
|
||||||
|
.prev-parrafo {
|
||||||
|
color: #4a5568; line-height: 1.7; font-size: .84rem;
|
||||||
|
min-height: 1.2rem;
|
||||||
|
}
|
||||||
|
.prev-parrafo.empty {
|
||||||
|
height: 2.5rem;
|
||||||
|
background: #f3f4f6; border-radius: .5rem;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: #9ca3af; font-style: italic; font-size: .78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-stats {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr;
|
||||||
|
gap: .75rem; margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-stat {
|
||||||
|
padding: .9rem;
|
||||||
|
background: linear-gradient(135deg,#f8faff,#f0f4ff);
|
||||||
|
border-radius: .85rem;
|
||||||
|
border: 1px solid rgba(8,52,183,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-stat-valor {
|
||||||
|
font-family: 'Sora', sans-serif;
|
||||||
|
font-size: 1.15rem; font-weight: 800;
|
||||||
|
color: var(--brand); letter-spacing: -.04em;
|
||||||
|
min-height: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-stat-etiqueta {
|
||||||
|
font-size: .75rem; color: var(--muted);
|
||||||
|
line-height: 1.4; margin-top: .25rem;
|
||||||
|
min-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-support {
|
||||||
|
margin-top: 1.25rem; padding: 1.1rem 1.25rem;
|
||||||
|
background: linear-gradient(135deg,#101b33,#15223c);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-support-text {
|
||||||
|
color: #c7d1df; font-size: .82rem; line-height: 1.6;
|
||||||
|
min-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-support-btns {
|
||||||
|
display: flex; gap: .6rem; margin-top: .85rem; flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn-fake {
|
||||||
|
padding: .5rem .9rem; border-radius: .6rem;
|
||||||
|
font-size: .78rem; font-weight: 800;
|
||||||
|
}
|
||||||
|
.prev-btn-primary { background: var(--brand); color: white; }
|
||||||
|
.prev-btn-secondary {
|
||||||
|
border: 1px solid rgba(255,255,255,.2); color: #d0d8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder text */
|
||||||
|
.placeholder { color: #cbd5e1; font-style: italic; }
|
||||||
|
|
||||||
|
/* ── Form pane ────────────────────────────────── */
|
||||||
|
.form-pane {
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.form-section:last-child { border-bottom: none; margin-bottom: 0; }
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: .72rem; font-weight: 800;
|
||||||
|
letter-spacing: .12em; text-transform: uppercase;
|
||||||
|
color: var(--brand); margin-bottom: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field { margin-bottom: .85rem; }
|
||||||
|
.field:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block; font-size: .8rem; font-weight: 700;
|
||||||
|
color: var(--ink); margin-bottom: .35rem;
|
||||||
|
}
|
||||||
|
label .hint {
|
||||||
|
font-weight: 400; color: var(--muted); font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="number"], select, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: .65rem .85rem;
|
||||||
|
border: 1.5px solid var(--line);
|
||||||
|
border-radius: .7rem;
|
||||||
|
font-family: inherit; font-size: .88rem;
|
||||||
|
color: var(--ink); background: #fafbff;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 140ms, background 140ms;
|
||||||
|
}
|
||||||
|
input:focus, select:focus, textarea:focus {
|
||||||
|
border-color: var(--brand); background: white;
|
||||||
|
}
|
||||||
|
textarea { resize: vertical; min-height: 70px; line-height: 1.55; }
|
||||||
|
|
||||||
|
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
||||||
|
.row-3 { display: grid; grid-template-columns: 1fr 1fr 80px; gap: .6rem; }
|
||||||
|
|
||||||
|
/* Stats grid */
|
||||||
|
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: .75rem;
|
||||||
|
background: #f8faff;
|
||||||
|
border: 1px solid rgba(8,52,183,.08);
|
||||||
|
border-radius: .75rem;
|
||||||
|
}
|
||||||
|
.stat-item label { color: var(--brand); font-size: .72rem; }
|
||||||
|
.stat-item input { font-size: .82rem; padding: .5rem .7rem; }
|
||||||
|
.stat-item input:first-of-type { margin-bottom: .4rem; }
|
||||||
|
|
||||||
|
/* Párrafos dinámicos */
|
||||||
|
.parrafos-list { display: flex; flex-direction: column; gap: .6rem; }
|
||||||
|
|
||||||
|
.parrafo-item { position: relative; }
|
||||||
|
.parrafo-item textarea { padding-right: 2.2rem; }
|
||||||
|
.parrafo-remove {
|
||||||
|
position: absolute; top: .5rem; right: .5rem;
|
||||||
|
width: 1.5rem; height: 1.5rem;
|
||||||
|
background: #fee2e2; color: #dc2626;
|
||||||
|
border: none; border-radius: .4rem;
|
||||||
|
font-size: .9rem; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: background 140ms;
|
||||||
|
}
|
||||||
|
.parrafo-remove:hover { background: #fca5a5; }
|
||||||
|
|
||||||
|
.btn-add-parrafo {
|
||||||
|
width: 100%; padding: .6rem;
|
||||||
|
border: 1.5px dashed rgba(8,52,183,.25);
|
||||||
|
border-radius: .7rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--brand); font-family: inherit;
|
||||||
|
font-size: .82rem; font-weight: 700;
|
||||||
|
cursor: pointer; transition: background 140ms;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
.btn-add-parrafo:hover { background: rgba(8,52,183,.05); }
|
||||||
|
|
||||||
|
/* ── Toast ────────────────────────────────────── */
|
||||||
|
#toast {
|
||||||
|
position: fixed; bottom: 1.5rem; left: 50%; transform: translateX(-50%);
|
||||||
|
padding: .75rem 1.4rem;
|
||||||
|
border-radius: .85rem;
|
||||||
|
font-weight: 700; font-size: .88rem;
|
||||||
|
opacity: 0; transition: opacity 300ms;
|
||||||
|
pointer-events: none; z-index: 100;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
#toast.show { opacity: 1; }
|
||||||
|
#toast.success { background: #16a34a; color: white; }
|
||||||
|
#toast.error { background: #dc2626; color: white; }
|
||||||
|
#toast.info { background: var(--brand); color: white; }
|
||||||
|
|
||||||
|
/* ── YAML Modal ───────────────────────────────── */
|
||||||
|
#yaml-modal {
|
||||||
|
border: none; border-radius: 1.25rem;
|
||||||
|
box-shadow: 0 30px 80px rgba(0,0,0,.25);
|
||||||
|
padding: 0; width: min(96vw, 680px);
|
||||||
|
max-height: 80svh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#yaml-modal::backdrop { background: rgba(0,0,0,.45); }
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 1.1rem 1.4rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #fafbff;
|
||||||
|
}
|
||||||
|
.modal-header h3 { font-size: 1rem; color: var(--ink); }
|
||||||
|
|
||||||
|
.modal-actions { display: flex; gap: .6rem; }
|
||||||
|
|
||||||
|
.modal-body { padding: 1.25rem; overflow-y: auto; max-height: 55svh; }
|
||||||
|
|
||||||
|
#yaml-output {
|
||||||
|
background: #0f172a; color: #e2e8f0;
|
||||||
|
padding: 1.25rem; border-radius: .85rem;
|
||||||
|
font-family: 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: .8rem; line-height: 1.65;
|
||||||
|
white-space: pre; overflow-x: auto;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
font-size: 1.3rem; color: var(--muted);
|
||||||
|
line-height: 1; padding: .2rem;
|
||||||
|
transition: color 140ms;
|
||||||
|
}
|
||||||
|
.btn-close:hover { color: var(--ink); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!-- Topbar -->
|
||||||
|
<nav class="topbar">
|
||||||
|
<div class="topbar-left">
|
||||||
|
<a href="/" class="brand">Renacer<span>.</span></a>
|
||||||
|
<span class="page-title">Constructor de Programa</span>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<a href="/keystatic/" class="btn btn-ghost">Panel CMS</a>
|
||||||
|
<button class="btn btn-primary" id="btn-yaml" type="button">Ver YAML</button>
|
||||||
|
<button class="btn btn-success" id="btn-save" type="button">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Builder: Preview + Form -->
|
||||||
|
<div class="builder">
|
||||||
|
|
||||||
|
<!-- ── PREVIEW ──────────────────────────────── -->
|
||||||
|
<div class="preview-pane">
|
||||||
|
<div class="preview-label">Preview en vivo</div>
|
||||||
|
|
||||||
|
<div class="prev-card">
|
||||||
|
<!-- Hero -->
|
||||||
|
<div class="prev-hero">
|
||||||
|
<div class="prev-back">← Inicio</div>
|
||||||
|
<div class="prev-kicker" id="prev-kicker">
|
||||||
|
<span class="placeholder">Kicker</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="prev-titulo" id="prev-titulo">
|
||||||
|
<span class="placeholder">Título del programa aparece aquí…</span>
|
||||||
|
</h1>
|
||||||
|
<p class="prev-lead" id="prev-lead">
|
||||||
|
<span class="placeholder">El párrafo introductorio aparecerá aquí mientras escribes.</span>
|
||||||
|
</p>
|
||||||
|
<div class="prev-info-pill" id="prev-estado">Activo</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cuerpo -->
|
||||||
|
<div class="prev-body">
|
||||||
|
<!-- Párrafos -->
|
||||||
|
<div class="prev-parrafos" id="prev-parrafos">
|
||||||
|
<p class="prev-parrafo empty">Los párrafos aparecerán aquí…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="prev-stats" id="prev-stats">
|
||||||
|
<div class="prev-stat">
|
||||||
|
<div class="prev-stat-valor"><span class="placeholder">—</span></div>
|
||||||
|
<div class="prev-stat-etiqueta"><span class="placeholder">Descripción</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="prev-stat">
|
||||||
|
<div class="prev-stat-valor"><span class="placeholder">—</span></div>
|
||||||
|
<div class="prev-stat-etiqueta"><span class="placeholder">Descripción</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="prev-stat">
|
||||||
|
<div class="prev-stat-valor"><span class="placeholder">—</span></div>
|
||||||
|
<div class="prev-stat-etiqueta"><span class="placeholder">Descripción</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="prev-stat">
|
||||||
|
<div class="prev-stat-valor"><span class="placeholder">—</span></div>
|
||||||
|
<div class="prev-stat-etiqueta"><span class="placeholder">Descripción</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apoyo -->
|
||||||
|
<div class="prev-support">
|
||||||
|
<p class="prev-support-text" id="prev-apoyo">
|
||||||
|
<span class="placeholder">El texto de apoyo aparecerá aquí.</span>
|
||||||
|
</p>
|
||||||
|
<div class="prev-support-btns">
|
||||||
|
<span class="prev-btn-fake prev-btn-primary">Colaborar mensualmente</span>
|
||||||
|
<span class="prev-btn-fake prev-btn-secondary">contacto@familiarenacer.cl</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── FORMULARIO ─────────────────────────────── -->
|
||||||
|
<div class="form-pane">
|
||||||
|
|
||||||
|
<!-- Sección 1: Tarjeta landing -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Tarjeta en el landing</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-nombre">Nombre del programa</label>
|
||||||
|
<input type="text" id="f-nombre" placeholder="Ej: Ropero Solidario" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field row-2">
|
||||||
|
<div>
|
||||||
|
<label for="f-estado">Estado</label>
|
||||||
|
<select id="f-estado">
|
||||||
|
<option value="Activo">Activo</option>
|
||||||
|
<option value="Vacaciones">Vacaciones</option>
|
||||||
|
<option value="Anual">Anual</option>
|
||||||
|
<option value="Temporada">Temporada</option>
|
||||||
|
<option value="En planificación">En planificación</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="f-orden">Orden <span class="hint">(posición)</span></label>
|
||||||
|
<input type="number" id="f-orden" value="99" min="1" max="999" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-detalle">Descripción breve <span class="hint">para la tarjeta</span></label>
|
||||||
|
<textarea id="f-detalle" rows="2" placeholder="Texto corto que aparece en la tarjeta del landing."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-icono">Icono de la tarjeta</label>
|
||||||
|
<select id="f-icono">
|
||||||
|
<option value="HeartHandshake">Manos y apoyo</option>
|
||||||
|
<option value="UtensilsCrossed">Alimentación</option>
|
||||||
|
<option value="Package">Canasta / apoyo</option>
|
||||||
|
<option value="Shirt">Vestimenta</option>
|
||||||
|
<option value="Sprout">Naturaleza</option>
|
||||||
|
<option value="Gift">Regalo / campaña</option>
|
||||||
|
<option value="Puzzle">Juego e infancia</option>
|
||||||
|
<option value="Users">Comunidad</option>
|
||||||
|
<option value="HandHelping">Inclusión</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-bajada">Resumen corto <span class="hint">opcional, ya no visible en la tarjeta</span></label>
|
||||||
|
<input type="text" id="f-bajada" placeholder="Campo editorial secundario" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 2: Página -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Configuración de página</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-kicker">Kicker <span class="hint">nombre corto sobre el título</span></label>
|
||||||
|
<input type="text" id="f-kicker" placeholder="Ej: Ropero Solidario" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-titulo">Título</label>
|
||||||
|
<input type="text" id="f-titulo" placeholder="Título largo y descriptivo de la página" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-lead">Párrafo introductorio</label>
|
||||||
|
<textarea id="f-lead" rows="3" placeholder="Párrafo destacado que aparece bajo el título…"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 3: Párrafos -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Párrafos del cuerpo</div>
|
||||||
|
|
||||||
|
<div class="parrafos-list" id="parrafos-list">
|
||||||
|
<div class="parrafo-item">
|
||||||
|
<textarea class="parrafo-input" rows="3" placeholder="Primer párrafo…"></textarea>
|
||||||
|
<button class="parrafo-remove" type="button" title="Eliminar">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-add-parrafo" id="btn-add-parrafo" type="button">+ Agregar párrafo</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 4: Estadísticas -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Estadísticas (4 datos clave)</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<label>Stat 1 — Valor</label>
|
||||||
|
<input type="text" class="stat-valor" data-idx="0" placeholder="167" />
|
||||||
|
<label>Descripción</label>
|
||||||
|
<input type="text" class="stat-etiqueta" data-idx="0" placeholder="niños acompañados" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<label>Stat 2 — Valor</label>
|
||||||
|
<input type="text" class="stat-valor" data-idx="1" placeholder="Todo el año" />
|
||||||
|
<label>Descripción</label>
|
||||||
|
<input type="text" class="stat-etiqueta" data-idx="1" placeholder="funcionamiento continuo" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<label>Stat 3 — Valor</label>
|
||||||
|
<input type="text" class="stat-valor" data-idx="2" placeholder="+20" />
|
||||||
|
<label>Descripción</label>
|
||||||
|
<input type="text" class="stat-etiqueta" data-idx="2" placeholder="actividades disponibles" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<label>Stat 4 — Valor</label>
|
||||||
|
<input type="text" class="stat-valor" data-idx="3" placeholder="Comunidad" />
|
||||||
|
<label>Descripción</label>
|
||||||
|
<input type="text" class="stat-etiqueta" data-idx="3" placeholder="impacto comunitario" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 5: Apoyo -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Texto de apoyo</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-apoyo">¿Cómo colaborar?</label>
|
||||||
|
<textarea id="f-apoyo" rows="3" placeholder="Si deseas conocer más o ser parte de esta iniciativa…"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sección 6: Avanzado -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-label">Opciones avanzadas</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="f-external">URL externa <span class="hint">dejar vacío para generar desde el slug</span></label>
|
||||||
|
<input type="text" id="f-external" placeholder="Ej: /ropero-solidario/" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="f-tiene-pagina" checked style="margin-right:.4rem" />
|
||||||
|
¿Tiene página propia?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /form-pane -->
|
||||||
|
</div><!-- /builder -->
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div id="toast"></div>
|
||||||
|
|
||||||
|
<!-- YAML Modal -->
|
||||||
|
<dialog id="yaml-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>YAML generado</h3>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" id="btn-copy">Copiar</button>
|
||||||
|
<button class="btn-close" id="btn-close-modal" title="Cerrar">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre id="yaml-output"></pre>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── Utilidades ────────────────────────────────────────────────
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
function setEl(id, text) {
|
||||||
|
const el = $(id);
|
||||||
|
if (!el) return;
|
||||||
|
if (text && text.trim()) {
|
||||||
|
el.textContent = text;
|
||||||
|
el.querySelectorAll('.placeholder').forEach(p => p.remove());
|
||||||
|
} else {
|
||||||
|
el.innerHTML = '<span class="placeholder">' + (el.dataset.ph || '') + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toast(msg, type = 'info') {
|
||||||
|
const t = $('toast');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'show ' + type;
|
||||||
|
clearTimeout(t._t);
|
||||||
|
t._t = setTimeout(() => t.className = '', 2800);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preview live ──────────────────────────────────────────────
|
||||||
|
function updatePreview() {
|
||||||
|
const nombre = $('f-nombre').value;
|
||||||
|
const kicker = $('f-kicker').value || nombre;
|
||||||
|
const titulo = $('f-titulo').value;
|
||||||
|
const lead = $('f-lead').value;
|
||||||
|
const estado = $('f-estado').value;
|
||||||
|
const apoyo = $('f-apoyo').value;
|
||||||
|
|
||||||
|
// Kicker
|
||||||
|
const kickEl = $('prev-kicker');
|
||||||
|
kickEl.textContent = kicker || '';
|
||||||
|
if (!kicker) kickEl.innerHTML = '<span class="placeholder">Kicker</span>';
|
||||||
|
|
||||||
|
// Título
|
||||||
|
const titEl = $('prev-titulo');
|
||||||
|
titEl.textContent = titulo || '';
|
||||||
|
if (!titulo) titEl.innerHTML = '<span class="placeholder">Título del programa aparece aquí…</span>';
|
||||||
|
|
||||||
|
// Lead
|
||||||
|
const leadEl = $('prev-lead');
|
||||||
|
leadEl.textContent = lead || '';
|
||||||
|
if (!lead) leadEl.innerHTML = '<span class="placeholder">El párrafo introductorio aparecerá aquí mientras escribes.</span>';
|
||||||
|
|
||||||
|
// Estado pill
|
||||||
|
$('prev-estado').textContent = estado;
|
||||||
|
|
||||||
|
// Párrafos
|
||||||
|
const parrafoInputs = document.querySelectorAll('.parrafo-input');
|
||||||
|
const parrafoContainer = $('prev-parrafos');
|
||||||
|
parrafoContainer.innerHTML = '';
|
||||||
|
let anyParagraph = false;
|
||||||
|
parrafoInputs.forEach(inp => {
|
||||||
|
const val = inp.value.trim();
|
||||||
|
if (val) {
|
||||||
|
anyParagraph = true;
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'prev-parrafo';
|
||||||
|
p.textContent = val;
|
||||||
|
parrafoContainer.appendChild(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!anyParagraph) {
|
||||||
|
parrafoContainer.innerHTML = '<p class="prev-parrafo empty">Los párrafos aparecerán aquí…</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const statValues = document.querySelectorAll('.stat-valor');
|
||||||
|
const statEtiqs = document.querySelectorAll('.stat-etiqueta');
|
||||||
|
const statsContainer = $('prev-stats');
|
||||||
|
statsContainer.innerHTML = '';
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const v = statValues[i]?.value || '';
|
||||||
|
const e = statEtiqs[i]?.value || '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'prev-stat';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="prev-stat-valor">${v || '<span class="placeholder">—</span>'}</div>
|
||||||
|
<div class="prev-stat-etiqueta">${e || '<span class="placeholder">Descripción</span>'}</div>`;
|
||||||
|
statsContainer.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apoyo
|
||||||
|
const apoyoEl = $('prev-apoyo');
|
||||||
|
apoyoEl.textContent = apoyo || '';
|
||||||
|
if (!apoyo) apoyoEl.innerHTML = '<span class="placeholder">El texto de apoyo aparecerá aquí.</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Eventos de formulario ─────────────────────────────────────
|
||||||
|
document.addEventListener('input', updatePreview);
|
||||||
|
document.addEventListener('change', updatePreview);
|
||||||
|
|
||||||
|
// ── Párrafos dinámicos ────────────────────────────────────────
|
||||||
|
function removeParagraph(btn) {
|
||||||
|
const list = $('parrafos-list');
|
||||||
|
if (list.querySelectorAll('.parrafo-item').length > 1) {
|
||||||
|
btn.closest('.parrafo-item').remove();
|
||||||
|
updatePreview();
|
||||||
|
} else {
|
||||||
|
btn.closest('.parrafo-item').querySelector('textarea').value = '';
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addParagraph() {
|
||||||
|
const list = $('parrafos-list');
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'parrafo-item';
|
||||||
|
const n = list.querySelectorAll('.parrafo-item').length + 1;
|
||||||
|
item.innerHTML = `
|
||||||
|
<textarea class="parrafo-input" rows="3" placeholder="Párrafo ${n}…"></textarea>
|
||||||
|
<button class="parrafo-remove" type="button" title="Eliminar" onclick="removeParagraph(this)">×</button>`;
|
||||||
|
list.appendChild(item);
|
||||||
|
item.querySelector('textarea').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('btn-add-parrafo').addEventListener('click', addParagraph);
|
||||||
|
|
||||||
|
// Wire up initial remove button
|
||||||
|
document.querySelector('.parrafo-remove')
|
||||||
|
.addEventListener('click', function() { removeParagraph(this); });
|
||||||
|
|
||||||
|
// ── Recolectar datos del formulario ───────────────────────────
|
||||||
|
function collectData() {
|
||||||
|
const parrafos = [...document.querySelectorAll('.parrafo-input')]
|
||||||
|
.map(t => t.value.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
const stats = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
stats.push({
|
||||||
|
valor: document.querySelector(`.stat-valor[data-idx="${i}"]`)?.value.trim() || '',
|
||||||
|
etiqueta: document.querySelector(`.stat-etiqueta[data-idx="${i}"]`)?.value.trim() || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nombre: $('f-nombre').value.trim(),
|
||||||
|
estado: $('f-estado').value,
|
||||||
|
bajada: $('f-bajada').value.trim(),
|
||||||
|
detalle: $('f-detalle').value.trim(),
|
||||||
|
icono: $('f-icono').value,
|
||||||
|
orden: parseInt($('f-orden').value) || 99,
|
||||||
|
tienePagina: $('f-tiene-pagina').checked,
|
||||||
|
externalHref: $('f-external').value.trim(),
|
||||||
|
kicker: $('f-kicker').value.trim(),
|
||||||
|
titulo: $('f-titulo').value.trim(),
|
||||||
|
lead: $('f-lead').value.trim(),
|
||||||
|
parrafos,
|
||||||
|
stats,
|
||||||
|
textoApoyo: $('f-apoyo').value.trim(),
|
||||||
|
imagen: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generador YAML ────────────────────────────────────────────
|
||||||
|
function toSlug(nombre) {
|
||||||
|
return nombre.toLowerCase()
|
||||||
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlStr(v) {
|
||||||
|
if (v == null || v === '') return "''";
|
||||||
|
v = String(v);
|
||||||
|
// Necesita comillas si tiene caracteres especiales
|
||||||
|
if (/[:#\[\]{}&*!|>'"?%@`]/.test(v) || /^[-+~\s]/.test(v) || /\s$/.test(v)) {
|
||||||
|
return '"' + v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlMultiline(v, indent = '') {
|
||||||
|
if (!v) return "''";
|
||||||
|
const lines = v.split('\n');
|
||||||
|
if (lines.length === 1 && v.length <= 72 && !/[:#\[\]{}&*!|>'"?%@`]/.test(v)) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return '>-\n' + lines.map(l => indent + ' ' + l).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateYAMLString(d) {
|
||||||
|
const slug = toSlug(d.nombre || 'nuevo-programa');
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
lines.push(`nombre: ${yamlStr(d.nombre)}`);
|
||||||
|
lines.push(`estado: ${yamlStr(d.estado)}`);
|
||||||
|
lines.push(`bajada: ${yamlStr(d.bajada)}`);
|
||||||
|
lines.push(`detalle: ${yamlStr(d.detalle)}`);
|
||||||
|
lines.push(`icono: ${yamlStr(d.icono || 'HeartHandshake')}`);
|
||||||
|
lines.push(`orden: ${d.orden}`);
|
||||||
|
lines.push(`tienePagina: ${d.tienePagina}`);
|
||||||
|
lines.push(`externalHref: ${yamlStr(d.externalHref || '')}`);
|
||||||
|
lines.push(`kicker: ${yamlStr(d.kicker || d.nombre)}`);
|
||||||
|
lines.push(`titulo: ${yamlStr(d.titulo)}`);
|
||||||
|
|
||||||
|
const leadYaml = yamlMultiline(d.lead);
|
||||||
|
if (leadYaml.includes('\n')) {
|
||||||
|
lines.push(`lead: ${leadYaml}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`lead: ${yamlStr(d.lead)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`parrafos:`);
|
||||||
|
for (const p of d.parrafos) {
|
||||||
|
const ml = yamlMultiline(p, ' ');
|
||||||
|
if (ml.includes('\n')) {
|
||||||
|
lines.push(` - ${ml}`);
|
||||||
|
} else {
|
||||||
|
lines.push(` - ${yamlStr(p)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!d.parrafos.length) lines.push(` []`);
|
||||||
|
|
||||||
|
lines.push(`stats:`);
|
||||||
|
for (const s of d.stats) {
|
||||||
|
lines.push(` - valor: ${yamlStr(s.valor)}`);
|
||||||
|
lines.push(` etiqueta: ${yamlStr(s.etiqueta)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apoyoYaml = yamlMultiline(d.textoApoyo);
|
||||||
|
if (apoyoYaml.includes('\n')) {
|
||||||
|
lines.push(`textoApoyo: ${apoyoYaml}`);
|
||||||
|
} else {
|
||||||
|
lines.push(`textoApoyo: ${yamlStr(d.textoApoyo)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`imagen: ''`);
|
||||||
|
|
||||||
|
return { yaml: lines.join('\n'), slug };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Modal YAML ────────────────────────────────────────────────
|
||||||
|
$('btn-yaml').addEventListener('click', () => {
|
||||||
|
const d = collectData();
|
||||||
|
const { yaml } = generateYAMLString(d);
|
||||||
|
$('yaml-output').textContent = yaml;
|
||||||
|
$('yaml-modal').showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-close-modal').addEventListener('click', () => {
|
||||||
|
$('yaml-modal').close();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('yaml-modal').addEventListener('click', (e) => {
|
||||||
|
if (e.target === $('yaml-modal')) $('yaml-modal').close();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-copy').addEventListener('click', () => {
|
||||||
|
const text = $('yaml-output').textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
toast('YAML copiado al portapapeles', 'success');
|
||||||
|
$('yaml-modal').close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Guardar programa ──────────────────────────────────────────
|
||||||
|
$('btn-save').addEventListener('click', async () => {
|
||||||
|
const d = collectData();
|
||||||
|
if (!d.nombre) {
|
||||||
|
toast('El nombre del programa es requerido', 'error');
|
||||||
|
$('f-nombre').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = $('btn-save');
|
||||||
|
btn.textContent = 'Guardando…';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/save-programa', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(d),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
toast(`✓ Guardado: src/content/programas/${json.slug}.yaml`, 'success');
|
||||||
|
} else {
|
||||||
|
toast('Error: ' + (json.error || 'desconocido'), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast('Error de conexión', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.textContent = 'Guardar';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init preview
|
||||||
|
updatePreview();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
86
src/pages/api/save-programa.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { writeFileSync, mkdirSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
|
||||||
|
const SESSION_COOKIE = 'renacer_admin';
|
||||||
|
|
||||||
|
function getExpectedToken() {
|
||||||
|
const user = import.meta.env.ADMIN_USER ?? 'admin';
|
||||||
|
const pass = import.meta.env.ADMIN_PASS ?? '';
|
||||||
|
return `${user}::${pass}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSlug(nombre: string): string {
|
||||||
|
return nombre
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request, cookies }) => {
|
||||||
|
// Verificar sesión
|
||||||
|
const session = cookies.get(SESSION_COOKIE);
|
||||||
|
if (session?.value !== getExpectedToken()) {
|
||||||
|
return new Response(JSON.stringify({ error: 'No autorizado' }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
data = await request.json();
|
||||||
|
} catch {
|
||||||
|
return new Response(JSON.stringify({ error: 'JSON inválido' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nombre = String(data.nombre ?? '').trim();
|
||||||
|
if (!nombre) {
|
||||||
|
return new Response(JSON.stringify({ error: 'El nombre es requerido' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = String(data.slug || '').trim() || toSlug(nombre);
|
||||||
|
const filePath = join(process.cwd(), 'src', 'content', 'programas', `${slug}.yaml`);
|
||||||
|
|
||||||
|
const content = {
|
||||||
|
nombre,
|
||||||
|
estado: data.estado ?? 'Activo',
|
||||||
|
bajada: data.bajada ?? '',
|
||||||
|
detalle: data.detalle ?? '',
|
||||||
|
icono: data.icono ?? 'HeartHandshake',
|
||||||
|
orden: Number(data.orden) || 99,
|
||||||
|
tienePagina: data.tienePagina !== false,
|
||||||
|
externalHref: data.externalHref ?? '',
|
||||||
|
kicker: data.kicker ?? nombre,
|
||||||
|
titulo: data.titulo ?? '',
|
||||||
|
lead: data.lead ?? '',
|
||||||
|
parrafos: Array.isArray(data.parrafos) ? data.parrafos.filter(Boolean) : [],
|
||||||
|
stats: Array.isArray(data.stats) ? data.stats : [],
|
||||||
|
textoApoyo: data.textoApoyo ?? '',
|
||||||
|
imagen: '',
|
||||||
|
galeria: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
mkdirSync(join(process.cwd(), 'src', 'content', 'programas'), { recursive: true });
|
||||||
|
writeFileSync(filePath, yaml.dump(content, { lineWidth: 80, forceQuotes: false }), 'utf8');
|
||||||
|
return new Response(JSON.stringify({ ok: true, slug, path: filePath }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return new Response(JSON.stringify({ error: String(err) }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
3
src/pages/canasta-familiar.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/canasta-familiar/', 301);
|
||||||
|
---
|
||||||
3
src/pages/cena-navidena.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/cena-navidena/', 301);
|
||||||
|
---
|
||||||
3
src/pages/comedor-solidario.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/comedor-solidario/', 301);
|
||||||
|
---
|
||||||
3
src/pages/fiestas-patrias.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/fiestas-patrias/', 301);
|
||||||
|
---
|
||||||
3
src/pages/germinando-suenos.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/germinando-suenos/', 301);
|
||||||
|
---
|
||||||
3
src/pages/juegotetra.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/juegotetra/', 301);
|
||||||
|
---
|
||||||
3
src/pages/navidad-solidaria.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/navidad-solidaria/', 301);
|
||||||
|
---
|
||||||
36
src/pages/programas/[slug].astro
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
import ProgramInfoPage from '../../components/ProgramInfoPage.astro';
|
||||||
|
import { getProgramBySlug } from '../../lib/programs';
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
if (!slug) return Astro.redirect('/');
|
||||||
|
|
||||||
|
const program = await getProgramBySlug(slug);
|
||||||
|
|
||||||
|
if (!program) return Astro.redirect('/');
|
||||||
|
if (!program.tienePagina) return Astro.redirect('/');
|
||||||
|
|
||||||
|
if (program.externalHrefNormalizado && program.externalHrefNormalizado !== program.dynamicHref) {
|
||||||
|
return Astro.redirect(program.externalHrefNormalizado, 301);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
title={`${program.nombreNormalizado} | Renacer`}
|
||||||
|
description={program.detalle}
|
||||||
|
>
|
||||||
|
<ProgramInfoPage
|
||||||
|
kicker={program.kicker ?? program.nombreNormalizado}
|
||||||
|
title={program.titulo ?? program.nombreNormalizado}
|
||||||
|
lead={program.lead ?? ''}
|
||||||
|
paragraphs={(program.parrafos ?? []) as string[]}
|
||||||
|
stats={
|
||||||
|
((program.stats ?? []) as { valor: string; etiqueta: string }[]).map(
|
||||||
|
(s) => ({ valor: s.valor, etiqueta: s.etiqueta })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
supportText={program.textoApoyo ?? ''}
|
||||||
|
images={program.imagenes}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
3
src/pages/ropero-solidario.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/ropero-solidario/', 301);
|
||||||
|
---
|
||||||
3
src/pages/sala-del-juguete.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/sala-del-juguete/', 301);
|
||||||
|
---
|
||||||
3
src/pages/tardes-recreativas.astro
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
return Astro.redirect('/programas/tardes-recreativas/', 301);
|
||||||
|
---
|
||||||