// Shared building blocks: buttons, badges, dividers, countdown, reveal
// Reveal: IntersectionObserver-driven fade-up.
// Uses a CSS keyframe animation triggered by adding the .is-in class.
// IMPORTANT: when document.hidden is true (e.g. preview iframe not in foreground)
// CSS animations and transitions are paused — so we fall back to instant-show.
const Reveal = ({ children, delay = 0, y = 40, className = "", once = true }) => {
const ref = React.useRef(null);
const [shown, setShown] = React.useState(false);
const [docVisible, setDocVisible] = React.useState(() =>
typeof document === 'undefined' ? true : !document.hidden
);
// Track visibility so we switch from instant-show to animated reveal when the
// tab/iframe becomes visible.
React.useEffect(() => {
const onChange = () => setDocVisible(!document.hidden);
document.addEventListener('visibilitychange', onChange);
return () => document.removeEventListener('visibilitychange', onChange);
}, []);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
let timer = null;
let io = null;
const reveal = () => {
timer = setTimeout(() => setShown(true), Math.max(20, delay * 1000));
};
const vh = window.innerHeight || document.documentElement.clientHeight || 800;
const rect = el.getBoundingClientRect();
if (rect.top < vh * 1.0 && rect.bottom > 0) {
reveal();
if (once) return () => { if (timer) clearTimeout(timer); };
}
io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
reveal();
if (once && io) io.unobserve(entry.target);
} else if (!once) {
setShown(false);
}
});
},
{ threshold: 0, rootMargin: '0px 0px -5% 0px' }
);
io.observe(el);
return () => {
if (timer) clearTimeout(timer);
if (io) io.disconnect();
};
}, [once, delay]);
// When the document is hidden (preview iframes etc.) CSS animations don't run,
// so the reveal would stay invisible. Render plain children with no animation.
if (!docVisible) {
return
{children}
;
}
return (
{children}
);
};
// Big monumental CTA — solid crimson, hover gold
const PrimaryCTA = ({ href = "#", children, large = false, icon = null, ...rest }) => (
{/* corner accents */}
{icon}
{children}
);
// Outlined gold CTA
const GhostCTA = ({ href = "#", children, large = false, icon = null, ...rest }) => (
{icon}
{children}
);
// Ornamental divider with center label
const OrnamentDivider = ({ label = null, className = "" }) => (
{label && (
<>
{label}
>
)}
{!label &&
}
);
// Section heading: kicker + display title
const SectionHeading = ({ kicker, title, subtitle, align = "center" }) => (
{kicker && (
)}
{title}
{subtitle && (
{subtitle}
)}
);
// Pull-quote between sections — gold, large, monumental, i18n-aware
const PullQuote = ({ tKey, children }) => {
const ctx = React.useContext(LangContext);
const text = tKey ? ctx.t(tKey) : children;
return (
);
};
// ============================================================
// COUNTDOWN — Next Sunday 17:00 (America/Sao_Paulo)
// ============================================================
function nextSundayAt17BRT() {
// BRT is UTC-3, no DST
const now = new Date();
// Get current time in São Paulo by offsetting from UTC
const utcMs = now.getTime();
const spOffsetMs = -3 * 60 * 60 * 1000; // BRT = UTC-3
const sp = new Date(utcMs + spOffsetMs);
// sp's UTC fields represent São Paulo wall-clock
const dow = sp.getUTCDay(); // 0=Sun
let daysUntilSun = (7 - dow) % 7;
// Construct candidate Sunday 17:00 in SP wall-clock
const candidateSp = new Date(Date.UTC(sp.getUTCFullYear(), sp.getUTCMonth(), sp.getUTCDate() + daysUntilSun, 17, 0, 0));
// If today is Sunday but past 17:00, push +7
if (candidateSp.getTime() - spOffsetMs <= utcMs) {
candidateSp.setUTCDate(candidateSp.getUTCDate() + 7);
}
// Convert back to real UTC instant
return candidateSp.getTime() - spOffsetMs;
}
const Countdown = () => {
const [target] = React.useState(() => nextSundayAt17BRT());
const [now, setNow] = React.useState(() => Date.now());
const ctx = React.useContext(LangContext);
const tt = ctx ? ctx.t : (k) => k;
React.useEffect(() => {
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
const diff = Math.max(0, target - now);
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff % 86400000) / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
const pad = (n) => String(n).padStart(2, '0');
const units = [
{ label: tt('cd_days'), value: pad(days) },
{ label: tt('cd_hours'), value: pad(hours) },
{ label: tt('cd_minutes'), value: pad(minutes) },
{ label: tt('cd_seconds'), value: pad(seconds) },
];
return (
{units.map((u, i) => (
{/* Card */}
{/* corner ticks */}
{/* horizontal seam */}
{u.value}
{u.label}
{i < units.length - 1 && (
:
)}
))}
);
};
Object.assign(window, {
Reveal, PrimaryCTA, GhostCTA, OrnamentDivider, SectionHeading, PullQuote, Countdown,
});