Finally gets the routing working in a somewhat reasonable way

This commit is contained in:
2025-07-21 13:34:06 -07:00
parent b30999e907
commit 3390ecfb95
9 changed files with 187 additions and 253 deletions

27
package-lock.json generated
View File

@@ -17,7 +17,8 @@
"pocketbase": "^0.26.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.6"
"tailwindcss": "^4.0.6",
"zod": "^4.0.5"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
@@ -1926,6 +1927,15 @@
}
}
},
"node_modules/@tanstack/router-generator/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/router-plugin": {
"version": "1.120.10",
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.120.10.tgz",
@@ -1982,6 +1992,15 @@
}
}
},
"node_modules/@tanstack/router-plugin/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/router-utils": {
"version": "1.115.0",
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.115.0.tgz",
@@ -4503,9 +4522,9 @@
}
},
"node_modules/zod": {
"version": "3.25.28",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.28.tgz",
"integrity": "sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz",
"integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -24,7 +24,8 @@
"pocketbase": "^0.26.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.6"
"tailwindcss": "^4.0.6",
"zod": "^4.0.5"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",

View File

@@ -1,15 +1,22 @@
import { DocumentEditForm } from "@/components/documents/DocumentEditForm";
import { useDocument } from "@/context/document/hooks";
import { displayName, relationshipsForDocument } from "@/lib/relationships";
import type { DocumentId } from "@/lib/types";
import { RelationshipType, type DocumentId } from "@/lib/types";
import { Route as CampaignRoute } from "@/routes/_app/_authenticated/campaigns.$campaignId";
import { Link } from "@tanstack/react-router";
import _ from "lodash";
import { Loader } from "../Loader";
import { Route as RelationshipRoute } from "@/routes/_app/_authenticated/document.$documentId/$relationshipType";
import { DocumentTitle } from "./DocumentTitle";
import { TabbedLayout } from "../layout/TabbedLayout";
import { DocumentEditForm } from "./DocumentEditForm";
import { RelatedDocumentList } from "./RelatedDocumentList";
export function DocumentView({ documentId }: { documentId: DocumentId }) {
export function DocumentView({
documentId,
relationshipType,
}: {
documentId: DocumentId;
relationshipType: RelationshipType | null;
}) {
const { docResult } = useDocument(documentId);
if (docResult?.type !== "ready") {
@@ -19,17 +26,17 @@ export function DocumentView({ documentId }: { documentId: DocumentId }) {
const doc = docResult.value.doc;
const relationshipCounts = _.mapValues(docResult.value.relationships, (v) => {
if (v.type === "ready") {
return v.value.secondary.length;
return v.value.secondary.length.toString();
}
return 0;
return "...";
});
const relationshipList = relationshipsForDocument(doc);
return (
<div key={doc.id} className="max-w-xl mx-auto py-2 px-4">
<DocumentTitle document={doc} />
<div>
<TabbedLayout
navigation={
<>
<Link
to={CampaignRoute.to}
params={{ campaignId: doc.campaign }}
@@ -44,14 +51,25 @@ export function DocumentView({ documentId }: { documentId: DocumentId }) {
>
Print
</Link>
</>
}
title={<DocumentTitle document={doc} />}
tabs={[
<Link
key={""}
to="/document/$documentId"
params={{
documentId,
}}
>
<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">
Attributes
</div>
<DocumentEditForm document={doc} />
<nav>
<ul className="flex flex-row gap-1">
{relationshipList.map((relationshipType) => (
</Link>,
...relationshipList.map((relationshipType) => (
<Link
key={relationshipType}
to={RelationshipRoute.to}
to="/document/$documentId/$relationshipType"
params={{
documentId,
relationshipType,
@@ -62,9 +80,18 @@ export function DocumentView({ documentId }: { documentId: DocumentId }) {
{relationshipCounts[relationshipType] ?? 0})
</div>
</Link>
))}
</ul>
</nav>
</div>
)),
]}
content={
relationshipType === null ? (
<DocumentEditForm document={doc} />
) : (
<RelatedDocumentList
documentId={doc.id}
relationshipType={relationshipType}
/>
)
}
/>
);
}

View File

@@ -0,0 +1,16 @@
export type Props = {
title: React.ReactNode;
navigation: React.ReactNode;
tabs: React.ReactNode[];
content: React.ReactNode;
};
export function TabbedLayout({ navigation, title, tabs, content }: Props) {
return (
<div>
<div>{navigation}</div>
<div>{title}</div>
<div>{tabs}</div>
<div>{content}</div>
</div>
);
}

View File

@@ -17,10 +17,8 @@ import { Route as AppLoginImport } from './routes/_app/login'
import { Route as AppAboutImport } from './routes/_app/about'
import { Route as AppAuthenticatedImport } from './routes/_app/_authenticated'
import { Route as AppAuthenticatedCampaignsIndexImport } from './routes/_app/_authenticated/campaigns.index'
import { Route as AppAuthenticatedDocumentDocumentIdImport } from './routes/_app/_authenticated/document.$documentId'
import { Route as AppAuthenticatedCampaignsCampaignIdImport } from './routes/_app/_authenticated/campaigns.$campaignId'
import { Route as AppauthenticatedDocumentDocumentIdPrintImport } from './routes/_app_._authenticated.document_.$documentId.print'
import { Route as AppAuthenticatedDocumentDocumentIdRelationshipTypeImport } from './routes/_app/_authenticated/document.$documentId/$relationshipType'
import { Route as AppAuthenticatedDocumentDocumentIdSplatImport } from './routes/_app/_authenticated/document.$documentId.$'
// Create/Update Routes
@@ -59,13 +57,6 @@ const AppAuthenticatedCampaignsIndexRoute =
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AppAuthenticatedDocumentDocumentIdRoute =
AppAuthenticatedDocumentDocumentIdImport.update({
id: '/document/$documentId',
path: '/document/$documentId',
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AppAuthenticatedCampaignsCampaignIdRoute =
AppAuthenticatedCampaignsCampaignIdImport.update({
id: '/campaigns/$campaignId',
@@ -73,18 +64,11 @@ const AppAuthenticatedCampaignsCampaignIdRoute =
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AppauthenticatedDocumentDocumentIdPrintRoute =
AppauthenticatedDocumentDocumentIdPrintImport.update({
id: '/_app_/_authenticated/document_/$documentId/print',
path: '/document/$documentId/print',
getParentRoute: () => rootRoute,
} as any)
const AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute =
AppAuthenticatedDocumentDocumentIdRelationshipTypeImport.update({
id: '/$relationshipType',
path: '/$relationshipType',
getParentRoute: () => AppAuthenticatedDocumentDocumentIdRoute,
const AppAuthenticatedDocumentDocumentIdSplatRoute =
AppAuthenticatedDocumentDocumentIdSplatImport.update({
id: '/document/$documentId/$',
path: '/document/$documentId/$',
getParentRoute: () => AppAuthenticatedRoute,
} as any)
// Populate the FileRoutesByPath interface
@@ -133,13 +117,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAuthenticatedCampaignsCampaignIdImport
parentRoute: typeof AppAuthenticatedImport
}
'/_app/_authenticated/document/$documentId': {
id: '/_app/_authenticated/document/$documentId'
path: '/document/$documentId'
fullPath: '/document/$documentId'
preLoaderRoute: typeof AppAuthenticatedDocumentDocumentIdImport
parentRoute: typeof AppAuthenticatedImport
}
'/_app/_authenticated/campaigns/': {
id: '/_app/_authenticated/campaigns/'
path: '/campaigns'
@@ -147,52 +124,30 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAuthenticatedCampaignsIndexImport
parentRoute: typeof AppAuthenticatedImport
}
'/_app/_authenticated/document/$documentId/$relationshipType': {
id: '/_app/_authenticated/document/$documentId/$relationshipType'
path: '/$relationshipType'
fullPath: '/document/$documentId/$relationshipType'
preLoaderRoute: typeof AppAuthenticatedDocumentDocumentIdRelationshipTypeImport
parentRoute: typeof AppAuthenticatedDocumentDocumentIdImport
}
'/_app_/_authenticated/document_/$documentId/print': {
id: '/_app_/_authenticated/document_/$documentId/print'
path: '/document/$documentId/print'
fullPath: '/document/$documentId/print'
preLoaderRoute: typeof AppauthenticatedDocumentDocumentIdPrintImport
parentRoute: typeof rootRoute
'/_app/_authenticated/document/$documentId/$': {
id: '/_app/_authenticated/document/$documentId/$'
path: '/document/$documentId/$'
fullPath: '/document/$documentId/$'
preLoaderRoute: typeof AppAuthenticatedDocumentDocumentIdSplatImport
parentRoute: typeof AppAuthenticatedImport
}
}
}
// Create and export the route tree
interface AppAuthenticatedDocumentDocumentIdRouteChildren {
AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute: typeof AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute
}
const AppAuthenticatedDocumentDocumentIdRouteChildren: AppAuthenticatedDocumentDocumentIdRouteChildren =
{
AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute:
AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute,
}
const AppAuthenticatedDocumentDocumentIdRouteWithChildren =
AppAuthenticatedDocumentDocumentIdRoute._addFileChildren(
AppAuthenticatedDocumentDocumentIdRouteChildren,
)
interface AppAuthenticatedRouteChildren {
AppAuthenticatedCampaignsCampaignIdRoute: typeof AppAuthenticatedCampaignsCampaignIdRoute
AppAuthenticatedDocumentDocumentIdRoute: typeof AppAuthenticatedDocumentDocumentIdRouteWithChildren
AppAuthenticatedCampaignsIndexRoute: typeof AppAuthenticatedCampaignsIndexRoute
AppAuthenticatedDocumentDocumentIdSplatRoute: typeof AppAuthenticatedDocumentDocumentIdSplatRoute
}
const AppAuthenticatedRouteChildren: AppAuthenticatedRouteChildren = {
AppAuthenticatedCampaignsCampaignIdRoute:
AppAuthenticatedCampaignsCampaignIdRoute,
AppAuthenticatedDocumentDocumentIdRoute:
AppAuthenticatedDocumentDocumentIdRouteWithChildren,
AppAuthenticatedCampaignsIndexRoute: AppAuthenticatedCampaignsIndexRoute,
AppAuthenticatedDocumentDocumentIdSplatRoute:
AppAuthenticatedDocumentDocumentIdSplatRoute,
}
const AppAuthenticatedRouteWithChildren =
@@ -220,10 +175,8 @@ export interface FileRoutesByFullPath {
'/login': typeof AppLoginRoute
'/': typeof AppIndexRoute
'/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRouteWithChildren
'/campaigns': typeof AppAuthenticatedCampaignsIndexRoute
'/document/$documentId/$relationshipType': typeof AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute
'/document/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
'/document/$documentId/$': typeof AppAuthenticatedDocumentDocumentIdSplatRoute
}
export interface FileRoutesByTo {
@@ -232,10 +185,8 @@ export interface FileRoutesByTo {
'/login': typeof AppLoginRoute
'/': typeof AppIndexRoute
'/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRouteWithChildren
'/campaigns': typeof AppAuthenticatedCampaignsIndexRoute
'/document/$documentId/$relationshipType': typeof AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute
'/document/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
'/document/$documentId/$': typeof AppAuthenticatedDocumentDocumentIdSplatRoute
}
export interface FileRoutesById {
@@ -246,10 +197,8 @@ export interface FileRoutesById {
'/_app/login': typeof AppLoginRoute
'/_app/': typeof AppIndexRoute
'/_app/_authenticated/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/_app/_authenticated/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRouteWithChildren
'/_app/_authenticated/campaigns/': typeof AppAuthenticatedCampaignsIndexRoute
'/_app/_authenticated/document/$documentId/$relationshipType': typeof AppAuthenticatedDocumentDocumentIdRelationshipTypeRoute
'/_app_/_authenticated/document_/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
'/_app/_authenticated/document/$documentId/$': typeof AppAuthenticatedDocumentDocumentIdSplatRoute
}
export interface FileRouteTypes {
@@ -260,10 +209,8 @@ export interface FileRouteTypes {
| '/login'
| '/'
| '/campaigns/$campaignId'
| '/document/$documentId'
| '/campaigns'
| '/document/$documentId/$relationshipType'
| '/document/$documentId/print'
| '/document/$documentId/$'
fileRoutesByTo: FileRoutesByTo
to:
| ''
@@ -271,10 +218,8 @@ export interface FileRouteTypes {
| '/login'
| '/'
| '/campaigns/$campaignId'
| '/document/$documentId'
| '/campaigns'
| '/document/$documentId/$relationshipType'
| '/document/$documentId/print'
| '/document/$documentId/$'
id:
| '__root__'
| '/_app'
@@ -283,22 +228,17 @@ export interface FileRouteTypes {
| '/_app/login'
| '/_app/'
| '/_app/_authenticated/campaigns/$campaignId'
| '/_app/_authenticated/document/$documentId'
| '/_app/_authenticated/campaigns/'
| '/_app/_authenticated/document/$documentId/$relationshipType'
| '/_app_/_authenticated/document_/$documentId/print'
| '/_app/_authenticated/document/$documentId/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
AppRoute: typeof AppRouteWithChildren
AppauthenticatedDocumentDocumentIdPrintRoute: typeof AppauthenticatedDocumentDocumentIdPrintRoute
}
const rootRouteChildren: RootRouteChildren = {
AppRoute: AppRouteWithChildren,
AppauthenticatedDocumentDocumentIdPrintRoute:
AppauthenticatedDocumentDocumentIdPrintRoute,
}
export const routeTree = rootRoute
@@ -311,8 +251,7 @@ export const routeTree = rootRoute
"__root__": {
"filePath": "__root.tsx",
"children": [
"/_app",
"/_app_/_authenticated/document_/$documentId/print"
"/_app"
]
},
"/_app": {
@@ -329,8 +268,8 @@ export const routeTree = rootRoute
"parent": "/_app",
"children": [
"/_app/_authenticated/campaigns/$campaignId",
"/_app/_authenticated/document/$documentId",
"/_app/_authenticated/campaigns/"
"/_app/_authenticated/campaigns/",
"/_app/_authenticated/document/$documentId/$"
]
},
"/_app/about": {
@@ -349,23 +288,13 @@ export const routeTree = rootRoute
"filePath": "_app/_authenticated/campaigns.$campaignId.tsx",
"parent": "/_app/_authenticated"
},
"/_app/_authenticated/document/$documentId": {
"filePath": "_app/_authenticated/document.$documentId.tsx",
"parent": "/_app/_authenticated",
"children": [
"/_app/_authenticated/document/$documentId/$relationshipType"
]
},
"/_app/_authenticated/campaigns/": {
"filePath": "_app/_authenticated/campaigns.index.tsx",
"parent": "/_app/_authenticated"
},
"/_app/_authenticated/document/$documentId/$relationshipType": {
"filePath": "_app/_authenticated/document.$documentId/$relationshipType.tsx",
"parent": "/_app/_authenticated/document/$documentId"
},
"/_app_/_authenticated/document_/$documentId/print": {
"filePath": "_app_._authenticated.document_.$documentId.print.tsx"
"/_app/_authenticated/document/$documentId/$": {
"filePath": "_app/_authenticated/document.$documentId.$.tsx",
"parent": "/_app/_authenticated"
}
}
}

View File

@@ -0,0 +1,49 @@
import { DocumentView } from "@/components/documents/DocumentView";
import { DocumentLoader } from "@/context/document/DocumentLoader";
import type { DocumentId } from "@/lib/types";
import { RelationshipType } from "@/lib/types";
import { createFileRoute } from "@tanstack/react-router";
import * as z from "zod";
export const Route = createFileRoute(
"/_app/_authenticated/document/$documentId/$",
)({
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() {
const { documentId, _splat } = Route.useParams();
const { relationshipType, childDoc } = documentParams.parse(_splat);
return (
<DocumentLoader documentId={documentId as DocumentId}>
<DocumentView
documentId={documentId as DocumentId}
relationshipType={relationshipType}
/>
</DocumentLoader>
);
}

View File

@@ -1,21 +0,0 @@
import { DocumentView } from "@/components/documents/DocumentView";
import { DocumentLoader } from "@/context/document/DocumentLoader";
import type { DocumentId } from "@/lib/types";
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute(
"/_app/_authenticated/document/$documentId",
)({
component: RouteComponent,
});
function RouteComponent() {
const { documentId } = Route.useParams();
return (
<DocumentLoader documentId={documentId as DocumentId}>
<DocumentView documentId={documentId as DocumentId} />
<Outlet />
</DocumentLoader>
);
}

View File

@@ -1,19 +0,0 @@
import { RelatedDocumentList } from "@/components/documents/RelatedDocumentList";
import type { DocumentId, RelationshipType } from "@/lib/types";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute(
"/_app/_authenticated/document/$documentId/$relationshipType",
)({
component: RouteComponent,
});
function RouteComponent() {
const { documentId, relationshipType } = Route.useParams();
return (
<RelatedDocumentList
documentId={documentId as DocumentId}
relationshipType={relationshipType as RelationshipType}
/>
);
}

View File

@@ -1,67 +0,0 @@
import { DocumentPrintRow } from "@/components/documents/DocumentPrintRow";
import { SessionPrintRow } from "@/components/documents/session/SessionPrintRow";
import { Loader } from "@/components/Loader";
import { useDocument, useDocumentCache } from "@/context/document/hooks";
import { RelationshipType, type DocumentId, type Session } from "@/lib/types";
import { createFileRoute } from "@tanstack/react-router";
import _ from "lodash";
export const Route = createFileRoute(
"/_app_/_authenticated/document_/$documentId/print",
)({
component: RouteComponent,
pendingComponent: Loader,
});
function RouteComponent() {
const params = Route.useParams();
const { cache } = useDocumentCache();
const { docResult } = useDocument(params.documentId as DocumentId);
if (docResult.type !== "ready") {
return <Loader />;
}
const session = docResult.value.doc as Session;
const relationships = _.mapValues(
docResult.value.relationships,
(relResult) => {
if (relResult.type != "ready") {
return [];
}
return relResult.value.secondary
.map((id) => cache.documents[id])
.flatMap((docResult) =>
docResult.type === "ready" ? [docResult.value.doc] : [],
);
},
);
return (
<div className="fill-w py-8 columns-2 gap-8 text-sm">
<SessionPrintRow session={session}></SessionPrintRow>
{[
RelationshipType.Scenes,
RelationshipType.Secrets,
RelationshipType.Locations,
RelationshipType.Npcs,
RelationshipType.Monsters,
RelationshipType.Treasures,
].map((relationshipType) => (
<div className="break-inside-avoid">
<h3 className="text-lg font-bold text-slate-600">
{relationshipType.charAt(0).toUpperCase() +
relationshipType.slice(1)}
</h3>
<ul className="list-disc pl-5">
{(relationships[relationshipType] ?? []).map((item) => (
<DocumentPrintRow document={item} />
))}
</ul>
</div>
))}
</div>
);
}