Compare commits

...

4 Commits

Author SHA1 Message Date
3390ecfb95 Finally gets the routing working in a somewhat reasonable way 2025-07-21 13:34:06 -07:00
b30999e907 Linting 2025-07-15 11:02:47 -07:00
2e9ea14507 Adds a title to the document page. 2025-07-15 11:00:37 -07:00
762306023b Removes Tanstack Query 2025-07-15 10:53:28 -07:00
16 changed files with 288 additions and 500 deletions

155
README.md
View File

@@ -1,15 +1,17 @@
Welcome to your new TanStack app! # DM Companion App
# Getting Started ## Development
### Getting Started
To run this application: To run this application:
```bash ```bash
npm install npm install
npm run start npm run start
``` ```
# Building For Production ### Building For Production
To build this application for production: To build this application for production:
@@ -17,7 +19,7 @@ To build this application for production:
npm run build npm run build
``` ```
## Testing ### Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
@@ -25,17 +27,15 @@ This project uses [Vitest](https://vitest.dev/) for testing. You can run the tes
npm run test npm run test
``` ```
## Styling ### Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
### Routing
## Routing
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
### Adding A Route #### Adding A Route
To add a new route to your application just add another a new file in the `./src/routes` directory. To add a new route to your application just add another a new file in the `./src/routes` directory.
@@ -43,7 +43,7 @@ TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a `Link` component to navigate between them. Now that you have two routes you can use a `Link` component to navigate between them.
### Adding Links #### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
@@ -61,15 +61,15 @@ This will create a link that will navigate to the `/about` route.
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
### Using A Layout #### Using A Layout
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component. In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
Here is an example layout that includes a header: Here is an example layout that includes a header:
```tsx ```tsx
import { Outlet, createRootRoute } from '@tanstack/react-router' import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
@@ -86,129 +86,20 @@ export const Route = createRootRoute({
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</> </>
), ),
}) });
``` ```
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout. The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
### Data Fetching
## Data Fetching #### Pocketbase
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. TODO
For example: ### State Management
```tsx
const peopleRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/people",
loader: async () => {
const response = await fetch("https://swapi.dev/api/people");
return response.json() as Promise<{
results: {
name: string;
}[];
}>;
},
component: () => {
const data = peopleRoute.useLoaderData();
return (
<ul>
{data.results.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
);
},
});
```
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
### React-Query
React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.
First add your dependencies:
```bash
npm install @tanstack/react-query @tanstack/react-query-devtools
```
Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`.
```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// ...
const queryClient = new QueryClient();
// ...
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
```
You can also add TanStack Query Devtools to the root route (optional).
```tsx
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const rootRoute = createRootRoute({
component: () => (
<>
<Outlet />
<ReactQueryDevtools buttonPosition="top-right" />
<TanStackRouterDevtools />
</>
),
});
```
Now you can use `useQuery` to fetch your data.
```tsx
import { useQuery } from "@tanstack/react-query";
import "./App.css";
function App() {
const { data } = useQuery({
queryKey: ["people"],
queryFn: () =>
fetch("https://swapi.dev/api/people")
.then((res) => res.json())
.then((data) => data.results as { name: string }[]),
initialData: [],
});
return (
<div>
<ul>
{data.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
</div>
);
}
export default App;
```
You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).
## State Management
Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project. Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.
@@ -280,11 +171,3 @@ We use the `Derived` class to create a new store that is derived from another st
Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook. Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.
You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest). You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).
# Demo files
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
# Learn More
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).

30
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.79.0",
"@tanstack/react-query-devtools": "^5.79.0", "@tanstack/react-query-devtools": "^5.79.0",
"@tanstack/react-router": "^1.114.3", "@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3", "@tanstack/react-router-devtools": "^1.114.3",
@@ -18,7 +17,8 @@
"pocketbase": "^0.26.0", "pocketbase": "^0.26.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwindcss": "^4.0.6" "tailwindcss": "^4.0.6",
"zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.4", "@astrojs/ts-plugin": "^1.10.4",
@@ -1722,6 +1722,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz",
"integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==", "integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
@@ -1742,6 +1743,7 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz",
"integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==", "integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.79.0" "@tanstack/query-core": "5.79.0"
}, },
@@ -1925,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": { "node_modules/@tanstack/router-plugin": {
"version": "1.120.10", "version": "1.120.10",
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.120.10.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.120.10.tgz",
@@ -1981,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": { "node_modules/@tanstack/router-utils": {
"version": "1.115.0", "version": "1.115.0",
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.115.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.115.0.tgz",
@@ -4502,9 +4522,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.28", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.28.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz",
"integrity": "sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==", "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -16,7 +16,6 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4", "@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.79.0",
"@tanstack/react-query-devtools": "^5.79.0", "@tanstack/react-query-devtools": "^5.79.0",
"@tanstack/react-router": "^1.114.3", "@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3", "@tanstack/react-router-devtools": "^1.114.3",
@@ -25,7 +24,8 @@
"pocketbase": "^0.26.0", "pocketbase": "^0.26.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwindcss": "^4.0.6" "tailwindcss": "^4.0.6",
"zod": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.4", "@astrojs/ts-plugin": "^1.10.4",

View File

@@ -0,0 +1,24 @@
import { type AnyDocument, type Session } from "@/lib/types";
import { FormattedDate } from "../FormattedDate";
/**
* Renders the document title to go at the top a document page.
*/
export const DocumentTitle = ({
document,
}: {
document: AnyDocument;
session?: Session;
}) => {
switch (document.type) {
case "session":
return (
<h1>
<FormattedDate date={document.created} />
</h1>
);
default:
return <h1>document.type</h1>;
}
};

View File

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

View File

@@ -1,7 +1,7 @@
import { useDocument, useDocumentCache } from "@/context/document/hooks"; import { useDocument } from "@/context/document/hooks";
import type { DocumentId, RelationshipType } from "@/lib/types"; import type { DocumentId, RelationshipType } from "@/lib/types";
import { RelationshipList } from "../RelationshipList";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
import { RelationshipList } from "../RelationshipList";
export type Props = { export type Props = {
documentId: DocumentId; documentId: DocumentId;
@@ -11,8 +11,6 @@ export type Props = {
export function RelatedDocumentList({ documentId, relationshipType }: Props) { export function RelatedDocumentList({ documentId, relationshipType }: Props) {
const { docResult } = useDocument(documentId); const { docResult } = useDocument(documentId);
const { cache } = useDocumentCache();
if (docResult?.type !== "ready") { if (docResult?.type !== "ready") {
return <Loader />; return <Loader />;
} }

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

@@ -1,5 +1,4 @@
import { createContext, useContext, useCallback } from "react"; import { createContext, useContext, useCallback, useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import type { AuthRecord } from "pocketbase"; import type { AuthRecord } from "pocketbase";
@@ -26,91 +25,47 @@ export interface AuthContextValue {
const AuthContext = createContext<AuthContextValue | undefined>(undefined); const AuthContext = createContext<AuthContextValue | undefined>(undefined);
/** /**
* Fetches the currently authenticated user from PocketBase. * Provider for authentication context.
*/
async function fetchUser(): Promise<AuthRecord | null> {
if (pb.authStore.isValid) {
return pb.authStore.record;
}
return null;
}
/**
* Provider for authentication context, using TanStack Query for state management.
*/ */
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient(); const [isLoading, setIsLoading] = useState(false);
const { data: user, isLoading } = useQuery({ const [user, setUser] = useState<AuthRecord | null>(pb.authStore.record);
queryKey: ["auth", "user"],
queryFn: fetchUser,
});
const navigate = useNavigate(); const navigate = useNavigate();
const loginMutation = useMutation({ function updateUser() {
mutationFn: async ({ if (pb.authStore.isValid) {
email, setUser(pb.authStore.record);
password, }
}: { setIsLoading(false);
email: string; }
password: string;
}) => {
await pb.collection("users").authWithPassword(email, password);
return fetchUser();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
},
});
const signupMutation = useMutation({ const login = useCallback(async (email: string, password: string) => {
mutationFn: async ({ console.log("login");
email, setIsLoading(true);
password, await pb.collection("users").authWithPassword(email, password);
passwordConfirm, updateUser();
}: { navigate({ to: "/campaigns" });
email: string; }, []);
password: string;
passwordConfirm: string;
}) => {
await pb.collection("users").create({ email, password, passwordConfirm });
await pb.collection("users").authWithPassword(email, password);
return fetchUser();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
},
});
const logoutMutation = useMutation({
mutationFn: async () => {
pb.authStore.clear();
return null;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
},
});
const login = useCallback(
async (email: string, password: string) => {
await loginMutation.mutateAsync({ email, password });
navigate({ to: "/campaigns" });
},
[loginMutation],
);
const signup = useCallback( const signup = useCallback(
async (email: string, password: string, passwordConfirm: string) => { async (email: string, password: string, passwordConfirm: string) => {
await signupMutation.mutateAsync({ email, password, passwordConfirm }); console.log("signup");
setIsLoading(true);
await pb.collection("users").create({ email, password, passwordConfirm });
await pb.collection("users").authWithPassword(email, password);
updateUser();
navigate({ to: "/campaigns" }); navigate({ to: "/campaigns" });
}, },
[signupMutation], [],
); );
const logout = useCallback(async () => { const logout = useCallback(async () => {
await logoutMutation.mutateAsync(); console.log("logout");
pb.authStore.clear();
setUser(null);
navigate({ to: "/" }); navigate({ to: "/" });
}, [logoutMutation]); }, []);
return ( return (
<AuthContext.Provider <AuthContext.Provider

View File

@@ -1,7 +1,6 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
@@ -9,16 +8,13 @@ import { routeTree } from "./routeTree.gen";
import "./styles.css"; import "./styles.css";
import reportWebVitals from "./reportWebVitals.ts"; import reportWebVitals from "./reportWebVitals.ts";
const queryClient = new QueryClient();
// Create a new router instance // Create a new router instance
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: { queryClient },
defaultPreload: "intent", defaultPreload: "intent",
scrollRestoration: true, scrollRestoration: true,
defaultStructuralSharing: true, defaultStructuralSharing: true,
defaultPreloadStaleTime: 0, defaultPendingMinMs: 0,
}); });
// Register the router instance for type safety // Register the router instance for type safety
@@ -34,9 +30,7 @@ if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <RouterProvider router={router} />
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>, </StrictMode>,
); );
} }

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

View File

@@ -1,6 +1,5 @@
import { AuthProvider } from "@/context/auth/AuthContext"; import { AuthProvider } from "@/context/auth/AuthContext";
import { DocumentProvider } from "@/context/document/DocumentContext"; import { DocumentProvider } from "@/context/document/DocumentContext";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { Outlet, createRootRoute } from "@tanstack/react-router"; import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
@@ -13,7 +12,6 @@ export const Route = createRootRoute({
</DocumentProvider> </DocumentProvider>
</AuthProvider> </AuthProvider>
<TanStackRouterDevtools /> <TanStackRouterDevtools />
<ReactQueryDevtools buttonPosition="bottom-right" />
</> </>
), ),
}); });

View File

@@ -1,11 +1,10 @@
import { useCallback } from "react"; import { useCallback, useEffect, useState } from "react";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import { SessionRow } from "@/components/documents/session/SessionRow"; import { SessionRow } from "@/components/documents/session/SessionRow";
import { Button } from "@headlessui/react"; import { Button } from "@headlessui/react";
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Loader } from "@/components/Loader"; import { Loader } from "@/components/Loader";
import type { Relationship } from "@/lib/types"; import type { Campaign, Relationship, Session } from "@/lib/types";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_app/_authenticated/campaigns/$campaignId", "/_app/_authenticated/campaigns/$campaignId",
@@ -15,14 +14,15 @@ export const Route = createFileRoute(
}); });
function RouteComponent() { function RouteComponent() {
const queryClient = useQueryClient();
const params = Route.useParams(); const params = Route.useParams();
const { const [loading, setLoading] = useState(true);
data: { campaign, sessions }, const [campaign, setCampaign] = useState<Campaign | null>(null);
} = useSuspenseQuery({ const [sessions, setSessions] = useState<Session[]>([]);
queryKey: ["campaign"],
queryFn: async () => { useEffect(() => {
async function fetchData() {
setLoading(true);
const campaign = await pb const campaign = await pb
.collection("campaigns") .collection("campaigns")
.getOne(params.campaignId); .getOne(params.campaignId);
@@ -31,14 +31,17 @@ function RouteComponent() {
filter: `campaign = "${params.campaignId}" && type = 'session'`, filter: `campaign = "${params.campaignId}" && type = 'session'`,
sort: "-created", sort: "-created",
}); });
return { setSessions(sessions as Session[]);
campaign, setCampaign(campaign as Campaign);
sessions, setLoading(false);
}; }
}, fetchData();
}); }, [setCampaign, setSessions, setLoading]);
const createNewSession = useCallback(async () => { const createNewSession = useCallback(async () => {
if (campaign === null) {
return;
}
// Check for a previous session // Check for a previous session
const prevSession = await pb const prevSession = await pb
.collection("documents") .collection("documents")
@@ -70,10 +73,12 @@ function RouteComponent() {
}); });
} }
} }
queryClient.invalidateQueries({ queryKey: ["campaign"] });
}, [campaign]); }, [campaign]);
if (loading || campaign === null) {
return <Loader />;
}
return ( return (
<div className="max-w-xl mx-auto py-8"> <div className="max-w-xl mx-auto py-8">
<div className="mb-2"> <div className="mb-2">

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,72 +0,0 @@
import { DocumentPrintRow } from "@/components/documents/DocumentPrintRow";
import { SessionPrintRow } from "@/components/documents/session/SessionPrintRow";
import { Loader } from "@/components/Loader";
import { pb } from "@/lib/pocketbase";
import { RelationshipType, type Relationship, type Session } from "@/lib/types";
import { useSuspenseQuery } from "@tanstack/react-query";
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 {
data: { session, relationships },
} = useSuspenseQuery({
queryKey: ["session", "relationships"],
queryFn: async () => {
const session = await pb
.collection("documents")
.getOne(params.documentId);
const relationships: Relationship[] = await pb
.collection("relationships")
.getFullList({
filter: `primary = "${params.documentId}"`,
expand: "secondary",
});
console.log("Fetched data: ", relationships);
return {
session: session as Session,
relationships: _.mapValues(
_.groupBy(relationships, (r) => r.type),
(rs: Relationship[]) => rs.flatMap((r) => r.expand?.secondary),
),
};
},
});
console.log("Parsed data: ", relationships);
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>
);
}