From 8533f63a2266434ee077ba2cbd5017623817eefc Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Mon, 21 Jul 2025 20:50:18 -0700 Subject: [PATCH] Completes the three-panel layout --- src/components/DocumentList.tsx | 2 +- src/components/RelationshipList.tsx | 2 +- src/components/documents/BasicRow.tsx | 12 ++-- src/components/documents/DocumentLink.tsx | 21 +++++++ src/components/documents/DocumentRow.tsx | 8 +-- src/components/documents/DocumentView.tsx | 44 +++++++------ .../documents/secret/SecretToggleRow.tsx | 12 ++-- .../documents/treasure/TreasureToggleRow.tsx | 12 ++-- src/components/layout/TabbedLayout.tsx | 61 +++++++++++++++++-- src/lib/documentPath.ts | 54 ++++++++++++++++ .../_authenticated/document.$documentId.$.tsx | 30 +-------- 11 files changed, 184 insertions(+), 74 deletions(-) create mode 100644 src/components/documents/DocumentLink.tsx create mode 100644 src/lib/documentPath.ts diff --git a/src/components/DocumentList.tsx b/src/components/DocumentList.tsx index caba9b9..5c18b5a 100644 --- a/src/components/DocumentList.tsx +++ b/src/components/DocumentList.tsx @@ -48,7 +48,7 @@ export function DocumentList({ return (
-
+

{title}

{isEditing && ( diff --git a/src/components/RelationshipList.tsx b/src/components/RelationshipList.tsx index b2de2f3..59bd4be 100644 --- a/src/components/RelationshipList.tsx +++ b/src/components/RelationshipList.tsx @@ -118,7 +118,7 @@ export function RelationshipList({ title={displayName(relationshipType)} items={items} error={error} - renderRow={(document) => } + renderRow={(document) => } removeItem={handleRemove} newItemForm={(onSubmit) => ( string; }; /** @@ -13,13 +14,12 @@ export type Props = { export const BasicRow = ({ doc, title, description }: Props) => { return (
-

{title}

- + {description &&

{description}

}
); diff --git a/src/components/documents/DocumentLink.tsx b/src/components/documents/DocumentLink.tsx new file mode 100644 index 0000000..ce093c5 --- /dev/null +++ b/src/components/documents/DocumentLink.tsx @@ -0,0 +1,21 @@ +import { makeDocumentPath, useDocumentPath } from "@/lib/documentPath"; +import type { DocumentId, RelationshipType } from "@/lib/types"; +import { Link } from "@tanstack/react-router"; + +export type Props = React.PropsWithChildren<{ + childDocId: DocumentId; + className?: string; +}>; + +export function DocumentLink({ childDocId, className, children }: Props) { + const { documentId, relationshipType } = useDocumentPath(); + + return ( + + {children} + + ); +} diff --git a/src/components/documents/DocumentRow.tsx b/src/components/documents/DocumentRow.tsx index e7cde0e..e82dc5f 100644 --- a/src/components/documents/DocumentRow.tsx +++ b/src/components/documents/DocumentRow.tsx @@ -11,10 +11,10 @@ import { TreasureToggleRow } from "./treasure/TreasureToggleRow"; */ export const DocumentRow = ({ document, - session, + root, }: { document: AnyDocument; - session?: Session; + root?: AnyDocument; }) => { switch (document.type) { case "location": @@ -48,12 +48,12 @@ export const DocumentRow = ({ ); case "secret": - return ; + return ; case "scene": return ; case "treasure": - return ; + return ; } }; diff --git a/src/components/documents/DocumentView.tsx b/src/components/documents/DocumentView.tsx index 4d2550e..64b5467 100644 --- a/src/components/documents/DocumentView.tsx +++ b/src/components/documents/DocumentView.tsx @@ -6,16 +6,18 @@ import { Link } from "@tanstack/react-router"; import _ from "lodash"; import { Loader } from "../Loader"; import { DocumentTitle } from "./DocumentTitle"; -import { TabbedLayout } from "../layout/TabbedLayout"; +import { Tab, TabbedLayout } from "../layout/TabbedLayout"; import { DocumentEditForm } from "./DocumentEditForm"; import { RelatedDocumentList } from "./RelatedDocumentList"; export function DocumentView({ documentId, relationshipType, + childDocId, }: { documentId: DocumentId; relationshipType: RelationshipType | null; + childDocId: DocumentId | null; }) { const { docResult } = useDocument(documentId); @@ -55,31 +57,24 @@ export function DocumentView({ } title={} tabs={[ - -
- Attributes -
- , - ...relationshipList.map((relationshipType) => ( - , + ...relationshipList.map((relationshipEntry) => ( + -
- {displayName(relationshipType)} ( - {relationshipCounts[relationshipType] ?? 0}) -
- + label={`${displayName(relationshipEntry)} (${relationshipCounts[relationshipEntry] ?? 0})`} + active={relationshipEntry === relationshipType} + /> )), ]} content={ @@ -92,6 +87,19 @@ export function DocumentView({ /> ) } + flyout={childDocId && } /> ); } + +function Flyout({ docId }: { docId: DocumentId }) { + const { docResult } = useDocument(docId); + + if (docResult?.type !== "ready") { + return ; + } + + const doc = docResult.value.doc; + + return ; +} diff --git a/src/components/documents/secret/SecretToggleRow.tsx b/src/components/documents/secret/SecretToggleRow.tsx index 5fa275b..192e36b 100644 --- a/src/components/documents/secret/SecretToggleRow.tsx +++ b/src/components/documents/secret/SecretToggleRow.tsx @@ -1,7 +1,7 @@ // SecretRow.tsx // Displays a single secret with discovered checkbox and text. import { pb } from "@/lib/pocketbase"; -import type { Secret, Session } from "@/lib/types"; +import type { AnyDocument, Secret } from "@/lib/types"; import { useState } from "react"; /** @@ -10,10 +10,10 @@ import { useState } from "react"; */ export const SecretToggleRow = ({ secret, - session, + root, }: { secret: Secret; - session?: Session; + root?: AnyDocument; }) => { const [checked, setChecked] = useState( !!(secret.data as any)?.secret?.discovered, @@ -36,7 +36,7 @@ export const SecretToggleRow = ({ }, }, }); - if (session || !newChecked) { + if (root || !newChecked) { // If the session exists or the element is being unchecked, remove any // existing discoveredIn relationship const rels = await pb.collection("relationships").getList(1, 1, { @@ -46,11 +46,11 @@ export const SecretToggleRow = ({ await pb.collection("relationships").delete(rels.items[0].id); } } - if (session) { + if (root) { if (newChecked) { await pb.collection("relationships").create({ primary: secret.id, - secondary: [session.id], + secondary: [root.id], type: "discoveredIn", }); } diff --git a/src/components/documents/treasure/TreasureToggleRow.tsx b/src/components/documents/treasure/TreasureToggleRow.tsx index 3c61967..e9e6ef0 100644 --- a/src/components/documents/treasure/TreasureToggleRow.tsx +++ b/src/components/documents/treasure/TreasureToggleRow.tsx @@ -1,7 +1,7 @@ // TreasureRow.tsx // Displays a single treasure with discovered checkbox and text. import { pb } from "@/lib/pocketbase"; -import type { Session, Treasure } from "@/lib/types"; +import type { AnyDocument, Treasure } from "@/lib/types"; import { Link } from "@tanstack/react-router"; import { useState } from "react"; @@ -11,10 +11,10 @@ import { useState } from "react"; */ export const TreasureToggleRow = ({ treasure, - session, + root, }: { treasure: Treasure; - session?: Session; + root?: AnyDocument; }) => { const [checked, setChecked] = useState( !!(treasure.data as any)?.treasure?.discovered, @@ -35,7 +35,7 @@ export const TreasureToggleRow = ({ }, }, }); - if (session || !newChecked) { + if (root || !newChecked) { // If the session exists or the element is being unchecked, remove any // existing discoveredIn relationship const rels = await pb.collection("relationships").getList(1, 1, { @@ -45,11 +45,11 @@ export const TreasureToggleRow = ({ await pb.collection("relationships").delete(rels.items[0].id); } } - if (session) { + if (root) { if (newChecked) { await pb.collection("relationships").create({ primary: treasure.id, - secondary: [session.id], + secondary: [root.id], type: "discoveredIn", }); } diff --git a/src/components/layout/TabbedLayout.tsx b/src/components/layout/TabbedLayout.tsx index e34782e..3153b3b 100644 --- a/src/components/layout/TabbedLayout.tsx +++ b/src/components/layout/TabbedLayout.tsx @@ -1,16 +1,67 @@ +import { Link } from "@tanstack/react-router"; + export type Props = { title: React.ReactNode; navigation: React.ReactNode; tabs: React.ReactNode[]; content: React.ReactNode; + flyout?: React.ReactNode; }; -export function TabbedLayout({ navigation, title, tabs, content }: Props) { +export function TabbedLayout({ + navigation, + title, + tabs, + content, + flyout, +}: Props) { return (
-
{navigation}
-
{title}
-
{tabs}
-
{content}
+
+
{navigation}
+
{title}
+
+
+
{tabs}
+
+ {content} +
+ {flyout && ( +
+ {flyout} +
+ )} +
); } + +export type TabProps = { + label: string; + to: string; + params: Record; + active?: boolean; +}; + +const activeTabClass = + "text-slate-100 font-bold bg-slate-800 border-t border-b border-l"; +const inactiveTabClass = "text-slate-300 bg-slate-900 border"; + +export function Tab({ label, to, params, active }: TabProps) { + return ( + + {label} + + ); +} diff --git a/src/lib/documentPath.ts b/src/lib/documentPath.ts new file mode 100644 index 0000000..a61e985 --- /dev/null +++ b/src/lib/documentPath.ts @@ -0,0 +1,54 @@ +import { useParams } from "@tanstack/react-router"; +import * as z from "zod"; +import type { RelationshipType, DocumentId } from "./types"; + +const documentParams = z + .templateLiteral([ + z.string(), + z.optional(z.literal("/")), + z.optional(z.string()), + ]) + .pipe( + z.transform((path: string) => { + if (path === "") { + return { + relationshipType: null, + childDocId: null, + }; + } + const [relationshipType, childDocId] = path.split("/"); + return { + relationshipType: (relationshipType ?? null) as RelationshipType | null, + childDocId: (childDocId ?? null) as DocumentId | null, + }; + }), + ); + +export function useDocumentPath(): { + documentId: DocumentId; + relationshipType: RelationshipType | null; + childDocId: DocumentId | null; +} { + const params = useParams({ + from: "/_app/_authenticated/document/$documentId/$", + }); + + const { relationshipType, childDocId } = documentParams.parse(params._splat); + + return { + documentId: params.documentId as DocumentId, + relationshipType, + childDocId, + }; +} + +export function makeDocumentPath( + documentId: DocumentId, + relationshipType?: RelationshipType | null, + childDocId?: DocumentId | null, +) { + return ( + "/document/" + + [documentId, relationshipType, childDocId].filter((x) => x).join("/") + ); +} diff --git a/src/routes/_app/_authenticated/document.$documentId.$.tsx b/src/routes/_app/_authenticated/document.$documentId.$.tsx index 60146b3..0b4b637 100644 --- a/src/routes/_app/_authenticated/document.$documentId.$.tsx +++ b/src/routes/_app/_authenticated/document.$documentId.$.tsx @@ -1,9 +1,8 @@ import { DocumentView } from "@/components/documents/DocumentView"; import { DocumentLoader } from "@/context/document/DocumentLoader"; +import { useDocumentPath } from "@/lib/documentPath"; 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/$", @@ -11,38 +10,15 @@ export const Route = createFileRoute( 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); + const { documentId, relationshipType, childDocId } = useDocumentPath(); return ( );