← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
import { useEffect, useState } from 'react'
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
appId?: string
}
/**
* About metadata + manual update check.
* Times render in the browser's local timezone; last-checked is stored locally.
*/
export function AboutPanel(props: AboutPanelProps) {
const appId = props.appId || '__PACKAGE_ID__'
const [lastCheckedAt, setLastCheckedAt] = useState<string | null>(null)
const [checking, setChecking] = useState(false)
const [showBusy, setShowBusy] = useState(false)
const [status, setStatus] = useState<'idle' | 'ok' | 'update' | 'error'>('idle')
const [statusDetail, setStatusDetail] = useState('')
useEffect(() => {
setLastCheckedAt(readLastUpdateCheckAt(appId))
}, [appId])
async function runCheck() {
if (checking || typeof window === 'undefined') return
setChecking(true)
setStatus('idle')
setStatusDetail('')
const gate = createBusyGate({ delayMs: 200, minDurationMs: 400 })
gate.start((phase) => setShowBusy(phase === 'busy'))
try {
const root = document.documentElement
const appBase = root.getAttribute('data-app-base') || ''
const runningSha =
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)
}
}
const checked = writeLastUpdateCheckAt(new Date().toISOString(), appId)
setLastCheckedAt(checked)
const latestSha = String(version.sha || '')
if (waiting || (runningSha && latestSha && runningSha !== latestSha)) {
setStatus('update')
setStatusDetail(
waiting ? 'Service worker waiting' : `Latest ${version.shortSha || latestSha.slice(0, 7)}`,
)
} else {
setStatus('ok')
setStatusDetail('Up to date')
}
} catch (error) {
setStatus('error')
setStatusDetail(error instanceof Error ? error.message : String(error))
setLastCheckedAt(writeLastUpdateCheckAt(new Date().toISOString(), appId))
} finally {
setChecking(false)
await gate.stop((phase) => setShowBusy(phase === 'busy'))
}
}
const {
sha,
shortSha,
message,
publishedAt,
committedAt,
codeUrl,
} = props
const publishedLocal = formatLocalDateTime(publishedAt)
const committedLocal = formatLocalDateTime(committedAt)
const checkedLocal = formatLocalDateTime(lastCheckedAt)
return (
<section className="pak-card pak-stack" data-about-panel>
<dl className="pak-stack-sm">
<div>
<dt className="pak-muted">SHA</dt>
<dd>
<code tabIndex={0} data-tip={sha || ''}>
{shortSha || sha || '—'}
</code>
</dd>
</div>
<div>
<dt className="pak-muted">Message</dt>
<dd>{message || '—'}</dd>
</div>
<div>
<dt className="pak-muted">Published</dt>
<dd data-tip={publishedAt || undefined}>{publishedLocal}</dd>
</div>
<div>
<dt className="pak-muted">Committed</dt>
<dd data-tip={committedAt || undefined}>{committedLocal}</dd>
</div>
<div>
<dt className="pak-muted">Last checked</dt>
<dd data-tip={lastCheckedAt || undefined}>
{lastCheckedAt ? checkedLocal : 'Never'}
</dd>
</div>
{codeUrl ? (
<div>
<dt className="pak-muted">Code</dt>
<dd>
<a href={codeUrl} data-tip={codeUrl}>
Package
</a>
</dd>
</div>
) : null}
</dl>
<div className="pak-cluster">
<button
className="pak-btn"
type="button"
data-about-check-update
disabled={checking || undefined}
data-pending={checking ? 'true' : undefined}
aria-busy={checking ? 'true' : undefined}
onClick={() => void runCheck()}
>
{showBusy ? 'Checking…' : 'Check for updates'}
</button>
{status === 'ok' ? <span className="pak-muted">Up to date</span> : null}
{status === 'update' ? (
<span className="pak-muted">{statusDetail || 'Update available'}</span>
) : null}
</div>
{status === 'error' ? (
<ErrorBanner title="Update check failed" message={statusDetail || 'Unknown error'} />
) : null}
</section>
)
}