From d6e29a74afc1b73df0203a1dd632fa387884a515 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 26 May 2026 14:59:16 +0200 Subject: [PATCH] feat: integrate TanStack Router and update routing structure - Added TanStack Router for improved routing management. - Created route tree and individual routes for login, index, add-product, inventory, and view-product. - Implemented authentication checks for the inventory route. - Introduced a landing page component. - Updated App component to utilize RouterProvider and ToastContainer for notifications. - Refactored CSS to use Tailwind CSS, removing custom styles. - Added API configuration for backend URL management. - Implemented authentication utilities for user sign-in and sign-out. - Integrated i18next for internationalization with English and German language support. - Created localization files for English and German languages. --- .gitignore | 3 +- backend/routes/app/users.route.js | 6 +- frontend/package-lock.json | 358 ++++++++++++++++++++- frontend/package.json | 18 +- frontend/src/App.css | 185 +---------- frontend/src/App.tsx | 136 ++------ frontend/src/components/LandingPage.tsx | 7 + frontend/src/config/api.config.ts | 4 + frontend/src/index.css | 112 +------ frontend/src/routeTree.gen.ts | 147 +++++++++ frontend/src/routes/__root.tsx | 5 + frontend/src/routes/app/add-product.tsx | 9 + frontend/src/routes/app/inventory.tsx | 17 + frontend/src/routes/app/view-product.tsx | 9 + frontend/src/routes/index.tsx | 9 + frontend/src/routes/login.tsx | 9 + frontend/src/utils/auth.ts | 55 ++++ frontend/src/utils/i18n/index.ts | 34 ++ frontend/src/utils/i18n/locales/de/de.json | 0 frontend/src/utils/i18n/locales/en/en.json | 0 frontend/vite.config.ts | 2 + 21 files changed, 706 insertions(+), 419 deletions(-) create mode 100644 frontend/src/components/LandingPage.tsx create mode 100644 frontend/src/config/api.config.ts create mode 100644 frontend/src/routeTree.gen.ts create mode 100644 frontend/src/routes/__root.tsx create mode 100644 frontend/src/routes/app/add-product.tsx create mode 100644 frontend/src/routes/app/inventory.tsx create mode 100644 frontend/src/routes/app/view-product.tsx create mode 100644 frontend/src/routes/index.tsx create mode 100644 frontend/src/routes/login.tsx create mode 100644 frontend/src/utils/auth.ts create mode 100644 frontend/src/utils/i18n/index.ts create mode 100644 frontend/src/utils/i18n/locales/de/de.json create mode 100644 frontend/src/utils/i18n/locales/en/en.json diff --git a/.gitignore b/.gitignore index d16cc03..f827c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ Temporary Items ToDo.txt .env -.docker/volumes \ No newline at end of file +.docker/volumes +.tanstack/tmp \ No newline at end of file diff --git a/backend/routes/app/users.route.js b/backend/routes/app/users.route.js index b0968d1..c85f339 100644 --- a/backend/routes/app/users.route.js +++ b/backend/routes/app/users.route.js @@ -1,10 +1,14 @@ import express from "express"; import dotenv from "dotenv"; -import { generateToken } from "../../services/tokenService.js"; +import { authenticate, generateToken } from "../../services/tokenService.js"; import { findUser, loginUser } from "./database/users.database.js"; dotenv.config(); const router = express.Router(); +router.post("/verify-token", authenticate, async (req, res) => { + res.status(200); +}); + router.post("/login", async (req, res) => { const username = req.body.username; const password = req.body.password; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 778b499..4afdf22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,10 +17,12 @@ "@tanstack/react-query": "^5.100.14", "@tanstack/react-router": "^1.170.8", "i18next": "^26.0.10", - "js-cookie": "^3.0.5", + "js-cookie": "^3.0.7", "lucide-react": "^1.14.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-i18next": "^17.0.8", + "react-toastify": "^11.1.0", "tailwindcss": "^4.3.0", "validator": "^13.15.35", "zod": "^4.4.3" @@ -29,7 +31,9 @@ "@babel/core": "^7.29.0", "@eslint/js": "^10.0.1", "@rolldown/plugin-babel": "^0.2.3", + "@tanstack/router-vite-plugin": "^1.167.11", "@types/babel__core": "^7.20.5", + "@types/js-cookie": "^3.0.6", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -176,6 +180,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -233,6 +247,38 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -1752,6 +1798,125 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/router-generator": { + "version": "1.167.10", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.10.tgz", + "integrity": "sha512-CjbjWRSo6djLU/C7ncb9IbKUcf4IwpdqhLGngkwKkXaVFXGxEAafA/uhvOCv/UEUVR7NI3tJqqQmxYXGcJPbjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.171.6", + "@tanstack/router-utils": "1.162.1", + "@tanstack/virtual-file-routes": "1.162.0", + "jiti": "^2.7.0", + "magic-string": "^0.30.21", + "prettier": "^3.5.0", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.168.11", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.11.tgz", + "integrity": "sha512-b2eom/8xCWL/OiWxKub8kYsr8p+kvmB/eXwYGqCWG8vilcJo+eQCSyp54nKt0AZ5k/ET1+eINc+4mwL3bVeAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.171.6", + "@tanstack/router-generator": "1.167.10", + "@tanstack/router-utils": "1.162.1", + "@tanstack/virtual-file-routes": "1.162.0", + "chokidar": "^5.0.0", + "unplugin": "^3.0.0", + "zod": "^4.4.3" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2 || ^2.0.0", + "@tanstack/react-router": "^1.170.8", + "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", + "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.162.1", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.162.1.tgz", + "integrity": "sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ansis": "^4.1.0", + "babel-dead-code-elimination": "^1.0.12", + "diff": "^8.0.2", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.15" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-vite-plugin": { + "version": "1.167.11", + "resolved": "https://registry.npmjs.org/@tanstack/router-vite-plugin/-/router-vite-plugin-1.167.11.tgz", + "integrity": "sha512-kzKQgwUfxPD8dLs9XEao5Yze5am+8gnRmzNrjOMCiDnlQ0ilEWi+QTDmMdj+Zqnj+IzblwT8Fx9Jl+YSvMZRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/router-plugin": "1.168.11" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/store": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", @@ -1762,6 +1927,20 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.162.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.162.0.tgz", + "integrity": "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1831,6 +2010,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2189,6 +2375,29 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.0.tgz", + "integrity": "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2314,6 +2523,22 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2415,6 +2640,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.361", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", @@ -2861,6 +3096,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/i18next": { "version": "26.2.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz", @@ -3589,6 +3833,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3645,6 +3896,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3693,12 +3960,66 @@ "react": "^19.2.6" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", "license": "MIT" }, + "node_modules/react-toastify": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.1.0.tgz", + "integrity": "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -3971,6 +4292,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/unplugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4107,6 +4443,22 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a264abb..17bece3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,24 +18,28 @@ "@emotion/styled": "^11.14.1", "@fontsource/inter": "^5.2.8", "@mui/joy": "^5.0.0-beta.52", + "@tailwindcss/vite": "^4.3.0", "@tanstack/react-form": "^1.32.0", "@tanstack/react-query": "^5.100.14", "@tanstack/react-router": "^1.170.8", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "@tailwindcss/vite": "^4.3.0", "i18next": "^26.0.10", - "js-cookie": "^3.0.5", + "js-cookie": "^3.0.7", + "lucide-react": "^1.14.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-i18next": "^17.0.8", + "react-toastify": "^11.1.0", "tailwindcss": "^4.3.0", "validator": "^13.15.35", - "zod": "^4.4.3", - "lucide-react": "^1.14.0" + "zod": "^4.4.3" }, "devDependencies": { "@babel/core": "^7.29.0", "@eslint/js": "^10.0.1", "@rolldown/plugin-babel": "^0.2.3", + "@tanstack/router-vite-plugin": "^1.167.11", "@types/babel__core": "^7.20.5", + "@types/js-cookie": "^3.0.6", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -49,4 +53,4 @@ "typescript-eslint": "^8.59.2", "vite": "^8.0.12" } -} \ No newline at end of file +} diff --git a/frontend/src/App.css b/frontend/src/App.css index f90339d..a461c50 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,184 +1 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} +@import "tailwindcss"; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1fd432d..7a0a92d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,120 +1,32 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import viteLogo from "./assets/vite.svg"; -import heroImg from "./assets/hero.png"; +import { createRouter, RouterProvider } from "@tanstack/react-router"; import "./App.css"; +import { ToastContainer } from "react-toastify"; +import { routeTree } from "./routeTree.gen"; + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} function App() { - const [count, setCount] = useState(0); - return ( <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.tsx and save to test HMR -

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
+ + ); } diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx new file mode 100644 index 0000000..a8155b9 --- /dev/null +++ b/frontend/src/components/LandingPage.tsx @@ -0,0 +1,7 @@ +export const LandingPage = () => { + return ( + <> +

Landing Page

+ + ); +}; diff --git a/frontend/src/config/api.config.ts b/frontend/src/config/api.config.ts new file mode 100644 index 0000000..fc9d0f3 --- /dev/null +++ b/frontend/src/config/api.config.ts @@ -0,0 +1,4 @@ +export const API_BASE = + (import.meta as any).env?.VITE_BACKEND_URL || + import.meta.env.VITE_BACKEND_URL || + "http://localhost:8004"; diff --git a/frontend/src/index.css b/frontend/src/index.css index 5fb3313..a461c50 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,111 +1 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -body { - margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} +@import "tailwindcss"; \ No newline at end of file diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts new file mode 100644 index 0000000..dfe1a17 --- /dev/null +++ b/frontend/src/routeTree.gen.ts @@ -0,0 +1,147 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as LoginRouteImport } from './routes/login' +import { Route as IndexRouteImport } from './routes/index' +import { Route as AppViewProductRouteImport } from './routes/app/view-product' +import { Route as AppInventoryRouteImport } from './routes/app/inventory' +import { Route as AppAddProductRouteImport } from './routes/app/add-product' + +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const AppViewProductRoute = AppViewProductRouteImport.update({ + id: '/app/view-product', + path: '/app/view-product', + getParentRoute: () => rootRouteImport, +} as any) +const AppInventoryRoute = AppInventoryRouteImport.update({ + id: '/app/inventory', + path: '/app/inventory', + getParentRoute: () => rootRouteImport, +} as any) +const AppAddProductRoute = AppAddProductRouteImport.update({ + id: '/app/add-product', + path: '/app/add-product', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/app/add-product': typeof AppAddProductRoute + '/app/inventory': typeof AppInventoryRoute + '/app/view-product': typeof AppViewProductRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/app/add-product': typeof AppAddProductRoute + '/app/inventory': typeof AppInventoryRoute + '/app/view-product': typeof AppViewProductRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/app/add-product': typeof AppAddProductRoute + '/app/inventory': typeof AppInventoryRoute + '/app/view-product': typeof AppViewProductRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/login' + | '/app/add-product' + | '/app/inventory' + | '/app/view-product' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/login' + | '/app/add-product' + | '/app/inventory' + | '/app/view-product' + id: + | '__root__' + | '/' + | '/login' + | '/app/add-product' + | '/app/inventory' + | '/app/view-product' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LoginRoute: typeof LoginRoute + AppAddProductRoute: typeof AppAddProductRoute + AppInventoryRoute: typeof AppInventoryRoute + AppViewProductRoute: typeof AppViewProductRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/app/view-product': { + id: '/app/view-product' + path: '/app/view-product' + fullPath: '/app/view-product' + preLoaderRoute: typeof AppViewProductRouteImport + parentRoute: typeof rootRouteImport + } + '/app/inventory': { + id: '/app/inventory' + path: '/app/inventory' + fullPath: '/app/inventory' + preLoaderRoute: typeof AppInventoryRouteImport + parentRoute: typeof rootRouteImport + } + '/app/add-product': { + id: '/app/add-product' + path: '/app/add-product' + fullPath: '/app/add-product' + preLoaderRoute: typeof AppAddProductRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LoginRoute: LoginRoute, + AppAddProductRoute: AppAddProductRoute, + AppInventoryRoute: AppInventoryRoute, + AppViewProductRoute: AppViewProductRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx new file mode 100644 index 0000000..72414ec --- /dev/null +++ b/frontend/src/routes/__root.tsx @@ -0,0 +1,5 @@ +import { createRootRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: () => , +}); diff --git a/frontend/src/routes/app/add-product.tsx b/frontend/src/routes/app/add-product.tsx new file mode 100644 index 0000000..c218e62 --- /dev/null +++ b/frontend/src/routes/app/add-product.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/app/add-product')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/app/add-product"!
+} diff --git a/frontend/src/routes/app/inventory.tsx b/frontend/src/routes/app/inventory.tsx new file mode 100644 index 0000000..da772c9 --- /dev/null +++ b/frontend/src/routes/app/inventory.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { isAuthenticated } from "../../utils/auth"; + +export const Route = createFileRoute("/app/inventory")({ + beforeLoad: () => { + if (!isAuthenticated()) { + throw redirect({ + to: "/login", + }); + } + }, + component: RouteComponent, +}); + +function RouteComponent() { + return
Hello "/app/inventory"!
; +} diff --git a/frontend/src/routes/app/view-product.tsx b/frontend/src/routes/app/view-product.tsx new file mode 100644 index 0000000..1bc3a33 --- /dev/null +++ b/frontend/src/routes/app/view-product.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/app/view-product')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/app/view-product"!
+} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx new file mode 100644 index 0000000..a18157e --- /dev/null +++ b/frontend/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Index "/"!
+} diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx new file mode 100644 index 0000000..f7b477e --- /dev/null +++ b/frontend/src/routes/login.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/login')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/login"!
+} diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts new file mode 100644 index 0000000..b329cd1 --- /dev/null +++ b/frontend/src/utils/auth.ts @@ -0,0 +1,55 @@ +import { API_BASE } from "../config/api.config"; +import Cookies from "js-cookie"; +import { useTranslation } from "react-i18next"; +import { toast } from "react-toastify"; +import { redirect } from "@tanstack/react-router"; + +const { t } = useTranslation(); + +export async function isAuthenticated() { + const result = await fetch(`${API_BASE}/users/verify-token`, { + method: "POST", + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + }); + + if (result.status === 200) { + return true; + } + + return false; +} + +export async function signInUser(username: string, password: string) { + const result = await fetch(`${API_BASE}/users/login`, { + method: "POST", + headers: { + Authorization: `Bearer ${Cookies.get("token") || ""}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ username, password }), + }); + + const response = await result.json(); + + if (result.status === 202) { + Cookies.set("token", response.token); + return true; + } + + if (result.status !== 202) { + Cookies.remove("token"); + toast.error(t(response.code)); + } +} + +export function signOutUser() { + Cookies.remove("token"); + throw redirect({ + to: "/login", + }); +} diff --git a/frontend/src/utils/i18n/index.ts b/frontend/src/utils/i18n/index.ts new file mode 100644 index 0000000..cddd740 --- /dev/null +++ b/frontend/src/utils/i18n/index.ts @@ -0,0 +1,34 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Cookies from "js-cookie"; + +import enLang from "./locales/en/en.json"; +import deLang from "./locales/de/de.json"; + +// the translations +// (tip move them in a JSON file and import them, +// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) +const resources = { + en: { + translation: enLang, + }, + de: { + translation: deLang, + }, +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + fallbackLng: "en", // use en if detected lng is not available + lng: Cookies.get("language") || "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export default i18n; diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/utils/i18n/locales/en/en.json b/frontend/src/utils/i18n/locales/en/en.json new file mode 100644 index 0000000..e69de29 diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 111db15..b87fac5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,12 +2,14 @@ import { defineConfig } from "vite"; import react, { reactCompilerPreset } from "@vitejs/plugin-react"; import babel from "@rolldown/plugin-babel"; import tailwindcss from "@tailwindcss/vite"; +import { TanStackRouterVite } from "@tanstack/router-vite-plugin"; // https://vite.dev/config/ export default defineConfig({ plugins: [ react(), tailwindcss(), + TanStackRouterVite(), babel({ presets: [reactCompilerPreset()] }), ], });