enable us option
This commit is contained in:
parent
b7aa76ed45
commit
087eec0e29
|
|
@ -0,0 +1,565 @@
|
||||||
|
/**
|
||||||
|
* ==============================================================================
|
||||||
|
* SUPPORT WIDGET — Web Component (Shadow DOM)
|
||||||
|
* ==============================================================================
|
||||||
|
* Shadow DOM cô lập CSS hoàn toàn, tránh conflict/flicker với host page.
|
||||||
|
* 3 tabs: SMS (AU Only) / Email / WhatsApp
|
||||||
|
* ==============================================================================
|
||||||
|
*/
|
||||||
|
(function (w, d) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
if (w.__MSW_LOADED__) return;
|
||||||
|
w.__MSW_LOADED__ = true;
|
||||||
|
|
||||||
|
var P = w.PROLOGY_CONFIG || {};
|
||||||
|
var CFG = {
|
||||||
|
apiEndpoint:
|
||||||
|
P.CHAT_API_URL ||
|
||||||
|
P.API_URL ||
|
||||||
|
"https://prologyms.nswteam.net/chat-plugin/api/support",
|
||||||
|
whatsappNumber: P.CHAT_WHATSAPP_NUMBER || "84901234567",
|
||||||
|
accentColor: P.CHAT_ACCENT_COLOR || "#4f46e5",
|
||||||
|
position: P.CHAT_POSITION || "right",
|
||||||
|
rateLimit: {
|
||||||
|
max: P.CHAT_RATE_LIMIT_MAX != null ? P.CHAT_RATE_LIMIT_MAX : 3,
|
||||||
|
windowMin:
|
||||||
|
P.CHAT_RATE_LIMIT_WINDOW_MIN != null ? P.CHAT_RATE_LIMIT_WINDOW_MIN : 10,
|
||||||
|
},
|
||||||
|
minFillMs: P.CHAT_MIN_FILL_MS != null ? P.CHAT_MIN_FILL_MS : 3000,
|
||||||
|
fabImageUrl:
|
||||||
|
P.CHAT_FAB_IMAGE_URL ||
|
||||||
|
"https://prology.nswteam.net/media/wysiwyg/image_2026-04-02_15-43-14.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Utilities ── */
|
||||||
|
function sanitize(str, maxLen) {
|
||||||
|
if (typeof str !== "string") return "";
|
||||||
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/\//g, "/")
|
||||||
|
.trim()
|
||||||
|
.slice(0, maxLen || 2000);
|
||||||
|
}
|
||||||
|
function nonce() {
|
||||||
|
var a = new Uint8Array(16);
|
||||||
|
(w.crypto || w.msCrypto).getRandomValues(a);
|
||||||
|
return Array.from(a, function (b) {
|
||||||
|
return ("0" + b.toString(16)).slice(-2);
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
var RL_KEY = "_msw_rl";
|
||||||
|
function getRl() {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
JSON.parse(sessionStorage.getItem(RL_KEY)) || { n: 0, t: Date.now() }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return { n: 0, t: Date.now() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setRl(o) {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(RL_KEY, JSON.stringify(o));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
function isLimited() {
|
||||||
|
var cfg = CFG.rateLimit,
|
||||||
|
rl = getRl(),
|
||||||
|
win = cfg.windowMin * 60 * 1000;
|
||||||
|
if (Date.now() - rl.t > win) {
|
||||||
|
setRl({ n: 0, t: Date.now() });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return rl.n >= cfg.max;
|
||||||
|
}
|
||||||
|
function limitCooldown() {
|
||||||
|
var rl = getRl(),
|
||||||
|
win = CFG.rateLimit.windowMin * 60 * 1000;
|
||||||
|
return Math.max(0, Math.ceil((rl.t + win - Date.now()) / 1000));
|
||||||
|
}
|
||||||
|
function bumpRl() {
|
||||||
|
var rl = getRl();
|
||||||
|
rl.n++;
|
||||||
|
setRl(rl);
|
||||||
|
}
|
||||||
|
function csrfToken() {
|
||||||
|
var el = d.querySelector('input[name="form_key"]');
|
||||||
|
return el ? el.value : "";
|
||||||
|
}
|
||||||
|
function validateSMS(f) {
|
||||||
|
var e = [];
|
||||||
|
var name = (f.name || "").trim();
|
||||||
|
var phone = (f.phone || "").trim();
|
||||||
|
var message = (f.message || "").trim();
|
||||||
|
if (!name || !/^[\p{L}\p{M}'\-\s]{2,80}$/u.test(name)) e.push("sms-name");
|
||||||
|
if (
|
||||||
|
!phone ||
|
||||||
|
!/^[\d\s\+\-\(\)]{7,15}$/.test(phone) ||
|
||||||
|
!/\d{5,}/.test(phone)
|
||||||
|
)
|
||||||
|
e.push("sms-phone");
|
||||||
|
if (!message || message.length < 10) e.push("sms-msg");
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
function validateEmail(f) {
|
||||||
|
var e = [];
|
||||||
|
var name = (f.name || "").trim();
|
||||||
|
var email = (f.email || "").trim();
|
||||||
|
var message = (f.message || "").trim();
|
||||||
|
if (!name || !/^[\p{L}\p{M}'\-\s]{2,80}$/u.test(name)) e.push("email-name");
|
||||||
|
if (!email || !/^\w+([.\-]\w+)*@([\w\-]+\.)+[a-zA-Z]{2,12}$/.test(email))
|
||||||
|
e.push("email-email");
|
||||||
|
if (!message || message.length < 10) e.push("email-msg");
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── SVG assets ── */
|
||||||
|
var SVG_ARROW =
|
||||||
|
'<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M2 21L23 12 2 3v7l15 2-15 2z"/></svg>';
|
||||||
|
var WA_PATH =
|
||||||
|
"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347zM11.979 0C5.37 0 0 5.373 0 11.979c0 2.11.553 4.094 1.518 5.818L.057 24l6.349-1.663A11.938 11.938 0 0011.979 24C18.588 24 24 18.626 24 11.979 24 5.373 18.588 0 11.979 0zm0 21.818a9.839 9.839 0 01-5.012-1.369l-.36-.214-3.73.978.995-3.636-.235-.374a9.806 9.806 0 01-1.506-5.224c0-5.42 4.413-9.833 9.848-9.833 5.437 0 9.851 4.413 9.851 9.833 0 5.421-4.414 9.839-9.851 9.839z";
|
||||||
|
function waSVG(size) {
|
||||||
|
return (
|
||||||
|
'<svg viewBox="0 0 24 24" width="' +
|
||||||
|
size +
|
||||||
|
'" height="' +
|
||||||
|
size +
|
||||||
|
'" fill="currentColor"><path d="' +
|
||||||
|
WA_PATH +
|
||||||
|
'"/></svg>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var SVG_CHECK =
|
||||||
|
'<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
|
||||||
|
|
||||||
|
/* ── Shadow CSS ── */
|
||||||
|
var ac = CFG.accentColor;
|
||||||
|
var pos = CFG.position === "left" ? "left:24px;" : "right:24px;";
|
||||||
|
|
||||||
|
var SHADOW_CSS = [
|
||||||
|
':host{all:initial;display:block;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}',
|
||||||
|
/* FAB — no transition on background/box-shadow to kill flicker; only transform transitions */
|
||||||
|
"#fab{position:fixed;bottom:24px;" + pos + "z-index:99998;",
|
||||||
|
"width:60px;height:60px;border-radius:50%;",
|
||||||
|
"background-image:url(" + CFG.fabImageUrl + ");",
|
||||||
|
"background-size:cover;background-position:center;background-repeat:no-repeat;",
|
||||||
|
"border:none;cursor:pointer;outline:none;",
|
||||||
|
"box-shadow:0 6px 20px rgba(79,70,229,.4);",
|
||||||
|
"transform:translateZ(0);transition:transform .22s cubic-bezier(.34,1.56,.64,1);}",
|
||||||
|
"#fab:hover{transform:scale(1.08) translateY(-3px) translateZ(0);}",
|
||||||
|
"#badge{position:absolute;top:-4px;right:-4px;width:20px;height:20px;border-radius:50%;",
|
||||||
|
"background:#ef4444;font-size:11px;font-weight:700;color:#fff;",
|
||||||
|
"display:none;align-items:center;justify-content:center;border:2px solid #1a1f2e;}",
|
||||||
|
/* Panel */
|
||||||
|
"#panel{position:fixed;bottom:92px;" + pos + "z-index:99999;width:360px;",
|
||||||
|
"background:linear-gradient(145deg,rgb(42,59,84),rgb(30,41,59));border-radius:18px;overflow:hidden;",
|
||||||
|
"box-shadow:0 20px 60px rgba(0,0,0,.55),0 4px 16px rgba(0,0,0,.3);",
|
||||||
|
"transform:scale(.9) translateY(16px) translateZ(0);opacity:0;pointer-events:none;",
|
||||||
|
"transition:transform .28s cubic-bezier(.34,1.56,.64,1),opacity .2s ease;",
|
||||||
|
"will-change:transform,opacity;}",
|
||||||
|
"#panel.open{transform:scale(1) translateY(0) translateZ(0);opacity:1;pointer-events:auto;}",
|
||||||
|
/* Tabs */
|
||||||
|
"#tabs{display:flex;gap:3px;background:#242938;margin:14px 14px 0;border-radius:11px;padding:4px;}",
|
||||||
|
".tab{flex:1;padding:8px 4px;font-size:11.5px;font-weight:700;text-align:center;line-height:1.35;",
|
||||||
|
"color:#8b92a8;background:transparent;border:none;cursor:pointer;border-radius:8px;",
|
||||||
|
"transition:background .18s,color .18s;font-family:inherit;letter-spacing:.3px;}",
|
||||||
|
".tab:hover{color:#e0e2ea;}",
|
||||||
|
".tab.active{background:" + ac + ";color:#fff;}",
|
||||||
|
/* Panes */
|
||||||
|
".pane{display:none;padding:14px 14px 18px;}",
|
||||||
|
".pane.active{display:block;}",
|
||||||
|
/* Fields */
|
||||||
|
".field{margin-bottom:11px;}",
|
||||||
|
".field input,.field textarea{width:100%;padding:11px 13px;font-size:14px;font-family:inherit;",
|
||||||
|
"background:#252b3b;color:#dde0eb;border:1px solid #333b52;border-radius:9px;outline:none;",
|
||||||
|
"transition:border-color .18s,box-shadow .18s;box-sizing:border-box;-webkit-appearance:none;}",
|
||||||
|
".field input::placeholder,.field textarea::placeholder{color:#5d6478;}",
|
||||||
|
".field input:focus,.field textarea:focus{border-color:" +
|
||||||
|
ac +
|
||||||
|
";box-shadow:0 0 0 3px rgba(79,70,229,.22);}",
|
||||||
|
".field input.err,.field textarea.err{border-color:#ef4444;}",
|
||||||
|
".field textarea{resize:vertical;min-height:88px;}",
|
||||||
|
".hint{font-size:11.5px;color:#f87171;margin-top:4px;display:none;}",
|
||||||
|
".hint.show{display:block;}",
|
||||||
|
".hp{position:absolute;left:-9999px;opacity:0;height:0;width:0;overflow:hidden;pointer-events:none;}",
|
||||||
|
/* Submit */
|
||||||
|
".btn-submit{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:12px;",
|
||||||
|
"background:" + ac + ";color:#fff;border:none;border-radius:9px;",
|
||||||
|
"font-size:14.5px;font-weight:600;font-family:inherit;cursor:pointer;margin-top:6px;",
|
||||||
|
"transition:background .18s;}",
|
||||||
|
".btn-submit:hover{background:#4338ca;}",
|
||||||
|
".btn-submit:active{transform:scale(.98);}",
|
||||||
|
".btn-submit:disabled{background:#4a4f61;cursor:not-allowed;}",
|
||||||
|
/* Status */
|
||||||
|
".status{font-size:13px;font-weight:500;margin-top:9px;text-align:center;",
|
||||||
|
"border-radius:7px;padding:8px 12px;display:none;}",
|
||||||
|
".status.ok{display:block;background:#0a2e1a;color:#34d399;}",
|
||||||
|
".status.fail{display:block;background:#2e0a0a;color:#f87171;}",
|
||||||
|
".status.warn{display:block;background:#2c1f00;color:#fbbf24;}",
|
||||||
|
/* WhatsApp pane */
|
||||||
|
".wa-wrap{text-align:center;padding:4px 0 2px;}",
|
||||||
|
".wa-icon-wrap{width:54px;height:54px;border-radius:50%;background:#1a2e20;",
|
||||||
|
"display:flex;align-items:center;justify-content:center;margin:0 auto 14px;color:#25d366;}",
|
||||||
|
".wa-desc{font-size:13.5px;color:#8b92a8;line-height:1.6;margin:0 0 18px;}",
|
||||||
|
".btn-wa{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;padding:13px;",
|
||||||
|
"background:#25d366;color:#fff;border:none;border-radius:9px;",
|
||||||
|
"font-size:14.5px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .18s;}",
|
||||||
|
".btn-wa:hover{background:#1db954;}",
|
||||||
|
/* Success */
|
||||||
|
"#success{display:none;flex-direction:column;align-items:center;text-align:center;padding:36px 20px;}",
|
||||||
|
".success-icon{width:58px;height:58px;border-radius:50%;background:#0a2e1a;",
|
||||||
|
"display:flex;align-items:center;justify-content:center;color:#34d399;margin-bottom:15px;}",
|
||||||
|
"#success h3{font-size:18px;font-weight:700;color:#dde0eb;margin:0 0 8px;}",
|
||||||
|
"#success p{font-size:13.5px;color:#8b92a8;margin:0 0 22px;line-height:1.55;}",
|
||||||
|
".btn-new{padding:10px 24px;font-size:13.5px;font-weight:600;color:" +
|
||||||
|
ac +
|
||||||
|
";",
|
||||||
|
"background:rgba(79,70,229,.13);border:none;border-radius:8px;cursor:pointer;",
|
||||||
|
"transition:background .18s;font-family:inherit;}",
|
||||||
|
".btn-new:hover{background:rgba(79,70,229,.22);}",
|
||||||
|
/* Responsive */
|
||||||
|
"@media(max-width:420px){#panel{width:calc(100vw - 28px);" +
|
||||||
|
(CFG.position === "left" ? "left:14px;" : "right:14px;") +
|
||||||
|
"}",
|
||||||
|
"#fab{bottom:20px;}#panel{bottom:88px;}}",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
/* ── Inner HTML ── */
|
||||||
|
var INNER_HTML = [
|
||||||
|
'<button id="fab" aria-label="Open support" aria-expanded="false"><span id="badge">1</span></button>',
|
||||||
|
'<div id="panel" role="dialog" aria-modal="true" aria-label="Contact Support">',
|
||||||
|
'<div id="tabs">',
|
||||||
|
'<button class="tab active" data-tab="sms">SMS (AU<br>ONLY)</button>',
|
||||||
|
'<button class="tab" data-tab="email">EMAIL</button>',
|
||||||
|
'<button class="tab" data-tab="whatsapp">WHATSAPP</button>',
|
||||||
|
"</div>",
|
||||||
|
/* SMS */
|
||||||
|
'<div class="pane active" id="pane-sms">',
|
||||||
|
'<div class="field"><input id="sms-name" type="text" maxlength="80" autocomplete="name" placeholder="Your Name"><div class="hint" id="h-sms-name">Please enter your full name</div></div>',
|
||||||
|
'<div class="field"><input id="sms-phone" type="tel" maxlength="15" autocomplete="tel" placeholder="Mobile Number (Australia)"><div class="hint" id="h-sms-phone">Enter a valid AU mobile number</div></div>',
|
||||||
|
'<div class="field"><textarea id="sms-msg" maxlength="1000" placeholder="How can we help?"></textarea><div class="hint" id="h-sms-msg">Please provide more details (min 10 chars)</div></div>',
|
||||||
|
'<div class="hp"><input type="text" id="hp-sms" tabindex="-1" autocomplete="off"></div>',
|
||||||
|
'<button class="btn-submit" id="btn-sms">Send SMS ' +
|
||||||
|
SVG_ARROW +
|
||||||
|
"</button>",
|
||||||
|
'<div class="status" id="st-sms"></div>',
|
||||||
|
"</div>",
|
||||||
|
/* Email */
|
||||||
|
'<div class="pane" id="pane-email">',
|
||||||
|
'<div class="field"><input id="email-name" type="text" maxlength="80" autocomplete="name" placeholder="Your Name"><div class="hint" id="h-email-name">Please enter your full name</div></div>',
|
||||||
|
'<div class="field"><input id="email-email" type="email" maxlength="120" autocomplete="email" placeholder="Your Email Address"><div class="hint" id="h-email-email">Please enter a valid email address</div></div>',
|
||||||
|
'<div class="field"><textarea id="email-msg" maxlength="1000" placeholder="How can we help?"></textarea><div class="hint" id="h-email-msg">Please provide more details (min 10 chars)</div></div>',
|
||||||
|
'<div class="hp"><input type="text" id="hp-email" tabindex="-1" autocomplete="off"></div>',
|
||||||
|
'<button class="btn-submit" id="btn-email">Send Email ' +
|
||||||
|
SVG_ARROW +
|
||||||
|
"</button>",
|
||||||
|
'<div class="status" id="st-email"></div>',
|
||||||
|
"</div>",
|
||||||
|
/* WhatsApp */
|
||||||
|
'<div class="pane" id="pane-whatsapp">',
|
||||||
|
'<div class="wa-wrap">',
|
||||||
|
'<div class="wa-icon-wrap">' + waSVG(26) + "</div>",
|
||||||
|
'<p class="wa-desc">Chat with us on WhatsApp for support,<br>inquiries, and immediate expert advice.</p>',
|
||||||
|
'<button class="btn-wa" id="btn-wa">' +
|
||||||
|
waSVG(20) +
|
||||||
|
" Chat Now on WhatsApp</button>",
|
||||||
|
"</div>",
|
||||||
|
"</div>",
|
||||||
|
/* Success */
|
||||||
|
'<div id="success">',
|
||||||
|
'<div class="success-icon">' + SVG_CHECK + "</div>",
|
||||||
|
"<h3>Message Received!</h3>",
|
||||||
|
"<p>Thanks for reaching out.<br>We'll get back to you shortly.</p>",
|
||||||
|
'<button class="btn-new" id="btn-new">Send another message</button>',
|
||||||
|
"</div>",
|
||||||
|
"</div>",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
/* ── Web Component ── */
|
||||||
|
function SupportWidget() {
|
||||||
|
return Reflect.construct(HTMLElement, [], SupportWidget);
|
||||||
|
}
|
||||||
|
Object.setPrototypeOf(SupportWidget.prototype, HTMLElement.prototype);
|
||||||
|
Object.setPrototypeOf(SupportWidget, HTMLElement);
|
||||||
|
|
||||||
|
SupportWidget.prototype.connectedCallback = function () {
|
||||||
|
var shadow = this.attachShadow({ mode: "open" });
|
||||||
|
var style = d.createElement("style");
|
||||||
|
style.textContent = SHADOW_CSS;
|
||||||
|
shadow.appendChild(style);
|
||||||
|
var root = d.createElement("div");
|
||||||
|
root.innerHTML = INNER_HTML;
|
||||||
|
shadow.appendChild(root);
|
||||||
|
this._boot(shadow);
|
||||||
|
};
|
||||||
|
|
||||||
|
SupportWidget.prototype._boot = function (s) {
|
||||||
|
var isOpen = false;
|
||||||
|
var formStartTs = Date.now();
|
||||||
|
var $fab = s.getElementById("fab");
|
||||||
|
var $panel = s.getElementById("panel");
|
||||||
|
var $badge = s.getElementById("badge");
|
||||||
|
var $success = s.getElementById("success");
|
||||||
|
var $tabs = s.getElementById("tabs");
|
||||||
|
|
||||||
|
function openPanel() {
|
||||||
|
isOpen = true;
|
||||||
|
$panel.classList.add("open");
|
||||||
|
$fab.setAttribute("aria-expanded", "true");
|
||||||
|
$badge.style.display = "none";
|
||||||
|
formStartTs = Date.now();
|
||||||
|
}
|
||||||
|
function closePanel() {
|
||||||
|
isOpen = false;
|
||||||
|
$panel.classList.remove("open");
|
||||||
|
$fab.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
$fab.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
isOpen ? closePanel() : openPanel();
|
||||||
|
});
|
||||||
|
d.addEventListener("click", function (e) {
|
||||||
|
if (!isOpen) return;
|
||||||
|
var path = e.composedPath ? e.composedPath() : [];
|
||||||
|
if (
|
||||||
|
!path.some(function (n) {
|
||||||
|
return n === $panel || n === $fab;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
closePanel();
|
||||||
|
});
|
||||||
|
d.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape" && isOpen) closePanel();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
s.querySelectorAll(".tab").forEach(function (tab) {
|
||||||
|
tab.addEventListener("click", function () {
|
||||||
|
s.querySelectorAll(".tab").forEach(function (el) {
|
||||||
|
el.classList.toggle("active", el === tab);
|
||||||
|
});
|
||||||
|
s.querySelectorAll(".pane").forEach(function (el) {
|
||||||
|
el.classList.remove("active");
|
||||||
|
});
|
||||||
|
var pane = s.getElementById("pane-" + tab.dataset.tab);
|
||||||
|
if (pane) pane.classList.add("active");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* WhatsApp */
|
||||||
|
s.getElementById("btn-wa").addEventListener("click", function () {
|
||||||
|
w.open(
|
||||||
|
"https://wa.me/" + CFG.whatsappNumber,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Helpers */
|
||||||
|
function clearErr(prefix, fields) {
|
||||||
|
fields.forEach(function (f) {
|
||||||
|
var inp = s.getElementById(prefix + "-" + f),
|
||||||
|
hint = s.getElementById("h-" + prefix + "-" + f);
|
||||||
|
if (inp) inp.classList.remove("err");
|
||||||
|
if (hint) hint.classList.remove("show");
|
||||||
|
});
|
||||||
|
var st = s.getElementById("st-" + prefix);
|
||||||
|
if (st) {
|
||||||
|
st.className = "status";
|
||||||
|
st.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function markErr(ids) {
|
||||||
|
ids.forEach(function (id) {
|
||||||
|
var inp = s.getElementById(id),
|
||||||
|
hint = s.getElementById("h-" + id);
|
||||||
|
if (inp) inp.classList.add("err");
|
||||||
|
if (hint) hint.classList.add("show");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function setStatus(prefix, msg, type) {
|
||||||
|
var st = s.getElementById("st-" + prefix);
|
||||||
|
if (!st) return;
|
||||||
|
st.textContent = msg;
|
||||||
|
st.className = "status " + type;
|
||||||
|
}
|
||||||
|
function showSuccess() {
|
||||||
|
s.querySelectorAll(".pane").forEach(function (el) {
|
||||||
|
el.classList.remove("active");
|
||||||
|
});
|
||||||
|
$tabs.style.display = "none";
|
||||||
|
$success.style.display = "flex";
|
||||||
|
}
|
||||||
|
function doSubmit(prefix, payload, $btn, label) {
|
||||||
|
if (isLimited()) {
|
||||||
|
setStatus(
|
||||||
|
prefix,
|
||||||
|
"Too many requests. Try again in " + limitCooldown() + "s.",
|
||||||
|
"warn",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$btn.disabled = true;
|
||||||
|
/* Preserve the SVG node — only update the leading text node */
|
||||||
|
var tn = $btn.firstChild;
|
||||||
|
if (tn && tn.nodeType === 3) tn.textContent = "Sending... ";
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", CFG.apiEndpoint, true);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||||
|
xhr.setRequestHeader("X-Form-Key", csrfToken());
|
||||||
|
xhr.withCredentials = false;
|
||||||
|
xhr.timeout = 12000;
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState !== 4) return;
|
||||||
|
$btn.disabled = false;
|
||||||
|
if (tn && tn.nodeType === 3) tn.textContent = label + " ";
|
||||||
|
if (xhr.status === 200 || xhr.status === 201) {
|
||||||
|
bumpRl();
|
||||||
|
showSuccess();
|
||||||
|
} else if (xhr.status === 429)
|
||||||
|
setStatus(prefix, "Server busy. Please try again later.", "warn");
|
||||||
|
else setStatus(prefix, "Failed to send. Please try again.", "fail");
|
||||||
|
};
|
||||||
|
xhr.ontimeout = function () {
|
||||||
|
$btn.disabled = false;
|
||||||
|
if (tn && tn.nodeType === 3) tn.textContent = label + " ";
|
||||||
|
setStatus(prefix, "Connection timeout. Check your network.", "fail");
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SMS submit */
|
||||||
|
s.getElementById("btn-sms").addEventListener("click", function () {
|
||||||
|
clearErr("sms", ["name", "phone", "msg"]);
|
||||||
|
var hp = s.getElementById("hp-sms");
|
||||||
|
if (hp && hp.value) return;
|
||||||
|
var remaining = Math.ceil(
|
||||||
|
(CFG.minFillMs - (Date.now() - formStartTs)) / 1000,
|
||||||
|
);
|
||||||
|
if (remaining > 0) {
|
||||||
|
setStatus(
|
||||||
|
"sms",
|
||||||
|
"Please wait " + remaining + "s before submitting.",
|
||||||
|
"warn",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var raw = {
|
||||||
|
name: s.getElementById("sms-name").value,
|
||||||
|
phone: s.getElementById("sms-phone").value,
|
||||||
|
message: s.getElementById("sms-msg").value,
|
||||||
|
};
|
||||||
|
var errs = validateSMS(raw);
|
||||||
|
if (errs.length) {
|
||||||
|
markErr(errs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doSubmit(
|
||||||
|
"sms",
|
||||||
|
{
|
||||||
|
channel: "sms",
|
||||||
|
name: sanitize(raw.name, 80),
|
||||||
|
phone: sanitize(raw.phone, 15),
|
||||||
|
message: sanitize(raw.message, 1000),
|
||||||
|
nonce: nonce(),
|
||||||
|
source: w.location.href.slice(0, 200),
|
||||||
|
ts: Date.now(),
|
||||||
|
},
|
||||||
|
this,
|
||||||
|
"Send SMS",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Email submit */
|
||||||
|
s.getElementById("btn-email").addEventListener("click", function () {
|
||||||
|
clearErr("email", ["name", "email", "msg"]);
|
||||||
|
var hp = s.getElementById("hp-email");
|
||||||
|
if (hp && hp.value) return;
|
||||||
|
var remaining = Math.ceil(
|
||||||
|
(CFG.minFillMs - (Date.now() - formStartTs)) / 1000,
|
||||||
|
);
|
||||||
|
if (remaining > 0) {
|
||||||
|
setStatus(
|
||||||
|
"email",
|
||||||
|
"Please wait " + remaining + "s before submitting.",
|
||||||
|
"warn",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var raw = {
|
||||||
|
name: s.getElementById("email-name").value,
|
||||||
|
email: s.getElementById("email-email").value,
|
||||||
|
message: s.getElementById("email-msg").value,
|
||||||
|
};
|
||||||
|
var errs = validateEmail(raw);
|
||||||
|
if (errs.length) {
|
||||||
|
markErr(errs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doSubmit(
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
channel: "email",
|
||||||
|
name: sanitize(raw.name, 80),
|
||||||
|
email: sanitize(raw.email, 120),
|
||||||
|
message: sanitize(raw.message, 1000),
|
||||||
|
nonce: nonce(),
|
||||||
|
source: w.location.href.slice(0, 200),
|
||||||
|
ts: Date.now(),
|
||||||
|
},
|
||||||
|
this,
|
||||||
|
"Send Email",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* New message */
|
||||||
|
s.getElementById("btn-new").addEventListener("click", function () {
|
||||||
|
$success.style.display = "none";
|
||||||
|
$tabs.style.display = "flex";
|
||||||
|
s.querySelectorAll(".tab").forEach(function (el) {
|
||||||
|
el.classList.toggle("active", el.dataset.tab === "sms");
|
||||||
|
});
|
||||||
|
s.querySelectorAll(".pane").forEach(function (el) {
|
||||||
|
el.classList.remove("active");
|
||||||
|
});
|
||||||
|
s.getElementById("pane-sms").classList.add("active");
|
||||||
|
[
|
||||||
|
"sms-name",
|
||||||
|
"sms-phone",
|
||||||
|
"sms-msg",
|
||||||
|
"email-name",
|
||||||
|
"email-email",
|
||||||
|
"email-msg",
|
||||||
|
].forEach(function (id) {
|
||||||
|
var el = s.getElementById(id);
|
||||||
|
if (el) el.value = "";
|
||||||
|
});
|
||||||
|
clearErr("sms", ["name", "phone", "msg"]);
|
||||||
|
clearErr("email", ["name", "email", "msg"]);
|
||||||
|
formStartTs = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Badge */
|
||||||
|
setTimeout(function () {
|
||||||
|
if (!isOpen) $badge.style.display = "flex";
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Register & auto-mount */
|
||||||
|
if (!w.customElements.get("support-widget")) {
|
||||||
|
w.customElements.define("support-widget", SupportWidget);
|
||||||
|
}
|
||||||
|
function mount() {
|
||||||
|
if (!d.querySelector("support-widget"))
|
||||||
|
d.body.appendChild(d.createElement("support-widget"));
|
||||||
|
}
|
||||||
|
if (d.readyState === "loading") d.addEventListener("DOMContentLoaded", mount);
|
||||||
|
else mount();
|
||||||
|
})(window, document);
|
||||||
|
|
@ -2,6 +2,17 @@ window.PROLOGY_CONFIG = {
|
||||||
API_URL: "https://prologyms.nswteam.net/chat-plugin/api/support",
|
API_URL: "https://prologyms.nswteam.net/chat-plugin/api/support",
|
||||||
LOGO_SRC: "assets/Prology_logo.png",
|
LOGO_SRC: "assets/Prology_logo.png",
|
||||||
|
|
||||||
|
// ── Chat / Support widget ──
|
||||||
|
CHAT_API_URL: "https://prologyms.nswteam.net/chat-plugin/api/support",
|
||||||
|
CHAT_WHATSAPP_NUMBER: "84901234567",
|
||||||
|
CHAT_ACCENT_COLOR: "#4f46e5",
|
||||||
|
CHAT_POSITION: "right",
|
||||||
|
CHAT_RATE_LIMIT_MAX: 3,
|
||||||
|
CHAT_RATE_LIMIT_WINDOW_MIN: 10,
|
||||||
|
CHAT_MIN_FILL_MS: 3000,
|
||||||
|
CHAT_FAB_IMAGE_URL:
|
||||||
|
"https://prology.nswteam.net/media/wysiwyg/image_2026-04-02_15-43-14.png",
|
||||||
|
|
||||||
// ── Store picker (modal) ──
|
// ── Store picker (modal) ──
|
||||||
STORE_AU_URL: "https://prology.net/au",
|
STORE_AU_URL: "https://prology.net/au",
|
||||||
STORE_US_URL: "https://prology.net/us",
|
STORE_US_URL: "https://prology.net/us",
|
||||||
|
|
|
||||||
|
|
@ -1093,5 +1093,6 @@
|
||||||
|
|
||||||
<footer class="page-footer" role="contentinfo"></footer>
|
<footer class="page-footer" role="contentinfo"></footer>
|
||||||
<script src="assets/js/footer.js" defer></script>
|
<script src="assets/js/footer.js" defer></script>
|
||||||
|
<script src="assets/js/chat.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue