← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-remix/app/ui/about-panel.tsx
170 lines · 4.8 KB · TypeScript/** @jsxRuntime automatic */
/** @jsxImportSource remix/ui */
import { clientEntry, on, type Handle } from 'remix/ui'
import {
formatLocalDateTime,
readLastUpdateCheckAt,
writeLastUpdateCheckAt,
} from './local-time.ts'
import { createBusyGate } from './busy.ts'
import { ErrorBanner } from './error-banner.tsx'
export type AboutPanelProps = {
sha: string
shortSha: string
message: string
publishedAt: string
committedAt: string
codeUrl: string | null
runningSha: string
/** localStorage key prefix */
appId?: string
}
/**
* About metadata + manual update check.
* Times render in the browser's local timezone; last-checked is stored locally.
*/
export const AboutPanel = clientEntry(
'kody:app#AboutPanel',
function AboutPanel(handle: Handle<AboutPanelProps>) {
const appId = handle.props.appId || '__PACKAGE_ID__'
const checkBusy = createBusyGate({ delayMs: 200, minDurationMs: 400 })
let lastCheckedAt: string | null = null
let checking = false
let status: 'idle' | 'ok' | 'update' | 'error' = 'idle'
let statusDetail = ''
if (typeof window !== 'undefined') {
lastCheckedAt = readLastUpdateCheckAt(appId)
}
async function runCheck() {
if (checking || typeof window === 'undefined') return
checking = true
status = 'idle'
statusDetail = ''
checkBusy.start(() => handle.update())
handle.update()
try {
const root = document.documentElement
const appBase = root.getAttribute('data-app-base') || ''
const runningSha =
handle.props.runningSha || root.getAttribute('data-running-sha') || ''
const res = await fetch(`${appBase}/api/version`, {
cache: 'no-store',
credentials: 'same-origin',
})
const version = res.ok
? ((await res.json()) as { sha?: string; shortSha?: string })
: {}
let waiting = false
if ('serviceWorker' in navigator) {
const regs = await navigator.serviceWorker.getRegistrations()
for (const reg of regs) {
if (reg.waiting) waiting = true
if (reg.update) await reg.update().catch(() => null)
}
}
lastCheckedAt = writeLastUpdateCheckAt(new Date().toISOString(), appId)
const latestSha = String(version.sha || '')
if (waiting || (runningSha && latestSha && runningSha !== latestSha)) {
status = 'update'
statusDetail = waiting
? 'Service worker waiting'
: `Latest ${version.shortSha || latestSha.slice(0, 7)}`
} else {
status = 'ok'
statusDetail = 'Up to date'
}
} catch (error) {
status = 'error'
statusDetail = error instanceof Error ? error.message : String(error)
lastCheckedAt = writeLastUpdateCheckAt(new Date().toISOString(), appId)
} finally {
checking = false
await checkBusy.stop(() => handle.update())
handle.update()
}
}
return () => {
const {
sha,
shortSha,
message,
publishedAt,
committedAt,
codeUrl,
} = handle.props
const publishedLocal = formatLocalDateTime(publishedAt)
const committedLocal = formatLocalDateTime(committedAt)
const checkedLocal = formatLocalDateTime(lastCheckedAt)
return (
<section class="pak-card pak-stack" data-about-panel>
<dl class="pak-stack-sm">
<div>
<dt class="pak-muted">SHA</dt>
<dd>
<code tabindex="0" data-tip={sha || ''}>
{shortSha || sha || '—'}
</code>
</dd>
</div>
<div>
<dt class="pak-muted">Message</dt>
<dd>{message || '—'}</dd>
</div>
<div>
<dt class="pak-muted">Published</dt>
<dd data-tip={publishedAt || undefined}>{publishedLocal}</dd>
</div>
<div>
<dt class="pak-muted">Committed</dt>
<dd data-tip={committedAt || undefined}>{committedLocal}</dd>
</div>
<div>
<dt class="pak-muted">Last checked</dt>
<dd data-tip={lastCheckedAt || undefined}>
{lastCheckedAt ? checkedLocal : 'Never'}
</dd>
</div>
{codeUrl ? (
<div>
<dt class="pak-muted">Code</dt>
<dd>
<a href={codeUrl} data-tip={codeUrl}>
Package
</a>
</dd>
</div>
) : null}
</dl>
<div class="pak-cluster">
<button
class="pak-btn"
type="button"
data-about-check-update
disabled={checking ? true : undefined}
data-pending={checking ? 'true' : undefined}
aria-busy={checking ? 'true' : undefined}
mix={on('click', () => void runCheck())}
>
{checkBusy.showBusy ? 'Checking…' : 'Check for updates'}
</button>
{status === 'ok' ? (
<span class="pak-muted">Up to date</span>
) : null}
{status === 'update' ? (
<span class="pak-muted">{statusDetail || 'Update available'}</span>
) : null}
</div>
{status === 'error' ? (
<ErrorBanner title="Update check failed" message={statusDetail || 'Unknown error'} />
) : null}
</section>
)
}
},
)