Add Keystatic CMS and refresh landing experience
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
|
||||
.idea/
|
||||
|
||||
# Claude context
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
import react from '@astrojs/react';
|
||||
import keystatic from '@keystatic/astro';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -8,4 +10,5 @@ export default defineConfig({
|
||||
adapter: node({
|
||||
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": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"astro": "^5.16.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^5.0.3",
|
||||
"@keystatic/astro": "^5.0.6",
|
||||
"@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/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,299 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import canastaImagenPrincipal from '../assets/canasta-familiar/imagen-principal.jpg';
|
||||
import canastaEntrega from '../assets/canasta-familiar/entrega-canasta.jpeg';
|
||||
|
||||
const canastaStats = [
|
||||
{ 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' },
|
||||
];
|
||||
---
|
||||
|
||||
<main class="canasta-page">
|
||||
<section class="canasta-hero">
|
||||
<div class="hero-copy">
|
||||
<a href="/" class="back-link">← Volver a Renacer</a>
|
||||
<p class="section-kicker">Canasta Familiar</p>
|
||||
<h1>Apoyo directo para sostener la mesa y el hogar de muchas familias.</h1>
|
||||
<p class="hero-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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<Image
|
||||
src={canastaImagenPrincipal}
|
||||
alt="Canasta Familiar de Renacer preparada para apoyo comunitario."
|
||||
widths={[720, 1080]}
|
||||
sizes="(max-width: 960px) 100vw, 46vw"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="canasta-content">
|
||||
<div class="content-copy">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
{canastaStats.map((stat) => (
|
||||
<article class="stat-card">
|
||||
<strong>{stat.valor}</strong>
|
||||
<span>{stat.etiqueta}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gallery-section">
|
||||
<div class="gallery-copy">
|
||||
<p class="section-kicker">Apoyo concreto</p>
|
||||
<h2>Una red de ayuda que responde a necesidades básicas con cercanía y continuidad.</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gallery-grid">
|
||||
<div class="gallery-card">
|
||||
<Image
|
||||
src={canastaEntrega}
|
||||
alt="Entrega de apoyo del programa Canasta Familiar de Renacer."
|
||||
widths={[520, 800]}
|
||||
sizes="(max-width: 960px) 100vw, 30vw"
|
||||
/>
|
||||
</div>
|
||||
<div class="gallery-note">
|
||||
<h3>Cómo apoyar</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="note-actions">
|
||||
<a class="primary-link" href="/colaboradormensual/">Colaborar mensualmente</a>
|
||||
<a class="secondary-link" href="mailto:contacto@familiarenacer.cl">Contactar a Renacer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.canasta-page {
|
||||
width: min(100%, 1240px);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.canasta-hero,
|
||||
.canasta-content,
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.canasta-hero {
|
||||
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
margin-bottom: 1rem;
|
||||
color: #52637a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-kicker {
|
||||
margin: 0 0 0.9rem;
|
||||
color: var(--brand);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-family: 'Sora', sans-serif;
|
||||
letter-spacing: -0.05em;
|
||||
color: #1b2740;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.8rem, 6vw, 4.8rem);
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.hero-lead,
|
||||
.content-copy p,
|
||||
.gallery-copy p,
|
||||
.gallery-note p {
|
||||
color: #5b6a82;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-top: 1.2rem;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.hero-visual,
|
||||
.gallery-card,
|
||||
.gallery-note,
|
||||
.stat-card {
|
||||
overflow: hidden;
|
||||
border-radius: 1.6rem;
|
||||
border: 1px solid rgba(29, 39, 56, 0.08);
|
||||
background: white;
|
||||
box-shadow: 0 18px 48px rgba(34, 54, 73, 0.08);
|
||||
}
|
||||
|
||||
.hero-visual :global(img),
|
||||
.gallery-card :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
min-height: 32rem;
|
||||
}
|
||||
|
||||
.canasta-content {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
||||
align-items: start;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
display: block;
|
||||
color: var(--brand-dark);
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: #5b6a82;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gallery-section {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.gallery-copy {
|
||||
max-width: 48rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.gallery-copy h2 {
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gallery-card {
|
||||
min-height: 26rem;
|
||||
}
|
||||
|
||||
.gallery-note {
|
||||
padding: 1.8rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7f9ff 100%);
|
||||
}
|
||||
|
||||
.gallery-note h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.primary-link,
|
||||
.secondary-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.primary-link {
|
||||
background: linear-gradient(180deg, #1b49d6 0%, #0834b7 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary-link {
|
||||
background: rgba(8, 52, 183, 0.06);
|
||||
color: var(--brand-dark);
|
||||
border: 1px solid rgba(8, 52, 183, 0.14);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.canasta-hero,
|
||||
.canasta-content,
|
||||
.gallery-grid,
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-visual,
|
||||
.gallery-card {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.canasta-page {
|
||||
padding: 1.25rem 1rem 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,232 +1,360 @@
|
||||
---
|
||||
import LucideIcon from './LucideIcon.astro';
|
||||
|
||||
const donationLinks = [
|
||||
{
|
||||
name: 'Gesto Solidario',
|
||||
amount: 'CLP 1.000 a CLP 4.000',
|
||||
description: 'Un pequeño gesto que también ayuda a sembrar esperanza.',
|
||||
emoji: '🤍',
|
||||
icon: 'HeartHandshake',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/tDRHQvX1T0JL1gdx8A7S0mM4RFqV25AR',
|
||||
},
|
||||
{
|
||||
name: 'Semilla Solidaria',
|
||||
amount: '$5.000',
|
||||
description: 'Inicia la ayuda y permite sembrar nuevas oportunidades.',
|
||||
emoji: '🌱',
|
||||
icon: 'Sprout',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/95bH9li3iYcdJKxuugTd1NV2s7n5sglH',
|
||||
},
|
||||
{
|
||||
name: 'Brote de Esperanza',
|
||||
amount: '$10.000',
|
||||
description: 'Ayuda a que nuevos proyectos comiencen a crecer.',
|
||||
emoji: '🌿',
|
||||
icon: 'Leaf',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/jwtYOw0vQOk5QyWXg8Zps9t6HMBfksmY',
|
||||
},
|
||||
{
|
||||
name: 'Árbol de Apoyo',
|
||||
amount: '$20.000',
|
||||
description: 'Fortalece el acompañamiento a niños y familias.',
|
||||
emoji: '🌳',
|
||||
icon: 'Users',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/dZEYJhCPUUQwOU8zqfYtsIIkGLpPPFsD',
|
||||
},
|
||||
{
|
||||
name: 'Sol Solidario',
|
||||
amount: '$30.000',
|
||||
description: 'Entrega la energía que permite que todo siga creciendo.',
|
||||
emoji: '🌞',
|
||||
icon: 'CircleDollarSign',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/ayAQljPMkfiwbl8FOEl9SIBXU1u8wRhK',
|
||||
},
|
||||
{
|
||||
name: 'Raíz Fundadora',
|
||||
amount: '$40.000',
|
||||
description: 'Sostiene y da estabilidad al proyecto.',
|
||||
emoji: '✨',
|
||||
icon: 'BadgeCheck',
|
||||
href: 'https://app.reveniu.com/checkout-custom-link/kX0o2VEFugphajKBGUDhQEUew9YqYDtt',
|
||||
},
|
||||
{
|
||||
name: 'Guardián de Renacer',
|
||||
amount: '$50.000 o más',
|
||||
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',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<main class="linktree-page">
|
||||
<section class="linktree-card">
|
||||
<main class="donations-page">
|
||||
<div class="donations-shell">
|
||||
<aside class="donations-aside">
|
||||
<a href="/" class="back-link">← Volver a Renacer</a>
|
||||
<div class="brand-badge">Renacer</div>
|
||||
<h1>Colaborador mensual</h1>
|
||||
<p class="intro">
|
||||
Elige una forma de aportar mes a mes y ayúdanos a sostener el trabajo con niños, familias y
|
||||
comunidad.
|
||||
Elige un nivel de aporte mensual y abre el checkout seguro para sostener el trabajo con
|
||||
niños, familias y comunidad.
|
||||
</p>
|
||||
|
||||
<div class="links-list">
|
||||
<div class="aside-panel">
|
||||
<div class="aside-stat">
|
||||
<LucideIcon name="CircleDollarSign" size={18} />
|
||||
<div>
|
||||
<strong>Aporte recurrente</strong>
|
||||
<span>Ayuda a dar continuidad a programas y campañas.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aside-stat">
|
||||
<LucideIcon name="ShieldCheck" size={18} />
|
||||
<div>
|
||||
<strong>Checkout externo seguro</strong>
|
||||
<span>Los enlaces abren la plataforma protegida para completar tu aporte.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aside-stat">
|
||||
<LucideIcon name="HandHelping" size={18} />
|
||||
<div>
|
||||
<strong>Impacto real</strong>
|
||||
<span>Tu colaboración ayuda a sostener alimentación, infancia y apoyo familiar.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="donations-content">
|
||||
<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-link"
|
||||
class="donation-card"
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="nofollow sponsored noopener noreferrer"
|
||||
>
|
||||
<span class="link-emoji" aria-hidden="true">{item.emoji}</span>
|
||||
<span class="link-copy">
|
||||
<strong>
|
||||
{item.name} <span>{item.amount}</span>
|
||||
</strong>
|
||||
<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>
|
||||
<span class="link-arrow" aria-hidden="true">↗</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p class="footnote">Los enlaces de apoyo abren una plataforma externa y segura para completar tu aporte.</p>
|
||||
<p class="footnote">
|
||||
Si tienes dudas sobre aportes, coordinación institucional o donaciones directas, escríbenos
|
||||
a `contacto@familiarenacer.cl`.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.linktree-page {
|
||||
.donations-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(29, 107, 87, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, #f8f4ec 0%, #f2ede3 100%);
|
||||
radial-gradient(circle at 10% 10%, rgba(8, 52, 183, 0.14), transparent 20%),
|
||||
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 {
|
||||
width: min(100%, 32rem);
|
||||
.donations-shell {
|
||||
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;
|
||||
border: 1px solid rgba(31, 39, 37, 0.1);
|
||||
border-radius: 1.75rem;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 18px 45px rgba(35, 44, 41, 0.09);
|
||||
}
|
||||
|
||||
.donations-content {
|
||||
padding: 1.45rem;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
margin-bottom: 1rem;
|
||||
color: #47625b;
|
||||
color: #47649a;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-badge {
|
||||
.brand-badge,
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin: 0 auto 0.9rem;
|
||||
padding: 0.45rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 52, 183, 0.08);
|
||||
color: #0834b7;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
strong {
|
||||
font-family: 'Sora', sans-serif;
|
||||
letter-spacing: -0.04em;
|
||||
color: #122756;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: clamp(2rem, 5vw, 2.8rem);
|
||||
line-height: 1;
|
||||
margin: 1rem 0 0;
|
||||
font-size: clamp(2.2rem, 4vw, 3.4rem);
|
||||
line-height: 0.96;
|
||||
}
|
||||
|
||||
.intro,
|
||||
.footnote {
|
||||
margin: 0.85rem auto 0;
|
||||
max-width: 32ch;
|
||||
text-align: center;
|
||||
color: #5f6966;
|
||||
line-height: 1.6;
|
||||
.footnote,
|
||||
.aside-stat span,
|
||||
.card-copy small {
|
||||
color: #5e6f8d;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.links-list {
|
||||
.intro {
|
||||
margin: 1rem 0 0;
|
||||
max-width: 34ch;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.aside-panel {
|
||||
display: grid;
|
||||
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;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
padding: 1rem 1rem 1rem 0.95rem;
|
||||
border: 1px solid rgba(31, 39, 37, 0.1);
|
||||
align-items: start;
|
||||
gap: 0.85rem;
|
||||
padding: 1rem;
|
||||
border-radius: 1.2rem;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10px 24px rgba(35, 44, 41, 0.05);
|
||||
border: 1px solid rgba(18, 63, 184, 0.1);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f4f8ff 100%);
|
||||
box-shadow: 0 12px 28px rgba(18, 54, 144, 0.06);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 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);
|
||||
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 {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
.card-icon {
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 52, 183, 0.08);
|
||||
font-size: 1.25rem;
|
||||
background: rgba(8, 52, 183, 0.09);
|
||||
color: #0834b7;
|
||||
}
|
||||
|
||||
.link-copy {
|
||||
.card-copy {
|
||||
display: grid;
|
||||
gap: 0.22rem;
|
||||
}
|
||||
|
||||
.link-copy strong {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
.card-copy strong {
|
||||
font-size: 1rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.link-copy strong span,
|
||||
.link-arrow {
|
||||
.card-amount {
|
||||
color: #0834b7;
|
||||
font-weight: 800;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.card-copy small {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
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 {
|
||||
font-size: 0.9rem;
|
||||
margin-top: 1.2rem;
|
||||
margin: 1.2rem 0 0;
|
||||
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) {
|
||||
.linktree-page {
|
||||
.donations-page {
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.linktree-card {
|
||||
padding: 1.15rem;
|
||||
.donations-aside,
|
||||
.donations-content {
|
||||
padding: 1.1rem;
|
||||
border-radius: 1.35rem;
|
||||
}
|
||||
|
||||
.donation-link {
|
||||
padding: 0.9rem;
|
||||
gap: 0.75rem;
|
||||
.donation-card {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import comedorImagenPrincipal from '../assets/comedor-solidario/imagen1.jpg';
|
||||
import comedorImagenAccion from '../assets/comedor-solidario/comedor-accion.jpeg';
|
||||
|
||||
const comedorStats = [
|
||||
{ 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' },
|
||||
];
|
||||
---
|
||||
|
||||
<main class="comedor-page">
|
||||
<section class="comedor-hero">
|
||||
<div class="hero-copy">
|
||||
<a href="/" class="back-link">← Volver a Renacer</a>
|
||||
<p class="section-kicker">Comedor Solidario</p>
|
||||
<h1>Almuerzos calientes, cercanía y apoyo concreto en Quillota.</h1>
|
||||
<p class="hero-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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<Image
|
||||
src={comedorImagenPrincipal}
|
||||
alt="Preparación y servicio del Comedor Solidario de Renacer en Quillota."
|
||||
widths={[720, 1080]}
|
||||
sizes="(max-width: 960px) 100vw, 46vw"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="comedor-content">
|
||||
<div class="content-copy">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
{comedorStats.map((stat) => (
|
||||
<article class="stat-card">
|
||||
<strong>{stat.valor}</strong>
|
||||
<span>{stat.etiqueta}</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gallery-section">
|
||||
<div class="gallery-copy">
|
||||
<p class="section-kicker">Acompañamiento real</p>
|
||||
<h2>Una ayuda sostenida que combina alimentación, cuidado y presencia comunitaria.</h2>
|
||||
<p>
|
||||
Sabemos que muchas veces las dificultades económicas, sociales o de salud afectan lo más
|
||||
básico: la posibilidad de acceder a una comida diaria. Por eso buscamos sostener este apoyo
|
||||
de manera constante y en condiciones adecuadas de higiene y cuidado.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gallery-grid">
|
||||
<div class="gallery-card">
|
||||
<Image
|
||||
src={comedorImagenAccion}
|
||||
alt="Actividad del Comedor Solidario junto a la comunidad."
|
||||
widths={[520, 800]}
|
||||
sizes="(max-width: 960px) 100vw, 30vw"
|
||||
/>
|
||||
</div>
|
||||
<div class="gallery-note">
|
||||
<h3>Cómo apoyar</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="note-actions">
|
||||
<a class="primary-link" href="/colaboradormensual/">Colaborar mensualmente</a>
|
||||
<a class="secondary-link" href="mailto:contacto@familiarenacer.cl">Contactar a Renacer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.comedor-page {
|
||||
width: min(100%, 1240px);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.comedor-hero,
|
||||
.comedor-content,
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.comedor-hero {
|
||||
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
margin-bottom: 1rem;
|
||||
color: #52637a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-kicker {
|
||||
margin: 0 0 0.9rem;
|
||||
color: var(--brand);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-family: 'Sora', sans-serif;
|
||||
letter-spacing: -0.05em;
|
||||
color: #1b2740;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.8rem, 6vw, 4.8rem);
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.hero-lead,
|
||||
.content-copy p,
|
||||
.gallery-copy p,
|
||||
.gallery-note p {
|
||||
color: #5b6a82;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-top: 1.2rem;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.hero-visual,
|
||||
.gallery-card,
|
||||
.gallery-note,
|
||||
.stat-card {
|
||||
overflow: hidden;
|
||||
border-radius: 1.6rem;
|
||||
border: 1px solid rgba(29, 39, 56, 0.08);
|
||||
background: white;
|
||||
box-shadow: 0 18px 48px rgba(34, 54, 73, 0.08);
|
||||
}
|
||||
|
||||
.hero-visual :global(img),
|
||||
.gallery-card :global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
min-height: 32rem;
|
||||
}
|
||||
|
||||
.comedor-content {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
||||
align-items: start;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
display: block;
|
||||
color: var(--brand-dark);
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: #5b6a82;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.gallery-section {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.gallery-copy {
|
||||
max-width: 48rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.gallery-copy h2 {
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gallery-card {
|
||||
min-height: 26rem;
|
||||
}
|
||||
|
||||
.gallery-note {
|
||||
padding: 1.8rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7f9ff 100%);
|
||||
}
|
||||
|
||||
.gallery-note h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.primary-link,
|
||||
.secondary-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.primary-link {
|
||||
background: linear-gradient(180deg, #1b49d6 0%, #0834b7 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary-link {
|
||||
background: rgba(8, 52, 183, 0.06);
|
||||
color: var(--brand-dark);
|
||||
border: 1px solid rgba(8, 52, 183, 0.14);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.comedor-hero,
|
||||
.comedor-content,
|
||||
.gallery-grid,
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-visual,
|
||||
.gallery-card {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.comedor-page {
|
||||
padding: 1.25rem 1rem 3rem;
|
||||
}
|
||||
}
|
||||
</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}
|
||||
/>
|
||||
@@ -1,9 +1,17 @@
|
||||
---
|
||||
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;
|
||||
@@ -11,8 +19,8 @@ interface Props {
|
||||
lead: string;
|
||||
paragraphs: string[];
|
||||
stats: Stat[];
|
||||
supportTitle?: string;
|
||||
supportText: string;
|
||||
images?: ProgramImage[];
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -22,250 +30,575 @@ const {
|
||||
lead,
|
||||
paragraphs,
|
||||
stats,
|
||||
supportTitle = 'Cómo apoyar',
|
||||
supportText,
|
||||
images = [],
|
||||
} = Astro.props;
|
||||
|
||||
const hasImages = images.length > 0;
|
||||
const heroImage = images[0];
|
||||
const galleryImages = images.slice(1, 3);
|
||||
---
|
||||
|
||||
<main class="program-page">
|
||||
<section class="program-hero">
|
||||
<div class="hero-copy">
|
||||
|
||||
<!-- ── 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="section-kicker">{kicker}</p>
|
||||
<h1>{title}</h1>
|
||||
<p class="kicker">{kicker}</p>
|
||||
<h1 class="hero-title">{title}</h1>
|
||||
<p class="hero-lead">{lead}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-panel">
|
||||
<div class="panel-badge">Programa Renacer</div>
|
||||
<p>
|
||||
Una iniciativa comunitaria que combina apoyo concreto, cercanía y participación de redes
|
||||
solidarias para responder a necesidades reales.
|
||||
</p>
|
||||
{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>
|
||||
|
||||
<section class="program-content">
|
||||
<div class="content-copy">
|
||||
{paragraphs.map((paragraph) => <p>{paragraph}</p>)}
|
||||
<!-- ── 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">
|
||||
<strong>{stat.valor}</strong>
|
||||
<span>{stat.etiqueta}</span>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="support-section">
|
||||
<div class="support-copy">
|
||||
<p class="section-kicker">Apoyo y comunidad</p>
|
||||
<h2>{supportTitle}</h2>
|
||||
<p>{supportText}</p>
|
||||
<div class="note-actions">
|
||||
<a class="primary-link" href="/colaboradormensual/">Colaborar mensualmente</a>
|
||||
<a class="secondary-link" href="mailto:contacto@familiarenacer.cl">Contactar a Renacer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="support-panel">
|
||||
<h3>Red de colaboración</h3>
|
||||
<p>
|
||||
Estos programas se sostienen con voluntariado, donaciones, campañas y trabajo comunitario
|
||||
constante. Cada aporte ayuda a mantener el acompañamiento en el tiempo.
|
||||
</p>
|
||||
</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: min(100%, 1240px);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.program-hero,
|
||||
.program-content,
|
||||
.support-section {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
/* ── 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 {
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
|
||||
align-items: stretch;
|
||||
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); }
|
||||
|
||||
.section-kicker {
|
||||
margin: 0 0 0.9rem;
|
||||
.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.86rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
.hero-title {
|
||||
font-family: 'Sora', sans-serif;
|
||||
letter-spacing: -0.05em;
|
||||
font-size: clamp(2.2rem, 4.5vw, 3.6rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
color: #1b2740;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.8rem, 6vw, 4.8rem);
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.hero-lead,
|
||||
.content-copy p,
|
||||
.support-copy p,
|
||||
.hero-panel p,
|
||||
.support-panel p {
|
||||
color: #5b6a82;
|
||||
line-height: 1.8;
|
||||
margin: 0 0 1rem;
|
||||
max-width: 20ch;
|
||||
}
|
||||
|
||||
.hero-lead {
|
||||
margin-top: 1.2rem;
|
||||
font-size: 1.08rem;
|
||||
font-size: clamp(0.97rem, 1.4vw, 1.1rem);
|
||||
color: #4a5c73;
|
||||
line-height: 1.7;
|
||||
max-width: 52ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-panel,
|
||||
.support-panel,
|
||||
.stat-card {
|
||||
border-radius: 1.6rem;
|
||||
border: 1px solid rgba(29, 39, 56, 0.08);
|
||||
background: white;
|
||||
box-shadow: 0 18px 48px rgba(34, 54, 73, 0.08);
|
||||
.hero-visual {
|
||||
border-radius: 1.25rem;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 4 / 3;
|
||||
box-shadow: 0 20px 50px rgba(8, 52, 183, 0.14);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
.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;
|
||||
justify-content: end;
|
||||
min-height: 20rem;
|
||||
padding: 1.8rem;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(8, 52, 183, 0.11), transparent 28%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f7f9ff 100%);
|
||||
gap: 1.1rem;
|
||||
}
|
||||
|
||||
.panel-badge {
|
||||
width: fit-content;
|
||||
margin-bottom: 0.9rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 52, 183, 0.08);
|
||||
color: var(--brand-dark);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
.text-col p {
|
||||
color: #4a5568;
|
||||
line-height: 1.8;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.program-content {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
|
||||
align-items: start;
|
||||
margin-top: 2rem;
|
||||
/* ── Stats ────────────────────────────────────────── */
|
||||
.stats-col {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1.25rem;
|
||||
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-card strong {
|
||||
.stat-valor {
|
||||
display: block;
|
||||
color: var(--brand-dark);
|
||||
font-family: 'Sora', sans-serif;
|
||||
font-size: 2rem;
|
||||
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-card span {
|
||||
.stat-label {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: #5b6a82;
|
||||
line-height: 1.5;
|
||||
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 {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(300px, 0.9fr);
|
||||
align-items: stretch;
|
||||
margin-top: 4rem;
|
||||
width: min(100%, 1120px);
|
||||
margin: 0 auto 4rem;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.support-copy h2 {
|
||||
font-size: clamp(2rem, 4vw, 3.4rem);
|
||||
.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-panel {
|
||||
padding: 1.8rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7f9ff 100%);
|
||||
.support-kicker {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #7b96c8;
|
||||
margin: 0 0 0.6rem;
|
||||
}
|
||||
|
||||
.support-panel h3 {
|
||||
font-size: 1.6rem;
|
||||
.support-text {
|
||||
color: #c7d4e8;
|
||||
line-height: 1.7;
|
||||
font-size: 0.98rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
.support-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.9rem;
|
||||
margin-top: 1.5rem;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.primary-link,
|
||||
.secondary-link {
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 1rem;
|
||||
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); }
|
||||
|
||||
.primary-link {
|
||||
background: linear-gradient(180deg, #1b49d6 0%, #0834b7 100%);
|
||||
.btn-primary {
|
||||
background: linear-gradient(180deg, #2a5ae3 0%, var(--brand) 100%);
|
||||
color: white;
|
||||
box-shadow: 0 8px 20px rgba(8, 52, 183, 0.35);
|
||||
}
|
||||
|
||||
.secondary-link {
|
||||
background: rgba(8, 52, 183, 0.06);
|
||||
color: var(--brand-dark);
|
||||
border: 1px solid rgba(8, 52, 183, 0.14);
|
||||
.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) {
|
||||
.program-hero,
|
||||
.program-content,
|
||||
.support-section,
|
||||
.stats-grid {
|
||||
.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: 720px) {
|
||||
.program-page {
|
||||
padding: 1.25rem 1rem 3rem;
|
||||
@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' },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import CanastaFamiliarPage from '../components/CanastaFamiliarPage.astro';
|
||||
return Astro.redirect('/programas/canasta-familiar/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Canasta Familiar | Renacer"
|
||||
description="Conoce el programa Canasta Familiar de Renacer y cómo entrega apoyo directo con alimentos y productos esenciales para el hogar."
|
||||
>
|
||||
<CanastaFamiliarPage />
|
||||
</Layout>
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ProgramInfoPage from '../components/ProgramInfoPage.astro';
|
||||
return Astro.redirect('/programas/cena-navidena/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Cena Navideña | Renacer"
|
||||
description="Conoce la Cena Navideña de Renacer, una instancia de encuentro, compañía y entrega para personas y familias que viven la Navidad en dificultad o soledad."
|
||||
>
|
||||
<ProgramInfoPage
|
||||
kicker="Cena Navideña"
|
||||
title="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."
|
||||
paragraphs={[
|
||||
'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' },
|
||||
]}
|
||||
supportText="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."
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ComedorSolidarioPage from '../components/ComedorSolidarioPage.astro';
|
||||
return Astro.redirect('/programas/comedor-solidario/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Comedor Solidario | Renacer"
|
||||
description="Conoce el Programa Comedor Solidario de Renacer en Quillota y cómo sostiene almuerzos calientes y acompañamiento comunitario."
|
||||
>
|
||||
<ComedorSolidarioPage />
|
||||
</Layout>
|
||||
|
||||
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);
|
||||
---
|
||||
@@ -1,28 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ProgramInfoPage from '../components/ProgramInfoPage.astro';
|
||||
return Astro.redirect('/programas/navidad-solidaria/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Navidad Solidaria | Renacer"
|
||||
description="Conoce Navidad Solidaria de Renacer, una campaña comunitaria de apadrinamiento y celebración para niños y niñas."
|
||||
>
|
||||
<ProgramInfoPage
|
||||
kicker="Navidad Solidaria"
|
||||
title="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."
|
||||
paragraphs={[
|
||||
'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' },
|
||||
]}
|
||||
supportText="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."
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
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>
|
||||
@@ -1,28 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ProgramInfoPage from '../components/ProgramInfoPage.astro';
|
||||
return Astro.redirect('/programas/ropero-solidario/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Ropero Solidario | Renacer"
|
||||
description="Conoce el programa Ropero Solidario de Renacer y cómo acompaña a familias con vestimenta y apoyo digno durante todo el año."
|
||||
>
|
||||
<ProgramInfoPage
|
||||
kicker="Ropero Solidario"
|
||||
title="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."
|
||||
paragraphs={[
|
||||
'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' },
|
||||
]}
|
||||
supportText="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."
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ProgramInfoPage from '../components/ProgramInfoPage.astro';
|
||||
return Astro.redirect('/programas/sala-del-juguete/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Sala del Juguete | Renacer"
|
||||
description="Conoce la Sala del Juguete de Renacer, un espacio para rescatar el valor del juego, la infancia y la reutilización comunitaria."
|
||||
>
|
||||
<ProgramInfoPage
|
||||
kicker="Sala del Juguete"
|
||||
title="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."
|
||||
paragraphs={[
|
||||
'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: 'Juego', etiqueta: 'como herramienta de desarrollo, encuentro y bienestar' },
|
||||
{ valor: 'Circular', etiqueta: 'reutilización de juguetes nuevos y usados en buen estado' },
|
||||
{ valor: 'Infancia', etiqueta: 'espacio pensado para explorar, imaginar y compartir' },
|
||||
{ valor: 'Comunidad', etiqueta: 'apoyo a celebraciones y actividades durante el año' },
|
||||
]}
|
||||
supportText="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."
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import ProgramInfoPage from '../components/ProgramInfoPage.astro';
|
||||
return Astro.redirect('/programas/tardes-recreativas/', 301);
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Tardes Recreativas | Renacer"
|
||||
description="Conoce las Tardes Recreativas de Renacer, un espacio seguro y acompañado para niños y niñas durante vacaciones de verano e invierno."
|
||||
>
|
||||
<ProgramInfoPage
|
||||
kicker="Tardes Recreativas"
|
||||
title="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."
|
||||
paragraphs={[
|
||||
'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' },
|
||||
]}
|
||||
supportText="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."
|
||||
/>
|
||||
</Layout>
|
||||
|
||||