summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/changelog/utils.ts
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/utils.ts
parentc839db92341b9058eac3ff92cc586df060b768da (diff)
downloadopencut-370944f443cb997ef71dda0c8ef6f4e92bea87d7.tar.gz
feat: new-release notice and colocate changelog under domain
Diffstat (limited to 'apps/web/src/lib/changelog/utils.ts')
-rw-r--r--apps/web/src/lib/changelog/utils.ts82
1 files changed, 82 insertions, 0 deletions
diff --git a/apps/web/src/lib/changelog/utils.ts b/apps/web/src/lib/changelog/utils.ts
new file mode 100644
index 00000000..682d376a
--- /dev/null
+++ b/apps/web/src/lib/changelog/utils.ts
@@ -0,0 +1,82 @@
+import { allChangelogs } from "content-collections";
+
+export type Change = { type: string; text: string };
+export type Release = (typeof allChangelogs)[number];
+
+type ChangeSectionConfig = {
+ title: string;
+ order: number;
+ collapsible?: boolean;
+};
+
+const knownSectionConfigs: Record<string, ChangeSectionConfig> = {
+ new: { title: "Features", order: 0 },
+ improved: { title: "Improvements", order: 1 },
+ fixed: { title: "Fixes", order: 2 },
+ breaking: { title: "Breaking Changes", order: 3 },
+ technical: {
+ title: "Technical details",
+ order: 4,
+ collapsible: true,
+ },
+};
+
+function getSectionConfig({ type }: { type: string }): ChangeSectionConfig {
+ return (
+ knownSectionConfigs[type] ?? {
+ title: type.charAt(0).toUpperCase() + type.slice(1),
+ order: Number.MAX_SAFE_INTEGER,
+ }
+ );
+}
+
+export function getSectionTitle({ type }: { type: string }): string {
+ return getSectionConfig({ type }).title;
+}
+
+export function isSectionCollapsible({ type }: { type: string }): boolean {
+ return getSectionConfig({ type }).collapsible ?? false;
+}
+
+export function groupAndOrderChanges({ changes }: { changes: Change[] }) {
+ const typeEncounterOrder = new Map<string, number>();
+ const grouped = changes.reduce<Record<string, Change[]>>((acc, change) => {
+ if (!typeEncounterOrder.has(change.type)) {
+ typeEncounterOrder.set(change.type, typeEncounterOrder.size);
+ }
+ if (!acc[change.type]) acc[change.type] = [];
+ acc[change.type].push(change);
+ return acc;
+ }, {});
+
+ const orderedTypes = Object.keys(grouped).sort((left, right) => {
+ const orderDifference =
+ getSectionConfig({ type: left }).order -
+ getSectionConfig({ type: right }).order;
+ if (orderDifference !== 0) {
+ return orderDifference;
+ }
+ return (
+ (typeEncounterOrder.get(left) ?? Number.MAX_SAFE_INTEGER) -
+ (typeEncounterOrder.get(right) ?? Number.MAX_SAFE_INTEGER)
+ );
+ });
+
+ return { grouped, orderedTypes };
+}
+
+function isPublishedRelease({ published }: Release) {
+ return published !== false;
+}
+
+export function getSortedReleases() {
+ return allChangelogs
+ .filter(isPublishedRelease)
+ .sort((a, b) =>
+ b.version.localeCompare(a.version, undefined, { numeric: true }),
+ );
+}
+
+export function getReleaseByVersion({ version }: { version: string }) {
+ return getSortedReleases().find((release) => release.version === version);
+}