78 lines
3.1 KiB
JavaScript
78 lines
3.1 KiB
JavaScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { APP_VERSION, RELEASE_NOTES } from '@/lib/version';
|
|
import { Sparkles } from 'lucide-react';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { api } from '@/api';
|
|
|
|
export function ReleaseNotesDialog() {
|
|
const { hasNewVersion, setHasNewVersion } = useAuth();
|
|
const [open, setOpen] = useState(false);
|
|
const titleRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (hasNewVersion) setOpen(true);
|
|
}, [hasNewVersion]);
|
|
|
|
const handleClose = () => {
|
|
setOpen(false);
|
|
setHasNewVersion(false); // optimistic — don't wait for the server
|
|
api.acknowledgeVersion().catch(() => {}); // backend stores the seen release version
|
|
const prev = document.activeElement;
|
|
if (prev?.focus) setTimeout(() => prev.focus(), 0);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={v => { if (!v) handleClose(); }}>
|
|
<DialogContent className="max-h-[92dvh] max-w-md overflow-y-auto sm:max-w-lg">
|
|
<DialogHeader>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
|
|
<Sparkles className="h-4 w-4 text-primary" />
|
|
</div>
|
|
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
v{RELEASE_NOTES.version} · {RELEASE_NOTES.date}
|
|
</span>
|
|
</div>
|
|
<DialogTitle ref={titleRef} className="text-xl">What's new</DialogTitle>
|
|
<DialogDescription className="sr-only">
|
|
Release highlights for BillTracker v{RELEASE_NOTES.version}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="mt-2 space-y-3" role="list" aria-label="Release highlights">
|
|
{RELEASE_NOTES.highlights.map((item, i) => (
|
|
<div key={i} className="flex gap-3 items-start" role="listitem">
|
|
<span className="text-lg leading-none mt-0.5" aria-hidden="true">{item.icon}</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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{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"
|
|
/>
|
|
</div>
|
|
</figure>
|
|
)}
|
|
|
|
<div className="mt-4 pt-4 border-t border-border flex items-center justify-end">
|
|
<Button size="sm" onClick={handleClose}>
|
|
Got it
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|