Moves routes inside an _app for the header and builds a print route

This commit is contained in:
2025-06-01 12:34:02 -07:00
parent 9cfdfbaf23
commit 99236a36f8
21 changed files with 532 additions and 211 deletions

View File

@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "mprocs \"npm run start\" \"pocketbase serve\"",
"start": "vite --port 3000",
"start": "VITE_POCKETBASE_URL=http://localhost:8090 vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run",

View File

@@ -0,0 +1,64 @@
// DocumentRow.tsx
// Generic row component for displaying any document type.
import {
isLocation,
isMonster,
isNpc,
isScene,
isSecret,
isSession,
isTreasure,
type Document,
} from "@/lib/types";
import { LocationPrintRow } from "./location/LocationPrintRow";
import { MonsterPrintRow } from "./monsters/MonsterPrintRow";
import { TreasurePrintRow } from "./treasure/TreasurePrintRow";
import { SecretPrintRow } from "./secret/SecretPrintRow";
import { NpcPrintRow } from "./npc/NpcPrintRow";
import { ScenePrintRow } from "./scene/ScenePrintRow";
import { SessionPrintRow } from "./session/SessionPrintRow";
/**
* Renders a row for any document type. Prioritizes Session, then Secret, then falls back to ID and creation time.
* If rendering a SecretRow, uses the provided session prop if available.
*/
export const DocumentPrintRow = ({ document }: { document: Document }) => {
if (isLocation(document)) {
return <LocationPrintRow location={document} />;
}
if (isMonster(document)) {
return <MonsterPrintRow monster={document} />;
}
if (isNpc(document)) {
return <NpcPrintRow npc={document} />;
}
if (isSession(document)) {
return <SessionPrintRow session={document} />;
}
if (isSecret(document)) {
return <SecretPrintRow secret={document} />;
}
if (isScene(document)) {
return <ScenePrintRow scene={document} />;
}
if (isTreasure(document)) {
return <TreasurePrintRow treasure={document} />;
}
// Fallback: show ID and creation time
return (
<div>
<div className="font-semibold text-lg text-slate-300">
Unrecognized Document
</div>
<div className="text-slate-400 text-sm">ID: {document.id}</div>
<div className="text-slate-400 text-sm">Created: {document.created}</div>
</div>
);
};

View File

@@ -0,0 +1,13 @@
import type { Location } from "@/lib/types";
/**
* Renders an print-friendly location row
*/
export const LocationPrintRow = ({ location }: { location: Location }) => {
return (
<li>
<h4>{location.data.location.name}</h4>
<p>{location.data.location.description}</p>
</li>
);
};

View File

@@ -0,0 +1,8 @@
import type { Monster } from "@/lib/types";
/**
* Renders an editable monster row
*/
export const MonsterPrintRow = ({ monster }: { monster: Monster }) => {
return <li>{monster.data.monster.name}</li>;
};

View File

@@ -0,0 +1,13 @@
import type { Npc } from "@/lib/types";
/**
* Renders an editable npc row
*/
export const NpcPrintRow = ({ npc }: { npc: Npc }) => {
return (
<li className="">
<h4>{npc.data.npc.name}</h4>
<p>{npc.data.npc.description}</p>
</li>
);
};

View File

@@ -0,0 +1,8 @@
import type { Scene } from "@/lib/types";
/**
* Renders an editable scene row
*/
export const ScenePrintRow = ({ scene }: { scene: Scene }) => {
return <li className="">{scene.data.scene.text}</li>;
};

View File

@@ -0,0 +1,26 @@
// SecretRow.tsx
// Displays a single secret with discovered checkbox and text.
import type { Secret, Session } from "@/lib/types";
import { pb } from "@/lib/pocketbase";
import { useState } from "react";
/**
* Renders a secret row with a discovered checkbox and secret text.
* Handles updating the discovered state and discoveredIn relationship.
*/
export const SecretPrintRow = ({ secret }: { secret: Secret }) => {
return (
<li className="flex items-center gap-3">
<input
type="checkbox"
className="flex-none accent-emerald-500 w-5 h-5"
aria-label="Discovered"
/>
<span>
{(secret.data as any)?.secret?.text || (
<span className="italic text-slate-400">(No secret text)</span>
)}
</span>
</li>
);
};

View File

@@ -0,0 +1,10 @@
import type { Session } from "@/lib/types";
export const SessionPrintRow = ({ session }: { session: Session }) => {
return (
<div>
<h3 className="text-lg font-bold text-slate-600">StrongStart</h3>
<div className="">{session.data.session.strongStart}</div>
</div>
);
};

