/** * mybizapp.js — my-biz.app UI Components * ======================================== * Provides two global modal functions: * * show_trial(plan?) — free-trial sign-up modal * HTML lives in components/trial_modal.html (lazy-fetched) * * show_calendar() — book-a-meeting modal (Google Calendar iframe) * * CONFIGURATION (set before this script loads) * ---------------------------------------------- * window.TrialModalConfig = { * webhook : string — POST endpoint (multipart/form-data) * product : string — hidden field value (default: "odoo_19") * title : string — modal heading * subtitle : string — modal sub-heading * submitLabel : string — submit button label * confirmTitle : string — success heading * confirmSub : string — success body text * calendarUrl : string — Google Calendar embed URL * triggerSelector : string — CSS selectors for trial triggers * (default: "#open-modal, .open-modal") * calendarSelector : string — CSS selectors for calendar triggers * (default: ".open-calendar") * locations : Array<{ value: string, label: string }> * utm_source / utm_medium / utm_campaign / utm_term / utm_content * } * * AUTO-WIRING (on DOMContentLoaded) * ------------------------------------ * #open-modal, .open-modal → show_trial(data-plan?) * .open-calendar → show_calendar() * * DESIGN TOKENS (CSS custom properties, fallback to my-biz.app theme) * -------------------------------------------------------------------- * --tm-bg, --tm-card, --tm-white, --tm-silver, --tm-grey * --tm-gold, --tm-gold-hi, --tm-gold-lo, --tm-gold-glow, --tm-border, --tm-radius */ (function () { "use strict"; /* ------------------------------------------------------------------ */ /* 1. BASE PATH (resolved from this script's src) */ /* ------------------------------------------------------------------ */ var _scriptBase = (function () { var s = document.currentScript; if (s && s.src) return s.src.replace(/\/[^/]+$/, "/") + "/"; return "components/"; })(); /* ------------------------------------------------------------------ */ /* 2. CONFIG */ /* ------------------------------------------------------------------ */ var DEFAULTS = { webhook: "", product: "odoo_19", title: "Start Your 4 Weeks Free Trial", subtitle: "Test drive your back-office in a 4-week free trial.", submitLabel: "Start Free Trial", confirmTitle: "Thank you! \uD83C\uDF89", confirmSub: "We\u2019ve received your request and a specialist will be in touch within one business day.", calendarUrl: "https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ3DDbaiHFlhNhWySszAQoPXE_H73QLqYT3w7H9IYWC76RA_TgNIhLESjb4N7ep_D2D_OyW9q4-c", triggerSelector: "#open-modal, .open-modal", calendarSelector: ".open-calendar", locations: [ { value: "boston", label: "\uD83C\uDDFA\uD83C\uDDF8 Boston" }, { value: "manchester", label: "\uD83C\uDDEC\uD83C\uDDE7 Manchester" }, { value: "mumbai", label: "\uD83C\uDDEE\uD83C\uDDF3 Mumbai" }, { value: "saopaulo", label: "\uD83C\uDDE7\uD83C\uDDF7 S\u00e3o Paulo" }, { value: "meppel", label: "\uD83C\uDDF3\uD83C\uDDF1 Meppel" }, { value: "sydney", label: "\uD83C\uDDE6\uD83C\uDDFA Sydney" }, ], utm_source: "homepage", utm_medium: "direct", utm_campaign: "none", utm_term: "", utm_content: "", }; var cfg = {}; for (var k in DEFAULTS) { if (Object.prototype.hasOwnProperty.call(DEFAULTS, k)) cfg[k] = DEFAULTS[k]; } var userCfg = window.TrialModalConfig || {}; for (var k in userCfg) { if (Object.prototype.hasOwnProperty.call(userCfg, k)) cfg[k] = userCfg[k]; } /* URL query string overrides UTM config */ var urlParams = new URLSearchParams(window.location.search); ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"].forEach(function (p) { var v = urlParams.get(p); if (v) cfg[p] = v; }); /* ------------------------------------------------------------------ */ /* 3. INJECT SHARED CSS */ /* ------------------------------------------------------------------ */ var CSS = [ /* Token fallbacks — scoped so host-page :root overrides still win */ "#tm-overlay, #cal-overlay {", " --tm-bg: rgba(62,39,35,0.84);", " --tm-card: #fff3e0;", " --tm-white: #3e2723;", " --tm-silver: #6d4c41;", " --tm-grey: #a1887f;", " --tm-gold: #e65100;", " --tm-gold-hi: #fb8c00;", " --tm-gold-lo: #bf360c;", " --tm-gold-glow: rgba(230,81,0,0.18);", " --tm-border: rgba(230,81,0,0.18);", " --tm-radius: 28px;", "}", /* ---- Shared overlay ---- */ "#tm-overlay, #cal-overlay {", " position: fixed; inset: 0; z-index: 9900;", " display: none; align-items: center; justify-content: center;", " padding: 20px;", " background: var(--tm-bg);", " backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);", " box-sizing: border-box;", "}", /* ================================================================ */ /* TRIAL MODAL */ /* ================================================================ */ "#tm-box {", " position: relative; width: 100%; max-width: 640px;", " padding: 44px; background: var(--tm-card);", " border-radius: var(--tm-radius);", " border: 1px solid rgba(230,81,0,0.24);", " box-shadow: 0 30px 90px rgba(0,0,0,0.3);", " box-sizing: border-box;", "}", "#tm-close {", " position: absolute; top: 18px; right: 18px;", " width: 40px; height: 40px; border-radius: 50%;", " color: var(--tm-grey); background: rgba(93,64,55,0.05);", " font-size: 1.2rem; border: none; cursor: pointer; line-height: 1;", "}", "#tm-close:hover { color: var(--tm-gold); }", "#tm-box .tm-title {", " font-family: Georgia, 'Times New Roman', serif;", " font-size: 2.2rem; font-weight: 700; line-height: 1.05; margin: 0 0 10px;", " background: linear-gradient(135deg, var(--tm-gold-lo) 0%, var(--tm-gold) 38%, var(--tm-gold-hi) 55%, var(--tm-gold) 75%, var(--tm-gold-lo) 100%);", " -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;", "}", "#tm-box .tm-subtitle {", " font-size: 0.9rem; color: var(--tm-silver); margin: 0 0 26px;", "}", ".tm-grid {", " display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px;", "}", ".tm-field-group { margin-bottom: 18px; }", ".tm-grid .tm-field-group.full { grid-column: 1 / -1; }", ".tm-label {", " display: block; margin-bottom: 8px;", " font-size: 0.62rem; font-family: system-ui, -apple-system, sans-serif;", " text-transform: uppercase; letter-spacing: 0.08em; color: var(--tm-gold);", "}", ".tm-field {", " width: 100%; padding: 14px 16px; border-radius: 16px;", " background: rgba(93,64,55,0.05); border: 1px solid var(--tm-border);", " color: var(--tm-white); box-sizing: border-box; font: inherit; outline: none;", "}", ".tm-field:focus { border-color: var(--tm-gold); box-shadow: 0 0 0 3px var(--tm-gold-glow); }", ".tm-field::placeholder { color: var(--tm-grey); }", ".tm-select { position: relative; }", ".tm-select-btn { position: relative; text-align: left; cursor: pointer; padding-right: 42px; background: none; border: none; }", ".tm-select-btn::after {", " content: ''; position: absolute; right: 16px; top: 50%;", " width: 14px; height: 8px; transform: translateY(-50%);", " background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8'%3E%3Cpath d='M1 1l6 6 6-6' stroke='%23E65100' stroke-width='1.8' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");", " background-repeat: no-repeat; background-position: center; pointer-events: none;", "}", ".tm-select-btn.is-placeholder { color: var(--tm-grey); }", ".tm-select-menu {", " position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: 3;", " max-height: 220px; padding: 8px; background: var(--tm-card);", " border: 1px solid var(--tm-border); border-radius: 16px;", " box-shadow: 0 18px 40px rgba(62,39,35,0.16); overflow-y: auto; display: none;", "}", ".tm-select.open .tm-select-menu { display: block; }", ".tm-select-option {", " width: 100%; padding: 10px 12px; border: none; border-radius: 12px;", " background: transparent; color: var(--tm-white);", " text-align: left; font: inherit; cursor: pointer;", " transition: background 0.2s, color 0.2s;", "}", ".tm-select-option:hover, .tm-select-option:focus-visible, .tm-select-option.is-selected {", " outline: none; background: rgba(230,81,0,0.1); color: var(--tm-gold);", "}", ".tm-submit {", " width: 100%; margin-top: 8px; padding: 15px 20px; border-radius: 50px; border: none;", " cursor: pointer; font-family: system-ui, -apple-system, sans-serif;", " font-size: 0.95rem; font-weight: 600; letter-spacing: 0.02em;", " color: var(--tm-card);", " background: linear-gradient(135deg, var(--tm-gold-lo) 0%, var(--tm-gold) 38%, var(--tm-gold-hi) 55%, var(--tm-gold) 75%, var(--tm-gold-lo) 100%);", " transition: opacity 0.2s;", "}", ".tm-submit:hover { opacity: 0.88; }", ".tm-submit:disabled { opacity: 0.56; cursor: not-allowed; }", "#tm-confirm { display: none; text-align: center; padding-top: 8px; }", "#tm-confirm .tm-confirm-title {", " font-family: Georgia, 'Times New Roman', serif;", " font-size: 1.6rem; color: var(--tm-gold-hi); margin: 0 0 12px;", "}", "#tm-confirm .tm-confirm-sub { color: var(--tm-silver); font-size: 0.9rem; margin: 0 0 24px; }", /* ================================================================ */ /* CALENDAR MODAL */ /* ================================================================ */ "#cal-box {", " position: relative; width: 100%; max-width: 780px;", " background: var(--tm-card);", " border-radius: var(--tm-radius);", " border: 1px solid rgba(230,81,0,0.24);", " box-shadow: 0 30px 90px rgba(0,0,0,0.3);", " box-sizing: border-box; overflow: hidden;", "}", "#cal-header {", " display: flex; align-items: center; justify-content: space-between;", " padding: 22px 32px;", " border-bottom: 1px solid var(--tm-border);", "}", "#cal-header .cal-title {", " font-family: Georgia, 'Times New Roman', serif;", " font-size: 1.5rem; font-weight: 700; margin: 0;", " background: linear-gradient(135deg, var(--tm-gold-lo) 0%, var(--tm-gold) 38%, var(--tm-gold-hi) 55%, var(--tm-gold) 75%, var(--tm-gold-lo) 100%);", " -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;", "}", "#cal-close {", " width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;", " color: var(--tm-grey); background: rgba(93,64,55,0.05);", " font-size: 1.2rem; border: none; cursor: pointer; line-height: 1;", "}", "#cal-close:hover { color: var(--tm-gold); }", "#cal-frame {", " width: 100%; height: 620px; border: none; display: block;", "}", /* ================================================================ */ /* RESPONSIVE */ /* ================================================================ */ "@media (max-width: 860px) {", " #tm-box { padding: 34px 22px; }", " .tm-grid { grid-template-columns: 1fr; }", " #cal-header { padding: 18px 20px; }", " #cal-frame { height: 540px; }", "}", "@media (max-width: 480px) {", " #tm-box .tm-title { font-size: 1.8rem; }", " #cal-header .cal-title { font-size: 1.2rem; }", " #cal-frame { height: 480px; }", "}", ].join("\n"); var _styleEl = document.createElement("style"); _styleEl.id = "mba-styles"; _styleEl.textContent = CSS; document.head.appendChild(_styleEl); /* ================================================================== */ /* TRIAL MODAL */ /* ================================================================== */ var _trialLoading = false; var _trialLoaded = false; var _trialQueue = []; /* pending plan args while HTML is fetching */ /* --- show_trial(plan?) -------------------------------------------- */ function show_trial(plan) { if (_trialLoaded) { _openTrial(plan); return; } _trialQueue.push(plan !== undefined ? plan : null); if (!_trialLoading) { _trialLoading = true; _loadTrialModal(); } } /* --- Fetch + inject trial_modal.html ------------------------------ */ function _loadTrialModal() { var url = _scriptBase + "trial_modal.html"; fetch(url) .then(function (r) { if (!r.ok) throw new Error("[mybizapp] trial_modal.html fetch failed: " + r.status); return r.text(); }) .then(function (html) { var tmp = document.createElement("div"); tmp.innerHTML = html; var node = tmp.firstElementChild; document.body.appendChild(node); _applyTrialConfig(); _wireTrialEvents(); _trialLoaded = true; _trialLoading = false; if (_trialQueue.length) { var plan = _trialQueue[_trialQueue.length - 1]; _trialQueue = []; _openTrial(plan); } }) .catch(function (err) { console.error(err); _trialLoading = false; _trialQueue = []; }); } /* --- Apply TrialModalConfig overrides after HTML is in the DOM ---- */ function _applyTrialConfig() { /* Text content */ var titleEl = document.getElementById("tm-title"); if (titleEl) titleEl.textContent = cfg.title; var subEl = document.querySelector("#tm-box .tm-subtitle"); if (subEl) subEl.textContent = cfg.subtitle; var submitEl = document.querySelector("#tm-form .tm-submit"); if (submitEl) submitEl.textContent = cfg.submitLabel; var confirmTitleEl = document.querySelector("#tm-confirm .tm-confirm-title"); if (confirmTitleEl) confirmTitleEl.textContent = cfg.confirmTitle; var confirmSubEl = document.querySelector("#tm-confirm .tm-confirm-sub"); if (confirmSubEl) confirmSubEl.textContent = cfg.confirmSub; /* Locations — rebuild menu only if config provides a custom list */ if (userCfg.locations) { var menu = document.getElementById("tm-location-menu"); if (menu) { menu.innerHTML = cfg.locations.map(function (loc) { return '"; }).join("\n"); } } /* UTM hidden fields */ var form = document.getElementById("tm-form"); if (form) { var utmMap = { utm_source: cfg.utm_source, utm_medium: cfg.utm_medium, utm_campaign: cfg.utm_campaign, utm_term: cfg.utm_term, utm_content: cfg.utm_content, }; Object.keys(utmMap).forEach(function (name) { var el = form.querySelector('[name="' + name + '"]'); if (el) el.value = utmMap[name]; }); } } /* --- Wire all trial modal events ---------------------------------- */ function _wireTrialEvents() { var overlay = document.getElementById("tm-overlay"); var box = document.getElementById("tm-box"); var closeBtn = document.getElementById("tm-close"); var form = document.getElementById("tm-form"); var confirmDiv = document.getElementById("tm-confirm"); var confirmClose = document.getElementById("tm-confirm-close"); var planField = document.getElementById("tm-plan"); var productField = document.getElementById("tm-product"); var locationField = document.getElementById("tm-location"); var locationSelect = document.getElementById("tm-location-select"); var locationBtn = document.getElementById("tm-location-btn"); var locationMenu = document.getElementById("tm-location-menu"); var defaultProduct = productField.value; /* Location picker */ function _syncLabel() { var val = locationField.value || ""; var selected = null; locationMenu.querySelectorAll(".tm-select-option").forEach(function (opt) { var isSel = opt.getAttribute("data-value") === val; if (isSel) selected = opt; opt.classList.toggle("is-selected", isSel); opt.setAttribute("aria-selected", isSel ? "true" : "false"); }); locationBtn.textContent = selected ? selected.textContent.trim() : "Select your server location"; locationBtn.classList.toggle("is-placeholder", !selected); locationBtn.setAttribute("aria-expanded", locationSelect.classList.contains("open") ? "true" : "false"); } function _openLoc() { locationSelect.classList.add("open"); _syncLabel(); } function _closeLoc() { locationSelect.classList.remove("open"); _syncLabel(); } locationBtn.addEventListener("click", function () { locationSelect.classList.contains("open") ? _closeLoc() : _openLoc(); }); locationMenu.querySelectorAll(".tm-select-option").forEach(function (opt) { opt.addEventListener("click", function () { locationField.value = opt.getAttribute("data-value") || ""; _closeLoc(); }); }); document.addEventListener("click", function (e) { if (!locationSelect.contains(e.target)) _closeLoc(); }); /* Store open/close functions so show_trial() can call them */ overlay._openTrial = function (plan) { planField.value = plan || "General Free Trial"; productField.value = defaultProduct; /* always pinned */ if (window._mbsTracking) window._mbsTracking.applyToFields(form); _closeLoc(); overlay.style.display = "flex"; document.body.style.overflow = "hidden"; }; overlay._closeTrial = function () { overlay.style.display = "none"; document.body.style.overflow = ""; setTimeout(function () { form.style.display = ""; confirmDiv.style.display = "none"; form.reset(); planField.value = "General Free Trial"; productField.value = defaultProduct; locationField.value = ""; if (window._mbsTracking) window._mbsTracking.applyToFields(form); _closeLoc(); var btn = form.querySelector(".tm-submit"); btn.textContent = cfg.submitLabel; btn.disabled = false; }, 200); }; /* Close triggers */ closeBtn.addEventListener("click", function () { overlay._closeTrial(); }); confirmClose.addEventListener("click", function () { overlay._closeTrial(); }); overlay.addEventListener("click", function (e) { if (e.target === overlay || !box.contains(e.target)) overlay._closeTrial(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { if (locationSelect.classList.contains("open")) { _closeLoc(); return; } if (overlay.style.display === "flex") overlay._closeTrial(); } }); /* Form submit */ form.addEventListener("submit", function (e) { e.preventDefault(); if (!cfg.webhook) { console.warn("[mybizapp] No webhook URL set. Add window.TrialModalConfig.webhook."); return; } var btn = form.querySelector(".tm-submit"); var formData = new FormData(form); if (window._mbsTracking) { formData = window._mbsTracking.appendToFormData(formData); window._mbsTracking.trackEvent("Trial", { source: planField.value || "General Free Trial" }); } btn.textContent = "Sending\u2026"; btn.disabled = true; /* Show confirmation regardless — webhook may not return CORS headers */ fetch(cfg.webhook, { method: "POST", body: formData }) .then(function () { form.style.display = "none"; confirmDiv.style.display = "block"; }) .catch(function () { form.style.display = "none"; confirmDiv.style.display = "block"; }); }); } /* --- Internal open helper (called after HTML is confirmed ready) -- */ function _openTrial(plan) { var overlay = document.getElementById("tm-overlay"); if (overlay && overlay._openTrial) overlay._openTrial(plan); } /* ================================================================== */ /* CALENDAR MODAL */ /* ================================================================== */ var _calInjected = false; /* --- show_calendar() ---------------------------------------------- */ function show_calendar() { if (!_calInjected) _buildCalendarModal(); var overlay = document.getElementById("cal-overlay"); if (overlay) { overlay.style.display = "flex"; document.body.style.overflow = "hidden"; } } /* --- Build and inject the calendar modal HTML --------------------- */ function _buildCalendarModal() { var html = [ '