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 = [
- '',
- '
',
- '
',
- '',
- '',
- '',
- "
",
- /* SMS */
- '
',
- '
',
- '
',
- '
Please provide more details (min 10 chars)
',
- '
',
- '
",
- '
',
- "
",
- /* Email */
- '
',
- '
',
- '
',
- '
Please provide more details (min 10 chars)
',
- '
',
- '
",
- '
',
- "
",
- /* WhatsApp */
- '
',
- '
',
- '
' + waSVG(26) + "
",
- '
Chat with us on WhatsApp for support,
inquiries, and immediate expert advice.
',
- '
",
- "
",
- "
",
- /* Success */
- '
',
- '
' + SVG_CHECK + "
",
- "
Message Received!
",
- "
Thanks for reaching out.
We'll get back to you shortly.
",
- '
',
- "
",
- "
",
- ].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 "";