WIP: Linking in document list. Working on copying relationships to new sessions

This commit is contained in:
2025-06-27 17:52:57 -07:00
parent 93536b0ac2
commit 611eaca5b6
15 changed files with 281 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
import type { Document } from "@/lib/types";
import type { Document, DocumentId } from "@/lib/types";
import {
Dialog,
DialogPanel,
@@ -6,7 +6,8 @@ import {
Transition,
TransitionChild,
} from "@headlessui/react";
import { Fragment, useState } from "react";
import { Fragment, useCallback, useState } from "react";
import * as Icons from "@/components/Icons.tsx";
type Props<T extends Document> = {
title: React.ReactNode;
@@ -14,6 +15,7 @@ type Props<T extends Document> = {
items: T[];
renderRow: (item: T) => React.ReactNode;
newItemForm: (onSubmit: () => void) => React.ReactNode;
removeItem: (itemId: DocumentId) => void;
};
/**
@@ -30,8 +32,15 @@ export function DocumentList<T extends Document>({
items,
renderRow,
newItemForm,
removeItem,
}: Props<T>) {
const [open, setOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const toggleEditMode = useCallback(
() => setIsEditing((x) => !x),
[setIsEditing],
);
// Handles closing the dialog after form submission
const handleFormSubmit = (): void => {
@@ -42,35 +51,50 @@ export function DocumentList<T extends Document>({
<section className="w-full max-w-2xl mx-auto">
<div className="flex items-center justify-between my-4">
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-8 h-8 focus:outline-none focus:ring-2 focus:ring-violet-400"
aria-label="Add new item"
onClick={() => setOpen(true)}
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
<div className="flex gap-2">
{isEditing && (
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-8 h-8 focus:outline-none focus:ring-2 focus:ring-violet-400"
aria-label="Add new item"
onClick={() => setOpen(true)}
>
<Icons.Cross />
</button>
)}
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-8 h-8 focus:outline-none focus:ring-2 focus:ring-violet-400"
aria-label={isEditing ? "Exit edit mode" : "Enter edit mode"}
onClick={toggleEditMode}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
{isEditing ? <Icons.Done /> : <Icons.Edit />}
</button>
</div>
</div>
{error && (
<div className="bg-red-900 rounded p-4 text-slate-100">{error}</div>
)}
<ul className="space-y-2">
{items.map((item) => (
<li key={item.id} className="bg-slate-800 rounded p-4 text-slate-100">
{renderRow(item)}
<li
key={item.id}
className="bg-slate-800 rounded p-4 text-slate-100 flex flex-row justify-between items-center"
>
<div>{renderRow(item)}</div>
{isEditing && (
<div>
<button
type="button"
className="inline-flex items-center justify-center rounded-full bg-violet-600 hover:bg-violet-700 text-white w-8 h-8 focus:outline-none focus:ring-2 focus:ring-violet-400"
aria-label="Remove item"
onClick={() => removeItem(item.id)}
>
<Icons.Remove />
</button>
</div>
)}
</li>
))}
</ul>

View File

@@ -0,0 +1,3 @@
export const FormattedDate = ({ date }: { date: string }) => (
<span>{new Date(date).toLocaleString()}</span>
);

56
src/components/Icons.tsx Normal file
View File

@@ -0,0 +1,56 @@
export const Edit = () => (
// Pencil icon
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 5.487a2.25 2.25 0 1 1 3.182 3.182L8.25 20.463 3 21.75l1.287-5.25 12.575-11.013z"
/>
</svg>
);
export const Cross = () => (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
);
export const Remove = () => (
<svg
className="w-5 h-5 rotate-45"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
);
export const Done = () => (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);

View File

@@ -34,7 +34,7 @@ export function RelationshipList({
useEffect(() => {
async function fetchItems() {
const { items } = await queryClient.fetchQuery({
queryKey: [root.id, "relationship", relationshipType],
queryKey: ["relationship", relationshipType, root.id],
staleTime: 5 * 60 * 1000, // 5 mintues
queryFn: async () => {
setLoading(true);
@@ -81,7 +81,7 @@ export function RelationshipList({
});
}
queryClient.invalidateQueries({
queryKey: [root.id, "relationship", relationshipType],
queryKey: ["relationship", relationshipType, root.id],
});
setItems((prev) => [...prev, doc]);
} catch (e: any) {
@@ -101,6 +101,7 @@ export function RelationshipList({
items={items}
error={error}
renderRow={(document) => <DocumentRow document={document} />}
removeItem={() => {}}
newItemForm={(onSubmit) => (
<NewRelatedDocumentForm
campaignId={root.campaign}

View File

@@ -0,0 +1,26 @@
import type { AnyDocument } from "@/lib/types";
import { Link } from "@tanstack/react-router";
export type Props = {
doc: AnyDocument;
title: string;
description?: string;
};
/**
* Renders a simple row that links to the document
*/
export const BasicRow = ({ doc, title, description }: Props) => {
return (
<li>
<Link
to="/document/$documentId"
params={{ documentId: doc.id }}
className="text-lg"
>
<h4>{title}</h4>
</Link>
{description && <p>{description}</p>}
</li>
);
};

View File

@@ -12,11 +12,7 @@ import {
type Document,
type Session,
} from "@/lib/types";
import { LocationPrintRow } from "./location/LocationPrintRow";
import { MonsterPrintRow } from "./monsters/MonsterPrintRow";
import { NpcPrintRow } from "./npc/NpcPrintRow";
import { ScenePrintRow } from "./scene/ScenePrintRow";
import { SessionPrintRow } from "./session/SessionPrintRow";
import { BasicRow } from "./BasicRow";
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
/**
@@ -31,19 +27,37 @@ export const DocumentRow = ({
session?: Session;
}) => {
if (isLocation(document)) {
return <LocationPrintRow location={document} />;
return (
<BasicRow
doc={document}
title={document.data.location.name}
description={document.data.location.description}
/>
);
}
if (isMonster(document)) {
return <MonsterPrintRow monster={document} />;
return <BasicRow doc={document} title={document.data.monster.name} />;
}
if (isNpc(document)) {
return <NpcPrintRow npc={document} />;
return (
<BasicRow
doc={document}
title={document.data.npc.name}
description={document.data.npc.description}
/>
);
}
if (isSession(document)) {
return <SessionPrintRow session={document} />;
return (
<BasicRow
doc={document}
title={document.created}
description={document.data.session.strongStart}
/>
);
}
if (isSecret(document)) {
@@ -51,7 +65,7 @@ export const DocumentRow = ({
}
if (isScene(document)) {
return <ScenePrintRow scene={document} />;
return <BasicRow doc={document} title={document.data.scene.text} />;
}
if (isTreasure(document)) {

View File

@@ -1,11 +1,14 @@
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
import { pb } from "@/lib/pocketbase";
import type { Scene } from "@/lib/types";
import { useQueryClient } from "@tanstack/react-query";
/**
* Renders an editable scene form
*/
export const SceneEditForm = ({ scene }: { scene: Scene }) => {
const queryClient = useQueryClient();
async function saveScene(text: string) {
await pb.collection("documents").update(scene.id, {
data: {
@@ -15,6 +18,9 @@ export const SceneEditForm = ({ scene }: { scene: Scene }) => {
},
},
});
queryClient.invalidateQueries({
queryKey: ["relationship"],
});
}
return (

View File

@@ -1,3 +1,4 @@
import { FormattedDate } from "@/components/FormattedDate";
import type { Session } from "@/lib/types";
import { Link } from "@tanstack/react-router";
@@ -9,7 +10,7 @@ export const SessionRow = ({ session }: { session: Session }) => {
params={{ documentId: session.id }}
className="block font-semibold text-lg text-slate-300"
>
{session.created}
<FormattedDate date={session.created} />
</Link>
<div className="">{session.data.session.strongStart}</div>
</div>