This commit is contained in:
2026-01-28 20:48:02 +01:00
commit 709633bc3c
32 changed files with 6642 additions and 0 deletions

16
.cta.json Normal file
View File

@@ -0,0 +1,16 @@
{
"projectName": "deploy-tan",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"tailwind": true,
"addOnOptions": {},
"git": true,
"version": 1,
"framework": "react-cra",
"chosenAddOns": [
"eslint",
"nitro",
"start"
]
}

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

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
package-lock.json
pnpm-lock.yaml
yarn.lock

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
}
}

301
README.md Normal file
View File

@@ -0,0 +1,301 @@
Welcome to your new TanStack app!
# Getting Started
To run this application:
```bash
npm install
npm run dev
```
# 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.
## Linting & Formatting
This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available:
```bash
npm run lint
npm run format
npm run check
```
## 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).

5
eslint.config.js Normal file
View File

@@ -0,0 +1,5 @@
// @ts-check
import { tanstackConfig } from '@tanstack/eslint-config'
export default [...tanstackConfig]

45
package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "deploy-tan",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@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",
"tailwindcss": "^4.0.6",
"vite-tsconfig-paths": "^6.0.2"
},
"devDependencies": {
"@tanstack/devtools-vite": "^0.3.11",
"@tanstack/eslint-config": "^0.3.0",
"@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",
"prettier": "^3.5.3",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
}

5406
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

10
prettier.config.js Normal file
View File

@@ -0,0 +1,10 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: false,
singleQuote: true,
trailingComma: "all",
};
export default config;

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

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

@@ -0,0 +1,177 @@
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import {
ChevronDown,
ChevronRight,
Home,
Menu,
Network,
SquareFunction,
StickyNote,
X,
} from 'lucide-react'
export default function Header() {
const [isOpen, setIsOpen] = useState(false)
const [groupedExpanded, setGroupedExpanded] = useState<
Record<string, boolean>
>({})
return (
<>
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
<button
onClick={() => setIsOpen(true)}
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
aria-label="Open menu"
>
<Menu size={24} />
</button>
<h1 className="ml-4 text-xl font-semibold">
<Link to="/">
<img
src="/tanstack-word-logo-white.svg"
alt="TanStack Logo"
className="h-10"
/>
</Link>
</h1>
</header>
<aside
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-bold">Navigation</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
aria-label="Close menu"
>
<X size={24} />
</button>
</div>
<nav className="flex-1 p-4 overflow-y-auto">
<Link
to="/"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<Home size={20} />
<span className="font-medium">Home</span>
</Link>
{/* Demo Links Start */}
<Link
to="/demo/start/server-funcs"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<SquareFunction size={20} />
<span className="font-medium">Start - Server Functions</span>
</Link>
<Link
to="/demo/start/api-request"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<Network size={20} />
<span className="font-medium">Start - API Request</span>
</Link>
<div className="flex flex-row justify-between">
<Link
to="/demo/start/ssr"
onClick={() => setIsOpen(false)}
className="flex-1 flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex-1 flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<StickyNote size={20} />
<span className="font-medium">Start - SSR Demos</span>
</Link>
<button
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
onClick={() =>
setGroupedExpanded((prev) => ({
...prev,
StartSSRDemo: !prev.StartSSRDemo,
}))
}
>
{groupedExpanded.StartSSRDemo ? (
<ChevronDown size={20} />
) : (
<ChevronRight size={20} />
)}
</button>
</div>
{groupedExpanded.StartSSRDemo && (
<div className="flex flex-col ml-4">
<Link
to="/demo/start/ssr/spa-mode"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<StickyNote size={20} />
<span className="font-medium">SPA Mode</span>
</Link>
<Link
to="/demo/start/ssr/full-ssr"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<StickyNote size={20} />
<span className="font-medium">Full SSR</span>
</Link>
<Link
to="/demo/start/ssr/data-only"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<StickyNote size={20} />
<span className="font-medium">Data Only</span>
</Link>
</div>
)}
{/* Demo Links End */}
</nav>
</aside>
</>
)
}

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

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
}

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

