SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
11.2 KB · 323 lines javascript
Raw Blame History
1/* Groupe KA — ka-id.js : SDK client KA ID v2 (personnalisation).2 * SOURCE CANONIQUE : ka-ui.git/kaid/ka-id.js — copié dans frontend/public/3 * (ou public/) de chaque app par sync-kaid.sh. Corriger ICI puis redistribuer.4 *5 * Rôle : signaux navigateur que le serveur ne voit pas — position des clics6 * dans les résultats, temps passé sur une fiche (dwell), visites de retour —7 * plus les aides d'interface : KAID.hide() (« Pas pour moi »),8 * KAID.saveSearch() (recherche sauvegardée), badge « Recommandé pour vous ».9 *10 * Intégration (index.html, AVANT le bundle) :11 *   <script>12 *     window.KAID_CONF = {13 *       card:   "a[href^='/emploi/']",      // sélecteur des cartes résultat14 *       detail: "^/emploi/(.+)$",           // regex fiche → capture l'uid15 *       entity: "job",                      // entity_type des événements16 *     };17 *   </script>18 *   <script src="/ka-id.js" defer></script>19 *20 * Vie privée : ne fait RIEN si l'utilisateur n'est pas connecté via KA ID21 * (vérifié auprès de /api/kaid/status) ; le hub respecte ensuite ses22 * réglages Historique/Personnalisation (voir groupe-ka.com/mon-ka).23 */24(function () {25  "use strict";26  var CONF = window.KAID_CONF || {};27  var API = CONF.api || "/api/kaid";28  var connected = null;          // null = inconnu, sinon bool29  var queue = [];30  var flushTimer = null;3132  /* ---------- transport (lots de 10, sendBeacon à la fermeture) ---------- */3334  function post(events, useBeacon) {35    if (!events.length) return;36    var body = JSON.stringify({ events: events });37    if (useBeacon && navigator.sendBeacon) {38      navigator.sendBeacon(API + "/events",39        new Blob([body], { type: "application/json" }));40      return;41    }42    fetch(API + "/events", {43      method: "POST",44      headers: { "Content-Type": "application/json" },45      body: body,46      keepalive: true,47      credentials: "same-origin",48    }).catch(function () {});49  }5051  function flush(useBeacon) {52    if (!queue.length) return;53    var batch = queue.splice(0, queue.length);54    post(batch, useBeacon);55  }5657  function track(type, data) {58    if (connected === false) return;59    var e = { type: type };60    if (data) for (var k in data) if (data[k] != null) e[k] = data[k];61    queue.push(e);62    if (queue.length >= 10) flush(false);63    else if (!flushTimer)64      flushTimer = setTimeout(function () { flushTimer = null; flush(false); }, 4000);65  }6667  /* ---------- connexion ---------- */6869  function init(cb) {70    try {71      var cached = sessionStorage.getItem("kaid_status");72      if (cached) {73        var c = JSON.parse(cached);74        if (Date.now() - c.t < 600000) { connected = c.on; cb(); return; }75      }76    } catch (err) { /* stockage indisponible */ }77    fetch(API + "/status", { credentials: "same-origin" })78      .then(function (r) { return r.json(); })79      .then(function (d) {80        connected = !!d.connected;81        try {82          sessionStorage.setItem("kaid_status",83            JSON.stringify({ on: connected, t: Date.now() }));84        } catch (err) { /* stockage indisponible */ }85        cb();86      })87      .catch(function () { connected = false; });88  }8990  /* ---------- clics sur les cartes résultat ---------- */9192  function uidFromHref(el) {93    if (!CONF.detail) return null;94    var href = el.getAttribute("href") || "";95    var path = href.replace(/^https?:\/\/[^/]+/, "").split("?")[0];96    var m = path.match(new RegExp(CONF.detail));97    return m ? decodeURIComponent(m[1]) : null;98  }99100  function watchClicks() {101    if (!CONF.card) return;102    document.addEventListener("click", function (ev) {103      var el = ev.target && ev.target.closest && ev.target.closest(CONF.card);104      if (!el) return;105      var uid = (CONF.uid ? CONF.uid(el) : null) || uidFromHref(el);106      if (!uid) return;107      var cards = document.querySelectorAll(CONF.card);108      var pos = Array.prototype.indexOf.call(cards, el);109      track("click", {110        entity_type: CONF.entity, entity_id: uid,111        position: pos >= 0 ? pos : null,112      });113    }, true);114  }115116  /* ---------- temps passé sur une fiche (dwell) ---------- */117118  var dwell = { uid: null, t0: 0 };119120  function detailUid() {121    if (!CONF.detail) return null;122    var m = location.pathname.match(new RegExp(CONF.detail));123    return m ? decodeURIComponent(m[1]) : null;124  }125126  function endDwell(useBeacon) {127    if (!dwell.uid) return;128    var ms = Date.now() - dwell.t0;129    if (ms >= 8000)130      track("detail_dwell",131        { entity_type: CONF.entity, entity_id: dwell.uid, dwell_ms: ms });132    dwell.uid = null;133    if (useBeacon) flush(true);134  }135136  function checkRoute() {137    var uid = detailUid();138    if (uid !== dwell.uid) {139      endDwell(false);140      if (uid) dwell = { uid: uid, t0: Date.now() };141    }142    updateUi();143  }144145  function watchRoutes() {146    ["pushState", "replaceState"].forEach(function (fn) {147      var orig = history[fn];148      history[fn] = function () {149        var out = orig.apply(this, arguments);150        setTimeout(checkRoute, 50);151        return out;152      };153    });154    window.addEventListener("popstate", function () { setTimeout(checkRoute, 50); });155    document.addEventListener("visibilitychange", function () {156      if (document.visibilityState === "hidden") { endDwell(true); }157      else checkRoute();158    });159    window.addEventListener("pagehide", function () { endDwell(true); });160    checkRoute();161  }162163  /* ---------- interface : sauvegarder la recherche / pas pour moi ---------- */164165  var INK = "#141814";166  var PAPER = "#f5f3ee";167168  function injectCss() {169    if (document.getElementById("kaid-ui-css")) return;170    var st = document.createElement("style");171    st.id = "kaid-ui-css";172    st.textContent =173      ".kaid-pill{position:fixed;left:14px;bottom:14px;z-index:560;display:inline-flex;" +174      "align-items:center;gap:8px;padding:9px 14px;border-radius:999px;border:1.5px solid " + INK + ";" +175      "background:" + PAPER + ";color:" + INK + ";font:600 12.5px/1 system-ui,sans-serif;" +176      "box-shadow:0 2px 10px rgba(20,24,20,.18);cursor:pointer;max-width:78vw}" +177      ".kaid-pill b{font-weight:700}" +178      ".kaid-pill .kaid-x{margin-left:2px;opacity:.55;font-weight:700;cursor:pointer}" +179      ".kaid-pill a{color:inherit;text-decoration:underline}" +180      "@media (max-width:768px){.kaid-pill{bottom:calc(84px + env(safe-area-inset-bottom))}}";181    document.head.appendChild(st);182  }183184  function removePill() {185    var el = document.getElementById("kaid-pill");186    if (el) el.remove();187  }188189  function pillDismissed() {190    try {191      return Date.now() - Number(localStorage.getItem("kaid_pill_off") || 0) < 6048e5;192    } catch (err) { return false; }193  }194195  function showSavePill() {196    if (pillDismissed() || document.getElementById("kaid-pill")) return;197    injectCss();198    var el = document.createElement("div");199    el.id = "kaid-pill";200    el.className = "kaid-pill";201    el.innerHTML = "<span>☆ <b>Sauvegarder cette recherche</b></span>" +202      "<span class='kaid-x' role='button' aria-label='Ne plus proposer'>✕</span>";203    el.querySelector(".kaid-x").addEventListener("click", function (ev) {204      ev.stopPropagation();205      try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ }206      removePill();207    });208    el.addEventListener("click", function () {209      el.innerHTML = "<span>…</span>";210      var filters = {};211      new URLSearchParams(location.search).forEach(function (v, k) {212        if (v) filters[k] = v;213      });214      window.KAID.saveSearch({ alert: true, filters: filters,215                               query: filters.q || null })216        .then(function (r) {217          el.innerHTML = r && r.ok !== false && !r.error218            ? "<span>✓ Sauvegardée · alerte activée — <a href='https://www.groupe-ka.com/mon-ka'>Mon KA</a></span>"219            : "<span>Connexion KA ID requise</span>";220          setTimeout(removePill, 6000);221        })222        .catch(function () { removePill(); });223    });224    document.body.appendChild(el);225  }226227  function showHideLink(uid) {228    if (document.getElementById("kaid-pill")) return;229    injectCss();230    var el = document.createElement("div");231    el.id = "kaid-pill";232    el.className = "kaid-pill";233    el.innerHTML = "<span>✕ Pas pour moi</span>" +234      "<span class='kaid-x' role='button' aria-label='Fermer'>✕</span>";235    el.querySelector(".kaid-x").addEventListener("click", function (ev) {236      ev.stopPropagation();237      try { localStorage.setItem("kaid_pill_off", String(Date.now())); } catch (err) { /* privé */ }238      removePill();239    });240    el.addEventListener("click", function () {241      el.innerHTML = "<span>…</span>";242      window.KAID.hide(uid).then(function () {243        el.innerHTML = "<span>Masquée — elle ne réapparaîtra plus dans vos résultats.</span>";244        setTimeout(removePill, 5000);245      }).catch(function () { removePill(); });246    });247    document.body.appendChild(el);248  }249250  function updateUi() {251    if (!connected || CONF.ui === false) return;252    removePill();253    if (pillDismissed()) return;254    var uid = detailUid();255    if (uid) showHideLink(uid);256    else if (location.search.length > 1 && CONF.savePill !== false)257      showSavePill();258  }259260  /* ---------- visite de retour ---------- */261262  function returnVisit() {263    try {264      var last = Number(localStorage.getItem("kaid_last") || 0);265      var now = Date.now();266      if (last && now - last > 72e6) track("return_visit", {}); // > 20 h267      localStorage.setItem("kaid_last", String(now));268    } catch (err) { /* stockage indisponible */ }269  }270271  /* ---------- API publique ---------- */272273  window.KAID = {274    track: track,275    connected: function () { return connected; },276    /** « Pas pour moi » — masque l'annonce partout et nourrit le signal négatif. */277    hide: function (itemId, features) {278      return fetch(API + "/hide", {279        method: "POST",280        headers: { "Content-Type": "application/json" },281        credentials: "same-origin",282        body: JSON.stringify({ item_id: itemId, on: true, features: features || null }),283      }).then(function (r) { return r.json(); });284    },285    /** Sauvegarde la recherche courante (par défaut : URL + titre du document). */286    saveSearch: function (opts) {287      opts = opts || {};288      return fetch(API + "/saved-searches", {289        method: "POST",290        headers: { "Content-Type": "application/json" },291        credentials: "same-origin",292        body: JSON.stringify({293          action: "add",294          label: opts.label || document.title.split("|")[0].split("—")[0].trim(),295          query: opts.query || null,296          filters: opts.filters || null,297          url: opts.url || (location.origin + location.pathname + location.search),298          alert: !!opts.alert,299          frequency: opts.frequency || null,300        }),301      }).then(function (r) { return r.json(); });302    },303    external: function (itemId, features) {304      track("external_click",305        { entity_type: CONF.entity, entity_id: itemId, features: features });306      flush(true);307    },308  };309310  /* ---------- démarrage ---------- */311312  function start() {313    if (!connected) return;314    watchClicks();315    watchRoutes();316    returnVisit();317  }318319  if (document.readyState === "loading")320    document.addEventListener("DOMContentLoaded", function () { init(start); });321  else init(start);322})();323