feat(tanstack): initialize TanStack project with routing, API, and error handling components
22
tanstack/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.DS_Store
|
||||
.cache
|
||||
.env
|
||||
.vercel
|
||||
.output
|
||||
.vinxi
|
||||
|
||||
/build/
|
||||
/api/
|
||||
/server/build
|
||||
/public/build
|
||||
.vinxi
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
4
tanstack/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
**/build
|
||||
**/public
|
||||
pnpm-lock.yaml
|
||||
routeTree.gen.ts
|
||||
11
tanstack/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
}
|
||||
}
|
||||
72
tanstack/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Welcome to TanStack.com!
|
||||
|
||||
This site is built with TanStack Router!
|
||||
|
||||
- [TanStack Router Docs](https://tanstack.com/router)
|
||||
|
||||
It's deployed automagically with Netlify!
|
||||
|
||||
- [Netlify](https://netlify.com/)
|
||||
|
||||
## Development
|
||||
|
||||
From your terminal:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This starts your app in development mode, rebuilding assets on file changes.
|
||||
|
||||
## Editing and previewing the docs of TanStack projects locally
|
||||
|
||||
The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app.
|
||||
In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system.
|
||||
|
||||
Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally :
|
||||
|
||||
1. Create a new directory called `tanstack`.
|
||||
|
||||
```sh
|
||||
mkdir tanstack
|
||||
```
|
||||
|
||||
2. Enter the directory and clone this repo and the repo of the project there.
|
||||
|
||||
```sh
|
||||
cd tanstack
|
||||
git clone git@github.com:TanStack/tanstack.com.git
|
||||
git clone git@github.com:TanStack/form.git
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Your `tanstack` directory should look like this:
|
||||
>
|
||||
> ```
|
||||
> tanstack/
|
||||
> |
|
||||
> +-- form/
|
||||
> |
|
||||
> +-- tanstack.com/
|
||||
> ```
|
||||
|
||||
> [!WARNING]
|
||||
> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found.
|
||||
|
||||
3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode:
|
||||
|
||||
```sh
|
||||
cd tanstack.com
|
||||
pnpm i
|
||||
# The app will run on https://localhost:3000 by default
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`.
|
||||
|
||||
> [!NOTE]
|
||||
> The updated pages need to be manually reloaded in the browser.
|
||||
|
||||
> [!WARNING]
|
||||
> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page!
|
||||
15
tanstack/app.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from '@tanstack/react-start/config'
|
||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
export default defineConfig({
|
||||
tsr: {
|
||||
appDirectory: 'src',
|
||||
},
|
||||
vite: {
|
||||
plugins: [
|
||||
tsConfigPaths({
|
||||
projects: ['./tsconfig.json'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
31
tanstack/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "tanstack-start-example-basic",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vinxi dev",
|
||||
"build": "vinxi build",
|
||||
"start": "vinxi start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "^1.115.0",
|
||||
"@tanstack/react-router-devtools": "^1.115.0",
|
||||
"@tanstack/react-start": "^1.115.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"redaxios": "^0.5.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vinxi": "0.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"postcss": "^8.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
5883
tanstack/pnpm-lock.yaml
generated
Normal file
6
tanstack/postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
tanstack/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
tanstack/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
tanstack/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
tanstack/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 832 B |
BIN
tanstack/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
tanstack/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tanstack/public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
19
tanstack/public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
6
tanstack/src/api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {
|
||||
createStartAPIHandler,
|
||||
defaultAPIFileRouteHandler,
|
||||
} from '@tanstack/react-start/api'
|
||||
|
||||
export default createStartAPIHandler(defaultAPIFileRouteHandler)
|
||||
8
tanstack/src/client.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vinxi/types/client" />
|
||||
import { hydrateRoot } from 'react-dom/client'
|
||||
import { StartClient } from '@tanstack/react-start'
|
||||
import { createRouter } from './router'
|
||||
|
||||
const router = createRouter()
|
||||
|
||||
hydrateRoot(document, <StartClient router={router} />)
|
||||
53
tanstack/src/components/DefaultCatchBoundary.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
ErrorComponent,
|
||||
Link,
|
||||
rootRouteId,
|
||||
useMatch,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import type { ErrorComponentProps } from '@tanstack/react-router'
|
||||
|
||||
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
const router = useRouter()
|
||||
const isRoot = useMatch({
|
||||
strict: false,
|
||||
select: (state) => state.id === rootRouteId,
|
||||
})
|
||||
|
||||
console.error('DefaultCatchBoundary Error:', error)
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
|
||||
<ErrorComponent error={error} />
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.invalidate()
|
||||
}}
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
{isRoot ? (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.history.back()
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
tanstack/src/components/NotFound.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
export function NotFound({ children }: { children?: any }) {
|
||||
return (
|
||||
<div className="space-y-2 p-2">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{children || <p>The page you are looking for does not exist.</p>}
|
||||
</div>
|
||||
<p className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
className="bg-cyan-600 text-white px-2 py-1 rounded uppercase font-black text-sm"
|
||||
>
|
||||
Start Over
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
5
tanstack/src/components/PostError.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ErrorComponent, ErrorComponentProps } from '@tanstack/react-router'
|
||||
|
||||
export function PostErrorComponent({ error }: ErrorComponentProps) {
|
||||
return <ErrorComponent error={error} />
|
||||
}
|
||||
5
tanstack/src/components/UserError.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ErrorComponent, ErrorComponentProps } from '@tanstack/react-router'
|
||||
|
||||
export function UserErrorComponent({ error }: ErrorComponentProps) {
|
||||
return <ErrorComponent error={error} />
|
||||
}
|
||||
6
tanstack/src/global-middleware.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { registerGlobalMiddleware } from '@tanstack/react-start'
|
||||
import { logMiddleware } from './utils/loggingMiddleware'
|
||||
|
||||
registerGlobalMiddleware({
|
||||
middleware: [logMiddleware],
|
||||
})
|
||||
479
tanstack/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/* 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 UsersImport } from './routes/users'
|
||||
import { Route as RedirectImport } from './routes/redirect'
|
||||
import { Route as PostsImport } from './routes/posts'
|
||||
import { Route as DeferredImport } from './routes/deferred'
|
||||
import { Route as PathlessLayoutImport } from './routes/_pathlessLayout'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as UsersIndexImport } from './routes/users.index'
|
||||
import { Route as PostsIndexImport } from './routes/posts.index'
|
||||
import { Route as UsersUserIdImport } from './routes/users.$userId'
|
||||
import { Route as PostsPostIdImport } from './routes/posts.$postId'
|
||||
import { Route as PathlessLayoutNestedLayoutImport } from './routes/_pathlessLayout/_nested-layout'
|
||||
import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep'
|
||||
import { Route as PathlessLayoutNestedLayoutRouteBImport } from './routes/_pathlessLayout/_nested-layout/route-b'
|
||||
import { Route as PathlessLayoutNestedLayoutRouteAImport } from './routes/_pathlessLayout/_nested-layout/route-a'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const UsersRoute = UsersImport.update({
|
||||
id: '/users',
|
||||
path: '/users',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const RedirectRoute = RedirectImport.update({
|
||||
id: '/redirect',
|
||||
path: '/redirect',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PostsRoute = PostsImport.update({
|
||||
id: '/posts',
|
||||
path: '/posts',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const DeferredRoute = DeferredImport.update({
|
||||
id: '/deferred',
|
||||
path: '/deferred',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PathlessLayoutRoute = PathlessLayoutImport.update({
|
||||
id: '/_pathlessLayout',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const UsersIndexRoute = UsersIndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => UsersRoute,
|
||||
} as any)
|
||||
|
||||
const PostsIndexRoute = PostsIndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => PostsRoute,
|
||||
} as any)
|
||||
|
||||
const UsersUserIdRoute = UsersUserIdImport.update({
|
||||
id: '/$userId',
|
||||
path: '/$userId',
|
||||
getParentRoute: () => UsersRoute,
|
||||
} as any)
|
||||
|
||||
const PostsPostIdRoute = PostsPostIdImport.update({
|
||||
id: '/$postId',
|
||||
path: '/$postId',
|
||||
getParentRoute: () => PostsRoute,
|
||||
} as any)
|
||||
|
||||
const PathlessLayoutNestedLayoutRoute = PathlessLayoutNestedLayoutImport.update(
|
||||
{
|
||||
id: '/_nested-layout',
|
||||
getParentRoute: () => PathlessLayoutRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({
|
||||
id: '/posts_/$postId/deep',
|
||||
path: '/posts/$postId/deep',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const PathlessLayoutNestedLayoutRouteBRoute =
|
||||
PathlessLayoutNestedLayoutRouteBImport.update({
|
||||
id: '/route-b',
|
||||
path: '/route-b',
|
||||
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const PathlessLayoutNestedLayoutRouteARoute =
|
||||
PathlessLayoutNestedLayoutRouteAImport.update({
|
||||
id: '/route-a',
|
||||
path: '/route-a',
|
||||
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_pathlessLayout': {
|
||||
id: '/_pathlessLayout'
|
||||
path: ''
|
||||
fullPath: ''
|
||||
preLoaderRoute: typeof PathlessLayoutImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/deferred': {
|
||||
id: '/deferred'
|
||||
path: '/deferred'
|
||||
fullPath: '/deferred'
|
||||
preLoaderRoute: typeof DeferredImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/posts': {
|
||||
id: '/posts'
|
||||
path: '/posts'
|
||||
fullPath: '/posts'
|
||||
preLoaderRoute: typeof PostsImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/redirect': {
|
||||
id: '/redirect'
|
||||
path: '/redirect'
|
||||
fullPath: '/redirect'
|
||||
preLoaderRoute: typeof RedirectImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/users': {
|
||||
id: '/users'
|
||||
path: '/users'
|
||||
fullPath: '/users'
|
||||
preLoaderRoute: typeof UsersImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_pathlessLayout/_nested-layout': {
|
||||
id: '/_pathlessLayout/_nested-layout'
|
||||
path: ''
|
||||
fullPath: ''
|
||||
preLoaderRoute: typeof PathlessLayoutNestedLayoutImport
|
||||
parentRoute: typeof PathlessLayoutImport
|
||||
}
|
||||
'/posts/$postId': {
|
||||
id: '/posts/$postId'
|
||||
path: '/$postId'
|
||||
fullPath: '/posts/$postId'
|
||||
preLoaderRoute: typeof PostsPostIdImport
|
||||
parentRoute: typeof PostsImport
|
||||
}
|
||||
'/users/$userId': {
|
||||
id: '/users/$userId'
|
||||
path: '/$userId'
|
||||
fullPath: '/users/$userId'
|
||||
preLoaderRoute: typeof UsersUserIdImport
|
||||
parentRoute: typeof UsersImport
|
||||
}
|
||||
'/posts/': {
|
||||
id: '/posts/'
|
||||
path: '/'
|
||||
fullPath: '/posts/'
|
||||
preLoaderRoute: typeof PostsIndexImport
|
||||
parentRoute: typeof PostsImport
|
||||
}
|
||||
'/users/': {
|
||||
id: '/users/'
|
||||
path: '/'
|
||||
fullPath: '/users/'
|
||||
preLoaderRoute: typeof UsersIndexImport
|
||||
parentRoute: typeof UsersImport
|
||||
}
|
||||
'/_pathlessLayout/_nested-layout/route-a': {
|
||||
id: '/_pathlessLayout/_nested-layout/route-a'
|
||||
path: '/route-a'
|
||||
fullPath: '/route-a'
|
||||
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteAImport
|
||||
parentRoute: typeof PathlessLayoutNestedLayoutImport
|
||||
}
|
||||
'/_pathlessLayout/_nested-layout/route-b': {
|
||||
id: '/_pathlessLayout/_nested-layout/route-b'
|
||||
path: '/route-b'
|
||||
fullPath: '/route-b'
|
||||
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBImport
|
||||
parentRoute: typeof PathlessLayoutNestedLayoutImport
|
||||
}
|
||||
'/posts_/$postId/deep': {
|
||||
id: '/posts_/$postId/deep'
|
||||
path: '/posts/$postId/deep'
|
||||
fullPath: '/posts/$postId/deep'
|
||||
preLoaderRoute: typeof PostsPostIdDeepImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
interface PathlessLayoutNestedLayoutRouteChildren {
|
||||
PathlessLayoutNestedLayoutRouteARoute: typeof PathlessLayoutNestedLayoutRouteARoute
|
||||
PathlessLayoutNestedLayoutRouteBRoute: typeof PathlessLayoutNestedLayoutRouteBRoute
|
||||
}
|
||||
|
||||
const PathlessLayoutNestedLayoutRouteChildren: PathlessLayoutNestedLayoutRouteChildren =
|
||||
{
|
||||
PathlessLayoutNestedLayoutRouteARoute:
|
||||
PathlessLayoutNestedLayoutRouteARoute,
|
||||
PathlessLayoutNestedLayoutRouteBRoute:
|
||||
PathlessLayoutNestedLayoutRouteBRoute,
|
||||
}
|
||||
|
||||
const PathlessLayoutNestedLayoutRouteWithChildren =
|
||||
PathlessLayoutNestedLayoutRoute._addFileChildren(
|
||||
PathlessLayoutNestedLayoutRouteChildren,
|
||||
)
|
||||
|
||||
interface PathlessLayoutRouteChildren {
|
||||
PathlessLayoutNestedLayoutRoute: typeof PathlessLayoutNestedLayoutRouteWithChildren
|
||||
}
|
||||
|
||||
const PathlessLayoutRouteChildren: PathlessLayoutRouteChildren = {
|
||||
PathlessLayoutNestedLayoutRoute: PathlessLayoutNestedLayoutRouteWithChildren,
|
||||
}
|
||||
|
||||
const PathlessLayoutRouteWithChildren = PathlessLayoutRoute._addFileChildren(
|
||||
PathlessLayoutRouteChildren,
|
||||
)
|
||||
|
||||
interface PostsRouteChildren {
|
||||
PostsPostIdRoute: typeof PostsPostIdRoute
|
||||
PostsIndexRoute: typeof PostsIndexRoute
|
||||
}
|
||||
|
||||
const PostsRouteChildren: PostsRouteChildren = {
|
||||
PostsPostIdRoute: PostsPostIdRoute,
|
||||
PostsIndexRoute: PostsIndexRoute,
|
||||
}
|
||||
|
||||
const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
|
||||
|
||||
interface UsersRouteChildren {
|
||||
UsersUserIdRoute: typeof UsersUserIdRoute
|
||||
UsersIndexRoute: typeof UsersIndexRoute
|
||||
}
|
||||
|
||||
const UsersRouteChildren: UsersRouteChildren = {
|
||||
UsersUserIdRoute: UsersUserIdRoute,
|
||||
UsersIndexRoute: UsersIndexRoute,
|
||||
}
|
||||
|
||||
const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'': typeof PathlessLayoutNestedLayoutRouteWithChildren
|
||||
'/deferred': typeof DeferredRoute
|
||||
'/posts': typeof PostsRouteWithChildren
|
||||
'/redirect': typeof RedirectRoute
|
||||
'/users': typeof UsersRouteWithChildren
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts/': typeof PostsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
|
||||
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
|
||||
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'': typeof PathlessLayoutNestedLayoutRouteWithChildren
|
||||
'/deferred': typeof DeferredRoute
|
||||
'/redirect': typeof RedirectRoute
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts': typeof PostsIndexRoute
|
||||
'/users': typeof UsersIndexRoute
|
||||
'/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
|
||||
'/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
|
||||
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexRoute
|
||||
'/_pathlessLayout': typeof PathlessLayoutRouteWithChildren
|
||||
'/deferred': typeof DeferredRoute
|
||||
'/posts': typeof PostsRouteWithChildren
|
||||
'/redirect': typeof RedirectRoute
|
||||
'/users': typeof UsersRouteWithChildren
|
||||
'/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts/': typeof PostsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute
|
||||
'/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute
|
||||
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| ''
|
||||
| '/deferred'
|
||||
| '/posts'
|
||||
| '/redirect'
|
||||
| '/users'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts/'
|
||||
| '/users/'
|
||||
| '/route-a'
|
||||
| '/route-b'
|
||||
| '/posts/$postId/deep'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| ''
|
||||
| '/deferred'
|
||||
| '/redirect'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts'
|
||||
| '/users'
|
||||
| '/route-a'
|
||||
| '/route-b'
|
||||
| '/posts/$postId/deep'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/_pathlessLayout'
|
||||
| '/deferred'
|
||||
| '/posts'
|
||||
| '/redirect'
|
||||
| '/users'
|
||||
| '/_pathlessLayout/_nested-layout'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts/'
|
||||
| '/users/'
|
||||
| '/_pathlessLayout/_nested-layout/route-a'
|
||||
| '/_pathlessLayout/_nested-layout/route-b'
|
||||
| '/posts_/$postId/deep'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren
|
||||
DeferredRoute: typeof DeferredRoute
|
||||
PostsRoute: typeof PostsRouteWithChildren
|
||||
RedirectRoute: typeof RedirectRoute
|
||||
UsersRoute: typeof UsersRouteWithChildren
|
||||
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
PathlessLayoutRoute: PathlessLayoutRouteWithChildren,
|
||||
DeferredRoute: DeferredRoute,
|
||||
PostsRoute: PostsRouteWithChildren,
|
||||
RedirectRoute: RedirectRoute,
|
||||
UsersRoute: UsersRouteWithChildren,
|
||||
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
"__root__": {
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/_pathlessLayout",
|
||||
"/deferred",
|
||||
"/posts",
|
||||
"/redirect",
|
||||
"/users",
|
||||
"/posts_/$postId/deep"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/_pathlessLayout": {
|
||||
"filePath": "_pathlessLayout.tsx",
|
||||
"children": [
|
||||
"/_pathlessLayout/_nested-layout"
|
||||
]
|
||||
},
|
||||
"/deferred": {
|
||||
"filePath": "deferred.tsx"
|
||||
},
|
||||
"/posts": {
|
||||
"filePath": "posts.tsx",
|
||||
"children": [
|
||||
"/posts/$postId",
|
||||
"/posts/"
|
||||
]
|
||||
},
|
||||
"/redirect": {
|
||||
"filePath": "redirect.tsx"
|
||||
},
|
||||
"/users": {
|
||||
"filePath": "users.tsx",
|
||||
"children": [
|
||||
"/users/$userId",
|
||||
"/users/"
|
||||
]
|
||||
},
|
||||
"/_pathlessLayout/_nested-layout": {
|
||||
"filePath": "_pathlessLayout/_nested-layout.tsx",
|
||||
"parent": "/_pathlessLayout",
|
||||
"children": [
|
||||
"/_pathlessLayout/_nested-layout/route-a",
|
||||
"/_pathlessLayout/_nested-layout/route-b"
|
||||
]
|
||||
},
|
||||
"/posts/$postId": {
|
||||
"filePath": "posts.$postId.tsx",
|
||||
"parent": "/posts"
|
||||
},
|
||||
"/users/$userId": {
|
||||
"filePath": "users.$userId.tsx",
|
||||
"parent": "/users"
|
||||
},
|
||||
"/posts/": {
|
||||
"filePath": "posts.index.tsx",
|
||||
"parent": "/posts"
|
||||
},
|
||||
"/users/": {
|
||||
"filePath": "users.index.tsx",
|
||||
"parent": "/users"
|
||||
},
|
||||
"/_pathlessLayout/_nested-layout/route-a": {
|
||||
"filePath": "_pathlessLayout/_nested-layout/route-a.tsx",
|
||||
"parent": "/_pathlessLayout/_nested-layout"
|
||||
},
|
||||
"/_pathlessLayout/_nested-layout/route-b": {
|
||||
"filePath": "_pathlessLayout/_nested-layout/route-b.tsx",
|
||||
"parent": "/_pathlessLayout/_nested-layout"
|
||||
},
|
||||
"/posts_/$postId/deep": {
|
||||
"filePath": "posts_.$postId.deep.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
ROUTE_MANIFEST_END */
|
||||
22
tanstack/src/router.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
|
||||
import { NotFound } from './components/NotFound'
|
||||
|
||||
export function createRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
defaultNotFoundComponent: () => <NotFound />,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof createRouter>
|
||||
}
|
||||
}
|
||||
139
tanstack/src/routes/__root.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
HeadContent,
|
||||
Link,
|
||||
Outlet,
|
||||
Scripts,
|
||||
createRootRoute,
|
||||
} from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
||||
import * as React from 'react'
|
||||
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
|
||||
import { NotFound } from '~/components/NotFound'
|
||||
import appCss from '~/styles/app.css?url'
|
||||
import { seo } from '~/utils/seo'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
...seo({
|
||||
title:
|
||||
'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework',
|
||||
description: `TanStack Start is a type-safe, client-first, full-stack React framework. `,
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
{ rel: 'stylesheet', href: appCss },
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: '/apple-touch-icon.png',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
href: '/favicon-32x32.png',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
href: '/favicon-16x16.png',
|
||||
},
|
||||
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
|
||||
{ rel: 'icon', href: '/favicon.ico' },
|
||||
],
|
||||
}),
|
||||
errorComponent: (props) => {
|
||||
return (
|
||||
<RootDocument>
|
||||
<DefaultCatchBoundary {...props} />
|
||||
</RootDocument>
|
||||
)
|
||||
},
|
||||
notFoundComponent: () => <NotFound />,
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
)
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<div className="p-2 flex gap-2 text-lg">
|
||||
<Link
|
||||
to="/"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
Home
|
||||
</Link>{' '}
|
||||
<Link
|
||||
to="/posts"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Posts
|
||||
</Link>{' '}
|
||||
<Link
|
||||
to="/users"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</Link>{' '}
|
||||
<Link
|
||||
to="/route-a"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Pathless Layout
|
||||
</Link>{' '}
|
||||
<Link
|
||||
to="/deferred"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Deferred
|
||||
</Link>{' '}
|
||||
<Link
|
||||
// @ts-expect-error
|
||||
to="/this-route-does-not-exist"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
This Route Does Not Exist
|
||||
</Link>
|
||||
</div>
|
||||
<hr />
|
||||
{children}
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
16
tanstack/src/routes/_pathlessLayout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Outlet, createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_pathlessLayout')({
|
||||
component: LayoutComponent,
|
||||
})
|
||||
|
||||
function LayoutComponent() {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div className="border-b">I'm a layout</div>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
tanstack/src/routes/_pathlessLayout/_nested-layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({
|
||||
component: LayoutComponent,
|
||||
})
|
||||
|
||||
function LayoutComponent() {
|
||||
return (
|
||||
<div>
|
||||
<div>I'm a nested layout</div>
|
||||
<div className="flex gap-2 border-b">
|
||||
<Link
|
||||
to="/route-a"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Go to route A
|
||||
</Link>
|
||||
<Link
|
||||
to="/route-b"
|
||||
activeProps={{
|
||||
className: 'font-bold',
|
||||
}}
|
||||
>
|
||||
Go to route B
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')(
|
||||
{
|
||||
component: LayoutAComponent,
|
||||
},
|
||||
)
|
||||
|
||||
function LayoutAComponent() {
|
||||
return <div>I'm A!</div>
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')(
|
||||
{
|
||||
component: LayoutBComponent,
|
||||
},
|
||||
)
|
||||
|
||||
function LayoutBComponent() {
|
||||
return <div>I'm B!</div>
|
||||
}
|
||||
24
tanstack/src/routes/api/users.$id.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { json } from '@tanstack/react-start'
|
||||
import { createAPIFileRoute } from '@tanstack/react-start/api'
|
||||
import axios from 'redaxios'
|
||||
import type { User } from '../../utils/users'
|
||||
|
||||
export const APIRoute = createAPIFileRoute('/api/users/$id')({
|
||||
GET: async ({ request, params }) => {
|
||||
console.info(`Fetching users by id=${params.id}... @`, request.url)
|
||||
try {
|
||||
const res = await axios.get<User>(
|
||||
'https://jsonplaceholder.typicode.com/users/' + params.id,
|
||||
)
|
||||
|
||||
return json({
|
||||
id: res.data.id,
|
||||
name: res.data.name,
|
||||
email: res.data.email,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
},
|
||||
})
|
||||
17
tanstack/src/routes/api/users.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { json } from '@tanstack/react-start'
|
||||
import { createAPIFileRoute } from '@tanstack/react-start/api'
|
||||
import axios from 'redaxios'
|
||||
import type { User } from '../../utils/users'
|
||||
|
||||
export const APIRoute = createAPIFileRoute('/api/users')({
|
||||
GET: async ({ request }) => {
|
||||
console.info('Fetching users... @', request.url)
|
||||
const res = await axios.get<Array<User>>(
|
||||
'https://jsonplaceholder.typicode.com/users',
|
||||
)
|
||||
|
||||
const list = res.data.slice(0, 10)
|
||||
|
||||
return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email })))
|
||||
},
|
||||
})
|
||||
62
tanstack/src/routes/deferred.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Await, createFileRoute } from '@tanstack/react-router'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import { Suspense, useState } from 'react'
|
||||
|
||||
const personServerFn = createServerFn({ method: 'GET' })
|
||||
.validator((d: string) => d)
|
||||
.handler(({ data: name }) => {
|
||||
return { name, randomNumber: Math.floor(Math.random() * 100) }
|
||||
})
|
||||
|
||||
const slowServerFn = createServerFn({ method: 'GET' })
|
||||
.validator((d: string) => d)
|
||||
.handler(async ({ data: name }) => {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
return { name, randomNumber: Math.floor(Math.random() * 100) }
|
||||
})
|
||||
|
||||
export const Route = createFileRoute('/deferred')({
|
||||
loader: async () => {
|
||||
return {
|
||||
deferredStuff: new Promise<string>((r) =>
|
||||
setTimeout(() => r('Hello deferred!'), 2000),
|
||||
),
|
||||
deferredPerson: slowServerFn({ data: 'Tanner Linsley' }),
|
||||
person: await personServerFn({ data: 'John Doe' }),
|
||||
}
|
||||
},
|
||||
component: Deferred,
|
||||
})
|
||||
|
||||
function Deferred() {
|
||||
const [count, setCount] = useState(0)
|
||||
const { deferredStuff, deferredPerson, person } = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<div data-testid="regular-person">
|
||||
{person.name} - {person.randomNumber}
|
||||
</div>
|
||||
<Suspense fallback={<div>Loading person...</div>}>
|
||||
<Await
|
||||
promise={deferredPerson}
|
||||
children={(data) => (
|
||||
<div data-testid="deferred-person">
|
||||
{data.name} - {data.randomNumber}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Suspense>
|
||||
<Suspense fallback={<div>Loading stuff...</div>}>
|
||||
<Await
|
||||
promise={deferredStuff}
|
||||
children={(data) => <h3 data-testid="deferred-stuff">{data}</h3>}
|
||||
/>
|
||||
</Suspense>
|
||||
<div>Count: {count}</div>
|
||||
<div>
|
||||
<button onClick={() => setCount(count + 1)}>Increment</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
tanstack/src/routes/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Home,
|
||||
})
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!!!</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
tanstack/src/routes/posts.$postId.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router'
|
||||
import { fetchPost } from '../utils/posts'
|
||||
import { NotFound } from '~/components/NotFound'
|
||||
import { PostErrorComponent } from '~/components/PostError'
|
||||
|
||||
export const Route = createFileRoute('/posts/$postId')({
|
||||
loader: ({ params: { postId } }) => fetchPost({ data: postId }),
|
||||
errorComponent: PostErrorComponent,
|
||||
component: PostComponent,
|
||||
notFoundComponent: () => {
|
||||
return <NotFound>Post not found</NotFound>
|
||||
},
|
||||
})
|
||||
|
||||
function PostComponent() {
|
||||
const post = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-bold underline">{post.title}</h4>
|
||||
<div className="text-sm">{post.body}</div>
|
||||
<Link
|
||||
to="/posts/$postId/deep"
|
||||
params={{
|
||||
postId: post.id,
|
||||
}}
|
||||
activeProps={{ className: 'text-black font-bold' }}
|
||||
className="block py-1 text-blue-800 hover:text-blue-600"
|
||||
>
|
||||
Deep View
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
tanstack/src/routes/posts.index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/posts/')({
|
||||
component: PostsIndexComponent,
|
||||
})
|
||||
|
||||
function PostsIndexComponent() {
|
||||
return <div>Select a post.</div>
|
||||
}
|
||||
38
tanstack/src/routes/posts.route.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
|
||||
import { fetchPosts } from '../utils/posts'
|
||||
|
||||
export const Route = createFileRoute('/posts')({
|
||||
loader: async () => fetchPosts(),
|
||||
component: PostsLayoutComponent,
|
||||
})
|
||||
|
||||
function PostsLayoutComponent() {
|
||||
const posts = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="p-2 flex gap-2">
|
||||
<ul className="list-disc pl-4">
|
||||
{[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
|
||||
(post) => {
|
||||
return (
|
||||
<li key={post.id} className="whitespace-nowrap">
|
||||
<Link
|
||||
to="/posts/$postId"
|
||||
params={{
|
||||
postId: post.id,
|
||||
}}
|
||||
className="block py-1 text-blue-800 hover:text-blue-600"
|
||||
activeProps={{ className: 'text-black font-bold' }}
|
||||
>
|
||||
<div>{post.title.substring(0, 20)}</div>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
<hr />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
29
tanstack/src/routes/posts_.$postId.deep.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link, createFileRoute } from '@tanstack/react-router'
|
||||
import { fetchPost } from '../utils/posts'
|
||||
import { PostErrorComponent } from '~/components/PostError'
|
||||
|
||||
export const Route = createFileRoute('/posts_/$postId/deep')({
|
||||
loader: async ({ params: { postId } }) =>
|
||||
fetchPost({
|
||||
data: postId,
|
||||
}),
|
||||
errorComponent: PostErrorComponent,
|
||||
component: PostDeepComponent,
|
||||
})
|
||||
|
||||
function PostDeepComponent() {
|
||||
const post = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="p-2 space-y-2">
|
||||
<Link
|
||||
to="/posts"
|
||||
className="block py-1 text-blue-800 hover:text-blue-600"
|
||||
>
|
||||
← All Posts
|
||||
</Link>
|
||||
<h4 className="text-xl font-bold underline">{post.title}</h4>
|
||||
<div className="text-sm">{post.body}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
tanstack/src/routes/redirect.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/redirect')({
|
||||
beforeLoad: async () => {
|
||||
throw redirect({
|
||||
to: '/posts',
|
||||
})
|
||||
},
|
||||
})
|
||||
33
tanstack/src/routes/users.$userId.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import axios from 'redaxios'
|
||||
import type { User } from '~/utils/users'
|
||||
import { DEPLOY_URL } from '~/utils/users'
|
||||
import { NotFound } from '~/components/NotFound'
|
||||
import { UserErrorComponent } from '~/components/UserError'
|
||||
|
||||
export const Route = createFileRoute('/users/$userId')({
|
||||
loader: async ({ params: { userId } }) => {
|
||||
return await axios
|
||||
.get<User>(DEPLOY_URL + '/api/users/' + userId)
|
||||
.then((r) => r.data)
|
||||
.catch(() => {
|
||||
throw new Error('Failed to fetch user')
|
||||
})
|
||||
},
|
||||
errorComponent: UserErrorComponent,
|
||||
component: UserComponent,
|
||||
notFoundComponent: () => {
|
||||
return <NotFound>User not found</NotFound>
|
||||
},
|
||||
})
|
||||
|
||||
function UserComponent() {
|
||||
const user = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xl font-bold underline">{user.name}</h4>
|
||||
<div className="text-sm">{user.email}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
tanstack/src/routes/users.index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/users/')({
|
||||
component: UsersIndexComponent,
|
||||
})
|
||||
|
||||
function UsersIndexComponent() {
|
||||
return <div>Select a user.</div>
|
||||
}
|
||||
48
tanstack/src/routes/users.route.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
|
||||
import axios from 'redaxios'
|
||||
import { DEPLOY_URL } from '../utils/users'
|
||||
import type { User } from '../utils/users'
|
||||
|
||||
export const Route = createFileRoute('/users')({
|
||||
loader: async () => {
|
||||
return await axios
|
||||
.get<Array<User>>(DEPLOY_URL + '/api/users')
|
||||
.then((r) => r.data)
|
||||
.catch(() => {
|
||||
throw new Error('Failed to fetch users')
|
||||
})
|
||||
},
|
||||
component: UsersLayoutComponent,
|
||||
})
|
||||
|
||||
function UsersLayoutComponent() {
|
||||
const users = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="p-2 flex gap-2">
|
||||
<ul className="list-disc pl-4">
|
||||
{[
|
||||
...users,
|
||||
{ id: 'i-do-not-exist', name: 'Non-existent User', email: '' },
|
||||
].map((user) => {
|
||||
return (
|
||||
<li key={user.id} className="whitespace-nowrap">
|
||||
<Link
|
||||
to="/users/$userId"
|
||||
params={{
|
||||
userId: String(user.id),
|
||||
}}
|
||||
className="block py-1 text-blue-800 hover:text-blue-600"
|
||||
activeProps={{ className: 'text-black font-bold' }}
|
||||
>
|
||||
<div>{user.name}</div>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<hr />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
tanstack/src/ssr.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
/// <reference types="vinxi/types/server" />
|
||||
import {
|
||||
createStartHandler,
|
||||
defaultStreamHandler,
|
||||
} from '@tanstack/react-start/server'
|
||||
import { getRouterManifest } from '@tanstack/react-start/router-manifest'
|
||||
|
||||
import { createRouter } from './router'
|
||||
|
||||
export default createStartHandler({
|
||||
createRouter,
|
||||
getRouterManifest,
|
||||
})(defaultStreamHandler)
|
||||
22
tanstack/src/styles/app.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-gray-200 dark:border-gray-800;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200;
|
||||
}
|
||||
|
||||
.using-mouse * {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
41
tanstack/src/utils/loggingMiddleware.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createMiddleware } from '@tanstack/react-start'
|
||||
|
||||
const preLogMiddleware = createMiddleware()
|
||||
.client(async (ctx) => {
|
||||
const clientTime = new Date()
|
||||
|
||||
return ctx.next({
|
||||
context: {
|
||||
clientTime,
|
||||
},
|
||||
sendContext: {
|
||||
clientTime,
|
||||
},
|
||||
})
|
||||
})
|
||||
.server(async (ctx) => {
|
||||
const serverTime = new Date()
|
||||
|
||||
return ctx.next({
|
||||
sendContext: {
|
||||
serverTime,
|
||||
durationToServer:
|
||||
serverTime.getTime() - ctx.context.clientTime.getTime(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const logMiddleware = createMiddleware()
|
||||
.middleware([preLogMiddleware])
|
||||
.client(async (ctx) => {
|
||||
const res = await ctx.next()
|
||||
|
||||
const now = new Date()
|
||||
console.log('Client Req/Res:', {
|
||||
duration: res.context.clientTime.getTime() - now.getTime(),
|
||||
durationToServer: res.context.durationToServer,
|
||||
durationFromServer: now.getTime() - res.context.serverTime.getTime(),
|
||||
})
|
||||
|
||||
return res
|
||||
})
|
||||
36
tanstack/src/utils/posts.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { notFound } from '@tanstack/react-router'
|
||||
import { createServerFn } from '@tanstack/react-start'
|
||||
import axios from 'redaxios'
|
||||
|
||||
export type PostType = {
|
||||
id: string
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export const fetchPost = createServerFn({ method: 'GET' })
|
||||
.validator((d: string) => d)
|
||||
.handler(async ({ data }) => {
|
||||
console.info(`Fetching post with id ${data}...`)
|
||||
const post = await axios
|
||||
.get<PostType>(`https://jsonplaceholder.typicode.com/posts/${data}`)
|
||||
.then((r) => r.data)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
if (err.status === 404) {
|
||||
throw notFound()
|
||||
}
|
||||
throw err
|
||||
})
|
||||
|
||||
return post
|
||||
})
|
||||
|
||||
export const fetchPosts = createServerFn({ method: 'GET' }).handler(
|
||||
async () => {
|
||||
console.info('Fetching posts...')
|
||||
return axios
|
||||
.get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts')
|
||||
.then((r) => r.data.slice(0, 10))
|
||||
},
|
||||
)
|
||||
33
tanstack/src/utils/seo.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const seo = ({
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
image,
|
||||
}: {
|
||||
title: string
|
||||
description?: string
|
||||
image?: string
|
||||
keywords?: string
|
||||
}) => {
|
||||
const tags = [
|
||||
{ title },
|
||||
{ name: 'description', content: description },
|
||||
{ name: 'keywords', content: keywords },
|
||||
{ name: 'twitter:title', content: title },
|
||||
{ name: 'twitter:description', content: description },
|
||||
{ name: 'twitter:creator', content: '@tannerlinsley' },
|
||||
{ name: 'twitter:site', content: '@tannerlinsley' },
|
||||
{ name: 'og:type', content: 'website' },
|
||||
{ name: 'og:title', content: title },
|
||||
{ name: 'og:description', content: description },
|
||||
...(image
|
||||
? [
|
||||
{ name: 'twitter:image', content: image },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'og:image', content: image },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return tags
|
||||
}
|
||||
7
tanstack/src/utils/users.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export type User = {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export const DEPLOY_URL = 'http://localhost:3000'
|
||||
4
tanstack/tailwind.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
}
|
||||
22
tanstack/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||