// Main app — interactive layer over the static HTML
// Handles: prayer modal, request form, commitment counter, tweaks panel, scroll reveals
const { useState, useEffect, useRef } = React;
function t(key) { return (window.I18N && window.I18N.t(key)) || key; }
function useLang() {
const [lang, setLangState] = useState(() => (window.I18N ? window.I18N.getLang() : 'pt'));
useEffect(() => {
const h = (e) => setLangState(e.detail.lang);
window.addEventListener('langchange', h);
return () => window.removeEventListener('langchange', h);
}, []);
return lang;
}
const MYSTERY_IDS = ['joyful', 'sorrowful', 'glorious', 'luminous'];
// ─── Tweaks defaults ─────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"palette": ["#3A6CA2","#FFFFFF","#2F6099","#E6C57C"],
"fontSize": 19,
"showCandles": true,
"particleIntensity": "soft"
}/*EDITMODE-END*/;
// ─── Prayer modal ────────────────────────────────────────────────
function PrayerModal({ open, onClose }) {
const lang = useLang();
const [currentPrayer, setCurrentPrayer] = useState('hail-mary');
const [currentMystery, setCurrentMystery] = useState(0);
if (!open) return null;
const prayerTitle = t('prayer.' + currentPrayer + '.title');
const prayerBody = t('prayer.' + currentPrayer + '.body');
const mysteryId = MYSTERY_IDS[currentMystery];
const mysteryName = t('mystery.' + mysteryId + '.name');
const mysteryDay = t('mystery.' + mysteryId + '.day');
const mysteryList = t('mystery.' + mysteryId + '.list');
function handlePrayerClick(prayerKey, label) {
setCurrentPrayer(prayerKey);
}
return (
e.stopPropagation()} style={{ maxWidth: '900px' }}>
{t('modal.eyebrow')}
{t('modal.title')}
{t('modal.subtitle')}
{t('modal.prayer.label')}
{prayerTitle}
{prayerBody}
{t('modal.mysteries.label')}
{MYSTERY_IDS.map((id, i) => (
))}
{mysteryDay}
{mysteryList.map((item, i) => (
- {item}
))}
);
}
// ─── Commitment counter ─────────────────────────────────────────
function CommitmentCounter() {
useLang();
// Real-feel counter: starts at a number, increments on commit
const STORAGE_KEY = 'apostolado_commit_v1';
const COUNT_KEY = 'apostolado_count_v1';
const initialCount = 12473;
const [count, setCount] = useState(() => {
const stored = parseInt(localStorage.getItem(COUNT_KEY) || '');
return isNaN(stored) ? initialCount : stored;
});
const [committed, setCommitted] = useState(() => localStorage.getItem(STORAGE_KEY) === '1');
const [bump, setBump] = useState(false);
const counterRef = useRef(null);
// Animate count from initialCount to count on mount
useEffect(() => {
const el = counterRef.current;
if (!el) return;
const target = count;
const duration = 1800;
const start = performance.now();
let frame;
function tick(t) {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
const val = Math.round(target * eased);
el.textContent = val.toLocaleString('pt-BR');
if (p < 1) frame = requestAnimationFrame(tick);
}
// Only animate on first mount
const seen = sessionStorage.getItem('counter_seen');
if (!seen) {
frame = requestAnimationFrame(tick);
sessionStorage.setItem('counter_seen', '1');
} else {
el.textContent = target.toLocaleString('pt-BR');
}
return () => frame && cancelAnimationFrame(frame);
}, []);
useEffect(() => {
if (counterRef.current && sessionStorage.getItem('counter_seen')) {
counterRef.current.textContent = count.toLocaleString('pt-BR');
}
}, [count]);
function handleCommit() {
if (committed) return;
setCommitted(true);
localStorage.setItem(STORAGE_KEY, '1');
const next = count + 1;
setCount(next);
localStorage.setItem(COUNT_KEY, String(next));
setBump(true);
setTimeout(() => setBump(false), 600);
}
return (
<>
{initialCount.toLocaleString('pt-BR')}
{t('crusade.counter.label')}
{committed && (
{t('crusade.thanks')}
{t('crusade.thanks2')}
)}
>
);
}
// ─── Prayer request form ────────────────────────────────────────
function PrayerRequestForm() {
useLang();
const [submitted, setSubmitted] = useState(false);
const [data, setData] = useState({ name: '', city: '', intention: '' });
function submit(e) {
e.preventDefault();
if (!data.name || !data.intention) return;
// Persist locally
const all = JSON.parse(localStorage.getItem('prayer_requests') || '[]');
all.push({ ...data, at: Date.now() });
localStorage.setItem('prayer_requests', JSON.stringify(all));
setSubmitted(true);
}
if (submitted) {
return (
);
}
return (
);
}
// ─── Candle component ───────────────────────────────────────────
function VirtualCandle() {
useLang();
const [lit, setLit] = useState(() => localStorage.getItem('candle_lit') === '1');
const [intention, setIntention] = useState(() => localStorage.getItem('candle_intention') || '');
const [showInput, setShowInput] = useState(!lit);
const [draft, setDraft] = useState('');
function light() {
if (!draft.trim()) return;
setLit(true);
setIntention(draft);
setShowInput(false);
localStorage.setItem('candle_lit', '1');
localStorage.setItem('candle_intention', draft);
}
function relight() {
setLit(false);
setIntention('');
setDraft('');
setShowInput(true);
localStorage.removeItem('candle_lit');
localStorage.removeItem('candle_intention');
}
return (
{showInput && (
setDraft(e.target.value)}
style={{
width: 280, maxWidth: '90%', padding: '12px 16px',
background: 'transparent', border: 0,
borderBottom: '1px solid var(--gold)',
color: 'var(--ink)', fontFamily: 'var(--serif-body)',
fontSize: '1rem', fontStyle: 'italic', textAlign: 'center',
outline: 'none'
}}
/>
)}
{!showInput && lit && (
“{intention}”
{t('candle.burning')}
)}
);
}
// ─── Cruzada Mundial: formulário + mapa ─────────────────────────
const CRUSADE_GOAL = 1000000;
const CRUSADE_PINS_KEY = 'apostolado_crusade_pins_v1';
function loadPins() {
try {
const stored = JSON.parse(localStorage.getItem(CRUSADE_PINS_KEY) || 'null');
if (Array.isArray(stored)) return stored;
} catch (e) {}
return [];
}
function CrusadeFormModal({ open, onClose, onSubmitted }) {
useLang();
const [name, setName] = useState('');
const [city, setCity] = useState('');
const [country, setCountry] = useState('');
const [countryAuto, setCountryAuto] = useState(true);
const [coords, setCoords] = useState(null);
useEffect(() => {
if (!city.trim()) { setCountry(''); setCoords(null); return; }
const hit = window.guessCityCountry && window.guessCityCountry(city);
if (hit) {
const lang = window.I18N ? window.I18N.getLang() : 'pt';
setCountry(hit.country[lang] || hit.country.pt);
setCoords({ lat: hit.lat, lng: hit.lng });
setCountryAuto(true);
} else {
setCoords(null);
if (countryAuto) setCountry('');
}
}, [city]);
if (!open) return null;
function submit(e) {
e.preventDefault();
if (!name.trim() || !city.trim()) return;
// Só adicionamos um pino ao mapa quando temos coordenadas reais da cidade —
// nunca um ponto fictício/aleatório.
if (coords) {
const pins = loadPins();
const next = [...pins, { city: city.trim(), lat: coords.lat, lng: coords.lng, isNew: true }];
localStorage.setItem(CRUSADE_PINS_KEY, JSON.stringify(next));
var updatedPins = next;
} else {
var updatedPins = loadPins();
}
const COUNT_KEY = 'apostolado_count_v1';
const cur = parseInt(localStorage.getItem(COUNT_KEY) || '12473', 10);
localStorage.setItem(COUNT_KEY, String(cur + 1));
localStorage.setItem('apostolado_commit_v1', '1');
onSubmitted(updatedPins, cur + 1);
}
return (
);
}
function CrusadeSection() {
useLang();
const STORAGE_KEY = 'apostolado_commit_v1';
const COUNT_KEY = 'apostolado_count_v1';
const initialCount = 12473;
const [count, setCount] = useState(() => {
const stored = parseInt(localStorage.getItem(COUNT_KEY) || '', 10);
return isNaN(stored) ? initialCount : stored;
});
const [committed, setCommitted] = useState(() => localStorage.getItem(STORAGE_KEY) === '1');
const [pins, setPins] = useState(() => loadPins());
const [modalOpen, setModalOpen] = useState(false);
const counterRef = useRef(null);
useEffect(() => {
const el = counterRef.current;
if (!el) return;
const target = count;
const duration = 1800;
const start = performance.now();
let frame;
function tick(tt) {
const p = Math.min(1, (tt - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
const val = Math.round(target * eased);
el.textContent = val.toLocaleString();
if (p < 1) frame = requestAnimationFrame(tick);
}
const seen = sessionStorage.getItem('crusade_counter_seen');
if (!seen) {
frame = requestAnimationFrame(tick);
sessionStorage.setItem('crusade_counter_seen', '1');
} else {
el.textContent = target.toLocaleString();
}
return () => frame && cancelAnimationFrame(frame);
}, []);
useEffect(() => {
if (counterRef.current && sessionStorage.getItem('crusade_counter_seen')) {
counterRef.current.textContent = count.toLocaleString();
}
}, [count]);
function handleSubmitted(newPins, newCount) {
setPins(newPins);
setCount(newCount);
setCommitted(true);
setModalOpen(false);
}
const pct = Math.min(100, (count / CRUSADE_GOAL) * 100);
return (
<>
{initialCount.toLocaleString()}
{t('crusade.counter.label')}
{count.toLocaleString()} / {CRUSADE_GOAL.toLocaleString()} — {t('crusade.goal.label')}
{committed && (
{t('crusade.thanks')}
{t('crusade.thanks2')}
)}
{committed && (
)}
{t('crusade.map.note')}
setModalOpen(false)} onSubmitted={handleSubmitted} />
>
);
}
// ─── Tweaks (palette + density) ────────────────────────────────
function Tweaks() {
const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
// Apply palette
useEffect(() => {
const [bg, ink, marian, gold] = t.palette;
document.documentElement.style.setProperty('--bg', bg);
document.documentElement.style.setProperty('--ink', ink);
document.documentElement.style.setProperty('--marian', marian);
document.documentElement.style.setProperty('--gold', gold);
document.documentElement.style.setProperty('font-size', t.fontSize + 'px');
}, [t.palette, t.fontSize]);
// Particle visibility
useEffect(() => {
const c = document.getElementById('particle-canvas');
if (!c) return;
if (t.particleIntensity === 'off') c.style.display = 'none';
else {
c.style.display = 'block';
c.style.opacity = t.particleIntensity === 'rich' ? '0.8' : (t.particleIntensity === 'soft' ? '0.5' : '0.3');
}
}, [t.particleIntensity]);
return (
setTweak('palette', v)}
/>
setTweak('fontSize', v)} />
setTweak('particleIntensity', v)}
/>
);
}
// ─── Main App ──────────────────────────────────────────────────
function App() {
const [prayerModalOpen, setPrayerModalOpen] = useState(false);
// Expose modal open for static HTML buttons
useEffect(() => {
window.__openPrayerModal = () => setPrayerModalOpen(true);
}, []);
// Scroll reveal
useEffect(() => {
const els = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in');
io.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
els.forEach((el) => io.observe(el));
return () => io.disconnect();
}, []);
// Toggle dos nichos (mobile / toque)
useEffect(() => {
const niches = document.querySelectorAll('.dim-niche');
const handlers = [];
niches.forEach((n) => {
const h = () => {
const wasActive = n.classList.contains('active');
niches.forEach((m) => m.classList.remove('active'));
if (!wasActive) n.classList.add('active');
};
n.addEventListener('click', h);
handlers.push([n, h]);
});
return () => handlers.forEach(([n, h]) => n.removeEventListener('click', h));
}, []);
// Nav scroll state
useEffect(() => {
const nav = document.querySelector('.nav');
if (!nav) return;
const handler = () => {
if (window.scrollY > 40) nav.classList.add('scrolled');
else nav.classList.remove('scrolled');
};
handler();
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, []);
return (
<>
setPrayerModalOpen(false)} />
>
);
}
// ─── Render mountpoints ────────────────────────────────────────
function mount(id, comp) {
const el = document.getElementById(id);
if (el) ReactDOM.createRoot(el).render(comp);
}
document.addEventListener('DOMContentLoaded', () => {
mount('app-root', );
mount('hero-rosary-mount', );
mount('counter-mount', );
mount('request-form-mount', );
mount('candle-mount', );
});
// Babel scripts run AFTER DOMContentLoaded — if it already fired, mount now.
if (document.readyState !== 'loading') {
mount('app-root', );
mount('hero-rosary-mount', );
mount('counter-mount', );
mount('request-form-mount', );
mount('candle-mount', );
}