// ==================================================== // SECTION 1 — Hero (responsive, real scrollable site) // ==================================================== function S1Logo({ height = 38 }) { return ( Yojea ); } // ─── Global sticky nav ──────────────────────────────────────── // Fixed to the top of the viewport, lives OUTSIDE of HeroSection so // it persists across the entire page. Tracks which section is in view // via IntersectionObserver and highlights the matching nav item. const NAV_ITEMS = [ { label: 'Bahan', id: 'ingredients' }, { label: 'Tentang', id: 'tentang' }, { label: 'Manfaat', id: 'manfaat' }, { label: 'Uji Lab', id: 'lab' }, { label: 'Testimoni', id: 'testimoni' }, { label: 'FAQ', id: 'faq' }, ]; // Smooth-scroll helper. Most ids resolve to the section's top (with a // 72px nav offset). Two special targets get custom destinations: // • 'beli' → phase 5 of #story (the Beli Yojea finale block) // • 'manfaat' → top of #manfaat — already correct, but we round down // to land before the sachet animation kicks in. function smoothScrollToId(id) { if (id === 'beli') { const story = document.getElementById('story'); if (story) { // Land in the stable "fully-left" zone: the photo finishes its // leftward sweep at progress≈0.954 and the phase-4 cert text has // faded out by ≈0.94, so 0.96 shows the photo fully moved left with // no phase-4 text — while the section is still pinned (not at 1.0). const total = story.offsetHeight - window.innerHeight; const top = story.getBoundingClientRect().top + window.scrollY + Math.max(0, total) * 0.96; window.scrollTo({ top, behavior: 'smooth' }); return; } } const el = document.getElementById(id); if (!el) return; const top = el.getBoundingClientRect().top + window.scrollY - 72; window.scrollTo({ top, behavior: 'smooth' }); } function TopNav() { const [open, setOpen] = React.useState(false); const [scrolled, setScrolled] = React.useState(false); const [active, setActive] = React.useState('hero'); // Solidify nav background once user has scrolled past the very top React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 40); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); // Track which section is in view (incl. "hero" so nothing highlights // while at the very top of the page). React.useEffect(() => { const ids = ['hero', ...NAV_ITEMS.map(n => n.id)]; const els = ids .map(id => document.getElementById(id)) .filter(Boolean); if (!els.length) return; // For each section, fire when its center is near the viewport center. // We keep a sorted map of intersection ratios and pick the largest. const ratios = new Map(); const io = new IntersectionObserver((entries) => { entries.forEach(e => ratios.set(e.target.id, e.intersectionRatio)); let best = 'hero', bestR = 0; ratios.forEach((r, id) => { if (r > bestR) { bestR = r; best = id; } }); if (bestR > 0) setActive(best); }, { // Section is "active" once its middle band overlaps the viewport rootMargin: '-40% 0px -50% 0px', threshold: [0, 0.01, 0.25, 0.5, 0.75, 1], }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }, []); const handleClick = (e, id) => { e.preventDefault(); setOpen(false); smoothScrollToId(id); }; return ( ); } function S1Grain() { return ( ); } function HeroSection() { // ─── Cycling testimonial pop-ups around the photo ──────────── // We rotate through a small set of KEPUTIHAN-focused testimonials, // surfacing 2 at a time (each slot shows a different one). The card // fades + scales in when its index changes; a ~5s interval keeps // motion calm. // // Names are real customer handles (same source list as the Testimoni // section), masked: first 2 letters + "*". Quotes are short-form so // they fit the small popup card. const heroMask = (raw) => { const at = raw.startsWith('@') ? '@' : ''; const body = at ? raw.slice(1) : raw; if (!body) return raw; const v = body.slice(0, 2); return at + v + '*'.repeat(Math.max(0, body.length - v.length)); }; const HERO_TESTIS = [ { name: '@jeanette_lesi', city: 'Pelanggan Yojea', text: 'Keputihan auto berkurang.', stars: 5 }, { name: '@jundina_w', city: 'Pelanggan Yojea', text: '2x minum, ga gatal & keputihan ilang.', stars: 5 }, { name: '@na.ttto', city: 'Pelanggan Yojea', text: 'Keputihan jadi lebih bening.', stars: 5 }, { name: '@aditamutiara', city: 'Pelanggan Yojea', text: 'Keputihan jauh berkurang.', stars: 5 }, { name: '@tamara', city: 'Pelanggan Yojea', text: 'Keputihan berkurang, 1 box kerasa.', stars: 5 }, ]; const [tIdx, setTIdx] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setTIdx(i => (i + 2) % HERO_TESTIS.length), 5200); return () => clearInterval(id); }, []); const slot1 = HERO_TESTIS[tIdx % HERO_TESTIS.length]; const slot2 = HERO_TESTIS[(tIdx + 1) % HERO_TESTIS.length]; return (
{/* Soft animated radial glows behind everything */}
{/* LEFT — text column */}
Jamu Modern · Est. 1970

Beauty inside, Beauty outside.

Jamu Modern Wanita No. 1 di Indonesia. Diformulasikan dari ramuan herbal turun-temurun untuk kesehatan wanita setiap hari.

BPOM Terdaftar Halal MUI Uji Lab Terbukti
{/* RIGHT — photo column */}
Ribuan wanita
sudah merasakan manfaatnya
6
Bahan Alami
Pilihan Terbaik Indonesia
{/* Cycling testimonial pops around the photo */}
{'★'.repeat(slot1.stars)}
"{slot1.text}"
{heroMask(slot1.name)}
{'★'.repeat(slot2.stars)}
"{slot2.text}"
{heroMask(slot2.name)}
Wanita Yojea
); } Object.assign(window, { HeroSection, TopNav });