-
+ {multiline ? (
+
+ ) : (
+
+ )}
{saved && !saving && (
Saved
diff --git a/src/components/documents/DocumentForm.tsx b/src/components/documents/DocumentForm.tsx
index e3215c5..44f3ddc 100644
--- a/src/components/documents/DocumentForm.tsx
+++ b/src/components/documents/DocumentForm.tsx
@@ -2,6 +2,7 @@ import { RelationshipType, type CampaignId, type Document } from "@/lib/types";
import { SecretForm } from "./secret/SecretForm";
import { TreasureForm } from "./treasure/TreasureForm";
import { SceneForm } from "./scene/SceneForm";
+import { NpcForm } from "./npc/NpcForm";
function assertUnreachable(_x: never): never {
throw new Error("DocumentForm switch is not exhaustive");
@@ -20,6 +21,8 @@ export const DocumentForm = ({
onCreate: (document: Document) => Promise;
}) => {
switch (relationshipType) {
+ case RelationshipType.Npcs:
+ return ;
case RelationshipType.Secrets:
return ;
case RelationshipType.DiscoveredIn:
diff --git a/src/components/documents/DocumentRow.tsx b/src/components/documents/DocumentRow.tsx
index f38952f..fe2fc68 100644
--- a/src/components/documents/DocumentRow.tsx
+++ b/src/components/documents/DocumentRow.tsx
@@ -3,6 +3,7 @@
import { SessionRow } from "@/components/documents/session/SessionRow";
import { SecretRow } from "@/components/documents/secret/SecretRow";
import {
+ isNpc,
isScene,
isSecret,
isSession,
@@ -12,6 +13,7 @@ import {
} from "@/lib/types";
import { TreasureRow } from "./treasure/TreasureRow";
import { SceneRow } from "./scene/SceneRow";
+import { NpcRow } from "./npc/NpcRow";
/**
* Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time.
@@ -24,10 +26,14 @@ export const DocumentRow = ({
document: Document;
session?: Session;
}) => {
+ if (isNpc(document)) {
+ return ;
+ }
+
if (isSession(document)) {
- // Use SessionRow for session documents
return ;
}
+
if (isSecret(document)) {
return ;
}
diff --git a/src/components/documents/npc/NpcForm.tsx b/src/components/documents/npc/NpcForm.tsx
new file mode 100644
index 0000000..0695754
--- /dev/null
+++ b/src/components/documents/npc/NpcForm.tsx
@@ -0,0 +1,84 @@
+import { useState } from "react";
+import type { CampaignId, Npc } from "@/lib/types";
+import { pb } from "@/lib/pocketbase";
+
+/**
+ * Renders a form to add a new npc. Calls onCreate with the new npc document.
+ */
+export const NpcForm = ({
+ campaign,
+ onCreate,
+}: {
+ campaign: CampaignId;
+ onCreate: (npc: Npc) => Promise;
+}) => {
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [adding, setAdding] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!name.trim()) return;
+ setAdding(true);
+ setError(null);
+ try {
+ const npcDoc: Npc = await pb.collection("documents").create({
+ campaign,
+ data: {
+ npc: {
+ name,
+ description,
+ },
+ },
+ });
+ setName("");
+ setDescription("");
+ await onCreate(npcDoc);
+ } catch (e: any) {
+ setError(e?.message || "Failed to add npc.");
+ } finally {
+ setAdding(false);
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/npc/NpcRow.tsx b/src/components/documents/npc/NpcRow.tsx
new file mode 100644
index 0000000..5e2ab20
--- /dev/null
+++ b/src/components/documents/npc/NpcRow.tsx
@@ -0,0 +1,46 @@
+import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
+import { pb } from "@/lib/pocketbase";
+import type { Npc } from "@/lib/types";
+
+/**
+ * Renders an editable npc row
+ */
+export const NpcRow = ({ npc }: { npc: Npc }) => {
+ async function saveNpcName(name: string) {
+ await pb.collection("documents").update(npc.id, {
+ data: {
+ ...npc.data,
+ npc: {
+ ...npc.data.npc,
+ name,
+ },
+ },
+ });
+ }
+
+ async function saveNpcDescription(description: string) {
+ await pb.collection("documents").update(npc.id, {
+ data: {
+ ...npc.data,
+ npc: {
+ ...npc.data.npc,
+ description,
+ },
+ },
+ });
+ }
+
+ return (
+
+ );
+};
diff --git a/src/lib/types.ts b/src/lib/types.ts
index b64c94c..62e1d24 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -23,6 +23,7 @@ export const RelationshipType = {
Scenes: "scenes",
Secrets: "secrets",
Treasures: "treasures",
+ Npcs: "npcs",
} as const;
export type RelationshipType =
@@ -51,6 +52,21 @@ export type Document = RecordModel & {
updated: ISO8601Date;
};
+/** NPCs **/
+
+export type Npc = Document &
+ DocumentData<
+ "npc",
+ {
+ name: string;
+ description: string;
+ }
+ >;
+
+export function isNpc(doc: Document): doc is Npc {
+ return Object.hasOwn(doc.data, "npc");
+}
+
/** Session **/
export type Session = Document &
diff --git a/src/routes/_authenticated/document.$documentId.tsx b/src/routes/_authenticated/document.$documentId.tsx
index be6d087..2f47e3b 100644
--- a/src/routes/_authenticated/document.$documentId.tsx
+++ b/src/routes/_authenticated/document.$documentId.tsx
@@ -51,6 +51,7 @@ function RouteComponent() {
{[
RelationshipType.Scenes,
RelationshipType.Secrets,
+ RelationshipType.Npcs,
RelationshipType.Treasures,
].map((relationshipType) => (