WIP: Linking in document list. Working on copying relationships to new sessions
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lazy-dm",
|
"name": "lazy-dm",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-query": "^5.79.0",
|
"@tanstack/react-query": "^5.79.0",
|
||||||
@@ -67,6 +68,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-lZHnO9BJdHPKnwB0uvVUCyDnIhL+WAHzXQ2EXX0qacogOsnvIUiCgY0BLKhBqTCWln3/f/Ox5jU54MKO6ayh9A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"bind-event-listener": "^3.0.0",
|
||||||
|
"raf-schd": "^4.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -312,7 +324,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
|
||||||
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -2352,6 +2363,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bind-event-listener": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
@@ -3510,6 +3527,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"docker:build": "npm run docker:build:app && npm run docker:build:pocketbase"
|
"docker:build": "npm run docker:build:app && npm run docker:build:pocketbase"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@tanstack/react-query": "^5.79.0",
|
"@tanstack/react-query": "^5.79.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Document } from "@/lib/types";
|
import type { Document, DocumentId } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPanel,
|
DialogPanel,
|
||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
Transition,
|
Transition,
|
||||||
TransitionChild,
|
TransitionChild,
|
||||||
} from "@headlessui/react";
|
} 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> = {
|
type Props<T extends Document> = {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
@@ -14,6 +15,7 @@ type Props<T extends Document> = {
|
|||||||
items: T[];
|
items: T[];
|
||||||
renderRow: (item: T) => React.ReactNode;
|
renderRow: (item: T) => React.ReactNode;
|
||||||
newItemForm: (onSubmit: () => void) => React.ReactNode;
|
newItemForm: (onSubmit: () => void) => React.ReactNode;
|
||||||
|
removeItem: (itemId: DocumentId) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,8 +32,15 @@ export function DocumentList<T extends Document>({
|
|||||||
items,
|
items,
|
||||||
renderRow,
|
renderRow,
|
||||||
newItemForm,
|
newItemForm,
|
||||||
|
removeItem,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const toggleEditMode = useCallback(
|
||||||
|
() => setIsEditing((x) => !x),
|
||||||
|
[setIsEditing],
|
||||||
|
);
|
||||||
|
|
||||||
// Handles closing the dialog after form submission
|
// Handles closing the dialog after form submission
|
||||||
const handleFormSubmit = (): void => {
|
const handleFormSubmit = (): void => {
|
||||||
@@ -42,35 +51,50 @@ export function DocumentList<T extends Document>({
|
|||||||
<section className="w-full max-w-2xl mx-auto">
|
<section className="w-full max-w-2xl mx-auto">
|
||||||
<div className="flex items-center justify-between my-4">
|
<div className="flex items-center justify-between my-4">
|
||||||
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
|
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isEditing && (
|
||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
aria-label="Add new item"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<svg
|
<Icons.Cross />
|
||||||
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>
|
|
||||||
</button>
|
</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}
|
||||||
|
>
|
||||||
|
{isEditing ? <Icons.Done /> : <Icons.Edit />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-900 rounded p-4 text-slate-100">{error}</div>
|
<div className="bg-red-900 rounded p-4 text-slate-100">{error}</div>
|
||||||
)}
|
)}
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li key={item.id} className="bg-slate-800 rounded p-4 text-slate-100">
|
<li
|
||||||
{renderRow(item)}
|
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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
3
src/components/FormattedDate.tsx
Normal file
3
src/components/FormattedDate.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const FormattedDate = ({ date }: { date: string }) => (
|
||||||
|
<span>{new Date(date).toLocaleString()}</span>
|
||||||
|
);
|
||||||
56
src/components/Icons.tsx
Normal file
56
src/components/Icons.tsx
Normal 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>
|
||||||
|
);
|
||||||
@@ -34,7 +34,7 @@ export function RelationshipList({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchItems() {
|
async function fetchItems() {
|
||||||
const { items } = await queryClient.fetchQuery({
|
const { items } = await queryClient.fetchQuery({
|
||||||
queryKey: [root.id, "relationship", relationshipType],
|
queryKey: ["relationship", relationshipType, root.id],
|
||||||
staleTime: 5 * 60 * 1000, // 5 mintues
|
staleTime: 5 * 60 * 1000, // 5 mintues
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -81,7 +81,7 @@ export function RelationshipList({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [root.id, "relationship", relationshipType],
|
queryKey: ["relationship", relationshipType, root.id],
|
||||||
});
|
});
|
||||||
setItems((prev) => [...prev, doc]);
|
setItems((prev) => [...prev, doc]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -101,6 +101,7 @@ export function RelationshipList({
|
|||||||
items={items}
|
items={items}
|
||||||
error={error}
|
error={error}
|
||||||
renderRow={(document) => <DocumentRow document={document} />}
|
renderRow={(document) => <DocumentRow document={document} />}
|
||||||
|
removeItem={() => {}}
|
||||||
newItemForm={(onSubmit) => (
|
newItemForm={(onSubmit) => (
|
||||||
<NewRelatedDocumentForm
|
<NewRelatedDocumentForm
|
||||||
campaignId={root.campaign}
|
campaignId={root.campaign}
|
||||||
|
|||||||
26
src/components/documents/BasicRow.tsx
Normal file
26
src/components/documents/BasicRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,11 +12,7 @@ import {
|
|||||||
type Document,
|
type Document,
|
||||||
type Session,
|
type Session,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
import { LocationPrintRow } from "./location/LocationPrintRow";
|
import { BasicRow } from "./BasicRow";
|
||||||
import { MonsterPrintRow } from "./monsters/MonsterPrintRow";
|
|
||||||
import { NpcPrintRow } from "./npc/NpcPrintRow";
|
|
||||||
import { ScenePrintRow } from "./scene/ScenePrintRow";
|
|
||||||
import { SessionPrintRow } from "./session/SessionPrintRow";
|
|
||||||
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
|
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,19 +27,37 @@ export const DocumentRow = ({
|
|||||||
session?: Session;
|
session?: Session;
|
||||||
}) => {
|
}) => {
|
||||||
if (isLocation(document)) {
|
if (isLocation(document)) {
|
||||||
return <LocationPrintRow location={document} />;
|
return (
|
||||||
|
<BasicRow
|
||||||
|
doc={document}
|
||||||
|
title={document.data.location.name}
|
||||||
|
description={document.data.location.description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMonster(document)) {
|
if (isMonster(document)) {
|
||||||
return <MonsterPrintRow monster={document} />;
|
return <BasicRow doc={document} title={document.data.monster.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNpc(document)) {
|
if (isNpc(document)) {
|
||||||
return <NpcPrintRow npc={document} />;
|
return (
|
||||||
|
<BasicRow
|
||||||
|
doc={document}
|
||||||
|
title={document.data.npc.name}
|
||||||
|
description={document.data.npc.description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSession(document)) {
|
if (isSession(document)) {
|
||||||
return <SessionPrintRow session={document} />;
|
return (
|
||||||
|
<BasicRow
|
||||||
|
doc={document}
|
||||||
|
title={document.created}
|
||||||
|
description={document.data.session.strongStart}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSecret(document)) {
|
if (isSecret(document)) {
|
||||||
@@ -51,7 +65,7 @@ export const DocumentRow = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isScene(document)) {
|
if (isScene(document)) {
|
||||||
return <ScenePrintRow scene={document} />;
|
return <BasicRow doc={document} title={document.data.scene.text} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTreasure(document)) {
|
if (isTreasure(document)) {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
import { AutoSaveTextarea } from "@/components/AutoSaveTextarea";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import type { Scene } from "@/lib/types";
|
import type { Scene } from "@/lib/types";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders an editable scene form
|
* Renders an editable scene form
|
||||||
*/
|
*/
|
||||||
export const SceneEditForm = ({ scene }: { scene: Scene }) => {
|
export const SceneEditForm = ({ scene }: { scene: Scene }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
async function saveScene(text: string) {
|
async function saveScene(text: string) {
|
||||||
await pb.collection("documents").update(scene.id, {
|
await pb.collection("documents").update(scene.id, {
|
||||||
data: {
|
data: {
|
||||||
@@ -15,6 +18,9 @@ export const SceneEditForm = ({ scene }: { scene: Scene }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["relationship"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FormattedDate } from "@/components/FormattedDate";
|
||||||
import type { Session } from "@/lib/types";
|
import type { Session } from "@/lib/types";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ export const SessionRow = ({ session }: { session: Session }) => {
|
|||||||
params={{ documentId: session.id }}
|
params={{ documentId: session.id }}
|
||||||
className="block font-semibold text-lg text-slate-300"
|
className="block font-semibold text-lg text-slate-300"
|
||||||
>
|
>
|
||||||
{session.created}
|
<FormattedDate date={session.created} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="">{session.data.session.strongStart}</div>
|
<div className="">{session.data.session.strongStart}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
import type { RelationshipType } from "./types";
|
import { getDocumentType, RelationshipType, type AnyDocument } from "./types";
|
||||||
|
|
||||||
export function displayName(relationshipType: RelationshipType) {
|
export function displayName(relationshipType: RelationshipType) {
|
||||||
return relationshipType.charAt(0).toUpperCase() + relationshipType.slice(1);
|
return relationshipType.charAt(0).toUpperCase() + relationshipType.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function relationshipsForDocument(doc: AnyDocument): RelationshipType[] {
|
||||||
|
switch (getDocumentType(doc)) {
|
||||||
|
case "session":
|
||||||
|
return [
|
||||||
|
RelationshipType.Scenes,
|
||||||
|
RelationshipType.Secrets,
|
||||||
|
RelationshipType.Locations,
|
||||||
|
RelationshipType.Npcs,
|
||||||
|
RelationshipType.Monsters,
|
||||||
|
RelationshipType.Treasures,
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,6 +74,25 @@ export type AnyDocument =
|
|||||||
| Session
|
| Session
|
||||||
| Treasure;
|
| Treasure;
|
||||||
|
|
||||||
|
export function getDocumentType(doc: AnyDocument): DocumentType {
|
||||||
|
if (isLocation(doc)) {
|
||||||
|
return "location";
|
||||||
|
} else if (isMonster(doc)) {
|
||||||
|
return "monster";
|
||||||
|
} else if (isNpc(doc)) {
|
||||||
|
return "npc";
|
||||||
|
} else if (isScene(doc)) {
|
||||||
|
return "scene";
|
||||||
|
} else if (isSecret(doc)) {
|
||||||
|
return "secret";
|
||||||
|
} else if (isSession(doc)) {
|
||||||
|
return "session";
|
||||||
|
} else if (isTreasure(doc)) {
|
||||||
|
return "treasure";
|
||||||
|
}
|
||||||
|
throw new Error(`Document type not found: ${JSON.stringify(doc)}`);
|
||||||
|
}
|
||||||
|
|
||||||
/** Locations **/
|
/** Locations **/
|
||||||
|
|
||||||
export type Location = Document &
|
export type Location = Document &
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import { SessionRow } from "@/components/documents/session/SessionRow";
|
|||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@headlessui/react";
|
||||||
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
|
import type { Relationship } from "@/lib/types";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_app/_authenticated/campaigns/$campaignId")({
|
export const Route = createFileRoute(
|
||||||
|
"/_app/_authenticated/campaigns/$campaignId",
|
||||||
|
)({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
pendingComponent: Loader,
|
pendingComponent: Loader,
|
||||||
});
|
});
|
||||||
@@ -26,6 +29,7 @@ function RouteComponent() {
|
|||||||
// Fetch all documents for this campaign
|
// Fetch all documents for this campaign
|
||||||
const docs = await pb.collection("documents").getFullList({
|
const docs = await pb.collection("documents").getFullList({
|
||||||
filter: `campaign = "${params.campaignId}"`,
|
filter: `campaign = "${params.campaignId}"`,
|
||||||
|
sort: "-created",
|
||||||
});
|
});
|
||||||
// Filter to only those with data.session
|
// Filter to only those with data.session
|
||||||
const sessions = docs.filter((doc: any) => doc.data && doc.data.session);
|
const sessions = docs.filter((doc: any) => doc.data && doc.data.session);
|
||||||
@@ -37,7 +41,22 @@ function RouteComponent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createNewSession = useCallback(async () => {
|
const createNewSession = useCallback(async () => {
|
||||||
await pb.collection("documents").create({
|
// Check for a previous session
|
||||||
|
const prevSession = await pb
|
||||||
|
.collection("documents")
|
||||||
|
.getFirstListItem(
|
||||||
|
`campaign = "${campaign.id}" && json_extract(data, '$.session') IS NOT NULL`,
|
||||||
|
{
|
||||||
|
sort: "-created",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Previous session: ", {
|
||||||
|
id: prevSession.id,
|
||||||
|
created: prevSession.created,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSession = await pb.collection("documents").create({
|
||||||
campaign: campaign.id,
|
campaign: campaign.id,
|
||||||
data: {
|
data: {
|
||||||
session: {
|
session: {
|
||||||
@@ -45,6 +64,31 @@ function RouteComponent() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["campaign"] });
|
||||||
|
|
||||||
|
// If any, then copy things over
|
||||||
|
if (prevSession) {
|
||||||
|
const prevRelations = await pb
|
||||||
|
.collection<Relationship>("relationships")
|
||||||
|
.getFullList({
|
||||||
|
filter: `primary = "${prevSession.id}"`,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${prevRelations.length} previous relations`);
|
||||||
|
|
||||||
|
for (const relation of prevRelations) {
|
||||||
|
console.log(
|
||||||
|
`Adding ${relation.secondary.length} items to ${relation.type}`,
|
||||||
|
);
|
||||||
|
await pb.collection("relationships").create({
|
||||||
|
primary: newSession.id,
|
||||||
|
type: relation.type,
|
||||||
|
seciondary: relation.secondary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["campaign"] });
|
queryClient.invalidateQueries({ queryKey: ["campaign"] });
|
||||||
}, [campaign]);
|
}, [campaign]);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { useRouter } from "@tanstack/react-router";
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_app/_authenticated/campaigns/")({
|
export const Route = createFileRoute("/_app/_authenticated/campaigns/")({
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
const records = await pb.collection("campaigns").getFullList();
|
const records = await pb.collection("campaigns").getFullList({
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
campaigns: records.map((rec: any) => ({
|
campaigns: records.map((rec: any) => ({
|
||||||
id: rec.id,
|
id: rec.id,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { RelationshipList } from "@/components/RelationshipList";
|
import { RelationshipList } from "@/components/RelationshipList";
|
||||||
import { DocumentEditForm } from "@/components/documents/DocumentEditForm";
|
import { DocumentEditForm } from "@/components/documents/DocumentEditForm";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { displayName } from "@/lib/relationships";
|
import { displayName, relationshipsForDocument } from "@/lib/relationships";
|
||||||
import { RelationshipType, type AnyDocument } from "@/lib/types";
|
import { RelationshipType, type AnyDocument } from "@/lib/types";
|
||||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
@@ -23,17 +23,10 @@ function RouteComponent() {
|
|||||||
document: AnyDocument;
|
document: AnyDocument;
|
||||||
};
|
};
|
||||||
|
|
||||||
const relationshipList = [
|
const relationshipList = relationshipsForDocument(document);
|
||||||
RelationshipType.Scenes,
|
|
||||||
RelationshipType.Secrets,
|
|
||||||
RelationshipType.Locations,
|
|
||||||
RelationshipType.Npcs,
|
|
||||||
RelationshipType.Monsters,
|
|
||||||
RelationshipType.Treasures,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl mx-auto py-2 px-4">
|
<div key={document.id} className="max-w-xl mx-auto py-2 px-4">
|
||||||
<Link
|
<Link
|
||||||
to="/document/$documentId/print"
|
to="/document/$documentId/print"
|
||||||
params={{ documentId: document.id }}
|
params={{ documentId: document.id }}
|
||||||
|
|||||||
Reference in New Issue
Block a user