Adding UI for threads

This commit is contained in:
2025-08-09 15:49:36 -07:00
parent 135debdf7f
commit 1c26daa828
5 changed files with 124 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import { SceneEditForm } from "./scene/SceneEditForm";
import { SecretEditForm } from "./secret/SecretEditForm"; import { SecretEditForm } from "./secret/SecretEditForm";
import { SessionEditForm } from "./session/SessionEditForm"; import { SessionEditForm } from "./session/SessionEditForm";
import { TreasureEditForm } from "./treasure/TreasureEditForm"; import { TreasureEditForm } from "./treasure/TreasureEditForm";
import { GenericEditForm } from "./GenericEditForm";
/** /**
* Renders a form for any document type depending on the relationship. * Renders a form for any document type depending on the relationship.
@@ -26,5 +27,15 @@ export const DocumentEditForm = ({ document }: { document: AnyDocument }) => {
return <SessionEditForm session={document} />; return <SessionEditForm session={document} />;
case "treasure": case "treasure":
return <TreasureEditForm treasure={document} />; return <TreasureEditForm treasure={document} />;
case "thread":
return (
<GenericEditForm
doc={document}
fields={{
text: "multiline",
resolved: "checkbox",
}}
/>
);
} }
}; };

View File

@@ -32,6 +32,15 @@ export const DocumentPreview = ({ doc }: { doc: AnyDocument }) => {
const ShowDocument = ({ doc }: { doc: AnyDocument }) => { const ShowDocument = ({ doc }: { doc: AnyDocument }) => {
switch (doc.type) { switch (doc.type) {
case "front":
return (
<BasicPreview
id={doc.id}
title={doc.data.name}
description={doc.data.description}
/>
);
case "location": case "location":
return ( return (
<BasicPreview <BasicPreview
@@ -68,6 +77,9 @@ const ShowDocument = ({ doc }: { doc: AnyDocument }) => {
case "scene": case "scene":
return <BasicPreview id={doc.id} description={doc.data.text} />; return <BasicPreview id={doc.id} description={doc.data.text} />;
case "thread":
return <BasicPreview id={doc.id} title={doc.data.text} />;
case "treasure": case "treasure":
return <BasicPreview id={doc.id} title={doc.data.text} />; return <BasicPreview id={doc.id} title={doc.data.text} />;
} }

View File

@@ -17,6 +17,15 @@ export const DocumentRow = ({
root?: AnyDocument; root?: AnyDocument;
}) => { }) => {
switch (document.type) { switch (document.type) {
case "front":
return (
<BasicRow
doc={document}
title={document.data.name}
description={document.data.description}
/>
);
case "location": case "location":
return ( return (
<BasicRow <BasicRow
@@ -53,6 +62,9 @@ export const DocumentRow = ({
case "scene": case "scene":
return <BasicRow doc={document} description={document.data.text} />; return <BasicRow doc={document} description={document.data.text} />;
case "thread":
return <BasicRow doc={document} description={document.data.text} />;
case "treasure": case "treasure":
return <TreasureToggleRow treasure={document} root={root} />; return <TreasureToggleRow treasure={document} root={root} />;
} }

View File

@@ -0,0 +1,87 @@
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
import { pb } from "@/lib/pocketbase";
import type { AnyDocument, Location } from "@/lib/types";
import { useDocumentCache } from "@/context/document/hooks";
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
export type Props<T extends AnyDocument> = {
doc: T;
fields: { [K in keyof T["data"]]: GenericFieldType };
};
export const GenericEditForm = <T extends AnyDocument>({
doc,
fields,
}: Props<T>) => {
return (
<div className="">
{
// The type checker seems to lose the types when using Object.entries here.
Object.entries(fields).map(
([fieldName, fieldType]: [string, unknown]) => (
<GenericEditFormField
key={fieldName}
doc={doc}
fieldName={fieldName as keyof T["data"]}
fieldType={fieldType as GenericFieldType}
/>
),
)
}
</div>
);
};
const GenericEditFormField = <T extends AnyDocument>({
doc,
fieldName,
fieldType,
}: {
doc: T;
fieldName: keyof T["data"];
fieldType: GenericFieldType;
}) => {
const { dispatch } = useDocumentCache();
// The type checker really doesn't like indexing into this type implicitly, so we'll store it in a temporary to give it the right hints.
const data = doc.data as T["data"];
async function saveField(value: string) {
const updated: T = await pb.collection("documents").update(doc.id, {
data: {
...doc.data,
[fieldName]: value,
},
});
dispatch({ type: "setDocument", doc: updated });
}
switch (fieldType) {
case "multiline":
return (
<AutoSaveTextarea
multiline={true}
value={data[fieldName] as string}
onSave={saveField}
/>
);
case "singleline":
return (
<AutoSaveTextarea
multiline={false}
value={data[fieldName] as string}
onSave={saveField}
/>
);
case "checkbox":
return (
<input
type="checkbox"
checked={data[fieldName] as boolean}
onChange={(e) => saveField(e.target.value)}
className="accent-emerald-500 w-5 h-5"
/>
);
}
};

View File

@@ -20,6 +20,8 @@ export const NewCampaignDocumentForm = ({
switch (docType) { switch (docType) {
case "session": case "session":
return <NewSessionForm campaignId={campaignId} onCreate={onCreate} />; return <NewSessionForm campaignId={campaignId} onCreate={onCreate} />;
case "thread":
return <NewThreadForm campaignId={campaignId} onCreate={onCreate} />;
default: default:
throw new Error( throw new Error(
`Rendered NewCampaignDocumentForm with unsupported docType: ${docType}`, `Rendered NewCampaignDocumentForm with unsupported docType: ${docType}`,