diff --git a/assets/js/chat.js b/assets/js/chat.js new file mode 100644 index 0000000..a5a611f --- /dev/null +++ b/assets/js/chat.js @@ -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, "/") + .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); diff --git a/assets/js/config.js b/assets/js/config.js index deeace6..a161352 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -2,6 +2,17 @@ window.PROLOGY_CONFIG = { API_URL: "https://prologyms.nswteam.net/chat-plugin/api/support", 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_AU_URL: "https://prology.net/au", STORE_US_URL: "https://prology.net/us", diff --git a/index.html b/index.html index e096637..dd0c678 100644 --- a/index.html +++ b/index.html @@ -1093,5 +1093,6 @@ +