/* ============================================================
PUBLIC · carta para clientes
============================================================ */
const CARTA_THEME = {
aromas: { accent:'#955427', deep:'#74401C', tint:'rgba(149,84,39,.10)', icon:'coffee' },
restaurante: { accent:'#B0512C', deep:'#8C3D1E', tint:'rgba(176,81,44,.10)', icon:'utensils'},
bar: { accent:'#7A3243', deep:'#5F2433', tint:'rgba(122,50,67,.12)', icon:'wine' },
};
const themeFor = (id) => CARTA_THEME[id] || CARTA_THEME.restaurante;
const accentVars = (id) => { const t = themeFor(id); return { '--accent':t.accent, '--accent-deep':t.deep, '--accent-tint':t.tint }; };
function Seal({ brand, size }) {
if (brand.logo) return ;
return GC ;
}
/* ---- product row ---- */
function ProductRow({ p }) {
const { money } = useStore();
if (p.active === false) return null;
const hasVariants = p.variants && p.variants.length > 0;
let priceNode = null;
if (p.priceText) priceNode = {p.priceText} ;
else if (p.price != null) priceNode = {money(p.price)} ;
return (
{p.photo &&
}
{p.featured && }
{p.name}
{!hasVariants && priceNode}
{p.desc &&
{p.desc}
}
{hasVariants && (
{p.variants.map((v, i) => (
{v.label} {money(v.price)}
))}
)}
);
}
/* ---- carta view ---- */
function CartaView({ carta }) {
const [query, setQuery] = useState('');
const [activeCat, setActiveCat] = useState(carta.categories[0]?.id);
const sectionRefs = useRef({});
const theme = themeFor(carta.id);
useEffect(() => { setActiveCat(carta.categories[0]?.id); setQuery(''); window.scrollTo({top:0}); }, [carta.id]);
const q = query.trim().toLowerCase();
const filtered = useMemo(() => {
if (!q) return carta.categories;
return carta.categories.map(cat => ({
...cat,
subcats: cat.subcats.map(s => ({ ...s, products: s.products.filter(p =>
p.active !== false && ((p.name||'').toLowerCase().includes(q) || (p.desc||'').toLowerCase().includes(q))) }))
.filter(s => s.products.length)
})).filter(cat => cat.subcats.length);
}, [carta, q]);
const scrollToCat = (catId) => {
setActiveCat(catId);
const el = sectionRefs.current[catId];
if (el) { const top = el.getBoundingClientRect().top + window.scrollY - 78; window.scrollTo({ top, behavior:'smooth' }); }
};
useEffect(() => {
if (q) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) setActiveCat(e.target.dataset.cat); });
}, { rootMargin: '-78px 0px -70% 0px' });
Object.values(sectionRefs.current).forEach(el => el && obs.observe(el));
return () => obs.disconnect();
}, [carta.id, q]);
const countActive = (cat) => cat.subcats.reduce((n, s) => n + s.products.filter(p => p.active !== false).length, 0);
return (
Categorías
{carta.categories.map(cat => (
scrollToCat(cat.id)}>
{cat.name}
{countActive(cat)}
))}
{carta.kind}
{carta.name}
setQuery(e.target.value)} placeholder={`Buscar en ${carta.name}…`} />
{query && setQuery('')}> }
{carta.categories.map(cat => (
scrollToCat(cat.id)}
style={activeCat===cat.id?{background:'var(--accent)',color:'#fff',borderColor:'var(--accent)'}:null}>
{cat.name}
))}
{filtered.length === 0 && No encontramos resultados para “{query}”.
}
{filtered.map(cat => (
sectionRefs.current[cat.id] = el}>
{cat.name}
{cat.subtitle && {cat.subtitle} }
{cat.subcats.map(sub => {
const prods = sub.products.filter(p => p.active !== false);
if (!prods.length) return null;
return (
);
})}
))}
);
}
/* ---- portada ---- */
function Portada({ brand, cartas, onOpen }) {
return (
Antofagasta · Chile
{brand.name}
{brand.tagline}. Tres cartas, una sola mesa: cocina peruana y criolla, cafetería de especialidad y barra de autor.
{cartas.map(c => {
const t = themeFor(c.id);
let count = 0; c.categories.forEach(cat => cat.subcats.forEach(s => count += s.products.filter(p=>p.active!==false).length));
return (
onOpen(c.id)}>
{c.kind}
{c.name}
{c.categories.length} categorías · {count} productos
Ver carta
);
})}
);
}
/* ---- info modal ---- */
function InfoModal({ brand, onClose, onReserve }) {
const socials = [
brand.instagram && { icon:'instagram', url:`https://instagram.com/${brand.instagram}` },
brand.facebook && { icon:'facebook', url:`https://facebook.com/${brand.facebook}` },
brand.whatsapp && { icon:'whatsapp', url:`https://wa.me/${brand.whatsapp.replace(/[^0-9]/g,'')}` },
].filter(Boolean);
return (
Reservar mesa}>
{socials.length > 0 && <>
Síguenos
>}
);
}
/* ---- reservation modal ---- */
function ReservaModal({ brand, cartas, onClose }) {
const { addReservation, toast, sendMail } = useStore();
const [f, setF] = useState({ nombre:'', telefono:'', email:'', fecha:'', personas:2, local: cartas[0]?.name || '', comentario:'' });
const [done, setDone] = useState(false);
const set = (k,v) => setF(s => ({...s, [k]:v}));
const valid = f.nombre.trim() && f.telefono.trim() && f.fecha;
const submit = async () => {
addReservation(f);
const fechaTxt = (f.fecha||'').replace('T',' ');
// 1) correo al cliente: recibido, pendiente de aprobación
if (f.email) sendMail({ to:f.email, subject:`Recibimos tu solicitud de reserva — ${brand.name}`,
message:`Hola ${f.nombre},\n\nRecibimos tu solicitud de reserva. AÚN NO ESTÁ APROBADA: nuestro equipo la revisará y te enviaremos la confirmación.\n\n• Fecha y hora: ${fechaTxt}\n• Personas: ${f.personas}\n• Local: ${f.local}\n\nGracias por preferirnos.\n${brand.name}` });
// 2) aviso al administrador
if (brand.email) sendMail({ to:brand.email, subject:`Nueva reserva pendiente: ${f.nombre} (${f.personas}p)`,
message:`Nueva solicitud de reserva (pendiente de aprobar en el panel):\n\n• Nombre: ${f.nombre}\n• Teléfono: ${f.telefono}\n• Email: ${f.email}\n• Fecha/hora: ${fechaTxt}\n• Personas: ${f.personas}\n• Local: ${f.local}\n• Comentario: ${f.comentario}\n\nApruébala o recházala en Panel → Reservas.` });
// 3) alternativa por servidor (PHP), si está configurada
const ep = brand.reservationEndpoint;
if (ep) { try { await fetch(ep, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(f) }); } catch(e){} }
setDone(true);
};
const waLink = () => {
const msg = `Hola ${brand.name}, quiero reservar:%0A• Nombre: ${f.nombre}%0A• Fecha/hora: ${f.fecha.replace('T',' ')}%0A• Personas: ${f.personas}%0A• Local: ${f.local}%0A• Tel: ${f.telefono}${f.comentario?('%0A• Nota: '+f.comentario):''}`;
return `https://wa.me/${(brand.whatsapp||'').replace(/[^0-9]/g,'')}?text=${msg}`;
};
if (done) return (
Cerrar
{brand.whatsapp && Confirmar por WhatsApp }
>}>
Gracias, {f.nombre} . Registramos tu solicitud para {f.personas} personas en {f.local} . {f.email ? 'Te enviamos un correo confirmando la recepción; ' : ''}aún está pendiente de aprobación y te contactaremos para confirmarla.
);
return (
Cancelar
Enviar solicitud
>}>
set('nombre',e.target.value)} placeholder="Tu nombre" />
set('telefono',e.target.value)} placeholder="+56 9 …" />
set('email',e.target.value)} placeholder="tucorreo@mail.com" />
set('fecha',e.target.value)} />
set('personas',e.target.value)} />
set('local',e.target.value)}>
{cartas.map(c => {c.name} — {c.kind} )}
);
}
/* ---- legal ---- */
function LegalModal({ docKey, brand, onClose }) {
const year = new Date().getFullYear();
const DOCS = {
privacidad: {
title: 'Política de Privacidad',
body: [
['¿Qué datos recopilamos?', `Cuando solicitas una reserva a través de este sitio recopilamos únicamente los datos que tú nos entregas: nombre, teléfono, correo electrónico (opcional), fecha y hora deseada, número de personas, local y comentarios. No recopilamos datos de pago en este sitio.`],
['¿Para qué los usamos?', `Usamos tus datos exclusivamente para gestionar y confirmar tu reserva y para comunicarnos contigo respecto de ella. No vendemos ni cedemos tu información a terceros con fines comerciales.`],
['Conservación y seguridad', `Conservamos los datos de reservas solo el tiempo necesario para su gestión. Aplicamos medidas razonables para proteger la información.`],
['Tus derechos (Ley N° 19.628)', `Conforme a la Ley N° 19.628 sobre Protección de la Vida Privada, puedes solicitar acceder, rectificar, cancelar u oponerte al tratamiento de tus datos escribiendo a ${brand.email}.`],
['Contacto', `Para cualquier consulta sobre privacidad: ${brand.email} · ${brand.phone} · ${brand.address}.`],
],
},
terminos: {
title: 'Términos y Condiciones',
body: [
['Aceptación', `Al utilizar este sitio web declaras conocer y aceptar estos Términos y Condiciones. Si no estás de acuerdo, te pedimos no utilizar el sitio.`],
['Carta y precios', `La carta, descripciones y precios publicados son referenciales y pueden modificarse sin previo aviso. Ante cualquier diferencia, prevalecen los precios vigentes en el local.`],
['Reservas', `Las solicitudes de reserva enviadas por este sitio NO constituyen una reserva confirmada. Cada solicitud queda sujeta a revisión y aprobación por parte de ${brand.name}, que confirmará o rechazará la disponibilidad por los medios de contacto entregados.`],
['Uso del sitio', `No está permitido usar el sitio con fines ilícitos ni intentar dañar su funcionamiento o seguridad.`],
['Propiedad intelectual', `Todo el contenido del sitio está protegido. Ver "Protección Ley 17.336".`],
['Legislación aplicable', `Estos términos se rigen por las leyes de la República de Chile.`],
],
},
ley17336: {
title: 'Protección · Ley N° 17.336',
body: [
['Propiedad intelectual', `Todos los contenidos de este sitio —incluidos textos, nombres de productos, recetas, fotografías, logotipos, marca, diseño gráfico y código fuente— son de propiedad de ${brand.name} o se utilizan con autorización, y se encuentran protegidos por la Ley N° 17.336 sobre Propiedad Intelectual de la República de Chile y demás normativa aplicable.`],
['Usos no autorizados', `Queda prohibida la reproducción, copia, distribución, comunicación pública, transformación o cualquier otra forma de explotación, total o parcial, de estos contenidos sin autorización previa, expresa y por escrito de ${brand.name}.`],
['Infracciones', `El uso no autorizado podrá ser perseguido conforme a las acciones civiles y penales que franquea la Ley N° 17.336 y la legislación vigente.`],
['Aviso de derechos', `© ${year} ${brand.name}. Todos los derechos reservados.`],
],
},
};
const doc = DOCS[docKey] || DOCS.privacidad;
return (
Entendido}>
{doc.body.map(([h, p], i) => (
))}
);
}
Object.assign(window, { CARTA_THEME, themeFor, accentVars, Seal, ProductRow, CartaView, Portada, InfoModal, ReservaModal, LegalModal });