// site/sections.jsx — page-level sections. Each section is a standalone component.
const { useState, useEffect, useRef } = React;
// ─── parse helper: extract prefix(+), number, suffix(%, x, etc.) from "73%" / "+340" / "2.5x"
function parseStatValue(str) {
if (str == null) return null;
const m = String(str).match(/^(\+|-)?\s*([0-9]+(?:[.,][0-9]+)?)\s*(.*)$/);
if (!m) return null;
const prefix = m[1] || '';
const numStr = m[2];
const suffix = m[3] || '';
const decimals = (numStr.split(/[.,]/)[1] || '').length;
const useComma = numStr.indexOf(',') >= 0;
const num = parseFloat(numStr.replace(',', '.'));
if (isNaN(num)) return null;
return { prefix, num, suffix, decimals, useComma };
}
// ─── useCountUp hook ──────────────────────────────────────────────────────
function useCountUp(target, active) {
const [val, setVal] = useState(active ? target : 0);
const rafRef = useRef(null);
useEffect(() => {
if (!active) return;
const reduce = typeof window !== 'undefined'
&& window.matchMedia
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) { setVal(target); return; }
const duration = 1200;
const start = performance.now();
const tick = (now) => {
const p = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(target * eased);
if (p < 1) rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
}, [target, active]);
return val;
}
function CountUpValue({ raw, active }) {
const parsed = parseStatValue(raw);
if (!parsed) return <>{raw}>;
const current = useCountUp(parsed.num, active);
let formatted;
if (parsed.decimals > 0) {
formatted = current.toFixed(parsed.decimals);
if (parsed.useComma) formatted = formatted.replace('.', ',');
} else {
formatted = String(Math.round(current));
}
return <>{parsed.prefix}{formatted}{parsed.suffix}>;
}
// ─── Nav ──────────────────────────────────────────────────────────────────
function SiteNav({ tweaks }) {
const t = useT();
const { lang, setLang } = useLang();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 16);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
);
}
// ─── Hero ─────────────────────────────────────────────────────────────────
function HeroSection({ variant }) {
const t = useT();
return (
{variant === 'centered' && }
{variant === 'split' && }
{variant === 'stacked' && }
);
}
function HeroChannelsPreview({ t, variant }) {
const channels = ['email', 'whatsapp', 'telegram', 'sms', 'push'];
return (
{channels.map(id => (
{t('ch.' + id + '.name').replace(/\s*\*+$/, '')}
))}
);
}
function HeroCentered({ t }) {
return (
{t('hero.eyebrow')}
{t('hero.payoff')}
{t('hero.subline')}
trackEvent('cta_hero_primary', { section: 'hero' })}>{t('hero.cta.primary')}
trackEvent('cta_hero_secondary', { section: 'hero' })}>{t('hero.cta.secondary')}
);
}
function HeroSplit({ t }) {
return (
{t('hero.eyebrow')}
{t('hero.payoff')}
{t('hero.subline')}
{t('hero.cta.primary')}
{t('hero.cta.secondary')}
);
}
function HeroStacked({ t }) {
return (
{t('hero.payoff')}
{t('hero.c.before')}
{t('hero.c.after')}
{t('hero.subline')}
trackEvent('cta_hero_primary', { section: 'hero' })}>{t('hero.cta.primary')}
trackEvent('cta_hero_secondary', { section: 'hero' })}>{t('hero.cta.secondary')}
);
}
// ─── Trusted-by strip (right under hero) ───────────────────────────────────
function MarqueeStrip() {
const t = useT();
return (
{t('sp.eyebrow')}
{CUSTOMERS.map((c, i) => )}
{CUSTOMERS.map((c, i) => )}
);
}
// ─── Pain section ─────────────────────────────────────────────────────────
function PainSection() {
const t = useT();
return (
{t('pain.eyebrow')}
{t('pain.headline')}
{t('pain.body')}
{t('pain.body2')}
{t('pain.bridge.before')}
{PAIN_ROWS.map(r => - {t(r.before)}
)}
{t('pain.bridge.after')}
{PAIN_ROWS.map(r => - {t(r.after)}
)}
);
}
// ─── Solution bridge — short interstitial ──────────────────────────────────
function SolutionBridge() {
const t = useT();
return (
{t('sol.eyebrow')}
{t('sol.headline')}
);
}
// ─── Channels ─────────────────────────────────────────────────────────────
function ChannelsSection({ layout }) {
const t = useT();
const channels = ['email', 'whatsapp', 'telegram', 'sms', 'push'];
return (
{t('ch.eyebrow')}
{t('ch.headline')}
{t('ch.subline')}
{layout === 'grid' && (
{channels.map(id => (
{t('ch.' + id + '.name')}
{t('ch.' + id + '.tagline')}
{t('ch.' + id + '.body')}
))}
)}
{layout === 'row' && (
{channels.map(id => (
{t('ch.' + id + '.name')}
{t('ch.' + id + '.tagline')}
))}
)}
{layout === 'stack' && (
{channels.map((id, i) => (
{t('ch.' + id + '.name')}
{t('ch.' + id + '.tagline')}
{t('ch.' + id + '.body')}
))}
)}
{t('ch.app.note')}
);
}
// ─── Stats / industry numbers — yellow flood ───────────────────────────────
function StatsSection() {
const t = useT();
const gridRef = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = gridRef.current;
if (!el || inView) return;
const obs = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
setInView(true);
obs.disconnect();
break;
}
}
}, { threshold: 0.3 });
obs.observe(el);
return () => obs.disconnect();
}, [inView]);
return (
{t('stats.eyebrow')}
{t('stats.headline')}
{t('stats.subline')}
{STATS.map(s => (
{t(s.labelKey)}
{t(s.sourceKey)}
))}
);
}
// ─── Customer logos grid ──────────────────────────────────────────────────
function CustomersSection() {
const t = useT();
return (
{t('sp.eyebrow')}
{t('sp.headline')}
{CUSTOMERS.map((c, i) => )}
);
}
// ─── Pricing ──────────────────────────────────────────────────────────────
function PricingSection({ layout }) {
const t = useT();
return (
{t('pr.eyebrow')}
{t('pr.headline')}
{t('pr.subline')}
{layout === 'cards' &&
}
{layout === 'table' && (
<>
>
)}
{t('pr.note.1')}
{t('pr.note.2')}
);
}
function PricingCards({ t }) {
return (
{PLANS.map(plan => (
{plan.popular && {t('pr.popular')}
}
{t(plan.nameKey)}
{t(plan.tagKey)}
{t(plan.priceKey)}
{plan.id !== 'free' && plan.id !== 'custom' && {t('pr.month')}}
{plan.id !== 'free' && plan.id !== 'custom' && {t('pr.billing')}
}
{plan.id === 'custom' ? t('pr.contactsRaw', { n: t(plan.contactsKey) }) : t('pr.contactsUpTo', { n: t(plan.contactsKey) })}
{plan.features.map(f => (
- {t(f)}
))}
trackEvent('cta_plan', { plan_id: plan.id })}>
{t(plan.ctaKey)}
))}
);
}
function PricingTable({ t }) {
// Compact comparison table — same content, denser layout.
const rows = [
{ label: 'pr.contactsUpTo.label', values: ['pr.free.contacts','pr.starter.contacts','pr.pro.contacts'], isContact: true },
{ label: null, headerPrice: true },
{ label: 'pr.free.f1', values: ['pr.free.f1','pr.starter.f1','pr.pro.f1'] },
{ label: 'pr.free.f2', values: ['pr.free.f2','pr.starter.f2','pr.pro.f2'] },
{ label: 'pr.free.f3', values: ['pr.free.f3','pr.starter.f3','pr.pro.f3'] },
{ label: 'pr.free.f4', values: ['pr.free.f4','pr.starter.f4','pr.pro.f4'] },
{ label: 'pr.free.f5', values: ['pr.free.f5','pr.starter.f5','pr.pro.f5'] },
];
return (
{PLANS.map(p => (
{p.popular &&
{t('pr.popular')}}
{t(p.nameKey)}
{t(p.priceKey)}
{p.id !== 'free' && p.id !== 'custom' && {t('pr.month')}}
{p.id !== 'free' && p.id !== 'custom' &&
{t('pr.billing')}
}
{p.id === 'custom' ? t('pr.contactsRaw', { n: t(p.contactsKey) }) : t('pr.contactsUpTo', { n: t(p.contactsKey) })}
))}
{[0,1,2,3,4].map(i => (
{[i18nLabelFor(i)]}
{PLANS.map(p => {
const baseKey = 'pr.' + p.id + '.f' + (i+1);
const valKey = baseKey + '.value';
const valRaw = t(valKey);
const val = valRaw === valKey ? t(baseKey) : valRaw;
const lc = val.toLowerCase();
const notIncluded = val === '—' || lc.includes('non incluso') || lc.includes('not included');
return (
{notIncluded ? : <>{val}>}
);
})}
))}
{PLANS.map(p => (
trackEvent('cta_plan', { plan_id: p.id })}>
{t(p.ctaKey)}
))}
);
}
// helper for pricing-table row labels (the channel name shown on the left col)
function i18nLabelFor(i) {
const labels = ['Email', 'Telegram', 'WhatsApp *', 'Push **', 'SMS'];
return labels[i];
}
function NotIncludedIcon() {
return (
);
}
function CheckIcon() {
return (
);
}
// ─── FAQ ──────────────────────────────────────────────────────────────────
function FaqSection() {
const t = useT();
const [open, setOpen] = useState(0);
return (
{t('faq.eyebrow')}
{t('faq.headline')}
{FAQS.map((f, i) => (
{ if (e.currentTarget.open) { trackEvent('faq_open', { index: i }); setOpen(i); } }}>
{t(f.q)}
{t(f.a)}
))}
);
}
// ─── Final CTA ────────────────────────────────────────────────────────────
function FinalCta() {
const t = useT();
return (
);
}
// ─── Footer ───────────────────────────────────────────────────────────────
function SiteFooter() {
const t = useT();
const { lang, setLang } = useLang();
return (
);
}
Object.assign(window, {
SiteNav, HeroSection, MarqueeStrip, PainSection, SolutionBridge,
ChannelsSection, StatsSection, CustomersSection, PricingSection,
FaqSection, FinalCta, SiteFooter,
});