[nvim] envrc to flake, setup tanstack query for data loading

This commit is contained in:
2025-05-31 22:23:51 -07:00
parent 0ed2066b17
commit 4bed1c6e65
7 changed files with 140 additions and 65 deletions

2
.envrc
View File

@@ -1,4 +1,4 @@
use nix use flake
# Add Node modules only to the end of the path. # Add Node modules only to the end of the path.
# `layout node` will add it to the front, which is not as secure # `layout node` will add it to the front, which is not as secure

44
package-lock.json generated
View File

@@ -8,7 +8,8 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.77.2", "@tanstack/react-query": "^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",
"@tanstack/router-plugin": "^1.114.3", "@tanstack/router-plugin": "^1.114.3",
@@ -1659,9 +1660,19 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.77.2", "version": "5.79.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.77.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz",
"integrity": "sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q==", "integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.76.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.76.0.tgz",
"integrity": "sha512-1p92nqOBPYVqVDU0Ua5nzHenC6EGZNrLnB2OZphYw8CNA1exuvI97FVgIKON7Uug3uQqvH/QY8suUKpQo8qHNQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1669,12 +1680,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.77.2", "version": "5.79.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.77.2.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz",
"integrity": "sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA==", "integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.77.2" "@tanstack/query-core": "5.79.0"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1684,6 +1695,23 @@
"react": "^18 || ^19" "react": "^18 || ^19"
} }
}, },
"node_modules/@tanstack/react-query-devtools": {
"version": "5.79.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.79.0.tgz",
"integrity": "sha512-YVRWxjxsWycWChjKxvaIAPdNC5LX0zpiHoNyTB8teDZpQstM1b7mCuAp3x60cjX1MhLoO3vbaeY29EKst4D4ug==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.76.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.79.0",
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-router": { "node_modules/@tanstack/react-router": {
"version": "1.120.10", "version": "1.120.10",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.120.10.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.120.10.tgz",

View File

@@ -15,7 +15,8 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.77.2", "@tanstack/react-query": "^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",
"@tanstack/router-plugin": "^1.114.3", "@tanstack/router-plugin": "^1.114.3",

View File

@@ -1,42 +1,47 @@
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";
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: {}, context: { queryClient },
defaultPreload: 'intent', defaultPreload: "intent",
scrollRestoration: true, scrollRestoration: true,
defaultStructuralSharing: true, defaultStructuralSharing: true,
defaultPreloadStaleTime: 0, defaultPreloadStaleTime: 0,
}) });
// Register the router instance for type safety // Register the router instance for type safety
declare module '@tanstack/react-router' { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router router: typeof router;
} }
} }
// Render the app // Render the app
const rootElement = document.getElementById('app') const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) { 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>,
) );
} }
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals() reportWebVitals();

View File

@@ -1,9 +1,7 @@
import { Link, Outlet, createRootRoute } from "@tanstack/react-router"; import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { AuthProvider, useAuth } from "@/context/auth/AuthContext"; import { AuthProvider, useAuth } from "@/context/auth/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
/** /**
* Root header with navigation and user authentication controls. * Root header with navigation and user authentication controls.
@@ -69,12 +67,13 @@ function RootHeader() {
export const Route = createRootRoute({ export const Route = createRootRoute({
component: () => ( component: () => (
<QueryClientProvider client={queryClient}> <>
<AuthProvider> <AuthProvider>
<RootHeader /> <RootHeader />
<Outlet /> <Outlet />
<TanStackRouterDevtools />
</AuthProvider> </AuthProvider>
</QueryClientProvider> <TanStackRouterDevtools />
<ReactQueryDevtools buttonPosition="bottom-right" />
</>
), ),
}); });

View File

@@ -1,11 +1,28 @@
import { useCallback } 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 type { Campaign } from "@/lib/types";
import { SessionRow } from "@/components/documents/session/SessionRow"; 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("/_authenticated/campaigns/$campaignId")({ export const Route = createFileRoute("/_authenticated/campaigns/$campaignId")({
loader: async ({ params }) => { component: RouteComponent,
const record = await pb.collection("campaigns").getOne(params.campaignId); 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 // Fetch all documents for this campaign
const docs = await pb.collection("documents").getFullList({ const docs = await pb.collection("documents").getFullList({
filter: `campaign = "${params.campaignId}"`, filter: `campaign = "${params.campaignId}"`,
@@ -13,15 +30,24 @@ export const Route = createFileRoute("/_authenticated/campaigns/$campaignId")({
// Filter to only those with data.session // Filter to only those with data.session
const sessions = docs.filter((doc: any) => doc.data && doc.data.session); const sessions = docs.filter((doc: any) => doc.data && doc.data.session);
return { return {
campaign: { id: record.id, name: record.name } as Campaign, campaign,
sessions, sessions,
}; };
}, },
component: RouteComponent,
}); });
function RouteComponent() { const createNewSession = useCallback(async () => {
const { campaign, sessions } = Route.useLoaderData(); await pb.collection("documents").create({
campaign: campaign.id,
data: {
session: {
strongStart: "",
},
},
});
queryClient.invalidateQueries({ queryKey: ["campaign"] });
}, [campaign]);
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">
@@ -35,7 +61,17 @@ function RouteComponent() {
<h2 className="text-2xl font-bold mb-4 text-slate-100"> <h2 className="text-2xl font-bold mb-4 text-slate-100">
{campaign.name} {campaign.name}
</h2> </h2>
<div className="flex justify-between">
<h3 className="text-lg font-semibold mb-2 text-slate-200">Sessions</h3> <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 ? ( {sessions && sessions.length > 0 ? (
<div> <div>
<ul className="space-y-2"> <ul className="space-y-2">
@@ -51,7 +87,6 @@ function RouteComponent() {
No sessions found for this campaign. No sessions found for this campaign.
</div> </div>
)} )}
{/* More campaign details can go here */}
</div> </div>
); );
} }

View File

@@ -9,7 +9,12 @@ import { useRouter } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated/campaigns/")({ export const Route = createFileRoute("/_authenticated/campaigns/")({
loader: async () => { loader: async () => {
const records = await pb.collection("campaigns").getFullList(); const records = await pb.collection("campaigns").getFullList();
return { campaigns: records.map((rec: any) => ({ id: rec.id, name: rec.name })) as Campaign[] }; return {
campaigns: records.map((rec: any) => ({
id: rec.id,
name: rec.name,
})) as Campaign[],
};
}, },
component: RouteComponent, component: RouteComponent,
pendingComponent: Loader, pendingComponent: Loader,
@@ -23,11 +28,12 @@ function RouteComponent() {
await router.invalidate(); await router.invalidate();
}; };
if (!campaigns || campaigns.length === 0) return <div>No campaigns found.</div>;
return ( return (
<div className="max-w-xl mx-auto py-8"> <div className="max-w-xl mx-auto py-8">
<h2 className="text-xl font-bold mb-4 text-slate-100">Your Campaigns</h2> <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"> <ul className="space-y-2">
{campaigns.map((c) => ( {campaigns.map((c) => (
<li key={c.id}> <li key={c.id}>
@@ -41,6 +47,7 @@ function RouteComponent() {
</li> </li>
))} ))}
</ul> </ul>
)}
<div className="mt-8"> <div className="mt-8">
<CreateCampaignButton onCreated={handleCreated} /> <CreateCampaignButton onCreated={handleCreated} />
</div> </div>