Initial commit.

This commit is contained in:
Drew Haven 2025-05-27 16:29:14 -07:00
commit 4f44d5edca
29 changed files with 5230 additions and 0 deletions

12
.cta.json Normal file
View File

@ -0,0 +1,12 @@
{
"projectName": ".",
"mode": "file-router",
"typescript": true,
"tailwind": true,
"packageManager": "npm",
"git": true,
"version": 1,
"framework": "react-cra",
"chosenAddOns": []
}

5
.envrc Normal file
View File

@ -0,0 +1,5 @@
use nix
# Add Node modules only to the end of the path.
# `layout node` will add it to the front, which is not as secure
export PATH=$PATH:$(pwd)/node_modules/.bin

1
.github/copilot-instructions.md vendored Symbolic link
View File

@ -0,0 +1 @@
../STYLE.md

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.direnv/
.vite/
pb_data/

8
.markdownlint.yaml Normal file
View File

@ -0,0 +1,8 @@
# Indentation
MD007:
indent: 2
# Maximum multiple consecutive blank lines
MD012:
maximum: 2
# Disable the line-length warnings.
MD013: false

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}

290
README.md Normal file
View File

@ -0,0 +1,290 @@
Welcome to your new TanStack app!
# Getting Started
To run this application:
```bash
npm install
npm run start
```
# Building For Production
To build this application for production:
```bash
npm run build
```
## Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
```bash
npm run test
```
## Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
## 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`.
### Adding A Route
To add a new route to your application just add another a new file in the `./src/routes` directory.
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.
### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
```tsx
import { Link } from "@tanstack/react-router";
```
Then anywhere in your JSX you can use it like so:
```tsx
<Link to="/about">About</Link>
```
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).
### 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.
Here is an example layout that includes a header:
```tsx
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
import { Link } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
<Outlet />
<TanStackRouterDevtools />
</>
),
})
```
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).
## Data Fetching
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.
For example:
```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.
First you need to add TanStack Store as a dependency:
```bash
npm install @tanstack/store
```
Now let's create a simple counter in the `src/App.tsx` file as a demonstration.
```tsx
import { useStore } from "@tanstack/react-store";
import { Store } from "@tanstack/store";
import "./App.css";
const countStore = new Store(0);
function App() {
const count = useStore(countStore);
return (
<div>
<button onClick={() => countStore.setState((n) => n + 1)}>
Increment - {count}
</button>
</div>
);
}
export default App;
```
One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.
Let's check this out by doubling the count using derived state.
```tsx
import { useStore } from "@tanstack/react-store";
import { Store, Derived } from "@tanstack/store";
import "./App.css";
const countStore = new Store(0);
const doubledStore = new Derived({
fn: () => countStore.state * 2,
deps: [countStore],
});
doubledStore.mount();
function App() {
const count = useStore(countStore);
const doubledCount = useStore(doubledStore);
return (
<div>
<button onClick={() => countStore.setState((n) => n + 1)}>
Increment - {count}
</button>
<div>Doubled - {doubledCount}</div>
</div>
);
}
export default App;
```
We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.
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).
# 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).

75
STYLE.md Normal file
View File

@ -0,0 +1,75 @@
# Style.md
This file provides guidance when working with code in this
repository. This is applicable to both developers and agents.
- Analyze the feature requirements carefully.
- Ask clarifying questions and be sure that you understand the full scope of the feature before implementing.
- Clarify anything that is ambiguous or unclear.
- Application design is specified in `DESIGN.md`. Keep this file up to date if changes are needed.
- List and update items in `TODO.md`.
## Commands
- Build: `npm run build`
- Dev server: `npm run dev`
- Start server: `npm run start`
- Test: `npm test` or `npm run test`
## Code Style & Conventions
- **Framework**
- React
- TanStack Router
- Pocketbase
- Tailwind CSS
- **TypeScript**: Use strict mode and explicit types
- **Accessibility**: All UI should follow general accessibility guidelines
- **SEO**: Implement proper metadata for SEO
- ## **Database**
- **Code Style**
- **Imports**
- Remove any unnecessary imports
- Use absolute imports with `@/` prefix (e.g., `import { Component } from "@/components/Component"`)
- **Naming**: Use PascalCase for components, camelCase for functions/variables
- **Backend**: Project uses Appwrite for backend services
- **Error Handling**: Use try/catch blocks for async operations
- **File Structure**: Group files by feature in the `src` directory
- **Types**: All top-level declarations should have type annotations, as should any location where types are ambiguous.
- **Dependencies**
- Use third-party dependencies only for non-trivial behavior
- Minimize the number of third-party dependencies
- Avoid dependence redundancy
- **Testing**
- Do not bother with unit tests.
- **Comments**
- All non-trivial top-level declarations should contain a documentation comment
- The comment can be a single sentence in most cases.
- Documentation should include any behavior that is not obvious from the name.
- This could include but is not limited to input requirements, unusual
outputs, exceptions thrown.
- Any non-trivial code should be commented, particularly where a statement
may be related to code not in the immediate vicinity.
- **Documentation**
- The design document is in `design.md`
- This document should describe the architecture of the code, it's major data structures and any major features.
- Design should be added _first_, before any code is written.
- Approve design changes before writing any code.
- No code should be written that does not contribute to a feature in the design.
- **React**
- Implement proper error boundaries
- Use functional components with named exports
- Follow Next.js naming conventions for special files
- **Bash**
- Quote all paths in arguments
## Project Style
- The app should be light-on-dark
- Use the following tailwind palettes
- Slate for basic colors
- Violet for highlights and actions
- Emerald for success messages and call-outs
- Red for errors and dangerous actions

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-tsrouter-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Create TanStack App - .</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4119
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "lazy-dm",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"start": "vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-query": "^5.77.2",
"@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3",
"@tanstack/router-plugin": "^1.114.3",
"pocketbase": "^0.26.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.6"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^26.0.0",
"typescript": "^5.7.2",
"vite": "^6.1.0",
"vitest": "^3.0.5",
"web-vitals": "^4.2.4"
}
}

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

21
shell.nix Normal file
View File

@ -0,0 +1,21 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
packages = with pkgs; [
nodejs_22 # Even versions are more stable
pocketbase
# (vscode-with-extensions.override {
# vscodeExtensions = with pkgs.vscode-extensions; [
# asvetliakov.vscode-neovim
# enkia.tokyo-night
# github.copilot
# github.copilot-chat
# ];
# })
# Full VSCode seems to be required to get the Copilot extension to be able
# to authenticate, likely due to some setting not present in
# `vscode-with-extensions`.
# vscode
];
}

6
src/config.ts Normal file
View File

@ -0,0 +1,6 @@
/**
* Application configuration values.
*
* This includes endpoints and other environment-specific settings.
*/
export const POCKETBASE_URL: string = "http://127.0.0.1:8090"; // Update as needed for deployment

View File

@ -0,0 +1,127 @@
import { createContext, useContext, useCallback } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { pb } from "@/lib/pocketbase";
import type { AuthRecord } from "pocketbase";
/**
* Represents the shape of the authenticated user object from PocketBase.
*/
/**
* Context value for authentication state and actions.
*/
export interface AuthContextValue {
user: AuthRecord | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
signup: (
email: string,
password: string,
passwordConfirm: string,
) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
/**
* Fetches the currently authenticated user from PocketBase.
*/
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 }) {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery({
queryKey: ["auth", "user"],
queryFn: fetchUser,
});
const loginMutation = useMutation({
mutationFn: async ({
email,
password,
}: {
email: string;
password: string;
}) => {
await pb.collection("users").authWithPassword(email, password);
return fetchUser();
},
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 });
},
[loginMutation],
);
const signup = useCallback(
async (email: string, password: string, passwordConfirm: string) => {
await signupMutation.mutateAsync({ email, password, passwordConfirm });
},
[signupMutation],
);
const logout = useCallback(async () => {
await logoutMutation.mutateAsync();
}, [logoutMutation]);
return (
<AuthContext.Provider
value={{ user: user ?? null, isLoading, login, signup, logout }}
>
{children}
</AuthContext.Provider>
);
}
/**
* Hook to access authentication context.
* Throws if used outside of AuthProvider.
*/
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
return ctx;
}

9
src/lib/pocketbase.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* Initializes and exports a singleton PocketBase instance for use throughout the app.
*
* Throws if the PocketBase URL is invalid.
*/
import PocketBase from "pocketbase";
import { POCKETBASE_URL } from "@/config";
export const pb = new PocketBase(POCKETBASE_URL);

42
src/main.tsx Normal file
View File

@ -0,0 +1,42 @@
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
import './styles.css'
import reportWebVitals from './reportWebVitals.ts'
// Create a new router instance
const router = createRouter({
routeTree,
context: {},
defaultPreload: 'intent',
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
})
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('app')
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

13
src/reportWebVitals.ts Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = (onPerfEntry?: () => void) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
onCLS(onPerfEntry)
onINP(onPerfEntry)
onFCP(onPerfEntry)
onLCP(onPerfEntry)
onTTFB(onPerfEntry)
})
}
}
export default reportWebVitals

191
src/routeTree.gen.ts Normal file
View File

@ -0,0 +1,191 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root'
import { Route as AboutImport } from './routes/about'
import { Route as IndexImport } from './routes/index'
import { Route as SessionsIndexImport } from './routes/sessions.index'
import { Route as CampaignsIndexImport } from './routes/campaigns.index'
import { Route as CampaignsCampaignIdImport } from './routes/campaigns.$campaignId'
// Create/Update Routes
const AboutRoute = AboutImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const SessionsIndexRoute = SessionsIndexImport.update({
id: '/sessions/',
path: '/sessions/',
getParentRoute: () => rootRoute,
} as any)
const CampaignsIndexRoute = CampaignsIndexImport.update({
id: '/campaigns/',
path: '/campaigns/',
getParentRoute: () => rootRoute,
} as any)
const CampaignsCampaignIdRoute = CampaignsCampaignIdImport.update({
id: '/campaigns/$campaignId',
path: '/campaigns/$campaignId',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutImport
parentRoute: typeof rootRoute
}
'/campaigns/$campaignId': {
id: '/campaigns/$campaignId'
path: '/campaigns/$campaignId'
fullPath: '/campaigns/$campaignId'
preLoaderRoute: typeof CampaignsCampaignIdImport
parentRoute: typeof rootRoute
}
'/campaigns/': {
id: '/campaigns/'
path: '/campaigns'
fullPath: '/campaigns'
preLoaderRoute: typeof CampaignsIndexImport
parentRoute: typeof rootRoute
}
'/sessions/': {
id: '/sessions/'
path: '/sessions'
fullPath: '/sessions'
preLoaderRoute: typeof SessionsIndexImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/campaigns/$campaignId': typeof CampaignsCampaignIdRoute
'/campaigns': typeof CampaignsIndexRoute
'/sessions': typeof SessionsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/campaigns/$campaignId': typeof CampaignsCampaignIdRoute
'/campaigns': typeof CampaignsIndexRoute
'/sessions': typeof SessionsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/campaigns/$campaignId': typeof CampaignsCampaignIdRoute
'/campaigns/': typeof CampaignsIndexRoute
'/sessions/': typeof SessionsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/about'
| '/campaigns/$campaignId'
| '/campaigns'
| '/sessions'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/about' | '/campaigns/$campaignId' | '/campaigns' | '/sessions'
id:
| '__root__'
| '/'
| '/about'
| '/campaigns/$campaignId'
| '/campaigns/'
| '/sessions/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
CampaignsCampaignIdRoute: typeof CampaignsCampaignIdRoute
CampaignsIndexRoute: typeof CampaignsIndexRoute
SessionsIndexRoute: typeof SessionsIndexRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
CampaignsCampaignIdRoute: CampaignsCampaignIdRoute,
CampaignsIndexRoute: CampaignsIndexRoute,
SessionsIndexRoute: SessionsIndexRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/about",
"/campaigns/$campaignId",
"/campaigns/",
"/sessions/"
]
},
"/": {
"filePath": "index.tsx"
},
"/about": {
"filePath": "about.tsx"
},
"/campaigns/$campaignId": {
"filePath": "campaigns.$campaignId.tsx"
},
"/campaigns/": {
"filePath": "campaigns.index.tsx"
},
"/sessions/": {
"filePath": "sessions.index.tsx"
}
}
}
ROUTE_MANIFEST_END */

55
src/routes/__root.tsx Normal file
View File

@ -0,0 +1,55 @@
import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { AuthProvider } from "@/context/auth/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
/** Root of the application */
export const Route = createRootRoute({
component: () => (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<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="/sessions"
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",
}}
>
Sessions
</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>
</header>
<Outlet />
<TanStackRouterDevtools />
</AuthProvider>
</QueryClientProvider>
),
});

9
src/routes/about.tsx Normal file
View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/about')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/about"!</div>
}

View File

@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/campaigns/$campaignId")({
component: RouteComponent,
});
function RouteComponent() {
const { campaignId } = Route.useParams();
return <div>Hello "/campaigns/{campaignId}"!</div>;
}

View File

@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/campaigns/")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/campaigns/"!</div>;
}

20
src/routes/index.tsx Normal file
View File

@ -0,0 +1,20 @@
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: App,
});
function App() {
return (
<div className="text-center">
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
<h1>Hello, Tanstack</h1>
</header>
<div>
<Link to="/campaigns" activeProps={{ className: "weight-bold" }}>
Campaigns
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/sessions/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/sessions/"!</div>
}

49
src/styles.css Normal file
View File

@ -0,0 +1,49 @@
@import "tailwindcss";
html, body {
height: 100%;
min-height: 100%;
background-color: #0f172a; /* slate-900 */
color: #f1f5f9; /* slate-100 */
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.5;
}
body {
margin: 0;
padding: 0;
min-width: 320px;
}
code, pre {
font-family: 'Fira Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
background: #1e293b; /* slate-800 */
color: #a5b4fc; /* violet-300 */
border-radius: 0.25rem;
padding: 0.2em 0.4em;
}
a {
color: #a5b4fc; /* violet-300 */
text-decoration: underline;
transition: color 0.2s;
}
a:hover, a:focus {
color: #7c3aed; /* violet-600 */
}
/* Remove default outline, but keep focus-visible for accessibility */
:focus:not(:focus-visible) {
outline: none;
}
/* Scrollbar styling for dark mode */
::-webkit-scrollbar {
width: 8px;
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
}
}
}

20
vite.config.js Normal file
View File

@ -0,0 +1,20 @@
import { defineConfig } from "vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import { resolve } from "node:path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact(), tailwindcss()],
test: {
globals: true,
environment: "jsdom",
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
}
});