// ===== RIVAMAR Group — Componentes compartidos (v1.1) ===== const { useState, useEffect, useRef } = React; // --- Marca / Logotipo (editable desde el CMS) --- function Logo({ size = 'base', onClick }) { const s = (typeof window !== 'undefined' && window.SITE) || {}; const nombre = s.brand_nombre || 'RIVAMAR'; const sub = s.brand_sub != null ? s.brand_sub : 'GROUP'; const inicial = s.brand_inicial || nombre.charAt(0) || 'R'; const dim = size === 'lg' ? 'text-2xl' : 'text-xl'; return ( ); } // --- Placeholder de imagen (rayado industrial) o imagen real si hay src --- function ImgSlot({ label, className = '', tone = '#D4A52C', ratio, src }) { const style = {}; if (ratio) style.aspectRatio = ratio; if (src) { return (
{label
); } const placeholderStyle = { ...style, backgroundColor: '#141c26', backgroundImage: `repeating-linear-gradient(135deg, ${tone}16 0px, ${tone}16 2px, transparent 2px, transparent 11px)`, }; return (
[ imagen ] {label}
); } // --- Subida de imagen: archivo → dataURL reescalado (cabe en localStorage) --- function fileToScaledDataURL(file, maxW = 1400, quality = 0.82) { return new Promise((resolve, reject) => { if (!file || !file.type.startsWith('image/')) { reject(new Error('No es una imagen')); return; } const reader = new FileReader(); reader.onerror = reject; reader.onload = (e) => { const img = new Image(); img.onerror = reject; img.onload = () => { const scale = Math.min(1, maxW / img.width); const w = Math.round(img.width * scale), h = Math.round(img.height * scale); const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); try { resolve(canvas.toDataURL('image/jpeg', quality)); } catch (err) { reject(err); } }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } // --- Selector de imagen reutilizable: dropzone + URL --- function ImagePicker({ value, onChange, ratio = '16/10', maxW = 1400 }) { const [drag, setDrag] = useState(false); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); const inputRef = useRef(null); const handleFile = async (file) => { setErr(''); setBusy(true); try { onChange(await fileToScaledDataURL(file, maxW)); } catch (e) { setErr('No se pudo procesar la imagen.'); } setBusy(false); }; const onDrop = (e) => { e.preventDefault(); setDrag(false); const f = e.dataTransfer.files && e.dataTransfer.files[0]; if (f) handleFile(f); }; return (
{/* Dropzone */}
inputRef.current && inputRef.current.click()} onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={onDrop} className={`cursor-pointer rounded-lg border-2 border-dashed grid place-items-center text-center px-4 py-7 transition-colors ${drag ? 'border-gold bg-gold/5' : 'border-white/15 bg-carbon hover:border-white/30'}`}>
{busy ? 'Procesando…' : 'Subir imagen desde tu dispositivo'}
e.target.files[0] && handleFile(e.target.files[0])} />
o
{/* URL */} onChange(e.target.value)} />
{err &&

{err}

} {value && (
)}
); } // --- Badge de estado de reserva --- function EstadoBadge({ estado }) { const map = { pendiente: { bg: 'bg-orange-500/15', text: 'text-orange-400', dot: 'bg-orange-400', label: 'Pendiente' }, confirmada: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-400', label: 'Confirmada' }, cancelada: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-400', label: 'Cancelada' }, eliminada: { bg: 'bg-gray-500/15', text: 'text-gray-400', dot: 'bg-gray-500', label: 'Eliminada' }, }; const s = map[estado] || map.pendiente; return ( {s.label} ); } // --- Píldora EXPONOR --- function ExponorPill({ className = '' }) { return ( EXPONOR −15% ); } // --- Botón --- function Btn({ children, onClick, variant = 'primary', className = '', type = 'button', disabled }) { const base = 'fx-shine inline-flex items-center justify-center gap-2 font-semibold rounded-lg transition-all min-h-[44px] px-5 text-sm tracking-wide disabled:opacity-50 disabled:cursor-not-allowed'; const variants = { primary: 'bg-gold text-carbon hover:bg-copper hover:text-gray-50 shadow-lg shadow-gold/20 active:scale-[0.98]', gold: 'bg-gold text-carbon hover:bg-goldBright shadow-lg shadow-gold/25 active:scale-[0.98]', copper: 'bg-copper text-gray-50 hover:bg-copperDark shadow-lg shadow-copper/20 active:scale-[0.98]', ghost: 'bg-white/5 text-gray-100 hover:bg-white/10 border border-white/10', outline: 'bg-transparent text-gold border border-gold/50 hover:bg-gold/10', }; return ( ); } // --- Navbar corporativo --- function Navbar({ view, go }) { const [open, setOpen] = useState(false); const [localesOpen, setLocalesOpen] = useState(false); const links = [ { id: 'home', label: 'Inicio' }, { id: 'galeria', label: 'Galería' }, { id: 'nosotros', label: 'Nosotros' }, { id: 'convenios', label: 'Convenios B2B' }, ]; const linkCls = (id) => `fx-underline relative px-3 py-2 rounded-md text-sm font-medium transition-colors min-h-[44px] flex items-center ${ view === id ? 'text-gold bg-white/5' : 'text-gray-300 hover:text-gray-50' }`; return (
go('home')} />
go('reservas')} className="!px-4">Reservar Mesa
{open && (

Nuestros locales

{LOCALES.map((l) => ( ))}
{ go('reservas'); setOpen(false); }} className="mt-2">Reservar Mesa (15% Dcto.)
)}
); } function ChevronDown() { return ; } function LockIcon() { return ; } // --- Barra EXPONOR sticky (solo móvil) — maximiza conversión --- function ExponorMobileBar({ go }) { const [closed, setClosed] = useState(false); if (closed) return null; return (
15%

