Completes the three-panel layout

This commit is contained in:
2025-07-21 20:50:18 -07:00
parent 3390ecfb95
commit 8533f63a22
11 changed files with 184 additions and 74 deletions

View File

@@ -48,7 +48,7 @@ export function DocumentList<T extends AnyDocument>({
return (
<section className="w-full max-w-2xl mx-auto">
<div className="flex items-center justify-between my-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-slate-100">{title}</h2>
<div className="flex gap-2">
{isEditing && (

View File

@@ -118,7 +118,7 @@ export function RelationshipList({
title={displayName(relationshipType)}
items={items}
error={error}
renderRow={(document) => <DocumentRow document={document} />}
renderRow={(document) => <DocumentRow document={document} root={root} />}
removeItem={handleRemove}
newItemForm={(onSubmit) => (
<NewRelatedDocumentForm

View File

@@ -1,10 +1,11 @@
import type { AnyDocument } from "@/lib/types";
import { Link } from "@tanstack/react-router";
import type { AnyDocument, DocumentId } from "@/lib/types";
import { DocumentLink } from "./DocumentLink";
export type Props = {
doc: AnyDocument;
title: string;
description?: string;
link: (id: DocumentId) => string;
};
/**
@@ -13,13 +14,12 @@ export type Props = {
export const BasicRow = ({ doc, title, description }: Props) => {
return (
<div>
<Link
to="/document/$documentId"
params={{ documentId: doc.id }}
<DocumentLink
childDocId={doc.id}
className="text-lg !no-underline text-slate-100 hover:underline hover:text-violet-400"
>
<h4>{title}</h4>
</Link>
</DocumentLink>
{description && <p>{description}</p>}
</div>
);

View File

@@ -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 (
<Link
to={makeDocumentPath(documentId, relationshipType, childDocId)}
className={className}
>
{children}
</Link>
);
}

View File

@@ -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 <SecretToggleRow secret={document} session={session} />;
return <SecretToggleRow secret={document} root={root} />;
case "scene":
return <BasicRow doc={document} title={document.data.text} />;
case "treasure":
return <TreasureToggleRow treasure={document} session={session} />;
return <TreasureToggleRow treasure={document} root={root} />;
}
};

View File

@@ -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={<DocumentTitle document={doc} />}
tabs={[
<Link
key={""}
<Tab
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>
</Link>,
...relationshipList.map((relationshipType) => (
<Link
key={relationshipType}
label="Attributes"
active={relationshipType === null}
/>,
...relationshipList.map((relationshipEntry) => (
<Tab
to="/document/$documentId/$relationshipType"
params={{
documentId,
relationshipType,
relationshipType: relationshipEntry,
}}
>
<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">
{displayName(relationshipType)} (
{relationshipCounts[relationshipType] ?? 0})
</div>
</Link>
label={`${displayName(relationshipEntry)} (${relationshipCounts[relationshipEntry] ?? 0})`}
active={relationshipEntry === relationshipType}
/>
)),
]}
content={
@@ -92,6 +87,19 @@ export function DocumentView({
/>
)
}
flyout={childDocId && <Flyout docId={childDocId} />}
/>
);
}
function Flyout({ docId }: { docId: DocumentId }) {
const { docResult } = useDocument(docId);
if (docResult?.type !== "ready") {
return <Loader />;
}
const doc = docResult.value.doc;
return <DocumentEditForm document={doc} />;
}

View File

@@ -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",
});
}

View File

@@ -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",
});
}

View File

@@ -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 (
<div>
<div>{navigation}</div>
<div>{title}</div>
<div>{tabs}</div>
<div>{content}</div>
<div className="">
<div>{navigation}</div>
<div>{title}</div>
</div>
<div className="flex flex-row justify-start m-2 max-w-5xl">
<div className="grow max-w-2xs p-0">{tabs}</div>
<div
className={`grow p-2 bg-slate-800 border-t border-b border-r border-slate-700`}
>
{content}
</div>
{flyout && (
<div
className="w-lg p-2 bg-slate-800 border border-slate-700 shadow-[0_0_15px_0_rgba(0,0,0,0.5)]"
style={{
"margin-left": "-32rem",
}}
>
{flyout}
</div>
)}
</div>
</div>
);
}
export type TabProps = {
label: string;
to: string;
params: Record<string, any>;
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 (
<Link
key={label}
to={to}
params={params}
className={`block p-2 border-slate-700 whitespace-nowrap ${active ? activeTabClass : inactiveTabClass}`}
>
{label}
</Link>
);
}