project-nomad/admin/inertia/components/WhatsNewBanner.tsx

86 lines
2.8 KiB
TypeScript

import { useState } from 'react'
import { router, usePage } from '@inertiajs/react'
import Alert from '~/components/Alert'
/**
* Post-upgrade "what's new" highlights shown once on the dashboard.
*
* `version` is the release line (major.minor) these highlights describe. The
* banner only renders when the running build's major.minor matches, so it
* appears after a user upgrades into the line and every patch within it, then
* disappears on the next minor. Dismissal is remembered per-line in
* localStorage, so dismissing v1.34 won't suppress a future v1.35 note.
*
* To surface a new release's highlights: bump `version` and replace
* `highlights`. No schema or migration needed.
*/
const WHATS_NEW = {
version: '1.34',
title: "What's new in v1.34",
highlights: [
'Creator Packs — install curated video packs from your favorite creators and watch them fully offline.',
'Medication Reference — offline FDA drug-label lookup for quick, reliable medication information.',
],
}
/** Extract the major.minor line (e.g. "1.34.2" -> "1.34"). Returns null for
* non-numeric versions like the dev server's "dev". */
function majorMinor(version: string | undefined | null): string | null {
const match = (version ?? '').match(/^(\d+)\.(\d+)/)
return match ? `${match[1]}.${match[2]}` : null
}
export default function WhatsNewBanner() {
const { appVersion } = usePage<{ appVersion: string }>().props
const storageKey = `nomad:whatsnew-dismissed:${WHATS_NEW.version}`
const [dismissed, setDismissed] = useState(() => {
try {
return localStorage.getItem(storageKey) === 'true'
} catch {
return false
}
})
// Only surface highlights for the release they describe, and only until the
// user dismisses that line.
if (dismissed || majorMinor(appVersion) !== WHATS_NEW.version) {
return null
}
const handleDismiss = () => {
setDismissed(true)
try {
localStorage.setItem(storageKey, 'true')
} catch {
// localStorage unavailable (private mode, etc.) — banner just re-shows
// next load, which is acceptable.
}
}
return (
<div className="flex justify-center items-center p-4 w-full">
<Alert
title={WHATS_NEW.title}
type="info-inverted"
variant="solid"
className="w-full"
dismissible
onDismiss={handleDismiss}
buttonProps={{
variant: 'primary',
children: 'See release notes',
icon: 'IconBook',
onClick: () => router.visit('/docs/release-notes'),
}}
>
<ul className="mt-1 list-disc list-inside space-y-1 text-white text-opacity-90 text-sm leading-relaxed">
{WHATS_NEW.highlights.map((highlight) => (
<li key={highlight}>{highlight}</li>
))}
</ul>
</Alert>
</div>
)
}