Descuento EXPONOR 2026

Reserva con tu credencial

); } // --- Utilidad: escribir en una ruta tipo 'site.hero_titulo' o 'locales.0.nombre' --- function setPath(obj, path, value) { const keys = path.split('.'); const root = Array.isArray(obj) ? obj.slice() : { ...obj }; let cur = root; for (let i = 0; i < keys.length - 1; i++) { const k = keys[i]; const next = cur[k]; cur[k] = Array.isArray(next) ? next.slice() : { ...next }; cur = cur[k]; } cur[keys[keys.length - 1]] = value; return root; } function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>'); } // --- Texto editable inline (solo en modo edición del admin) --- function Editable({ as = 'span', value, path, edit, onEdit, className = '' }) { if (!edit) return React.createElement(as, { className }, value); return React.createElement(as, { className, 'data-cms': path, title: 'Clic para editar', contentEditable: true, suppressContentEditableWarning: true, spellCheck: false, onBlur: (e) => { const t = e.currentTarget.innerText.trim(); if (t !== value) onEdit(path, t); }, dangerouslySetInnerHTML: { __html: escapeHtml(value) }, }); } // --- Hook de scroll reveal (no-op: la lógica vive en un script global del HTML, // inmune a los re-renders de React) --- function useReveal(dep) { useEffect(() => { if (window.__revealKick) window.__revealKick(); }, [dep]); } // --- Barra flotante de Modo Edición (visible solo tras login admin) --- function EditModeBar({ edit, setEdit, go }) { return (
{edit ? 'Modo edición activo — clic sobre los textos' : 'Vista previa'}
); } // --- Banner Revista Oficial MCH (reutilizable, editable desde CMS) --- function RevistaMCH({ className = '' }) { const s = (typeof window !== 'undefined' && window.SITE) || {}; const url = s.mch_url || 'https://www.mch.cl/'; const eyebrow = s.mch_eyebrow || 'Prensa minera oficial'; const titulo = s.mch_titulo || 'Aparecemos en la Revista Oficial de la Minería Chilena'; const texto = s.mch_texto || 'Conoce la cobertura de EXPONOR 2026 y descubre por qué las principales empresas mineras del país eligen a RIVAMAR Group.'; const boton = s.mch_boton || 'Ver revista oficial'; return (
{eyebrow}

{titulo}

{texto}

{boton}
); } // --- Botón "ir arriba" (aparece al bajar) --- function ScrollTop() { const [show, setShow] = useState(false); useEffect(() => { const onScroll = () => setShow(window.scrollY > 500); window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); if (!show) return null; return ( ); } // --- Botón flotante de WhatsApp --- function WhatsAppFAB({ whatsapp = '56900000000' }) { const [hover, setHover] = useState(false); const msg = encodeURIComponent('¡Hola! Me gustaría hacer una consulta sobre reservas en RIVAMAR Group.'); return ( setHover(true)} onMouseLeave={() => setHover(false)} className="fixed bottom-20 lg:bottom-6 right-6 z-50 flex items-center gap-3 group"> {hover && ( Habla con un ejecutivo )} ); } // --- Footer --- function SocialIcon({ kind }) { const p = { ig: , fb: , in: , }; return {p[kind]}; } function Footer({ go, cms }) { const s = cms || (typeof window !== 'undefined' && window.SITE) || {}; const socials = [['ig', s.social_ig], ['fb', s.social_fb], ['in', s.social_in]]; return (

{s.footer_desc}

{socials.map(([k, href]) => ( ))}

Navegación

    {[['home','Inicio'],['reservas','Reservas'],['galeria','Galería'],['convenios','Convenios B2B'],['exponor','EXPONOR 2026'],['privacidad','Privacidad'],['admin','Panel Admin']].map(([id,l]) => (
  • ))}

Nuestros locales

{LOCALES.map((l) => (

{l.ubicacion}

{l.telefono}

))}
{s.footer_copy || '© 2026 RIVAMAR Group — Antofagasta, Chile · MAEDY GROUP'}
); } // --- Sección con título --- function SectionHead({ eyebrow, title, desc, center }) { return (
{eyebrow &&

{eyebrow}

}

{title}

{desc &&

{desc}

}
); } // --- Conteo animado (estadísticas) --- function CountUp({ end, suffix = '', dur = 1400 }) { const [val, setVal] = useState(0); const ref = useRef(null); useEffect(() => { let started = false; const obs = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !started) { started = true; const t0 = performance.now(); const tick = (t) => { const p = Math.min((t - t0) / dur, 1); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(end * eased)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }, { threshold: 0.4 }); if (ref.current) obs.observe(ref.current); return () => obs.disconnect(); }, [end, dur]); return {val}{suffix}; } Object.assign(window, { Logo, ImgSlot, EstadoBadge, ExponorPill, Btn, Navbar, WhatsAppFAB, Footer, SectionHead, LockIcon, CountUp, SocialIcon, ExponorMobileBar, ImagePicker, fileToScaledDataURL, setPath, escapeHtml, Editable, useReveal, EditModeBar, ScrollTop, RevistaMCH, });