// ───────────────────────────────────────────────────────────────────────────── // Shared client-side JS baked into every magic-link page. // Exposes: $, esc, toast, fmtTime, dlbl, api, router, go // Configure via window.UI_CFG = { token, hub, today, base } BEFORE this loads. // ───────────────────────────────────────────────────────────────────────────── (function () { var CFG = window.UI_CFG || {} var HUB = CFG.hub || '' var BASE = CFG.base || '' // e.g. "/t/" — prepended to all api paths // ── DOM / string ──────────────────────────────────────────────────────── function $ (id) { return document.getElementById(id) } function esc (s) { return (s == null ? '' : String(s)) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, ''') } // ── Toast ─────────────────────────────────────────────────────────────── function toast (message, ok) { var t = $('toast') if (!t) { t = document.createElement('div') t.id = 'toast' t.className = 'toast' document.body.appendChild(t) } t.textContent = message t.style.background = ok ? '#22c55e' : '#ef4444' t.classList.add('on') clearTimeout(t._timer) t._timer = setTimeout(function () { t.classList.remove('on') }, 2500) } // ── Date / time (mirror of server helpers) ────────────────────────────── function fmtTime (t) { if (!t) return '' var p = String(t).split(':') return p[0] + 'h' + (p[1] || '00') } function dlbl (d, today) { today = today || CFG.today if (!d) return 'Sans date' if (d === today) return "Aujourd'hui" try { var diff = Math.round((new Date(d + 'T00:00:00') - new Date(today + 'T00:00:00')) / 86400000) if (diff === -1) return 'Hier' if (diff === 1) return 'Demain' return new Date(d + 'T00:00:00').toLocaleDateString('fr-CA', { weekday: 'short', day: 'numeric', month: 'short' }) } catch (e) { return d } } // ── API wrapper ───────────────────────────────────────────────────────── // Single place to prepend HUB+BASE and parse JSON. Page-specific code // just does api.post('/note', {job, notes}) — no URL glue, no error swallowing. function apiUrl (path) { // Allow absolute paths through unchanged (e.g. fully qualified URLs) if (/^https?:/i.test(path)) return path if (path[0] !== '/') path = '/' + path return HUB + BASE + path } function apiFetch (path, opts) { opts = opts || {} var headers = Object.assign({}, opts.headers || {}) if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) { headers['Content-Type'] = headers['Content-Type'] || 'application/json' opts.body = JSON.stringify(opts.body) } opts.headers = headers return fetch(apiUrl(path), opts).then(function (r) { var ct = r.headers.get('content-type') || '' if (ct.indexOf('application/json') >= 0) { return r.json().then(function (data) { return { ok: r.ok, status: r.status, data: data } }) } return r.text().then(function (text) { return { ok: r.ok, status: r.status, text: text } }) }) } var api = { url: apiUrl, get: function (p) { return apiFetch(p) }, post: function (p, body){ return apiFetch(p, { method: 'POST', body: body }) }, put: function (p, body){ return apiFetch(p, { method: 'PUT', body: body }) }, } // ── Hash router ───────────────────────────────────────────────────────── // Patterns: "#home" (literal) or "#job/:name" (param). Handlers are called // with (params, rawHash). Calling router.go('#foo') updates location.hash. var routes = [] function on (pattern, handler) { routes.push({ pattern: pattern, handler: handler }) } function matchRoute (hash) { for (var i = 0; i < routes.length; i++) { var r = routes[i] if (r.pattern === hash) return { handler: r.handler, params: {} } // "#job/:name" — matches "#job/DJ-123" var parts = r.pattern.split('/') var actual = hash.split('/') if (parts.length !== actual.length) continue var params = {}, ok = true for (var j = 0; j < parts.length; j++) { if (parts[j][0] === ':') params[parts[j].slice(1)] = decodeURIComponent(actual[j] || '') else if (parts[j] !== actual[j]) { ok = false; break } } if (ok) return { handler: r.handler, params: params } } return null } function dispatch () { var hash = location.hash || (routes[0] && routes[0].pattern) || '#' var m = matchRoute(hash) if (m) { m.handler(m.params, hash) } else if (routes[0]) { routes[0].handler({}, routes[0].pattern) } window.scrollTo(0, 0) } function go (hash) { if (location.hash === hash) dispatch() else location.hash = hash } window.addEventListener('hashchange', dispatch) var router = { on: on, dispatch: dispatch, go: go } // ── Expose globals (only ones pages reference directly) ───────────────── window.$ = $ window._esc = esc // underscore to avoid colliding with page esc window.toast = toast window.fmtTime = fmtTime window.dlbl = dlbl window.api = api window.router = router window.go = go })()