Files
jiang13-bbs/frontend/components/SnippetActions.tsx

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}
/>
</>
);
}