Threads done with generic forms.

This commit is contained in:
2025-09-24 15:52:02 -07:00
parent 6979bc4b8f
commit ab323798e9
6 changed files with 106 additions and 99 deletions

View File

@@ -1,6 +1,6 @@
import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath";
import { makeDocumentPath } from "@/lib/documentPath";
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<{
childDocId: DocumentId;

View File

@@ -7,6 +7,7 @@ import {
type DocumentField,
type FieldType,
} from "@/lib/fields";
import { ToggleInput } from "../form/ToggleInput";
export type GenericFieldType = "multiline" | "singleline" | "checkbox";
@@ -70,12 +71,11 @@ const GenericEditFormField = <T extends AnyDocument>({
);
case "toggle":
return (
<input
type="checkbox"
checked={!!field.getter(data)}
onChange={(e) => saveField(!!e.target.value)}
className="accent-emerald-500 w-5 h-5"
id={field.name}
<ToggleInput
label={field.name}
value={!!field.getter(data)}
onChange={saveField}
placeholder={field.name}
/>
);
}

View File

@@ -1,5 +1,5 @@
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
import { useDocumentCache } from "@/context/document/hooks";
import { DocumentTypeLabel } from "@/lib/documents";
import {
getFieldsForType,
type DocumentField,
@@ -14,6 +14,10 @@ import {
type DocumentType,
} from "@/lib/types";
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";
@@ -69,83 +73,68 @@ export const GenericNewDocumentForm = <T extends DocumentType>({
setIsLoading(false);
}, [campaignId, setIsLoading, setError, docData]);
// TODO: display name for docType
return (
<div className="">
{error && (
// TODO: class and style for errors
<div className="error">{error}</div>
)}
{
<BaseForm
title={`Create new ${DocumentTypeLabel[docType]}`}
onSubmit={saveData}
isLoading={isLoading}
error={error}
content={
// The type checker seems to lose the types when using Object.entries here.
fields.map((field) => (
<GenericNewFormField
field={field}
value={field.getter(docData)}
isLoading={isLoading}
onUpdate={updateData(field)}
/>
))
}
<button disabled={isLoading} onClick={saveData}>
Save
</button>
</div>
/>
);
};
const GenericNewFormField = <T extends DocumentType, F extends FieldType>({
field,
value,
isLoading,
onUpdate,
}: {
field: DocumentField<T, F>;
value: ValueForFieldType<F>;
onUpdate: (value: ValueForFieldType<F>) => void;
}) => {
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>;
isLoading: boolean;
onUpdate: (value: ValueForFieldType<F>) => void;
}) => {
switch (field.fieldType) {
case "longText":
return (
<textarea
<MultiLineInput
label={field.name}
value={value as string}
onChange={(e) =>
onUpdate((e.target.value || "") as ValueForFieldType<F>)
}
onChange={onUpdate as (v: string) => void}
disabled={isLoading}
placeholder={field.name}
/>
);
case "shortText":
return (
<input
type="text"
<SingleLineInput
label={field.name}
value={value as string}
onChange={(e) =>
onUpdate((e.target.value || "") as ValueForFieldType<F>)
}
onChange={onUpdate as (v: string) => void}
disabled={isLoading}
placeholder={field.name}
/>
);
case "toggle":
return (
<input
type="checkbox"
checked={value as boolean}
onChange={(e) =>
(onUpdate as (value: boolean) => void)(!!e.target.value)
}
<ToggleInput
label={field.name}
value={value as boolean}
onChange={onUpdate as (v: boolean) => void}
disabled={isLoading}
placeholder={field.name}
/>
);
}

View File

@@ -1,5 +1,3 @@
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 {
@@ -8,7 +6,8 @@ import type {
Relationship,
Session,
} from "@/lib/types";
import { useCallback, useState } from "react";
import { useCallback } from "react";
import { GenericNewDocumentForm } from "../GenericNewDocumentForm";
export type Props = {
campaignId: CampaignId;
@@ -16,14 +15,10 @@ export type 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 createNewSession = useCallback(async () => {
setIsLoading(true);
try {
const createSessionRelations = useCallback(
async (newSession: Session) => {
// Check for a previous session
const prevSession = await pb
.collection("documents")
@@ -31,15 +26,6 @@ export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
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
@@ -56,38 +42,16 @@ export const NewSessionForm = ({ campaignId, onCreate }: Props) => {
});
}
}
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]);
await onCreate(newSession);
},
[campaignId, dispatch],
);
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"
/>
</>
}
<GenericNewDocumentForm
docType="session"
campaignId={campaignId}
onCreate={createSessionRelations}
/>
);
};

View 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>
);