diff --git a/project/frontend/.dockerignore b/project/frontend/.dockerignore new file mode 100644 index 0000000..f965aed --- /dev/null +++ b/project/frontend/.dockerignore @@ -0,0 +1,15 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +Makefile +helm-charts +.env +.editorconfig +.idea +coverage* diff --git a/project/frontend/.gitignore b/project/frontend/.gitignore new file mode 100644 index 0000000..d451ff1 --- /dev/null +++ b/project/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/project/frontend/.vscode/settings.json b/project/frontend/.vscode/settings.json new file mode 100644 index 0000000..b001961 --- /dev/null +++ b/project/frontend/.vscode/settings.json @@ -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" + } +} diff --git a/project/frontend/Dockerfile b/project/frontend/Dockerfile new file mode 100644 index 0000000..617e964 --- /dev/null +++ b/project/frontend/Dockerfile @@ -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 diff --git a/project/frontend/README.md b/project/frontend/README.md new file mode 100644 index 0000000..9c45ff3 --- /dev/null +++ b/project/frontend/README.md @@ -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 +About +``` + +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 `` 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: () => ( + <> +
+ +
+ + + + ), +}) +``` + +The `` 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 ( +
    + {data.results.map((person) => ( +
  • {person.name}
  • + ))} +
+ ); + }, +}); +``` + +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( + + + + ); +} +``` + +You can also add TanStack Query Devtools to the root route (optional). + +```tsx +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const rootRoute = createRootRoute({ + component: () => ( + <> + + + + + ), +}); +``` + +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 ( +
+
    + {data.map((person) => ( +
  • {person.name}
  • + ))} +
+
+ ); +} + +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 ( +
+ +
+ ); +} + +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 ( +
+ +
Doubled - {doubledCount}
+
+ ); +} + +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). diff --git a/project/frontend/biome.json b/project/frontend/biome.json new file mode 100644 index 0000000..55240aa --- /dev/null +++ b/project/frontend/biome.json @@ -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" + } + } +} diff --git a/project/frontend/bun.lockb b/project/frontend/bun.lockb new file mode 100755 index 0000000..776a922 Binary files /dev/null and b/project/frontend/bun.lockb differ diff --git a/project/frontend/docker/nginx.conf b/project/frontend/docker/nginx.conf new file mode 100644 index 0000000..4bd5b3b --- /dev/null +++ b/project/frontend/docker/nginx.conf @@ -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; + } +} \ No newline at end of file diff --git a/project/frontend/index.html b/project/frontend/index.html new file mode 100644 index 0000000..2f632de --- /dev/null +++ b/project/frontend/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + Create TanStack App - frontend + + +
+ + + diff --git a/project/frontend/package.json b/project/frontend/package.json new file mode 100644 index 0000000..cb88c9a --- /dev/null +++ b/project/frontend/package.json @@ -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" + } +} diff --git a/project/frontend/public/favicon.ico b/project/frontend/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/project/frontend/public/favicon.ico differ diff --git a/project/frontend/public/logo192.png b/project/frontend/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/project/frontend/public/logo192.png differ diff --git a/project/frontend/public/logo512.png b/project/frontend/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/project/frontend/public/logo512.png differ diff --git a/project/frontend/public/manifest.json b/project/frontend/public/manifest.json new file mode 100644 index 0000000..078ef50 --- /dev/null +++ b/project/frontend/public/manifest.json @@ -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" +} diff --git a/project/frontend/public/robots.txt b/project/frontend/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/project/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/project/frontend/src/components/Header.tsx b/project/frontend/src/components/Header.tsx new file mode 100644 index 0000000..383cf9e --- /dev/null +++ b/project/frontend/src/components/Header.tsx @@ -0,0 +1,17 @@ +import { Link } from "@tanstack/react-router"; + +export default function Header() { + return ( +
+ +
+ ); +} diff --git a/project/frontend/src/integrations/tanstack-query/layout.tsx b/project/frontend/src/integrations/tanstack-query/layout.tsx new file mode 100644 index 0000000..68b9cdd --- /dev/null +++ b/project/frontend/src/integrations/tanstack-query/layout.tsx @@ -0,0 +1,5 @@ +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +export default function LayoutAddition() { + return ; +} diff --git a/project/frontend/src/integrations/tanstack-query/root-provider.tsx b/project/frontend/src/integrations/tanstack-query/root-provider.tsx new file mode 100644 index 0000000..2fb2aa9 --- /dev/null +++ b/project/frontend/src/integrations/tanstack-query/root-provider.tsx @@ -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 ( + {children} + ); +} diff --git a/project/frontend/src/main.tsx b/project/frontend/src/main.tsx new file mode 100644 index 0000000..ddffcc6 --- /dev/null +++ b/project/frontend/src/main.tsx @@ -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( + + + + + + + + , + ); +} diff --git a/project/frontend/src/routeTree.gen.ts b/project/frontend/src/routeTree.gen.ts new file mode 100644 index 0000000..b19f273 --- /dev/null +++ b/project/frontend/src/routeTree.gen.ts @@ -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() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/", + "/demo" + ] + }, + "/": { + "filePath": "index.tsx" + }, + "/demo": { + "filePath": "demo.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/project/frontend/src/routes/__root.tsx b/project/frontend/src/routes/__root.tsx new file mode 100644 index 0000000..0816bdd --- /dev/null +++ b/project/frontend/src/routes/__root.tsx @@ -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()({ + component: () => ( + <> + {/*
*/} + + + + + + + ), +}); diff --git a/project/frontend/src/routes/demo.tsx b/project/frontend/src/routes/demo.tsx new file mode 100644 index 0000000..8d1e906 --- /dev/null +++ b/project/frontend/src/routes/demo.tsx @@ -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 ( +
+

People list

+
    + {data.map((person) => ( +
  • {person.name}
  • + ))} +
+
+ ); +} diff --git a/project/frontend/src/routes/index.tsx b/project/frontend/src/routes/index.tsx new file mode 100644 index 0000000..7414b29 --- /dev/null +++ b/project/frontend/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: App, +}); + +function App() { + return <>Test; +} diff --git a/project/frontend/tsconfig.json b/project/frontend/tsconfig.json new file mode 100644 index 0000000..7920df9 --- /dev/null +++ b/project/frontend/tsconfig.json @@ -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/*"], + } + } +} diff --git a/project/frontend/vite.config.js b/project/frontend/vite.config.js new file mode 100644 index 0000000..059d9e2 --- /dev/null +++ b/project/frontend/vite.config.js @@ -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", + }, +});