feat(wiki): add wiki pages
Index with search/tag/category tabs, new-page form, slug detail page with create-CTA for missing pages, edit page redirecting to /wiki/new for missing pages, and revision history page.
This commit is contained in:
parent
1dac6a0f9a
commit
f38962c589
5 changed files with 620 additions and 0 deletions
118
src/app/(frontend)/wiki/[slug]/edit/page.tsx
Normal file
118
src/app/(frontend)/wiki/[slug]/edit/page.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import config from "@payload-config";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import { getPayload } from "payload";
|
||||
import { ArrowLeftIcon, PencilIcon } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { LockdownBanner } from "@/components/frontend/wiki/LockdownBanner";
|
||||
import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
|
||||
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
||||
|
||||
export const metadata = {
|
||||
title: "Edit wiki page — Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function EditWikiPage({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
const headers = await nextHeaders();
|
||||
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) {
|
||||
redirect(`/wiki/new?title=${encodeURIComponent(slug)}`);
|
||||
}
|
||||
|
||||
if (page.lockdown) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<Link
|
||||
href={`/wiki/${page.slug}`}
|
||||
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Back to page
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<PencilIcon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Edit {page.title}</h1>
|
||||
</div>
|
||||
<LockdownBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [templates, pagesResult, hasEditedRevisions] = await Promise.all([
|
||||
listTemplates(payload),
|
||||
payload.find({
|
||||
collection: "wiki-pages",
|
||||
sort: "title",
|
||||
limit: 200,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
}),
|
||||
user
|
||||
? payload
|
||||
.find({
|
||||
collection: "wiki-revisions",
|
||||
where: { editor: { equals: user.id } },
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
.then((result) => result.docs.length > 0)
|
||||
: Promise.resolve(true),
|
||||
]);
|
||||
const existingPages = pagesResult.docs.map((existingPage) => ({ title: existingPage.title }));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Link
|
||||
href={`/wiki/${page.slug}`}
|
||||
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Back to page
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<PencilIcon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Edit {page.title}</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Update the article and leave a short note for the revision history.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<WikiEditor
|
||||
mode="edit"
|
||||
initial={{
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
category: page.category,
|
||||
tags: page.tags ?? [],
|
||||
body: page.body,
|
||||
}}
|
||||
templates={templateMap(templates)}
|
||||
existingPages={existingPages}
|
||||
isFirstEdit={!hasEditedRevisions}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
src/app/(frontend)/wiki/[slug]/history/page.tsx
Normal file
129
src/app/(frontend)/wiki/[slug]/history/page.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import config from "@payload-config";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { getPayload } from "payload";
|
||||
import { ArrowLeftIcon, HistoryIcon } from "lucide-react";
|
||||
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 { RevisionList, type WikiRevisionSummary } from "@/components/frontend/wiki/RevisionList";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
|
||||
export const metadata = {
|
||||
title: "Wiki history — Polaris Task Force",
|
||||
};
|
||||
|
||||
function editorName(editor: WikiRevision["editor"]): string {
|
||||
if (typeof editor === "object" && editor !== null) {
|
||||
return editor.payloadDisplayName || editor.displayName || editor.username || "Unknown editor";
|
||||
}
|
||||
|
||||
return typeof editor === "number" ? `User #${editor}` : "Unknown editor";
|
||||
}
|
||||
|
||||
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">
|
||||
There is no revision history until this page is created.
|
||||
</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 WikiHistoryPage({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{ slug: 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 [revisionsResult, isModerator] = await Promise.all([
|
||||
payload.find({
|
||||
collection: "wiki-revisions",
|
||||
where: { page: { equals: page.id } },
|
||||
sort: "-revisionNumber",
|
||||
limit: 200,
|
||||
depth: 1,
|
||||
overrideAccess: true,
|
||||
}),
|
||||
hasIntelligenceQualification(payload, user),
|
||||
]);
|
||||
const revisions: WikiRevisionSummary[] = revisionsResult.docs.map((revision) => ({
|
||||
id: revision.id,
|
||||
revisionNumber: revision.revisionNumber,
|
||||
titleSnapshot: revision.titleSnapshot,
|
||||
editorName: editorName(revision.editor),
|
||||
summary: revision.summary ?? "",
|
||||
type: revision.type,
|
||||
restoredFromRevision: revision.restoredFromRevision ?? null,
|
||||
createdAt: revision.createdAt,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Link
|
||||
href={`/wiki/${page.slug}`}
|
||||
className="flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-primary"
|
||||
>
|
||||
<ArrowLeftIcon className="size-3" />
|
||||
Back to page
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<HistoryIcon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">History: {page.title}</h1>
|
||||
<Badge variant="outline">{revisions.length} revisions</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Review the article's edit trail and restore an earlier snapshot when needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RevisionList
|
||||
pageId={page.id}
|
||||
slug={page.slug}
|
||||
revisions={revisions}
|
||||
isModerator={isModerator}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
232
src/app/(frontend)/wiki/[slug]/page.tsx
Normal file
232
src/app/(frontend)/wiki/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
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";
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<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>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<WikiContent prepared={prepared} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const prepared = prepareMarkdown(page.body, templateMapValue);
|
||||
const lockdown = page.lockdown === true;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<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 />}
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<WikiContent prepared={prepared} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<WikiToolbar
|
||||
slug={page.slug}
|
||||
id={page.id}
|
||||
lockdown={lockdown}
|
||||
isModerator={isModerator}
|
||||
canEdit={!lockdown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
src/app/(frontend)/wiki/new/page.tsx
Normal file
74
src/app/(frontend)/wiki/new/page.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import config from "@payload-config";
|
||||
import Link from "next/link";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import { getPayload } from "payload";
|
||||
import { ArrowLeftIcon, FilePlus2Icon } from "lucide-react";
|
||||
import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
|
||||
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
||||
|
||||
export const metadata = {
|
||||
title: "New wiki page — Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function NewWikiPage({
|
||||
searchParams,
|
||||
}: {
|
||||
readonly searchParams: Promise<{ title?: string | string[] }>;
|
||||
}) {
|
||||
const { title } = await searchParams;
|
||||
const initialTitle = Array.isArray(title) ? (title[0] ?? "") : (title ?? "");
|
||||
const payload = await getPayload({ config });
|
||||
const headers = await nextHeaders();
|
||||
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
||||
const [templates, pagesResult, hasEditedRevisions] = await Promise.all([
|
||||
listTemplates(payload),
|
||||
payload.find({
|
||||
collection: "wiki-pages",
|
||||
sort: "title",
|
||||
limit: 200,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
}),
|
||||
user
|
||||
? payload
|
||||
.find({
|
||||
collection: "wiki-revisions",
|
||||
where: { editor: { equals: user.id } },
|
||||
limit: 1,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
})
|
||||
.then((result) => result.docs.length > 0)
|
||||
: Promise.resolve(true),
|
||||
]);
|
||||
const existingPages = pagesResult.docs.map((page) => ({ title: page.title }));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<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 items-center gap-2">
|
||||
<FilePlus2Icon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">New wiki page</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add a durable reference for the rest of the task force.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WikiEditor
|
||||
mode="create"
|
||||
initialTitle={initialTitle}
|
||||
templates={templateMap(templates)}
|
||||
existingPages={existingPages}
|
||||
isFirstEdit={!hasEditedRevisions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/app/(frontend)/wiki/page.tsx
Normal file
67
src/app/(frontend)/wiki/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import config from "@payload-config";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import { getPayload } from "payload";
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
import type { WikiPage as WikiPageDocument } from "@/payload-types";
|
||||
import { WikiIndex, type WikiIndexPage } from "@/components/frontend/wiki/WikiIndex";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
|
||||
export const metadata = {
|
||||
title: "Wiki — 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";
|
||||
}
|
||||
|
||||
export default async function WikiPage() {
|
||||
const headers = await nextHeaders();
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({
|
||||
headers,
|
||||
canSetHeaders: false,
|
||||
});
|
||||
|
||||
const [pagesResult, isModerator] = await Promise.all([
|
||||
payload.find({
|
||||
collection: "wiki-pages",
|
||||
sort: "-updatedAt",
|
||||
limit: 200,
|
||||
depth: 1,
|
||||
overrideAccess: true,
|
||||
}),
|
||||
hasIntelligenceQualification(payload, user),
|
||||
]);
|
||||
|
||||
const pages: WikiIndexPage[] = pagesResult.docs.map((page) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
slug: page.slug,
|
||||
category: page.category,
|
||||
tags: page.tags ?? [],
|
||||
updatedAt: page.updatedAt,
|
||||
lastEditorName: relationName(page.lastEditor),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpenIcon className="size-5 text-muted-foreground" />
|
||||
<h1 className="text-lg font-semibold">Wiki</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
A shared field guide for campaigns, characters, places, and the stories around them.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WikiIndex pages={pages} isModerator={isModerator} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue