init tanstack

This commit is contained in:
BeauTroll
2026-01-19 09:32:47 +01:00
parent 1e33b87c96
commit 4c8a6e9fd3
34 changed files with 5097 additions and 4 deletions

16
.cta.json Normal file
View File

@@ -0,0 +1,16 @@
{
"projectName": "dofus-manager2",
"mode": "file-router",
"typescript": true,
"tailwind": false,
"packageManager": "pnpm",
"git": true,
"install": true,
"addOnOptions": {},
"version": 1,
"framework": "react-cra",
"chosenAddOns": [
"start",
"nitro"
]
}

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
count.txt
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
todos.json

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
pnpm install
pnpm dev
```
# Building For Production
To build this application for production:
```bash
pnpm build
```
## Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
```bash
pnpm test
```
## Styling
This project uses CSS 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
pnpm add @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
pnpm add @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).

View File

@@ -25,7 +25,7 @@ Draft
## Tasks / Subtasks ## Tasks / Subtasks
- [ ] Task 1: Initialize TanStack Start project (AC: 1) - [ ] Task 1: Initialize TanStack Start project (AC: 1)
- [ ] Create new TanStack Start project with `pnpm create @tanstack/start` - [x] Create new TanStack Start project with `pnpm create @tanstack/start`
- [ ] Configure `tsconfig.json` with strict mode enabled - [ ] Configure `tsconfig.json` with strict mode enabled
- [ ] Configure path aliases (`@/` pointing to `src/`) - [ ] Configure path aliases (`@/` pointing to `src/`)
- [ ] Verify TypeScript strict compilation works - [ ] Verify TypeScript strict compilation works

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "dofus-manager2",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.131.7",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"lucide-react": "^0.561.0",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"vite-tsconfig-paths": "^6.0.2"
},
"devDependencies": {
"@tanstack/devtools-vite": "^0.3.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
}

3910
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

38
src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

18
src/components/Header.css Normal file
View File

@@ -0,0 +1,18 @@
.header {
padding: 0.5rem;
display: flex;
gap: 0.5rem;
background-color: #fff;
color: #000;
justify-content: space-between;
}
.nav {
display: flex;
flex-direction: row;
}
.nav-item {
padding: 0 0.5rem;
font-weight: bold;
}

27
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { Link } from '@tanstack/react-router'
import './Header.css'
export default function Header() {
return (
<header className="header">
<nav className="nav">
<div className="nav-item">
<Link to="/">Home</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/server-funcs">Start - Server Functions</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/api-request">Start - API Request</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/ssr">Start - SSR Demos</Link>
</div>
</nav>
</header>
)
}

View File

@@ -0,0 +1,13 @@
import { createServerFn } from '@tanstack/react-start'
export const getPunkSongs = createServerFn({
method: 'GET',
}).handler(async () => [
{ id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' },
{ id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' },
{ id: 3, name: 'The Middle', artist: 'Jimmy Eat World' },
{ id: 4, name: 'My Own Worst Enemy', artist: 'Lit' },
{ id: 5, name: 'Fat Lip', artist: 'Sum 41' },
{ id: 6, name: 'All the Small Things', artist: 'blink-182' },
{ id: 7, name: 'Beverly Hills', artist: 'Weezer' },
])

12
src/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

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

@@ -0,0 +1,219 @@
/* 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 { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request'
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names'
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index'
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode'
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr'
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
id: '/demo/start/server-funcs',
path: '/demo/start/server-funcs',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
id: '/demo/start/api-request',
path: '/demo/start/api-request',
getParentRoute: () => rootRouteImport,
} as any)
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
id: '/demo/api/names',
path: '/demo/api/names',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
id: '/demo/start/ssr/',
path: '/demo/start/ssr/',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
id: '/demo/start/ssr/spa-mode',
path: '/demo/start/ssr/spa-mode',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
id: '/demo/start/ssr/full-ssr',
path: '/demo/start/ssr/full-ssr',
getParentRoute: () => rootRouteImport,
} as any)
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
id: '/demo/start/ssr/data-only',
path: '/demo/start/ssr/data-only',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/demo/api/names': typeof DemoApiNamesRoute
'/demo/start/api-request': typeof DemoStartApiRequestRoute
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/demo/api/names': typeof DemoApiNamesRoute
'/demo/start/api-request': typeof DemoStartApiRequestRoute
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
'/demo/start/ssr': typeof DemoStartSsrIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/demo/api/names': typeof DemoApiNamesRoute
'/demo/start/api-request': typeof DemoStartApiRequestRoute
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr'
id:
| '__root__'
| '/'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DemoApiNamesRoute: typeof DemoApiNamesRoute
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute
DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute
DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute
DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute
DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/server-funcs': {
id: '/demo/start/server-funcs'
path: '/demo/start/server-funcs'
fullPath: '/demo/start/server-funcs'
preLoaderRoute: typeof DemoStartServerFuncsRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/api-request': {
id: '/demo/start/api-request'
path: '/demo/start/api-request'
fullPath: '/demo/start/api-request'
preLoaderRoute: typeof DemoStartApiRequestRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/api/names': {
id: '/demo/api/names'
path: '/demo/api/names'
fullPath: '/demo/api/names'
preLoaderRoute: typeof DemoApiNamesRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/ssr/': {
id: '/demo/start/ssr/'
path: '/demo/start/ssr'
fullPath: '/demo/start/ssr/'
preLoaderRoute: typeof DemoStartSsrIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/ssr/spa-mode': {
id: '/demo/start/ssr/spa-mode'
path: '/demo/start/ssr/spa-mode'
fullPath: '/demo/start/ssr/spa-mode'
preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/ssr/full-ssr': {
id: '/demo/start/ssr/full-ssr'
path: '/demo/start/ssr/full-ssr'
fullPath: '/demo/start/ssr/full-ssr'
preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport
parentRoute: typeof rootRouteImport
}
'/demo/start/ssr/data-only': {
id: '/demo/start/ssr/data-only'
path: '/demo/start/ssr/data-only'
fullPath: '/demo/start/ssr/data-only'
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DemoApiNamesRoute: DemoApiNamesRoute,
DemoStartApiRequestRoute: DemoStartApiRequestRoute,
DemoStartServerFuncsRoute: DemoStartServerFuncsRoute,
DemoStartSsrDataOnlyRoute: DemoStartSsrDataOnlyRoute,
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

17
src/router.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { createRouter } from '@tanstack/react-router'
// Import the generated route tree
import { routeTree } from './routeTree.gen'
// Create a new router instance
export const getRouter = () => {
const router = createRouter({
routeTree,
context: {},
scrollRestoration: true,
defaultPreloadStaleTime: 0,
})
return router
}

View File

@@ -1,5 +1,58 @@
import { AppShell } from "@/components/layout/app-shell"; import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
export default function RootLayout({ children }) { import Header from '../components/Header'
return <AppShell>{children}</AppShell>;
import appCss from '../styles.css?url'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<Header />
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
<Scripts />
</body>
</html>
)
} }

View File

@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/demo/api/names')({
server: {
handlers: {
GET: () => json(['Alice', 'Bob', 'Charlie']),
},
},
})

View File

@@ -0,0 +1,33 @@
import { useEffect, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import './start.css'
function getNames() {
return fetch('/demo/api/names').then((res) => res.json() as Promise<string[]>)
}
export const Route = createFileRoute('/demo/start/api-request')({
component: Home,
})
function Home() {
const [names, setNames] = useState<Array<string>>([])
useEffect(() => {
getNames().then(setNames)
}, [])
return (
<div className="api-page">
<div className="content">
<h1>Start API Request Demo - Names List</h1>
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
</div>
</div>
)
}

43
src/routes/demo/start.css Normal file
View File

@@ -0,0 +1,43 @@
.api-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
color: #fff;
}
.api-page .content {
width: 100%;
max-width: 2xl;
padding: 8rem;
border-radius: 1rem;
backdrop-filter: blur(1rem);
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
border: 0.5rem solid rgba(0, 0, 0, 0.1);
}
.api-page .content h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
.api-page .content ul {
margin-bottom: 1rem;
list-style: none;
padding: 0;
}
.api-page .content li {
background-color: rgba(255, 255, 255, 0.1);
padding: 0.5rem;
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(0.5rem);
box-shadow: 0 0 0.5rem 0 rgba(255, 255, 255, 0.1);
}
.api-page .content li span {
font-size: 1.2rem;
}

View File

@@ -0,0 +1,92 @@
import fs from 'node:fs'
import { useCallback, useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import './start.css'
/*
const loggingMiddleware = createMiddleware().server(
async ({ next, request }) => {
console.log("Request:", request.url);
return next();
}
);
const loggedServerFunction = createServerFn({ method: "GET" }).middleware([
loggingMiddleware,
]);
*/
const TODOS_FILE = 'todos.json'
async function readTodos() {
return JSON.parse(
await fs.promises.readFile(TODOS_FILE, 'utf-8').catch(() =>
JSON.stringify(
[
{ id: 1, name: 'Get groceries' },
{ id: 2, name: 'Buy a new phone' },
],
null,
2,
),
),
)
}
const getTodos = createServerFn({
method: 'GET',
}).handler(async () => await readTodos())
const addTodo = createServerFn({ method: 'POST' })
.inputValidator((d: string) => d)
.handler(async ({ data }) => {
const todos = await readTodos()
todos.push({ id: todos.length + 1, name: data })
await fs.promises.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2))
return todos
})
export const Route = createFileRoute('/demo/start/server-funcs')({
component: Home,
loader: async () => await getTodos(),
})
function Home() {
const router = useRouter()
let todos = Route.useLoaderData()
const [todo, setTodo] = useState('')
const submitTodo = useCallback(async () => {
todos = await addTodo({ data: todo })
setTodo('')
router.invalidate()
}, [addTodo, todo])
return (
<div>
<h1>Start Server Functions - Todo Example</h1>
<ul>
{todos?.map((t) => (
<li key={t.id}>{t.name}</li>
))}
</ul>
<div className="flex flex-col gap-2">
<input
type="text"
value={todo}
onChange={(e) => setTodo(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitTodo()
}
}}
placeholder="Enter a new todo..."
/>
<button disabled={todo.trim().length === 0} onClick={submitTodo}>
Add todo
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { createFileRoute } from '@tanstack/react-router'
import { getPunkSongs } from '@/data/demo.punk-songs'
export const Route = createFileRoute('/demo/start/ssr/data-only')({
ssr: 'data-only',
component: RouteComponent,
loader: async () => await getPunkSongs(),
})
function RouteComponent() {
const punkSongs = Route.useLoaderData()
return (
<div>
<h1>Data Only SSR - Punk Songs</h1>
<ul>
{punkSongs.map((song) => (
<li key={song.id}>
{song.name} - {song.artist}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { createFileRoute } from '@tanstack/react-router'
import { getPunkSongs } from '@/data/demo.punk-songs'
export const Route = createFileRoute('/demo/start/ssr/full-ssr')({
component: RouteComponent,
loader: async () => await getPunkSongs(),
})
function RouteComponent() {
const punkSongs = Route.useLoaderData()
return (
<div>
<h1>Full SSR - Punk Songs</h1>
<ul>
{punkSongs.map((song) => (
<li key={song.id}>
{song.name} - {song.artist}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { createFileRoute, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/demo/start/ssr/')({
component: RouteComponent,
})
function RouteComponent() {
return (
<div>
<h1>SSR Demos</h1>
<ul>
<li>
<Link to="/demo/start/ssr/spa-mode">SPA Mode</Link>
</li>
<li>
<Link to="/demo/start/ssr/full-ssr">Full SSR</Link>
</li>
<li>
<Link to="/demo/start/ssr/data-only">Data Only</Link>
</li>
</ul>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { useEffect, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { getPunkSongs } from '@/data/demo.punk-songs'
export const Route = createFileRoute('/demo/start/ssr/spa-mode')({
ssr: false,
component: RouteComponent,
})
function RouteComponent() {
const [punkSongs, setPunkSongs] = useState<
Awaited<ReturnType<typeof getPunkSongs>>
>([])
useEffect(() => {
getPunkSongs().then(setPunkSongs)
}, [])
return (
<div>
<h1>SPA Mode - Punk Songs</h1>
<ul>
{punkSongs.map((song) => (
<li key={song.id}>
{song.name} - {song.artist}
</li>
))}
</ul>
</div>
)
}

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

@@ -0,0 +1,37 @@
import { createFileRoute } from '@tanstack/react-router'
import '../App.css'
export const Route = createFileRoute('/')({ component: App })
function App() {
return (
<div className="App">
<header className="App-header">
<img
src="/tanstack-circle-logo.png"
className="App-logo"
alt="TanStack Logo"
/>
<p>
Edit <code>src/routes/index.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<a
className="App-link"
href="https://tanstack.com"
target="_blank"
rel="noopener noreferrer"
>
Learn TanStack
</a>
</header>
</div>
)
}

14
src/styles.css Normal file
View File

@@ -0,0 +1,14 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

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": false,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

28
vite.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import viteTsConfigPaths from 'vite-tsconfig-paths'
import { fileURLToPath, URL } from 'url'
import { nitro } from 'nitro/vite'
const config = defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
plugins: [
devtools(),
nitro(),
// this is the plugin that enables path aliases
viteTsConfigPaths({
projects: ['./tsconfig.json'],
}),
tanstackStart(),
viteReact(),
],
})
export default config