// ===== RIVAMAR Group — CMS Lite: editor de contenido completo (v4) =====
// Edita TODO el sitio: General (hero, banner, contacto), Locales (x3 con menús),
// Nosotros (historia, valores, hitos, equipo) y Convenios. Persiste vía store.
const { useState } = React;
// ---------- Controles reutilizables ----------
function CField({ label, value, onChange, area, hint }) {
return (
{label}
{area
?
);
}
function SliderField({ label, value, onChange, min, max, step, unit = '', hint }) {
return (
{label}
{value}{unit}
onChange(parseFloat(e.target.value))}
className="w-full accent-[color:rgb(var(--c-gold))] cursor-pointer" />
{hint && {hint} }
);
}
function RowAdd({ onClick, label }) {
return {label} ;
}
function RowDel({ onClick }) {
return ;
}
// Editor de pares [titulo, descripcion] (valores, beneficios)
function PairListEditor({ items, onChange, labelA = 'Título', labelB = 'Descripción' }) {
const upd = (i, j, v) => onChange(items.map((row, idx) => idx === i ? row.map((c, jc) => jc === j ? v : c) : row));
const add = () => onChange([...items, ['', '']]);
const del = (i) => onChange(items.filter((_, idx) => idx !== i));
return (
{items.map((row, i) => (
))}
);
}
// Editor de hitos [año, texto]
function HitosEditor({ items, onChange }) {
const upd = (i, j, v) => onChange(items.map((row, idx) => idx === i ? row.map((c, jc) => jc === j ? v : c) : row));
const add = () => onChange([...items, ['', '']]);
const del = (i) => onChange(items.filter((_, idx) => idx !== i));
return (
);
}
// Editor de equipo [rol, area, desc]
function EquipoEditor({ items, onChange }) {
const upd = (i, j, v) => onChange(items.map((row, idx) => idx === i ? row.map((c, jc) => jc === j ? v : c) : row));
const updImg = (i, v) => onChange(items.map((row, idx) => {
if (idx !== i) return row;
const r = row.slice(); r[3] = v; return r;
}));
const add = () => onChange([...items, ['', '', '', '']]);
const del = (i) => onChange(items.filter((_, idx) => idx !== i));
return (
{items.map((row, i) => (
Foto de {row[0] || 'este integrante'}
updImg(i, v)} ratio="4/3" maxW={1000} />
))}
);
}
// Editor de tags (destacados)
function TagsEditor({ items, onChange }) {
const upd = (i, v) => onChange(items.map((c, idx) => idx === i ? v : c));
return (
);
}
// Editor de menú [{plato, desc, precio}]
function MenuEditor({ items, onChange }) {
const upd = (i, k, v) => onChange(items.map((row, idx) => idx === i ? { ...row, [k]: v } : row));
const add = () => onChange([...items, { plato: '', desc: '', precio: '' }]);
const del = (i) => onChange(items.filter((_, idx) => idx !== i));
return (
{items.map((row, i) => (
))}
);
}
function CmsCard({ title, children }) {
return (
{title &&
{title} }
{children}
);
}
// ---------- CMS principal ----------
function CmsLite({ content, setContent, resetContent, onVerSitio }) {
const pick = (c) => ({ site: c.site, locales: c.locales, nosotros: c.nosotros, convenios: c.convenios, exponor: c.exponor });
const [draft, setDraft] = useState(pick(content));
const [tab, setTab] = useState('general');
const [localIdx, setLocalIdx] = useState(0);
const [history, setHistory] = useState([]);
const [toast, setToast] = useState('');
const flash = (m) => { setToast(m); setTimeout(() => setToast(''), 2600); };
const dirty = JSON.stringify(draft) !== JSON.stringify(pick(content));
const setSite = (k, v) => setDraft((d) => ({ ...d, site: { ...d.site, [k]: v } }));
const setNos = (k, v) => setDraft((d) => ({ ...d, nosotros: { ...d.nosotros, [k]: v } }));
const setConv = (k, v) => setDraft((d) => ({ ...d, convenios: { ...d.convenios, [k]: v } }));
const setExpo = (k, v) => setDraft((d) => ({ ...d, exponor: { ...d.exponor, [k]: v } }));
const setLocal = (k, v) => setDraft((d) => ({ ...d, locales: d.locales.map((l, i) => i === localIdx ? { ...l, [k]: v } : l) }));
const publish = () => {
setHistory((h) => [{ ts: new Date().toLocaleString('es-CL'), data: pick(content) }, ...h].slice(0, 5));
setContent(draft);
flash('Contenido publicado · visible en el sitio');
};
const restore = (snap) => { setDraft(snap.data); flash('Versión cargada al editor — revisa y publica'); };
const resetSeccion = () => {
const def = buildDefaults();
if (tab === 'general') setDraft((d) => ({ ...d, site: def.site }));
if (tab === 'locales') setDraft((d) => ({ ...d, locales: def.locales }));
if (tab === 'nosotros') setDraft((d) => ({ ...d, nosotros: def.nosotros }));
if (tab === 'convenios') setDraft((d) => ({ ...d, convenios: def.convenios }));
if (tab === 'exponor') setDraft((d) => ({ ...d, exponor: def.exponor }));
flash('Sección restablecida en el editor');
};
const tabBtn = (id, label) => (
setTab(id)} className={`px-3.5 py-2 rounded-lg text-sm font-medium ${tab === id ? 'bg-gold text-carbon' : 'bg-iron border border-white/10 text-gray-300 hover:text-gray-50'}`}>{label}
);
const L = draft.locales[localIdx];
return (
{tabBtn('general', 'General')}{tabBtn('locales', 'Locales')}{tabBtn('nosotros', 'Nosotros')}{tabBtn('convenios', 'Convenios')}{tabBtn('exponor', 'EXPONOR')}
Ver Sitio ↗
Restablecer sección
{dirty ? 'Publicar cambios' : 'Publicado ✓'}
{/* ---- GENERAL ---- */}
{tab === 'general' && (
Sube tu logo (PNG/JPG) o usa el texto de marca. Aparece en la barra superior y el pie de página.
setSite('logo_img', v)} ratio="3/1" maxW={600} />
{!draft.site.logo_img && (
setSite('brand_nombre', v)} />
setSite('brand_sub', v)} hint="Vacío = sin bajada" />
setSite('brand_inicial', v)} hint="Letra del cuadro" />
)}
{draft.site.logo_img && Con logo de imagen, el texto de marca se oculta automáticamente.
}
setSite('hero_titulo', v)} area />
setSite('hero_subtitulo', v)} />
setSite('hero_cuerpo', v)} area />
Imágenes de portada (slider — rotan automáticamente)
setSite('hero_img', v)} ratio="16/9" maxW={1800} />
setSite('hero_img2', v)} ratio="16/9" maxW={1800} />
setSite('hero_img3', v)} ratio="16/9" maxW={1800} />
setSite('hero_blur', v)} hint="Aplica al slider y a las cabeceras de Locales/Nosotros." />
setSite('hero_overlay', v)} hint="Menos = foto más visible. Más = texto más legible." />
setSite('banner_exponor', v)} area />
setSite('mch_url', v)} hint="Ej. https://www.mch.cl/ — o el link a tu edición/artículo" />
setSite('mch_eyebrow', v)} />
setSite('mch_titulo', v)} />
setSite('mch_texto', v)} area />
setSite('mch_boton', v)} />
setSite('contacto_correo', v)} />
setSite('contacto_telefono', v)} />
setSite('whatsapp', v)} hint="Ej. 56912345678" />
setSite('footer_desc', v)} area />
setSite('footer_copy', v)} />
setSite('social_ig', v)} hint="Vacío = oculto" />
setSite('social_fb', v)} />
setSite('social_in', v)} />
setSite('email_auto', e.target.checked)} className="w-5 h-5 rounded border-white/20 bg-carbon accent-[color:rgb(var(--c-gold))]" />
Activar envío automático al recibir una reserva
setSite('email_modo', 'php')} className={`text-left p-3 rounded-lg border transition-all ${(draft.site.email_modo || 'php') === 'php' ? 'border-gold bg-gold/10' : 'border-white/10 bg-carbon hover:border-white/25'}`}>
Servidor (recomendado)
✓ Sin límites · ya incluido
setSite('email_modo', 'emailjs')} className={`text-left p-3 rounded-lg border transition-all ${draft.site.email_modo === 'emailjs' ? 'border-gold bg-gold/10' : 'border-white/10 bg-carbon hover:border-white/25'}`}>
EmailJS
Externo · máx. 200/mes
{(draft.site.email_modo || 'php') === 'php' ? (
Usa el correo de tu propio servidor Hostinger (archivo api/reserva-mail.php , ya incluido). Sin límites ni cuentas externas.
setSite('email_destino', v)} hint="Dónde llegan las reservas. Recomendado: un correo @rivamar.cl creado en Hostinger." />
✓ Envío por servidor configurado. Solo asegúrate de subir la carpeta api/ a Hostinger.
) : (
Crea una cuenta en emailjs.com y pega las 3 claves. Variables de la plantilla: reserva_id, nombre, empresa, correo, telefono, local, fecha, hora, personas, exponor, comentarios .
setSite('emailjs_public', v)} />
setSite('email_destino', v)} hint="Dónde llegan las reservas" />
setSite('emailjs_service', v)} />
setSite('emailjs_template', v)} />
{draft.site.email_auto && (!draft.site.emailjs_public || !draft.site.emailjs_service || !draft.site.emailjs_template)
?
Completa las 3 claves para que el envío funcione.
: draft.site.email_auto ?
✓ Envío por EmailJS configurado.
: null}
)}
setSite('notify_wa', e.target.checked)} className="w-5 h-5 rounded border-white/20 bg-carbon accent-[color:rgb(var(--c-gold))]" />
Avisarme al WhatsApp cuando llegue una reserva
Usa CallMeBot (gratis, sin límite mensual). Activación única: agrega el número +34 644 51 95 23 a tus contactos y envíale por WhatsApp el mensaje "I allow callmebot to send me messages" ; recibirás tu apikey . Pégala aquí.
setSite('callmebot_phone', v)} hint="Ej. +56964979087" />
setSite('callmebot_apikey', v)} />
{draft.site.notify_wa && !draft.site.callmebot_apikey
? Pega tu API key para activar el aviso.
: draft.site.notify_wa ? ✓ Aviso por WhatsApp configurado.
: null}
Cambia la identidad visual completa. Se aplica al publicar — sin tocar código.
{[
['midnight', 'Midnight Premium', ['#0B1622', '#C9A24B', '#F5A623']],
['rivamar', 'Rivamar (Gran Chimú)', ['#EFE8DB', '#B5552E', '#6E2433']],
['claro', 'Claro Gourmet', ['#F8F6F1', '#A97A20', '#F09619']],
['esmeralda', 'Esmeralda Premium', ['#0C1A16', '#D4A24C', '#C17A4A']],
['burdeos', 'Burdeos Carbón', ['#1A0E12', '#D6A85C', '#C07860']],
['industrial', 'Industrial', ['#111827', '#C8A000', '#B87333']],
].map(([id, label, sw]) => (
setSite('theme', id)}
className={`text-left p-3 rounded-lg border transition-all ${(draft.site.theme || 'midnight') === id ? 'border-gold bg-gold/10' : 'border-white/10 bg-carbon hover:border-white/25'}`}>
{sw.map((c) => )}
{label}
{(draft.site.theme || 'midnight') === id && ● Activa }
))}
)}
{/* ---- LOCALES ---- */}
{tab === 'locales' && (
{draft.locales.map((l, i) => (
setLocalIdx(i)} className={`px-3 py-2 rounded-lg text-sm font-medium border ${localIdx === i ? 'border-gold bg-gold/10 text-gold' : 'border-white/10 bg-iron text-gray-300 hover:text-gray-50'}`}>{l.nombre}
))}
setLocal('nombre', v)} />
setLocal('tagline', v)} />
setLocal('especialidad', v)} area />
setLocal('descripcion', v)} area />
setLocal('ubicacion', v)} />
setLocal('mapa', v)} hint="Ej. Av. Costanera 1420, Antofagasta — o pega coordenadas lat,long" />
setLocal('horario', v)} />
setLocal('telefono', v)} />
setLocal('capacidadLabel', v)} hint="El tope numérico (6/4/10+) es regla de negocio fija." />
setLocal('perfil', v)} />
Destacados
setLocal('destacados', v)} />
setLocal('menu', v)} />
Se usa en la tarjeta del inicio y en la portada de la página del local.
setLocal('foto_img', v)} ratio="16/10" maxW={1800} />
setSite('local_blur', v)} />
setSite('local_overlay', v)} hint="Menos = foto más visible." />
)}
{/* ---- NOSOTROS ---- */}
{tab === 'nosotros' && (
setNos('intro', v)} area />
setNos('valores', v)} labelA="Valor" labelB="Descripción" />
setNos('hitos', v)} />
setNos('equipo', v)} />
setNos('img', v)} ratio="16/9" maxW={1800} />
Se usa solo si un integrante no tiene foto propia. Asigna la foto de cada uno arriba, en "Equipo".
setNos('equipo_img', v)} ratio="4/3" maxW={1400} />
setNos('blur', v)} />
setNos('overlay', v)} hint="Menos = foto más visible." />
)}
{/* ---- EXPONOR ---- */}
{tab === 'exponor' && (
setExpo('cuerpo', v)} area />
setExpo('subtitulo', v)} />
setExpo('fecha_evento', v)} hint="AAAA-MM-DD" />
setExpo('img', v)} ratio="16/9" maxW={1800} />
setExpo('img2', v)} ratio="16/9" maxW={1800} />
setExpo('img3', v)} ratio="16/9" maxW={1800} />
setExpo('blur', v)} />
setExpo('overlay', v)} hint="Menos = foto más visible." />
)}
{/* ---- CONVENIOS ---- */}
{tab === 'convenios' && (
setConv('intro', v)} area />
setConv('beneficios', v)} labelA="Beneficio" labelB="Descripción" />
Aparece en la sección "Convenios" de la página de Inicio.
setConv('img', v)} ratio="4/3" maxW={1600} />
Aparece dentro de la página de Convenios B2B.
setConv('img2', v)} ratio="2/1" maxW={1600} />
)}
{/* Historial */}
Historial de cambios (últimas 5 versiones)
{history.length === 0 ? (
Aún no hay versiones guardadas. Cada vez que publiques, la versión anterior quedará disponible aquí para restaurar.
) : (
)}
{toast &&
{toast}
}
);
}
Object.assign(window, { CmsLite });