/** * Campaign UTM link helper (composer + report). * utm_source from data-utm-source (brand slug), utm_medium=email|sms|postcard, * utm_campaign=slug(name). Short URLs come from the portal JSON endpoint. */ (function (global) { "use strict"; var SLUG_MAX = 80; var SHORTEN_WAIT_MS = 400; var SHORT_URL_RE = /https?:\/\/(?:(?:www\.)?(?:beta\.)?piha\.li|(?:127\.0\.0\.1|localhost):\d+)\/[a-z0-9]{4,8}/i; function slug(name) { var s = String(name || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); return (s || "campaign").slice(0, SLUG_MAX); } function sourceFrom(panel) { return slug((panel && panel.getAttribute("data-utm-source")) || "campaign"); } function utmUrlRe(source) { return new RegExp( "https?:\\/\\/[^\\s<>\"]+utm_source=" + source + "[^\\s<>\"]*", "i" ); } function buildUrl(base, medium, name, source) { var root = String(base || "").replace(/\/+$/, ""); var ch = String(medium || "email").toLowerCase(); if (ch !== "email" && ch !== "sms" && ch !== "postcard") ch = "email"; var q = "utm_source=" + encodeURIComponent(source || "campaign") + "&utm_medium=" + encodeURIComponent(ch) + "&utm_campaign=" + encodeURIComponent(slug(name)); return root + "/?" + q; } function replaceTextUrl(text, url, source) { var raw = String(text || ""); var utmRe = utmUrlRe(source || "campaign"); if (utmRe.test(raw)) { return raw.replace(utmRe, url); } if (SHORT_URL_RE.test(raw)) { return raw.replace(SHORT_URL_RE, url); } var trimmed = raw.replace(/\s+$/, ""); return trimmed ? trimmed + "\n\n" + url : url; } function linkify(text) { var esc = String(text || "").replace(/[&<>"']/g, function (c) { return ( { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ c ] || c ); }); return esc.replace( /(https?:\/\/[^\s]+)/g, '$1' ); } function ensureQuillLink(quill, url, label, source) { if (!quill || !quill.root) return; var src = source || "campaign"; var existing = quill.root.querySelectorAll( 'a[data-campaign-utm], a[href*="utm_source=' + src + '"], a[href*="piha.li/"]' ); if (existing.length) { existing.forEach(function (a) { a.setAttribute("href", url); a.setAttribute("data-campaign-utm", "1"); }); return; } var labelText = label || "Website"; var start = Math.max(0, quill.getLength() - 1); quill.insertText(start, "\n" + labelText, "silent"); quill.formatText(start + 1, labelText.length, "link", url, "silent"); var links = quill.root.querySelectorAll("a"); var last = links[links.length - 1]; if (last) last.setAttribute("data-campaign-utm", "1"); } function renderQr(canvas, url) { if (!canvas || !url || typeof QRCode === "undefined") return Promise.resolve(); return QRCode.toCanvas(canvas, url, { width: 192, margin: 1, color: { dark: "#00626c", light: "#ffffff" }, }); } function flash(el, okText, errText, ok) { if (!el) return; el.textContent = ok ? okText : errText; el.hidden = false; window.setTimeout(function () { el.hidden = true; }, 2200); } function copyText(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text); } return Promise.reject(new Error("clipboard unavailable")); } function copyCanvas(canvas) { if (!canvas || !canvas.toBlob) { return Promise.reject(new Error("canvas unavailable")); } return new Promise(function (resolve, reject) { canvas.toBlob(function (blob) { if (!blob) { reject(new Error("qr blob failed")); return; } if ( !navigator.clipboard || !navigator.clipboard.write || typeof ClipboardItem === "undefined" ) { reject(new Error("image clipboard unavailable")); return; } navigator.clipboard .write([new ClipboardItem({ "image/png": blob })]) .then(resolve, reject); }, "image/png"); }); } function downloadCanvas(canvas, filename) { if (!canvas) return; var a = document.createElement("a"); a.href = canvas.toDataURL("image/png"); a.download = filename || "campaign-qr.png"; document.body.appendChild(a); a.click(); a.remove(); } function bindPanel(panel, opts) { if (!panel) return; opts = opts || {}; var urlEl = panel.querySelector("[data-utm-url]"); var canvas = panel.querySelector("[data-utm-qr]"); var qrWrap = panel.querySelector("[data-utm-qr-wrap]"); var smsHint = panel.querySelector("[data-utm-sms-hint]"); var emailHint = panel.querySelector("[data-utm-email-hint]"); var postcardHint = panel.querySelector("[data-utm-postcard-hint]"); var statusEl = panel.querySelector("[data-utm-status]"); var base = panel.getAttribute("data-base-url") || ""; var source = sourceFrom(panel); var label = panel.getAttribute("data-link-label") || "Website"; var live = panel.getAttribute("data-live") === "1"; var shortenUrl = panel.getAttribute("data-shorten-url") || ""; var csrf = (opts.csrfToken || panel.getAttribute("data-csrf") || "").trim(); var shortenTimer = null; var shortenSeq = 0; function medium() { if (typeof opts.getMedium === "function") return opts.getMedium(); return panel.getAttribute("data-medium") || "email"; } function name() { if (typeof opts.getName === "function") return opts.getName(); return panel.getAttribute("data-campaign-name") || ""; } function campaignId() { return ( (typeof opts.getCampaignId === "function" && opts.getCampaignId()) || panel.getAttribute("data-campaign-id") || "" ); } function paint(url, inject) { var ch = medium(); if (urlEl) urlEl.textContent = url; if (qrWrap) qrWrap.hidden = ch !== "postcard"; if (smsHint) smsHint.hidden = ch !== "sms"; if (emailHint) emailHint.hidden = ch !== "email"; if (postcardHint) postcardHint.hidden = ch !== "postcard"; if (ch === "postcard" && canvas) { renderQr(canvas, url).catch(function () {}); } if (inject && typeof opts.onUrlChange === "function") { opts.onUrlChange(url, ch, label); } } function apply() { var ch = medium(); var longUrl = buildUrl(base, ch, name(), source); if (!shortenUrl) { paint(longUrl, true); return longUrl; } paint(longUrl, false); var seq = ++shortenSeq; window.clearTimeout(shortenTimer); shortenTimer = window.setTimeout(function () { fetch(shortenUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRFToken": csrf, }, credentials: "same-origin", body: JSON.stringify({ name: name(), medium: ch, campaign_id: campaignId(), }), }) .then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (data) { if (seq !== shortenSeq) return; paint((data && data.display_url) || longUrl, true); }) .catch(function () { if (seq !== shortenSeq) return; paint(longUrl, true); }); }, SHORTEN_WAIT_MS); return longUrl; } panel.querySelectorAll("[data-utm-copy-url]").forEach(function (btn) { btn.addEventListener("click", function () { var url = (urlEl && urlEl.textContent) || ""; copyText(url).then( function () { flash(statusEl, "Link copied.", "Could not copy.", true); }, function () { flash(statusEl, "", "Could not copy link.", false); } ); }); }); panel.querySelectorAll("[data-utm-copy-qr]").forEach(function (btn) { btn.addEventListener("click", function () { copyCanvas(canvas).then( function () { flash(statusEl, "QR image copied.", "Could not copy QR.", true); }, function () { flash(statusEl, "", "Copy failed — use Download PNG.", false); } ); }); }); panel.querySelectorAll("[data-utm-download-qr]").forEach(function (btn) { btn.addEventListener("click", function () { downloadCanvas(canvas, "qr-" + slug(name()) + ".png"); flash(statusEl, "QR downloaded.", "", true); }); }); if (live) { var nameField = document.getElementById("id_name"); if (nameField) nameField.addEventListener("input", apply); } panel._utmApply = apply; apply(); return { apply: apply, buildUrl: buildUrl }; } global.CampaignUtm = { slug: slug, buildUrl: buildUrl, replaceTextUrl: replaceTextUrl, linkify: linkify, ensureQuillLink: ensureQuillLink, bindPanel: bindPanel, }; })(window);