From caac8addcb8af7c1bf8edec96c12fb16ebe90301 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 15 May 2026 14:35:23 -0300 Subject: [PATCH] feat: split components into mybizapp.js + trial_modal.html, add show_calendar() --- components/mybizapp.js | 629 +++++++++++++++++++++++++++++++++++ components/trial_modal.html | 64 ++++ components/trial_modal.js | 644 ------------------------------------ index.html | 10 +- 4 files changed, 698 insertions(+), 649 deletions(-) create mode 100644 components/mybizapp.js create mode 100644 components/trial_modal.html delete mode 100644 components/trial_modal.js diff --git a/components/mybizapp.js b/components/mybizapp.js new file mode 100644 index 0000000..17e9b44 --- /dev/null +++ b/components/mybizapp.js @@ -0,0 +1,629 @@ +/** + * 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 = [ + '', + ].join("\n"); + + var tmp = document.createElement("div"); + tmp.innerHTML = html; + document.body.appendChild(tmp.firstElementChild); + _calInjected = true; + + _wireCalendarEvents(); + } + + /* --- Wire calendar modal events ----------------------------------- */ + function _wireCalendarEvents() { + var overlay = document.getElementById("cal-overlay"); + var box = document.getElementById("cal-box"); + var closeBtn = document.getElementById("cal-close"); + + function _closeCalendar() { + overlay.style.display = "none"; + document.body.style.overflow = ""; + } + + closeBtn.addEventListener("click", _closeCalendar); + overlay.addEventListener("click", function (e) { + if (e.target === overlay || !box.contains(e.target)) _closeCalendar(); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && overlay.style.display === "flex") _closeCalendar(); + }); + } + + /* ================================================================== */ + /* DOM AUTO-WIRING */ + /* ================================================================== */ + + function _wireAll() { + /* Trial triggers */ + var primary = document.getElementById("open-modal"); + if (primary) { + primary.addEventListener("click", function (e) { + e.preventDefault(); + show_trial(); + }); + } + try { + document.querySelectorAll(cfg.triggerSelector).forEach(function (el) { + if (el.id === "open-modal") return; /* already handled above */ + el.addEventListener("click", function (e) { + e.preventDefault(); + show_trial(el.getAttribute("data-plan") || undefined); + }); + }); + } catch (err) { + console.warn("[mybizapp] invalid triggerSelector:", cfg.triggerSelector, err); + } + + /* Calendar triggers */ + try { + document.querySelectorAll(cfg.calendarSelector).forEach(function (el) { + el.addEventListener("click", function (e) { + e.preventDefault(); + show_calendar(); + }); + }); + } catch (err) { + console.warn("[mybizapp] invalid calendarSelector:", cfg.calendarSelector, err); + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", _wireAll); + } else { + _wireAll(); + } + + /* ================================================================== */ + /* UTILITIES */ + /* ================================================================== */ + + function _esc(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + /* ================================================================== */ + /* PUBLIC API */ + /* ================================================================== */ + + window.show_trial = show_trial; + window.show_calendar = show_calendar; + +})(); diff --git a/components/trial_modal.html b/components/trial_modal.html new file mode 100644 index 0000000..bedad3c --- /dev/null +++ b/components/trial_modal.html @@ -0,0 +1,64 @@ + diff --git a/components/trial_modal.js b/components/trial_modal.js deleted file mode 100644 index db94d08..0000000 --- a/components/trial_modal.js +++ /dev/null @@ -1,644 +0,0 @@ -/** - * 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, - }; -})(); diff --git a/index.html b/index.html index 5eb3636..8c6ba5b 100644 --- a/index.html +++ b/index.html @@ -1662,8 +1662,8 @@