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