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 ( +
+ setNewTreasure(e.target.value)} + disabled={adding} + aria-label="Add new treasure" + /> + {error &&
{error}
} + +
+ ); +}; 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} /> +
); }