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 bef5e98480
21 changed files with 527 additions and 211 deletions

View File

@@ -0,0 +1,92 @@
import { useCallback } from "react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { pb } from "@/lib/pocketbase";
import { SessionRow } from "@/components/documents/session/SessionRow";
import { Button } from "@headlessui/react";
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Loader } from "@/components/Loader";
export const Route = createFileRoute("/_app/_authenticated/campaigns/$campaignId")({
component: RouteComponent,
pendingComponent: Loader,
});
function RouteComponent() {
const queryClient = useQueryClient();
const params = Route.useParams();
const {
data: { campaign, sessions },
} = useSuspenseQuery({
queryKey: ["campaign"],
queryFn: async () => {
const campaign = await pb
.collection("campaigns")
.getOne(params.campaignId);
// Fetch all documents for this campaign
const docs = await pb.collection("documents").getFullList({
filter: `campaign = "${params.campaignId}"`,
});
// Filter to only those with data.session
const sessions = docs.filter((doc: any) => doc.data && doc.data.session);
return {
campaign,
sessions,
};
},
});
const createNewSession = useCallback(async () => {
await pb.collection("documents").create({
campaign: campaign.id,
data: {
session: {
strongStart: "",
},
},
});
queryClient.invalidateQueries({ queryKey: ["campaign"] });
}, [campaign]);
return (
<div className="max-w-xl mx-auto py-8">
<div className="mb-2">
<Link
to="/campaigns"
className="text-slate-400 hover:text-violet-400 text-sm underline underline-offset-2 transition-colors"
>
Back to campaigns
</Link>
</div>
<h2 className="text-2xl font-bold mb-4 text-slate-100">
{campaign.name}
</h2>
<div className="flex justify-between">
<h3 className="text-lg font-semibold mb-2 text-slate-200">Sessions</h3>
<div>
<Button
onClick={() => createNewSession()}
className="inline-flex items-center justify-center rounded bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-violet-400"
>
New Session
</Button>
</div>
</div>
{sessions && sessions.length > 0 ? (
<div>
<ul className="space-y-2">
{sessions.map((s: any) => (
<li key={s.id}>
<SessionRow session={s} />
</li>
))}
</ul>
</div>
) : (
<div className="text-slate-400">
No sessions found for this campaign.
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,56 @@
import { createFileRoute } from "@tanstack/react-router";
import { pb } from "@/lib/pocketbase";
import type { Campaign } from "@/lib/types";
import { Link } from "@tanstack/react-router";
import { Loader } from "@/components/Loader";
import { CreateCampaignButton } from "@/components/CreateCampaignButton";
import { useRouter } from "@tanstack/react-router";
export const Route = createFileRoute("/_app/_authenticated/campaigns/")({
loader: async () => {
const records = await pb.collection("campaigns").getFullList();
return {
campaigns: records.map((rec: any) => ({
id: rec.id,
name: rec.name,
})) as Campaign[],
};
},
component: RouteComponent,
pendingComponent: Loader,
});
function RouteComponent() {
const { campaigns } = Route.useLoaderData();
const router = useRouter();
const handleCreated = async () => {
await router.invalidate();
};
return (
<div className="max-w-xl mx-auto py-8">
<h2 className="text-xl font-bold mb-4 text-slate-100">Your Campaigns</h2>
{!campaigns || campaigns.length === 0 ? (
<div>No campaigns found.</div>
) : (
<ul className="space-y-2">
{campaigns.map((c) => (
<li key={c.id}>
<Link
to="/campaigns/$campaignId"
params={{ campaignId: c.id }}
className="block px-4 py-2 rounded bg-slate-800 hover:bg-violet-700 text-slate-100 transition-colors"
>
{c.name}
</Link>
</li>
))}
</ul>
)}
<div className="mt-8">
<CreateCampaignButton onCreated={handleCreated} />
</div>
</div>
);
}

View File

@@ -0,0 +1,68 @@
import _ from "lodash";
import { createFileRoute } from "@tanstack/react-router";
import { pb } from "@/lib/pocketbase";
import {
RelationshipType,
type Relationship,
type Session,
type Document,
} from "@/lib/types";
import { RelationshipList } from "@/components/RelationshipList";
import { SessionForm } from "@/components/documents/session/SessionForm";
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
.collection("relationships")
.getFullList({
filter: `primary = "${params.documentId}"`,
expand: "secondary",
});
console.log("Fetched data: ", relationships);
return {
document: doc,
relationships: _.mapValues(
_.groupBy(relationships, (r) => r.type),
(rs: Relationship[]) => rs.flatMap((r) => r.expand?.secondary),
),
};
},
component: RouteComponent,
});
function RouteComponent() {
const { document: session, relationships } = Route.useLoaderData() as {
document: Session;
relationships: Record<RelationshipType, Document[]>;
};
async function handleSaveSession(data: Session["data"]) {
await pb.collection("documents").update(session.id, {
data,
});
}
console.log("Parsed data: ", relationships);
return (
<div className="max-w-xl mx-auto py-8">
<SessionForm session={session as Session} onSubmit={handleSaveSession} />
{[
RelationshipType.Scenes,
RelationshipType.Secrets,
RelationshipType.Locations,
RelationshipType.Npcs,
RelationshipType.Monsters,
RelationshipType.Treasures,
].map((relationshipType) => (
<RelationshipList
key={relationshipType}
root={session}
relationshipType={relationshipType}
items={relationships[relationshipType] ?? []}
/>
))}
</div>
);
}