@@ -0,0 +1,58 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import Header from '../components/Header'
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,44 @@
import { useEffect, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
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="flex items-center justify-center min-h-screen p-4 text-white"
style={{
backgroundColor: '#000',
backgroundImage:
'radial-gradient(ellipse 60% 60% at 0% 100%, #444 0%, #222 60%, #000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-2xl mb-4">Start API Request Demo - Names List</h1>
<ul className="mb-4 space-y-2">
{names.map((name) => (
<li
key={name}
className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md"
>
<span className="text-lg text-white">{name}</span>
</li>
))}
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,109 @@
import fs from 'node:fs'
import { useCallback, useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
/*
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
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white"
style={{
backgroundImage:
'radial-gradient(50% 50% at 20% 60%, #23272a 0%, #18181b 50%, #000000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-2xl mb-4">Start Server Functions - Todo Example</h1>
<ul className="mb-4 space-y-2">
{todos?.map((t) => (
<li
key={t.id}
className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md"
>
<span className="text-lg text-white">{t.name}</span>
</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..."
className="w-full px-4 py-3 rounded-lg border border-white/20 bg-white/10 backdrop-blur-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent"
/>
<button
disabled={todo.trim().length === 0}
onClick={submitTodo}
className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-500/50 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition-colors"
>
Add todo
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
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
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white"
style={{
backgroundImage:
'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-3xl font-bold mb-6 text-pink-400">
Data Only SSR - Punk Songs
</h1>
<ul className="space-y-3">
{punkSongs.map((song) => (
<li
key={song.id}
className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md"
>
<span className="text-lg text-white font-medium">
{song.name}
</span>
<span className="text-white/60"> - {song.artist}</span>
</li>
))}
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
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
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white"
style={{
backgroundImage:
'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-3xl font-bold mb-6 text-purple-400">
Full SSR - Punk Songs
</h1>
<ul className="space-y-3">
{punkSongs.map((song) => (
<li
key={song.id}
className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md"
>
<span className="text-lg text-white font-medium">
{song.name}
</span>
<span className="text-white/60"> - {song.artist}</span>
</li>
))}
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,43 @@
import { createFileRoute, Link } from '@tanstack/react-router'
export const Route = createFileRoute('/demo/start/ssr/')({
component: RouteComponent,
})
function RouteComponent() {
return (
<div
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-900 to-black p-4 text-white"
style={{
backgroundImage:
'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-4xl font-bold mb-8 text-center bg-gradient-to-r from-pink-500 via-purple-500 to-green-400 bg-clip-text text-transparent">
SSR Demos
</h1>
<div className="flex flex-col gap-4">
<Link
to="/demo/start/ssr/spa-mode"
className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-pink-600 to-pink-500 hover:from-pink-700 hover:to-pink-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-pink-500/50 border-2 border-pink-400"
>
SPA Mode
</Link>
<Link
to="/demo/start/ssr/full-ssr"
className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-700 hover:to-purple-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-purple-500/50 border-2 border-purple-400"
>
Full SSR
</Link>
<Link
to="/demo/start/ssr/data-only"
className="text-2xl font-bold py-6 px-8 rounded-lg bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white text-center shadow-lg transform transition-all hover:scale-105 hover:shadow-green-500/50 border-2 border-green-400"
>
Data Only
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
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
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-zinc-800 to-black p-4 text-white"
style={{
backgroundImage:
'radial-gradient(50% 50% at 20% 60%, #1a1a1a 0%, #0a0a0a 50%, #000000 100%)',
}}
>
<div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
<h1 className="text-3xl font-bold mb-6 text-green-400">
SPA Mode - Punk Songs
</h1>
<ul className="space-y-3">
{punkSongs.map((song) => (
<li
key={song.id}
className="bg-white/10 border border-white/20 rounded-lg p-4 backdrop-blur-sm shadow-md"
>
<span className="text-lg text-white font-medium">
{song.name}
</span>
<span className="text-white/60"> - {song.artist}</span>
</li>
))}
</ul>
</div>
</div>
)
}

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

@@ -0,0 +1,118 @@
import { createFileRoute } from '@tanstack/react-router'
import {
Zap,
Server,
Route as RouteIcon,
Shield,
Waves,
Sparkles,
} from 'lucide-react'
export const Route = createFileRoute('/')({ component: App })
function App() {
const features = [
{
icon: <Zap className="w-12 h-12 text-cyan-400" />,
title: 'Powerful Server Functions',
description:
'Write server-side code that seamlessly integrates with your client components. Type-safe, secure, and simple.',
},
{
icon: <Server className="w-12 h-12 text-cyan-400" />,
title: 'Flexible Server Side Rendering',
description:
'Full-document SSR, streaming, and progressive enhancement out of the box. Control exactly what renders where.',
},
{
icon: <RouteIcon className="w-12 h-12 text-cyan-400" />,
title: 'API Routes',
description:
'Build type-safe API endpoints alongside your application. No separate backend needed.',
},
{
icon: <Shield className="w-12 h-12 text-cyan-400" />,
title: 'Strongly Typed Everything',
description:
'End-to-end type safety from server to client. Catch errors before they reach production.',
},
{
icon: <Waves className="w-12 h-12 text-cyan-400" />,
title: 'Full Streaming Support',
description:
'Stream data from server to client progressively. Perfect for AI applications and real-time updates.',
},
{
icon: <Sparkles className="w-12 h-12 text-cyan-400" />,
title: 'Next Generation Ready',
description:
'Built from the ground up for modern web applications. Deploy anywhere JavaScript runs.',
},
]
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900">
<section className="relative py-20 px-6 text-center overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10"></div>
<div className="relative max-w-5xl mx-auto">
<div className="flex items-center justify-center gap-6 mb-6">
<img
src="/tanstack-circle-logo.png"
alt="TanStack Logo"
className="w-24 h-24 md:w-32 md:h-32"
/>
<h1 className="text-6xl md:text-7xl font-black text-white [letter-spacing:-0.08em]">
<span className="text-gray-300">TANSTACK</span>{' '}
<span className="bg-gradient-to-r from-cyan-400 to-blue-400 bg-clip-text text-transparent">
START
</span>
</h1>
</div>
<p className="text-2xl md:text-3xl text-gray-300 mb-4 font-light">
The framework for next generation AI applications
</p>
<p className="text-lg text-gray-400 max-w-3xl mx-auto mb-8">
Full-stack framework powered by TanStack Router for React and Solid.
Build modern applications with server functions, streaming, and type
safety.
</p>
<div className="flex flex-col items-center gap-4">
<a
href="https://tanstack.com/start"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3 bg-cyan-500 hover:bg-cyan-600 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-cyan-500/50"
>
Documentation
</a>
<p className="text-gray-400 text-sm mt-2">
Begin your TanStack Start journey by editing{' '}
<code className="px-2 py-1 bg-slate-700 rounded text-cyan-400">
/src/routes/index.tsx
</code>
</p>
</div>
</div>
</section>
<section className="py-16 px-6 max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<div
key={index}
className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6 hover:border-cyan-500/50 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-500/10"
>
<div className="mb-4">{feature.icon}</div>
<h3 className="text-xl font-semibold text-white mb-3">
{feature.title}
</h3>
<p className="text-gray-400 leading-relaxed">
{feature.description}
</p>
</div>
))}
</div>
</section>
</div>
)
}

15
src/styles.css Normal file
View File

@@ -0,0 +1,15 @@
@import "tailwindcss";
body {
@apply m-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;
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"include": ["**/*.ts", "**/*.tsx", "eslint.config.js", "prettier.config.js", "vite.config.js"],
"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/*"]
}
}
}

30
vite.config.ts Normal file
View File

@@ -0,0 +1,30 @@
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 tailwindcss from '@tailwindcss/vite'
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'],
}),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
export default config