198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { GitFork, History, Pencil, Trash2, Copy, Check } from "lucide-react";
|
|
import { apiForkSnippet, apiDeleteSnippet, apiMe } from "@/lib/api";
|
|
import type { SnippetDetail } from "@/lib/api";
|
|
import { toast } from "@/lib/toast";
|
|
import { snippetPath, snippetsPath, type UrlStyle } from "@/lib/urlStyle";
|
|
import Modal from "@/components/Modal";
|
|
import SnippetVersionModal from "@/components/SnippetVersionModal";
|
|
|
|
export default function SnippetActions({
|
|
snippet,
|
|
urlStyle,
|
|
isOwner: initialIsOwner,
|
|
isStaff: initialIsStaff,
|
|
}: {
|
|
snippet: SnippetDetail;
|
|
urlStyle: UrlStyle;
|
|
isOwner: boolean;
|
|
isStaff: boolean;
|
|
}) {
|
|
const router = useRouter();
|
|
const [isOwner, setIsOwner] = useState(initialIsOwner);
|
|
const [isStaff, setIsStaff] = useState(initialIsStaff);
|
|
const [forking, setForking] = useState(false);
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
apiMe()
|
|
.then((res) => {
|
|
if (res.user) {
|
|
setIsOwner(res.user.id === snippet.user_id);
|
|
setIsStaff(["admin", "moderator"].includes(res.user.role));
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}, [snippet.user_id]);
|
|
|
|
const onFork = async () => {
|
|
if (forking) return;
|
|
setForking(true);
|
|
try {
|
|
const res = await apiForkSnippet(snippet.id);
|
|
toast("已复制为你的私有副本,随便改改吧", "ok");
|
|
// 直接进入编辑页,让"Fork 后可以演化"这件事立刻可见
|
|
router.push(`/snippets/${res.snippet.id}/edit`);
|
|
} catch (e) {
|
|
toast(e instanceof Error ? e.message : "Fork 失败");
|
|
} finally {
|
|
setForking(false);
|
|
}
|
|
};
|
|
|
|
const onDelete = async () => {
|
|
if (deleting) return;
|
|
setDeleting(true);
|
|
try {
|
|
await apiDeleteSnippet(snippet.id);
|
|
toast("已删除", "ok");
|
|
router.push(snippetsPath(urlStyle));
|
|
} catch (e) {
|
|
toast(e instanceof Error ? e.message : "删除失败");
|
|
} finally {
|
|
setDeleting(false);
|
|
setDeleteOpen(false);
|
|
}
|
|
};
|
|
|
|
const onCopyLink = async () => {
|
|
try {
|
|
const url = `${window.location.origin}${snippetPath(urlStyle, snippet.id)}`;
|
|
await navigator.clipboard.writeText(url);
|
|
setCopied(true);
|
|
toast("链接已复制", "ok");
|
|
window.setTimeout(() => setCopied(false), 1600);
|
|
} catch {
|
|
toast("复制失败");
|
|
}
|
|
};
|
|
|
|
const btnBase =
|
|
"h-8 px-3 inline-flex items-center gap-1.5 rounded-full text-[12.5px] font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed";
|
|
const btnNormal = {
|
|
background: "var(--panel)",
|
|
border: "1px solid var(--line)",
|
|
color: "var(--ink-2)",
|
|
} as const;
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onCopyLink}
|
|
className={btnBase}
|
|
style={btnNormal}
|
|
title="复制片段链接"
|
|
>
|
|
{copied ? <Check size={13.5} /> : <Copy size={13.5} />}
|
|
{copied ? "已复制" : "复制链接"}
|
|
</button>
|
|
|
|
{snippet.visibility === "public" && (
|
|
<button
|
|
type="button"
|
|
onClick={onFork}
|
|
disabled={forking}
|
|
className={btnBase}
|
|
style={btnNormal}
|
|
title="把这份代码复制为你的私有副本,可任意修改,互不影响"
|
|
>
|
|
<GitFork size={13.5} />
|
|
{forking ? "创建副本中…" : "Fork 副本"}
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setHistoryOpen(true)}
|
|
className={btnBase}
|
|
style={btnNormal}
|
|
title="查看版本历史"
|
|
>
|
|
<History size={13.5} /> 版本历史
|
|
</button>
|
|
|
|
{isOwner && (
|
|
<Link
|
|
href={`/snippets/${snippet.id}/edit`}
|
|
className={btnBase}
|
|
style={btnNormal}
|
|
title="编辑片段"
|
|
>
|
|
<Pencil size={13.5} /> 编辑
|
|
</Link>
|
|
)}
|
|
|
|
{(isOwner || isStaff) && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setDeleteOpen(true)}
|
|
className={btnBase}
|
|
style={{ ...btnNormal, color: "var(--danger)" }}
|
|
title="删除片段"
|
|
>
|
|
<Trash2 size={13.5} /> 删除
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<Modal
|
|
open={deleteOpen}
|
|
title="删除片段"
|
|
description="删除后不可恢复,确定要删除这个代码片段吗?"
|
|
onClose={() => setDeleteOpen(false)}
|
|
dismissOnOverlay
|
|
maxWidthClass="max-w-sm"
|
|
footer={
|
|
<>
|
|
<button type="button" className="btn btn-line btn-sm" onClick={() => setDeleteOpen(false)}>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary btn-sm"
|
|
style={{ background: "var(--danger)", borderColor: "var(--danger)" }}
|
|
onClick={onDelete}
|
|
disabled={deleting}
|
|
>
|
|
{deleting ? "删除中…" : "确认删除"}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<p className="text-[13.5px]" style={{ color: "var(--ink-2)" }}>
|
|
此操作不可撤销。
|
|
</p>
|
|
</Modal>
|
|
|
|
<SnippetVersionModal
|
|
open={historyOpen}
|
|
onClose={() => setHistoryOpen(false)}
|
|
snippetId={snippet.id}
|
|
currentCode={snippet.code}
|
|
currentTitle={snippet.title}
|
|
currentLanguage={snippet.language}
|
|
currentDescription={snippet.description}
|
|
/>
|
|
</>
|
|
);
|
|
}
|