// Update check + update toast, adapted from @kentcdodds/package-app-kit starter-remix // (app/ui/update-banner.tsx and app/ui/about-panel.tsx). Inlined on every page. // Plain JS in a raw string: no backticks or template placeholders inside. export const UPDATE_JS = String.raw` (function () { 'use strict' var APP_ID = 'christ-paintings' var root = document.documentElement var appBase = root.getAttribute('data-app-base') || '' var runningSha = root.getAttribute('data-running-sha') || '' function lastCheckKey() { return APP_ID + '-last-update-check' } function readLastCheck() { try { var v = localStorage.getItem(lastCheckKey()) return v && Date.parse(v) ? v : null } catch (e) { return null } } function writeLastCheck(iso) { try { localStorage.setItem(lastCheckKey(), iso) } catch (e) {} return iso } function dismissKey(token) { return APP_ID + '-update-dismissed-' + token } function isDismissed(token) { try { return localStorage.getItem(dismissKey(token)) !== null } catch (e) { return false } } function markDismissed(token) { try { localStorage.setItem(dismissKey(token), String(Date.now())) } catch (e) {} } function formatLocal(iso) { var raw = String(iso || '').trim() if (!raw) return '-' var ms = Date.parse(raw) if (!isFinite(ms)) return '-' try { return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(ms)) } catch (e) { return new Date(ms).toLocaleString() } } var waitingWorker = null var inFlight = null // Compare the running shell's SHA with the latest recorded publish, and nudge the // service worker to look for a new version. Resolves { kind, label, latest }. function check() { if (inFlight) return inFlight inFlight = fetch(appBase + '/api/version', { cache: 'no-store', credentials: 'same-origin' }) .then(function (res) { if (!res.ok) throw new Error('Version check failed (' + res.status + ')') var type = res.headers.get('content-type') || '' if (type.indexOf('json') < 0) throw new Error('Version check got an unexpected response. Reopen the app and try again.') return res.json() }) .then(function (version) { var regsP = 'serviceWorker' in navigator ? navigator.serviceWorker.getRegistrations() : Promise.resolve([]) return regsP.then(function (regs) { waitingWorker = null return Promise.all(regs.map(function (reg) { if (reg.waiting) waitingWorker = reg.waiting return reg.update ? reg.update().catch(function () { return null }) : null })) }).then(function () { return version }) }) .then(function (version) { writeLastCheck(new Date().toISOString()) var latestSha = String(version.sha || '') var kind = waitingWorker ? 'worker' : runningSha && latestSha && runningSha !== latestSha ? 'published' : null return { kind: kind, token: kind === 'worker' ? 'worker' : kind ? version.shortSha || latestSha.slice(0, 7) : null, latest: version, } }) .catch(function (error) { writeLastCheck(new Date().toISOString()) throw error }) .finally(function () { inFlight = null }) return inFlight } // Ask the service worker to re-fetch the cached app pages, then reload into the new version. function refresh() { if (waitingWorker) waitingWorker.postMessage('SKIP_WAITING') var controller = navigator.serviceWorker && navigator.serviceWorker.controller var done = function () { location.reload() } if (!controller) return done() var settled = false var channel = new MessageChannel() channel.port1.onmessage = function () { if (!settled) { settled = true; done() } } controller.postMessage({ type: 'REFRESH_SHELL' }, [channel.port2]) setTimeout(function () { if (!settled) { settled = true; done() } }, 4000) } // ---------- Fixed, dismissible toast (no layout shift) ---------- var toast = null var available = null function renderToast() { var show = available && !isDismissed(available) if (!show) { if (toast) toast.hidden = true return } if (!toast) { toast = document.createElement('div') toast.className = 'update-toast' toast.setAttribute('role', 'status') toast.setAttribute('aria-live', 'polite') toast.setAttribute('data-update-toast', '') toast.innerHTML = '

Refresh when you are ready

' + '
' + '
' toast.querySelector('[data-update-refresh]').addEventListener('click', function (e) { var btn = e.currentTarget if (btn.getAttribute('aria-busy') === 'true') return btn.setAttribute('aria-busy', 'true') btn.textContent = 'Refreshing...' requestAnimationFrame(refresh) }) toast.querySelector('[data-update-dismiss]').addEventListener('click', function () { if (available) markDismissed(available) renderToast() }) document.body.appendChild(toast) } toast.querySelector('[data-update-label]').textContent = available === 'worker' ? 'Update ready' : 'Update available (' + available + ')' toast.hidden = false } function checkAndToast() { return check().then(function (result) { available = result.token renderToast() return result }, function () { return null }) } function showResult(result) { available = result ? result.token : null renderToast() } window.christPaintingsUpdate = { check: check, showResult: showResult, checkAndToast: checkAndToast, refresh: refresh, readLastCheck: readLastCheck, formatLocal: formatLocal, get runningSha() { return runningSha }, get available() { return available }, } if ('serviceWorker' in navigator && (location.protocol === 'https:' || location.hostname === '127.0.0.1' || location.hostname === 'localhost')) { navigator.serviceWorker.register(appBase + '/sw.js', { scope: appBase + '/' }).catch(function () {}) } checkAndToast() document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') checkAndToast() }) })() `