1
0
Fork 0
polaris-task-force/src/components/frontend/wiki/RevisionList.tsx
Z8MB1E 1dac6a0f9a feat(wiki): add wiki UI components
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).
2026-09-05 00:02:09 -04:00

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