/** * trial_modal.js — Reusable Free-Trial Modal Component * ====================================================== * Drop this script into any page. Configure via window.TrialModalConfig * before the script loads, or pass nothing and accept the defaults. * * CONFIGURATION (all keys optional) * ---------------------------------- * window.TrialModalConfig = { * webhook : string — POST endpoint (multipart/form-data) * product : string — hidden field value (default: "odoo_19") * title : string — modal heading (default: "Start Your 4 Weeks Free Trial") * subtitle : string — modal sub-heading (default: "...") * submitLabel : string — submit button label (default: "Start Free Trial") * confirmTitle : string — success heading (default: "Thank you! 🎉") * confirmSub : string — success body (default: "...") * triggerSelector : string — CSS selector(s) for open triggers * (default: "#open-modal, .open-modal") * locations : Array<{ value: string, label: string }> * (default: 6 preset server locations) * utm_source : string — overridden by ?utm_source (default: "homepage") * utm_medium : string — overridden by ?utm_medium (default: "direct") * utm_campaign : string — overridden by ?utm_campaign (default: "none") * utm_term : string (default: "") * utm_content : string (default: "") * } * * PUBLIC API (window.TrialModal) * -------------------------------- * TrialModal.open(plan?) — open the modal, optionally pre-filling plan * TrialModal.close() — close the modal * * DESIGN TOKENS * ------------- * The modal reads CSS custom properties from the host page when available. * Fallback values match the my-biz.app warm coffee / burnt-orange theme. * Override any token on your :root to re-theme without touching this file. * * TOKEN MAP * --tm-bg modal overlay tint (rgba(62,39,35,0.84)) * --tm-card modal box background (#fff3e0) * --tm-white heading / body text on light bg (#3e2723) * --tm-silver muted body text (#6d4c41) * --tm-grey placeholder / label text (#a1887f) * --tm-gold primary accent / CTA (#e65100) * --tm-gold-hi hover highlight (#fb8c00) * --tm-gold-lo gradient end-stop (#bf360c) * --tm-gold-glow focus ring glow (rgba(230,81,0,0.18)) * --tm-border field border (rgba(230,81,0,0.18)) * --tm-radius box border-radius (28px) */ (function () { "use strict"; /* ------------------------------------------------------------------ */ /* 1. DEFAULTS */ /* ------------------------------------------------------------------ */ 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.", triggerSelector: "#open-modal, .open-modal", 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: "", }; /* ------------------------------------------------------------------ */ /* 2. MERGE CONFIG */ /* ------------------------------------------------------------------ */ 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]; } } /* Resolve UTM params from URL query string — URL wins over 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 CSS */ /* ------------------------------------------------------------------ */ var CSS = [ /* --- Token fallbacks scoped to the overlay (inherited by children) --- Host-page :root tokens take precedence automatically. */ "#tm-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;", "}", /* --- Overlay --- */ "#tm-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;", "}", /* --- Box --- */ "#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;", "}", /* --- Close button --- */ "#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); }", /* --- Title --- */ "#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;", "}", /* --- Subtitle --- */ "#tm-box .tm-subtitle {", " font-size: 0.9rem;", " color: var(--tm-silver);", " margin: 0 0 26px;", "}", /* --- Grid --- */ ".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;", "}", /* --- Field label --- */ ".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);", "}", /* --- Field (input / select / custom button) --- */ ".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); }", /* --- Custom select --- */ ".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);", "}", /* --- Submit button --- */ ".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; }", /* --- Confirmation panel --- */ "#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;", "}", /* --- Responsive --- */ "@media (max-width: 860px) {", " #tm-box { padding: 34px 22px; }", " .tm-grid { grid-template-columns: 1fr; }", "}", "@media (max-width: 480px) {", " #tm-box .tm-title { font-size: 1.8rem; }", "}", ].join("\n"); var styleEl = document.createElement("style"); styleEl.id = "tm-styles"; styleEl.textContent = CSS; document.head.appendChild(styleEl); /* ------------------------------------------------------------------ */ /* 4. BUILD & INJECT HTML */ /* ------------------------------------------------------------------ */ function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function buildLocationOptions() { return cfg.locations .map(function (loc) { return [ '", ].join(""); }) .join("\n"); } var html = [ '", ].join("\n"); var container = document.createElement("div"); container.innerHTML = html; document.body.appendChild(container.firstElementChild); /* ------------------------------------------------------------------ */ /* 5. WIRE UP DOM REFERENCES */ /* ------------------------------------------------------------------ */ 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; /* ------------------------------------------------------------------ */ /* 6. LOCATION PICKER LOGIC */ /* ------------------------------------------------------------------ */ function syncLocationLabel() { var value = locationField.value || ""; var selected = null; locationMenu.querySelectorAll(".tm-select-option").forEach(function (opt) { var isSel = opt.getAttribute("data-value") === value; 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 openLocationMenu() { locationSelect.classList.add("open"); syncLocationLabel(); } function closeLocationMenu() { locationSelect.classList.remove("open"); syncLocationLabel(); } locationBtn.addEventListener("click", function () { locationSelect.classList.contains("open") ? closeLocationMenu() : openLocationMenu(); }); locationMenu.querySelectorAll(".tm-select-option").forEach(function (opt) { opt.addEventListener("click", function () { locationField.value = opt.getAttribute("data-value") || ""; closeLocationMenu(); }); }); /* Close location dropdown on outside click */ document.addEventListener("click", function (e) { if (!locationSelect.contains(e.target)) closeLocationMenu(); }); /* ------------------------------------------------------------------ */ /* 7. MODAL OPEN / CLOSE */ /* ------------------------------------------------------------------ */ function openModal(plan) { planField.value = plan || "General Free Trial"; /* Keep product pinned to configured value — never expose or change */ productField.value = defaultProduct; /* Apply tracking fields if window._mbsTracking is present */ if (window._mbsTracking) window._mbsTracking.applyToFields(form); closeLocationMenu(); overlay.style.display = "flex"; document.body.style.overflow = "hidden"; } function closeModal() { overlay.style.display = "none"; document.body.style.overflow = ""; /* Reset after the fade (200 ms gives any CSS transition time) */ 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); closeLocationMenu(); var btn = form.querySelector(".tm-submit"); btn.textContent = cfg.submitLabel; btn.disabled = false; }, 200); } /* --- Built-in close triggers --- */ closeBtn.addEventListener("click", closeModal); confirmClose.addEventListener("click", closeModal); overlay.addEventListener("click", function (e) { if (e.target === overlay || !box.contains(e.target)) closeModal(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { if (locationSelect.classList.contains("open")) { closeLocationMenu(); return; } if (overlay.style.display === "flex") closeModal(); } }); /* ------------------------------------------------------------------ */ /* 8. WIRE TRIGGER ELEMENTS */ /* ------------------------------------------------------------------ */ function attachTriggers() { /* Primary #open-modal (handled separately for legacy compat) */ var primary = document.getElementById("open-modal"); if (primary) { primary.addEventListener("click", function (e) { e.preventDefault(); openModal(); }); } /* Any element matching triggerSelector */ try { document.querySelectorAll(cfg.triggerSelector).forEach(function (el) { /* Avoid double-binding the primary element */ if (el.id === "open-modal") return; el.addEventListener("click", function (e) { e.preventDefault(); openModal(el.getAttribute("data-plan") || undefined); }); }); } catch (err) { console.warn("[TrialModal] invalid triggerSelector:", cfg.triggerSelector, err); } } /* Run immediately if DOM is ready, otherwise wait */ if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", attachTriggers); } else { attachTriggers(); } /* ------------------------------------------------------------------ */ /* 9. FORM SUBMISSION */ /* ------------------------------------------------------------------ */ form.addEventListener("submit", function (e) { e.preventDefault(); if (!cfg.webhook) { console.warn("[TrialModal] No webhook URL configured. Set window.TrialModalConfig.webhook."); return; } var btn = form.querySelector(".tm-submit"); var formData = new FormData(form); /* Append tracking data if available */ 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 of network outcome (webhook may not return CORS-safe 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"; }); }); /* ------------------------------------------------------------------ */ /* 10. PUBLIC API */ /* ------------------------------------------------------------------ */ window.TrialModal = { /** * Open the trial modal. * @param {string} [plan] - Optional plan name pre-filled in hidden field. */ open: openModal, /** * Close the trial modal and reset the form. */ close: closeModal, }; })();