diff --git a/pb_migrations/1748738493_updated_relationships.js b/pb_migrations/1748738493_updated_relationships.js
new file mode 100644
index 0000000..9889db9
--- /dev/null
+++ b/pb_migrations/1748738493_updated_relationships.js
@@ -0,0 +1,43 @@
+///
+migrate((app) => {
+ const collection = app.findCollectionByNameOrId("pbc_617371094")
+
+ // update field
+ collection.fields.addAt(3, new Field({
+ "hidden": false,
+ "id": "select2363381545",
+ "maxSelect": 1,
+ "name": "type",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "discoveredIn",
+ "secrets",
+ "treasures"
+ ]
+ }))
+
+ return app.save(collection)
+}, (app) => {
+ const collection = app.findCollectionByNameOrId("pbc_617371094")
+
+ // update field
+ collection.fields.addAt(3, new Field({
+ "hidden": false,
+ "id": "select2363381545",
+ "maxSelect": 1,
+ "name": "type",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "select",
+ "values": [
+ "discoveredIn",
+ "secrets"
+ ]
+ }))
+
+ return app.save(collection)
+})
diff --git a/src/components/documents/DocumentForm.tsx b/src/components/documents/DocumentForm.tsx
index 5984e57..f01a212 100644
--- a/src/components/documents/DocumentForm.tsx
+++ b/src/components/documents/DocumentForm.tsx
@@ -1,5 +1,6 @@
import { RelationshipType, type CampaignId, type Document } from "@/lib/types";
import { SecretForm } from "./secret/SecretForm";
+import { TreasureForm } from "./treasure/TreasureForm";
function assertUnreachable(_x: never): never {
throw new Error("DocumentForm switch is not exhaustive");
@@ -22,6 +23,8 @@ export const DocumentForm = ({
return ;
case RelationshipType.DiscoveredIn:
return "Form not supported here";
+ case RelationshipType.Treasures:
+ return ;
}
return assertUnreachable(relationshipType);
diff --git a/src/components/documents/DocumentRow.tsx b/src/components/documents/DocumentRow.tsx
index 61d7748..74d11d3 100644
--- a/src/components/documents/DocumentRow.tsx
+++ b/src/components/documents/DocumentRow.tsx
@@ -2,7 +2,14 @@
// Generic row component for displaying any document type.
import { SessionRow } from "@/components/documents/session/SessionRow";
import { SecretRow } from "@/components/documents/secret/SecretRow";
-import { isSecret, isSession, type Document, type Session } from "@/lib/types";
+import {
+ isSecret,
+ isSession,
+ isTreasure,
+ type Document,
+ type Session,
+} from "@/lib/types";
+import { TreasureRow } from "./treasure/TreasureRow";
/**
* Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time.
@@ -22,6 +29,10 @@ export const DocumentRow = ({
if (isSecret(document)) {
return ;
}
+ if (isTreasure(document)) {
+ return ;
+ }
+
// Fallback: show ID and creation time
return (
diff --git a/src/components/documents/treasure/TreasureForm.tsx b/src/components/documents/treasure/TreasureForm.tsx
new file mode 100644
index 0000000..f186d2c
--- /dev/null
+++ b/src/components/documents/treasure/TreasureForm.tsx
@@ -0,0 +1,66 @@
+// TreasureForm.tsx
+// Form for adding a new treasure to a session.
+import { useState } from "react";
+import type { CampaignId, Treasure } from "@/lib/types";
+import { pb } from "@/lib/pocketbase";
+
+/**
+ * Renders a form to add a new treasure. Calls onCreate with the new treasure document.
+ */
+export const TreasureForm = ({
+ campaign,
+ onCreate,
+}: {
+ campaign: CampaignId;
+ onCreate: (treasure: Treasure) => Promise
;
+}) => {
+ const [newTreasure, setNewTreasure] = useState("");
+ const [adding, setAdding] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!newTreasure.trim()) return;
+ setAdding(true);
+ setError(null);
+ try {
+ const treasureDoc: Treasure = await pb.collection("documents").create({
+ campaign,
+ data: {
+ treasure: {
+ text: newTreasure,
+ discovered: false,
+ },
+ },
+ });
+ setNewTreasure("");
+ await onCreate(treasureDoc);
+ } catch (e: any) {
+ setError(e?.message || "Failed to add treasure.");
+ } finally {
+ setAdding(false);
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/src/components/documents/treasure/TreasureRow.tsx b/src/components/documents/treasure/TreasureRow.tsx
new file mode 100644
index 0000000..9bc4a88
--- /dev/null
+++ b/src/components/documents/treasure/TreasureRow.tsx
@@ -0,0 +1,78 @@
+// TreasureRow.tsx
+// Displays a single treasure with discovered checkbox and text.
+import type { Treasure, Session } from "@/lib/types";
+import { pb } from "@/lib/pocketbase";
+import { useState } from "react";
+
+/**
+ * Renders a treasure row with a discovered checkbox and treasure text.
+ * Handles updating the discovered state and discoveredIn relationship.
+ */
+export const TreasureRow = ({
+ treasure,
+ session,
+}: {
+ treasure: Treasure;
+ session?: Session;
+}) => {
+ const [checked, setChecked] = useState(
+ !!(treasure.data as any)?.treasure?.discovered,
+ );
+ const [loading, setLoading] = useState(false);
+
+ async function handleChange(e: React.ChangeEvent) {
+ const newChecked = e.target.checked;
+ setLoading(true);
+ setChecked(newChecked);
+ try {
+ await pb.collection("documents").update(treasure.id, {
+ data: {
+ ...treasure.data,
+ treasure: {
+ ...(treasure.data as any).treasure,
+ discovered: newChecked,
+ },
+ },
+ });
+ if (session || !newChecked) {
+ // If the session exists or the element is being unchecked, remove any
+ // existing discoveredIn relationship
+ const rels = await pb.collection("relationships").getList(1, 1, {
+ filter: `primary = "${treasure.id}" && type = "discoveredIn"`,
+ });
+ if (rels.items.length > 0) {
+ await pb.collection("relationships").delete(rels.items[0].id);
+ }
+ }
+ if (session) {
+ if (newChecked) {
+ await pb.collection("relationships").create({
+ primary: treasure.id,
+ secondary: [session.id],
+ type: "discoveredIn",
+ });
+ }
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+ {(treasure.data as any)?.treasure?.text || (
+ (No treasure text)
+ )}
+
+
+ );
+};
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 5908c07..2ee7e8e 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -6,23 +6,48 @@ export type UserId = Id<"User">;
export type CampaignId = Id<"Campaign">;
export type DocumentId = Id<"Document">;
+export type ISO8601Date = string & { __type: "iso8601date" };
+
export type Campaign = RecordModel & {
id: CampaignId;
name: string;
owner: UserId;
};
+/******************************************
+ * Relationships
+ ******************************************/
+
+export const RelationshipType = {
+ Secrets: "secrets",
+ DiscoveredIn: "discoveredIn",
+ Treasures: "treasures",
+} as const;
+
+export type RelationshipType =
+ (typeof RelationshipType)[keyof typeof RelationshipType];
+
+export type Relationship = RecordModel & {
+ primary: DocumentId;
+ secondary: DocumentId[];
+ type: RelationshipType;
+};
+
+/******************************************
+ * Documents
+ ******************************************/
+
+export type DocumentData = {
+ data: Record;
+};
+
export type Document = RecordModel & {
id: DocumentId;
campaign: CampaignId;
data: {};
// These two are not in Pocketbase's types, but they seem to always be present
- created: string;
- updated: string;
-};
-
-export type DocumentData = {
- data: Record;
+ created: ISO8601Date;
+ updated: ISO8601Date;
};
export type Session = Document &
@@ -37,8 +62,6 @@ export function isSession(doc: Document): doc is Session {
return Object.hasOwn(doc.data, "session");
}
-export type ISO8601Date = string & { __type: "iso8601date" };
-
export type Secret = Document &
DocumentData<
"secret",
@@ -52,16 +75,15 @@ export function isSecret(doc: Document): doc is Secret {
return Object.hasOwn(doc.data, "secret");
}
-export const RelationshipType = {
- Secrets: "secrets",
- DiscoveredIn: "discoveredIn",
-} as const;
+export type Treasure = Document &
+ DocumentData<
+ "treasure",
+ {
+ text: string;
+ discovered: boolean;
+ }
+ >;
-export type RelationshipType =
- (typeof RelationshipType)[keyof typeof RelationshipType];
-
-export type Relationship = RecordModel & {
- primary: DocumentId;
- secondary: DocumentId[];
- type: RelationshipType;
-};
+export function isTreasure(doc: Document): doc is Treasure {
+ return Object.hasOwn(doc.data, "treasure");
+}
diff --git a/src/routes/_authenticated/document.$documentId.tsx b/src/routes/_authenticated/document.$documentId.tsx
index 0f5a344..a80dd07 100644
--- a/src/routes/_authenticated/document.$documentId.tsx
+++ b/src/routes/_authenticated/document.$documentId.tsx
@@ -28,6 +28,10 @@ function RouteComponent() {
root={session}
relationshipType={RelationshipType.Secrets}
/>
+
);
}