From 3390ecfb952cb76182f91f3b98b0274021292cd4 Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Mon, 21 Jul 2025 13:34:06 -0700 Subject: [PATCH] Finally gets the routing working in a somewhat reasonable way --- package-lock.json | 27 +++- package.json | 3 +- src/components/documents/DocumentView.tsx | 113 ++++++++++------ src/components/layout/TabbedLayout.tsx | 16 +++ src/routeTree.gen.ts | 125 ++++-------------- .../_authenticated/document.$documentId.$.tsx | 49 +++++++ .../_authenticated/document.$documentId.tsx | 21 --- .../$relationshipType.tsx | 19 --- ...henticated.document_.$documentId.print.tsx | 67 ---------- 9 files changed, 187 insertions(+), 253 deletions(-) create mode 100644 src/components/layout/TabbedLayout.tsx create mode 100644 src/routes/_app/_authenticated/document.$documentId.$.tsx delete mode 100644 src/routes/_app/_authenticated/document.$documentId.tsx delete mode 100644 src/routes/_app/_authenticated/document.$documentId/$relationshipType.tsx delete mode 100644 src/routes/_app_._authenticated.document_.$documentId.print.tsx diff --git a/package-lock.json b/package-lock.json index 3a15b57..c9af873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 57e6a4a..7f6548e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/documents/DocumentView.tsx b/src/components/documents/DocumentView.tsx index 9e6eb06..4d2550e 100644 --- a/src/components/documents/DocumentView.tsx +++ b/src/components/documents/DocumentView.tsx @@ -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,52 +26,72 @@ 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 ( -
- -
+ + + Back to campaign + + + Print + + + } + title={} + tabs={[ - Back to campaign - - - Print - -
- - -
+
+ Attributes +
+ , + ...relationshipList.map((relationshipType) => ( + +
+ {displayName(relationshipType)} ( + {relationshipCounts[relationshipType] ?? 0}) +
+ + )), + ]} + content={ + relationshipType === null ? ( + + ) : ( + + ) + } + /> ); } diff --git a/src/components/layout/TabbedLayout.tsx b/src/components/layout/TabbedLayout.tsx new file mode 100644 index 0000000..e34782e --- /dev/null +++ b/src/components/layout/TabbedLayout.tsx @@ -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 ( +
+
{navigation}
+
{title}
+
{tabs}
+
{content}
+
+ ); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index eddc217..02ee140 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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" } } } diff --git a/src/routes/_app/_authenticated/document.$documentId.$.tsx b/src/routes/_app/_authenticated/document.$documentId.$.tsx new file mode 100644 index 0000000..60146b3 --- /dev/null +++ b/src/routes/_app/_authenticated/document.$documentId.$.tsx @@ -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 ( + + + + ); +} diff --git a/src/routes/_app/_authenticated/document.$documentId.tsx b/src/routes/_app/_authenticated/document.$documentId.tsx deleted file mode 100644 index e2fd599..0000000 --- a/src/routes/_app/_authenticated/document.$documentId.tsx +++ /dev/null @@ -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 ( - - - - - ); -} diff --git a/src/routes/_app/_authenticated/document.$documentId/$relationshipType.tsx b/src/routes/_app/_authenticated/document.$documentId/$relationshipType.tsx deleted file mode 100644 index 7d49a7a..0000000 --- a/src/routes/_app/_authenticated/document.$documentId/$relationshipType.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/routes/_app_._authenticated.document_.$documentId.print.tsx b/src/routes/_app_._authenticated.document_.$documentId.print.tsx deleted file mode 100644 index 9f2dc96..0000000 --- a/src/routes/_app_._authenticated.document_.$documentId.print.tsx +++ /dev/null @@ -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 ; - } - - 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 ( -
- - {[ - RelationshipType.Scenes, - RelationshipType.Secrets, - RelationshipType.Locations, - RelationshipType.Npcs, - RelationshipType.Monsters, - RelationshipType.Treasures, - ].map((relationshipType) => ( -
-

- {relationshipType.charAt(0).toUpperCase() + - relationshipType.slice(1)} -

- -
    - {(relationships[relationshipType] ?? []).map((item) => ( - - ))} -
-
- ))} -
- ); -}