// KineSuite landing — extra sections: animated hero phone,
// clinics (B2B), changelog, cookie banner.
const { useState: useS, useEffect: useE, useRef: useR } = React;
function usePhoneWidth(max = 360, min = 260) {
const [w, setW] = useS(max);
useE(() => {
const update = () => {
const next = Math.min(max, Math.max(min, window.innerWidth - 48));
setW(next);
};
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, [max, min]);
return w;
}
function HeroMockDisclaimer({ lang }) {
const text = LANDING_COPY[lang].heroMockDisclaimer;
return (
{text}
);
}
function HeroPhoneStack({ lang, width: widthProp, animated, children }) {
const responsiveW = usePhoneWidth();
const width = widthProp != null ? Math.min(widthProp, responsiveW) : responsiveW;
return (
{animated ? (
) : (
{children}
)}
);
}
// ─────────────────────────────────────────────────────────────
// AnimatedHeroPhone — auto-cycles through 3 app screens
// Catalog → Eval → Result → loop, with a crossfade and a
// play/pause control. Keeps the same LandingPhone bezel.
// ─────────────────────────────────────────────────────────────
function AnimatedHeroPhone({ lang, width = 360 }) {
const screens = ['catalog', 'eval', 'result'];
const labels = {
es: { catalog: 'Catálogo', eval: 'Evaluación · Berg', result: 'Resultado' },
en: { catalog: 'Library', eval: 'Assessment · Berg', result: 'Result' },
};
const [idx, setIdx] = useS(0);
const [paused, setPaused] = useS(false);
useE(() => {
if (paused) return;
const id = setInterval(() => setIdx((i) => (i + 1) % screens.length), 3400);
return () => clearInterval(id);
}, [paused]);
const renderScreen = (kind) => {
if (kind === 'eval') return ;
if (kind === 'result') return ;
return ;
};
return (
{/* Crossfade stack: all 3 screens, the active one is opacity 1 */}
{screens.map((s, i) => (
{renderScreen(s)}
))}
{/* Step indicator + play / pause */}
{screens.map((s, i) => (
{labels[lang][screens[idx]].toUpperCase()}
);
}
// ─────────────────────────────────────────────────────────────
// ClinicsSection — B2B card, different visual treatment
// ─────────────────────────────────────────────────────────────
function ClinicsSection({ t, lang }) {
return (
{/* Decorative arc, brand mark echoed top-right */}
{t.clinicsEyebrow}
{t.clinicsTitle}
{t.clinicsSub}
{t.clinicsEmail}
);
}
// ─────────────────────────────────────────────────────────────
// ChangelogSection — timeline of versions
// ─────────────────────────────────────────────────────────────
function ChangelogSection({ t }) {
return (
{t.changelogEyebrow}
{t.changelogTitle}
{t.changelogSub}
{t.changelogItems.map((v, i) => {
const isFirst = i === 0;
return (
-
{v.version}
{v.date}
{isFirst && (
{t.changelogLatest}
)}
{v.title}
{v.notes.map((n, j) => (
-
{n}
))}
);
})}
);
}
// ─────────────────────────────────────────────────────────────
// CookieBanner — minimal, dismissible, persists via localStorage
// ─────────────────────────────────────────────────────────────
function CookieBanner({ t }) {
const [visible, setVisible] = useS(false);
const [details, setDetails] = useS(false);
useE(() => {
try {
const accepted = localStorage.getItem('kinesuite-cookies');
if (!accepted) {
// Slight delay so the banner doesn't fight the page mount
const id = setTimeout(() => setVisible(true), 700);
return () => clearTimeout(id);
}
} catch (e) { /* private mode: just show it */ setVisible(true); }
}, []);
const dismiss = (mode) => {
try { localStorage.setItem('kinesuite-cookies', mode); } catch {}
setVisible(false);
};
if (!visible) return null;
return (
{t.cookieBody}
{details && (
{t.cookieDetails.map((d, i) => (
-
{d.t}
{d.required ? t.cookieRequired : t.cookieOptional}
))}
)}
);
}
Object.assign(window, {
HeroPhoneStack, HeroMockDisclaimer,
AnimatedHeroPhone, ClinicsSection, ChangelogSection, CookieBanner,
});