1543 lines
49 KiB
JavaScript
1543 lines
49 KiB
JavaScript
(() => {
|
||
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();
|
||
|
||
function getHashId(anchor) {
|
||
const raw = anchor.getAttribute("href") || "";
|
||
if (!raw || raw === "#") return "";
|
||
try {
|
||
const url = new URL(anchor.href, window.location.href);
|
||
if (!url.hash || url.hash === "#") return "";
|
||
return decodeURIComponent(url.hash.slice(1));
|
||
} catch {
|
||
return raw.charAt(0) === "#" ? raw.slice(1) : "";
|
||
}
|
||
}
|
||
|
||
let activeNavId = "";
|
||
let navSpyLocked = false;
|
||
let navSpyUnlockTimer = 0;
|
||
|
||
function setActiveNav(id) {
|
||
activeNavId = id || "";
|
||
document
|
||
.querySelectorAll(".navlinks-desktop a, .nav-drawer-links a")
|
||
.forEach((a) => {
|
||
if (a.target === "_blank") return;
|
||
const linkId = getHashId(a);
|
||
if (!linkId) return;
|
||
const on = Boolean(activeNavId) && linkId === activeNavId;
|
||
a.classList.toggle("is-active", on);
|
||
if (on) a.setAttribute("aria-current", "location");
|
||
else a.removeAttribute("aria-current");
|
||
});
|
||
}
|
||
|
||
function lockActiveNav(id) {
|
||
navSpyLocked = true;
|
||
setActiveNav(id);
|
||
window.clearTimeout(navSpyUnlockTimer);
|
||
navSpyUnlockTimer = window.setTimeout(() => {
|
||
navSpyLocked = false;
|
||
}, 900);
|
||
}
|
||
|
||
/* ---------- 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: 1024px)");
|
||
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 = `
|
||
<div class="nav-drawer-head">
|
||
<span class="nav-drawer-title">Menu</span>
|
||
<button type="button" class="nav-drawer-close" aria-label="Close menu">
|
||
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="22" width="22" xmlns="http://www.w3.org/2000/svg"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||
</button>
|
||
</div>
|
||
<div class="nav-drawer-links">
|
||
${links
|
||
.map((l) => {
|
||
const extra = l.target
|
||
? ` target="${l.target}" rel="noopener noreferrer"`
|
||
: "";
|
||
return `<a href="${l.href}"${extra}>${l.label}</a>`;
|
||
})
|
||
.join("")}
|
||
</div>
|
||
<a href="#cta" class="btn btn-primary nav-drawer-cta" data-demo-open="true">Request demo</a>
|
||
`;
|
||
|
||
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();
|
||
setActiveNav(activeNavId);
|
||
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);
|
||
}
|
||
|
||
/* ---------- In-page section links (no #hash in the URL) ---------- */
|
||
function initInPageNav() {
|
||
const reduceMotion = window.matchMedia(
|
||
"(prefers-reduced-motion: reduce)"
|
||
).matches;
|
||
const PENDING_KEY = "eb-scroll-to";
|
||
|
||
function normalizePath(path) {
|
||
return path.replace(/\/index\.html$/i, "/") || "/";
|
||
}
|
||
|
||
function scrollToId(id) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return false;
|
||
lockActiveNav(id);
|
||
el.scrollIntoView({
|
||
behavior: reduceMotion ? "auto" : "smooth",
|
||
block: "start",
|
||
});
|
||
return true;
|
||
}
|
||
|
||
function consumeHash() {
|
||
if (!window.location.hash || window.location.hash === "#") return "";
|
||
const id = decodeURIComponent(window.location.hash.slice(1));
|
||
history.replaceState(
|
||
null,
|
||
"",
|
||
window.location.pathname + window.location.search
|
||
);
|
||
return id;
|
||
}
|
||
|
||
let pending = "";
|
||
try {
|
||
pending = sessionStorage.getItem(PENDING_KEY) || "";
|
||
if (pending) sessionStorage.removeItem(PENDING_KEY);
|
||
} catch {
|
||
pending = "";
|
||
}
|
||
|
||
const fromUrl = consumeHash();
|
||
const target = pending || fromUrl;
|
||
if (target) {
|
||
const go = () => scrollToId(target);
|
||
if (document.readyState === "complete") requestAnimationFrame(go);
|
||
else window.addEventListener("load", () => requestAnimationFrame(go), { once: true });
|
||
}
|
||
|
||
document.addEventListener("click", (e) => {
|
||
if (
|
||
e.defaultPrevented ||
|
||
e.button !== 0 ||
|
||
e.metaKey ||
|
||
e.ctrlKey ||
|
||
e.shiftKey ||
|
||
e.altKey
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const a = e.target.closest("a[href]");
|
||
if (!a || a.target === "_blank" || a.hasAttribute("download")) return;
|
||
if (a.hasAttribute("data-demo-open") || a.hasAttribute("data-talk-open")) {
|
||
return;
|
||
}
|
||
|
||
const raw = a.getAttribute("href") || "";
|
||
if (!raw || raw === "#") return;
|
||
|
||
let url;
|
||
try {
|
||
url = new URL(a.href);
|
||
} catch {
|
||
return;
|
||
}
|
||
if (!url.hash || url.hash === "#") return;
|
||
|
||
const id = decodeURIComponent(url.hash.slice(1));
|
||
if (!id) return;
|
||
|
||
if (
|
||
url.origin === window.location.origin &&
|
||
normalizePath(url.pathname) === normalizePath(window.location.pathname)
|
||
) {
|
||
if (!document.getElementById(id)) return;
|
||
e.preventDefault();
|
||
scrollToId(id);
|
||
return;
|
||
}
|
||
|
||
if (url.origin !== window.location.origin) return;
|
||
try {
|
||
sessionStorage.setItem(PENDING_KEY, id);
|
||
} catch {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
window.location.assign(url.pathname + url.search);
|
||
});
|
||
}
|
||
|
||
/* ---------- Highlight nav link for the section in view ---------- */
|
||
function initNavSpy() {
|
||
const links = Array.from(
|
||
document.querySelectorAll(".navlinks-desktop a")
|
||
).filter((a) => a.target !== "_blank" && getHashId(a));
|
||
const items = links
|
||
.map((a) => {
|
||
const id = getHashId(a);
|
||
const el = document.getElementById(id);
|
||
return el ? { id, el } : null;
|
||
})
|
||
.filter(Boolean)
|
||
.sort((a, b) => {
|
||
const pos = a.el.compareDocumentPosition(b.el);
|
||
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
||
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
||
return 0;
|
||
});
|
||
if (!items.length) return;
|
||
|
||
function currentId() {
|
||
const nav = document.querySelector("nav");
|
||
const marker = (nav ? nav.offsetHeight : 58) + 20;
|
||
let id = "";
|
||
items.forEach((item) => {
|
||
if (item.el.getBoundingClientRect().top <= marker) id = item.id;
|
||
});
|
||
return id;
|
||
}
|
||
|
||
let ticking = false;
|
||
function update() {
|
||
ticking = false;
|
||
if (navSpyLocked) return;
|
||
setActiveNav(currentId());
|
||
}
|
||
|
||
function onScroll() {
|
||
if (ticking) return;
|
||
ticking = true;
|
||
requestAnimationFrame(update);
|
||
}
|
||
|
||
window.addEventListener("scroll", onScroll, { passive: true });
|
||
window.addEventListener("resize", onScroll);
|
||
update();
|
||
}
|
||
|
||
/* ---------- Demo modal ---------- */
|
||
const EVENT_TYPES = [
|
||
"Webinar",
|
||
"Conference",
|
||
"Exhibition",
|
||
"Internal Sales Meet",
|
||
"Others",
|
||
];
|
||
|
||
function validateDemo(data) {
|
||
const errors = {};
|
||
const name = (data.name || "").trim();
|
||
const mobile = (data.mobile || "").trim();
|
||
const email = (data.email || "").trim();
|
||
const eventType = (data.eventType || "").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 (!eventType) errors.eventType = "Select an event type.";
|
||
else if (!EVENT_TYPES.includes(eventType)) {
|
||
errors.eventType = "Select a valid event type.";
|
||
}
|
||
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,
|
||
eventType,
|
||
message,
|
||
captchaAnswer,
|
||
},
|
||
};
|
||
}
|
||
|
||
function bindDemoForm(root, options = {}) {
|
||
const idPrefix = options.idPrefix || "demo";
|
||
let captchaToken = "";
|
||
let captchaLoading = false;
|
||
let submitting = false;
|
||
let success = false;
|
||
|
||
async function loadCaptcha() {
|
||
if (!root) return;
|
||
const questionEl = root.querySelector("[data-demo-captcha-question]");
|
||
const answerEl = root.querySelector('[name="captchaAnswer"]');
|
||
const reloadBtn = root.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"
|
||
? `${idPrefix}-captcha-err`
|
||
: `${idPrefix}-${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 showSuccess() {
|
||
success = true;
|
||
const formView = root.querySelector(".demo-modal-form-view");
|
||
const successView = root.querySelector(".demo-modal-success");
|
||
if (formView) formView.hidden = true;
|
||
if (successView) successView.hidden = false;
|
||
}
|
||
|
||
function reset() {
|
||
success = false;
|
||
captchaToken = "";
|
||
const form = root.querySelector(".demo-modal-form");
|
||
form?.reset();
|
||
root.querySelectorAll(".demo-field-error").forEach((el) => {
|
||
el.hidden = true;
|
||
el.textContent = "";
|
||
});
|
||
root.querySelectorAll(".demo-field").forEach((el) => {
|
||
el.classList.remove("has-error");
|
||
});
|
||
const formErr = root.querySelector(".demo-form-error");
|
||
if (formErr) {
|
||
formErr.hidden = true;
|
||
formErr.textContent = "";
|
||
}
|
||
const formView = root.querySelector(".demo-modal-form-view");
|
||
const successView = root.querySelector(".demo-modal-success");
|
||
if (formView) formView.hidden = false;
|
||
if (successView) successView.hidden = true;
|
||
void loadCaptcha();
|
||
}
|
||
|
||
const form = root.querySelector(".demo-modal-form");
|
||
if (!form) {
|
||
return { loadCaptcha, reset, isSuccess: () => success };
|
||
}
|
||
|
||
root
|
||
.querySelector("[data-demo-captcha-reload]")
|
||
?.addEventListener("click", () => {
|
||
void loadCaptcha();
|
||
});
|
||
root.querySelector("[data-demo-reset]")?.addEventListener("click", reset);
|
||
|
||
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"),
|
||
eventType: fd.get("eventType"),
|
||
message: fd.get("message"),
|
||
captchaAnswer: fd.get("captchaAnswer"),
|
||
});
|
||
|
||
["name", "mobile", "email", "eventType", "message", "captchaAnswer"].forEach(
|
||
(key) => {
|
||
const errKey = key === "captchaAnswer" ? "captcha" : key;
|
||
setFieldError(form, key, result.errors[errKey] || "");
|
||
}
|
||
);
|
||
|
||
const formErr = root.querySelector(".demo-form-error");
|
||
if (formErr) {
|
||
formErr.hidden = true;
|
||
formErr.textContent = "";
|
||
}
|
||
|
||
if (!result.ok) {
|
||
const first =
|
||
["name", "mobile", "email", "eventType", "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 = root.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,
|
||
eventType: result.data.eventType,
|
||
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 { loadCaptcha, reset, isSuccess: () => success };
|
||
}
|
||
|
||
function initDemoPage() {
|
||
const root = document.querySelector("[data-demo-page]");
|
||
if (!root) return;
|
||
bindDemoForm(root, { idPrefix: "help" }).reset();
|
||
}
|
||
|
||
function initDemoModal() {
|
||
let overlay = null;
|
||
let open = false;
|
||
let formApi = null;
|
||
|
||
function build() {
|
||
if (overlay) return overlay;
|
||
|
||
overlay = document.createElement("div");
|
||
overlay.className = "demo-modal-overlay";
|
||
overlay.setAttribute("role", "presentation");
|
||
overlay.innerHTML = `
|
||
<div class="demo-modal" role="dialog" aria-modal="true" aria-labelledby="demo-modal-title">
|
||
<button type="button" class="demo-modal-close" aria-label="Close">×</button>
|
||
<div class="demo-modal-form-view">
|
||
<div class="demo-modal-head">
|
||
<h2 id="demo-modal-title">Request a demo</h2>
|
||
<p>Tell us a bit about your event and we'll get back to you.</p>
|
||
</div>
|
||
<form class="demo-modal-form" novalidate>
|
||
<div class="demo-field">
|
||
<span>Name</span>
|
||
<input id="demo-name" name="name" type="text" autocomplete="name" placeholder="Full name" required />
|
||
<p class="demo-field-error" id="demo-name-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Mobile</span>
|
||
<input id="demo-mobile" name="mobile" type="tel" autocomplete="tel" placeholder="Mobile number" required />
|
||
<p class="demo-field-error" id="demo-mobile-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Email</span>
|
||
<input id="demo-email" name="email" type="email" autocomplete="email" placeholder="Work email" required />
|
||
<p class="demo-field-error" id="demo-email-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Event type</span>
|
||
<select id="demo-event-type" name="eventType" required>
|
||
<option value="" disabled selected>Select event type</option>
|
||
<option value="Webinar">Webinar</option>
|
||
<option value="Conference">Conference</option>
|
||
<option value="Exhibition">Exhibition</option>
|
||
<option value="Internal Sales Meet">Internal Sales Meet</option>
|
||
<option value="Others">Others</option>
|
||
</select>
|
||
<p class="demo-field-error" id="demo-eventType-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Message</span>
|
||
<textarea id="demo-message" name="message" rows="4" placeholder="Tell us about your event" required></textarea>
|
||
<p class="demo-field-error" id="demo-message-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field demo-captcha-field">
|
||
<span>Security check</span>
|
||
<div class="demo-captcha-row">
|
||
<span class="demo-captcha-question" data-demo-captcha-question aria-live="polite">Loading…</span>
|
||
<input
|
||
id="demo-captcha"
|
||
name="captchaAnswer"
|
||
type="text"
|
||
inputmode="numeric"
|
||
autocomplete="off"
|
||
placeholder="Answer"
|
||
required
|
||
/>
|
||
<button type="button" class="demo-captcha-reload" data-demo-captcha-reload aria-label="Reload captcha">↻</button>
|
||
</div>
|
||
<p class="demo-field-error" id="demo-captcha-err" hidden></p>
|
||
</div>
|
||
<p class="demo-form-error" hidden></p>
|
||
<div class="demo-modal-actions">
|
||
<button type="submit" class="btn btn-primary demo-submit-btn">Request demo</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="demo-modal-success" hidden>
|
||
<h2>Thanks, We'll be in touch.</h2>
|
||
<p>We have received your request. Our team will get in touch with you shortly.</p>
|
||
<div class="demo-modal-actions">
|
||
<button type="button" class="btn btn-primary demo-success-close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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);
|
||
formApi = bindDemoForm(overlay, { idPrefix: "demo" });
|
||
|
||
return overlay;
|
||
}
|
||
|
||
function openModal() {
|
||
const el = build();
|
||
if (!open) formApi.reset();
|
||
open = true;
|
||
document.body.style.overflow = "hidden";
|
||
requestAnimationFrame(() => {
|
||
el.classList.add("is-open");
|
||
el.querySelector(".demo-modal")?.classList.add("is-open");
|
||
if (!formApi.isSuccess()) {
|
||
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();
|
||
});
|
||
}
|
||
|
||
const PLANS = ["Start", "Grow", "Scale", "Enterprise"];
|
||
|
||
function validateTalk(data) {
|
||
const errors = {};
|
||
const name = (data.name || "").trim();
|
||
const mobile = (data.mobile || "").trim();
|
||
const email = (data.email || "").trim();
|
||
const selectedPlan = (data.selectedPlan || "").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 (!selectedPlan) errors.selectedPlan = "Selected plan is required.";
|
||
else if (!PLANS.includes(selectedPlan)) {
|
||
errors.selectedPlan = "Select a valid plan.";
|
||
}
|
||
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,
|
||
selectedPlan,
|
||
message,
|
||
captchaAnswer,
|
||
},
|
||
};
|
||
}
|
||
|
||
function initTalkModal() {
|
||
let overlay = null;
|
||
let open = false;
|
||
let success = false;
|
||
let captchaToken = "";
|
||
let captchaLoading = false;
|
||
let submitting = false;
|
||
let selectedPlan = "";
|
||
|
||
async function loadCaptcha() {
|
||
if (!overlay) return;
|
||
const questionEl = overlay.querySelector("[data-talk-captcha-question]");
|
||
const answerEl = overlay.querySelector('[name="captchaAnswer"]');
|
||
const reloadBtn = overlay.querySelector("[data-talk-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"
|
||
? "talk-captcha-err"
|
||
: `talk-${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 = `
|
||
<div class="demo-modal" role="dialog" aria-modal="true" aria-labelledby="talk-modal-title">
|
||
<button type="button" class="demo-modal-close" aria-label="Close">×</button>
|
||
<div class="demo-modal-form-view">
|
||
<div class="demo-modal-head">
|
||
<h2 id="talk-modal-title">Talk to our team</h2>
|
||
<p>Tell us about your event and the plan you’re interested in.</p>
|
||
</div>
|
||
<form class="demo-modal-form" novalidate>
|
||
<div class="demo-field">
|
||
<span>Name</span>
|
||
<input id="talk-name" name="name" type="text" autocomplete="name" placeholder="Full name" required />
|
||
<p class="demo-field-error" id="talk-name-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Mobile</span>
|
||
<input id="talk-mobile" name="mobile" type="tel" autocomplete="tel" placeholder="Mobile number" required />
|
||
<p class="demo-field-error" id="talk-mobile-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Email</span>
|
||
<input id="talk-email" name="email" type="email" autocomplete="email" placeholder="Work email" required />
|
||
<p class="demo-field-error" id="talk-email-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Selected plan</span>
|
||
<input id="talk-plan" name="selectedPlan" type="text" readonly tabindex="-1" />
|
||
<p class="demo-field-error" id="talk-selectedPlan-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field">
|
||
<span>Message</span>
|
||
<textarea id="talk-message" name="message" rows="4" placeholder="Tell us about your event" required></textarea>
|
||
<p class="demo-field-error" id="talk-message-err" hidden></p>
|
||
</div>
|
||
<div class="demo-field demo-captcha-field">
|
||
<span>Security check</span>
|
||
<div class="demo-captcha-row">
|
||
<span class="demo-captcha-question" data-talk-captcha-question aria-live="polite">Loading…</span>
|
||
<input
|
||
id="talk-captcha"
|
||
name="captchaAnswer"
|
||
type="text"
|
||
inputmode="numeric"
|
||
autocomplete="off"
|
||
placeholder="Answer"
|
||
required
|
||
/>
|
||
<button type="button" class="demo-captcha-reload" data-talk-captcha-reload aria-label="Reload captcha">↻</button>
|
||
</div>
|
||
<p class="demo-field-error" id="talk-captcha-err" hidden></p>
|
||
</div>
|
||
<p class="demo-form-error" hidden></p>
|
||
<div class="demo-modal-actions">
|
||
<button type="submit" class="btn btn-primary talk-submit-btn">Talk to our team</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="demo-modal-success" hidden>
|
||
<h2>Thanks, we'll be in touch.</h2>
|
||
<p>We have received your request. Our team will get in touch with you shortly.</p>
|
||
<div class="demo-modal-actions">
|
||
<button type="button" class="btn btn-primary talk-success-close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(overlay);
|
||
|
||
overlay.addEventListener("click", (e) => {
|
||
if (e.target === overlay) close();
|
||
});
|
||
overlay.querySelector(".demo-modal-close")?.addEventListener("click", close);
|
||
overlay.querySelector(".talk-success-close")?.addEventListener("click", close);
|
||
overlay.querySelector("[data-talk-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 = validateTalk({
|
||
name: fd.get("name"),
|
||
mobile: fd.get("mobile"),
|
||
email: fd.get("email"),
|
||
selectedPlan: fd.get("selectedPlan") || selectedPlan,
|
||
message: fd.get("message"),
|
||
captchaAnswer: fd.get("captchaAnswer"),
|
||
});
|
||
|
||
["name", "mobile", "email", "selectedPlan", "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", "selectedPlan", "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(".talk-submit-btn");
|
||
submitting = true;
|
||
if (submitBtn) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = "Sending…";
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/public/talk-to-team`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
name: result.data.name,
|
||
mobile: result.data.mobile,
|
||
email: result.data.email,
|
||
selectedPlan: result.data.selectedPlan,
|
||
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 = "Talk to our team";
|
||
}
|
||
}
|
||
});
|
||
|
||
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;
|
||
const planInput = overlay?.querySelector('[name="selectedPlan"]');
|
||
if (planInput) planInput.value = selectedPlan;
|
||
void loadCaptcha();
|
||
}
|
||
|
||
function openModal(plan) {
|
||
selectedPlan = plan;
|
||
const el = build();
|
||
if (!open) reset();
|
||
else {
|
||
const planInput = el.querySelector('[name="selectedPlan"]');
|
||
if (planInput) planInput.value = selectedPlan;
|
||
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-talk-open]");
|
||
if (!trigger) return;
|
||
e.preventDefault();
|
||
const plan =
|
||
(trigger.getAttribute("data-plan") || "").trim() ||
|
||
trigger.closest(".tier")?.querySelector("h3")?.textContent.trim() ||
|
||
"";
|
||
openModal(plan);
|
||
});
|
||
|
||
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;
|
||
}
|
||
|
||
const revealLine = () => window.innerHeight * 0.88;
|
||
const isPastRevealPoint = (el) => el.getBoundingClientRect().top < revealLine();
|
||
|
||
if (window.gsap && window.ScrollTrigger) {
|
||
const gsap = window.gsap;
|
||
const ScrollTrigger = window.ScrollTrigger;
|
||
gsap.registerPlugin(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);
|
||
|
||
gsap.fromTo(
|
||
el,
|
||
{ autoAlpha: 0, y, willChange: "opacity, transform" },
|
||
{
|
||
autoAlpha: 1,
|
||
y: 0,
|
||
duration,
|
||
delay,
|
||
ease: "power2.out",
|
||
onComplete: () => gsap.set(el, { clearProps: "willChange" }),
|
||
scrollTrigger: {
|
||
trigger: el,
|
||
start: "top 88%",
|
||
toggleActions: "play none none none",
|
||
once: true,
|
||
},
|
||
}
|
||
);
|
||
});
|
||
|
||
const showRestored = () => {
|
||
nodes.forEach((el) => {
|
||
if (isPastRevealPoint(el)) {
|
||
gsap.set(el, { autoAlpha: 1, y: 0, overwrite: true, clearProps: "willChange" });
|
||
}
|
||
});
|
||
ScrollTrigger.refresh();
|
||
};
|
||
|
||
requestAnimationFrame(showRestored);
|
||
window.addEventListener("load", showRestored, { once: true });
|
||
window.addEventListener("pageshow", showRestored);
|
||
return;
|
||
}
|
||
|
||
// Fallback without GSAP
|
||
const show = (el) => {
|
||
el.style.opacity = "1";
|
||
el.style.transform = "none";
|
||
};
|
||
const io = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
show(entry.target);
|
||
io.unobserve(entry.target);
|
||
}
|
||
});
|
||
},
|
||
{ threshold: 0.12, rootMargin: "0px 0px -12% 0px" }
|
||
);
|
||
const syncFallback = () => {
|
||
nodes.forEach((el) => {
|
||
if (isPastRevealPoint(el)) {
|
||
show(el);
|
||
io.unobserve(el);
|
||
}
|
||
});
|
||
};
|
||
nodes.forEach((el) => {
|
||
if (isPastRevealPoint(el)) {
|
||
show(el);
|
||
return;
|
||
}
|
||
el.style.opacity = "0";
|
||
el.style.transform = "translateY(28px)";
|
||
el.style.transition = "opacity .7s ease, transform .7s ease";
|
||
io.observe(el);
|
||
});
|
||
window.addEventListener("load", syncFallback, { once: true });
|
||
window.addEventListener("pageshow", syncFallback);
|
||
}
|
||
|
||
/* ---------- Customer stories slider ---------- */
|
||
function initStoriesSlider() {
|
||
const root = document.querySelector(".stories-slider");
|
||
if (!root) return;
|
||
|
||
const viewport = root.querySelector(".stories-slider-viewport");
|
||
const track = root.querySelector(".stories-slider-track");
|
||
const slides = Array.from(root.querySelectorAll(".stories-slide"));
|
||
const buttons = root.querySelectorAll(".stories-slider-btn");
|
||
const prevBtn = buttons[0];
|
||
const nextBtn = buttons[1];
|
||
if (!viewport || !track || slides.length < 2 || !prevBtn || !nextBtn) return;
|
||
|
||
const GAP = 20;
|
||
const DEFAULT_AUTOPLAY_MS = 5000;
|
||
const reduceMotion = window.matchMedia(
|
||
"(prefers-reduced-motion: reduce)"
|
||
).matches;
|
||
let index = 0;
|
||
let dragStartX = 0;
|
||
let dragging = false;
|
||
let autoplayTimer = null;
|
||
|
||
function autoplayDelay() {
|
||
const raw = root.getAttribute("data-autoplay");
|
||
if (raw == null || raw === "") return DEFAULT_AUTOPLAY_MS;
|
||
if (/^(false|off|none)$/i.test(raw.trim())) return 0;
|
||
const ms = Number(raw);
|
||
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
||
}
|
||
|
||
function perView() {
|
||
const w = viewport.clientWidth;
|
||
if (w < 640) return 1;
|
||
if (w < 900) return 2;
|
||
return 3;
|
||
}
|
||
|
||
function maxIndex() {
|
||
return Math.max(0, slides.length - perView());
|
||
}
|
||
|
||
function stepWidth() {
|
||
const n = perView();
|
||
return (viewport.clientWidth - GAP * (n - 1)) / n + GAP;
|
||
}
|
||
|
||
function goTo(next, animate) {
|
||
index = Math.max(0, Math.min(next, maxIndex()));
|
||
const offset = index * stepWidth();
|
||
track.style.transition =
|
||
animate && !reduceMotion ? "transform .4s ease" : "none";
|
||
track.style.transform = offset ? `translateX(-${offset}px)` : "none";
|
||
prevBtn.disabled = index <= 0;
|
||
nextBtn.disabled = index >= maxIndex();
|
||
}
|
||
|
||
function stopAutoplay() {
|
||
if (autoplayTimer) {
|
||
clearInterval(autoplayTimer);
|
||
autoplayTimer = null;
|
||
}
|
||
}
|
||
|
||
function startAutoplay() {
|
||
stopAutoplay();
|
||
const delay = autoplayDelay();
|
||
if (!delay || reduceMotion || maxIndex() <= 0) return;
|
||
autoplayTimer = setInterval(() => {
|
||
const max = maxIndex();
|
||
if (max <= 0) return;
|
||
goTo(index >= max ? 0 : index + 1, true);
|
||
}, delay);
|
||
}
|
||
|
||
function goAndRestart(next) {
|
||
goTo(next, true);
|
||
startAutoplay();
|
||
}
|
||
|
||
prevBtn.addEventListener("click", () => goAndRestart(index - 1));
|
||
nextBtn.addEventListener("click", () => goAndRestart(index + 1));
|
||
|
||
viewport.addEventListener("pointerdown", (e) => {
|
||
if (e.pointerType === "mouse" && e.button !== 0) return;
|
||
dragging = true;
|
||
dragStartX = e.clientX;
|
||
stopAutoplay();
|
||
});
|
||
const endDrag = (e) => {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
const dx = e.clientX - dragStartX;
|
||
if (Math.abs(dx) >= 40) goTo(index + (dx < 0 ? 1 : -1), true);
|
||
startAutoplay();
|
||
};
|
||
viewport.addEventListener("pointerup", endDrag);
|
||
viewport.addEventListener("pointercancel", () => {
|
||
dragging = false;
|
||
startAutoplay();
|
||
});
|
||
|
||
root.addEventListener("mouseenter", stopAutoplay);
|
||
root.addEventListener("mouseleave", startAutoplay);
|
||
root.addEventListener("focusin", stopAutoplay);
|
||
root.addEventListener("focusout", (e) => {
|
||
if (!root.contains(e.relatedTarget)) startAutoplay();
|
||
});
|
||
document.addEventListener("visibilitychange", () => {
|
||
if (document.hidden) stopAutoplay();
|
||
else startAutoplay();
|
||
});
|
||
|
||
const onResize = () => goTo(index, false);
|
||
window.addEventListener("resize", onResize);
|
||
if (typeof ResizeObserver !== "undefined") {
|
||
new ResizeObserver(onResize).observe(viewport);
|
||
}
|
||
goTo(0, false);
|
||
startAutoplay();
|
||
}
|
||
|
||
/* ---------- Back to top ---------- */
|
||
function initBackToTop() {
|
||
if (document.querySelector(".back-to-top")) return;
|
||
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "back-to-top";
|
||
btn.setAttribute("aria-label", "Back to top");
|
||
btn.innerHTML =
|
||
'<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg>';
|
||
document.body.appendChild(btn);
|
||
|
||
const SHOW_AFTER = 400;
|
||
const reduceMotion = window.matchMedia(
|
||
"(prefers-reduced-motion: reduce)"
|
||
).matches;
|
||
|
||
const sync = () => {
|
||
btn.classList.toggle("is-visible", window.scrollY > SHOW_AFTER);
|
||
};
|
||
|
||
window.addEventListener("scroll", sync, { passive: true });
|
||
sync();
|
||
|
||
btn.addEventListener("click", () => {
|
||
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
||
});
|
||
}
|
||
|
||
/* ---------- Platform feature showcase ---------- */
|
||
function initPlatShowcase() {
|
||
const root = document.querySelector(".plat-split");
|
||
if (!root) return;
|
||
|
||
const cards = Array.from(root.querySelectorAll(".plat-card"));
|
||
const images = Array.from(root.querySelectorAll(".plat-preview img"));
|
||
const descEl = root.querySelector("[data-plat-desc]");
|
||
if (!cards.length || !images.length) return;
|
||
|
||
const DEFAULT_AUTOPLAY_MS = 5000;
|
||
const reduceMotion = window.matchMedia(
|
||
"(prefers-reduced-motion: reduce)"
|
||
).matches;
|
||
const gsap = window.gsap;
|
||
const canAnimate = Boolean(gsap) && !reduceMotion;
|
||
if (canAnimate) root.classList.add("is-gsap-preview");
|
||
|
||
let index = Math.max(
|
||
0,
|
||
cards.findIndex((card) => card.classList.contains("is-active"))
|
||
);
|
||
let autoplayTimer = null;
|
||
let inView = typeof IntersectionObserver === "undefined";
|
||
|
||
function autoplayDelay() {
|
||
const raw = root.getAttribute("data-autoplay");
|
||
if (raw == null || raw === "") return DEFAULT_AUTOPLAY_MS;
|
||
if (/^(false|off|none)$/i.test(raw.trim())) return 0;
|
||
const ms = Number(raw);
|
||
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
||
}
|
||
|
||
function imageFor(i) {
|
||
const id = cards[i]?.dataset.plat;
|
||
return images.find((img) => img.dataset.plat === id) || null;
|
||
}
|
||
|
||
function setCards(i) {
|
||
cards.forEach((card, idx) => {
|
||
const on = idx === i;
|
||
card.classList.toggle("is-active", on);
|
||
card.setAttribute("aria-selected", on ? "true" : "false");
|
||
});
|
||
if (descEl) {
|
||
descEl.textContent =
|
||
cards[i]?.querySelector("p")?.textContent.trim() || "";
|
||
}
|
||
}
|
||
|
||
function direction(from, to) {
|
||
const n = cards.length;
|
||
if (from === n - 1 && to === 0) return 1;
|
||
if (from === 0 && to === n - 1) return -1;
|
||
return to > from ? 1 : -1;
|
||
}
|
||
|
||
function showInstant(nextIndex) {
|
||
index = nextIndex;
|
||
setCards(index);
|
||
images.forEach((img) => {
|
||
const on = img === imageFor(index);
|
||
img.classList.toggle("is-active", on);
|
||
if (gsap) {
|
||
gsap.killTweensOf(img);
|
||
gsap.set(img, { autoAlpha: on ? 1 : 0, x: 0, zIndex: on ? 1 : 0 });
|
||
}
|
||
});
|
||
}
|
||
|
||
function show(nextIndex, animate) {
|
||
if (nextIndex === index) return;
|
||
const prevImg = imageFor(index);
|
||
const nextImg = imageFor(nextIndex);
|
||
if (!nextImg) return;
|
||
|
||
if (!canAnimate || !animate || !prevImg) {
|
||
showInstant(nextIndex);
|
||
return;
|
||
}
|
||
|
||
const dir = direction(index, nextIndex);
|
||
index = nextIndex;
|
||
setCards(index);
|
||
|
||
gsap.killTweensOf(images);
|
||
images.forEach((img) => {
|
||
if (img !== prevImg && img !== nextImg) {
|
||
img.classList.remove("is-active");
|
||
gsap.set(img, { autoAlpha: 0, x: 0, zIndex: 0 });
|
||
}
|
||
});
|
||
|
||
nextImg.classList.add("is-active");
|
||
prevImg.classList.remove("is-active");
|
||
gsap.set(prevImg, { zIndex: 1, autoAlpha: 1, x: 0 });
|
||
gsap.set(nextImg, { zIndex: 2, autoAlpha: 0, x: 40 * dir });
|
||
|
||
gsap.to(prevImg, {
|
||
autoAlpha: 0,
|
||
x: -32 * dir,
|
||
duration: 0.45,
|
||
ease: "power2.inOut",
|
||
onComplete: () => {
|
||
gsap.set(prevImg, { x: 0, zIndex: 0 });
|
||
},
|
||
});
|
||
gsap.to(nextImg, {
|
||
autoAlpha: 1,
|
||
x: 0,
|
||
duration: 0.5,
|
||
ease: "power2.out",
|
||
});
|
||
}
|
||
|
||
function stopAutoplay() {
|
||
if (autoplayTimer) {
|
||
clearInterval(autoplayTimer);
|
||
autoplayTimer = null;
|
||
}
|
||
}
|
||
|
||
function startAutoplay() {
|
||
stopAutoplay();
|
||
const delay = autoplayDelay();
|
||
if (!delay || !inView || document.hidden || cards.length < 2) return;
|
||
autoplayTimer = setInterval(() => {
|
||
show((index + 1) % cards.length, true);
|
||
}, delay);
|
||
}
|
||
|
||
function goAndRestart(nextIndex) {
|
||
show(nextIndex, true);
|
||
startAutoplay();
|
||
}
|
||
|
||
cards.forEach((card, i) => {
|
||
card.addEventListener("click", () => goAndRestart(i));
|
||
});
|
||
|
||
root.addEventListener("mouseenter", stopAutoplay);
|
||
root.addEventListener("mouseleave", startAutoplay);
|
||
root.addEventListener("focusin", stopAutoplay);
|
||
root.addEventListener("focusout", (e) => {
|
||
if (!root.contains(e.relatedTarget)) startAutoplay();
|
||
});
|
||
document.addEventListener("visibilitychange", () => {
|
||
if (document.hidden) stopAutoplay();
|
||
else startAutoplay();
|
||
});
|
||
|
||
if (typeof IntersectionObserver !== "undefined") {
|
||
const io = new IntersectionObserver(
|
||
(entries) => {
|
||
inView = entries.some((entry) => entry.isIntersecting);
|
||
if (inView) startAutoplay();
|
||
else stopAutoplay();
|
||
},
|
||
{ threshold: 0.35 }
|
||
);
|
||
io.observe(root);
|
||
}
|
||
|
||
showInstant(index);
|
||
startAutoplay();
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
initNav();
|
||
initInPageNav();
|
||
initNavSpy();
|
||
initDemoPage();
|
||
initDemoModal();
|
||
initTalkModal();
|
||
initReveals();
|
||
initStoriesSlider();
|
||
initPlatShowcase();
|
||
initBackToTop();
|
||
});
|
||
})();
|