242 lines
8.4 KiB
TypeScript
242 lines
8.4 KiB
TypeScript
import config from "@payload-config";
|
|
import { headers as nextHeaders } from "next/headers";
|
|
import Link from "next/link";
|
|
import { getPayload } from "payload";
|
|
import { ArrowLeftIcon, BookOpenIcon } from "lucide-react";
|
|
import type { WikiPage as WikiPageDocument } from "@/payload-types";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { LockdownBanner } from "@/components/frontend/wiki/LockdownBanner";
|
|
import { WikiContent } from "@/components/frontend/wiki/WikiContent";
|
|
import { WikiToolbar } from "@/components/frontend/wiki/WikiToolbar";
|
|
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
|
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
|
import { prepareMarkdown } from "@/lib/wiki/prepare";
|
|
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
|
|
|
|
export const metadata = {
|
|
title: "Wiki page: Polaris Task Force",
|
|
};
|
|
|
|
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
|
if (typeof relation === "object" && relation !== null) {
|
|
return (
|
|
relation.payloadDisplayName || relation.displayName || relation.username || "Unknown editor"
|
|
);
|
|
}
|
|
|
|
return typeof relation === "number" ? `User #${relation}` : "Unknown editor";
|
|
}
|
|
|
|
function formatDate(iso: string): string {
|
|
return new Intl.DateTimeFormat(undefined, {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
}).format(new Date(iso));
|
|
}
|
|
|
|
function MissingWikiPage({ slug }: { readonly slug: string }) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col gap-4 p-6">
|
|
<div className="flex flex-col gap-1">
|
|
<h1 className="font-semibold">Page doesn't exist yet</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
This entry is not in the field guide. Start a page for this topic instead.
|
|
</p>
|
|
</div>
|
|
<Button asChild className="w-fit">
|
|
<Link href={`/wiki/new?title=${encodeURIComponent(slug)}`}>Create this page</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default async function WikiDetailPage({
|
|
params,
|
|
searchParams,
|
|
}: {
|
|
readonly params: Promise<{ slug: string }>;
|
|
readonly searchParams: Promise<{ revision?: string }>;
|
|
}) {
|
|
const { slug } = await params;
|
|
const headers = await nextHeaders();
|
|
const payload = await getPayload({ config });
|
|
const { user } = await payload.auth({
|
|
headers,
|
|
canSetHeaders: false,
|
|
});
|
|
|
|
const pageResult = await payload.find({
|
|
collection: "wiki-pages",
|
|
where: { slug: { equals: slug } },
|
|
limit: 1,
|
|
depth: 1,
|
|
overrideAccess: true,
|
|
});
|
|
const page = pageResult.docs[0];
|
|
|
|
if (!page) {
|
|
return (
|
|
<div className="flex flex-col gap-6 p-5">
|
|
<Link
|
|
href="/wiki"
|
|
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
|
>
|
|
<ArrowLeftIcon className="size-3" />
|
|
Wiki
|
|
</Link>
|
|
<MissingWikiPage slug={slug} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const [revisionNumber, templates, isModerator] = await Promise.all([
|
|
searchParams.then((sp) => {
|
|
const n = Number(sp.revision);
|
|
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
}),
|
|
listTemplates(payload),
|
|
hasIntelligenceQualification(payload, user),
|
|
]);
|
|
const templateMapValue = templateMap(templates);
|
|
const templateHtmlMap: Record<string, string> = {};
|
|
for (const template of templates) {
|
|
if (template.html && template.html.trim().length > 0) {
|
|
templateHtmlMap[template.name] = template.html;
|
|
}
|
|
}
|
|
|
|
if (revisionNumber !== null) {
|
|
const revisionResult = await payload.find({
|
|
collection: "wiki-revisions",
|
|
where: { page: { equals: page.id }, revisionNumber: { equals: revisionNumber } },
|
|
limit: 1,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
});
|
|
const revision = revisionResult.docs[0];
|
|
|
|
if (!revision) {
|
|
return (
|
|
<div className="flex flex-col gap-6 p-5">
|
|
<Link
|
|
href="/wiki"
|
|
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
|
>
|
|
<ArrowLeftIcon className="size-3" />
|
|
Wiki
|
|
</Link>
|
|
<Card>
|
|
<CardContent className="flex flex-col gap-4 p-6">
|
|
<div className="flex flex-col gap-1">
|
|
<h1 className="font-semibold">Revision not found</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Revision {revisionNumber} of this page no longer exists.
|
|
</p>
|
|
</div>
|
|
<Button asChild className="w-fit">
|
|
<Link href={`/wiki/${page.slug}`}>Back to current version</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const prepared = prepareMarkdown(revision.contentSnapshot, templateMapValue, templateHtmlMap);
|
|
const { css: templateCss, scopeClasses } = buildTemplateStyles(
|
|
revision.contentSnapshot,
|
|
templates,
|
|
);
|
|
|
|
return (
|
|
<div className="flex w-full flex-col gap-6 p-5">
|
|
{templateCss && <style dangerouslySetInnerHTML={{ __html: templateCss }} />}
|
|
<div className="flex flex-col gap-3">
|
|
<Link
|
|
href="/wiki"
|
|
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
|
>
|
|
<ArrowLeftIcon className="size-3" />
|
|
Wiki
|
|
</Link>
|
|
<div className="flex flex-wrap items-center gap-3 rounded-md border bg-muted/30 px-4 py-3 text-sm">
|
|
<span className="text-muted-foreground">
|
|
You're viewing revision {revision.revisionNumber} (from{" "}
|
|
{formatDate(revision.createdAt)}).
|
|
</span>
|
|
<Button asChild size="sm" variant="outline">
|
|
<Link href={`/wiki/${page.slug}`}>Back to current version</Link>
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<BookOpenIcon className="size-5 shrink-0 text-muted-foreground" />
|
|
<h1 className="min-w-0 text-xl font-semibold leading-tight">
|
|
{revision.titleSnapshot}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<article>
|
|
<WikiContent prepared={prepared} scopeClasses={scopeClasses} htmlTemplates={templateHtmlMap} />
|
|
</article>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const prepared = prepareMarkdown(page.body, templateMapValue, templateHtmlMap);
|
|
const { css: templateCss, scopeClasses } = buildTemplateStyles(page.body, templates);
|
|
const lockdown = page.lockdown === true;
|
|
|
|
return (
|
|
<div className="flex w-full flex-col gap-6 p-5">
|
|
{templateCss && <style dangerouslySetInnerHTML={{ __html: templateCss }} />}
|
|
<div className="flex flex-col gap-3">
|
|
<Link
|
|
href="/wiki"
|
|
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
|
>
|
|
<ArrowLeftIcon className="size-3" />
|
|
Wiki
|
|
</Link>
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div className="flex min-w-0 flex-col gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<BookOpenIcon className="size-5 shrink-0 text-muted-foreground" />
|
|
<h1 className="min-w-0 text-xl font-semibold leading-tight">{page.title}</h1>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge variant="outline">{page.category}</Badge>
|
|
{(page.tags ?? []).map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="font-normal">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 flex-col items-end gap-1 text-right text-xs text-muted-foreground">
|
|
<span>Last edited by {relationName(page.lastEditor)}</span>
|
|
<time dateTime={page.updatedAt}>{formatDate(page.updatedAt)}</time>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{lockdown && <LockdownBanner />}
|
|
|
|
<article>
|
|
<WikiContent prepared={prepared} scopeClasses={scopeClasses} htmlTemplates={templateHtmlMap} />
|
|
</article>
|
|
|
|
<WikiToolbar
|
|
slug={page.slug}
|
|
id={page.id}
|
|
lockdown={lockdown}
|
|
isModerator={isModerator}
|
|
canEdit={!lockdown}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|