Wire Up the Project UI
Let’s build the frontend so users can create projects, navigate to them, and manage tasks within each project.
This requires routing. The project list is one page, and each project’s kanban board is another. We will use TanStack Router, the same library we used in earlier chapters.
Install TanStack Router
pnpm add @tanstack/react-router
pnpm add -D @tanstack/router-plugin
Add the router plugin to vite.config.ts:
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite"; // 👀
// https://vite.dev/config/
export default defineConfig({
base: "/project-planner/",
plugins: [tanstackRouter(), react(), tailwindcss()], // 👀
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
Create the Root Layout
Create src/routes/__root.tsx:
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootLayout,
});
function RootLayout() {
return (
<div className="min-h-screen bg-background">
<header className="border-b">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-6 sm:px-6 lg:px-8">
<Link to="/" className="text-3xl font-bold tracking-tight">
Project Planner
</Link>
</div>
</header>
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<Outlet />
</main>
</div>
);
}
The header is now a <Link> to the home page, so users can click “Project Planner” to get back to the project list from any page.
The <Outlet /> is where the child routes render. When we navigate to /, the project list page will render here. When we navigate to /projects/123, the project detail page will render here.
Create the Project List Page
Create src/routes/index.tsx:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: ProjectListPage,
});
function ProjectListPage() {
return <div>Project List</div>;
}
This page is a placeholder for now.
Create the Project Detail Page
Create src/routes/projects/$projectId.tsx:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/projects/$projectId")({
component: ProjectDetailPage,
});
function ProjectDetailPage() {
const { projectId } = Route.useParams();
return <div>Project Detail: {projectId}</div>;
}
The $projectId in the file path creates a dynamic route parameter. We extract it with Route.useParams() and display it on the page for now. Later, we will use it to fetch and display the project’s data.
Update main.tsx
Replace src/main.tsx to use the router instead of rendering App directly:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { createRouter, RouterProvider } from "@tanstack/react-router"; // 👀
import { routeTree } from "./routeTree.gen"; // 👀
import "./index.css";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const router = createRouter({ routeTree, basepath: "/project-planner" }); // 👀
// Tell TanStack Router about our router instance for type safety in route components
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ConvexProvider client={convex}>
<RouterProvider router={router} /> {/* 👀 This replaces <App /> */}
</ConvexProvider>
</StrictMode>,
);
The routeTree is auto-generated by the TanStack Router Vite plugin from the files in src/routes/. The basepath matches the base in vite.config.ts so that route paths resolve under the /project-planner/ subpath when the app is deployed to GitHub Pages.
Delete App.tsx
The old src/App.tsx is no longer needed. The root layout in src/routes/__root.tsx replaces it. Delete it:
rm src/App.tsx
Checkpoint: Commit your progress.
git add .
git commit -m "planner-04: Wire up project UI with TanStack Router"
git push