Add React frontend with TanStack and Material UI

pull/34/head
Jaronim Pracht 2025-05-22 11:53:15 +02:00
parent 3cac55fe00
commit 83afee2a25
25 changed files with 820 additions and 0 deletions

View File

@ -0,0 +1,15 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*

5
project/frontend/.gitignore vendored 100644
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@ -0,0 +1,35 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}

View File

@ -0,0 +1,15 @@
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
# install dependencies into temp directory
# this will cache them and speed up future builds
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM nginx:1.28.0-alpine3.21
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=base /usr/src/app/dist /usr/share/nginx/html

View File

@ -0,0 +1,295 @@
Welcome to your new TanStack app!
# Getting Started
To run this application:
```bash
bun install
bunx --bun run start
```
# Building For Production
To build this application for production:
```bash
bunx --bun run build
```
## Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
```bash
bunx --bun run test
```
## Linting & Formatting
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
```bash
bunx --bun run lint
bunx --bun run format
bunx --bun 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
bun 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
bun 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).

View File

@ -0,0 +1,31 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["src/routeTree.gen.ts"],
"include": ["src/*", ".vscode/*", "index.html", "vite.config.js"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Caching configuration for static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Always serve index.html for any request
location / {
try_files $uri $uri/ /index.html;
}
# Error handling
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

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

View File

@ -0,0 +1,42 @@
{
"name": "frontend",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"start": "vite",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run",
"format": "biome format --write",
"lint": "biome lint",
"check": "biome check"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@tanstack/react-query": "^5.66.5",
"@tanstack/react-query-devtools": "^5.66.5",
"@tanstack/react-router": "^1.114.3",
"@tanstack/react-router-devtools": "^1.114.3",
"@tanstack/router-plugin": "^1.114.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-pdf": "^9.2.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^26.0.0",
"typescript": "^5.7.2",
"vite": "^6.1.0",
"vitest": "^3.0.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

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

View File

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

View File

@ -0,0 +1,17 @@
import { Link } from "@tanstack/react-router";
export default function Header() {
return (
<header className="p-2 flex gap-2 bg-white text-black justify-between">
<nav className="flex flex-row">
<div className="px-2 font-bold">
<Link to="/">Home</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/tanstack-query">TanStack Query</Link>
</div>
</nav>
</header>
);
}

View File

@ -0,0 +1,5 @@
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
export default function LayoutAddition() {
return <ReactQueryDevtools buttonPosition="bottom-right" />;
}

View File

@ -0,0 +1,15 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function getContext() {
return {
queryClient,
};
}
export function Provider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View File

@ -0,0 +1,63 @@
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import * as TanStackQueryProvider from "./integrations/tanstack-query/root-provider.tsx";
import { pdfjs } from "react-pdf";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";
// Create a new router instance
const router = createRouter({
routeTree,
context: {
...TanStackQueryProvider.getContext(),
},
defaultPreload: "intent",
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
});
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
// Initialize PDF.js worker
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url,
).toString();
const darkTheme = createTheme({
palette: {
mode: "dark",
},
});
// Render the app
const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<TanStackQueryProvider.Provider>
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<RouterProvider router={router} />
</ThemeProvider>
</TanStackQueryProvider.Provider>
</StrictMode>,
);
}

View File

@ -0,0 +1,111 @@
/* 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 DemoImport } from './routes/demo'
import { Route as IndexImport } from './routes/index'
// Create/Update Routes
const DemoRoute = DemoImport.update({
id: '/demo',
path: '/demo',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/demo': {
id: '/demo'
path: '/demo'
fullPath: '/demo'
preLoaderRoute: typeof DemoImport
parentRoute: typeof rootRoute
}
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/demo': typeof DemoRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/demo'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/demo'
id: '__root__' | '/' | '/demo'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DemoRoute: typeof DemoRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DemoRoute: DemoRoute,
}
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/demo"
]
},
"/": {
"filePath": "index.tsx"
},
"/demo": {
"filePath": "demo.tsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@ -0,0 +1,25 @@
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
// import Header from "../components/Header";
import TanStackQueryLayout from "../integrations/tanstack-query/layout.tsx";
import type { QueryClient } from "@tanstack/react-query";
interface MyRouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => (
<>
{/* <Header /> */}
<Outlet />
<TanStackRouterDevtools />
<TanStackQueryLayout />
</>
),
});

View File

@ -0,0 +1,26 @@
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/demo")({
component: TanStackQueryDemo,
});
function TanStackQueryDemo() {
const { data } = useQuery({
queryKey: ["people"],
queryFn: () =>
Promise.resolve([{ name: "John Doe" }, { name: "Jane Doe" }]),
initialData: [],
});
return (
<div className="p-4">
<h1 className="text-2xl mb-4">People list</h1>
<ul>
{data.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
</div>
);
}

View File

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

View File

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

View File

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