← Public packages
@kentcdodds/christ-paintings
Official Latter-day Saint paintings of Jesus Christ, one at a time, to show children during church, plus coloring pages of the Savior. Swipe to move. Works offline.
src/update-script.ts
163 lines · 6.0 KB · TypeScript// 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 =
'<div><strong data-update-label></strong><p>Refresh when you are ready</p></div>' +
'<div class="update-actions"><button type="button" class="accent" data-update-refresh>Refresh</button>' +
'<button type="button" data-update-dismiss aria-label="Dismiss update">Dismiss</button></div>'
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()
})
})()
`