// ============================================================ // SECTION HORMON — Hormonal imbalance explanation // Two-column editorial. Left: big serif title + visual. // Right: explanation paragraphs. // ============================================================ function SectionHormon() { const [balanced, setBalanced] = React.useState(false); // Bubble lifecycle per symptom id: 'visible' | 'popping-out' | 'gone' | 'popping-in' const [bubbleStates, setBubbleStates] = React.useState({}); // Track mobile viewport so the hormone bubbles can scale up for narrow screens. const [isMobile, setIsMobile] = React.useState(false); React.useEffect(() => { const mq = window.matchMedia('(max-width: 700px)'); const update = () => setIsMobile(mq.matches); update(); if (mq.addEventListener) mq.addEventListener('change', update); else mq.addListener(update); return () => { if (mq.removeEventListener) mq.removeEventListener('change', update); else mq.removeListener(update); }; }, []); // Circle radii — imbalanced: progesteron noticeably bigger than estrogen. // After Yojea: both equal. Mobile pushes radii close to the viewBox limits // so the bubbles fill the entire diagram area dramatically. const balancedR = isMobile ? 84 : 64; // Imbalanced → both circles continuously take new RANDOM radii that are // always clearly different and either one can be the bigger — so sometimes // estrogen > progesteron and sometimes the reverse. A new target is set // before the previous transition finishes (interval < transition duration) // so the circles glide continuously and never visibly "snap". const [dynR, setDynR] = React.useState(null); React.useEffect(() => { if (balanced) { setDynR(null); return; } const lo = isMobile ? 52 : 42; const hi = isMobile ? 104 : 92; const gap = isMobile ? 16 : 13; // guaranteed min difference const tick = () => { const a = lo + Math.random() * (hi - lo); let b = lo + Math.random() * (hi - lo); let guard = 0; while (Math.abs(a - b) < gap && guard++ < 20) b = lo + Math.random() * (hi - lo); setDynR({ a: Math.round(a), b: Math.round(b) }); }; tick(); const id = setInterval(tick, 1900); return () => clearInterval(id); }, [balanced, isMobile]); const estrogenR = balanced ? balancedR : (dynR ? dynR.a : (isMobile ? 60 : 48)); const progesteronR = balanced ? balancedR : (dynR ? dynR.b : (isMobile ? 96 : 86)); // Envelope rect dimensions follow balanced radius so margin stays consistent // (~10px around the two balanced bubbles centered at cx=102 and cx=282 in // the 0–384 viewBox). const CX_A = 102; const CX_B = 282; const CY = 120; const VB_W = 384; const envMargin = 10; const envX = CX_A - balancedR - envMargin; const envY = CY - balancedR - envMargin; const envW = VB_W - 2 * envX; const envH = 2 * (balancedR + envMargin); const envR = balancedR + envMargin; // Floating symptom bubbles scattered across the WHOLE "Akar Masalah" section. // The set is randomized every Reset click (duplicates allowed) so the user // sees a different count and mix each cycle. const BASE_SYMPTOMS = React.useMemo(() => [ { key: 'pd', label: 'Tidak Percaya Diri', size: 96 }, { key: 'keputihan', label: 'Keputihan', size: 84 }, { key: 'mood', label: 'Mood Swing', size: 88 }, { key: 'siklus', label: 'Siklus Tak Teratur', size: 92 }, { key: 'jerawat', label: 'Jerawatan', size: 80 }, { key: 'nyeri', label: 'Nyeri Berlebih', size: 86 }, { key: 'bau', label: 'Bau Tak Sedap', size: 90 }, { key: 'lelah', label: 'Mudah Lelah', size: 84 }, { key: 'sakit', label: 'Sakit Kepala', size: 88 }, { key: 'cemas', label: 'Mudah Cemas', size: 86 }, { key: 'gemuk', label: 'Berat Naik', size: 82 }, ], []); const makeBatch = React.useCallback(() => { // Always 10 bubbles, with duplicates allowed (random sample). const count = 10; const batch = []; for (let i = 0; i < count; i++) { const src = BASE_SYMPTOMS[Math.floor(Math.random() * BASE_SYMPTOMS.length)]; const sizeJitter = Math.round((Math.random() - 0.5) * 18); batch.push({ id: `${src.key}_${i}_${Math.random().toString(36).slice(2, 6)}`, key: src.key, label: src.label, size: Math.max(70, src.size + sizeJitter), top: 6 + Math.random() * 86, left: 3 + Math.random() * 94, }); } return batch; }, [BASE_SYMPTOMS]); const [symptomBatch, setSymptomBatch] = React.useState(makeBatch); // Positions keyed by bubble id, regenerated whenever the batch changes. const [positions, setPositions] = React.useState(() => { const p = {}; symptomBatch.forEach(s => { p[s.id] = { top: s.top, left: s.left }; }); return p; }); React.useEffect(() => { const p = {}; symptomBatch.forEach(s => { p[s.id] = { top: s.top, left: s.left }; }); setPositions(p); }, [symptomBatch]); // Continuously reposition each symptom bubble to a new random spot within // the section (with safe-zone padding from edges). Each bubble has its own // independent timing so the motion looks organic, not synchronized. React.useEffect(() => { const timeouts = {}; const scheduleNext = (id, initialDelay) => { const tick = () => { setPositions(prev => ({ ...prev, [id]: { top: 6 + Math.random() * 86, left: 3 + Math.random() * 94, }, })); timeouts[id] = setTimeout(tick, 5000 + Math.random() * 6000); }; timeouts[id] = setTimeout(tick, initialDelay); }; symptomBatch.forEach((s, i) => { scheduleNext(s.id, 1200 + i * 220 + Math.random() * 1500); }); return () => Object.values(timeouts).forEach(clearTimeout); }, [symptomBatch]); // Drive the staggered pop-out (on Yojea) and pop-in (on Reset) lifecycles. const isFirstRun = React.useRef(true); React.useEffect(() => { if (isFirstRun.current) { isFirstRun.current = false; return; } const timers = []; symptomBatch.forEach((s, i) => { const stagger = Math.min(900, 50 * i); if (balanced) { // POP-OUT: visible → popping-out (0.7s burst) → gone timers.push(setTimeout(() => { setBubbleStates(prev => ({ ...prev, [s.id]: 'popping-out' })); }, stagger)); timers.push(setTimeout(() => { setBubbleStates(prev => ({ ...prev, [s.id]: 'gone' })); }, stagger + 700)); } else { // POP-IN: gone → popping-in (0.6s reverse burst) → visible timers.push(setTimeout(() => { setBubbleStates(prev => ({ ...prev, [s.id]: 'popping-in' })); }, stagger)); timers.push(setTimeout(() => { setBubbleStates(prev => ({ ...prev, [s.id]: 'visible' })); }, stagger + 600)); } }); return () => timers.forEach(clearTimeout); }, [balanced, symptomBatch]); // Click handler: toggles balanced; when Reset is pressed we also regenerate // a fresh random batch (so count + mix changes every cycle). const handleMinumClick = () => { if (balanced) { // Reset → regenerate batch & start new bubbles hidden, then flip. const newBatch = makeBatch(); const initial = {}; newBatch.forEach(s => { initial[s.id] = 'gone'; }); setBubbleStates(initial); setSymptomBatch(newBatch); setBalanced(false); } else { setBalanced(true); } }; // ─── Brighten a bubble's label when it overlaps the (dark) hormone box ── // The card is opaque dark-red while imbalanced, so dark bubble text on top // of it is unreadable. We poll each bubble's live rect vs the card rect and // flag the overlapping ones so their label flips to a light color. const cardRef = React.useRef(null); const bubbleRefs = React.useRef({}); const [overBox, setOverBox] = React.useState(() => new Set()); React.useEffect(() => { const check = () => { const card = cardRef.current; if (!card) return; const cr = card.getBoundingClientRect(); const next = new Set(); for (const id in bubbleRefs.current) { const el = bubbleRefs.current[id]; if (!el) continue; const r = el.getBoundingClientRect(); if (r.right > cr.left && r.left < cr.right && r.bottom > cr.top && r.top < cr.bottom) { next.add(id); } } setOverBox(prev => { if (prev.size === next.size && [...next].every(x => prev.has(x))) return prev; return next; }); }; check(); const id = setInterval(check, 150); return () => clearInterval(id); }, [symptomBatch]); return (
{/* Section-wide scatter of floating symptom bubbles — sits behind the content layer and across the entire "Akar Masalah" area. */}
{/* Interactive hormone balance diagram — full-width card on top */}
{/* State-reactive background FX: • imbalanced → harsh red radial pulse • balanced → soft blue wave drift */} {/* Status indicator — matches "Akar Masalah" eyebrow style */}
{balanced ? 'Seimbang' : 'Tidak Seimbang'}
{/* Stage — SVG diagram + floating symptom bubbles around it */}
{/* Akar Masalah — eyebrow */}
Akar Masalah
{/* Two-column: big serif title (left) + explanation (right) */}

Ketidakseimbangan
hormon
pangkal gangguan
haid.

Yojea bekerja dengan menyeimbangkan hormon estrogen dan progesteron dalam tubuh wanita.

Formula Yojea mengandung bahan aktif dengan sifat anti-inflamasi dan antioksidan yang berperan penting dalam menjaga kesehatan tubuh.

Dengan kombinasi ini, Yojea tidak hanya membantu mengatasi nyeri menstruasi, tetapi juga mendukung kesehatan tubuh secara menyeluruh.

{/* Feature trio — full-width row below the columns. Order: Penyeimbang Hormonal · Antioksidan · Anti-inflamasi */}
Penyeimbang Hormonal
Menstabilkan siklus & mood sepanjang bulan.
Antioksidan
Membantu melawan radikal bebas, menjaga sel tubuh tetap sehat.
Anti-inflamasi
Membantu mengurangi peradangan & nyeri haid secara alami.
); } Object.assign(window, { SectionHormon });