Prototype of making the new threads via generic interfaces

This commit is contained in:
2025-09-17 16:39:50 -07:00
parent 1c26daa828
commit 43afdc8684
3 changed files with 130 additions and 38 deletions

View File

@@ -1,33 +1,30 @@
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
import { pb } from "@/lib/pocketbase";
import type { AnyDocument, Location } from "@/lib/types";
import { getDocumentType, type AnyDocument } from "@/lib/types";
import { useDocumentCache } from "@/context/document/hooks";
import {
getFieldsForType,
type DocumentField,
type FieldType,
} from "@/lib/fields";
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>) => {
export const GenericEditForm = <T extends AnyDocument>({ doc }: Props<T>) => {
const docType = getDocumentType(doc) as T["type"];
const fields = getFieldsForType(docType);
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}
/>
),
)
fields.map((documentField) => (
<GenericEditFormField doc={doc} field={documentField} />
))
}
</div>
);
@@ -35,51 +32,46 @@ export const GenericEditForm = <T extends AnyDocument>({
const GenericEditFormField = <T extends AnyDocument>({
doc,
fieldName,
fieldType,
field,
}: {
doc: T;
fieldName: keyof T["data"];
fieldType: GenericFieldType;
field: DocumentField<T["type"], FieldType>;
}) => {
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) {
async function saveField(value: string | boolean) {
const updated: T = await pb.collection("documents").update(doc.id, {
data: {
...doc.data,
[fieldName]: value,
},
data: field.setter(value, doc.data),
});
dispatch({ type: "setDocument", doc: updated });
}
switch (fieldType) {
case "multiline":
switch (field.fieldType) {
case "longText":
return (
<AutoSaveTextarea
multiline={true}
value={data[fieldName] as string}
value={field.getter(data) as string}
onSave={saveField}
/>
);
case "singleline":
case "shortText":
return (
<AutoSaveTextarea
multiline={false}
value={data[fieldName] as string}
value={field.getter(data) as string}
onSave={saveField}
/>
);
case "checkbox":
case "toggle":
return (
<input
type="checkbox"
checked={data[fieldName] as boolean}
onChange={(e) => saveField(e.target.value)}
checked={!!field.getter(data)}
onChange={(e) => saveField(!!e.target.value)}
className="accent-emerald-500 w-5 h-5"
/>
);

88
src/lib/fields.ts Normal file
View File

@@ -0,0 +1,88 @@
import {
type DocumentData,
type DocumentsByType,
type DocumentType,
} from "./types";
export type FieldType = "shortText" | "longText" | "toggle";
export type ValueForFieldType<T extends FieldType> = {
shortText: string;
longText: string;
toggle: boolean;
}[T];
export type DocumentField<D extends DocumentType, F extends FieldType> = {
name: string;
fieldType: F;
getter: (doc: DocumentData<D>) => ValueForFieldType<F>;
setter: (
value: ValueForFieldType<F>,
doc: DocumentData<D>,
) => DocumentData<D>;
};
const simpleField = <D extends DocumentType, F extends FieldType>(
name: string,
key: keyof DocumentData<D>,
fieldType: F,
): DocumentField<D, F> => ({
name,
fieldType,
getter: (doc) => doc[key] as unknown as ValueForFieldType<F>,
setter: (value, doc) => ({ ...doc, [key]: value }),
});
const simpleFields = <D extends DocumentType>(
fields: Record<string, [keyof DocumentData<D>, FieldType]>,
): DocumentField<D, FieldType>[] =>
Object.entries(fields).map(([name, [key, fieldType]]) =>
simpleField(name, key, fieldType),
);
export function getFieldsForType<D extends DocumentType>(
docType: D,
): DocumentField<D, FieldType>[] {
switch (docType) {
case "front":
return simpleFields<"front">({
Name: ["name", "shortText"],
Description: ["description", "longText"],
Resolved: ["resolved", "toggle"],
});
case "location":
return [
simpleField("Name", "name", "shortText"),
simpleField("Description", "description", "longText"),
];
case "monster":
return [simpleField("Name", "name", "shortText")];
case "npc":
return [
simpleField("Name", "name", "shortText"),
simpleField("Description", "description", "longText"),
];
case "scene":
return [simpleField("Text", "text", "longText")];
case "secret":
return [
simpleField("Discovered", "discovered", "toggle"),
simpleField("Text", "text", "shortText"),
];
case "session":
return [
simpleField("Name", "name", "shortText"),
simpleField("Strong Start", "strongStart", "longText"),
];
case "thread":
return [
simpleField("Resolved", "resolved", "toggle"),
simpleField("Text", "text", "shortText"),
];
case "treasure":
return [
simpleField("Discovered", "discovered", "toggle"),
simpleField("Text", "text", "shortText"),
];
}
}

View File

@@ -75,11 +75,6 @@ export type DocumentType =
| "thread"
| "treasure";
export type DocumentData<Type extends DocumentType, Data> = {
type: Type;
data: Data;
};
export type Document<Type extends DocumentType, Data> = RecordModel & {
id: DocumentId;
collectionName: typeof CollectionIds.Documents;
@@ -102,6 +97,23 @@ export type AnyDocument =
| Thread
| Treasure;
export type DocumentsByType = {
front: Front;
location: Location;
monster: Monster;
npc: Npc;
scene: Scene;
secret: Secret;
session: Session;
thread: Thread;
treasure: Treasure;
};
export type DocumentData<Type extends DocumentType> =
DocumentsByType[Type]["data"];
export type GetDocumentType<D extends AnyDocument> = D["type"];
export function getDocumentType(doc: AnyDocument): DocumentType {
return doc.type;
}