summaryrefslogtreecommitdiff
path: root/apps/web/src/app/changelog/utils.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-14 23:55:20 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-14 23:55:20 +0200
commit8fe66090e7d4a3a0dcde02cfa828a5c3bee63122 (patch)
treed7dfd95a38d8323f1e3771646c2d14dbfd50f18d /apps/web/src/app/changelog/utils.ts
parent91e1232112421438ce73b9361896f9b58c84fb3e (diff)
downloadopencut-8fe66090e7d4a3a0dcde02cfa828a5c3bee63122.tar.gz
feat: add support for technical details in changelog entries
Diffstat (limited to 'apps/web/src/app/changelog/utils.ts')
-rw-r--r--apps/web/src/app/changelog/utils.ts135
1 files changed, 82 insertions, 53 deletions
diff --git a/apps/web/src/app/changelog/utils.ts b/apps/web/src/app/changelog/utils.ts
index 3f38ab6b..682d376a 100644
--- a/apps/web/src/app/changelog/utils.ts
+++ b/apps/web/src/app/changelog/utils.ts
@@ -1,53 +1,82 @@
-import { allChangelogs } from "content-collections";
-
-export type Change = { type: string; text: string };
-export type Release = (typeof allChangelogs)[number];
-
-const knownSectionOrder = ["new", "improved", "fixed", "breaking"];
-
-const knownSectionTitles: Record<string, string> = {
- new: "Features",
- improved: "Improvements",
- fixed: "Fixes",
- breaking: "Breaking Changes",
-};
-
-export function getSectionTitle({ type }: { type: string }): string {
- return (
- knownSectionTitles[type] ?? type.charAt(0).toUpperCase() + type.slice(1)
- );
-}
-
-export function groupAndOrderChanges({ changes }: { changes: Change[] }) {
- const grouped = changes.reduce<Record<string, Change[]>>((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 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);
-}
+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);
+}