Threads done with generic forms.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath";
|
import { makeDocumentPath } from "@/lib/documentPath";
|
||||||
import type { DocumentId } from "@/lib/types";
|
import type { DocumentId } from "@/lib/types";
|
||||||
import { Link, useParams, useSearch } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export type Props = React.PropsWithChildren<{
|
export type Props = React.PropsWithChildren<{
|
||||||
childDocId: DocumentId;
|
childDocId: DocumentId;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type DocumentField,
|
type DocumentField,
|
||||||
type FieldType,
|
type FieldType,
|
||||||
} from "@/lib/fields";
|
} from "@/lib/fields";
|
||||||
|
import { ToggleInput } from "../form/ToggleInput";
|
||||||
|
|
||||||
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
||||||
|
|
||||||
@@ -70,12 +71,11 @@ const GenericEditFormField = <T extends AnyDocument>({
|
|||||||
);
|
);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
<input
|
<ToggleInput
|
||||||
type="checkbox"
|
label={field.name}
|
||||||
checked={!!field.getter(data)}
|
value={!!field.getter(data)}
|
||||||
onChange={(e) => saveField(!!e.target.value)}
|
onChange={saveField}
|
||||||
className="accent-emerald-500 w-5 h-5"
|
placeholder={field.name}
|
||||||
id={field.name}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
|
||||||
import { useDocumentCache } from "@/context/document/hooks";
|
import { useDocumentCache } from "@/context/document/hooks";
|
||||||
|
import { DocumentTypeLabel } from "@/lib/documents";
|
||||||
import {
|
import {
|
||||||
getFieldsForType,
|
getFieldsForType,
|
||||||
type DocumentField,
|
type DocumentField,
|
||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
type DocumentType,
|
type DocumentType,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import { BaseForm } from "../form/BaseForm";
|
||||||
|
import { MultiLineInput } from "../form/MultiLineInput";
|
||||||
|
import { SingleLineInput } from "../form/SingleLineInput";
|
||||||
|
import { ToggleInput } from "../form/ToggleInput";
|
||||||
|
|
||||||
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
|
||||||
|
|
||||||
@@ -69,83 +73,68 @@ export const GenericNewDocumentForm = <T extends DocumentType>({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [campaignId, setIsLoading, setError, docData]);
|
}, [campaignId, setIsLoading, setError, docData]);
|
||||||
|
|
||||||
|
// TODO: display name for docType
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<BaseForm
|
||||||
{error && (
|
title={`Create new ${DocumentTypeLabel[docType]}`}
|
||||||
// TODO: class and style for errors
|
onSubmit={saveData}
|
||||||
<div className="error">{error}</div>
|
isLoading={isLoading}
|
||||||
)}
|
error={error}
|
||||||
{
|
content={
|
||||||
// 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.
|
||||||
fields.map((field) => (
|
fields.map((field) => (
|
||||||
<GenericNewFormField
|
<GenericNewFormField
|
||||||
field={field}
|
field={field}
|
||||||
value={field.getter(docData)}
|
value={field.getter(docData)}
|
||||||
|
isLoading={isLoading}
|
||||||
onUpdate={updateData(field)}
|
onUpdate={updateData(field)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
<button disabled={isLoading} onClick={saveData}>
|
/>
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GenericNewFormField = <T extends DocumentType, F extends FieldType>({
|
const GenericNewFormField = <T extends DocumentType, F extends FieldType>({
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
|
isLoading,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: {
|
}: {
|
||||||
field: DocumentField<T, F>;
|
field: DocumentField<T, F>;
|
||||||
value: ValueForFieldType<F>;
|
value: ValueForFieldType<F>;
|
||||||
onUpdate: (value: ValueForFieldType<F>) => void;
|
isLoading: boolean;
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor={field.name}>{field.name}</label>
|
|
||||||
<GenericNewFormInput field={field} value={value} onUpdate={onUpdate} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GenericNewFormInput = <T extends DocumentType, F extends FieldType>({
|
|
||||||
field,
|
|
||||||
value,
|
|
||||||
onUpdate,
|
|
||||||
}: {
|
|
||||||
field: DocumentField<T, F>;
|
|
||||||
value: ValueForFieldType<F>;
|
|
||||||
onUpdate: (value: ValueForFieldType<F>) => void;
|
onUpdate: (value: ValueForFieldType<F>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
switch (field.fieldType) {
|
switch (field.fieldType) {
|
||||||
case "longText":
|
case "longText":
|
||||||
return (
|
return (
|
||||||
<textarea
|
<MultiLineInput
|
||||||
|
label={field.name}
|
||||||
value={value as string}
|
value={value as string}
|
||||||
onChange={(e) =>
|
onChange={onUpdate as (v: string) => void}
|
||||||
onUpdate((e.target.value || "") as ValueForFieldType<F>)
|
disabled={isLoading}
|
||||||
}
|
placeholder={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "shortText":
|
case "shortText":
|
||||||
return (
|
return (
|
||||||
<input
|
<SingleLineInput
|
||||||
type="text"
|
label={field.name}
|
||||||
value={value as string}
|
value={value as string}
|
||||||
onChange={(e) =>
|
onChange={onUpdate as (v: string) => void}
|
||||||
onUpdate((e.target.value || "") as ValueForFieldType<F>)
|
disabled={isLoading}
|
||||||
}
|
placeholder={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
<input
|
<ToggleInput
|
||||||
type="checkbox"
|
label={field.name}
|
||||||
checked={value as boolean}
|
value={value as boolean}
|
||||||
onChange={(e) =>
|
onChange={onUpdate as (v: boolean) => void}
|
||||||
(onUpdate as (value: boolean) => void)(!!e.target.value)
|
disabled={isLoading}
|
||||||
}
|
placeholder={field.name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { BaseForm } from "@/components/form/BaseForm";
|
|
||||||
import { SingleLineInput } from "@/components/form/SingleLineInput";
|
|
||||||
import { useDocumentCache } from "@/context/document/hooks";
|
import { useDocumentCache } from "@/context/document/hooks";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import type {
|
import type {
|
||||||
@@ -8,7 +6,8 @@ import type {
|
|||||||
Relationship,
|
Relationship,
|
||||||
Session,
|
Session,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { GenericNewDocumentForm } from "../GenericNewDocumentForm";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
campaignId: CampaignId;
|
campaignId: CampaignId;
|
||||||
@@ -16,14 +15,10 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
|
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();
|
const { dispatch } = useDocumentCache();
|
||||||
|
|
||||||
const createNewSession = useCallback(async () => {
|
const createSessionRelations = useCallback(
|
||||||
setIsLoading(true);
|
async (newSession: Session) => {
|
||||||
try {
|
|
||||||
// Check for a previous session
|
// Check for a previous session
|
||||||
const prevSession = await pb
|
const prevSession = await pb
|
||||||
.collection("documents")
|
.collection("documents")
|
||||||
@@ -31,15 +26,6 @@ export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
|
|||||||
sort: "-created",
|
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 any relations, then copy things over
|
||||||
if (prevSession) {
|
if (prevSession) {
|
||||||
const prevRelations = await pb
|
const prevRelations = await pb
|
||||||
@@ -56,38 +42,16 @@ export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatch({
|
await onCreate(newSession);
|
||||||
type: "setDocument",
|
},
|
||||||
doc: newSession,
|
[campaignId, dispatch],
|
||||||
});
|
);
|
||||||
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 (
|
return (
|
||||||
<BaseForm
|
<GenericNewDocumentForm
|
||||||
title="Create new session"
|
docType="session"
|
||||||
onSubmit={createNewSession}
|
campaignId={campaignId}
|
||||||
isLoading={isLoading}
|
onCreate={createSessionRelations}
|
||||||
error={error}
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<SingleLineInput
|
|
||||||
label="Name"
|
|
||||||
value={name}
|
|
||||||
onChange={setName}
|
|
||||||
disabled={isLoading}
|
|
||||||
placeholder="Enter session name"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
29
src/components/form/ToggleInput.tsx
Normal file
29
src/components/form/ToggleInput.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export type Props = {
|
||||||
|
value: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
} & Omit<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
"value" | "onChange" | "className"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ToggleInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className = "",
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}: Props) => (
|
||||||
|
<div className="flex flex-row gap-4 p-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
className={`rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${className}`}
|
||||||
|
aria-label={label}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{label && <label>{label}</label>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
25
src/lib/documents.ts
Normal file
25
src/lib/documents.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { DocumentType } from "./types";
|
||||||
|
|
||||||
|
export const DocumentTypeLabel: Record<DocumentType, string> = {
|
||||||
|
session: "Session",
|
||||||
|
secret: "Secret",
|
||||||
|
npc: "NPC",
|
||||||
|
location: "Location",
|
||||||
|
thread: "Thread",
|
||||||
|
front: "Front",
|
||||||
|
monster: "Monster",
|
||||||
|
scene: "Scene",
|
||||||
|
treasure: "Treasure",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentTypeLabePlural: Record<DocumentType, string> = {
|
||||||
|
session: "Sessions",
|
||||||
|
secret: "Secrets",
|
||||||
|
npc: "NPCs",
|
||||||
|
location: "Locations",
|
||||||
|
thread: "Threads",
|
||||||
|
front: "Fronts",
|
||||||
|
monster: "Monsters",
|
||||||
|
scene: "Scenes",
|
||||||
|
treasure: "Treasures",
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user