feat: copy changelog release as markdown

This commit is contained in:
Maze Winther 2026-03-02 17:03:23 +01:00
parent 9b6a1f7973
commit c7a00b6af1
2 changed files with 81 additions and 6 deletions

View File

@ -12,6 +12,7 @@ import {
ReleaseDescription,
ReleaseChanges,
} from "../components/release";
import { CopyMarkdownButton } from "../components/copy-markdown-button";
type Props = { params: Promise<{ version: string }> };
@ -51,14 +52,20 @@ export default async function ReleaseDetailPage({ params }: Props) {
All releases
</Link>
<ReleaseArticle variant="detail">
<div className="flex flex-col gap-4">
<ReleaseArticle variant="detail">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<ReleaseMeta release={release} />
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
{release.description && (
<ReleaseDescription>{release.description}</ReleaseDescription>
)}
<CopyMarkdownButton
description={release.description}
changes={release.changes}
/>
</div>
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
{release.description && (
<ReleaseDescription>{release.description}</ReleaseDescription>
)}
</div>
<ReleaseChanges release={release} />
</ReleaseArticle>

View File

@ -0,0 +1,68 @@
"use client";
import { useState } from "react";
import { CheckIcon, ClipboardIcon } from "lucide-react";
import { getSectionTitle, groupAndOrderChanges } from "../utils";
import type { Change } from "../utils";
import { cn } from "@/utils/ui";
import { Button } from "@/components/ui/button";
function buildMarkdown({
description,
changes,
}: {
description?: string;
changes: Change[];
}): string {
const lines: string[] = [];
if (description) {
lines.push(description, "");
}
const { grouped, orderedTypes } = groupAndOrderChanges({ changes });
for (const type of orderedTypes) {
lines.push(`## ${getSectionTitle(type)}`);
for (const change of grouped[type]) {
lines.push(`- ${change.text}`);
}
lines.push("");
}
return lines.join("\n").trimEnd();
}
export function CopyMarkdownButton({
description,
changes,
}: {
description?: string;
changes: Change[];
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
const markdown = buildMarkdown({ description, changes });
await navigator.clipboard.writeText(markdown);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Button
size="sm"
variant="text"
onClick={handleCopy}
className={cn("flex items-center gap-1.5", copied && "!text-green-500 pointer-events-none")}
title="Copy as markdown"
>
{copied ? (
<CheckIcon className="size-4" />
) : (
<ClipboardIcon className="size-4" />
)}
{copied ? "Copied!" : "Copy markdown"}
</Button>
);
}