Prototype of making the new threads via generic interfaces
This commit is contained in:
@@ -1,33 +1,30 @@
|
|||||||
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
||||||
import { pb } from "@/lib/pocketbase";
|
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 { useDocumentCache } from "@/context/document/hooks";
|
||||||
|
import {
|
||||||
|
getFieldsForType,
|
||||||
|
type DocumentField,
|
||||||
|
type FieldType,
|
||||||
|
} from "@/lib/fields";
|
||||||
|
|
||||||
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
||||||
|
|
||||||
export type Props<T extends AnyDocument> = {
|
export type Props<T extends AnyDocument> = {
|
||||||
doc: T;
|
doc: T;
|
||||||
fields: { [K in keyof T["data"]]: GenericFieldType };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GenericEditForm = <T extends AnyDocument>({
|
export const GenericEditForm = <T extends AnyDocument>({ doc }: Props<T>) => {
|
||||||
doc,
|
const docType = getDocumentType(doc) as T["type"];
|
||||||
fields,
|
const fields = getFieldsForType(docType);
|
||||||
}: Props<T>) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
{
|
{
|
||||||
// The type checker seems to lose the types when using Object.entries here.
|
// The type checker seems to lose the types when using Object.entries here.
|
||||||
Object.entries(fields).map(
|
fields.map((documentField) => (
|
||||||
([fieldName, fieldType]: [string, unknown]) => (
|
<GenericEditFormField doc={doc} field={documentField} />
|
||||||
<GenericEditFormField
|
))
|
||||||
key={fieldName}
|
|
||||||
doc={doc}
|
|
||||||
fieldName={fieldName as keyof T["data"]}
|
|
||||||
fieldType={fieldType as GenericFieldType}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -35,51 +32,46 @@ export const GenericEditForm = <T extends AnyDocument>({
|
|||||||
|
|
||||||
const GenericEditFormField = <T extends AnyDocument>({
|
const GenericEditFormField = <T extends AnyDocument>({
|
||||||
doc,
|
doc,
|
||||||
fieldName,
|
field,
|
||||||
fieldType,
|
|
||||||
}: {
|
}: {
|
||||||
doc: T;
|
doc: T;
|
||||||
fieldName: keyof T["data"];
|
field: DocumentField<T["type"], FieldType>;
|
||||||
fieldType: GenericFieldType;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { dispatch } = useDocumentCache();
|
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.
|
// 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"];
|
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, {
|
const updated: T = await pb.collection("documents").update(doc.id, {
|
||||||
data: {
|
data: field.setter(value, doc.data),
|
||||||
...doc.data,
|
|
||||||
[fieldName]: value,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
dispatch({ type: "setDocument", doc: updated });
|
dispatch({ type: "setDocument", doc: updated });
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (fieldType) {
|
switch (field.fieldType) {
|
||||||
case "multiline":
|
case "longText":
|
||||||
return (
|
return (
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={true}
|
multiline={true}
|
||||||
value={data[fieldName] as string}
|
value={field.getter(data) as string}
|
||||||
onSave={saveField}
|
onSave={saveField}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "singleline":
|
case "shortText":
|
||||||
return (
|
return (
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={data[fieldName] as string}
|
value={field.getter(data) as string}
|
||||||
onSave={saveField}
|
onSave={saveField}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "checkbox":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={data[fieldName] as boolean}
|
checked={!!field.getter(data)}
|
||||||
onChange={(e) => saveField(e.target.value)}
|
onChange={(e) => saveField(!!e.target.value)}
|
||||||
className="accent-emerald-500 w-5 h-5"
|
className="accent-emerald-500 w-5 h-5"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
88
src/lib/fields.ts
Normal file
88
src/lib/fields.ts
Normal 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"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,11 +75,6 @@ export type DocumentType =
|
|||||||
| "thread"
|
| "thread"
|
||||||
| "treasure";
|
| "treasure";
|
||||||
|
|
||||||
export type DocumentData<Type extends DocumentType, Data> = {
|
|
||||||
type: Type;
|
|
||||||
data: Data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Document<Type extends DocumentType, Data> = RecordModel & {
|
export type Document<Type extends DocumentType, Data> = RecordModel & {
|
||||||
id: DocumentId;
|
id: DocumentId;
|
||||||
collectionName: typeof CollectionIds.Documents;
|
collectionName: typeof CollectionIds.Documents;
|
||||||
@@ -102,6 +97,23 @@ export type AnyDocument =
|
|||||||
| Thread
|
| Thread
|
||||||
| Treasure;
|
| 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 {
|
export function getDocumentType(doc: AnyDocument): DocumentType {
|
||||||
return doc.type;
|
return doc.type;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user