/* PRODUCTO SAAS — página individual (template compartido) */
const { useState: useStateD, useEffect: useEffectD } = React;
/* ===== Explorador de módulos interactivo (demo funcional · datos hardcodeados) ===== */
const MOD_ICONS = {
cart: <> >,
box: <> >,
tag: <> >,
cash: <> >,
users: <> >,
user: <> >,
truck: <> >,
chart: <> >,
gear: <> >,
calendar: <> >,
bed: <> >,
bell: <> >,
broom: <> >,
food: <> >,
receipt: <> >,
folder: <> >,
cross: <> >,
flask: <> >,
pill: <> >,
};
const ModIcon = ({ name }) => (
{MOD_ICONS[name] || }
);
const useTickerD = (interval = 2400) => {
const [t, setT] = useStateD(0);
useEffectD(() => { const id = setInterval(() => setT(x => x + 1), interval); return () => clearInterval(id); }, [interval]);
return t;
};
const useIsMobileD = () => {
const [m, setM] = useStateD(false);
useEffectD(() => {
const mq = window.matchMedia('(max-width: 880px)');
const on = () => setM(mq.matches);
on();
if (mq.addEventListener) mq.addEventListener('change', on); else mq.addListener(on);
return () => { if (mq.removeEventListener) mq.removeEventListener('change', on); else mq.removeListener(on); };
}, []);
return m;
};
const AnimatedValue = ({ text }) => {
const str = String(text);
const match = str.match(/^([^\d]*)([\d.,]+)(.*)$/);
const target = match ? parseFloat(match[2].replace(/,/g, '')) : null;
const [v, setV] = useStateD(target || 0);
useEffectD(() => {
if (target === null) return;
let raf; const start = performance.now(); const dur = 850;
const step = (now) => {
const t = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - t, 3);
setV(target * eased);
if (t < 1) raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [text]);
if (target === null) return <>{text}>;
const decimals = (match[2].split('.')[1] || '').length;
const shown = target >= 1000 ? Math.round(v).toLocaleString('es-MX') : v.toFixed(decimals);
return <>{match[1]}{shown}{match[3]}>;
};
const DemoChart = ({ title, legend }) => {
const tick = useTickerD(2400);
const uid = (React.useId ? React.useId() : 'demo').replace(/[^a-zA-Z0-9]/g, '');
const W = 600, H = 120, P = 4;
const gen = (base) => { let y = base; const out = []; for (let i = 0; i < 26; i++) { y += (Math.random() - 0.45) * 14; y = Math.max(18, Math.min(94, y)); out.push(y); } return out; };
const a = React.useMemo(() => gen(55), [tick]);
const b = React.useMemo(() => a.map(y => Math.max(12, y - 14 - Math.random() * 10)), [a]);
const line = (arr) => arr.map((y, i) => { const x = P + (i / (arr.length - 1)) * (W - P * 2); const yy = H - P - (y / 100) * (H - P * 2); return `${i ? 'L' : 'M'} ${x.toFixed(1)} ${yy.toFixed(1)}`; }).join(' ');
const fill = (arr) => line(arr) + ` L ${W - P} ${H - P} L ${P} ${H - P} Z`;
return (
{title}
{legend && {legend.map((l, i) => {l.l} )} }
{[0, 1, 2, 3].map(i => )}
);
};
const classifyStatus = (v) => {
if (/^↓/.test(v) || /-\d/.test(v)) return 'warn';
if (/^↑/.test(v)) return '';
const WARN = ['Bajo', 'Crítico', 'Inactivo', 'Pendiente', 'Ausente', 'Falta firma', 'Por confirmar', 'Anulada'];
const INFO = ['En proceso', 'En consulta', 'Nuevo', 'Recordatorio enviado', 'Crédito'];
if (WARN.includes(v)) return 'warn';
if (INFO.includes(v)) return 'info';
return '';
};
const ROOM_COLORS = { free: ['#22c55e', 'Libre'], busy: ['#06b6d4', 'Ocupada'], clean: ['#f59e0b', 'Limpieza'], maint: ['#ef4444', 'Mantenim.'] };
const DemoBlock = ({ b }) => {
if (b.type === 'chart') return ;
if (b.type === 'table') return (
{b.cols.map((c, i) => {c} )}
{b.rows.map((r, ri) => (
{r.map((cell, ci) => {
if (ci === r.length - 1 && b.status) return {cell} ;
return {cell} ;
})}
))}
);
if (b.type === 'rows') return (
{b.items.map((it, i) => (
))}
);
if (b.type === 'grid') return (
{Object.keys(ROOM_COLORS).map(k => (
{ROOM_COLORS[k][1]}
))}
{b.items.map((it, i) => {
const c = (ROOM_COLORS[it.s] || ROOM_COLORS.free)[0];
return (
);
})}
);
return null;
};
const DemoKpis = ({ items }) => (
{items.map((k, i) => (
{k.l}
{k.down ? '↓' : '↑'} {k.d}
))}
);
const ModuleExplorer = ({ product, slug }) => {
const allDemos = (window.MartzProducts.moduleDemos && window.MartzProducts.moduleDemos[slug]) || {};
const mods = product.modules.filter(m => allDemos[m]);
const [active, setActive] = useStateD(mods[0]);
const isMobile = useIsMobileD();
const demo = allDemos[active] || { kpis: [], blocks: [] };
const app = slug === 'martz-pos' ? 'mi-tienda' : slug === 'martz-hospitality' ? 'mi-hotel' : 'mi-clinica';
const tabStyle = (on) => ({ flex: '0 0 auto', padding: '8px 14px', borderRadius: 8, border: '1px solid ' + (on ? 'rgba(34,197,94,0.4)' : 'rgba(255,255,255,0.1)'), background: on ? 'rgba(34,197,94,0.12)' : 'rgba(255,255,255,0.03)', color: on ? '#4ade80' : 'rgba(255,255,255,0.7)', fontSize: 12.5, fontWeight: on ? 600 : 500, cursor: 'pointer', whiteSpace: 'nowrap', fontFamily: 'inherit' });
return (
app.martzlab.com/{app}
{isMobile && (
{mods.map(m => setActive(m)} style={tabStyle(m === active)}>{m} )}
)}
{!isMobile && (
Módulos
{mods.map(m => (
setActive(m)}>
{m}
))}
)}
{active}
En vivo · datos de demostración
{demo.kpis && demo.kpis.length > 0 && }
{(demo.blocks || []).map((b, i) => )}
);
};
const ScreenshotPlaceholder = ({ label, src }) => (
{label}
{src ? (
) : (
Captura de pantalla · próximamente
)}
);
const DetailFaq = ({ items }) => {
const [open, setOpen] = useStateD(0);
return (
{items.map((f, i) => (
setOpen(open === i ? -1 : i)}
style={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit', padding: 0 }}
>
{f.q}
{open === i &&
{f.a}
}
))}
);
};
const ProductoDetailApp = () => {
const slug = window.PRODUCT_SLUG;
const product = window.MartzProducts.details[slug];
if (!product) return Producto no encontrado.
;
const summary = window.MartzProducts.list.find(p => p.slug === slug);
const isLive = product.status === 'Disponible';
const hasDetailedPlans = product.plans && product.plans.some(plan => plan.includes);
return (
<>
{product.name}
{product.tag}
{product.heroDesc}
Descripción
Hecho para {product.audiences.slice(0, 3).join(', ').toLowerCase()} y más.
{product.intro}
{product.audiences.map(a => (
{a}
))}
Características
Lo que incluye {product.name}.
{product.features.map(f => (
))}
Módulos
Todo lo que necesitas, integrado.
Explora cada módulo como si estuvieras dentro del sistema. Haz clic para navegar — los datos son de demostración.
Beneficios
Impacto medible en tu operación.
{product.benefits.map(b => (
))}
Vista previa
Así se ve {product.name}.
{slug === 'martz-pos' ? (
<>
>
) : (
<>
>
)}
{product.plansEyebrow || 'Planes'}
{product.plansTitle || (isLive ? 'Planes simples para empezar y crecer.' : 'Próximamente con precios accesibles.')}
{(product.plansSubtitle || isLive) &&
{product.plansSubtitle || 'Elige el nivel de operación que mejor se adapta a tu negocio. Puedes empezar pequeño y crecer sin cambiar de sistema.'}
}
{product.plans.map(plan => (
{plan.featured &&
{plan.featuredLabel || (isLive ? 'Recomendado' : 'Lista de espera')} }
{plan.name}
{plan.value &&
{plan.value}
}
Precio
{plan.price}{plan.unit}
{plan.capacity || plan.note}
{plan.priceNote &&
{plan.priceNote}
}
{plan.includes && (
{plan.includes.map(item => (
{item}
))}
)}
{plan.idealFor &&
Ideal para: {plan.idealFor}
}
{plan.cta} →
))}
{product.plansNote && (
{product.plansNote.title}
{product.plansNote.text}
)}
{(product.launchNote || product.waitlistNote) && (
{product.launchNote &&
{product.launchNote}
}
{product.waitlistNote &&
{product.waitlistNote}
}
)}
{product.addOns && (
{product.addOns.title}
{product.addOns.text}
{product.addOns.items.map(item => {item} )}
)}
Preguntas frecuentes
Dudas sobre {product.name}.
{isLive ? <>Empieza con {product.name} hoy.> : <>Sé de los primeros en probar {product.name} .>}
{isLive ? 'Demo en vivo de 30 minutos. Sin compromiso, sin tarjeta de crédito.' : 'Únete a la lista de espera y recibe acceso anticipado con precio preferencial.'}
>
);
};
ReactDOM.createRoot(document.getElementById('root')).render( );