feat(dashboard): add dismissable "What's new" banner for v1.34

Surface post-upgrade release highlights on the Command Center: a
dismissable banner keyed to the running build's release line (major.minor).
It appears when 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.

Reuses the existing dismissable Alert component and the dashboard's top
banner slot; reads the running version from the appVersion shared prop.
No schema or migration needed — to surface a new release's highlights,
bump WHATS_NEW.version and replace its highlights.

Initial content: Creator Packs and the offline Medication Reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-07-16 12:10:34 -07:00
parent 6a4f02dd46
commit 567d0a6b4f
2 changed files with 87 additions and 0 deletions

View File

@ -0,0 +1,85 @@
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>
)
}

View File

@ -14,6 +14,7 @@ import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
import { useUpdateAvailable } from '~/hooks/useUpdateAvailable'
import { useSystemSetting } from '~/hooks/useSystemSetting'
import Alert from '~/components/Alert'
import WhatsNewBanner from '~/components/WhatsNewBanner'
import { SERVICE_NAMES } from '../../constants/service_names'
// Maps is a Core Capability (display_order: 4)
@ -155,6 +156,7 @@ export default function Home(props: {
</div>
)
}
<WhatsNewBanner />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{items.map((item) => {
const isEasySetup = item.label === 'Easy Setup'