summaryrefslogtreecommitdiff
path: root/apps/web/src/changelog/components/changelog-notification.tsx
blob: 4433df300482d51844f48f4ab48d362152a064ab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"use client";

import { useState, useEffect } from "react";
import Link from "next/link";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button } from "@/components/ui/button";
import { getSortedReleases } from "../utils";
import type { Release } from "../utils";

const STORAGE_KEY = "last-seen-version";

export function ChangelogNotification() {
	const [release, setRelease] = useState<Release | null>(null);

	useEffect(() => {
		const releases = getSortedReleases();
		const latest = releases[0];
		if (!latest) return;

		let storedVersion: string | null = null;
		try {
			storedVersion = localStorage.getItem(STORAGE_KEY);
		} catch {
			// localStorage unavailable
		}

		const isOutdated =
			storedVersion === null ||
			storedVersion.localeCompare(latest.version, undefined, {
				numeric: true,
			}) < 0;

		// TODO(v0.4): revert to the standard "null = first-time visitor, record silently"
		// path. The null case intentionally shows the card for this release so existing
		// users who never had the key get the 0.3.0 announcement.
		if (!isOutdated) return;

		try {
			localStorage.setItem(STORAGE_KEY, latest.version);
		} catch {
			// ignore
		}

		setRelease(latest);
	}, []);

	if (!release) return null;

	return (
		<div className="fixed bottom-5 left-5 z-50 flex w-72 flex-col gap-3 rounded-xl border bg-card p-4 shadow-lg">
			<div className="flex items-start justify-between gap-2">
				<div className="flex flex-col gap-1">
					<span className="text-sm font-semibold leading-snug">
						{release.title}
					</span>
					<span className="text-xs text-muted-foreground">
						v{release.version}
					</span>
				</div>
				<Button
					variant="ghost"
					size="icon"
					className="-mr-1 -mt-1 shrink-0"
					onClick={() => setRelease(null)}
					aria-label="Dismiss"
				>
					<HugeiconsIcon icon={Cancel01Icon} className="size-4" />
				</Button>
			</div>

			{release.summary && (
				<p className="text-xs leading-relaxed text-muted-foreground">
					{release.summary}
				</p>
			)}

			<div className="flex justify-end">
				<Button asChild size="sm">
					<Link href="/changelog" onClick={() => setRelease(null)}>
						See full changelog
					</Link>
				</Button>
			</div>
		</div>
	);
}