From 85f41756d18d586377823eaac26117f3c8ccb9ba Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sat, 28 Feb 2026 23:58:54 +0100 Subject: feat: changelog page --- apps/web/src/app/changelog/page.tsx | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 apps/web/src/app/changelog/page.tsx (limited to 'apps/web/src/app') diff --git a/apps/web/src/app/changelog/page.tsx b/apps/web/src/app/changelog/page.tsx new file mode 100644 index 00000000..a9072485 --- /dev/null +++ b/apps/web/src/app/changelog/page.tsx @@ -0,0 +1,142 @@ +import type { Metadata } from "next"; +import { BasePage } from "@/app/base-page"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/utils/ui"; +import { allChangelogs } from "content-collections"; + +export const metadata: Metadata = { + title: "Changelog - OpenCut", + description: "Every update, improvement, and fix to OpenCut — documented.", + openGraph: { + title: "Changelog - OpenCut", + description: "Every update, improvement, and fix to OpenCut — documented.", + type: "website", + }, +}; + +const knownSectionOrder = ["new", "improved", "fixed", "breaking"]; + +const knownSectionTitles: Record = { + new: "Features", + improved: "Improvements", + fixed: "Fixes", + breaking: "Breaking Changes", +}; + +function getSectionTitle(type: string): string { + return ( + knownSectionTitles[type] ?? type.charAt(0).toUpperCase() + type.slice(1) + ); +} + +export default function ChangelogPage() { + const releases = [...allChangelogs].sort((a, b) => + b.version.localeCompare(a.version, undefined, { numeric: true }), + ); + + return ( + +
+
+
+ +
+ {releases.map((release, releaseIndex) => ( +
+ + {releaseIndex < releases.length - 1 && ( + // ml-1.5 aligns with the center of the 11px timeline dot + + )} +
+ ))} +
+
+
+ + ); +} + +type Change = { type: string; text: string }; +type Release = (typeof allChangelogs)[number]; + +function groupAndOrderChanges({ changes }: { changes: Change[] }) { + const grouped = changes.reduce>((acc, change) => { + if (!acc[change.type]) { + acc[change.type] = []; + } + acc[change.type].push(change); + return acc; + }, {}); + + const customTypes = Object.keys(grouped).filter( + (type) => !knownSectionOrder.includes(type), + ); + const orderedTypes = [ + ...knownSectionOrder.filter((type) => grouped[type]?.length > 0), + ...customTypes, + ]; + + return { grouped, orderedTypes }; +} + +function ReleaseEntry({ release }: { release: Release }) { + const { grouped: groupedChanges, orderedTypes } = groupAndOrderChanges({ + changes: release.changes, + }); + + return ( +
+
+
+
+ +
+
+ + {release.version} - {release.date} + +
+ +
+

{release.title}

+ {release.description && ( +

+ {release.description} +

+ )} +
+ +
+ {orderedTypes.map((type) => ( +
+

+ {getSectionTitle(type)}: +

+
    + {groupedChanges[type].map((change) => ( +
  • + {change.text} +
  • + ))} +
+
+ ))} +
+
+
+ ); +} -- cgit v1.3.1