Adds new campaign form. Adds fronts and thread types

This commit is contained in:
2025-08-03 14:27:06 -07:00
parent 2fbc2c853f
commit 135debdf7f
15 changed files with 319 additions and 75 deletions

View File

@@ -1,6 +1,7 @@
import {
type AnyDocument,
type CampaignId,
type DocumentId,
type DocumentType,
} from "@/lib/types";
import { useDocumentCache } from "@/context/document/hooks";
@@ -9,6 +10,7 @@ import { getAllDocumentsOfType } from "@/context/document/state";
import { DocumentRow } from "../documents/DocumentRow";
import { pb } from "@/lib/pocketbase";
import { useEffect } from "react";
import { NewCampaignDocumentForm } from "../documents/NewCampaignDocumentForm";
export type Props = {
campaignId: CampaignId;
@@ -40,12 +42,28 @@ export const CampaignDocuments = ({ campaignId, docType }: Props) => {
fetchDocuments();
}, [campaignId, docType]);
const handleRemove = (id: DocumentId) => {
pb.collection("documents").delete(id);
dispatch({
type: "removeDocument",
docId: id,
});
};
return (
<DocumentList
items={items}
renderRow={(doc) => <DocumentRow document={doc} />}
newItemForm={() => <div>New Item Form</div>}
removeItem={() => console.error("TODO")}
newItemForm={(onSubmit) => (
<NewCampaignDocumentForm
campaignId={campaignId}
docType={docType}
onCreate={async () => {
onSubmit();
}}
/>
)}
removeItem={handleRemove}
/>
);
};

View File

@@ -57,7 +57,7 @@ const ShowDocument = ({ doc }: { doc: AnyDocument }) => {
return (
<BasicPreview
id={doc.id}
title={doc.created}
title={doc.data.name ?? doc.created}
description={doc.data.strongStart}
/>
);

View File

@@ -1,7 +1,7 @@
// DocumentRow.tsx
// Generic row component for displaying any document type.
import { SecretToggleRow } from "@/components/documents/secret/SecretToggleRow";
import { type AnyDocument, type Session } from "@/lib/types";
import { type AnyDocument } from "@/lib/types";
import { BasicRow } from "./BasicRow";
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
@@ -42,7 +42,7 @@ export const DocumentRow = ({
return (
<BasicRow
doc={document}
title={document.created}
title={document.data.name || document.created}
description={document.data.strongStart}
/>
);

View File

@@ -1,24 +1,28 @@
import { type AnyDocument, type Session } from "@/lib/types";
import { type AnyDocument } from "@/lib/types";
import { FormattedDate } from "../FormattedDate";
/**
* Renders the document title to go at the top a document page.
*/
export const DocumentTitle = ({
document,
}: {
document: AnyDocument;
session?: Session;
}) => {
switch (document.type) {
export const DocumentTitle = ({ doc }: { doc: AnyDocument }) => {
return (
<h1 className="text-2xl font-bold">
<TitleText doc={doc} />
</h1>
);
};
const TitleText = ({ doc }: { doc: AnyDocument }) => {
switch (doc.type) {
case "session":
return (
<h1>
<FormattedDate date={document.created} />
</h1>
);
if (doc.data.name) {
return doc.data.name;
}
return <FormattedDate date={doc.created} />;
default:
return <h1>document.type</h1>;
// TODO: Put in proper names for other document types
return doc.type;
}
};

View File

@@ -48,16 +48,17 @@ export function DocumentView({
>
Back to campaign
</Link>
<Link
to="/document/$documentId/print"
params={{ documentId: doc.id }}
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
>
Print
</Link>
{/* Print link isn't currently working */}
{/* <Link */}
{/* to="/document/$documentId/print" */}
{/* params={{ documentId: doc.id }} */}
{/* className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors" */}
{/* > */}
{/* Print */}
{/* </Link> */}
</>
}
title={<DocumentTitle document={doc} />}
title={<DocumentTitle doc={doc} />}
tabs={[
<Tab
to="/document/$documentId"

View File

@@ -0,0 +1,28 @@
import {
type AnyDocument,
type CampaignId,
type DocumentType,
} from "@/lib/types";
import { NewSessionForm } from "./session/NewSessionForm";
/**
* Renders a form for any document type depending on the relationship.
*/
export const NewCampaignDocumentForm = ({
campaignId,
docType,
onCreate,
}: {
campaignId: CampaignId;
docType: DocumentType;
onCreate: (doc: AnyDocument) => Promise<void>;
}) => {
switch (docType) {
case "session":
return <NewSessionForm campaignId={campaignId} onCreate={onCreate} />;
default:
throw new Error(
`Rendered NewCampaignDocumentForm with unsupported docType: ${docType}`,
);
}
};

View File

@@ -0,0 +1,96 @@
import { BaseForm } from "@/components/form/BaseForm";
import { SingleLineInput } from "@/components/form/SingleLineInput";
import { useDocumentCache } from "@/context/document/hooks";
import { pb } from "@/lib/pocketbase";
import type {
AnyDocument,
CampaignId,
Relationship,
Session,
} from "@/lib/types";
import { useCallback, useState } from "react";
export type Props = {
campaignId: CampaignId;
onCreate: (doc: AnyDocument) => Promise<void>;
};
export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState<string>("");
const { dispatch } = useDocumentCache();
console.log("Rendering with name: ", name);
const createNewSession = useCallback(async () => {
setIsLoading(true);
console.log("Creating session: ", name);
try {
// Check for a previous session
const prevSession = await pb
.collection("documents")
.getFirstListItem(`campaign = "${campaignId}" && type = 'session'`, {
sort: "-created",
});
const newSession: Session = await pb.collection("documents").create({
campaign: campaignId,
type: "session",
data: {
name,
strongStart: "",
},
});
// If any relations, then copy things over
if (prevSession) {
const prevRelations = await pb
.collection<Relationship>("relationships")
.getFullList({
filter: `primary = "${prevSession.id}"`,
});
for (const relation of prevRelations) {
await pb.collection("relationships").create({
primary: newSession.id,
type: relation.type,
secondary: relation.secondary,
});
}
}
dispatch({
type: "setDocument",
doc: newSession,
});
await onCreate?.(newSession);
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message);
} else {
setError("An unknown error occurred while creating the session.");
}
}
setIsLoading(false);
}, [campaignId, name, dispatch, setIsLoading, setError]);
return (
<BaseForm
title="Create new session"
onSubmit={createNewSession}
isLoading={isLoading}
error={error}
content={
<>
<SingleLineInput
label="Name"
value={name}
onChange={setName}
disabled={isLoading}
placeholder="Enter session name"
/>
</>
}
/>
);
};

View File

@@ -11,7 +11,7 @@ export const SessionRow = ({ session }: { session: Session }) => {
search={{ tab: "sessions", docId: session.id }}
className="block font-semibold text-lg text-slate-300"
>
<FormattedDate date={session.created} />
{session.name ? session.name : <FormattedDate date={session.created} />}
</Link>
<div className="">{session.data.strongStart}</div>
</div>

View File

@@ -16,7 +16,13 @@ export const BaseForm = ({
onSubmit,
}: Props) => {
return (
<form className="flex flex-col items-left gap-2" onSubmit={onSubmit}>
<form
className="flex flex-col items-left gap-2"
onSubmit={(e) => {
e.preventDefault();
onSubmit(e);
}}
>
<h3 className="text-lg font-semibold text-slate-100">{title}</h3>
<div className="flex flex-col gap-2 w-full items-stretch">{content}</div>
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}