(() => { const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const MOBILE_RE = /^\d{10,15}$/; function resolveApiBase() { if (window.EVENTBUZZ_API_BASE) { return String(window.EVENTBUZZ_API_BASE).replace(/\/$/, ""); } const host = window.location.hostname; if (host === "localhost" || host === "127.0.0.1") { return "http://localhost:3001/api"; } return "https://api.eventbuzz.live/api"; } const API_BASE = resolveApiBase(); /* ---------- Mobile nav drawer ---------- */ function initNav() { const nav = document.querySelector("nav"); const menuBtn = document.querySelector(".nav-menu-btn"); if (!nav || !menuBtn) return; const mq = window.matchMedia("(max-width: 900px)"); let drawerOpen = false; let drawerEls = null; const links = Array.from( document.querySelectorAll(".navlinks-desktop a") ).map((a) => ({ href: a.getAttribute("href") || "#", label: a.textContent.trim(), target: a.getAttribute("target") || null, })); function syncDrawerMode() { nav.classList.toggle("is-drawer-mode", mq.matches); if (!mq.matches) closeDrawer(true); } function ensureDrawer() { if (drawerEls) return drawerEls; const overlay = document.createElement("div"); overlay.className = "nav-drawer-overlay"; overlay.setAttribute("role", "presentation"); const aside = document.createElement("aside"); aside.className = "nav-drawer"; aside.id = "nav-drawer"; aside.setAttribute("role", "dialog"); aside.setAttribute("aria-modal", "true"); aside.setAttribute("aria-label", "Navigation menu"); aside.innerHTML = ` Request demo `; overlay.appendChild(aside); document.body.appendChild(overlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeDrawer(); }); aside .querySelector(".nav-drawer-close") ?.addEventListener("click", () => closeDrawer()); aside.querySelectorAll("a").forEach((a) => { a.addEventListener("click", () => closeDrawer()); }); drawerEls = { overlay, aside }; return drawerEls; } function openDrawer() { const { overlay, aside } = ensureDrawer(); drawerOpen = true; menuBtn.setAttribute("aria-expanded", "true"); menuBtn.setAttribute("aria-label", "Close menu"); requestAnimationFrame(() => { overlay.classList.add("is-open"); aside.classList.add("is-open"); }); document.body.style.overflow = "hidden"; } function closeDrawer(instant) { if (!drawerEls) return; drawerOpen = false; menuBtn.setAttribute("aria-expanded", "false"); menuBtn.setAttribute("aria-label", "Open menu"); drawerEls.overlay.classList.remove("is-open"); drawerEls.aside.classList.remove("is-open"); document.body.style.overflow = ""; if (instant) { drawerEls.overlay.style.transition = "none"; drawerEls.aside.style.transition = "none"; requestAnimationFrame(() => { drawerEls.overlay.style.transition = ""; drawerEls.aside.style.transition = ""; }); } } menuBtn.addEventListener("click", () => { if (drawerOpen) closeDrawer(); else openDrawer(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && drawerOpen) closeDrawer(); }); syncDrawerMode(); mq.addEventListener("change", syncDrawerMode); } /* ---------- Demo modal ---------- */ function validateDemo(data) { const errors = {}; const name = (data.name || "").trim(); const mobile = (data.mobile || "").trim(); const email = (data.email || "").trim(); const message = (data.message || "").trim(); const captchaAnswer = (data.captchaAnswer || "").trim(); if (!name) errors.name = "Full name is required."; if (!mobile) errors.mobile = "Mobile is required."; else { const digits = mobile.replace(/[\s\-().]/g, ""); if (!MOBILE_RE.test(digits)) { errors.mobile = "Enter a valid mobile number (10-15 digits)."; } } if (!email) errors.email = "Email is required."; else if (!EMAIL_RE.test(email)) { errors.email = "Enter a valid email address."; } if (!message) errors.message = "Message is required."; if (!captchaAnswer) errors.captcha = "Please solve the captcha."; return { ok: Object.keys(errors).length === 0, errors, data: { name, mobile: mobile.replace(/[\s\-().]/g, ""), email, message, captchaAnswer, }, }; } function initDemoModal() { let overlay = null; let open = false; let success = false; let captchaToken = ""; let captchaLoading = false; let submitting = false; async function loadCaptcha() { if (!overlay) return; const questionEl = overlay.querySelector("[data-demo-captcha-question]"); const answerEl = overlay.querySelector('[name="captchaAnswer"]'); const reloadBtn = overlay.querySelector("[data-demo-captcha-reload]"); if (!questionEl) return; captchaLoading = true; questionEl.textContent = "Loading…"; if (answerEl) answerEl.value = ""; if (reloadBtn) reloadBtn.disabled = true; try { const res = await fetch(`${API_BASE}/public/captcha/challenge`, { cache: "no-store", }); const body = await res.json(); if (!res.ok || !body?.success || !body?.data?.captchaToken) { throw new Error(body?.error?.message || "Captcha unavailable"); } captchaToken = body.data.captchaToken; questionEl.textContent = body.data.question || "Solve the sum"; } catch { captchaToken = ""; questionEl.textContent = "Unavailable — tap reload"; } finally { captchaLoading = false; if (reloadBtn) reloadBtn.disabled = false; } } function setFieldError(form, key, message) { const inputName = key === "captcha" ? "captchaAnswer" : key; const errId = key === "captchaAnswer" || key === "captcha" ? "demo-captcha-err" : `demo-${key}-err`; const field = form.querySelector(`[name="${inputName}"]`)?.closest(".demo-field"); const errEl = document.getElementById(errId); const input = form.querySelector(`[name="${inputName}"]`); if (message) { field?.classList.add("has-error"); if (errEl) { errEl.hidden = false; errEl.textContent = message; } input?.setAttribute("aria-invalid", "true"); } else { field?.classList.remove("has-error"); if (errEl) { errEl.hidden = true; errEl.textContent = ""; } input?.removeAttribute("aria-invalid"); } } function build() { if (overlay) return overlay; overlay = document.createElement("div"); overlay.className = "demo-modal-overlay"; overlay.setAttribute("role", "presentation"); overlay.innerHTML = ` `; document.body.appendChild(overlay); overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); overlay .querySelector(".demo-modal-close") ?.addEventListener("click", close); overlay .querySelector(".demo-success-close") ?.addEventListener("click", close); overlay .querySelector("[data-demo-captcha-reload]") ?.addEventListener("click", () => { void loadCaptcha(); }); const form = overlay.querySelector(".demo-modal-form"); form.addEventListener("submit", async (e) => { e.preventDefault(); if (submitting || captchaLoading) return; const fd = new FormData(form); const result = validateDemo({ name: fd.get("name"), mobile: fd.get("mobile"), email: fd.get("email"), message: fd.get("message"), captchaAnswer: fd.get("captchaAnswer"), }); ["name", "mobile", "email", "message", "captchaAnswer"].forEach((key) => { const errKey = key === "captchaAnswer" ? "captcha" : key; setFieldError(form, key, result.errors[errKey] || ""); }); const formErr = overlay.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } if (!result.ok) { const first = ["name", "mobile", "email", "message", "captcha"].find( (k) => result.errors[k] ) || "name"; const focusName = first === "captcha" ? "captchaAnswer" : first; form.querySelector(`[name="${focusName}"]`)?.focus(); return; } if (!captchaToken) { setFieldError( form, "captchaAnswer", "Captcha is not ready. Please reload and try again." ); void loadCaptcha(); return; } const submitBtn = overlay.querySelector(".demo-submit-btn"); submitting = true; if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = "Sending…"; } try { const res = await fetch(`${API_BASE}/public/demo-request`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: result.data.name, mobile: result.data.mobile, email: result.data.email, message: result.data.message, captchaToken, captchaAnswer: result.data.captchaAnswer, }), }); let body = null; try { body = await res.json(); } catch { body = null; } if (!res.ok || !body?.success) { const msg = body?.error?.message || "Could not send your request. Please try again."; if (/captcha/i.test(msg)) { setFieldError(form, "captchaAnswer", msg); void loadCaptcha(); } else if (formErr) { formErr.hidden = false; formErr.textContent = msg; } return; } showSuccess(); } catch { if (formErr) { formErr.hidden = false; formErr.textContent = "Could not reach the server. Please try again in a moment."; } } finally { submitting = false; if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = "Request demo"; } } }); return overlay; } function showSuccess() { success = true; const formView = overlay.querySelector(".demo-modal-form-view"); const successView = overlay.querySelector(".demo-modal-success"); if (formView) formView.hidden = true; if (successView) successView.hidden = false; } function reset() { success = false; captchaToken = ""; const form = overlay?.querySelector(".demo-modal-form"); form?.reset(); overlay?.querySelectorAll(".demo-field-error").forEach((el) => { el.hidden = true; el.textContent = ""; }); overlay?.querySelectorAll(".demo-field").forEach((el) => { el.classList.remove("has-error"); }); const formErr = overlay?.querySelector(".demo-form-error"); if (formErr) { formErr.hidden = true; formErr.textContent = ""; } const formView = overlay?.querySelector(".demo-modal-form-view"); const successView = overlay?.querySelector(".demo-modal-success"); if (formView) formView.hidden = false; if (successView) successView.hidden = true; void loadCaptcha(); } function openModal() { const el = build(); if (!open) reset(); else if (!captchaToken) void loadCaptcha(); open = true; document.body.style.overflow = "hidden"; requestAnimationFrame(() => { el.classList.add("is-open"); el.querySelector(".demo-modal")?.classList.add("is-open"); if (!success) { el.querySelector('[name="name"]')?.focus(); } }); } function close() { if (!overlay) return; open = false; overlay.classList.remove("is-open"); overlay.querySelector(".demo-modal")?.classList.remove("is-open"); document.body.style.overflow = ""; } document.addEventListener("click", (e) => { const trigger = e.target.closest("[data-demo-open]"); if (!trigger) return; e.preventDefault(); openModal(); }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && open) close(); }); } /* ---------- Scroll reveals (GSAP ScrollTrigger when available) ---------- */ function initReveals() { const nodes = document.querySelectorAll(".reveal"); if (!nodes.length) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { return; } if (window.gsap && window.ScrollTrigger) { window.gsap.registerPlugin(window.ScrollTrigger); nodes.forEach((el) => { const y = Number(el.dataset.revealY ?? 28); const duration = Number(el.dataset.revealDuration ?? 0.7); const delay = Number(el.dataset.revealDelay ?? 0); window.gsap.set(el, { opacity: 0, y, willChange: "opacity, transform" }); window.gsap.to(el, { opacity: 1, y: 0, duration, delay, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 88%", end: "bottom top", toggleActions: "play reverse play reverse", }, }); }); return; } // Fallback without GSAP const io = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.style.opacity = "1"; entry.target.style.transform = "none"; } }); }, { threshold: 0.12, rootMargin: "0px 0px -12% 0px" } ); nodes.forEach((el) => { el.style.opacity = "0"; el.style.transform = "translateY(28px)"; el.style.transition = "opacity .7s ease, transform .7s ease"; io.observe(el); }); } document.addEventListener("DOMContentLoaded", () => { initNav(); initDemoModal(); initReveals(); }); })();