(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!name.trim()) return;
+ setAdding(true);
+ setError(null);
+ try {
+ const locationDoc: Location = await pb.collection("documents").create({
+ campaign,
+ data: {
+ location: {
+ name,
+ description,
+ },
+ },
+ });
+ setName("");
+ setDescription("");
+ await onCreate(locationDoc);
+ } catch (e: any) {
+ setError(e?.message || "Failed to add location.");
+ } finally {
+ setAdding(false);
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/location/LocationRow.tsx b/src/components/documents/location/LocationRow.tsx
new file mode 100644
index 0000000..cf3b0fa
--- /dev/null
+++ b/src/components/documents/location/LocationRow.tsx
@@ -0,0 +1,46 @@
+import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
+import { pb } from "@/lib/pocketbase";
+import type { Location } from "@/lib/types";
+
+/**
+ * Renders an editable location row
+ */
+export const LocationRow = ({ location }: { location: Location }) => {
+ async function saveLocationName(name: string) {
+ await pb.collection("documents").update(location.id, {
+ data: {
+ ...location.data,
+ location: {
+ ...location.data.location,
+ name,
+ },
+ },
+ });
+ }
+
+ async function saveLocationDescription(description: string) {
+ await pb.collection("documents").update(location.id, {
+ data: {
+ ...location.data,
+ location: {
+ ...location.data.location,
+ description,
+ },
+ },
+ });
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/monsters/MonsterForm.tsx b/src/components/documents/monsters/MonsterForm.tsx
new file mode 100644
index 0000000..93fc14d
--- /dev/null
+++ b/src/components/documents/monsters/MonsterForm.tsx
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import type { CampaignId, Monster } from "@/lib/types";
+import { pb } from "@/lib/pocketbase";
+
+/**
+ * Renders a form to add a new monster. Calls onCreate with the new monster document.
+ */
+export const MonsterForm = ({
+ campaign,
+ onCreate,
+}: {
+ campaign: CampaignId;
+ onCreate: (monster: Monster) => 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 monsterDoc: Monster = await pb.collection("documents").create({
+ campaign,
+ data: {
+ monster: {
+ name,
+ description,
+ },
+ },
+ });
+ setName("");
+ setDescription("");
+ await onCreate(monsterDoc);
+ } catch (e: any) {
+ setError(e?.message || "Failed to add monster.");
+ } finally {
+ setAdding(false);
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/monsters/MonsterRow.tsx b/src/components/documents/monsters/MonsterRow.tsx
new file mode 100644
index 0000000..d38cf74
--- /dev/null
+++ b/src/components/documents/monsters/MonsterRow.tsx
@@ -0,0 +1,30 @@
+import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
+import { pb } from "@/lib/pocketbase";
+import type { Monster } from "@/lib/types";
+
+/**
+ * Renders an editable monster row
+ */
+export const MonsterRow = ({ monster }: { monster: Monster }) => {
+ async function saveMonsterName(name: string) {
+ await pb.collection("documents").update(monster.id, {
+ data: {
+ ...monster.data,
+ monster: {
+ ...monster.data.monster,
+ name,
+ },
+ },
+ });
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/npc/NpcForm.tsx b/src/components/documents/npc/NpcForm.tsx
index 0695754..2f4454d 100644
--- a/src/components/documents/npc/NpcForm.tsx
+++ b/src/components/documents/npc/NpcForm.tsx
@@ -48,7 +48,7 @@ export const NpcForm = ({
onSubmit={handleSubmit}
>
Create new npc
-
+
-
+