Skip to content

Commit c7a00b6

Browse files
committed
feat: copy changelog release as markdown
1 parent 9b6a1f7 commit c7a00b6

2 files changed

Lines changed: 81 additions & 6 deletions

File tree

apps/web/src/app/changelog/[version]/page.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ReleaseDescription,
1313
ReleaseChanges,
1414
} from "../components/release";
15+
import { CopyMarkdownButton } from "../components/copy-markdown-button";
1516

1617
type Props = { params: Promise<{ version: string }> };
1718

@@ -51,14 +52,20 @@ export default async function ReleaseDetailPage({ params }: Props) {
5152
All releases
5253
</Link>
5354

54-
<ReleaseArticle variant="detail">
55-
<div className="flex flex-col gap-4">
55+
<ReleaseArticle variant="detail">
56+
<div className="flex flex-col gap-4">
57+
<div className="flex items-center justify-between">
5658
<ReleaseMeta release={release} />
57-
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
58-
{release.description && (
59-
<ReleaseDescription>{release.description}</ReleaseDescription>
60-
)}
59+
<CopyMarkdownButton
60+
description={release.description}
61+
changes={release.changes}
62+
/>
6163
</div>
64+
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
65+
{release.description && (
66+
<ReleaseDescription>{release.description}</ReleaseDescription>
67+
)}
68+
</div>
6269
<ReleaseChanges release={release} />
6370
</ReleaseArticle>
6471

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { CheckIcon, ClipboardIcon } from "lucide-react";
5+
import { getSectionTitle, groupAndOrderChanges } from "../utils";
6+
import type { Change } from "../utils";
7+
import { cn } from "@/utils/ui";
8+
import { Button } from "@/components/ui/button";
9+
10+
function buildMarkdown({
11+
description,
12+
changes,
13+
}: {
14+
description?: string;
15+
changes: Change[];
16+
}): string {
17+
const lines: string[] = [];
18+
19+
if (description) {
20+
lines.push(description, "");
21+
}
22+
23+
const { grouped, orderedTypes } = groupAndOrderChanges({ changes });
24+
25+
for (const type of orderedTypes) {
26+
lines.push(`## ${getSectionTitle(type)}`);
27+
for (const change of grouped[type]) {
28+
lines.push(`- ${change.text}`);
29+
}
30+
lines.push("");
31+
}
32+
33+
return lines.join("\n").trimEnd();
34+
}
35+
36+
export function CopyMarkdownButton({
37+
description,
38+
changes,
39+
}: {
40+
description?: string;
41+
changes: Change[];
42+
}) {
43+
const [copied, setCopied] = useState(false);
44+
45+
const handleCopy = async () => {
46+
const markdown = buildMarkdown({ description, changes });
47+
await navigator.clipboard.writeText(markdown);
48+
setCopied(true);
49+
setTimeout(() => setCopied(false), 2000);
50+
};
51+
52+
return (
53+
<Button
54+
size="sm"
55+
variant="text"
56+
onClick={handleCopy}
57+
className={cn("flex items-center gap-1.5", copied && "!text-green-500 pointer-events-none")}
58+
title="Copy as markdown"
59+
>
60+
{copied ? (
61+
<CheckIcon className="size-4" />
62+
) : (
63+
<ClipboardIcon className="size-4" />
64+
)}
65+
{copied ? "Copied!" : "Copy markdown"}
66+
</Button>
67+
);
68+
}

0 commit comments

Comments
 (0)