View File

@@ -0,0 +1,26 @@
// TreasureRow.tsx
// Displays a single treasure with discovered checkbox and text.
import type { Treasure, Session } from "@/lib/types";
import { pb } from "@/lib/pocketbase";
import { useState } from "react";
/**
* Renders a treasure row with a discovered checkbox and treasure text.
* Handles updating the discovered state and discoveredIn relationship.
*/
export const TreasurePrintRow = ({ treasure }: { treasure: Treasure }) => {
return (
<li className="flex items-center gap-3">
<input
type="checkbox"
className="flex-none accent-emerald-500 w-5 h-5"
aria-label="Discovered"
/>
<span>
{(treasure.data as any)?.treasure?.text || (
<span className="italic text-slate-400">(No treasure text)</span>
)}
</span>
</li>
);
};

View File

@@ -3,4 +3,5 @@
*
* This includes endpoints and other environment-specific settings.
*/
export const POCKETBASE_URL: string = import.meta.env.VITE_POCKETBASE_URL || "http://127.0.0.1:8090"; // Update as needed for deployment
export const POCKETBASE_URL: string =
import.meta.env.VITE_POCKETBASE_URL || "/"; // Update as needed for deployment

View File

@@ -11,208 +11,258 @@
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as LoginImport } from './routes/login'
import { Route as AboutImport } from './routes/about'
import { Route as AuthenticatedImport } from './routes/_authenticated'
import { Route as IndexImport } from './routes/index'
import { Route as AuthenticatedCampaignsIndexImport } from './routes/_authenticated/campaigns.index'
import { Route as AuthenticatedDocumentDocumentIdImport } from './routes/_authenticated/document.$documentId'
import { Route as AuthenticatedCampaignsCampaignIdImport } from './routes/_authenticated/campaigns.$campaignId'
import { Route as AppImport } from './routes/_app'
import { Route as AppIndexImport } from './routes/_app/index'
import { Route as AppLoginImport } from './routes/_app/login'
import { Route as AppAboutImport } from './routes/_app/about'
import { Route as AppAuthenticatedImport } from './routes/_app/_authenticated'
import { Route as AppAuthenticatedCampaignsIndexImport } from './routes/_app/_authenticated/campaigns.index'
import { Route as AppAuthenticatedDocumentDocumentIdImport } from './routes/_app/_authenticated/document.$documentId'
import { Route as AppAuthenticatedCampaignsCampaignIdImport } from './routes/_app/_authenticated/campaigns.$campaignId'
import { Route as AppauthenticatedDocumentDocumentIdPrintImport } from './routes/_app_._authenticated.document_.$documentId.print'
// Create/Update Routes
const LoginRoute = LoginImport.update({
id: '/login',
path: '/login',
const AppRoute = AppImport.update({
id: '/_app',
getParentRoute: () => rootRoute,
} as any)
const AboutRoute = AboutImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const AuthenticatedRoute = AuthenticatedImport.update({
id: '/_authenticated',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
const AppIndexRoute = AppIndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
getParentRoute: () => AppRoute,
} as any)
const AuthenticatedCampaignsIndexRoute =
AuthenticatedCampaignsIndexImport.update({
const AppLoginRoute = AppLoginImport.update({
id: '/login',
path: '/login',
getParentRoute: () => AppRoute,
} as any)
const AppAboutRoute = AppAboutImport.update({
id: '/about',
path: '/about',
getParentRoute: () => AppRoute,
} as any)
const AppAuthenticatedRoute = AppAuthenticatedImport.update({
id: '/_authenticated',
getParentRoute: () => AppRoute,
} as any)
const AppAuthenticatedCampaignsIndexRoute =
AppAuthenticatedCampaignsIndexImport.update({
id: '/campaigns/',
path: '/campaigns/',
getParentRoute: () => AuthenticatedRoute,
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AuthenticatedDocumentDocumentIdRoute =
AuthenticatedDocumentDocumentIdImport.update({
const AppAuthenticatedDocumentDocumentIdRoute =
AppAuthenticatedDocumentDocumentIdImport.update({
id: '/document/$documentId',
path: '/document/$documentId',
getParentRoute: () => AuthenticatedRoute,
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AuthenticatedCampaignsCampaignIdRoute =
AuthenticatedCampaignsCampaignIdImport.update({
const AppAuthenticatedCampaignsCampaignIdRoute =
AppAuthenticatedCampaignsCampaignIdImport.update({
id: '/campaigns/$campaignId',
path: '/campaigns/$campaignId',
getParentRoute: () => AuthenticatedRoute,
getParentRoute: () => AppAuthenticatedRoute,
} as any)
const AppauthenticatedDocumentDocumentIdPrintRoute =
AppauthenticatedDocumentDocumentIdPrintImport.update({
id: '/_app_/_authenticated/document_/$documentId/print',
path: '/document/$documentId/print',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/_authenticated': {
id: '/_authenticated'
'/_app': {
id: '/_app'
path: ''
fullPath: ''
preLoaderRoute: typeof AuthenticatedImport
preLoaderRoute: typeof AppImport
parentRoute: typeof rootRoute
}
'/about': {
id: '/about'
'/_app/_authenticated': {
id: '/_app/_authenticated'
path: ''
fullPath: ''
preLoaderRoute: typeof AppAuthenticatedImport
parentRoute: typeof AppImport
}
'/_app/about': {
id: '/_app/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutImport
parentRoute: typeof rootRoute
preLoaderRoute: typeof AppAboutImport
parentRoute: typeof AppImport
}
'/login': {
id: '/login'
'/_app/login': {
id: '/_app/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginImport
parentRoute: typeof rootRoute
preLoaderRoute: typeof AppLoginImport
parentRoute: typeof AppImport
}
'/_authenticated/campaigns/$campaignId': {
id: '/_authenticated/campaigns/$campaignId'
'/_app/': {
id: '/_app/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AppIndexImport
parentRoute: typeof AppImport
}
'/_app/_authenticated/campaigns/$campaignId': {
id: '/_app/_authenticated/campaigns/$campaignId'
path: '/campaigns/$campaignId'
fullPath: '/campaigns/$campaignId'
preLoaderRoute: typeof AuthenticatedCampaignsCampaignIdImport
parentRoute: typeof AuthenticatedImport
preLoaderRoute: typeof AppAuthenticatedCampaignsCampaignIdImport
parentRoute: typeof AppAuthenticatedImport
}
'/_authenticated/document/$documentId': {
id: '/_authenticated/document/$documentId'
'/_app/_authenticated/document/$documentId': {
id: '/_app/_authenticated/document/$documentId'
path: '/document/$documentId'
fullPath: '/document/$documentId'
preLoaderRoute: typeof AuthenticatedDocumentDocumentIdImport
parentRoute: typeof AuthenticatedImport
preLoaderRoute: typeof AppAuthenticatedDocumentDocumentIdImport
parentRoute: typeof AppAuthenticatedImport
}
'/_authenticated/campaigns/': {
id: '/_authenticated/campaigns/'
'/_app/_authenticated/campaigns/': {
id: '/_app/_authenticated/campaigns/'
path: '/campaigns'
fullPath: '/campaigns'
preLoaderRoute: typeof AuthenticatedCampaignsIndexImport
parentRoute: typeof AuthenticatedImport
preLoaderRoute: typeof AppAuthenticatedCampaignsIndexImport
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
interface AuthenticatedRouteChildren {
AuthenticatedCampaignsCampaignIdRoute: typeof AuthenticatedCampaignsCampaignIdRoute
AuthenticatedDocumentDocumentIdRoute: typeof AuthenticatedDocumentDocumentIdRoute
AuthenticatedCampaignsIndexRoute: typeof AuthenticatedCampaignsIndexRoute
interface AppAuthenticatedRouteChildren {
AppAuthenticatedCampaignsCampaignIdRoute: typeof AppAuthenticatedCampaignsCampaignIdRoute
AppAuthenticatedDocumentDocumentIdRoute: typeof AppAuthenticatedDocumentDocumentIdRoute
AppAuthenticatedCampaignsIndexRoute: typeof AppAuthenticatedCampaignsIndexRoute
}
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedCampaignsCampaignIdRoute: AuthenticatedCampaignsCampaignIdRoute,
AuthenticatedDocumentDocumentIdRoute: AuthenticatedDocumentDocumentIdRoute,
AuthenticatedCampaignsIndexRoute: AuthenticatedCampaignsIndexRoute,
const AppAuthenticatedRouteChildren: AppAuthenticatedRouteChildren = {
AppAuthenticatedCampaignsCampaignIdRoute:
AppAuthenticatedCampaignsCampaignIdRoute,
AppAuthenticatedDocumentDocumentIdRoute:
AppAuthenticatedDocumentDocumentIdRoute,
AppAuthenticatedCampaignsIndexRoute: AppAuthenticatedCampaignsIndexRoute,
}
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
AuthenticatedRouteChildren,
)
const AppAuthenticatedRouteWithChildren =
AppAuthenticatedRoute._addFileChildren(AppAuthenticatedRouteChildren)
interface AppRouteChildren {
AppAuthenticatedRoute: typeof AppAuthenticatedRouteWithChildren
AppAboutRoute: typeof AppAboutRoute
AppLoginRoute: typeof AppLoginRoute
AppIndexRoute: typeof AppIndexRoute
}
const AppRouteChildren: AppRouteChildren = {
AppAuthenticatedRoute: AppAuthenticatedRouteWithChildren,
AppAboutRoute: AppAboutRoute,
AppLoginRoute: AppLoginRoute,
AppIndexRoute: AppIndexRoute,
}
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'': typeof AuthenticatedRouteWithChildren
'/about': typeof AboutRoute
'/login': typeof LoginRoute
'/campaigns/$campaignId': typeof AuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AuthenticatedDocumentDocumentIdRoute
'/campaigns': typeof AuthenticatedCampaignsIndexRoute
'': typeof AppAuthenticatedRouteWithChildren
'/about': typeof AppAboutRoute
'/login': typeof AppLoginRoute
'/': typeof AppIndexRoute
'/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRoute
'/campaigns': typeof AppAuthenticatedCampaignsIndexRoute
'/document/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'': typeof AuthenticatedRouteWithChildren
'/about': typeof AboutRoute
'/login': typeof LoginRoute
'/campaigns/$campaignId': typeof AuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AuthenticatedDocumentDocumentIdRoute
'/campaigns': typeof AuthenticatedCampaignsIndexRoute
'': typeof AppAuthenticatedRouteWithChildren
'/about': typeof AppAboutRoute
'/login': typeof AppLoginRoute
'/': typeof AppIndexRoute
'/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRoute
'/campaigns': typeof AppAuthenticatedCampaignsIndexRoute
'/document/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/_authenticated': typeof AuthenticatedRouteWithChildren
'/about': typeof AboutRoute
'/login': typeof LoginRoute
'/_authenticated/campaigns/$campaignId': typeof AuthenticatedCampaignsCampaignIdRoute
'/_authenticated/document/$documentId': typeof AuthenticatedDocumentDocumentIdRoute
'/_authenticated/campaigns/': typeof AuthenticatedCampaignsIndexRoute
'/_app': typeof AppRouteWithChildren
'/_app/_authenticated': typeof AppAuthenticatedRouteWithChildren
'/_app/about': typeof AppAboutRoute
'/_app/login': typeof AppLoginRoute
'/_app/': typeof AppIndexRoute
'/_app/_authenticated/campaigns/$campaignId': typeof AppAuthenticatedCampaignsCampaignIdRoute
'/_app/_authenticated/document/$documentId': typeof AppAuthenticatedDocumentDocumentIdRoute
'/_app/_authenticated/campaigns/': typeof AppAuthenticatedCampaignsIndexRoute
'/_app_/_authenticated/document_/$documentId/print': typeof AppauthenticatedDocumentDocumentIdPrintRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| ''
| '/about'
| '/login'
| '/'
| '/campaigns/$campaignId'
| '/document/$documentId'
| '/campaigns'
| '/document/$documentId/print'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| ''
| '/about'
| '/login'
| '/'
| '/campaigns/$campaignId'
| '/document/$documentId'
| '/campaigns'
| '/document/$documentId/print'
id:
| '__root__'
| '/'
| '/_authenticated'
| '/about'
| '/login'
| '/_authenticated/campaigns/$campaignId'
| '/_authenticated/document/$documentId'
| '/_authenticated/campaigns/'
| '/_app'
| '/_app/_authenticated'
| '/_app/about'
| '/_app/login'
| '/_app/'
| '/_app/_authenticated/campaigns/$campaignId'
| '/_app/_authenticated/document/$documentId'
| '/_app/_authenticated/campaigns/'
| '/_app_/_authenticated/document_/$documentId/print'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
AboutRoute: typeof AboutRoute
LoginRoute: typeof LoginRoute
AppRoute: typeof AppRouteWithChildren
AppauthenticatedDocumentDocumentIdPrintRoute: typeof AppauthenticatedDocumentDocumentIdPrintRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthenticatedRoute: AuthenticatedRouteWithChildren,
AboutRoute: AboutRoute,
LoginRoute: LoginRoute,
AppRoute: AppRouteWithChildren,
AppauthenticatedDocumentDocumentIdPrintRoute:
AppauthenticatedDocumentDocumentIdPrintRoute,
}
export const routeTree = rootRoute
@@ -225,40 +275,54 @@ export const routeTree = rootRoute
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/_authenticated",
"/about",
"/login"
"/_app",
"/_app_/_authenticated/document_/$documentId/print"
]
},
"/": {
"filePath": "index.tsx"
},
"/_authenticated": {
"filePath": "_authenticated.tsx",
"/_app": {
"filePath": "_app.tsx",
"children": [
"/_authenticated/campaigns/$campaignId",
"/_authenticated/document/$documentId",
"/_authenticated/campaigns/"
"/_app/_authenticated",
"/_app/about",
"/_app/login",
"/_app/"
]
},
"/about": {
"filePath": "about.tsx"
"/_app/_authenticated": {
"filePath": "_app/_authenticated.tsx",
"parent": "/_app",
"children": [
"/_app/_authenticated/campaigns/$campaignId",
"/_app/_authenticated/document/$documentId",
"/_app/_authenticated/campaigns/"
]
},
"/login": {
"filePath": "login.tsx"
"/_app/about": {
"filePath": "_app/about.tsx",
"parent": "/_app"
},
"/_authenticated/campaigns/$campaignId": {
"filePath": "_authenticated/campaigns.$campaignId.tsx",
"parent": "/_authenticated"
"/_app/login": {
"filePath": "_app/login.tsx",
"parent": "/_app"
},
"/_authenticated/document/$documentId": {
"filePath": "_authenticated/document.$documentId.tsx",
"parent": "/_authenticated"
"/_app/": {
"filePath": "_app/index.tsx",
"parent": "/_app"
},
"/_authenticated/campaigns/": {
"filePath": "_authenticated/campaigns.index.tsx",
"parent": "/_authenticated"
"/_app/_authenticated/campaigns/$campaignId": {
"filePath": "_app/_authenticated/campaigns.$campaignId.tsx",
"parent": "/_app/_authenticated"
},
"/_app/_authenticated/document/$documentId": {
"filePath": "_app/_authenticated/document.$documentId.tsx",
"parent": "/_app/_authenticated"
},
"/_app/_authenticated/campaigns/": {
"filePath": "_app/_authenticated/campaigns.index.tsx",
"parent": "/_app/_authenticated"
},
"/_app_/_authenticated/document_/$documentId/print": {
"filePath": "_app_._authenticated.document_.$documentId.print.tsx"
}
}
}

View File

@@ -1,75 +1,12 @@
import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { AuthProvider, useAuth } from "@/context/auth/AuthContext";
import { AuthProvider } from "@/context/auth/AuthContext";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
/**
* Root header with navigation and user authentication controls.
*/
function RootHeader() {
const { user, logout, isLoading } = useAuth();
return (
<header className="flex items-center justify-between px-8 py-4 border-b border-slate-700 bg-slate-900">
<h1 className="text-2xl font-bold text-slate-100 m-0">
DM's Table Companion
</h1>
<nav aria-label="Main navigation" className="flex gap-6">
<Link
to="/campaigns"
className="no-underline text-slate-200 hover:text-violet-400 transition-colors font-medium border-b-2 border-transparent pb-1"
activeProps={{
className:
"no-underline text-violet-400 border-violet-400 border-b-2 pb-1",
}}
>
Campaigns
</Link>
<Link
to="/about"
className="no-underline text-slate-200 hover:text-violet-400 transition-colors font-medium border-b-2 border-transparent pb-1"
activeProps={{
className:
"no-underline text-violet-400 border-violet-400 border-b-2 pb-1",
}}
>
About
</Link>
</nav>
<div className="flex items-center gap-4">
{user ? (
<>
<span className="text-slate-200 text-sm" aria-label="User email">
{user.email}
</span>
<button
onClick={logout}
disabled={isLoading}
className="bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-3 py-1 rounded transition-colors disabled:opacity-60"
aria-label="Log out"
type="button"
>
Log out
</button>
</>
) : (
<Link
to="/login"
className="bg-violet-600 hover:bg-violet-700 text-white text-sm font-semibold px-3 py-1 rounded transition-colors"
aria-label="Log in"
>
Log in
</Link>
)}
</div>
</header>
);
}
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
export const Route = createRootRoute({
component: () => (
<>
<AuthProvider>
<RootHeader />
<Outlet />
</AuthProvider>
<TanStackRouterDevtools />

77
src/routes/_app.tsx Normal file
View File

@@ -0,0 +1,77 @@
import { useAuth } from "@/context/auth/AuthContext";
import { Link, Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_app")({
component: RouteComponent,
});
/**
* Root header with navigation and user authentication controls.
*/
function AppHeader() {
const { user, logout, isLoading } = useAuth();
return (
<header className="flex items-center justify-between px-8 py-4 border-b border-slate-700 bg-slate-900">
<h1 className="text-2xl font-bold text-slate-100 m-0">
DM's Table Companion
</h1>
<nav aria-label="Main navigation" className="flex gap-6">
<Link
to="/campaigns"
className="no-underline text-slate-200 hover:text-violet-400 transition-colors font-medium border-b-2 border-transparent pb-1"
activeProps={{
className:
"no-underline text-violet-400 border-violet-400 border-b-2 pb-1",
}}
>
Campaigns
</Link>
<Link
to="/about"
className="no-underline text-slate-200 hover:text-violet-400 transition-colors font-medium border-b-2 border-transparent pb-1"
activeProps={{
className:
"no-underline text-violet-400 border-violet-400 border-b-2 pb-1",
}}
>
About
</Link>
</nav>
<div className="flex items-center gap-4">
{user ? (
<>
<span className="text-slate-200 text-sm" aria-label="User email">
{user.email}
</span>
<button
onClick={logout}
disabled={isLoading}
className="bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-3 py-1 rounded transition-colors disabled:opacity-60"
aria-label="Log out"
type="button"
>
Log out
</button>
</>
) : (
<Link
to="/login"
className="bg-violet-600 hover:bg-violet-700 text-white text-sm font-semibold px-3 py-1 rounded transition-colors"
aria-label="Log in"
>
Log in
</Link>
)}
</div>
</header>
);
}
function RouteComponent() {
return (
<>
<AppHeader />
<Outlet />
</>
);
}

View File

@@ -1,7 +1,7 @@
import { isAuthenticated } from "@/lib/pocketbase";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated")({
export const Route = createFileRoute("/_app/_authenticated")({
beforeLoad: () => {
if (!isAuthenticated()) {
throw redirect({

View File

@@ -6,7 +6,7 @@ import { Button } from "@headlessui/react";
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Loader } from "@/components/Loader";
export const Route = createFileRoute("/_authenticated/campaigns/$campaignId")({
export const Route = createFileRoute("/_app/_authenticated/campaigns/$campaignId")({
component: RouteComponent,
pendingComponent: Loader,
});

View File

@@ -6,7 +6,7 @@ import { Loader } from "@/components/Loader";
import { CreateCampaignButton } from "@/components/CreateCampaignButton";
import { useRouter } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated/campaigns/")({
export const Route = createFileRoute("/_app/_authenticated/campaigns/")({
loader: async () => {
const records = await pb.collection("campaigns").getFullList();
return {

View File

@@ -10,7 +10,7 @@ import {
import { RelationshipList } from "@/components/RelationshipList";
import { SessionForm } from "@/components/documents/session/SessionForm";
export const Route = createFileRoute("/_authenticated/document/$documentId")({
export const Route = createFileRoute("/_app/_authenticated/document/$documentId")({
loader: async ({ params }) => {
const doc = await pb.collection("documents").getOne(params.documentId);
const relationships: Relationship[] = await pb

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
export const Route = createFileRoute('/_app/about')({
component: RouteComponent,
})

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
export const Route = createFileRoute("/_app/")({
component: App,
});

View File

@@ -8,7 +8,7 @@ import { useState } from "react";
* Login and signup page for authentication.
* Allows users to log in or create a new account.
*/
export const Route = createFileRoute("/login")({
export const Route = createFileRoute("/_app/login")({
beforeLoad: () => {
if (isAuthenticated()) {
throw redirect({

View File

@@ -0,0 +1,74 @@
import { DocumentPrintRow } from "@/components/documents/DocumentPrintRow";
import { SessionPrintRow } from "@/components/documents/session/SessionPrintRow";
import { Loader } from "@/components/Loader";
import { RelationshipList } from "@/components/RelationshipList";
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">
<SessionPrintRow session={session}></SessionPrintRow>
{[
RelationshipType.Scenes,
RelationshipType.Secrets,
RelationshipType.Locations,
RelationshipType.Npcs,
RelationshipType.Monsters,
RelationshipType.Treasures,
].map((relationshipType) => (
<div className="break-before-column">
<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>
);
}