diff --git a/src/components/RelationshipList.tsx b/src/components/RelationshipList.tsx index 4f4de5f..5cf085b 100644 --- a/src/components/RelationshipList.tsx +++ b/src/components/RelationshipList.tsx @@ -1,15 +1,16 @@ import { DocumentList } from "@/components/DocumentList"; import { pb } from "@/lib/pocketbase"; -import type { Document, RelationshipType } from "@/lib/types"; +import { displayName } from "@/lib/relationships"; +import type { Document, DocumentId, RelationshipType } from "@/lib/types"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useState } from "react"; import { Loader } from "./Loader"; -import { NewRelatedDocumentForm } from "./documents/NewRelatedDocumentForm"; import { DocumentRow } from "./documents/DocumentRow"; -import { displayName } from "@/lib/relationships"; +import { NewRelatedDocumentForm } from "./documents/NewRelatedDocumentForm"; interface RelationshipListProps { root: Document; - items: Document[]; + items: DocumentId[]; relationshipType: RelationshipType; } diff --git a/src/components/documents/DocumentEditForm.tsx b/src/components/documents/DocumentEditForm.tsx new file mode 100644 index 0000000..6cf3f05 --- /dev/null +++ b/src/components/documents/DocumentEditForm.tsx @@ -0,0 +1,56 @@ +import { + isLocation, + isMonster, + isNpc, + isScene, + isSecret, + isSession, + isTreasure, + type AnyDocument, +} from "@/lib/types"; +import { LocationEditForm } from "./location/LocationEditForm"; +import { MonsterEditForm } from "./monsters/MonsterEditForm"; +import { NpcEditForm } from "./npc/NpcEditForm"; +import { SceneEditForm } from "./scene/SceneEditForm"; +import { SecretEditForm } from "./secret/SecretEditForm"; +import { SessionEditForm } from "./session/SessionEditForm"; +import { TreasureEditForm } from "./treasure/TreasureEditForm"; + +function assertUnreachable(_x: never): never { + throw new Error("DocumentForm switch is not exhaustive"); +} + +/** + * Renders a form for any document type depending on the relationship. + */ +export const DocumentEditForm = ({ document }: { document: AnyDocument }) => { + if (isLocation(document)) { + return ; + } + + if (isMonster(document)) { + return ; + } + + if (isNpc(document)) { + return ; + } + + if (isScene(document)) { + return ; + } + + if (isSecret(document)) { + return ; + } + + if (isSession(document)) { + return ; + } + + if (isTreasure(document)) { + return ; + } + + return assertUnreachable(document); +}; diff --git a/src/components/documents/DocumentRow.tsx b/src/components/documents/DocumentRow.tsx index 7a2515e..36b6e72 100644 --- a/src/components/documents/DocumentRow.tsx +++ b/src/components/documents/DocumentRow.tsx @@ -1,7 +1,6 @@ // DocumentRow.tsx // Generic row component for displaying any document type. -import { SecretRow } from "@/components/documents/secret/SecretRow"; -import { SessionRow } from "@/components/documents/session/SessionRow"; +import { SecretToggleRow } from "@/components/documents/secret/SecretToggleRow"; import { isLocation, isMonster, @@ -13,11 +12,12 @@ import { type Document, type Session, } from "@/lib/types"; -import { LocationRow } from "./location/LocationRow"; -import { MonsterRow } from "./monsters/MonsterRow"; -import { NpcRow } from "./npc/NpcRow"; -import { SceneRow } from "./scene/SceneRow"; -import { TreasureRow } from "./treasure/TreasureRow"; +import { LocationPrintRow } from "./location/LocationPrintRow"; +import { MonsterPrintRow } from "./monsters/MonsterPrintRow"; +import { NpcPrintRow } from "./npc/NpcPrintRow"; +import { ScenePrintRow } from "./scene/ScenePrintRow"; +import { SessionPrintRow } from "./session/SessionPrintRow"; +import { TreasureToggleRow } from "./treasure/TreasureToggleRow"; /** * Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time. @@ -31,31 +31,31 @@ export const DocumentRow = ({ session?: Session; }) => { if (isLocation(document)) { - return ; + return ; } if (isMonster(document)) { - return ; + return ; } if (isNpc(document)) { - return ; + return ; } if (isSession(document)) { - return ; + return ; } if (isSecret(document)) { - return ; + return ; } if (isScene(document)) { - return ; + return ; } if (isTreasure(document)) { - return ; + return ; } // Fallback: show ID and creation time diff --git a/src/components/documents/location/LocationRow.tsx b/src/components/documents/location/LocationEditForm.tsx similarity index 89% rename from src/components/documents/location/LocationRow.tsx rename to src/components/documents/location/LocationEditForm.tsx index cf3b0fa..19854ef 100644 --- a/src/components/documents/location/LocationRow.tsx +++ b/src/components/documents/location/LocationEditForm.tsx @@ -3,9 +3,9 @@ import { pb } from "@/lib/pocketbase"; import type { Location } from "@/lib/types"; /** - * Renders an editable location row + * Renders an editable location form */ -export const LocationRow = ({ location }: { location: Location }) => { +export const LocationEditForm = ({ location }: { location: Location }) => { async function saveLocationName(name: string) { await pb.collection("documents").update(location.id, { data: { diff --git a/src/components/documents/monsters/MonsterRow.tsx b/src/components/documents/monsters/MonsterEditForm.tsx similarity index 89% rename from src/components/documents/monsters/MonsterRow.tsx rename to src/components/documents/monsters/MonsterEditForm.tsx index d38cf74..2b57ee3 100644 --- a/src/components/documents/monsters/MonsterRow.tsx +++ b/src/components/documents/monsters/MonsterEditForm.tsx @@ -5,7 +5,7 @@ import type { Monster } from "@/lib/types"; /** * Renders an editable monster row */ -export const MonsterRow = ({ monster }: { monster: Monster }) => { +export const MonsterEditForm = ({ monster }: { monster: Monster }) => { async function saveMonsterName(name: string) { await pb.collection("documents").update(monster.id, { data: { diff --git a/src/components/documents/npc/NpcRow.tsx b/src/components/documents/npc/NpcEditForm.tsx similarity index 91% rename from src/components/documents/npc/NpcRow.tsx rename to src/components/documents/npc/NpcEditForm.tsx index 5e2ab20..0e9f18a 100644 --- a/src/components/documents/npc/NpcRow.tsx +++ b/src/components/documents/npc/NpcEditForm.tsx @@ -3,9 +3,9 @@ import { pb } from "@/lib/pocketbase"; import type { Npc } from "@/lib/types"; /** - * Renders an editable npc row + * Renders an editable npc form */ -export const NpcRow = ({ npc }: { npc: Npc }) => { +export const NpcEditForm = ({ npc }: { npc: Npc }) => { async function saveNpcName(name: string) { await pb.collection("documents").update(npc.id, { data: { diff --git a/src/components/documents/scene/SceneRow.tsx b/src/components/documents/scene/SceneEditForm.tsx similarity index 83% rename from src/components/documents/scene/SceneRow.tsx rename to src/components/documents/scene/SceneEditForm.tsx index b91c244..bcb6660 100644 --- a/src/components/documents/scene/SceneRow.tsx +++ b/src/components/documents/scene/SceneEditForm.tsx @@ -3,9 +3,9 @@ import { pb } from "@/lib/pocketbase"; import type { Scene } from "@/lib/types"; /** - * Renders an editable scene row + * Renders an editable scene form */ -export const SceneRow = ({ scene }: { scene: Scene }) => { +export const SceneEditForm = ({ scene }: { scene: Scene }) => { async function saveScene(text: string) { await pb.collection("documents").update(scene.id, { data: { diff --git a/src/components/documents/secret/SecretEditForm.tsx b/src/components/documents/secret/SecretEditForm.tsx new file mode 100644 index 0000000..d099482 --- /dev/null +++ b/src/components/documents/secret/SecretEditForm.tsx @@ -0,0 +1,90 @@ +// Displays a single secret with discovered checkbox and text. +import type { Secret, Session } from "@/lib/types"; +import { pb } from "@/lib/pocketbase"; +import { useState } from "react"; +import { AutoSaveTextarea } from "@/components/AutoSaveTextarea"; + +/** + * Renders an editable secret form. + * Handles updating the discovered state and discoveredIn relationship. + */ +export const SecretEditForm = ({ + secret, + session, +}: { + secret: Secret; + session?: Session; +}) => { + const [checked, setChecked] = useState( + !!(secret.data as any)?.secret?.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(secret.id, { + data: { + ...secret.data, + secret: { + ...(secret.data as any).secret, + 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 = "${secret.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: secret.id, + secondary: [session.id], + type: "discoveredIn", + }); + } + } + } finally { + setLoading(false); + } + } + + async function saveText(text: string) { + await pb.collection("documents").update(secret.id, { + data: { + ...secret.data, + secret: { + ...secret.data.secret, + text, + }, + }, + }); + } + + return ( +
+ + +
+ ); +}; diff --git a/src/components/documents/secret/SecretRow.tsx b/src/components/documents/secret/SecretToggleRow.tsx similarity index 98% rename from src/components/documents/secret/SecretRow.tsx rename to src/components/documents/secret/SecretToggleRow.tsx index eee109b..bd1b7f0 100644 --- a/src/components/documents/secret/SecretRow.tsx +++ b/src/components/documents/secret/SecretToggleRow.tsx @@ -8,7 +8,7 @@ import { useState } from "react"; * Renders a secret row with a discovered checkbox and secret text. * Handles updating the discovered state and discoveredIn relationship. */ -export const SecretRow = ({ +export const SecretToggleRow = ({ secret, session, }: { diff --git a/src/components/documents/session/EditSessionForm.tsx b/src/components/documents/session/SessionEditForm.tsx similarity index 51% rename from src/components/documents/session/EditSessionForm.tsx rename to src/components/documents/session/SessionEditForm.tsx index adcf127..7af3776 100644 --- a/src/components/documents/session/EditSessionForm.tsx +++ b/src/components/documents/session/SessionEditForm.tsx @@ -1,19 +1,26 @@ import { AutoSaveTextarea } from "@/components/AutoSaveTextarea"; +import { pb } from "@/lib/pocketbase"; import type { Session } from "@/lib/types"; -export const EditSessionForm = ({ - session, - onSubmit, -}: { - session: Session; - onSubmit: (data: Session["data"]) => Promise; -}) => { +export const SessionEditForm = ({ session }: { session: Session }) => { + async function saveStrongStart(strongStart: string) { + await pb.collection("documents").update(session.id, { + data: { + ...session.data, + session: { + ...session.data.session, + strongStart, + }, + }, + }); + } + return (

Strong Start

onSubmit({ session: { strongStart: value } })} + onSave={saveStrongStart} placeholder="Enter a strong start for this session..." aria-label="Strong Start" /> diff --git a/src/components/documents/treasure/TreasureEditForm.tsx b/src/components/documents/treasure/TreasureEditForm.tsx new file mode 100644 index 0000000..68ff1fe --- /dev/null +++ b/src/components/documents/treasure/TreasureEditForm.tsx @@ -0,0 +1,90 @@ +// 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"; +import { AutoSaveTextarea } from "@/components/AutoSaveTextarea"; + +/** + * Renders an editable treasure form. + * Handles updating the discovered state and discoveredIn relationship. + */ +export const TreasureEditForm = ({ + 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); + } + } + + async function saveText(text: string) { + await pb.collection("documents").update(treasure.id, { + data: { + ...treasure.data, + treasure: { + ...treasure.data.treasure, + text, + }, + }, + }); + } + + return ( +
+ + +
+ ); +}; diff --git a/src/components/documents/treasure/TreasureRow.tsx b/src/components/documents/treasure/TreasureToggleRow.tsx similarity index 98% rename from src/components/documents/treasure/TreasureRow.tsx rename to src/components/documents/treasure/TreasureToggleRow.tsx index 9bc4a88..516b40a 100644 --- a/src/components/documents/treasure/TreasureRow.tsx +++ b/src/components/documents/treasure/TreasureToggleRow.tsx @@ -8,7 +8,7 @@ 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 = ({ +export const TreasureToggleRow = ({ treasure, session, }: { diff --git a/src/lib/types.ts b/src/lib/types.ts index 8ef9fc9..824c40b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -48,12 +48,32 @@ export type DocumentData = { export type Document = RecordModel & { id: DocumentId; campaign: CampaignId; - data: {}; + data: { + [K in DocumentType]?: unknown; + }; // These two are not in Pocketbase's types, but they seem to always be present created: ISO8601Date; updated: ISO8601Date; }; +export type DocumentType = + | "location" + | "monster" + | "npc" + | "scene" + | "secret" + | "session" + | "treasure"; + +export type AnyDocument = + | Location + | Monster + | Npc + | Scene + | Secret + | Session + | Treasure; + /** Locations **/ export type Location = Document & diff --git a/src/routes/_app/_authenticated/document.$documentId.tsx b/src/routes/_app/_authenticated/document.$documentId.tsx index d010cd0..c2c6cad 100644 --- a/src/routes/_app/_authenticated/document.$documentId.tsx +++ b/src/routes/_app/_authenticated/document.$documentId.tsx @@ -1,16 +1,15 @@ -import _ from "lodash"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { RelationshipList } from "@/components/RelationshipList"; +import { DocumentEditForm } from "@/components/documents/DocumentEditForm"; import { pb } from "@/lib/pocketbase"; +import { displayName } from "@/lib/relationships"; import { RelationshipType, + type AnyDocument, type Relationship, - type Session, - type Document, } from "@/lib/types"; -import { RelationshipList } from "@/components/RelationshipList"; -import { EditSessionForm } from "@/components/documents/session/EditSessionForm"; -import { displayName } from "@/lib/relationships"; -import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/react"; +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import _ from "lodash"; export const Route = createFileRoute( "/_app/_authenticated/document/$documentId", @@ -36,17 +35,11 @@ export const Route = createFileRoute( }); function RouteComponent() { - const { document: session, relationships } = Route.useLoaderData() as { - document: Session; - relationships: Record; + const { document, relationships } = Route.useLoaderData() as { + document: AnyDocument; + relationships: Record; }; - async function handleSaveSession(data: Session["data"]) { - await pb.collection("documents").update(session.id, { - data, - }); - } - console.log("Parsed data: ", relationships); const relationshipList = [ @@ -62,15 +55,12 @@ function RouteComponent() {
Print - + {relationshipList.map((relationshipType) => ( @@ -84,7 +74,7 @@ function RouteComponent() {