);
}
// ─── 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 (
Jamu Modern Wanita No. 1 di Indonesia. Diformulasikan dari ramuan herbal turun-temurun untuk kesehatan wanita setiap hari.