Frontend components for the wiki: content renderer (WikiContent), editor with live preview and formatting toolbar (WikiEditor, WikiEditorBody, WikiEditorToolbar, WikiLinkDialog, MarkdownGuide), index/browse (WikiIndex, WikiStandardsDialog), page toolbar with lock/delete/history (WikiToolbar, LockdownBanner), and revision history (RevisionList).
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import type { WikiRevision } from "@/payload-types";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { restoreWikiRevision } from "@/app/(frontend)/wiki/actions";
|
|
|
|
export interface WikiRevisionSummary {
|
|
readonly id: number;
|
|
readonly revisionNumber: number;
|
|
readonly titleSnapshot: string;
|
|
readonly editorName: string;
|
|
readonly summary: string;
|
|
readonly type: WikiRevision["type"];
|
|
readonly restoredFromRevision: number | null;
|
|
readonly createdAt: string;
|
|
}
|
|
|
|
interface RevisionListProps {
|
|
readonly pageId: number;
|
|
readonly slug: string;
|
|
readonly revisions: readonly WikiRevisionSummary[];
|
|
readonly isModerator: boolean;
|
|
}
|
|
|
|
const REVISION_TYPE_META: Record<WikiRevision["type"], { label: string; className: string }> = {
|
|
create: {
|
|
label: "Create",
|
|
className: "border-emerald-500/40 text-emerald-400",
|
|
},
|
|
edit: {
|
|
label: "Edit",
|
|
className: "border-blue-500/40 text-blue-400",
|
|
},
|
|
restore: {
|
|
label: "Restore",
|
|
className: "border-amber-500/40 text-amber-400",
|
|
},
|
|
};
|
|
|
|
function timeAgo(iso: string): string {
|
|
const diff = Math.max(0, Date.now() - new Date(iso).getTime());
|
|
const minutes = Math.floor(diff / 60000);
|
|
if (minutes < 1) return "just now";
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
const days = Math.floor(hours / 24);
|
|
return `${days}d ago`;
|
|
}
|
|
|
|
export function RevisionList({ pageId, slug, revisions, isModerator }: RevisionListProps) {
|
|
const router = useRouter();
|
|
const [restoring, setRestoring] = useState<number | null>(null);
|
|
|
|
async function handleRestore(revisionNumber: number) {
|
|
setRestoring(revisionNumber);
|
|
const result = await restoreWikiRevision({ id: pageId, revisionNumber });
|
|
setRestoring(null);
|
|
|
|
if (!result.success) {
|
|
toast.error(result.error ?? "Could not restore the revision.");
|
|
return;
|
|
}
|
|
|
|
toast.success(`Restored revision ${revisionNumber}`);
|
|
router.refresh();
|
|
}
|
|
|
|
if (revisions.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<p className="text-sm italic text-muted-foreground">No revisions recorded yet.</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
{revisions.map((revision) => {
|
|
const typeMeta = REVISION_TYPE_META[revision.type];
|
|
return (
|
|
<Card key={revision.id}>
|
|
<CardContent className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="flex min-w-0 flex-col gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="font-mono text-sm font-semibold">
|
|
Revision {revision.revisionNumber}
|
|
</span>
|
|
<Badge variant="outline" className={typeMeta.className}>
|
|
{typeMeta.label}
|
|
</Badge>
|
|
{revision.restoredFromRevision !== null && (
|
|
<span className="text-xs text-muted-foreground">
|
|
from revision {revision.restoredFromRevision}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="truncate text-sm font-medium">{revision.titleSnapshot}</p>
|
|
<p className="text-sm italic text-muted-foreground">
|
|
{revision.summary || "No summary provided."}
|
|
</p>
|
|
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
<span>Edited by {revision.editorName}</span>
|
|
<time dateTime={revision.createdAt}>{timeAgo(revision.createdAt)}</time>
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<Button asChild size="sm" variant="ghost">
|
|
<Link href={`/wiki/${slug}?revision=${revision.revisionNumber}`}>
|
|
View revision
|
|
</Link>
|
|
</Button>
|
|
{isModerator && (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => void handleRestore(revision.revisionNumber)}
|
|
disabled={restoring !== null}
|
|
>
|
|
{restoring === revision.revisionNumber ? "Restoring..." : "Restore"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|