diff --git a/src/app/(frontend)/wiki/[slug]/edit/page.tsx b/src/app/(frontend)/wiki/[slug]/edit/page.tsx
new file mode 100644
index 0000000..005f247
--- /dev/null
+++ b/src/app/(frontend)/wiki/[slug]/edit/page.tsx
@@ -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 (
+
+
+
+ Back to page
+
+
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+ Back to page
+
+
+
+ Update the article and leave a short note for the revision history.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(frontend)/wiki/[slug]/history/page.tsx b/src/app/(frontend)/wiki/[slug]/history/page.tsx
new file mode 100644
index 0000000..90ec2e3
--- /dev/null
+++ b/src/app/(frontend)/wiki/[slug]/history/page.tsx
@@ -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 (
+
+
+
+
Page doesn't exist yet
+
+ There is no revision history until this page is created.
+
+
+
+
+
+ );
+}
+
+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 (
+
+ );
+ }
+
+ 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 (
+
+
+
+
+ Back to page
+
+
+
+
History: {page.title}
+ {revisions.length} revisions
+
+
+ Review the article's edit trail and restore an earlier snapshot when needed.
+
+
+
+
+
+ );
+}
diff --git a/src/app/(frontend)/wiki/[slug]/page.tsx b/src/app/(frontend)/wiki/[slug]/page.tsx
new file mode 100644
index 0000000..1bba8ab
--- /dev/null
+++ b/src/app/(frontend)/wiki/[slug]/page.tsx
@@ -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 (
+
+
+
+
Page doesn't exist yet
+
+ This entry is not in the field guide. Start a page for this topic instead.
+
+
+
+
+
+ );
+}
+
+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 (
+
+ );
+ }
+
+ 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 (
+
+
+
+ Wiki
+
+
+
+
+
Revision not found
+
+ Revision {revisionNumber} of this page no longer exists.
+
+
+
+
+
+
+ );
+ }
+
+ const prepared = prepareMarkdown(revision.contentSnapshot, templateMapValue);
+
+ return (
+
+
+
+
+ Wiki
+
+
+
+ You're viewing revision {revision.revisionNumber} (from{" "}
+ {formatDate(revision.createdAt)}).
+
+
+
+
+
+
+ {revision.titleSnapshot}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const prepared = prepareMarkdown(page.body, templateMapValue);
+ const lockdown = page.lockdown === true;
+
+ return (
+
+
+
+
+ Wiki
+
+
+
+
+
+
{page.title}
+
+
+ {page.category}
+ {(page.tags ?? []).map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+ Last edited by {relationName(page.lastEditor)}
+
+
+
+
+
+ {lockdown &&
}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(frontend)/wiki/new/page.tsx b/src/app/(frontend)/wiki/new/page.tsx
new file mode 100644
index 0000000..831a487
--- /dev/null
+++ b/src/app/(frontend)/wiki/new/page.tsx
@@ -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 (
+
+
+
+
+ Wiki
+
+
+
+
New wiki page
+
+
+ Add a durable reference for the rest of the task force.
+
+
+
+
+
+ );
+}
diff --git a/src/app/(frontend)/wiki/page.tsx b/src/app/(frontend)/wiki/page.tsx
new file mode 100644
index 0000000..3105a98
--- /dev/null
+++ b/src/app/(frontend)/wiki/page.tsx
@@ -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 (
+
+
+
+
+
Wiki
+
+
+ A shared field guide for campaigns, characters, places, and the stories around them.
+
+
+
+
+
+ );
+}