1/* ========================================================================== 2 OPTIMIZER365 CONFIG 3 4 Everything a marketer needs to change lives in CONFIG below. 5 Nothing under it needs editing for the site to run. 6 ========================================================================== */ 7 8var CONFIG = { 9 10 /* --- booking ------------------------------------------------------ 11 A real link, not a form. It opens cal.com directly. */ 12 BOOKING_URL: "https://cal.com/optimizer365/30min", 13 NEWSLETTER_URL: "https://www.linkedin.com/newsletters/this-week-in-cloudops-7459508050698321920/", 14 COMMUNITY_NEWSLETTER_URL: "https://thecloudopscollective.beehiiv.com/subscribe", 15 16 /* Enquiries are addressed to [email protected]. 17 FormSubmit handles delivery after the recipient activates the service. 18 Native submission preserves its CAPTCHA and delivery confirmation flow. */ 19 FORM_ENDPOINT: "https://formsubmit.co/[email protected]", // Recipient activation verified on the Cloudflare site. 20 21 /* --- contact ----------------------------------------------------- */ 22 EMAIL: "[email protected]", 23 LINKEDIN: "https://www.linkedin.com/company/optimizer365", 24 25 /* --- resources feed ---------------------------------------------- 26 Elfsight app id for the Optimizer365 LinkedIn feed. Nandan's personal 27 feed is deliberately not included. Blank hides the embed and leaves 28 the written fallback in place. */ 29 ELFSIGHT_APP_ID: "52d0aed8-8836-4b01-a737-ea9fd4432755", 30 31 /* --- analytics ----------------------------------------------------- 32 Google Analytics 4. GA sets cookies, so nothing loads until a 33 visitor accepts. Blank this and analytics stops entirely, the 34 banner disappears, and the cookies paragraph in the privacy notice 35 has to change back. */ 36 GA_ID: "", 37 38 /* --- switches ----------------------------------------------------- */ 39 SHOW_EVENT_BANNER: false, 40 EVENT_NAME: "", 41 EVENT_DETAIL: "", 42 EVENT_URL: "", 43 44 /* --- legal --------------------------------------------------------- */ 45 LEGAL_ENTITY: "Itenic Holding Pty Ltd (ABN 30 633 888 561) trading as Optimizer365" 46}; 47 48/* ========================================================================== 49 Wiring only below this line. 50 ========================================================================== */ 51(function () { 52 "use strict"; 53 54 function track(name) { 55 if (window.dataLayer) window.dataLayer.push({ event: "cta_click", cta: name }); 56 if (window.gtag) window.gtag("event", "cta_click", { cta: name }); 57 } 58 59 function mailto(subject, body) { 60 return "mailto:" + CONFIG.EMAIL + 61 "?subject=" + encodeURIComponent(subject) + 62 "&body=" + encodeURIComponent(body); 63 } 64 65 // Footer enquiries work directly in the browser, including devices without a mail app. 66 document.querySelectorAll('[data-cta="foot-email"]').forEach(function (link) { 67 link.href = "/contact/#contactForm"; 68 }); 69 70 /* ---------- the forms behind each call to action -------------------- 71 One dialog serves them all. The fields change with the purpose, so 72 we never ask for something we do not need to answer the request. */ 73 var FORMS = { 74 assessment: { 75 title: "Get a free assessment", 76 note: "Read only access. Nothing installed. Free for your first environment.", 77 submit: "Request the assessment", 78 fields: [ 79 { n: "Name", l: "Your name", t: "text", req: true, ac: "name" }, 80 { n: "Email", l: "Work email", t: "email", req: true, ac: "email" }, 81 { n: "Company", l: "Company", t: "text", ac: "organization" }, 82 { n: "Environment", l: "Which environment", t: "select", 83 o: ["Azure", "AWS", "Microsoft 365", "A mix of them"] }, 84 { n: "Users", l: "Roughly how many users", t: "text" } 85 ] 86 }, 87 quote: { 88 title: "Ask for a quote", 89 note: "A quote takes one working day. We do not book you into a discovery call first.", 90 submit: "Ask for a quote", 91 fields: [ 92 { n: "Name", l: "Your name", t: "text", req: true, ac: "name" }, 93 { n: "Email", l: "Work email", t: "email", req: true, ac: "email" }, 94 { n: "Company", l: "Company", t: "text", ac: "organization" }, 95 { n: "Scope", l: "What you want priced", t: "textarea", 96 p: "One environment, a client base, or ongoing coverage. Rough size is enough." } 97 ] 98 }, 99 sample: { 100 title: "Send me a sample report", 101 note: "A real report with the client details removed.", 102 submit: "Send it to me", 103 fields: [ 104 { n: "Name", l: "Your name", t: "text", req: true, ac: "name" }, 105 { n: "Email", l: "Work email", t: "email", req: true, ac: "email" }, 106 { n: "Assessment", l: "Which assessment", t: "select", 107 o: ["Azure", "AWS", "Microsoft 365", "Not sure yet"] } 108 ] 109 }, 110 newsletter: { 111 title: "Get the newsletter", 112 note: "Every week or two. Unsubscribe from any issue.", 113 submit: "Subscribe free", 114 fields: [ 115 { n: "Email", l: "Work email", t: "email", req: true, ac: "email" }, 116 { n: "Name", l: "Your name", t: "text", ac: "name" } 117 ] 118 }, 119 index: { 120 title: "Cloud Maturity Index", 121 note: "We will email you when the first edition is published.", 122 submit: "Tell me when it lands", 123 fields: [ 124 { n: "Email", l: "Work email", t: "email", req: true, ac: "email" }, 125 { n: "Name", l: "Your name", t: "text", ac: "name" }, 126 { n: "Company", l: "Company", t: "text", ac: "organization" } 127 ] 128 } 129 }; 130 131 /* ---------- dialog --------------------------------------------------- */ 132 var dlg = null, lastFocus = null; 133 134 function buildField(f, i) { 135 var id = "df-" + i; 136 var input; 137 if (f.t === "textarea") 138 input = '<textarea id="' + id + '" name="' + f.n + '"' + 139 (f.p ? ' placeholder="' + f.p + '"' : "") + (f.req ? " required" : "") + "></textarea>"; 140 else if (f.t === "select") 141 input = '<select id="' + id + '" name="' + f.n + '">' + 142 f.o.map(function (o) { return "<option>" + o + "</option>"; }).join("") + "</select>"; 143 else 144 input = '<input id="' + id + '" name="' + f.n + '" type="' + f.t + '"' + 145 (f.ac ? ' autocomplete="' + f.ac + '"' : "") + (f.req ? " required" : "") + ">"; 146 return '<div class="field"><label for="' + id + '">' + f.l + "</label>" + input + "</div>"; 147 } 148 149 function openDialog(purpose, source) { 150 var spec = FORMS[purpose]; 151 if (!spec) return; 152 lastFocus = document.activeElement; 153 154 if (!dlg) { 155 dlg = document.createElement("div"); 156 dlg.className = "modal"; 157 dlg.setAttribute("role", "dialog"); 158 dlg.setAttribute("aria-modal", "true"); 159 document.body.appendChild(dlg); 160 } 161 162 dlg.innerHTML = 163 '<div class="modal-back" data-close></div>' + 164 '<div class="modal-card" role="document">' + 165 '<button class="modal-x" type="button" data-close aria-label="Close">' + 166 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" ' + 167 'stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>' + 168 '<h2 id="modalTitle">' + spec.title + "</h2>" + 169 '<form class="form mt4" novalidate>' + 170 '<input type="text" name="website" tabindex="-1" autocomplete="off" ' + 171 'aria-hidden="true" class="hp">' + 172 spec.fields.map(buildField).join("") + 173 '<button class="btn btn-1 btn-lg mt2" type="submit">' + spec.submit + "</button>" + 174 '<p class="form-note">' + spec.note + "</p>" +
175 '<p class="form-msg" role="status" aria-live="polite"></p>' + 176 "</form>" + 177 "</div>"; 178 179 dlg.setAttribute("aria-labelledby", "modalTitle"); 180 dlg.classList.add("open"); 181 document.body.classList.add("noscroll"); 182 document.querySelectorAll("body > header, body > main, body > footer").forEach(function (el) { el.inert = true; }); 183 var first = dlg.querySelector("input:not(.hp), select, textarea"); 184 if (first) first.focus(); 185 186 wireForm(dlg.querySelector("form"), purpose, source); 187 } 188 189 function closeDialog() { 190 if (!dlg) return; 191 dlg.classList.remove("open"); 192 document.body.classList.remove("noscroll"); 193 document.querySelectorAll("body > header, body > main, body > footer").forEach(function (el) { el.inert = false; }); 194 if (lastFocus && lastFocus.focus) lastFocus.focus(); 195 } 196 197 document.addEventListener("click", function (e) { 198 if (e.target.closest("[data-close]")) closeDialog(); 199 }); 200 document.addEventListener("keydown", function (e) { 201 if (e.key === "Escape") closeDialog(); 202 if (e.key === "Tab" && dlg && dlg.classList.contains("open")) { 203 var f = dlg.querySelectorAll("button, input:not(.hp), select, textarea, a[href]"); 204 if (!f.length) return; 205 var first = f[0], last = f[f.length - 1]; 206 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } 207 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } 208 } 209 }); 210 211 /* ---------- submitting ------------------------------------------------ */ 212 function wireForm(form, purpose, source) { 213 if (!form || form.dataset.wired) return; 214 form.dataset.wired = "1"; 215 if (CONFIG.FORM_ENDPOINT.indexOf("https://formsubmit.co/") === 0) { 216 form.action = CONFIG.FORM_ENDPOINT; 217 form.method = "POST"; 218 var serviceNote = document.createElement("p"); 219 serviceNote.className = "form-note"; 220 serviceNote.textContent = "Continue to the secure submission check to send your enquiry to [email protected]. FormSubmit processes the delivery."; 221 form.appendChild(serviceNote); 222 } 223 if (!CONFIG.FORM_ENDPOINT) { 224 var deliveryNote = document.createElement("p"); 225 deliveryNote.className = "form-note"; 226 deliveryNote.textContent = "This opens a prepared email. Send it from your email app to complete your request."; 227 form.appendChild(deliveryNote); 228 var sendButton = form.querySelector('button[type="submit"]'); 229 if (sendButton) sendButton.textContent = "Continue by email"; 230 } 231 232 form.addEventListener("submit", function (e) { 233 e.preventDefault(); 234 var msg = form.querySelector(".form-msg"); 235 if (!msg) { 236 msg = document.createElement("p"); 237 msg.className = "form-msg"; 238 msg.setAttribute("role", "status"); 239 msg.setAttribute("aria-live", "polite"); 240 form.appendChild(msg); 241 } 242 var btn = form.querySelector('button[type="submit"]'); 243 if (!form.reportValidity()) return; 244 var data = {}; 245 new FormData(form).forEach(function (v, k) { data[k] = v; }); 246 data.purpose = purpose; 247 data.source = source || ""; 248 data.email = data.Email || data.email || ""; 249 data.name = data.Name || data.name || ""; 250 251 if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(data.email)) { 252 if (msg) { msg.textContent = "Please enter a work email address."; msg.className = "form-msg bad"; } 253 return; 254 } 255 256 /* no backend configured, so compose an email. it still works. */ 257 if (!CONFIG.FORM_ENDPOINT) { 258 var lines = Object.keys(data) 259 .filter(function (k) { return k !== "website" && data[k]; }) 260 .map(function (k) { return k + ": " + data[k]; }); 261 var subject = FORMS[purpose] ? FORMS[purpose].title : "Website enquiry"; 262 var draftUrl = mailto(subject, lines.join("\n") + "\n"); 263 msg.textContent = "Your request has not been sent yet. Send the prepared email from your email app, or contact " + CONFIG.EMAIL + ". "; 264 var draftLink = document.createElement("a"); 265 draftLink.href = draftUrl; 266 draftLink.textContent = "Open email draft"; 267 msg.appendChild(draftLink); 268 window.location.href = draftUrl; 269 return; 270 } 271 272 if (CONFIG.FORM_ENDPOINT.indexOf("https://formsubmit.co/") === 0) { 273 if (data.website) return; 274 var deliveryFields = { 275 _subject: "Optimizer365: " + (FORMS[purpose] ? FORMS[purpose].title : "Website enquiry"), 276 _template: "table", _replyto: data.email, 277 purpose: purpose, source: source || "", email: data.email, 278 _url: window.location.href 279 };
280 Object.keys(deliveryFields).forEach(function (key) { 281 var field = form.querySelector('input[name="' + key + '"]'); 282 if (!field) { field = document.createElement("input"); field.type = "hidden"; field.name = key; form.appendChild(field); } 283 field.value = deliveryFields[key]; 284 }); 285 HTMLFormElement.prototype.submit.call(form); 286 return; 287 } 288 289 if (btn) { btn.disabled = true; btn.dataset.label = btn.textContent; btn.textContent = "Sending"; } 290 if (msg) { msg.textContent = ""; msg.className = "form-msg"; } 291 292 fetch(CONFIG.FORM_ENDPOINT, { 293 method: "POST", 294 headers: { "content-type": "application/json" }, 295 body: JSON.stringify(data) 296 }) 297 .then(function (r) { 298 if (!r.ok) throw new Error("Submission failed"); 299 return r.json(); 300 }) 301 .then(function (res) { 302 if (btn) { btn.disabled = false; btn.textContent = btn.dataset.label; } 303 if (res && res.ok) { 304 track(purpose + "-submitted"); 305 var done = document.createElement("div"); 306 done.className = "form-done"; 307 var heading = document.createElement("h3"); 308 heading.textContent = res.message || "Thanks. We have received your request."; 309 done.appendChild(heading); 310 form.replaceChildren(done); 311 } else if (msg) { 312 msg.textContent = (res && res.message) || 313 "That did not go through. Please email " + CONFIG.EMAIL + "."; 314 msg.className = "form-msg bad"; 315 } 316 }) 317 .catch(function () { 318 if (btn) { btn.disabled = false; btn.textContent = btn.dataset.label; } 319 if (msg) { 320 msg.textContent = "That did not go through. Please email " + CONFIG.EMAIL + "."; 321 msg.className = "form-msg bad"; 322 } 323 }); 324 }); 325 } 326 327 /* ---------- calls to action ------------------------------------------- */ 328 document.querySelectorAll("[data-action]").forEach(function (el) { 329 var action = el.getAttribute("data-action"); 330 if (action === "booking" || action === "newsletter" || action === "community-newsletter") { 331 if (CONFIG.BOOKING_URL) { 332 el.setAttribute("href", action === "newsletter" ? CONFIG.NEWSLETTER_URL : action === "community-newsletter" ? CONFIG.COMMUNITY_NEWSLETTER_URL : CONFIG.BOOKING_URL); 333 el.setAttribute("target", "_blank"); 334 el.setAttribute("rel", "noopener"); 335 el.addEventListener("click", function () { track(el.getAttribute("data-cta") || action); }); 336 } 337 return; 338 } 339 if (!FORMS[action]) return; 340 // Keep the HTML fallback destination if JavaScript is interrupted. 341 el.addEventListener("click", function (e) { 342 e.preventDefault(); 343 track(el.getAttribute("data-cta") || action); 344 openDialog(action, el.getAttribute("data-cta") || ""); 345 }); 346 }); 347 348 document.addEventListener("click", function (e) { 349 var el = e.target.closest("[data-cta]"); 350 if (el && !el.hasAttribute("data-action")) track(el.getAttribute("data-cta")); 351 }); 352 353 /* ---------- the contact page form ------------------------------------- */ 354 wireForm(document.getElementById("contactForm"), "contact", "contact-page"); 355 wireForm(document.getElementById("quoteForm"), "quote", "pricing-page-inline"); 356 357 /* ---------- event banner ---------------------------------------------- */ 358 var banner = document.getElementById("eventBanner"); 359 if (banner) { 360 if (!CONFIG.SHOW_EVENT_BANNER || !CONFIG.EVENT_NAME) { 361 banner.remove(); 362 } else { 363 var n = banner.querySelector("[data-event-name]"); 364 var d = banner.querySelector("[data-event-detail]"); 365 var l = banner.querySelector("[data-event-link]"); 366 if (n) n.textContent = CONFIG.EVENT_NAME; 367 if (d) d.textContent = CONFIG.EVENT_DETAIL; 368 if (l && CONFIG.EVENT_URL) l.setAttribute("href", CONFIG.EVENT_URL); 369 } 370 } 371 372 /* ---------- resources feed -------------------------------------------- 373 The embed is third party. It can be slow, blocked by an extension, or 374 simply empty. Write something useful in immediately and only swap it 375 for the embed once the embed has actually painted. */ 376 var feed = document.getElementById("linkedinFeed"); 377 if (feed) { 378 var fallback = 379 '<div class="feed-alt">' + 380 "<h4>Recent posts</h4>" + 381 "<p>We publish on cloud cost, licensing and security posture as we go. " + 382 "The full stream lives on LinkedIn.</p>" + 383 '<a class="btn btn-2" href="' + CONFIG.LINKEDIN + '" rel="noopener" ' + 384 'target="_blank" data-cta="feed-fallback">Read it on LinkedIn</a>' + 385 "</div>"; 386 feed.innerHTML = fallback; 387 388 if (CONFIG.ELFSIGHT_APP_ID) { 389 var consent = document.createElement("p"); 390 consent.className = "form-note"; 391 consent.textContent = "Loading the feed connects to Elfsight and may load LinkedIn content. Those services receive browser information and may use cookies."; 392 feed.appendChild(consent); 393 var load = document.createElement("button"); 394 load.type = "button"; load.className = "btn btn-2"; 395 load.textContent = "Load LinkedIn feed"; 396 feed.appendChild(load); 397 load.addEventListener("click", function () { 398 load.disabled = true; load.textContent = "Loading feed"; 399 var host = document.createElement("div"); 400 host.className = "elfsight-app-" + CONFIG.ELFSIGHT_APP_ID; 401 feed.appendChild(host); 402 var script = document.createElement("script"); 403 script.src = "https://elfsightcdn.com/platform.js"; script.async = true; 404 function failed() { load.textContent = "Feed unavailable. Use the LinkedIn link above."; } 405 script.onerror = failed; 406 script.onload = function () { setTimeout(function () { if (host.offsetHeight < 60) failed(); else load.remove(); }, 6000); }; 407 document.body.appendChild(script); 408 }, {once: true}); 409 } 410 } 411 412 /* ---------- legal line -------------------------------------------------- */ 413 document.querySelectorAll("[data-legal]").forEach(function (el) { 414 el.textContent = "© " + new Date().getFullYear() + " " + 415 CONFIG.LEGAL_ENTITY + ". All rights reserved."; 416 }); 417 418 /* ---------- collapsing header --------------------------------------------- 419 past the hero the bar detaches into a pill. class only; css does the rest. */ 420 var scrolled = false;
421 function onScroll() { 422 var now = window.scrollY > 320; 423 if (now !== scrolled) { 424 scrolled = now; 425 document.body.classList.toggle("scrolled", now); 426 } 427 } 428 window.addEventListener("scroll", onScroll, { passive: true }); 429 onScroll(); 430 431 /* ---------- mobile navigation -------------------------------------------- */ 432 var toggle = document.querySelector(".navtoggle"); 433 var links = document.querySelector(".navlinks"); 434 if (toggle && links) { 435 toggle.addEventListener("click", function () { 436 var open = links.classList.toggle("open"); 437 toggle.setAttribute("aria-expanded", open ? "true" : "false"); 438 toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu"); 439 }); 440 } 441 442 /* ---------- analytics, only after consent ------------------------------- 443 GA sets cookies, so it does not load until a visitor says yes. The 444 choice is remembered so the banner is asked once. */ 445 function loadGA() { 446 if (!CONFIG.GA_ID || window.gtag) return; 447 var s = document.createElement("script"); 448 s.async = true; 449 s.src = "https://www.googletagmanager.com/gtag/js?id=" + CONFIG.GA_ID; 450 document.head.appendChild(s); 451 window.dataLayer = window.dataLayer || []; 452 window.gtag = function () { window.dataLayer.push(arguments); }; 453 window.gtag("js", new Date()); 454 window.gtag("config", CONFIG.GA_ID, { anonymize_ip: true }); 455 } 456 457 var KEY = "o365-cookies"; 458 var choice = null; 459 try { choice = localStorage.getItem(KEY); } catch (e) {} 460 461 if (CONFIG.GA_ID) { 462 if (choice === "yes") { 463 loadGA(); 464 } else if (choice !== "no") { 465 var bar = document.createElement("div"); 466 bar.className = "cookiebar"; 467 bar.innerHTML = 468 "<p>We use Google Analytics to see which pages are read. It sets cookies. " + 469 'Nothing loads until you agree. <a href="/privacy/">What we collect</a>.</p>' + 470 '<div class="cookiebtns">' + 471 '<button class="btn btn-2 btn-sm" type="button" data-cookie="no">No thanks</button>' + 472 '<button class="btn btn-1 btn-sm" type="button" data-cookie="yes">Accept</button>' + 473 "</div>"; 474 document.body.appendChild(bar); 475 bar.addEventListener("click", function (e) { 476 var b = e.target.closest("[data-cookie]"); 477 if (!b) return; 478 var v = b.getAttribute("data-cookie"); 479 try { localStorage.setItem(KEY, v); } catch (err) {} 480 if (v === "yes") loadGA(); 481 bar.remove(); 482 }); 483 } 484 } 485})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.