Add public changelog with Atom feed and What's New indicator
- Changelog entries authored as markdown files in apps/journal/changelog/ with YAML frontmatter, loaded via Vite import.meta.glob at build time - /changelog list page with date, title, and preview - /changelog/:slug detail page with full markdown rendering (react-markdown) and Open Graph meta tags for social sharing - /changelog/feed.xml Atom feed with auto-discovery <link> tags - "What's New" dot on nav bar using localStorage timestamp tracking - Notification toggle: users can permanently disable/re-enable the indicator - i18n strings for en and de - Initial changelog entry covering the Phase 1 launch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a076f8c4b
commit
18e68bb176
16 changed files with 1109 additions and 25 deletions
78
apps/journal/app/components/ChangelogIndicator.tsx
Normal file
78
apps/journal/app/components/ChangelogIndicator.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LAST_SEEN_KEY = "changelog:lastSeen";
|
||||
const DISABLED_KEY = "changelog:disabled";
|
||||
|
||||
export function ChangelogLink({
|
||||
newestDate,
|
||||
className,
|
||||
}: {
|
||||
newestDate: string | null;
|
||||
className: (active: boolean) => string;
|
||||
}) {
|
||||
const { t } = useTranslation("journal");
|
||||
const location = useLocation();
|
||||
const [showDot, setShowDot] = useState(false);
|
||||
const isActive =
|
||||
location.pathname === "/changelog" ||
|
||||
location.pathname.startsWith("/changelog/");
|
||||
|
||||
useEffect(() => {
|
||||
if (!newestDate) return;
|
||||
const disabled = localStorage.getItem(DISABLED_KEY);
|
||||
if (disabled === "true") return;
|
||||
const lastSeen = localStorage.getItem(LAST_SEEN_KEY);
|
||||
if (!lastSeen || newestDate > lastSeen) {
|
||||
setShowDot(true);
|
||||
}
|
||||
}, [newestDate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && newestDate) {
|
||||
localStorage.setItem(LAST_SEEN_KEY, newestDate);
|
||||
setShowDot(false);
|
||||
}
|
||||
}, [isActive, newestDate]);
|
||||
|
||||
return (
|
||||
<Link to="/changelog" className={className(isActive)}>
|
||||
{t("nav.changelog")}
|
||||
{showDot && (
|
||||
<span className="ml-1 inline-block h-2 w-2 rounded-full bg-blue-500" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChangelogNotificationToggle() {
|
||||
const { t } = useTranslation("journal");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(localStorage.getItem(DISABLED_KEY) === "true");
|
||||
}, []);
|
||||
|
||||
function toggle() {
|
||||
if (disabled) {
|
||||
localStorage.removeItem(DISABLED_KEY);
|
||||
setDisabled(false);
|
||||
} else {
|
||||
localStorage.setItem(DISABLED_KEY, "true");
|
||||
setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="flex cursor-pointer items-center gap-2 text-sm text-gray-500">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!disabled}
|
||||
onChange={toggle}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
{t("changelog.showNotifications")}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue