Compare commits
2 Commits
611eaca5b6
...
6ce462a77d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ce462a77d | |||
| c00eb1d965 |
46
pb_migrations/1751082417_updated_documents.js
Normal file
46
pb_migrations/1751082417_updated_documents.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"indexes": [
|
||||||
|
"CREATE INDEX `idx_gxNj5R3hxv` ON `documents` (`type`)"
|
||||||
|
]
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(3, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "select2363381545",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"name": "type",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "select",
|
||||||
|
"values": [
|
||||||
|
"location",
|
||||||
|
"monster",
|
||||||
|
"npc",
|
||||||
|
"scene",
|
||||||
|
"secret",
|
||||||
|
"session",
|
||||||
|
"treasure"
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"indexes": []
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("select2363381545")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
25
pb_migrations/1751082429_updated_documents.js
Normal file
25
pb_migrations/1751082429_updated_documents.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"indexes": [
|
||||||
|
"CREATE INDEX `idx_gxNj5R3hxv` ON `documents` (`type`)",
|
||||||
|
"CREATE INDEX `idx_KtpMErDe1C` ON `documents` (`campaign`)"
|
||||||
|
]
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update collection data
|
||||||
|
unmarshal({
|
||||||
|
"indexes": [
|
||||||
|
"CREATE INDEX `idx_gxNj5R3hxv` ON `documents` (`type`)"
|
||||||
|
]
|
||||||
|
}, collection)
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
54
pb_migrations/1751082553_extract_document_types.js
Normal file
54
pb_migrations/1751082553_extract_document_types.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const DocType = [
|
||||||
|
"location",
|
||||||
|
"monster",
|
||||||
|
"npc",
|
||||||
|
"scene",
|
||||||
|
"secret",
|
||||||
|
"session",
|
||||||
|
"treasure",
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseJsonB(data) {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} else if (data instanceof Array) {
|
||||||
|
return JSON.parse(String.fromCharCode.apply(String, data));
|
||||||
|
}
|
||||||
|
throw new Error("Unsupported data type for JSON parsing");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate(
|
||||||
|
(app) => {
|
||||||
|
let documents = app.findAllRecords("documents");
|
||||||
|
console.log("Records to parse: ", documents.length);
|
||||||
|
|
||||||
|
documents: for (const doc of documents) {
|
||||||
|
if (!doc) continue;
|
||||||
|
|
||||||
|
let data = parseJsonB(doc.get("data"));
|
||||||
|
|
||||||
|
if (data[""]) {
|
||||||
|
data = data[""];
|
||||||
|
}
|
||||||
|
for (const t of DocType) {
|
||||||
|
if (data[t]) {
|
||||||
|
doc.set("type", t);
|
||||||
|
doc.set("data", data[t]);
|
||||||
|
app.save(doc);
|
||||||
|
continue documents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(app) => {
|
||||||
|
// add down queries...
|
||||||
|
let documents = app.findAllRecords("documents");
|
||||||
|
|
||||||
|
for (const doc of documents) {
|
||||||
|
if (!doc) continue;
|
||||||
|
doc.set("data", { [doc.get("type")]: doc.get("data") });
|
||||||
|
app.save(doc);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
42
pb_migrations/1751155422_updated_relationships.js
Normal file
42
pb_migrations/1751155422_updated_relationships.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(0, new Field({
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": true,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(0, new Field({
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
34
pb_migrations/1751155435_updated_documents.js
Normal file
34
pb_migrations/1751155435_updated_documents.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(2, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json2918445923",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "data",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_3332084752")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(2, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json2918445923",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "data",
|
||||||
|
"presentable": true,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
40
pb_migrations/1751156191_updated_relationships.js
Normal file
40
pb_migrations/1751156191_updated_relationships.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(1, new Field({
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"collectionId": "pbc_3332084752",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation390457990",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "primary",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_617371094")
|
||||||
|
|
||||||
|
// update field
|
||||||
|
collection.fields.addAt(1, new Field({
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_3332084752",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation390457990",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "primary",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -63,7 +63,7 @@ export function AutoSaveTextarea({
|
|||||||
<textarea
|
<textarea
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`w-full min-h-[6em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${flash ? "ring-2 ring-emerald-400 border-emerald-400 bg-emerald-950" : ""} ${className}`}
|
className={`w-full min-h-[10em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${flash ? "ring-2 ring-emerald-400 border-emerald-400 bg-emerald-950" : ""} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Document, DocumentId } from "@/lib/types";
|
import type { AnyDocument, DocumentId } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPanel,
|
DialogPanel,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Fragment, useCallback, useState } from "react";
|
import { Fragment, useCallback, useState } from "react";
|
||||||
import * as Icons from "@/components/Icons.tsx";
|
import * as Icons from "@/components/Icons.tsx";
|
||||||
|
|
||||||
type Props<T extends Document> = {
|
type Props<T extends AnyDocument> = {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
error?: React.ReactNode;
|
error?: React.ReactNode;
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -26,7 +26,7 @@ type Props<T extends Document> = {
|
|||||||
* @param renderRow - Function to render each row's content
|
* @param renderRow - Function to render each row's content
|
||||||
* @param newItemForm - Function that renders a form for creating a new item; receives an onSubmit callback
|
* @param newItemForm - Function that renders a form for creating a new item; receives an onSubmit callback
|
||||||
*/
|
*/
|
||||||
export function DocumentList<T extends Document>({
|
export function DocumentList<T extends AnyDocument>({
|
||||||
title,
|
title,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
@@ -122,9 +122,6 @@ export function DocumentList<T extends Document>({
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<DialogPanel className="bg-slate-900 rounded-lg shadow-xl max-w-md w-full p-6 border border-slate-700 relative">
|
<DialogPanel className="bg-slate-900 rounded-lg shadow-xl max-w-md w-full p-6 border border-slate-700 relative">
|
||||||
<DialogTitle className="text-lg font-semibold text-slate-100 mb-4">
|
|
||||||
Add New
|
|
||||||
</DialogTitle>
|
|
||||||
{newItemForm(handleFormSubmit)}
|
{newItemForm(handleFormSubmit)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { pb } from "@/lib/pocketbase";
|
|||||||
import { displayName } from "@/lib/relationships";
|
import { displayName } from "@/lib/relationships";
|
||||||
import type {
|
import type {
|
||||||
AnyDocument,
|
AnyDocument,
|
||||||
Document,
|
DocumentId,
|
||||||
Relationship,
|
Relationship,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
@@ -26,14 +26,15 @@ export function RelationshipList({
|
|||||||
root,
|
root,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
}: RelationshipListProps) {
|
}: RelationshipListProps) {
|
||||||
const [items, setItems] = useState<Document[]>([]);
|
const [items, setItems] = useState<AnyDocument[]>([]);
|
||||||
|
const [relationshipId, setRelationshipId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchItems() {
|
async function fetchItems() {
|
||||||
const { items } = await queryClient.fetchQuery({
|
const { relationship } = await queryClient.fetchQuery({
|
||||||
queryKey: ["relationship", relationshipType, root.id],
|
queryKey: ["relationship", relationshipType, root.id],
|
||||||
staleTime: 5 * 60 * 1000, // 5 mintues
|
staleTime: 5 * 60 * 1000, // 5 mintues
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -49,36 +50,35 @@ export function RelationshipList({
|
|||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return { items: relationship.expand?.secondary ?? [] };
|
return { relationship };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setItems(items);
|
setRelationshipId(relationship.id);
|
||||||
|
setItems(relationship.expand?.secondary ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchItems();
|
fetchItems();
|
||||||
});
|
}, [root, relationshipType]);
|
||||||
|
|
||||||
// Handles creation of a new document and adds it to the relationship
|
// Handles creation of a new document and adds it to the relationship
|
||||||
const handleCreate = async (doc: Document) => {
|
const handleCreate = async (doc: AnyDocument) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// Check for existing relationship
|
// Check for existing relationship
|
||||||
const existing = await pb.collection("relationships").getFullList({
|
if (relationshipId) {
|
||||||
filter: `primary = "${root.id}" && type = "${relationshipType}"`,
|
console.debug("Adding to existing relationship", relationshipId);
|
||||||
});
|
await pb.collection("relationships").update(relationshipId, {
|
||||||
if (existing.length > 0) {
|
|
||||||
console.debug("Adding to existing relationship");
|
|
||||||
await pb.collection("relationships").update(existing[0].id, {
|
|
||||||
"+secondary": doc.id,
|
"+secondary": doc.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.debug("Creating new relationship");
|
console.debug("Creating new relationship");
|
||||||
await pb.collection("relationships").create({
|
const relationship = await pb.collection("relationships").create({
|
||||||
primary: root.id,
|
primary: root.id,
|
||||||
secondary: [doc.id],
|
secondary: [doc.id],
|
||||||
type: relationshipType,
|
type: relationshipType,
|
||||||
});
|
});
|
||||||
|
setRelationshipId(relationship.id);
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["relationship", relationshipType, root.id],
|
queryKey: ["relationship", relationshipType, root.id],
|
||||||
@@ -91,6 +91,32 @@ export function RelationshipList({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemove = async (documentId: DocumentId) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (relationshipId) {
|
||||||
|
console.debug("Removing from existing relationship", relationshipId);
|
||||||
|
await pb.collection("relationships").update(relationshipId, {
|
||||||
|
secondary: items
|
||||||
|
.map((item) => item.id)
|
||||||
|
.filter((id) => id !== documentId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["relationship", relationshipType, root.id],
|
||||||
|
});
|
||||||
|
setItems((prev) => prev.filter((item) => item.id != documentId));
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(
|
||||||
|
e?.message || `Failed to remove document from ${relationshipType}.`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
<Loader />;
|
<Loader />;
|
||||||
}
|
}
|
||||||
@@ -101,12 +127,12 @@ export function RelationshipList({
|
|||||||
items={items}
|
items={items}
|
||||||
error={error}
|
error={error}
|
||||||
renderRow={(document) => <DocumentRow document={document} />}
|
renderRow={(document) => <DocumentRow document={document} />}
|
||||||
removeItem={() => {}}
|
removeItem={handleRemove}
|
||||||
newItemForm={(onSubmit) => (
|
newItemForm={(onSubmit) => (
|
||||||
<NewRelatedDocumentForm
|
<NewRelatedDocumentForm
|
||||||
campaignId={root.campaign}
|
campaignId={root.campaign}
|
||||||
relationshipType={relationshipType}
|
relationshipType={relationshipType}
|
||||||
onCreate={async (doc: Document) => {
|
onCreate={async (doc: AnyDocument) => {
|
||||||
await handleCreate(doc);
|
await handleCreate(doc);
|
||||||
onSubmit();
|
onSubmit();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ export type Props = {
|
|||||||
*/
|
*/
|
||||||
export const BasicRow = ({ doc, title, description }: Props) => {
|
export const BasicRow = ({ doc, title, description }: Props) => {
|
||||||
return (
|
return (
|
||||||
<li>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
to="/document/$documentId"
|
to="/document/$documentId"
|
||||||
params={{ documentId: doc.id }}
|
params={{ documentId: doc.id }}
|
||||||
className="text-lg"
|
className="text-lg !no-underline text-slate-100 hover:underline hover:text-violet-400"
|
||||||
>
|
>
|
||||||
<h4>{title}</h4>
|
<h4>{title}</h4>
|
||||||
</Link>
|
</Link>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { type AnyDocument } from "@/lib/types";
|
||||||
isLocation,
|
|
||||||
isMonster,
|
|
||||||
isNpc,
|
|
||||||
isScene,
|
|
||||||
isSecret,
|
|
||||||
isSession,
|
|
||||||
isTreasure,
|
|
||||||
type AnyDocument,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import { LocationEditForm } from "./location/LocationEditForm";
|
import { LocationEditForm } from "./location/LocationEditForm";
|
||||||
import { MonsterEditForm } from "./monsters/MonsterEditForm";
|
import { MonsterEditForm } from "./monsters/MonsterEditForm";
|
||||||
import { NpcEditForm } from "./npc/NpcEditForm";
|
import { NpcEditForm } from "./npc/NpcEditForm";
|
||||||
@@ -16,41 +7,24 @@ import { SecretEditForm } from "./secret/SecretEditForm";
|
|||||||
import { SessionEditForm } from "./session/SessionEditForm";
|
import { SessionEditForm } from "./session/SessionEditForm";
|
||||||
import { TreasureEditForm } from "./treasure/TreasureEditForm";
|
import { TreasureEditForm } from "./treasure/TreasureEditForm";
|
||||||
|
|
||||||
function assertUnreachable(_x: never): never {
|
|
||||||
throw new Error("DocumentForm switch is not exhaustive");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a form for any document type depending on the relationship.
|
* Renders a form for any document type depending on the relationship.
|
||||||
*/
|
*/
|
||||||
export const DocumentEditForm = ({ document }: { document: AnyDocument }) => {
|
export const DocumentEditForm = ({ document }: { document: AnyDocument }) => {
|
||||||
if (isLocation(document)) {
|
switch (document.type) {
|
||||||
|
case "location":
|
||||||
return <LocationEditForm location={document} />;
|
return <LocationEditForm location={document} />;
|
||||||
}
|
case "monster":
|
||||||
|
|
||||||
if (isMonster(document)) {
|
|
||||||
return <MonsterEditForm monster={document} />;
|
return <MonsterEditForm monster={document} />;
|
||||||
}
|
case "npc":
|
||||||
|
|
||||||
if (isNpc(document)) {
|
|
||||||
return <NpcEditForm npc={document} />;
|
return <NpcEditForm npc={document} />;
|
||||||
}
|
case "scene":
|
||||||
|
|
||||||
if (isScene(document)) {
|
|
||||||
return <SceneEditForm scene={document} />;
|
return <SceneEditForm scene={document} />;
|
||||||
}
|
case "secret":
|
||||||
|
|
||||||
if (isSecret(document)) {
|
|
||||||
return <SecretEditForm secret={document} />;
|
return <SecretEditForm secret={document} />;
|
||||||
}
|
case "session":
|
||||||
|
|
||||||
if (isSession(document)) {
|
|
||||||
return <SessionEditForm session={document} />;
|
return <SessionEditForm session={document} />;
|
||||||
}
|
case "treasure":
|
||||||
|
|
||||||
if (isTreasure(document)) {
|
|
||||||
return <TreasureEditForm treasure={document} />;
|
return <TreasureEditForm treasure={document} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return assertUnreachable(document);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,64 +1,33 @@
|
|||||||
// DocumentRow.tsx
|
// DocumentRow.tsx
|
||||||
// Generic row component for displaying any document type.
|
// Generic row component for displaying any document type.
|
||||||
import {
|
import { type AnyDocument } from "@/lib/types";
|
||||||
isLocation,
|
|
||||||
isMonster,
|
|
||||||
isNpc,
|
|
||||||
isScene,
|
|
||||||
isSecret,
|
|
||||||
isSession,
|
|
||||||
isTreasure,
|
|
||||||
type Document,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import { LocationPrintRow } from "./location/LocationPrintRow";
|
import { LocationPrintRow } from "./location/LocationPrintRow";
|
||||||
import { MonsterPrintRow } from "./monsters/MonsterPrintRow";
|
import { MonsterPrintRow } from "./monsters/MonsterPrintRow";
|
||||||
import { TreasurePrintRow } from "./treasure/TreasurePrintRow";
|
|
||||||
import { SecretPrintRow } from "./secret/SecretPrintRow";
|
|
||||||
import { NpcPrintRow } from "./npc/NpcPrintRow";
|
import { NpcPrintRow } from "./npc/NpcPrintRow";
|
||||||
import { ScenePrintRow } from "./scene/ScenePrintRow";
|
import { ScenePrintRow } from "./scene/ScenePrintRow";
|
||||||
|
import { SecretPrintRow } from "./secret/SecretPrintRow";
|
||||||
import { SessionPrintRow } from "./session/SessionPrintRow";
|
import { SessionPrintRow } from "./session/SessionPrintRow";
|
||||||
|
import { TreasurePrintRow } from "./treasure/TreasurePrintRow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time.
|
* Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time.
|
||||||
* If rendering a SecretRow, uses the provided session prop if available.
|
* If rendering a SecretRow, uses the provided session prop if available.
|
||||||
*/
|
*/
|
||||||
export const DocumentPrintRow = ({ document }: { document: Document }) => {
|
export const DocumentPrintRow = ({ document }: { document: AnyDocument }) => {
|
||||||
if (isLocation(document)) {
|
switch (document.type) {
|
||||||
|
case "location":
|
||||||
return <LocationPrintRow location={document} />;
|
return <LocationPrintRow location={document} />;
|
||||||
}
|
case "monster":
|
||||||
|
|
||||||
if (isMonster(document)) {
|
|
||||||
return <MonsterPrintRow monster={document} />;
|
return <MonsterPrintRow monster={document} />;
|
||||||
}
|
case "npc":
|
||||||
|
|
||||||
if (isNpc(document)) {
|
|
||||||
return <NpcPrintRow npc={document} />;
|
return <NpcPrintRow npc={document} />;
|
||||||
}
|
case "scene":
|
||||||
|
|
||||||
if (isSession(document)) {
|
|
||||||
return <SessionPrintRow session={document} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSecret(document)) {
|
|
||||||
return <SecretPrintRow secret={document} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isScene(document)) {
|
|
||||||
return <ScenePrintRow scene={document} />;
|
return <ScenePrintRow scene={document} />;
|
||||||
}
|
case "secret":
|
||||||
|
return <SecretPrintRow secret={document} />;
|
||||||
if (isTreasure(document)) {
|
case "session":
|
||||||
|
return <SessionPrintRow session={document} />;
|
||||||
|
case "treasure":
|
||||||
return <TreasurePrintRow treasure={document} />;
|
return <TreasurePrintRow treasure={document} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: show ID and creation time
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-lg text-slate-300">
|
|
||||||
Unrecognized Document
|
|
||||||
</div>
|
|
||||||
<div className="text-slate-400 text-sm">ID: {document.id}</div>
|
|
||||||
<div className="text-slate-400 text-sm">Created: {document.created}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
// DocumentRow.tsx
|
// DocumentRow.tsx
|
||||||
// Generic row component for displaying any document type.
|
// Generic row component for displaying any document type.
|
||||||
import { SecretToggleRow } from "@/components/documents/secret/SecretToggleRow";
|
import { SecretToggleRow } from "@/components/documents/secret/SecretToggleRow";
|
||||||
import {
|
import { type AnyDocument, type Session } from "@/lib/types";
|
||||||
isLocation,
|
|
||||||
isMonster,
|
|
||||||
isNpc,
|
|
||||||
isScene,
|
|
||||||
isSecret,
|
|
||||||
isSession,
|
|
||||||
isTreasure,
|
|
||||||
type Document,
|
|
||||||
type Session,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import { BasicRow } from "./BasicRow";
|
import { BasicRow } from "./BasicRow";
|
||||||
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
|
import { TreasureToggleRow } from "./treasure/TreasureToggleRow";
|
||||||
|
|
||||||
@@ -23,63 +13,47 @@ export const DocumentRow = ({
|
|||||||
document,
|
document,
|
||||||
session,
|
session,
|
||||||
}: {
|
}: {
|
||||||
document: Document;
|
document: AnyDocument;
|
||||||
session?: Session;
|
session?: Session;
|
||||||
}) => {
|
}) => {
|
||||||
if (isLocation(document)) {
|
switch (document.type) {
|
||||||
|
case "location":
|
||||||
return (
|
return (
|
||||||
<BasicRow
|
<BasicRow
|
||||||
doc={document}
|
doc={document}
|
||||||
title={document.data.location.name}
|
title={document.data.name}
|
||||||
description={document.data.location.description}
|
description={document.data.description}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (isMonster(document)) {
|
case "monster":
|
||||||
return <BasicRow doc={document} title={document.data.monster.name} />;
|
return <BasicRow doc={document} title={document.data.name} />;
|
||||||
}
|
|
||||||
|
|
||||||
if (isNpc(document)) {
|
case "npc":
|
||||||
return (
|
return (
|
||||||
<BasicRow
|
<BasicRow
|
||||||
doc={document}
|
doc={document}
|
||||||
title={document.data.npc.name}
|
title={document.data.name}
|
||||||
description={document.data.npc.description}
|
description={document.data.description}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (isSession(document)) {
|
case "session":
|
||||||
return (
|
return (
|
||||||
<BasicRow
|
<BasicRow
|
||||||
doc={document}
|
doc={document}
|
||||||
title={document.created}
|
title={document.created}
|
||||||
description={document.data.session.strongStart}
|
description={document.data.strongStart}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (isSecret(document)) {
|
case "secret":
|
||||||
return <SecretToggleRow secret={document} session={session} />;
|
return <SecretToggleRow secret={document} session={session} />;
|
||||||
}
|
|
||||||
|
|
||||||
if (isScene(document)) {
|
case "scene":
|
||||||
return <BasicRow doc={document} title={document.data.scene.text} />;
|
return <BasicRow doc={document} title={document.data.text} />;
|
||||||
}
|
|
||||||
|
|
||||||
if (isTreasure(document)) {
|
case "treasure":
|
||||||
return <TreasureToggleRow treasure={document} session={session} />;
|
return <TreasureToggleRow treasure={document} session={session} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: show ID and creation time
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-lg text-slate-300">
|
|
||||||
Unrecognized Document
|
|
||||||
</div>
|
|
||||||
<div className="text-slate-400 text-sm">ID: {document.id}</div>
|
|
||||||
<div className="text-slate-400 text-sm">Created: {document.created}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { RelationshipType, type CampaignId, type Document } from "@/lib/types";
|
import {
|
||||||
|
RelationshipType,
|
||||||
|
type CampaignId,
|
||||||
|
type AnyDocument,
|
||||||
|
} from "@/lib/types";
|
||||||
import { NewLocationForm } from "./location/NewLocationForm";
|
import { NewLocationForm } from "./location/NewLocationForm";
|
||||||
import { NewMonsterForm } from "./monsters/NewMonsterForm";
|
import { NewMonsterForm } from "./monsters/NewMonsterForm";
|
||||||
import { NewNpcForm } from "./npc/NewNpcForm";
|
import { NewNpcForm } from "./npc/NewNpcForm";
|
||||||
@@ -6,10 +10,6 @@ import { NewSceneForm } from "./scene/NewSceneForm";
|
|||||||
import { NewSecretForm } from "./secret/NewSecretForm";
|
import { NewSecretForm } from "./secret/NewSecretForm";
|
||||||
import { NewTreasureForm } from "./treasure/NewTreasureForm";
|
import { NewTreasureForm } from "./treasure/NewTreasureForm";
|
||||||
|
|
||||||
function assertUnreachable(_x: never): never {
|
|
||||||
throw new Error("DocumentForm switch is not exhaustive");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a form for any document type depending on the relationship.
|
* Renders a form for any document type depending on the relationship.
|
||||||
*/
|
*/
|
||||||
@@ -20,7 +20,7 @@ export const NewRelatedDocumentForm = ({
|
|||||||
}: {
|
}: {
|
||||||
campaignId: CampaignId;
|
campaignId: CampaignId;
|
||||||
relationshipType: RelationshipType;
|
relationshipType: RelationshipType;
|
||||||
onCreate: (document: Document) => Promise<void>;
|
onCreate: (doc: AnyDocument) => Promise<void>;
|
||||||
}) => {
|
}) => {
|
||||||
switch (relationshipType) {
|
switch (relationshipType) {
|
||||||
case RelationshipType.Locations:
|
case RelationshipType.Locations:
|
||||||
@@ -38,6 +38,4 @@ export const NewRelatedDocumentForm = ({
|
|||||||
case RelationshipType.DiscoveredIn:
|
case RelationshipType.DiscoveredIn:
|
||||||
return "Form not supported here";
|
return "Form not supported here";
|
||||||
}
|
}
|
||||||
|
|
||||||
return assertUnreachable(relationshipType);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export const LocationEditForm = ({ location }: { location: Location }) => {
|
|||||||
await pb.collection("documents").update(location.id, {
|
await pb.collection("documents").update(location.id, {
|
||||||
data: {
|
data: {
|
||||||
...location.data,
|
...location.data,
|
||||||
location: {
|
|
||||||
...location.data.location,
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +19,8 @@ export const LocationEditForm = ({ location }: { location: Location }) => {
|
|||||||
await pb.collection("documents").update(location.id, {
|
await pb.collection("documents").update(location.id, {
|
||||||
data: {
|
data: {
|
||||||
...location.data,
|
...location.data,
|
||||||
location: {
|
|
||||||
...location.data.location,
|
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,11 +28,11 @@ export const LocationEditForm = ({ location }: { location: Location }) => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={location.data.location.name}
|
value={location.data.name}
|
||||||
onSave={saveLocationName}
|
onSave={saveLocationName}
|
||||||
/>
|
/>
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
value={location.data.location.description}
|
value={location.data.description}
|
||||||
onSave={saveLocationDescription}
|
onSave={saveLocationDescription}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type { Location } from "@/lib/types";
|
|||||||
*/
|
*/
|
||||||
export const LocationPrintRow = ({ location }: { location: Location }) => {
|
export const LocationPrintRow = ({ location }: { location: Location }) => {
|
||||||
return (
|
return (
|
||||||
<li>
|
<div>
|
||||||
<h4>{location.data.location.name}</h4>
|
<h4>{location.data.name}</h4>
|
||||||
<p>{location.data.location.description}</p>
|
<p>{location.data.description}</p>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export const NewLocationForm = ({
|
|||||||
try {
|
try {
|
||||||
const locationDoc: Location = await pb.collection("documents").create({
|
const locationDoc: Location = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "location",
|
||||||
data: {
|
data: {
|
||||||
location: {
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export const MonsterEditForm = ({ monster }: { monster: Monster }) => {
|
|||||||
await pb.collection("documents").update(monster.id, {
|
await pb.collection("documents").update(monster.id, {
|
||||||
data: {
|
data: {
|
||||||
...monster.data,
|
...monster.data,
|
||||||
monster: {
|
|
||||||
...monster.data.monster,
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +19,7 @@ export const MonsterEditForm = ({ monster }: { monster: Monster }) => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={monster.data.monster.name}
|
value={monster.data.name}
|
||||||
onSave={saveMonsterName}
|
onSave={saveMonsterName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import type { Monster } from "@/lib/types";
|
|||||||
* Renders an editable monster row
|
* Renders an editable monster row
|
||||||
*/
|
*/
|
||||||
export const MonsterPrintRow = ({ monster }: { monster: Monster }) => {
|
export const MonsterPrintRow = ({ monster }: { monster: Monster }) => {
|
||||||
return <li>{monster.data.monster.name}</li>;
|
return <div>{monster.data.name}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export const NewMonsterForm = ({
|
|||||||
try {
|
try {
|
||||||
const monsterDoc: Monster = await pb.collection("documents").create({
|
const monsterDoc: Monster = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "monster",
|
||||||
data: {
|
data: {
|
||||||
monster: {
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export const NewNpcForm = ({
|
|||||||
try {
|
try {
|
||||||
const npcDoc: Npc = await pb.collection("documents").create({
|
const npcDoc: Npc = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "npc",
|
||||||
data: {
|
data: {
|
||||||
npc: {
|
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ export const NpcEditForm = ({ npc }: { npc: Npc }) => {
|
|||||||
await pb.collection("documents").update(npc.id, {
|
await pb.collection("documents").update(npc.id, {
|
||||||
data: {
|
data: {
|
||||||
...npc.data,
|
...npc.data,
|
||||||
npc: {
|
|
||||||
...npc.data.npc,
|
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +19,8 @@ export const NpcEditForm = ({ npc }: { npc: Npc }) => {
|
|||||||
await pb.collection("documents").update(npc.id, {
|
await pb.collection("documents").update(npc.id, {
|
||||||
data: {
|
data: {
|
||||||
...npc.data,
|
...npc.data,
|
||||||
npc: {
|
|
||||||
...npc.data.npc,
|
|
||||||
description,
|
description,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,11 +28,11 @@ export const NpcEditForm = ({ npc }: { npc: Npc }) => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={npc.data.npc.name}
|
value={npc.data.name}
|
||||||
onSave={saveNpcName}
|
onSave={saveNpcName}
|
||||||
/>
|
/>
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
value={npc.data.npc.description}
|
value={npc.data.description}
|
||||||
onSave={saveNpcDescription}
|
onSave={saveNpcDescription}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type { Npc } from "@/lib/types";
|
|||||||
*/
|
*/
|
||||||
export const NpcPrintRow = ({ npc }: { npc: Npc }) => {
|
export const NpcPrintRow = ({ npc }: { npc: Npc }) => {
|
||||||
return (
|
return (
|
||||||
<li className="">
|
<div className="">
|
||||||
<h4>{npc.data.npc.name}</h4>
|
<h4>{npc.data.name}</h4>
|
||||||
<p>{npc.data.npc.description}</p>
|
<p>{npc.data.description}</p>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { CampaignId, Scene } from "@/lib/types";
|
import type { CampaignId, Scene } from "@/lib/types";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { BaseForm } from "@/components/form/BaseForm";
|
||||||
|
import { MultiLineInput } from "@/components/form/MultiLineInput";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a form to add a new scene. Calls onCreate with the new scene document.
|
* Renders a form to add a new scene. Calls onCreate with the new scene document.
|
||||||
@@ -26,11 +28,10 @@ export const NewSceneForm = ({
|
|||||||
try {
|
try {
|
||||||
const sceneDoc: Scene = await pb.collection("documents").create({
|
const sceneDoc: Scene = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "scene",
|
||||||
data: {
|
data: {
|
||||||
scene: {
|
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setText("");
|
setText("");
|
||||||
await onCreate(sceneDoc);
|
await onCreate(sceneDoc);
|
||||||
@@ -42,28 +43,22 @@ export const NewSceneForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<BaseForm
|
||||||
className="flex flex-col items-left gap-2 mt-4"
|
title="Create new scene"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
error={error}
|
||||||
<h3>Create new scene</h3>
|
buttonText={adding ? "Adding..." : "Create"}
|
||||||
<input
|
content={
|
||||||
type="text"
|
<>
|
||||||
className="flex-1 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"
|
<MultiLineInput
|
||||||
placeholder="Add a new scene..."
|
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(v) => setText(v)}
|
||||||
disabled={adding}
|
disabled={adding}
|
||||||
|
placeholder="Scene description..."
|
||||||
aria-label="Add new scene"
|
aria-label="Add new scene"
|
||||||
/>
|
/>
|
||||||
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}
|
</>
|
||||||
<button
|
}
|
||||||
type="submit"
|
/>
|
||||||
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
|
||||||
disabled={adding || !text.trim()}
|
|
||||||
>
|
|
||||||
{adding ? "Adding..." : "Create"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ export const SceneEditForm = ({ scene }: { scene: Scene }) => {
|
|||||||
await pb.collection("documents").update(scene.id, {
|
await pb.collection("documents").update(scene.id, {
|
||||||
data: {
|
data: {
|
||||||
...scene.data,
|
...scene.data,
|
||||||
scene: {
|
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["relationship"],
|
queryKey: ["relationship"],
|
||||||
@@ -25,7 +23,7 @@ export const SceneEditForm = ({ scene }: { scene: Scene }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<AutoSaveTextarea value={scene.data.scene.text} onSave={saveScene} />
|
<AutoSaveTextarea value={scene.data.text} onSave={saveScene} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import type { Scene } from "@/lib/types";
|
|||||||
* Renders an editable scene row
|
* Renders an editable scene row
|
||||||
*/
|
*/
|
||||||
export const ScenePrintRow = ({ scene }: { scene: Scene }) => {
|
export const ScenePrintRow = ({ scene }: { scene: Scene }) => {
|
||||||
return <li className="">{scene.data.scene.text}</li>;
|
return <div className="">{scene.data.text}</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ export const NewSecretForm = ({
|
|||||||
try {
|
try {
|
||||||
const secretDoc: Secret = await pb.collection("documents").create({
|
const secretDoc: Secret = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "secret",
|
||||||
data: {
|
data: {
|
||||||
secret: {
|
|
||||||
text: newSecret,
|
text: newSecret,
|
||||||
discovered: false,
|
discovered: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setNewSecret("");
|
setNewSecret("");
|
||||||
await onCreate(secretDoc);
|
await onCreate(secretDoc);
|
||||||
|
|||||||
@@ -28,11 +28,8 @@ export const SecretEditForm = ({
|
|||||||
await pb.collection("documents").update(secret.id, {
|
await pb.collection("documents").update(secret.id, {
|
||||||
data: {
|
data: {
|
||||||
...secret.data,
|
...secret.data,
|
||||||
secret: {
|
|
||||||
...(secret.data as any).secret,
|
|
||||||
discovered: newChecked,
|
discovered: newChecked,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (session || !newChecked) {
|
if (session || !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
|
||||||
@@ -62,11 +59,8 @@ export const SecretEditForm = ({
|
|||||||
await pb.collection("documents").update(secret.id, {
|
await pb.collection("documents").update(secret.id, {
|
||||||
data: {
|
data: {
|
||||||
...secret.data,
|
...secret.data,
|
||||||
secret: {
|
|
||||||
...secret.data.secret,
|
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +76,7 @@ export const SecretEditForm = ({
|
|||||||
/>
|
/>
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={secret.data.secret.text}
|
value={secret.data.text}
|
||||||
onSave={saveText}
|
onSave={saveText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ export const SecretToggleRow = ({
|
|||||||
aria-label="Discovered"
|
aria-label="Discovered"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>{secret.data.text}</span>
|
||||||
{(secret.data as any)?.secret?.text || (
|
|
||||||
<span className="italic text-slate-400">(No secret text)</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ export const SessionEditForm = ({ session }: { session: Session }) => {
|
|||||||
await pb.collection("documents").update(session.id, {
|
await pb.collection("documents").update(session.id, {
|
||||||
data: {
|
data: {
|
||||||
...session.data,
|
...session.data,
|
||||||
session: {
|
|
||||||
...session.data.session,
|
|
||||||
strongStart,
|
strongStart,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +16,7 @@ export const SessionEditForm = ({ session }: { session: Session }) => {
|
|||||||
<form>
|
<form>
|
||||||
<h3 className="text-lg font-bold mb-4 text-slate-100">Strong Start</h3>
|
<h3 className="text-lg font-bold mb-4 text-slate-100">Strong Start</h3>
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
value={session.data.session.strongStart}
|
value={session.data.strongStart}
|
||||||
onSave={saveStrongStart}
|
onSave={saveStrongStart}
|
||||||
placeholder="Enter a strong start for this session..."
|
placeholder="Enter a strong start for this session..."
|
||||||
aria-label="Strong Start"
|
aria-label="Strong Start"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const SessionPrintRow = ({ session }: { session: Session }) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-slate-600">StrongStart</h3>
|
<h3 className="text-lg font-bold text-slate-600">StrongStart</h3>
|
||||||
<div className="">{session.data.session.strongStart}</div>
|
<div className="">{session.data.strongStart}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const SessionRow = ({ session }: { session: Session }) => {
|
|||||||
>
|
>
|
||||||
<FormattedDate date={session.created} />
|
<FormattedDate date={session.created} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="">{session.data.session.strongStart}</div>
|
<div className="">{session.data.strongStart}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ export const NewTreasureForm = ({
|
|||||||
try {
|
try {
|
||||||
const treasureDoc: Treasure = await pb.collection("documents").create({
|
const treasureDoc: Treasure = await pb.collection("documents").create({
|
||||||
campaign,
|
campaign,
|
||||||
|
type: "treasure",
|
||||||
data: {
|
data: {
|
||||||
treasure: {
|
|
||||||
text: newTreasure,
|
text: newTreasure,
|
||||||
discovered: false,
|
discovered: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setNewTreasure("");
|
setNewTreasure("");
|
||||||
await onCreate(treasureDoc);
|
await onCreate(treasureDoc);
|
||||||
|
|||||||
@@ -62,11 +62,8 @@ export const TreasureEditForm = ({
|
|||||||
await pb.collection("documents").update(treasure.id, {
|
await pb.collection("documents").update(treasure.id, {
|
||||||
data: {
|
data: {
|
||||||
...treasure.data,
|
...treasure.data,
|
||||||
treasure: {
|
|
||||||
...treasure.data.treasure,
|
|
||||||
text,
|
text,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +79,7 @@ export const TreasureEditForm = ({
|
|||||||
/>
|
/>
|
||||||
<AutoSaveTextarea
|
<AutoSaveTextarea
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={treasure.data.treasure.text}
|
value={treasure.data.text}
|
||||||
onSave={saveText}
|
onSave={saveText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { Treasure } from "@/lib/types";
|
|||||||
*/
|
*/
|
||||||
export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
|
export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="flex-none accent-emerald-500 w-5 h-5"
|
className="flex-none accent-emerald-500 w-5 h-5"
|
||||||
@@ -19,6 +19,6 @@ export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
|
|||||||
<span className="italic text-slate-400">(No treasure text)</span>
|
<span className="italic text-slate-400">(No treasure text)</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ export const TreasureToggleRow = ({
|
|||||||
aria-label="Discovered"
|
aria-label="Discovered"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>{treasure.data.text}</span>
|
||||||
{(treasure.data as any)?.treasure?.text || (
|
|
||||||
<span className="italic text-slate-400">(No treasure text)</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
32
src/components/form/BaseForm.tsx
Normal file
32
src/components/form/BaseForm.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export type Props = {
|
||||||
|
title?: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
buttonText?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BaseForm = ({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
buttonText,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
onSubmit,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<form className="flex flex-col items-left gap-2" onSubmit={onSubmit}>
|
||||||
|
<h3 className="text-lg font-semibold text-slate-100 mb-4">{title}</h3>
|
||||||
|
<div>{content}</div>
|
||||||
|
{error && <div className="text-red-400 mt-2 text-sm">{error}</div>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 rounded bg-emerald-600 hover:bg-emerald-700 text-white font-semibold transition-colors disabled:opacity-60"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{buttonText ? buttonText : "Submit"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/components/form/MultiLineInput.tsx
Normal file
22
src/components/form/MultiLineInput.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export type Props = {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
} & Omit<
|
||||||
|
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||||
|
"value" | "onChange" | "className"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const MultiLineInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: Props) => (
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className={`w-full min-h-[10em] field-sizing-content p-2 rounded border bg-slate-800 text-slate-100 border-slate-700 focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors ${className}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -41,21 +41,6 @@ export type Relationship = RecordModel & {
|
|||||||
* Documents
|
* Documents
|
||||||
******************************************/
|
******************************************/
|
||||||
|
|
||||||
export type DocumentData<K extends string, V> = {
|
|
||||||
data: Record<K, V>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Document = RecordModel & {
|
|
||||||
id: DocumentId;
|
|
||||||
campaign: CampaignId;
|
|
||||||
data: {
|
|
||||||
[K in DocumentType]?: unknown;
|
|
||||||
};
|
|
||||||
// These two are not in Pocketbase's types, but they seem to always be present
|
|
||||||
created: ISO8601Date;
|
|
||||||
updated: ISO8601Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DocumentType =
|
export type DocumentType =
|
||||||
| "location"
|
| "location"
|
||||||
| "monster"
|
| "monster"
|
||||||
@@ -65,6 +50,21 @@ export type DocumentType =
|
|||||||
| "session"
|
| "session"
|
||||||
| "treasure";
|
| "treasure";
|
||||||
|
|
||||||
|
export type DocumentData<Type extends DocumentType, Data> = {
|
||||||
|
type: Type;
|
||||||
|
data: Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Document<Type extends DocumentType, Data> = RecordModel & {
|
||||||
|
id: DocumentId;
|
||||||
|
campaign: CampaignId;
|
||||||
|
type: Type;
|
||||||
|
data: Data;
|
||||||
|
// These two are not in Pocketbase's types, but they seem to always be present
|
||||||
|
created: ISO8601Date;
|
||||||
|
updated: ISO8601Date;
|
||||||
|
};
|
||||||
|
|
||||||
export type AnyDocument =
|
export type AnyDocument =
|
||||||
| Location
|
| Location
|
||||||
| Monster
|
| Monster
|
||||||
@@ -75,28 +75,11 @@ export type AnyDocument =
|
|||||||
| Treasure;
|
| Treasure;
|
||||||
|
|
||||||
export function getDocumentType(doc: AnyDocument): DocumentType {
|
export function getDocumentType(doc: AnyDocument): DocumentType {
|
||||||
if (isLocation(doc)) {
|
return doc.type;
|
||||||
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 &
|
|
||||||
DocumentData<
|
|
||||||
"location",
|
"location",
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -104,28 +87,18 @@ export type Location = Document &
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isLocation(doc: Document): doc is Location {
|
|
||||||
return Object.hasOwn(doc.data, "location");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Monsters **/
|
/** Monsters **/
|
||||||
|
|
||||||
export type Monster = Document &
|
export type Monster = Document<
|
||||||
DocumentData<
|
|
||||||
"monster",
|
"monster",
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isMonster(doc: Document): doc is Monster {
|
|
||||||
return Object.hasOwn(doc.data, "monster");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** NPCs **/
|
/** NPCs **/
|
||||||
|
|
||||||
export type Npc = Document &
|
export type Npc = Document<
|
||||||
DocumentData<
|
|
||||||
"npc",
|
"npc",
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -133,42 +106,27 @@ export type Npc = Document &
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isNpc(doc: Document): doc is Npc {
|
|
||||||
return Object.hasOwn(doc.data, "npc");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Session **/
|
/** Session **/
|
||||||
|
|
||||||
export type Session = Document &
|
export type Session = Document<
|
||||||
DocumentData<
|
|
||||||
"session",
|
"session",
|
||||||
{
|
{
|
||||||
strongStart: string;
|
strongStart: string;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isSession(doc: Document): doc is Session {
|
|
||||||
return Object.hasOwn(doc.data, "session");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Scene **/
|
/** Scene **/
|
||||||
|
|
||||||
export type Scene = Document &
|
export type Scene = Document<
|
||||||
DocumentData<
|
|
||||||
"scene",
|
"scene",
|
||||||
{
|
{
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isScene(doc: Document): doc is Scene {
|
|
||||||
return Object.hasOwn(doc.data, "scene");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Secret **/
|
/** Secret **/
|
||||||
|
|
||||||
export type Secret = Document &
|
export type Secret = Document<
|
||||||
DocumentData<
|
|
||||||
"secret",
|
"secret",
|
||||||
{
|
{
|
||||||
text: string;
|
text: string;
|
||||||
@@ -176,21 +134,12 @@ export type Secret = Document &
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isSecret(doc: Document): doc is Secret {
|
|
||||||
return Object.hasOwn(doc.data, "secret");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Treasure **/
|
/** Treasure **/
|
||||||
|
|
||||||
export type Treasure = Document &
|
export type Treasure = Document<
|
||||||
DocumentData<
|
|
||||||
"treasure",
|
"treasure",
|
||||||
{
|
{
|
||||||
text: string;
|
text: string;
|
||||||
discovered: boolean;
|
discovered: boolean;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function isTreasure(doc: Document): doc is Treasure {
|
|
||||||
return Object.hasOwn(doc.data, "treasure");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ function RouteComponent() {
|
|||||||
.collection("campaigns")
|
.collection("campaigns")
|
||||||
.getOne(params.campaignId);
|
.getOne(params.campaignId);
|
||||||
// Fetch all documents for this campaign
|
// Fetch all documents for this campaign
|
||||||
const docs = await pb.collection("documents").getFullList({
|
const sessions = await pb.collection("documents").getFullList({
|
||||||
filter: `campaign = "${params.campaignId}"`,
|
filter: `campaign = "${params.campaignId}" && type = 'session'`,
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
});
|
});
|
||||||
// Filter to only those with data.session
|
|
||||||
const sessions = docs.filter((doc: any) => doc.data && doc.data.session);
|
|
||||||
return {
|
return {
|
||||||
campaign,
|
campaign,
|
||||||
sessions,
|
sessions,
|
||||||
@@ -44,12 +42,9 @@ function RouteComponent() {
|
|||||||
// Check for a previous session
|
// Check for a previous session
|
||||||
const prevSession = await pb
|
const prevSession = await pb
|
||||||
.collection("documents")
|
.collection("documents")
|
||||||
.getFirstListItem(
|
.getFirstListItem(`campaign = "${campaign.id}" && type = 'session'`, {
|
||||||
`campaign = "${campaign.id}" && json_extract(data, '$.session') IS NOT NULL`,
|
|
||||||
{
|
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Previous session: ", {
|
console.log("Previous session: ", {
|
||||||
id: prevSession.id,
|
id: prevSession.id,
|
||||||
@@ -58,16 +53,13 @@ function RouteComponent() {
|
|||||||
|
|
||||||
const newSession = await pb.collection("documents").create({
|
const newSession = await pb.collection("documents").create({
|
||||||
campaign: campaign.id,
|
campaign: campaign.id,
|
||||||
|
type: "session",
|
||||||
data: {
|
data: {
|
||||||
session: {
|
|
||||||
strongStart: "",
|
strongStart: "",
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["campaign"] });
|
// If any relations, then copy things over
|
||||||
|
|
||||||
// If any, then copy things over
|
|
||||||
if (prevSession) {
|
if (prevSession) {
|
||||||
const prevRelations = await pb
|
const prevRelations = await pb
|
||||||
.collection<Relationship>("relationships")
|
.collection<Relationship>("relationships")
|
||||||
@@ -84,7 +76,7 @@ function RouteComponent() {
|
|||||||
await pb.collection("relationships").create({
|
await pb.collection("relationships").create({
|
||||||
primary: newSession.id,
|
primary: newSession.id,
|
||||||
type: relation.type,
|
type: relation.type,
|
||||||
seciondary: relation.secondary,
|
secondary: relation.secondary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,17 @@ function RouteComponent() {
|
|||||||
<TabGroup>
|
<TabGroup>
|
||||||
<TabList className="flex flex-row flex-wrap gap-1 mt-2">
|
<TabList className="flex flex-row flex-wrap gap-1 mt-2">
|
||||||
{relationshipList.map((relationshipType) => (
|
{relationshipList.map((relationshipType) => (
|
||||||
<Tab 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">
|
<Tab
|
||||||
|
key={relationshipType}
|
||||||
|
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"
|
||||||
|
>
|
||||||
{displayName(relationshipType)}
|
{displayName(relationshipType)}
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{relationshipList.map((relationshipType) => (
|
{relationshipList.map((relationshipType) => (
|
||||||
<TabPanel>
|
<TabPanel key={relationshipType}>
|
||||||
<RelationshipList
|
<RelationshipList
|
||||||
key={relationshipType}
|
key={relationshipType}
|
||||||
root={document}
|
root={document}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background-color: #0f172a; /* slate-900 */
|
background-color: #0f172a; /* slate-900 */
|
||||||
color: #f1f5f9; /* slate-100 */
|
color: #f1f5f9; /* slate-100 */
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: "Inter", system-ui, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,24 +17,15 @@ body {
|
|||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code, pre {
|
code,
|
||||||
font-family: 'Fira Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
pre {
|
||||||
|
font-family: "Fira Mono", "Menlo", "Monaco", "Consolas", monospace;
|
||||||
background: #1e293b; /* slate-800 */
|
background: #1e293b; /* slate-800 */
|
||||||
color: #a5b4fc; /* violet-300 */
|
color: #a5b4fc; /* violet-300 */
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: #a5b4fc; /* violet-300 */
|
|
||||||
text-decoration: underline;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover, a:focus {
|
|
||||||
color: #7c3aed; /* violet-600 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove default outline, but keep focus-visible for accessibility */
|
/* Remove default outline, but keep focus-visible for accessibility */
|
||||||
:focus:not(:focus-visible) {
|
:focus:not(:focus-visible) {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user