From a1794db11ce89058b692e4394e07b88c9ca72680 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 20 May 2026 10:19:09 +0700 Subject: [PATCH 1/3] update(chat): update ui chat --- assets/js/chat.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/assets/js/chat.js b/assets/js/chat.js index a5a611f..abb95e7 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -12,7 +12,6 @@ if (w.__MSW_LOADED__) return; w.__MSW_LOADED__ = true; - var P = w.PROLOGY_CONFIG || {}; var CFG = { apiEndpoint: P.CHAT_API_URL || @@ -24,7 +23,9 @@ 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, + 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: @@ -97,11 +98,7 @@ 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) - ) + if (!phone || !/^04\s?\d{2}\s?\d{3}\s?\d{3}$/.test(phone)) e.push("sms-phone"); if (!message || message.length < 10) e.push("sms-msg"); return e; @@ -146,7 +143,7 @@ /* 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-image:url(https://prology.nswteam.net/media/wysiwyg/image_2026-04-02_15-43-14.png);", "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);", @@ -241,7 +238,7 @@ /* SMS */ '
', '
Please enter your full name
', - '
Enter a valid AU mobile number
', + '
Enter AU mobile in format: 04 XX XXX XXX
', '
Please provide more details (min 10 chars)
', '
', '', + '', "
", "", ].join(""); @@ -424,8 +421,17 @@ 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"); + setStatus( + prefix, + "Too many requests. Please try again later.", + "warn", + ); + else + setStatus( + prefix, + "Too many requests. Please try again later.", + "fail", + ); }; xhr.ontimeout = function () { $btn.disabled = false; -- 2.39.2 From cb03912252dab720f775b3a19a3e0d15fecefe10 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 20 May 2026 10:20:05 +0700 Subject: [PATCH 2/3] update(chat): update ui chat --- assets/js/chat.js | 569 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 569 insertions(+) diff --git a/assets/js/chat.js b/assets/js/chat.js index abb95e7..dc2f980 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -12,6 +12,575 @@ 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, "/") + .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 = + ''; + 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 ( + '' + ); + } + var SVG_CHECK = + ''; + + /* ── 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 = [ + '', + '", + ].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); + +/** + * ============================================================================== + * 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 || -- 2.39.2 From 34786fb633757b651051b6237fad195850b6ccaa Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 20 May 2026 10:20:37 +0700 Subject: [PATCH 3/3] update(chat): update ui chat --- assets/js/chat.js | 568 ---------------------------------------------- 1 file changed, 568 deletions(-) diff --git a/assets/js/chat.js b/assets/js/chat.js index dc2f980..4d85e22 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -34,574 +34,6 @@ "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, "/") - .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 = - ''; - 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 ( - '' - ); - } - var SVG_CHECK = - ''; - - /* ── 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 = [ - '', - '", - ].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); - -/** - * ============================================================================== - * 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 ""; -- 2.39.2