/* ============================================================ invite-main.jsx — Thiệp cưới "Thanh lịch" · responsive (web + ĐT) Dùng lại khối chung từ invite-shared.jsx (WED, Photo, Sprig, useCountdown, MapButton, MusicPlayer, Reveal). ============================================================ */ const { useState, useEffect, useRef } = React; const pad2 = (n) => String(n).padStart(2, '0'); /* ---------- Ảnh thật trong folder image/ — gán theo id slot ---------- */ const PHOTOS = { 'hero-main': 'image/DSC_6096.jpg', 'band-1': 'image/L1340434.jpg', 'band-2': 'image/L1340194.jpg', 'gal-0': 'image/L1340100.jpg', 'gal-1': 'image/L1340194.jpg', 'gal-2': 'image/L1340422.jpg', 'gal-3': 'image/L1340434.jpg', 'gal-4': 'image/DSC_6096.jpg', 'gal-5': 'image/DSC_7069.jpg', }; /* ---------- Ô ảnh (gán sẵn src, vẫn cho phép kéo-thả ghi đè) ---------- */ function ImgSlot({ id, placeholder, shape = 'rect', radius, fit = 'cover', style }) { const props = { id, placeholder, shape, fit, style }; if (radius != null) props.radius = radius; if (PHOTOS[id]) props.src = PHOTOS[id]; return React.createElement('image-slot', props); } /* ---------- Đếm ngược responsive ---------- */ function CountdownR() { const { d, h, m, s } = window.useCountdown(window.WED.dateISO); const items = [[d, 'Ngày'], [h, 'Giờ'], [m, 'Phút'], [s, 'Giây']]; return (
{items.map(([v, lab]) => (
{pad2(v)} {lab}
))}
); } /* ---------- Dải ảnh full-bleed (điểm nhấn) + parallax chữ ---------- */ function Band({ id, lab, big, sub }) { const bandRef = useRef(null); const capRef = useRef(null); // Parallax: chữ trôi nhẹ ngược chiều cuộn — ghi thẳng DOM qua rAF, // biên độ giới hạn để không trôi quá đà. useEffect(() => { if (window.__reduceMotion) return; const band = bandRef.current, cap = capRef.current; if (!band || !cap) return; let raf = 0; const update = () => { raf = 0; const r = band.getBoundingClientRect(); const vh = window.innerHeight; if (r.bottom < 0 || r.top > vh) return; // ngoài màn hình thì bỏ qua // tiến trình -1..1 khi band đi qua khung nhìn const prog = (r.top + r.height / 2 - vh / 2) / (vh / 2 + r.height / 2); const shift = Math.max(-40, Math.min(40, prog * -40)); // giới hạn ±40px cap.style.transform = 'translate3d(0,' + shift.toFixed(1) + 'px,0)'; }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); }; window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); update(); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); return (
{lab && {lab}} {big &&

{big}

} {sub && {sub}}
); } /* ---------- Album slideshow (crossfade + auto-advance + dots) ---------- */ function GalleryR() { const ids = ['gal-0', 'gal-1', 'gal-2', 'gal-3', 'gal-4', 'gal-5']; const [idx, setIdx] = useState(0); const [paused, setPaused] = useState(false); const total = ids.length; useEffect(() => { if (paused) return; const t = setInterval(() => setIdx((i) => (i + 1) % total), 5000); return () => clearInterval(t); }, [paused, total]); const go = (n) => setIdx((n + total) % total); return (
setPaused(true)} onMouseLeave={() => setPaused(false)}>
{ids.map((id, i) => (
))}
{pad2(idx + 1)} / {pad2(total)}
{ids.map((_, i) => (
); } /* ---------- Nhãn + gạch mảnh ---------- */ function LabelRule({ children }) { return (
{children}
); } /* ---------- Bìa thiệp ---------- */ function Cover({ open, onOpen }) { const W = window.WED; return (

Save the date

{W.groom} & {W.bride}

{W.dateLong}

); } /* ---------- Sổ lưu bút (lời chúc) — lưu localStorage ---------- */ function Wishes() { const seed = [ { name: 'Gia đình hai họ', msg: 'Chúc hai con trăm năm hạnh phúc, sắt son một đời.' }, ]; const [list, setList] = useState(() => { try { const s = JSON.parse(localStorage.getItem('wedding-wishes') || 'null'); return s && s.length ? s : seed; } catch { return seed; } }); const [name, setName] = useState(''); const [msg, setMsg] = useState(''); const add = (e) => { e.preventDefault(); if (!name.trim() || !msg.trim()) return; const next = [{ name: name.trim(), msg: msg.trim() }, ...list]; setList(next); try { localStorage.setItem('wedding-wishes', JSON.stringify(next)); } catch {} setName(''); setMsg(''); }; return (
setName(e.target.value)} required />