Compare commits
4 Commits
8f96062058
...
3390ecfb95
| Author | SHA1 | Date | |
|---|---|---|---|
| 3390ecfb95 | |||
| b30999e907 | |||
| 2e9ea14507 | |||
| 762306023b |
153
README.md
153
README.md
@@ -1,6 +1,8 @@
|
|||||||
Welcome to your new TanStack app!
|
# DM Companion App
|
||||||
|
|
||||||
# Getting Started
|
## Development
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
To run this application:
|
To run this application:
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ 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
30
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
24
src/components/documents/DocumentTitle.tsx
Normal file
24
src/components/documents/DocumentTitle.tsx
Normal 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>;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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,16 +26,17 @@ 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
|
<Link
|
||||||
to={CampaignRoute.to}
|
to={CampaignRoute.to}
|
||||||
params={{ campaignId: doc.campaign }}
|
params={{ campaignId: doc.campaign }}
|
||||||
@@ -42,14 +51,25 @@ export function DocumentView({ documentId }: { documentId: DocumentId }) {
|
|||||||
>
|
>
|
||||||
Print
|
Print
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
<DocumentEditForm document={doc} />
|
</Link>,
|
||||||
<nav>
|
...relationshipList.map((relationshipType) => (
|
||||||
<ul className="flex flex-row gap-1">
|
|
||||||
{relationshipList.map((relationshipType) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={relationshipType}
|
key={relationshipType}
|
||||||
to={RelationshipRoute.to}
|
to="/document/$documentId/$relationshipType"
|
||||||
params={{
|
params={{
|
||||||
documentId,
|
documentId,
|
||||||
relationshipType,
|
relationshipType,
|
||||||
@@ -60,9 +80,18 @@ export function DocumentView({ documentId }: { documentId: DocumentId }) {
|
|||||||
{relationshipCounts[relationshipType] ?? 0})
|
{relationshipCounts[relationshipType] ?? 0})
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
)),
|
||||||
</ul>
|
]}
|
||||||
</nav>
|
content={
|
||||||
</div>
|
relationshipType === null ? (
|
||||||
|
<DocumentEditForm document={doc} />
|
||||||
|
) : (
|
||||||
|
<RelatedDocumentList
|
||||||
|
documentId={doc.id}
|
||||||
|
relationshipType={relationshipType}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/components/layout/TabbedLayout.tsx
Normal file
16
src/components/layout/TabbedLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}) => {
|
const login = useCallback(async (email: string, password: string) => {
|
||||||
|
console.log("login");
|
||||||
|
setIsLoading(true);
|
||||||
await pb.collection("users").authWithPassword(email, password);
|
await pb.collection("users").authWithPassword(email, password);
|
||||||
return fetchUser();
|
updateUser();
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "user"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const signupMutation = useMutation({
|
|
||||||
mutationFn: async ({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
passwordConfirm,
|
|
||||||
}: {
|
|
||||||
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" });
|
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
|
||||||
|
|||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
49
src/routes/_app/_authenticated/document.$documentId.$.tsx
Normal file
49
src/routes/_app/_authenticated/document.$documentId.$.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user