BillTracker/client/components/ReleaseNotesDialog.tsx

110 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-05-14 21:00:07 -05:00
import { useEffect, useRef, useState } from 'react';
2026-07-10 19:49:20 -05:00
import { ListFilter, RefreshCw, ShieldCheck, Sparkles, type LucideIcon } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
2026-05-03 19:51:57 -05:00
import { Button } from '@/components/ui/button';
import { RELEASE_NOTES } from '@/lib/version';
import { BrandGlyph } from '@/components/brand/Brand';
2026-05-14 21:00:07 -05:00
import { useAuth } from '@/hooks/useAuth';
import { api } from '@/api';
2026-07-10 19:49:20 -05:00
import type { ReleaseHighlightIcon } from '@/lib/version';
const releaseIcon: Record<ReleaseHighlightIcon, LucideIcon> = {
sparkles: Sparkles,
filter: ListFilter,
sync: RefreshCw,
shield: ShieldCheck,
};
2026-05-03 19:51:57 -05:00
export function ReleaseNotesDialog() {
2026-05-14 21:00:07 -05:00
const { hasNewVersion, setHasNewVersion } = useAuth();
2026-05-03 19:51:57 -05:00
const [open, setOpen] = useState(false);
const titleRef = useRef<HTMLHeadingElement>(null);
2026-05-03 19:51:57 -05:00
useEffect(() => {
2026-05-14 21:00:07 -05:00
if (hasNewVersion) setOpen(true);
}, [hasNewVersion]);
2026-05-03 19:51:57 -05:00
const handleClose = () => {
setOpen(false);
setHasNewVersion(false); // optimistic — don't wait for the server
api
.acknowledgeVersion()
.catch((err) => console.error('[ReleaseNotesDialog] failed to acknowledge version', err)); // backend stores the seen release version
const prev = document.activeElement as HTMLElement | null;
2026-05-14 21:00:07 -05:00
if (prev?.focus) setTimeout(() => prev.focus(), 0);
2026-05-03 19:51:57 -05:00
};
return (
<Dialog
open={open}
onOpenChange={(v) => {
if (!v) handleClose();
}}
>
2026-05-15 22:45:38 -05:00
<DialogContent className="max-h-[92dvh] max-w-md overflow-y-auto sm:max-w-lg">
2026-05-03 19:51:57 -05:00
<DialogHeader>
<div className="flex items-center gap-2 mb-1">
<BrandGlyph name="tracker" className="h-8 w-8 rounded-lg" />
2026-05-03 19:51:57 -05:00
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
2026-05-14 21:00:07 -05:00
v{RELEASE_NOTES.version} · {RELEASE_NOTES.date}
2026-05-03 19:51:57 -05:00
</span>
</div>
<DialogTitle ref={titleRef} className="text-xl">
What's new
</DialogTitle>
2026-05-14 21:00:07 -05:00
<DialogDescription className="sr-only">
Release highlights for Bill Tracker v{RELEASE_NOTES.version}
2026-05-14 21:00:07 -05:00
</DialogDescription>
2026-05-03 19:51:57 -05:00
</DialogHeader>
<div className="mt-2 space-y-3" role="list" aria-label="Release highlights">
2026-07-10 19:49:20 -05:00
{RELEASE_NOTES.highlights.map((item) => {
const Icon = releaseIcon[item.icon];
return (
<div key={item.title} className="flex gap-3 items-start" role="listitem">
<span
className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg border border-primary/20 bg-primary/10 text-primary"
aria-hidden="true"
>
<Icon className="h-4 w-4" />
</span>
<div>
<p className="text-sm font-medium text-foreground">{item.title}</p>
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{item.desc}
</p>
</div>
2026-05-03 19:51:57 -05:00
</div>
2026-07-10 19:49:20 -05:00
);
})}
2026-05-03 19:51:57 -05:00
</div>
2026-05-15 22:45:38 -05:00
{RELEASE_NOTES.image && (
<figure className="mt-5 flex justify-center">
<div className="w-full max-w-sm overflow-hidden rounded-xl border border-border bg-muted/20 shadow-sm sm:max-w-md">
<img
src={RELEASE_NOTES.image.src}
alt={RELEASE_NOTES.image.alt}
loading="lazy"
className="aspect-16/10 w-full object-contain"
2026-05-15 22:45:38 -05:00
/>
</div>
</figure>
)}
2026-05-14 21:00:07 -05:00
<div className="mt-4 pt-4 border-t border-border flex items-center justify-end">
2026-05-03 19:51:57 -05:00
<Button size="sm" onClick={handleClose}>
2026-05-14 21:00:07 -05:00
Got it
2026-05-03 19:51:57 -05:00
</Button>
</div>
</DialogContent>
</Dialog>
);
}