Compare commits
2 Commits
3390ecfb95
...
4c2ebdc292
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c2ebdc292 | |||
| 8533f63a22 |
@@ -11,7 +11,7 @@
|
|||||||
<title>Dungeon Master's Companion</title>
|
<title>Dungeon Master's Companion</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app" class="flex flex-col h-full w-full"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -13,7 +13,9 @@
|
|||||||
"@tanstack/react-router": "^1.114.3",
|
"@tanstack/react-router": "^1.114.3",
|
||||||
"@tanstack/react-router-devtools": "^1.114.3",
|
"@tanstack/react-router-devtools": "^1.114.3",
|
||||||
"@tanstack/router-plugin": "^1.114.3",
|
"@tanstack/router-plugin": "^1.114.3",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"marked": "^16.1.1",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -2182,6 +2184,13 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
|
||||||
@@ -2754,6 +2763,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.157",
|
"version": "1.5.157",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
|
||||||
@@ -3406,6 +3424,18 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "16.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-16.1.1.tgz",
|
||||||
|
"integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
"@tanstack/react-router": "^1.114.3",
|
"@tanstack/react-router": "^1.114.3",
|
||||||
"@tanstack/react-router-devtools": "^1.114.3",
|
"@tanstack/react-router-devtools": "^1.114.3",
|
||||||
"@tanstack/router-plugin": "^1.114.3",
|
"@tanstack/router-plugin": "^1.114.3",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"marked": "^16.1.1",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export function DocumentList<T extends AnyDocument>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl mx-auto">
|
<section className="w-full">
|
||||||
<div className="flex items-center justify-between my-4">
|
<div className="flex items-center justify-between">
|
||||||
<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">
|
<div className="flex gap-2">
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
@@ -78,7 +78,7 @@ export function DocumentList<T extends AnyDocument>({
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="p-4 bg-slate-800 rounded text-slate-100 flex flex-row justify-between items-center"
|
className="p-2 m-0 border-b-1 last:border-0 border-slate-700 flex flex-row justify-between items-center"
|
||||||
>
|
>
|
||||||
{renderRow(item)}
|
{renderRow(item)}
|
||||||
|
|
||||||
|
|||||||
22
src/components/FormattedText.tsx
Normal file
22
src/components/FormattedText.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import * as Marked from "marked";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatText(text: React.ReactNode): { __html: string } {
|
||||||
|
if (typeof text === "string") {
|
||||||
|
return {
|
||||||
|
__html: DOMPurify.sanitize(
|
||||||
|
Marked.parse(text, { async: false }) as string,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Attempted to safe-render a non-string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormattedText({ children }: React.PropsWithChildren) {
|
||||||
|
return <div dangerouslySetInnerHTML={formatText(children)}></div>;
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ export function RelationshipList({
|
|||||||
title={displayName(relationshipType)}
|
title={displayName(relationshipType)}
|
||||||
items={items}
|
items={items}
|
||||||
error={error}
|
error={error}
|
||||||
renderRow={(document) => <DocumentRow document={document} />}
|
renderRow={(document) => <DocumentRow document={document} root={root} />}
|
||||||
removeItem={handleRemove}
|
removeItem={handleRemove}
|
||||||
newItemForm={(onSubmit) => (
|
newItemForm={(onSubmit) => (
|
||||||
<NewRelatedDocumentForm
|
<NewRelatedDocumentForm
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { AnyDocument } from "@/lib/types";
|
import type { AnyDocument } from "@/lib/types";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { FormattedText } from "../FormattedText";
|
||||||
|
import { DocumentLink } from "./DocumentLink";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
doc: AnyDocument;
|
doc: AnyDocument;
|
||||||
title: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,14 +14,13 @@ export type Props = {
|
|||||||
export const BasicRow = ({ doc, title, description }: Props) => {
|
export const BasicRow = ({ doc, title, description }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<DocumentLink
|
||||||
to="/document/$documentId"
|
childDocId={doc.id}
|
||||||
params={{ documentId: doc.id }}
|
className="!no-underline text-slate-100 hover:underline hover:text-violet-400"
|
||||||
className="text-lg !no-underline text-slate-100 hover:underline hover:text-violet-400"
|
|
||||||
>
|
>
|
||||||
<h4>{title}</h4>
|
{title && <h4 className="font-bold">{title}</h4>}
|
||||||
</Link>
|
{description && <FormattedText>{description}</FormattedText>}
|
||||||
{description && <p>{description}</p>}
|
</DocumentLink>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
21
src/components/documents/DocumentLink.tsx
Normal file
21
src/components/documents/DocumentLink.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath";
|
||||||
|
import type { DocumentId, RelationshipType } from "@/lib/types";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export type Props = React.PropsWithChildren<{
|
||||||
|
childDocId: DocumentId;
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function DocumentLink({ childDocId, className, children }: Props) {
|
||||||
|
const { documentId, relationshipType } = useDocumentPath();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={makeDocumentPath(documentId, relationshipType, childDocId)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
|
|||||||
*/
|
*/
|
||||||
export const DocumentRow = ({
|
export const DocumentRow = ({
|
||||||
document,
|
document,
|
||||||
session,
|
root,
|
||||||
}: {
|
}: {
|
||||||
document: AnyDocument;
|
document: AnyDocument;
|
||||||
session?: Session;
|
root?: AnyDocument;
|
||||||
}) => {
|
}) => {
|
||||||
switch (document.type) {
|
switch (document.type) {
|
||||||
case "location":
|
case "location":
|
||||||
@@ -48,12 +48,12 @@ export const DocumentRow = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
case "secret":
|
case "secret":
|
||||||
return <SecretToggleRow secret={document} session={session} />;
|
return <SecretToggleRow secret={document} root={root} />;
|
||||||
|
|
||||||
case "scene":
|
case "scene":
|
||||||
return <BasicRow doc={document} title={document.data.text} />;
|
return <BasicRow doc={document} description={document.data.text} />;
|
||||||
|
|
||||||
case "treasure":
|
case "treasure":
|
||||||
return <TreasureToggleRow treasure={document} session={session} />;
|
return <TreasureToggleRow treasure={document} root={root} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { DocumentTitle } from "./DocumentTitle";
|
import { DocumentTitle } from "./DocumentTitle";
|
||||||
import { TabbedLayout } from "../layout/TabbedLayout";
|
import { Tab, TabbedLayout } from "../layout/TabbedLayout";
|
||||||
import { DocumentEditForm } from "./DocumentEditForm";
|
import { DocumentEditForm } from "./DocumentEditForm";
|
||||||
import { RelatedDocumentList } from "./RelatedDocumentList";
|
import { RelatedDocumentList } from "./RelatedDocumentList";
|
||||||
|
|
||||||
export function DocumentView({
|
export function DocumentView({
|
||||||
documentId,
|
documentId,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
|
childDocId,
|
||||||
}: {
|
}: {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relationshipType: RelationshipType | null;
|
relationshipType: RelationshipType | null;
|
||||||
|
childDocId: DocumentId | null;
|
||||||
}) {
|
}) {
|
||||||
const { docResult } = useDocument(documentId);
|
const { docResult } = useDocument(documentId);
|
||||||
|
|
||||||
@@ -40,14 +42,14 @@ export function DocumentView({
|
|||||||
<Link
|
<Link
|
||||||
to={CampaignRoute.to}
|
to={CampaignRoute.to}
|
||||||
params={{ campaignId: doc.campaign }}
|
params={{ campaignId: doc.campaign }}
|
||||||
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors mb-4"
|
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
|
||||||
>
|
>
|
||||||
Back to campaign
|
← Back to campaign
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/document/$documentId/print"
|
to="/document/$documentId/print"
|
||||||
params={{ documentId: doc.id }}
|
params={{ documentId: doc.id }}
|
||||||
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors mb-4"
|
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
|
||||||
>
|
>
|
||||||
Print
|
Print
|
||||||
</Link>
|
</Link>
|
||||||
@@ -55,31 +57,24 @@ export function DocumentView({
|
|||||||
}
|
}
|
||||||
title={<DocumentTitle document={doc} />}
|
title={<DocumentTitle document={doc} />}
|
||||||
tabs={[
|
tabs={[
|
||||||
<Link
|
<Tab
|
||||||
key={""}
|
|
||||||
to="/document/$documentId"
|
to="/document/$documentId"
|
||||||
params={{
|
params={{
|
||||||
documentId,
|
documentId,
|
||||||
}}
|
}}
|
||||||
>
|
label="Attributes"
|
||||||
<div className="px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 data-selected:bg-violet-900 data-selected:border-violet-700 whitespace-nowrap">
|
active={relationshipType === null}
|
||||||
Attributes
|
/>,
|
||||||
</div>
|
...relationshipList.map((relationshipEntry) => (
|
||||||
</Link>,
|
<Tab
|
||||||
...relationshipList.map((relationshipType) => (
|
|
||||||
<Link
|
|
||||||
key={relationshipType}
|
|
||||||
to="/document/$documentId/$relationshipType"
|
to="/document/$documentId/$relationshipType"
|
||||||
params={{
|
params={{
|
||||||
documentId,
|
documentId,
|
||||||
relationshipType,
|
relationshipType: relationshipEntry,
|
||||||
}}
|
}}
|
||||||
>
|
label={`${displayName(relationshipEntry)} (${relationshipCounts[relationshipEntry] ?? 0})`}
|
||||||
<div className="px-3 py-2 rounded bg-slate-800 text-slate-100 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 data-selected:bg-violet-900 data-selected:border-violet-700 whitespace-nowrap">
|
active={relationshipEntry === relationshipType}
|
||||||
{displayName(relationshipType)} (
|
/>
|
||||||
{relationshipCounts[relationshipType] ?? 0})
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)),
|
)),
|
||||||
]}
|
]}
|
||||||
content={
|
content={
|
||||||
@@ -92,6 +87,19 @@ export function DocumentView({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
flyout={childDocId && <Flyout key={childDocId} docId={childDocId} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Flyout({ docId }: { docId: DocumentId }) {
|
||||||
|
const { docResult } = useDocument(docId);
|
||||||
|
|
||||||
|
if (docResult?.type !== "ready") {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = docResult.value.doc;
|
||||||
|
|
||||||
|
return <DocumentEditForm document={doc} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// SecretRow.tsx
|
// SecretRow.tsx
|
||||||
// Displays a single secret with discovered checkbox and text.
|
// Displays a single secret with discovered checkbox and text.
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import type { Secret, Session } from "@/lib/types";
|
import type { AnyDocument, Secret } from "@/lib/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { DocumentLink } from "../DocumentLink";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a secret row with a discovered checkbox and secret text.
|
* Renders a secret row with a discovered checkbox and secret text.
|
||||||
@@ -10,10 +11,10 @@ import { useState } from "react";
|
|||||||
*/
|
*/
|
||||||
export const SecretToggleRow = ({
|
export const SecretToggleRow = ({
|
||||||
secret,
|
secret,
|
||||||
session,
|
root,
|
||||||
}: {
|
}: {
|
||||||
secret: Secret;
|
secret: Secret;
|
||||||
session?: Session;
|
root?: AnyDocument;
|
||||||
}) => {
|
}) => {
|
||||||
const [checked, setChecked] = useState(
|
const [checked, setChecked] = useState(
|
||||||
!!(secret.data as any)?.secret?.discovered,
|
!!(secret.data as any)?.secret?.discovered,
|
||||||
@@ -36,7 +37,7 @@ export const SecretToggleRow = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (session || !newChecked) {
|
if (root || !newChecked) {
|
||||||
// If the session exists or the element is being unchecked, remove any
|
// If the session exists or the element is being unchecked, remove any
|
||||||
// existing discoveredIn relationship
|
// existing discoveredIn relationship
|
||||||
const rels = await pb.collection("relationships").getList(1, 1, {
|
const rels = await pb.collection("relationships").getList(1, 1, {
|
||||||
@@ -46,11 +47,11 @@ export const SecretToggleRow = ({
|
|||||||
await pb.collection("relationships").delete(rels.items[0].id);
|
await pb.collection("relationships").delete(rels.items[0].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (session) {
|
if (root) {
|
||||||
if (newChecked) {
|
if (newChecked) {
|
||||||
await pb.collection("relationships").create({
|
await pb.collection("relationships").create({
|
||||||
primary: secret.id,
|
primary: secret.id,
|
||||||
secondary: [session.id],
|
secondary: [root.id],
|
||||||
type: "discoveredIn",
|
type: "discoveredIn",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,12 @@ export const SecretToggleRow = ({
|
|||||||
aria-label="Discovered"
|
aria-label="Discovered"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
{secret.data.text}
|
<DocumentLink
|
||||||
|
childDocId={secret.id}
|
||||||
|
className="!no-underline text-slate-100 hover:underline hover:text-violet-400"
|
||||||
|
>
|
||||||
|
{secret.data.text}
|
||||||
|
</DocumentLink>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// TreasureRow.tsx
|
// TreasureRow.tsx
|
||||||
// Displays a single treasure with discovered checkbox and text.
|
// Displays a single treasure with discovered checkbox and text.
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import type { Session, Treasure } from "@/lib/types";
|
import type { AnyDocument, Treasure } from "@/lib/types";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@ import { useState } from "react";
|
|||||||
*/
|
*/
|
||||||
export const TreasureToggleRow = ({
|
export const TreasureToggleRow = ({
|
||||||
treasure,
|
treasure,
|
||||||
session,
|
root,
|
||||||
}: {
|
}: {
|
||||||
treasure: Treasure;
|
treasure: Treasure;
|
||||||
session?: Session;
|
root?: AnyDocument;
|
||||||
}) => {
|
}) => {
|
||||||
const [checked, setChecked] = useState(
|
const [checked, setChecked] = useState(
|
||||||
!!(treasure.data as any)?.treasure?.discovered,
|
!!(treasure.data as any)?.treasure?.discovered,
|
||||||
@@ -35,7 +35,7 @@ export const TreasureToggleRow = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (session || !newChecked) {
|
if (root || !newChecked) {
|
||||||
// If the session exists or the element is being unchecked, remove any
|
// If the session exists or the element is being unchecked, remove any
|
||||||
// existing discoveredIn relationship
|
// existing discoveredIn relationship
|
||||||
const rels = await pb.collection("relationships").getList(1, 1, {
|
const rels = await pb.collection("relationships").getList(1, 1, {
|
||||||
@@ -45,11 +45,11 @@ export const TreasureToggleRow = ({
|
|||||||
await pb.collection("relationships").delete(rels.items[0].id);
|
await pb.collection("relationships").delete(rels.items[0].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (session) {
|
if (root) {
|
||||||
if (newChecked) {
|
if (newChecked) {
|
||||||
await pb.collection("relationships").create({
|
await pb.collection("relationships").create({
|
||||||
primary: treasure.id,
|
primary: treasure.id,
|
||||||
secondary: [session.id],
|
secondary: [root.id],
|
||||||
type: "discoveredIn",
|
type: "discoveredIn",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,62 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
navigation: React.ReactNode;
|
navigation: React.ReactNode;
|
||||||
tabs: React.ReactNode[];
|
tabs: React.ReactNode[];
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
|
flyout?: React.ReactNode;
|
||||||
};
|
};
|
||||||
export function TabbedLayout({ navigation, title, tabs, content }: Props) {
|
export function TabbedLayout({
|
||||||
|
navigation,
|
||||||
|
title,
|
||||||
|
tabs,
|
||||||
|
content,
|
||||||
|
flyout,
|
||||||
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="grow p-2 flex flex-col">
|
||||||
<div>{navigation}</div>
|
<div>
|
||||||
<div>{title}</div>
|
<div className="flex flex-row gap-2">{navigation}</div>
|
||||||
<div>{tabs}</div>
|
<div>{title}</div>
|
||||||
<div>{content}</div>
|
</div>
|
||||||
|
<div className="flex flex-row justify-start m-2 grow">
|
||||||
|
<div className="shrink-0 grow-0 w-40 p-0">{tabs}</div>
|
||||||
|
<div
|
||||||
|
className={`grow p-2 bg-slate-800 border-t border-b border-r border-slate-700`}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
{flyout && (
|
||||||
|
<div className="w-md p-2 bg-slate-800 border border-slate-700">
|
||||||
|
{flyout}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TabProps = {
|
||||||
|
label: string;
|
||||||
|
to: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
active?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTabClass =
|
||||||
|
"text-slate-100 font-bold bg-slate-800 border-t border-b border-l";
|
||||||
|
const inactiveTabClass = "text-slate-300 bg-slate-900 border";
|
||||||
|
|
||||||
|
export function Tab({ label, to, params, active }: TabProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={label}
|
||||||
|
to={to}
|
||||||
|
params={params}
|
||||||
|
className={`block p-2 border-slate-700 whitespace-nowrap ${active ? activeTabClass : inactiveTabClass}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
54
src/lib/documentPath.ts
Normal file
54
src/lib/documentPath.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useParams } from "@tanstack/react-router";
|
||||||
|
import * as z from "zod";
|
||||||
|
import type { RelationshipType, DocumentId } from "./types";
|
||||||
|
|
||||||
|
const documentParams = z
|
||||||
|
.templateLiteral([
|
||||||
|
z.string(),
|
||||||
|
z.optional(z.literal("/")),
|
||||||
|
z.optional(z.string()),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
z.transform((path: string) => {
|
||||||
|
if (path === "") {
|
||||||
|
return {
|
||||||
|
relationshipType: null,
|
||||||
|
childDocId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const [relationshipType, childDocId] = path.split("/");
|
||||||
|
return {
|
||||||
|
relationshipType: (relationshipType ?? null) as RelationshipType | null,
|
||||||
|
childDocId: (childDocId ?? null) as DocumentId | null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useDocumentPath(): {
|
||||||
|
documentId: DocumentId;
|
||||||
|
relationshipType: RelationshipType | null;
|
||||||
|
childDocId: DocumentId | null;
|
||||||
|
} {
|
||||||
|
const params = useParams({
|
||||||
|
from: "/_app/_authenticated/document/$documentId/$",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { relationshipType, childDocId } = documentParams.parse(params._splat);
|
||||||
|
|
||||||
|
return {
|
||||||
|
documentId: params.documentId as DocumentId,
|
||||||
|
relationshipType,
|
||||||
|
childDocId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeDocumentPath(
|
||||||
|
documentId: DocumentId,
|
||||||
|
relationshipType?: RelationshipType | null,
|
||||||
|
childDocId?: DocumentId | null,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
"/document/" +
|
||||||
|
[documentId, relationshipType, childDocId].filter((x) => x).join("/")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { DocumentView } from "@/components/documents/DocumentView";
|
import { DocumentView } from "@/components/documents/DocumentView";
|
||||||
import { DocumentLoader } from "@/context/document/DocumentLoader";
|
import { DocumentLoader } from "@/context/document/DocumentLoader";
|
||||||
|
import { useDocumentPath } from "@/lib/documentPath";
|
||||||
import type { DocumentId } from "@/lib/types";
|
import type { DocumentId } from "@/lib/types";
|
||||||
import { RelationshipType } from "@/lib/types";
|
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import * as z from "zod";
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_app/_authenticated/document/$documentId/$",
|
"/_app/_authenticated/document/$documentId/$",
|
||||||
@@ -11,38 +10,15 @@ export const Route = createFileRoute(
|
|||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentParams = z
|
|
||||||
.templateLiteral([
|
|
||||||
z.string(),
|
|
||||||
z.optional(z.literal("/")),
|
|
||||||
z.optional(z.string()),
|
|
||||||
])
|
|
||||||
.pipe(
|
|
||||||
z.transform((path: string) => {
|
|
||||||
if (path === "") {
|
|
||||||
return {
|
|
||||||
relationshipType: null,
|
|
||||||
childDoc: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [relationshipType, childDoc] = path.split("/");
|
|
||||||
return {
|
|
||||||
relationshipType: (relationshipType ?? null) as RelationshipType | null,
|
|
||||||
childDoc: (childDoc ?? null) as DocumentId | null,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { documentId, _splat } = Route.useParams();
|
const { documentId, relationshipType, childDocId } = useDocumentPath();
|
||||||
|
|
||||||
const { relationshipType, childDoc } = documentParams.parse(_splat);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentLoader documentId={documentId as DocumentId}>
|
<DocumentLoader documentId={documentId as DocumentId}>
|
||||||
<DocumentView
|
<DocumentView
|
||||||
documentId={documentId as DocumentId}
|
documentId={documentId as DocumentId}
|
||||||
relationshipType={relationshipType}
|
relationshipType={relationshipType}
|
||||||
|
childDocId={childDocId}
|
||||||
/>
|
/>
|
||||||
</DocumentLoader>
|
</DocumentLoader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@tailwind utilities;
|
@import "tailwindcss/utilities";
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@@ -17,6 +17,12 @@ body {
|
|||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The container for all content */
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
pre {
|
pre {
|
||||||
font-family: "Fira Mono", "Menlo", "Monaco", "Consolas", monospace;
|
font-family: "Fira Mono", "Menlo", "Monaco", "Consolas", monospace;
|
||||||
|
|||||||
Reference in New Issue
Block a user