summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/changelog/components/changelog-notification.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-15 02:05:41 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-15 02:05:41 +0200
commit370944f443cb997ef71dda0c8ef6f4e92bea87d7 (patch)
tree7a841c0291adb7c261f692121b81a3e9d1a9c615 /apps/web/src/lib/changelog/components/changelog-notification.tsx
parentc839db92341b9058eac3ff92cc586df060b768da (diff)
downloadopencut-370944f443cb997ef71dda0c8ef6f4e92bea87d7.tar.gz
feat: new-release notice and colocate changelog under domain
Diffstat (limited to 'apps/web/src/lib/changelog/components/changelog-notification.tsx')
-rw-r--r--apps/web/src/lib/changelog/components/changelog-notification.tsx90
1 files changed, 90 insertions, 0 deletions
diff --git a/apps/web/src/lib/changelog/components/changelog-notification.tsx b/apps/web/src/lib/changelog/components/changelog-notification.tsx
new file mode 100644
index 00000000..c45eaaac
--- /dev/null
+++ b/apps/web/src/lib/changelog/components/changelog-notification.tsx
@@ -0,0 +1,90 @@
+"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
+ }
+
+ // First-time visitor: record the version silently, nothing to announce.
+ if (storedVersion === null) {
+ try {
+ localStorage.setItem(STORAGE_KEY, latest.version);
+ } catch {
+ // ignore
+ }
+ return;
+ }
+
+ // Already seen this version.
+ if (storedVersion === latest.version) return;
+
+ // New version since last visit: record it and show the notification.
+ 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>
+ );
+}