(() => { // ── API Config ── const API_ENDPOINT = window.PROLOGY_CONFIG.API_URL; // ── Per-field error messages ── const FIELD_ERROR_MSG = { name: "Please enter a valid name.", email: "Please enter a valid email address.", phone: "Please enter a valid Australian mobile number (04xx xxx xxx).", message: "Please enter at least 10 characters.", email_or_phone_required: "Please provide an email or phone number.", }; // ── Client-side validators (mirror server rules) ── const RX_EMAIL = /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/; const RX_NAME = /^[\p{L}\p{M}'\-\s.]{2,80}$/u; function isValidAuMobile(phone) { const raw = (phone || "").trim(); if (!raw || !/^[\d\s\-()]+$/.test(raw)) return false; const digits = raw.replace(/\D/g, ""); return /^04\d{8}$/.test(digits); } function validateClient(fields) { const bad = []; if ("name" in fields && (!fields.name || !RX_NAME.test(fields.name.trim()))) bad.push("name"); if ("email" in fields && (!fields.email || !RX_EMAIL.test(fields.email.trim()))) bad.push("email"); if ("phone" in fields && !isValidAuMobile(fields.phone)) bad.push("phone"); if ("message" in fields && (!fields.message || fields.message.trim().length < 10)) bad.push("message"); return bad; } function clearFieldErrors(sr) { if (!sr) return; sr.querySelectorAll(".field-error").forEach((el) => el.remove()); sr.querySelectorAll(".invalid").forEach((el) => el.classList.remove("invalid")); } function showFieldErrors(sr, badKeys, fieldMap) { if (!sr || !fieldMap) return false; let shown = 0; badKeys.forEach((key) => { let names = fieldMap[key]; if (!names) return; if (!Array.isArray(names)) names = [names]; names.forEach((name) => { const input = sr.querySelector('[name="' + name + '"]'); if (!input) return; input.classList.add("invalid"); const field = input.closest(".field"); if (field && !field.querySelector(".field-error")) { const err = document.createElement("div"); err.className = "field-error"; err.textContent = FIELD_ERROR_MSG[key] || "Invalid value."; field.appendChild(err); shown++; } }); }); return shown > 0; } function bindClearOnInput(sr) { if (!sr || sr._clearBound) return; sr._clearBound = true; sr.querySelectorAll("input, textarea, select").forEach((el) => { el.addEventListener("input", () => { if (!el.classList.contains("invalid")) return; el.classList.remove("invalid"); const field = el.closest(".field"); const err = field && field.querySelector(".field-error"); if (err) err.remove(); }); }); } function showButtonError(btnEl, msg) { const origHTML = btnEl.dataset.origHtml || btnEl.innerHTML; btnEl.dataset.origHtml = origHTML; btnEl.innerHTML = "⚠ " + msg + ""; setTimeout(() => { btnEl.innerHTML = origHTML; btnEl.disabled = false; delete btnEl.dataset.origHtml; }, 3000); } // ── Shared submit function ── async function submitToAPI(payload, btnEl, onSuccess, opts) { const { sr, validateFields, fieldMap } = opts || {}; if (sr) { clearFieldErrors(sr); bindClearOnInput(sr); } // Client-side validation — avoid burning an API call on obvious errors if (validateFields) { const bad = validateClient(validateFields); if (bad.length) { const shown = showFieldErrors(sr, bad, fieldMap); if (!shown) showButtonError(btnEl, "Please check your inputs and try again."); return; } } btnEl.disabled = true; const origHTML = btnEl.innerHTML; btnEl.innerHTML = "Sending..."; const restore = () => { btnEl.innerHTML = origHTML; btnEl.disabled = false; }; try { const res = await fetch(API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", }, body: JSON.stringify({ ...payload, source: window.location.href, ts: Date.now(), nonce: Math.random().toString(36).slice(2), }), }); const data = await res.json(); if (res.ok && data.ok) { onSuccess(restore); } else { let msg = data.error || "Something went wrong. Please try again."; if (res.status === 429) { msg = "Too many requests. Please try again later."; } else if (/validation failed/i.test(msg)) { const shown = showFieldErrors(sr, data.fields || [], fieldMap); if (shown) { restore(); return; } msg = "Please check your inputs and try again."; } btnEl.innerHTML = "⚠ " + msg + ""; setTimeout(restore, 3000); } } catch (e) { btnEl.innerHTML = "⚠ Network error. Please try again."; setTimeout(restore, 3000); } } // ── Shared Shadow DOM CSS ── const MODAL_CSS = ` :host { display: none; } :host(.open) { display: block; } .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(12px); z-index: 9999; display: flex; align-items: flex-start; justify-content: center; padding: 20px; overflow-y: auto; } .box { background: linear-gradient(145deg, #2A3B54, #1E293B); border: 1px solid rgba(255,255,255,0.15); border-radius: 24px; width: 100%; max-width: 580px; max-height: calc(100vh - 40px); overflow-y: auto; padding: 36px; margin: auto; position: relative; box-shadow: 0 40px 100px rgba(0,0,0,0.8); font-family: 'Plus Jakarta Sans', sans-serif; color: white; box-sizing: border-box; } .badge { display: inline-block; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: #1A73E8; background: rgba(26,115,232,0.15); padding: 4px 12px; border-radius: 4px; margin-bottom: 10px; } h3 { font-size: 1.75rem; font-weight: 800; margin: 0 0 8px; color: white; } .intro { font-size: 0.95rem; color: #94A3B8; line-height: 1.6; margin-bottom: 28px; } .close { position: absolute; top: 20px; right: 20px; background: none; border: none; color: #94A3B8; font-size: 1.4rem; cursor: pointer; line-height: 1; padding: 4px 8px; border-radius: 6px; transition: color 0.2s; } .close:hover { color: white; } .field { margin-bottom: 20px; } .field-row { display: flex; gap: 16px; margin-bottom: 20px; } .field-row .field { flex: 1; margin-bottom: 0; } label { display: block; font-size: 0.85rem; font-weight: 600; color: #E2E8F0; margin-bottom: 8px; } .req { color: #f87171; font-size: 0.8em; margin-left: 2px; font-weight: 700; } input, textarea, select { width: 100%; box-sizing: border-box; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; padding: 12px 16px; color: white; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 0.95rem; outline: none; transition: border-color 0.2s; -webkit-appearance: none; appearance: none; } input:focus, textarea:focus, select:focus { border-color: #1A73E8; background: rgba(255,255,255,0.09); } input.invalid, textarea.invalid, select.invalid { border-color: #f87171; background: rgba(248,113,113,0.06); } .field-error { color: #f87171; font-size: 0.78rem; margin-top: 6px; line-height: 1.35; } input::placeholder, textarea::placeholder { color: #64748B; } textarea { min-height: 120px; resize: vertical; } select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2394A3B8' d='M1 1l5 5 5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; cursor: pointer; } select option { background: #1E293B; color: white; } .btn-submit { width: 100%; padding: 16px; background: #1A73E8; color: white; border: none; border-radius: 14px; font-size: 1rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; transition: background 0.2s, transform 0.2s; font-family: 'Plus Jakarta Sans', sans-serif; margin-top: 8px; } .btn-submit:hover { background: #174ea6; transform: translateY(-2px); } .btn-submit svg { flex-shrink: 0; } .btn-submit:disabled { opacity: 0.7; cursor: not-allowed; transform: none !important; } `; const ARROW_SVG = ``; // ── Base class ── class ProloModal extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); } open(prefillEmail = "") { this.classList.add("open"); document.body.style.overflow = "hidden"; if (prefillEmail) { const emailInput = this.shadowRoot.querySelector( 'input[type="email"]', ); if (emailInput) emailInput.value = prefillEmail; } requestAnimationFrame(() => { const first = this.shadowRoot.querySelector("input, textarea"); if (first) first.focus(); }); } close() { this.classList.remove("open"); document.body.style.overflow = ""; const form = this.shadowRoot.querySelector("form"); if (form) form.reset(); } _bindClose() { this.shadowRoot .querySelector(".close") .addEventListener("click", () => this.close()); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && this.classList.contains("open")) this.close(); }); } } // ── 1. Sell / Valuation Modal ── class PrologySellModal extends ProloModal { connectedCallback() { this.shadowRoot.innerHTML = `
Asset Recovery

Get a Valuation

Tell us what hardware you have. Our team will provide a competitive buy-back offer within 4 business hours.

`; this._bindClose(); this.shadowRoot .querySelector("form") .addEventListener("submit", (e) => { e.preventDefault(); const sr = this.shadowRoot; const email = sr.querySelector('[name="email"]').value; if (!email) return; const inventory = sr.querySelector('[name="inventory"]').value; const condition = sr.querySelector('[name="condition"]').value; const btn = sr.querySelector(".btn-submit"); submitToAPI( { email, message: `[Asset Recovery]\nCondition: ${condition}\n\n${inventory || "(no inventory listed)"}`, channel: "sell", }, btn, (restore) => { btn.innerHTML = "✓ Request Sent!"; setTimeout(() => { this.close(); restore(); }, 1800); }, { sr, validateFields: { email, message: inventory }, fieldMap: { email: "email", message: "inventory" }, }, ); }); } } // ── 2. Source / Quote Modal ── class PrologySourceModal extends ProloModal { connectedCallback() { this.shadowRoot.innerHTML = `
Hardware Sourcing

Request a Quote

Looking for specific equipment? We source authorized and refurbished Cisco, HP, and more.

`; this._bindClose(); this.shadowRoot .querySelector("form") .addEventListener("submit", (e) => { e.preventDefault(); const sr = this.shadowRoot; const email = sr.querySelector('[name="email"]').value; if (!email) return; const parts = sr.querySelector('[name="parts"]').value; const timing = sr.querySelector('[name="timing"]').value; const btn = sr.querySelector(".btn-submit"); submitToAPI( { email, message: `[Hardware Sourcing]\nTiming: ${timing}\n\n${parts || "(no parts listed)"}`, channel: "source", }, btn, (restore) => { btn.innerHTML = "✓ Request Sent!"; setTimeout(() => { this.close(); restore(); }, 1800); }, { sr, validateFields: { email }, fieldMap: { email: "email", message: "parts" }, }, ); }); } } // ── 3. Quote Modal (Request a Quote from Who We Serve) ── class PrologyQuoteModal extends ProloModal { connectedCallback() { this.shadowRoot.innerHTML = `
IT Procurement

Request a Quote

Please provide details about your project or hardware requirements. Our team will verify stock and get back to you within 24 hours.

`; this._bindClose(); this.shadowRoot .querySelector("form") .addEventListener("submit", (e) => { e.preventDefault(); const sr = this.shadowRoot; const name = sr.querySelector('[name="name"]').value; const email = sr.querySelector('[name="email"]').value; const phone = sr.querySelector('[name="phone"]').value; const company = sr.querySelector('[name="company"]').value; const requirements = sr.querySelector( '[name="requirements"]', ).value; const destination = sr.querySelector( '[name="destination"]', ).value; const btn = sr.querySelector(".btn-submit"); submitToAPI( { name, email, phone, message: `[Quote Request]\nCompany: ${company || "N/A"}\nDestination: ${destination || "N/A"}\n\n${requirements}`, channel: "quote", }, btn, (restore) => { btn.innerHTML = "✓ Request Sent!"; setTimeout(() => { this.close(); restore(); }, 1800); }, { sr, validateFields: { name, email, phone, message: requirements }, fieldMap: { name: "name", email: "email", phone: "phone", message: "requirements", email_or_phone_required: ["email", "phone"], }, }, ); }); } } // ── 4. Enquire Modal (Gov & Corp) ── class PrologyEnquireModal extends ProloModal { connectedCallback() { this.shadowRoot.innerHTML = `
Government & Corporate

Corporate Enquiry

Get in touch with our specialized team to establish customized procurement workflows for your organization.

`; this._bindClose(); this.shadowRoot .querySelector("form") .addEventListener("submit", (e) => { e.preventDefault(); const sr = this.shadowRoot; const name = sr.querySelector('[name="name"]').value; const email = sr.querySelector('[name="email"]').value; const phone = sr.querySelector('[name="phone"]').value; const org = sr.querySelector('[name="org"]').value; const details = sr.querySelector('[name="details"]').value; const btn = sr.querySelector(".btn-submit"); submitToAPI( { name, email, phone, message: `[Corporate Enquiry]\nOrganization: ${org}\n\n${details}`, channel: "enquire", }, btn, (restore) => { btn.innerHTML = "✓ Enquiry Sent!"; setTimeout(() => { this.close(); restore(); }, 1800); }, { sr, validateFields: { name, email, phone, message: details }, fieldMap: { name: "name", email: "email", phone: "phone", message: "details", email_or_phone_required: ["email", "phone"], }, }, ); }); } } customElements.define("prology-sell-modal", PrologySellModal); customElements.define("prology-source-modal", PrologySourceModal); customElements.define("prology-quote-modal", PrologyQuoteModal); customElements.define("prology-enquire-modal", PrologyEnquireModal); // ── Global API (backward compatible with onclick calls) ── window.openValuationModal = (email = "") => document.getElementById("wc-sell-modal").open(email); window.openSourcingModal = () => document.getElementById("wc-source-modal").open(); window.openQuoteModal = () => document.getElementById("wc-quote-modal").open(); window.openEnquireModal = () => document.getElementById("wc-enquire-modal").open(); // ── Store picker modal ── const cfg = window.PROLOGY_CONFIG || {}; // Builds a store URL, optionally appending a category slug. // e.g. ("https://prology.net/au", "compute") → "https://prology.net/au/compute" const buildStoreUrl = (base, slug) => { if (!base) return ""; const trimmed = base.replace(/\/+$/, ""); return slug ? `${trimmed}/${slug}` : base; }; const applyStoreHrefs = (slug) => { document.querySelectorAll('[data-store="au"]').forEach((el) => { el.setAttribute("href", buildStoreUrl(cfg.STORE_AU_URL, slug)); }); document.querySelectorAll('[data-store="us"]').forEach((el) => { el.setAttribute("href", buildStoreUrl(cfg.STORE_US_URL, slug)); }); }; window.openStoreModal = function (categorySlug) { applyStoreHrefs(categorySlug || ""); const overlay = document.getElementById("store-modal-overlay"); if (overlay) overlay.classList.add("open"); }; window.closeStoreModal = function () { const overlay = document.getElementById("store-modal-overlay"); if (overlay) overlay.classList.remove("open"); }; document .getElementById("store-modal-overlay") .addEventListener("click", function (e) { if (e.target === this) closeStoreModal(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeStoreModal(); }); // Initial wire-up (no category) applyStoreHrefs(""); // Intercept any /shop link → open store picker instead document.addEventListener("click", function (e) { const a = e.target.closest('a[href="/shop"]'); if (!a) return; e.preventDefault(); window.openStoreModal(); }); window.goToSellStep2 = function () { const emailInput = document.getElementById("sell-email-init"); const email = emailInput ? emailInput.value : ""; if (!email || !email.includes("@")) { alert("Please input a valid email address."); return; } window.openValuationModal(email); }; })();