Makes campaigns load all types of docs and then link to the docs
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
import { Fragment, useCallback, useState } from "react";
|
import { Fragment, useCallback, useState } from "react";
|
||||||
|
|
||||||
type Props<T extends AnyDocument> = {
|
type Props<T extends AnyDocument> = {
|
||||||
title: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
error?: React.ReactNode;
|
error?: React.ReactNode;
|
||||||
items: T[];
|
items: T[];
|
||||||
renderRow: (item: T) => React.ReactNode;
|
renderRow: (item: T) => React.ReactNode;
|
||||||
@@ -49,7 +49,7 @@ export function DocumentList<T extends AnyDocument>({
|
|||||||
return (
|
return (
|
||||||
<section className="w-full">
|
<section className="w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
|
{title && <h2 className="text-xl font-bold text-slate-100">{title}</h2>}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
51
src/components/campaign/CampaignDocuments.tsx
Normal file
51
src/components/campaign/CampaignDocuments.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
type AnyDocument,
|
||||||
|
type CampaignId,
|
||||||
|
type DocumentType,
|
||||||
|
} from "@/lib/types";
|
||||||
|
import { useDocumentCache } from "@/context/document/hooks";
|
||||||
|
import { DocumentList } from "../DocumentList";
|
||||||
|
import { getAllDocumentsOfType } from "@/context/document/state";
|
||||||
|
import { DocumentRow } from "../documents/DocumentRow";
|
||||||
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
campaignId: CampaignId;
|
||||||
|
docType: DocumentType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CampaignDocuments = ({ campaignId, docType }: Props) => {
|
||||||
|
const { cache, dispatch } = useDocumentCache();
|
||||||
|
|
||||||
|
const items = getAllDocumentsOfType(docType, cache);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchDocuments() {
|
||||||
|
const documents: AnyDocument[] = await pb
|
||||||
|
.collection("documents")
|
||||||
|
.getFullList({
|
||||||
|
filter: `campaign = "${campaignId}" && type = "${docType}"`,
|
||||||
|
sort: "created",
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const doc of documents) {
|
||||||
|
dispatch({
|
||||||
|
type: "setDocument",
|
||||||
|
doc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDocuments();
|
||||||
|
}, [campaignId, docType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentList
|
||||||
|
items={items}
|
||||||
|
renderRow={(doc) => <DocumentRow document={doc} />}
|
||||||
|
newItemForm={() => <div>New Item Form</div>}
|
||||||
|
removeItem={() => console.error("TODO")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
import { FormattedText } from "../FormattedText";
|
import { FormattedText } from "../FormattedText";
|
||||||
|
import type { DocumentId } from "@/lib/types";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
id: DocumentId;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BasicPreview = ({ title, description }: Props) => {
|
export const BasicPreview = ({ id, title, description }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/document/$documentId/$"
|
||||||
|
params={{
|
||||||
|
documentId: id,
|
||||||
|
}}
|
||||||
|
className="!no-underline text-violet-400 hover:underline hover:text-violet-500"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
{title && <h4 className="font-bold">{title}</h4>}
|
{title && <h4 className="font-bold">{title}</h4>}
|
||||||
{description && <FormattedText>{description}</FormattedText>}
|
{description && <FormattedText>{description}</FormattedText>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath";
|
import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath";
|
||||||
import type { DocumentId, RelationshipType } from "@/lib/types";
|
import type { DocumentId } from "@/lib/types";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
export type Props = React.PropsWithChildren<{
|
export type Props = React.PropsWithChildren<{
|
||||||
childDocId: DocumentId;
|
childDocId: DocumentId;
|
||||||
@@ -8,13 +8,37 @@ export type Props = React.PropsWithChildren<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function DocumentLink({ childDocId, className, children }: Props) {
|
export function DocumentLink({ childDocId, className, children }: Props) {
|
||||||
const { documentId, relationshipType } = useDocumentPath();
|
const docPath = useDocumentPath();
|
||||||
|
|
||||||
|
const params = useParams({
|
||||||
|
strict: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const campaignSearch = useSearch({
|
||||||
|
from: "/_app/_authenticated/campaigns/$campaignId",
|
||||||
|
shouldThrow: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const to = params.campaignId
|
||||||
|
? `/campaigns/${params.campaignId}`
|
||||||
|
: docPath
|
||||||
|
? makeDocumentPath(
|
||||||
|
docPath.documentId,
|
||||||
|
docPath?.relationshipType,
|
||||||
|
childDocId,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const search = campaignSearch
|
||||||
|
? { tab: campaignSearch.tab, docId: childDocId }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (to === undefined) {
|
||||||
|
throw new Error("Not in a document or campaign context");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link to={to} search={search} className={className}>
|
||||||
to={makeDocumentPath(documentId, relationshipType, childDocId)}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,17 +35,19 @@ const ShowDocument = ({ doc }: { doc: AnyDocument }) => {
|
|||||||
case "location":
|
case "location":
|
||||||
return (
|
return (
|
||||||
<BasicPreview
|
<BasicPreview
|
||||||
|
id={doc.id}
|
||||||
title={doc.data.name}
|
title={doc.data.name}
|
||||||
description={doc.data.description}
|
description={doc.data.description}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "monster":
|
case "monster":
|
||||||
return <BasicPreview title={doc.data.name} />;
|
return <BasicPreview id={doc.id} title={doc.data.name} />;
|
||||||
|
|
||||||
case "npc":
|
case "npc":
|
||||||
return (
|
return (
|
||||||
<BasicPreview
|
<BasicPreview
|
||||||
|
id={doc.id}
|
||||||
title={doc.data.name}
|
title={doc.data.name}
|
||||||
description={doc.data.description}
|
description={doc.data.description}
|
||||||
/>
|
/>
|
||||||
@@ -53,16 +55,20 @@ const ShowDocument = ({ doc }: { doc: AnyDocument }) => {
|
|||||||
|
|
||||||
case "session":
|
case "session":
|
||||||
return (
|
return (
|
||||||
<BasicPreview title={doc.created} description={doc.data.strongStart} />
|
<BasicPreview
|
||||||
|
id={doc.id}
|
||||||
|
title={doc.created}
|
||||||
|
description={doc.data.strongStart}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "secret":
|
case "secret":
|
||||||
return <BasicPreview title={doc.data.text} />;
|
return <BasicPreview id={doc.id} title={doc.data.text} />;
|
||||||
|
|
||||||
case "scene":
|
case "scene":
|
||||||
return <BasicPreview description={doc.data.text} />;
|
return <BasicPreview id={doc.id} description={doc.data.text} />;
|
||||||
|
|
||||||
case "treasure":
|
case "treasure":
|
||||||
return <BasicPreview title={doc.data.text} />;
|
return <BasicPreview id={doc.id} title={doc.data.text} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ export function DocumentView({
|
|||||||
navigation={
|
navigation={
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={CampaignRoute.to}
|
to="/campaigns/$campaignId"
|
||||||
params={{ campaignId: doc.campaign }}
|
params={{ campaignId: doc.campaign }}
|
||||||
|
search={{ tab: "sessions" }}
|
||||||
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
|
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
|
||||||
>
|
>
|
||||||
← Back to campaign
|
← Back to campaign
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function TabbedLayout({
|
|||||||
{tabs}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grow p-2 bg-slate-800 border-t border-b border-r border-slate-700 ${flyout && "hidden"} md:block`}
|
className={`grow md:w-md p-2 bg-slate-800 border-t border-b border-r border-slate-700 ${flyout && "hidden"} md:block`}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|||||||
*/
|
*/
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [user, setUser] = useState<AuthRecord | null>(pb.authStore.record);
|
const [user, setUser] = useState<AuthRecord | null>(
|
||||||
|
pb.authStore.isValid ? pb.authStore.record : null,
|
||||||
|
);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
AnyDocument,
|
AnyDocument,
|
||||||
DocumentId,
|
DocumentId,
|
||||||
|
DocumentType,
|
||||||
Relationship,
|
Relationship,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
@@ -30,3 +31,13 @@ export const initialState = (): DocumentState =>
|
|||||||
({
|
({
|
||||||
documents: {},
|
documents: {},
|
||||||
}) as DocumentState;
|
}) as DocumentState;
|
||||||
|
|
||||||
|
export const getAllDocumentsOfType = <T extends DocumentType>(
|
||||||
|
docType: T,
|
||||||
|
state: DocumentState,
|
||||||
|
): (AnyDocument & { type: T })[] =>
|
||||||
|
Object.values(state.documents).flatMap((docRecord) =>
|
||||||
|
docRecord.type === "ready" && docRecord.value.doc.type === docType
|
||||||
|
? [docRecord.value.doc as AnyDocument & { type: T }]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|||||||
@@ -24,17 +24,22 @@ const documentParams = z
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export function useDocumentPath(): {
|
export function useDocumentPath():
|
||||||
|
| {
|
||||||
documentId: DocumentId;
|
documentId: DocumentId;
|
||||||
relationshipType: RelationshipType | null;
|
relationshipType: RelationshipType | null;
|
||||||
childDocId: DocumentId | null;
|
childDocId: DocumentId | null;
|
||||||
} {
|
}
|
||||||
|
| undefined {
|
||||||
const params = useParams({
|
const params = useParams({
|
||||||
from: "/_app/_authenticated/document/$documentId/$",
|
from: "/_app/_authenticated/document/$documentId/$",
|
||||||
|
shouldThrow: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { relationshipType, childDocId } = documentParams.parse(params._splat);
|
if (params) {
|
||||||
|
const { relationshipType, childDocId } = documentParams.parse(
|
||||||
|
params._splat,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
documentId: params.documentId as DocumentId,
|
documentId: params.documentId as DocumentId,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
@@ -42,6 +47,9 @@ export function useDocumentPath(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function makeDocumentPath(
|
export function makeDocumentPath(
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
relationshipType?: RelationshipType | null,
|
relationshipType?: RelationshipType | null,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { getDocumentType, RelationshipType, type AnyDocument } from "./types";
|
import {
|
||||||
|
getDocumentType,
|
||||||
|
type DocumentType,
|
||||||
|
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);
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
|
import { CampaignDocuments } from "@/components/campaign/CampaignDocuments";
|
||||||
import { DocumentPreview } from "@/components/documents/DocumentPreview";
|
import { DocumentPreview } from "@/components/documents/DocumentPreview";
|
||||||
import { DocumentRow } from "@/components/documents/DocumentRow";
|
|
||||||
import { SessionRow } from "@/components/documents/session/SessionRow";
|
|
||||||
import { Tab, TabbedLayout } from "@/components/layout/TabbedLayout";
|
import { Tab, TabbedLayout } from "@/components/layout/TabbedLayout";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { DocumentLoader } from "@/context/document/DocumentLoader";
|
import { DocumentLoader } from "@/context/document/DocumentLoader";
|
||||||
import { useDocument } from "@/context/document/hooks";
|
import { useDocument } from "@/context/document/hooks";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import type { Campaign, DocumentId, Relationship, Session } from "@/lib/types";
|
import type { Campaign, DocumentId, Relationship, Session } from "@/lib/types";
|
||||||
import { Button } from "@headlessui/react";
|
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const CampaignTabs = {
|
const CampaignTabs = {
|
||||||
sessions: "Sessions",
|
sessions: { label: "Sessions", docType: "session" },
|
||||||
npcs: "NPCs",
|
secrets: { label: "Secrets", docType: "secret" },
|
||||||
locations: "Locations",
|
npcs: { label: "NPCs", docType: "npc" },
|
||||||
factions: "Factions",
|
locations: { label: "Locations", docType: "location" },
|
||||||
threads: "Threads",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const campaignSearchSchema = z.object({
|
const campaignSearchSchema = z.object({
|
||||||
@@ -50,11 +47,11 @@ 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 sessions = await pb.collection("documents").getFullList({
|
// const sessions = await pb.collection("documents").getFullList({
|
||||||
filter: `campaign = "${params.campaignId}" && type = 'session'`,
|
// filter: `campaign = "${params.campaignId}" && type = 'session'`,
|
||||||
sort: "-created",
|
// sort: "-created",
|
||||||
});
|
// });
|
||||||
setSessions(sessions as Session[]);
|
// setSessions(sessions as Session[]);
|
||||||
setCampaign(campaign as Campaign);
|
setCampaign(campaign as Campaign);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -115,8 +112,9 @@ function RouteComponent() {
|
|||||||
← Back to campaigns
|
← Back to campaigns
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
tabs={Object.entries(CampaignTabs).map(([key, label]) => (
|
tabs={Object.entries(CampaignTabs).map(([key, { label }]) => (
|
||||||
<Tab
|
<Tab
|
||||||
|
key={key}
|
||||||
label={label}
|
label={label}
|
||||||
active={tab === key}
|
active={tab === key}
|
||||||
to={Route.to}
|
to={Route.to}
|
||||||
@@ -129,36 +127,10 @@ function RouteComponent() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
content={
|
content={
|
||||||
<div>
|
<CampaignDocuments
|
||||||
<div className="flex justify-between">
|
campaignId={campaign.id}
|
||||||
<h3 className="text-lg font-semibold mb-2 text-slate-200">
|
docType={CampaignTabs[tab].docType}
|
||||||
Sessions
|
/>
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
onClick={() => createNewSession()}
|
|
||||||
className="inline-flex items-center justify-center rounded bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-violet-400"
|
|
||||||
>
|
|
||||||
New Session
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{sessions && sessions.length > 0 ? (
|
|
||||||
<div>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{sessions.map((s: any) => (
|
|
||||||
<li key={s.id}>
|
|
||||||
<SessionRow session={s} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-slate-400">
|
|
||||||
No sessions found for this campaign.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
flyout={docId && <Flyout key={docId} docId={docId} />}
|
flyout={docId && <Flyout key={docId} docId={docId} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export const Route = createFileRoute(
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { documentId, relationshipType, childDocId } = useDocumentPath();
|
const path = useDocumentPath();
|
||||||
|
const documentId = path?.documentId;
|
||||||
|
const relationshipType = path?.relationshipType ?? null;
|
||||||
|
const childDocId = path?.childDocId ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentLoader documentId={documentId as DocumentId}>
|
<DocumentLoader documentId={documentId as DocumentId}>
|
||||||
|
|||||||
Reference in New Issue
Block a user