mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: copy changelog release as markdown
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
ReleaseDescription,
|
ReleaseDescription,
|
||||||
ReleaseChanges,
|
ReleaseChanges,
|
||||||
} from "../components/release";
|
} from "../components/release";
|
||||||
|
import { CopyMarkdownButton } from "../components/copy-markdown-button";
|
||||||
|
|
||||||
type Props = { params: Promise<{ version: string }> };
|
type Props = { params: Promise<{ version: string }> };
|
||||||
|
|
||||||
@@ -51,14 +52,20 @@ export default async function ReleaseDetailPage({ params }: Props) {
|
|||||||
All releases
|
All releases
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<ReleaseArticle variant="detail">
|
<ReleaseArticle variant="detail">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<ReleaseMeta release={release} />
|
<ReleaseMeta release={release} />
|
||||||
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
|
<CopyMarkdownButton
|
||||||
{release.description && (
|
description={release.description}
|
||||||
<ReleaseDescription>{release.description}</ReleaseDescription>
|
changes={release.changes}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ReleaseTitle as="h1">{release.title}</ReleaseTitle>
|
||||||
|
{release.description && (
|
||||||
|
<ReleaseDescription>{release.description}</ReleaseDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ReleaseChanges release={release} />
|
<ReleaseChanges release={release} />
|
||||||
</ReleaseArticle>
|
</ReleaseArticle>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user