Compare commits
42 Commits
5ba35bb471
...
debian12_v
| Author | SHA1 | Date | |
|---|---|---|---|
| 53cf686746 | |||
| 893a7e041d | |||
| d4b2e8db20 | |||
| b52d707bf5 | |||
| b6ebfcd631 | |||
| 7ecd9dad3f | |||
| 7f9ed23a86 | |||
| 21b152ef2b | |||
| 451a5a92dd | |||
| 85b519c5b1 | |||
| 27db4c7390 | |||
| 1b08344a0f | |||
| a0be100db5 | |||
| 2143d53eb5 | |||
| c4d5ebd9ae | |||
| 938e9000f8 | |||
| 558a8330af | |||
| ad2395f98b | |||
| 51baf8d970 | |||
| d18465ff1d | |||
| b36f1ba9ba | |||
| 784bd1e8ce | |||
| ae7aec8d3b | |||
| 3f9381a80c | |||
| 1826086186 | |||
| af4abfc8f9 | |||
| ba0f06e104 | |||
| a932144e94 | |||
| 36ad60b782 | |||
| e4467dba32 | |||
| 410923af92 | |||
| 24c405386b | |||
| d5296bd3fa | |||
| 3ee2f6b670 | |||
| 09af4c760c | |||
| 3fd0fd9584 | |||
| 27984ebac8 | |||
| 3d4aab74d5 | |||
| 4076630eec | |||
| 6025212e93 | |||
| de554048eb | |||
| e1d79d2c79 |
24
FrontendV2/.gitignore
vendored
24
FrontendV2/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 8001
|
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# React + TypeScript + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## React Compiler
|
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
|
||||||
tseslint.configs.recommendedTypeChecked,
|
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
tseslint.configs.strictTypeChecked,
|
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
tseslint.configs.stylisticTypeChecked,
|
|
||||||
|
|
||||||
// Other configs...
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
// Enable lint rules for React
|
|
||||||
reactX.configs['recommended-typescript'],
|
|
||||||
// Enable lint rules for React DOM
|
|
||||||
reactDom.configs.recommended,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs['recommended-latest'],
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>frontendv2</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
6045
FrontendV2/package-lock.json
generated
6045
FrontendV2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "admin",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@chakra-ui/react": "^3.28.0",
|
|
||||||
"@emotion/react": "^11.14.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
|
||||||
"@tanstack/react-query": "^5.85.5",
|
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"lucide-react": "^0.539.0",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"primeicons": "^7.0.0",
|
|
||||||
"primereact": "^10.9.6",
|
|
||||||
"react": "^19.1.1",
|
|
||||||
"react-dom": "^19.1.1",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-router-dom": "^7.8.0",
|
|
||||||
"react-toastify": "^11.0.5",
|
|
||||||
"split-lines": "^3.0.0",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tailwindcss": "^4.1.11",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"tw-animate-css": "^1.3.5",
|
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.32.0",
|
|
||||||
"@types/js-cookie": "^3.0.6",
|
|
||||||
"@types/react": "^19.1.9",
|
|
||||||
"@types/react-dom": "^19.1.7",
|
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
|
||||||
"eslint": "^9.32.0",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
|
||||||
"globals": "^16.3.0",
|
|
||||||
"typescript": "~5.8.3",
|
|
||||||
"typescript-eslint": "^8.39.0",
|
|
||||||
"vite": "^7.1.0",
|
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,73 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text",
|
|
||||||
"SF Pro Display", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
|
||||||
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Display für größere Überschriften */
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Regular.woff2")
|
|
||||||
format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Medium.woff2") format("woff2");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text für Fließtext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Regular.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Medium.woff2") format("woff2");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global anwenden mit Fallbacks */
|
|
||||||
:root {
|
|
||||||
--font-sans: "SF Pro Text", "SF Pro Display", -apple-system,
|
|
||||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import "./App.css";
|
|
||||||
import { LoginPage } from "@/pages/LoginPage";
|
|
||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
|
||||||
import { HomePage } from "@/pages/HomePage";
|
|
||||||
import { ProtectedRoutes } from "./utils/ProtectedRoutes";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { setIsLoggedInAtom } from "@/states/Atoms";
|
|
||||||
import { UserContext, type User } from "./states/Context";
|
|
||||||
import { triggerLogoutAtom } from "@/states/Atoms";
|
|
||||||
import { MyLoansPage } from "./pages/MyLoansPage";
|
|
||||||
import Landingpage from "./pages/Landingpage";
|
|
||||||
|
|
||||||
const API_BASE =
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
|
||||||
"http://localhost:8002";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [user, setUser] = useState<User | undefined>(undefined);
|
|
||||||
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
|
||||||
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Cookies.get("token")) {
|
|
||||||
const verifyToken = async () => {
|
|
||||||
const response = await fetch(`${API_BASE}/api/verifyToken`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
setTriggerLogout(false);
|
|
||||||
const data = await response.json();
|
|
||||||
setUser({ username: data.user.username, role: data.user.role });
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
} else {
|
|
||||||
Cookies.remove("token");
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
verifyToken();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UserContext.Provider value={user}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
<Route element={<ProtectedRoutes />}>
|
|
||||||
<Route path="/" element={<HomePage />} />
|
|
||||||
<Route path="/my-loans" element={<MyLoansPage />} />
|
|
||||||
<Route path="/landing" element={<Landingpage />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
</UserContext.Provider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Changelog",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"version": "v2.1.0",
|
|
||||||
"date": "2025-10-24",
|
|
||||||
"changes": [
|
|
||||||
{
|
|
||||||
"type": "Hinzugefügt",
|
|
||||||
"text": [
|
|
||||||
"Neue Changelog-Komponente mit zentriertem Layout.",
|
|
||||||
"Unterstützung für mehrsprachige Einträge (Englisch und Deutsch)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Verbessert",
|
|
||||||
"text": [
|
|
||||||
"Performance-Optimierungen beim Laden der Listenansichten.",
|
|
||||||
"Verbesserte Barrierefreiheit durch ARIA-Attribute."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Behoben",
|
|
||||||
"text": [
|
|
||||||
"Fehler bei der Datumsauswahl im Safari-Browser.",
|
|
||||||
"Anzeigeprobleme bei hohen DPI-Einstellungen."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v2.0.3",
|
|
||||||
"date": "2025-10-10",
|
|
||||||
"changes": [
|
|
||||||
{
|
|
||||||
"type": "Geändert",
|
|
||||||
"text": [
|
|
||||||
"Standard-Timeout für API-Requests auf 10s erhöht."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Sicherheit",
|
|
||||||
"text": [
|
|
||||||
"Abhängigkeiten aktualisiert (kritische CVEs behoben)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "changelog";
|
|
||||||
|
|
||||||
type ChangeType =
|
|
||||||
| "Hinzugefügt"
|
|
||||||
| "Geändert"
|
|
||||||
| "Behoben"
|
|
||||||
| "Entfernt"
|
|
||||||
| "Verbessert"
|
|
||||||
| "Sicherheit"
|
|
||||||
| "Veraltet"
|
|
||||||
| string;
|
|
||||||
|
|
||||||
type ChangeEntry = {
|
|
||||||
type: ChangeType;
|
|
||||||
text: string | string[]; // aus localStorage kann es eine Liste sein
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChangelogItem = {
|
|
||||||
version?: string;
|
|
||||||
date: string;
|
|
||||||
changes: ChangeEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type StoredChangelog = {
|
|
||||||
title: string;
|
|
||||||
items: ChangelogItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const typeStyles: Record<string, string> = {
|
|
||||||
Hinzugefügt:
|
|
||||||
"bg-emerald-500/15 text-emerald-300 ring-1 ring-inset ring-emerald-500/30",
|
|
||||||
Geändert: "bg-blue-500/15 text-blue-300 ring-1 ring-inset ring-blue-500/30",
|
|
||||||
Behoben: "bg-amber-500/15 text-amber-300 ring-1 ring-inset ring-amber-500/30",
|
|
||||||
Entfernt: "bg-rose-500/15 text-rose-300 ring-1 ring-inset ring-rose-500/30",
|
|
||||||
Verbessert:
|
|
||||||
"bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30",
|
|
||||||
Sicherheit: "bg-red-500/15 text-red-300 ring-1 ring-inset ring-red-500/30",
|
|
||||||
Veraltet: "bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Changelog() {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [data, setData] = useState<StoredChangelog | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => setMounted(true), []);
|
|
||||||
|
|
||||||
const loadFromStorage = () => {
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
const raw =
|
|
||||||
typeof window !== "undefined"
|
|
||||||
? localStorage.getItem(STORAGE_KEY)
|
|
||||||
: null;
|
|
||||||
if (!raw) {
|
|
||||||
setData(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parsed = JSON.parse(raw) as StoredChangelog;
|
|
||||||
if (!parsed || !Array.isArray(parsed.items)) {
|
|
||||||
throw new Error("Ungültiges Format");
|
|
||||||
}
|
|
||||||
setData(parsed);
|
|
||||||
} catch (e) {
|
|
||||||
setError("Changelog konnte nicht aus localStorage geladen werden.");
|
|
||||||
setData(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadFromStorage();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") setOpen(false);
|
|
||||||
};
|
|
||||||
const onClickOutside = (e: MouseEvent) => {
|
|
||||||
if (cardRef.current && !cardRef.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onStorage = (e: StorageEvent) => {
|
|
||||||
if (e.key === STORAGE_KEY) loadFromStorage();
|
|
||||||
};
|
|
||||||
window.addEventListener("keydown", onKey);
|
|
||||||
document.addEventListener("mousedown", onClickOutside);
|
|
||||||
window.addEventListener("storage", onStorage);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKey);
|
|
||||||
document.removeEventListener("mousedown", onClickOutside);
|
|
||||||
window.removeEventListener("storage", onStorage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
const title = data?.title ?? "Changelog";
|
|
||||||
const items = data?.items ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-zinc-950 bg-[radial-gradient(60%_60%_at_50%_0%,rgba(99,102,241,0.12),rgba(24,24,27,0))] flex items-center justify-center p-6">
|
|
||||||
<div
|
|
||||||
ref={cardRef}
|
|
||||||
className={[
|
|
||||||
"relative w-full max-w-6xl transition-all duration-300 ease-out",
|
|
||||||
mounted
|
|
||||||
? "opacity-100 translate-y-0 scale-100"
|
|
||||||
: "opacity-0 translate-y-1 scale-[0.99]",
|
|
||||||
].join(" ")}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{/* Gradient border wrapper */}
|
|
||||||
<div className="rounded-2xl p-[1px] bg-gradient-to-b from-zinc-700/60 via-zinc-700/20 to-zinc-800/60 shadow-2xl">
|
|
||||||
{/* Card */}
|
|
||||||
<div className="relative rounded-[calc(theme(borderRadius.2xl)-1px)] border border-zinc-800/70 bg-zinc-900/70 supports-[backdrop-filter]:bg-zinc-900/60 backdrop-blur-xl ring-1 ring-white/10">
|
|
||||||
{/* Accent top line */}
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent" />
|
|
||||||
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
aria-label="Changelog schließen"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className="absolute right-3 top-3 inline-flex h-9 w-9 items-center justify-center rounded-md text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 transition"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-5 w-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.8}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M6 6l12 12M18 6L6 18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<header className="px-10 pt-8 pb-6 border-b border-zinc-800/70">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-500/15 text-indigo-300 ring-1 ring-inset ring-indigo-500/30">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-5 w-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.6}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-[30px] leading-8 font-semibold text-zinc-100 tracking-[-0.01em]">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
Aktuelle Änderungen und Updates
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<div className="relative max-h-[78vh] overflow-y-auto">
|
|
||||||
<div className="absolute pointer-events-none inset-x-0 top-0 h-8 bg-gradient-to-b from-zinc-900/70 to-transparent" />
|
|
||||||
<div className="absolute pointer-events-none inset-x-0 bottom-0 h-10 bg-gradient-to-t from-zinc-900/80 to-transparent" />
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="px-10 py-8">
|
|
||||||
<div className="rounded-lg border border-red-900/40 bg-red-900/10 px-4 py-3 text-sm text-red-300">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!error && items.length === 0 && (
|
|
||||||
<div className="px-10 py-16 text-center">
|
|
||||||
<p className="text-zinc-400">
|
|
||||||
Kein Changelog im localStorage gefunden (Key: {STORAGE_KEY}
|
|
||||||
).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="divide-y divide-zinc-800/70">
|
|
||||||
{items.map((entry, idx) => (
|
|
||||||
<li
|
|
||||||
key={`${entry.version ?? entry.date}-${idx}`}
|
|
||||||
className="px-10 py-8"
|
|
||||||
>
|
|
||||||
{/* Kopfzeile je Release */}
|
|
||||||
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
|
|
||||||
{entry.version && (
|
|
||||||
<span className="inline-flex items-center rounded-md bg-gradient-to-b from-zinc-100 to-zinc-300 text-zinc-900 px-3 py-0.5 text-sm font-semibold shadow-sm">
|
|
||||||
{entry.version}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<time
|
|
||||||
className="text-sm text-zinc-400"
|
|
||||||
dateTime={entry.date}
|
|
||||||
>
|
|
||||||
{new Date(entry.date).toLocaleDateString("de-DE", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "2-digit",
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Zweispaltiges Layout: Typ links, Text rechts (mit schöner Leselänge) */}
|
|
||||||
<dl
|
|
||||||
role="list"
|
|
||||||
className="mt-6 grid grid-cols-1 gap-x-8 gap-y-3 md:grid-cols-[max-content_1fr]"
|
|
||||||
>
|
|
||||||
{entry.changes.map((c, i) => (
|
|
||||||
<div key={i} className="contents">
|
|
||||||
<dt className="md:w-44 md:justify-end md:text-right">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium ${
|
|
||||||
typeStyles[c.type] ??
|
|
||||||
"bg-zinc-700/30 text-zinc-300 ring-1 ring-inset ring-zinc-600/40"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{c.type}
|
|
||||||
</span>
|
|
||||||
</dt>
|
|
||||||
|
|
||||||
<dd className="max-w-[74ch] text-[15px] leading-7 text-zinc-200 tracking-[0.005em]">
|
|
||||||
{Array.isArray(c.text) ? (
|
|
||||||
<ul className="ml-4 list-disc marker:text-zinc-500/70 space-y-1.5">
|
|
||||||
{c.text.map((t, k) => (
|
|
||||||
<li key={k} className="break-words">
|
|
||||||
{t}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="break-words">{c.text}</p>
|
|
||||||
)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* soft bottom glow */}
|
|
||||||
<div className="pointer-events-none absolute inset-x-12 -bottom-4 h-8 blur-2xl bg-indigo-600/20 rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
CloseButton,
|
|
||||||
Dialog,
|
|
||||||
Portal,
|
|
||||||
HStack,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
Box,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
CircleUserRound,
|
|
||||||
RotateCcwKey,
|
|
||||||
Code,
|
|
||||||
LifeBuoy,
|
|
||||||
LogOut,
|
|
||||||
CalendarPlus,
|
|
||||||
MoreVertical,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useUserContext } from "@/states/Context";
|
|
||||||
import { useState } from "react";
|
|
||||||
import MyAlert from "./myChakra/MyAlert";
|
|
||||||
|
|
||||||
const API_BASE =
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
|
||||||
"http://localhost:8002";
|
|
||||||
|
|
||||||
export const Header = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const userData = useUserContext();
|
|
||||||
|
|
||||||
// Error handling states
|
|
||||||
const [isMsg, setIsMsg] = useState(false);
|
|
||||||
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
|
|
||||||
const [msgTitle, setMsgTitle] = useState("");
|
|
||||||
const [msgDescription, setMsgDescription] = useState("");
|
|
||||||
|
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
|
|
||||||
const [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
|
||||||
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
|
||||||
|
|
||||||
// Dialog control
|
|
||||||
const [isPwOpen, setPwOpen] = useState(false);
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
setMsgTitle("Passwortänderung fehlgeschlagen");
|
|
||||||
setMsgDescription("Passwörter stimmen nicht überein");
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/api/changePassword`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ oldPassword, newPassword }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setMsgTitle("Passwortänderung fehlgeschlagen");
|
|
||||||
setMsgDescription("Bitte überprüfen Sie Ihre Eingaben");
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMsgTitle("Passwort erfolgreich geändert");
|
|
||||||
setMsgDescription("Ihr Passwort wurde erfolgreich geändert");
|
|
||||||
setMsgStatus("success");
|
|
||||||
setIsMsg(true);
|
|
||||||
|
|
||||||
setOldPassword("");
|
|
||||||
setNewPassword("");
|
|
||||||
setConfirmPassword("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const username = userData?.username
|
|
||||||
? userData.username[0].toUpperCase() + userData.username.slice(1)
|
|
||||||
: "User";
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
Cookies.remove("token");
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
setTriggerLogout(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
as="header"
|
|
||||||
gap={3}
|
|
||||||
className="mb-6"
|
|
||||||
position="relative"
|
|
||||||
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
|
||||||
>
|
|
||||||
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
|
||||||
<Box
|
|
||||||
display={{ base: "block", md: "none" }}
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
right="0"
|
|
||||||
transform="translateY(-50%)"
|
|
||||||
zIndex={2}
|
|
||||||
>
|
|
||||||
<Menu.Root>
|
|
||||||
<Menu.Trigger asChild>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Aktionen"
|
|
||||||
variant="solid"
|
|
||||||
colorScheme="teal"
|
|
||||||
size="md"
|
|
||||||
borderRadius="full"
|
|
||||||
boxShadow="md"
|
|
||||||
>
|
|
||||||
<MoreVertical size={20} />
|
|
||||||
</IconButton>
|
|
||||||
</Menu.Trigger>
|
|
||||||
<Menu.Positioner>
|
|
||||||
<Menu.Content>
|
|
||||||
<Menu.Item
|
|
||||||
value="create-loan"
|
|
||||||
onSelect={() => navigate("/", { replace: true })}
|
|
||||||
children={
|
|
||||||
<HStack gap={3}>
|
|
||||||
<CalendarPlus size={16} />
|
|
||||||
<Text as="span">Ausleihe erstellen</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Menu.Item
|
|
||||||
value="my-loans"
|
|
||||||
onSelect={() => navigate("/my-loans", { replace: true })}
|
|
||||||
children={
|
|
||||||
<HStack gap={3}>
|
|
||||||
<CircleUserRound size={16} />
|
|
||||||
<Text as="span">Meine Ausleihen</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Menu.Item
|
|
||||||
value="change-password"
|
|
||||||
onSelect={() => setPwOpen(true)}
|
|
||||||
children={
|
|
||||||
<HStack gap={3}>
|
|
||||||
<RotateCcwKey size={16} />
|
|
||||||
<Text as="span">Passwort ändern</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Menu.Item
|
|
||||||
value="help"
|
|
||||||
onSelect={() =>
|
|
||||||
window.open(
|
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
|
||||||
"_blank",
|
|
||||||
"noopener,noreferrer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
children={
|
|
||||||
<HStack gap={3}>
|
|
||||||
<LifeBuoy size={16} />
|
|
||||||
<Text as="span">Hilfe</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Menu.Item
|
|
||||||
value="source-code"
|
|
||||||
onSelect={() =>
|
|
||||||
window.open(
|
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system",
|
|
||||||
"_blank",
|
|
||||||
"noopener,noreferrer"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
children={
|
|
||||||
<HStack gap={3}>
|
|
||||||
<Code size={16} />
|
|
||||||
<Text as="span">Source Code</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Menu.Separator />
|
|
||||||
<Menu.Item
|
|
||||||
value="logout"
|
|
||||||
onSelect={logout}
|
|
||||||
children={
|
|
||||||
<HStack gap={3} color="red.500">
|
|
||||||
<LogOut size={16} />
|
|
||||||
<Text as="span">Logout</Text>
|
|
||||||
</HStack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Menu.Content>
|
|
||||||
</Menu.Positioner>
|
|
||||||
</Menu.Root>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
direction={{ base: "column", md: "row" }}
|
|
||||||
align={{ base: "stretch", md: "center" }}
|
|
||||||
justify="space-between"
|
|
||||||
gap={4}
|
|
||||||
>
|
|
||||||
{/* Left: Title + user info */}
|
|
||||||
<Stack gap={1}>
|
|
||||||
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
|
||||||
<Flex align="center" justify="space-between" gap={2}>
|
|
||||||
<Heading
|
|
||||||
size="2xl"
|
|
||||||
className="tracking-tight text-slate-900 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</Heading>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<HStack gap={3} align="center" flexWrap="wrap">
|
|
||||||
<Text fontSize="md" className="text-slate-600 dark:text-slate-400">
|
|
||||||
Willkommen zurück, {username}!
|
|
||||||
</Text>
|
|
||||||
<Badge variant="subtle" px={2} py={1} borderRadius="full">
|
|
||||||
Rolle: {userData?.role ?? "—"}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* Right: Actions */}
|
|
||||||
{/* Desktop actions */}
|
|
||||||
<HStack
|
|
||||||
gap={2}
|
|
||||||
align="center"
|
|
||||||
justify="flex-end"
|
|
||||||
flexWrap="wrap"
|
|
||||||
display={{ base: "none", md: "flex" }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
colorScheme="teal"
|
|
||||||
onClick={() => navigate("/", { replace: true })}
|
|
||||||
>
|
|
||||||
<HStack gap={2}>
|
|
||||||
<CalendarPlus size={18} />
|
|
||||||
<Text as="span">Ausleihe erstellen</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button onClick={() => navigate("/my-loans", { replace: true })}>
|
|
||||||
<HStack gap={2}>
|
|
||||||
<CircleUserRound size={18} />
|
|
||||||
<Text as="span">Meine Ausleihen</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="ghost" onClick={() => setPwOpen(true)}>
|
|
||||||
<HStack gap={2}>
|
|
||||||
<RotateCcwKey size={18} />
|
|
||||||
<Text as="span">Passwort ändern</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<HStack gap={2}>
|
|
||||||
<LifeBuoy size={18} />
|
|
||||||
<Text as="span">Hilfe</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<HStack gap={2}>
|
|
||||||
<Code size={18} />
|
|
||||||
<Text as="span">Source Code</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<Button onClick={logout} variant="outline" colorScheme="red">
|
|
||||||
<HStack gap={2}>
|
|
||||||
<LogOut size={18} />
|
|
||||||
<Text as="span">Logout</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* Passwort-Dialog (kontrolliert) */}
|
|
||||||
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
|
||||||
<Portal>
|
|
||||||
<Dialog.Backdrop />
|
|
||||||
<Dialog.Positioner>
|
|
||||||
<Dialog.Content maxW="md">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Passwort ändern</Dialog.Title>
|
|
||||||
</Dialog.Header>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
changePassword();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Body>
|
|
||||||
<Stack gap={3}>
|
|
||||||
<PasswordInput
|
|
||||||
value={oldPassword}
|
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
|
||||||
placeholder="Altes Passwort"
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
placeholder="Neues Passwort"
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
placeholder="Neues Passwort wiederholen"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Stack w="100%" gap={3}>
|
|
||||||
{isMsg && (
|
|
||||||
<MyAlert
|
|
||||||
status={msgStatus}
|
|
||||||
title={msgTitle}
|
|
||||||
description={msgDescription}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<HStack justify="flex-end" gap={2}>
|
|
||||||
<Dialog.ActionTrigger asChild>
|
|
||||||
<Button variant="outline">Abbrechen</Button>
|
|
||||||
</Dialog.ActionTrigger>
|
|
||||||
<Button type="submit" colorScheme="teal">
|
|
||||||
Speichern
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</form>
|
|
||||||
<Dialog.CloseTrigger asChild>
|
|
||||||
<CloseButton size="sm" />
|
|
||||||
</Dialog.CloseTrigger>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Alert } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
type MyAlertProps = {
|
|
||||||
status: "error" | "success";
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MyAlert: React.FC<MyAlertProps> = ({ title, description, status }) => {
|
|
||||||
return (
|
|
||||||
<Alert.Root status={status}>
|
|
||||||
<Alert.Indicator />
|
|
||||||
<Alert.Content>
|
|
||||||
<Alert.Title>{title}</Alert.Title>
|
|
||||||
<Alert.Description>{description}</Alert.Description>
|
|
||||||
</Alert.Content>
|
|
||||||
</Alert.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MyAlert;
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
|
||||||
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
|
||||||
import { ThemeProvider, useTheme } from "next-themes"
|
|
||||||
import type { ThemeProviderProps } from "next-themes"
|
|
||||||
import * as React from "react"
|
|
||||||
import { LuMoon, LuSun } from "react-icons/lu"
|
|
||||||
|
|
||||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
|
||||||
|
|
||||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
|
||||||
return (
|
|
||||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ColorMode = "light" | "dark"
|
|
||||||
|
|
||||||
export interface UseColorModeReturn {
|
|
||||||
colorMode: ColorMode
|
|
||||||
setColorMode: (colorMode: ColorMode) => void
|
|
||||||
toggleColorMode: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useColorMode(): UseColorModeReturn {
|
|
||||||
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
|
||||||
const colorMode = forcedTheme || resolvedTheme
|
|
||||||
const toggleColorMode = () => {
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
colorMode: colorMode as ColorMode,
|
|
||||||
setColorMode: setTheme,
|
|
||||||
toggleColorMode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useColorModeValue<T>(light: T, dark: T) {
|
|
||||||
const { colorMode } = useColorMode()
|
|
||||||
return colorMode === "dark" ? dark : light
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ColorModeIcon() {
|
|
||||||
const { colorMode } = useColorMode()
|
|
||||||
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
|
||||||
|
|
||||||
export const ColorModeButton = React.forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
ColorModeButtonProps
|
|
||||||
>(function ColorModeButton(props, ref) {
|
|
||||||
const { toggleColorMode } = useColorMode()
|
|
||||||
return (
|
|
||||||
<ClientOnly fallback={<Skeleton boxSize="9" />}>
|
|
||||||
<IconButton
|
|
||||||
onClick={toggleColorMode}
|
|
||||||
variant="ghost"
|
|
||||||
aria-label="Toggle color mode"
|
|
||||||
size="sm"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
css={{
|
|
||||||
_icon: {
|
|
||||||
width: "5",
|
|
||||||
height: "5",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColorModeIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ClientOnly>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
|
||||||
function LightMode(props, ref) {
|
|
||||||
return (
|
|
||||||
<Span
|
|
||||||
color="fg"
|
|
||||||
display="contents"
|
|
||||||
className="chakra-theme light"
|
|
||||||
colorPalette="gray"
|
|
||||||
colorScheme="light"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
|
||||||
function DarkMode(props, ref) {
|
|
||||||
return (
|
|
||||||
<Span
|
|
||||||
color="fg"
|
|
||||||
display="contents"
|
|
||||||
className="chakra-theme dark"
|
|
||||||
colorPalette="gray"
|
|
||||||
colorScheme="dark"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ButtonProps,
|
|
||||||
GroupProps,
|
|
||||||
InputProps,
|
|
||||||
StackProps,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
HStack,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
Stack,
|
|
||||||
mergeRefs,
|
|
||||||
useControllableState,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import * as React from "react"
|
|
||||||
import { LuEye, LuEyeOff } from "react-icons/lu"
|
|
||||||
|
|
||||||
export interface PasswordVisibilityProps {
|
|
||||||
/**
|
|
||||||
* The default visibility state of the password input.
|
|
||||||
*/
|
|
||||||
defaultVisible?: boolean
|
|
||||||
/**
|
|
||||||
* The controlled visibility state of the password input.
|
|
||||||
*/
|
|
||||||
visible?: boolean
|
|
||||||
/**
|
|
||||||
* Callback invoked when the visibility state changes.
|
|
||||||
*/
|
|
||||||
onVisibleChange?: (visible: boolean) => void
|
|
||||||
/**
|
|
||||||
* Custom icons for the visibility toggle button.
|
|
||||||
*/
|
|
||||||
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PasswordInputProps
|
|
||||||
extends InputProps,
|
|
||||||
PasswordVisibilityProps {
|
|
||||||
rootProps?: GroupProps
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PasswordInput = React.forwardRef<
|
|
||||||
HTMLInputElement,
|
|
||||||
PasswordInputProps
|
|
||||||
>(function PasswordInput(props, ref) {
|
|
||||||
const {
|
|
||||||
rootProps,
|
|
||||||
defaultVisible,
|
|
||||||
visible: visibleProp,
|
|
||||||
onVisibleChange,
|
|
||||||
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
|
||||||
...rest
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const [visible, setVisible] = useControllableState({
|
|
||||||
value: visibleProp,
|
|
||||||
defaultValue: defaultVisible || false,
|
|
||||||
onChange: onVisibleChange,
|
|
||||||
})
|
|
||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InputGroup
|
|
||||||
endElement={
|
|
||||||
<VisibilityTrigger
|
|
||||||
disabled={rest.disabled}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
if (rest.disabled) return
|
|
||||||
if (e.button !== 0) return
|
|
||||||
e.preventDefault()
|
|
||||||
setVisible(!visible)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{visible ? visibilityIcon.off : visibilityIcon.on}
|
|
||||||
</VisibilityTrigger>
|
|
||||||
}
|
|
||||||
{...rootProps}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...rest}
|
|
||||||
ref={mergeRefs(ref, inputRef)}
|
|
||||||
type={visible ? "text" : "password"}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
function VisibilityTrigger(props, ref) {
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
tabIndex={-1}
|
|
||||||
ref={ref}
|
|
||||||
me="-2"
|
|
||||||
aspectRatio="square"
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
height="calc(100% - {spacing.2})"
|
|
||||||
aria-label="Toggle password visibility"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
interface PasswordStrengthMeterProps extends StackProps {
|
|
||||||
max?: number
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PasswordStrengthMeter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
PasswordStrengthMeterProps
|
|
||||||
>(function PasswordStrengthMeter(props, ref) {
|
|
||||||
const { max = 4, value, ...rest } = props
|
|
||||||
|
|
||||||
const percent = (value / max) * 100
|
|
||||||
const { label, colorPalette } = getColorPalette(percent)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
|
||||||
<HStack width="full" {...rest}>
|
|
||||||
{Array.from({ length: max }).map((_, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
height="1"
|
|
||||||
flex="1"
|
|
||||||
rounded="sm"
|
|
||||||
data-selected={index < value ? "" : undefined}
|
|
||||||
layerStyle="fill.subtle"
|
|
||||||
colorPalette="gray"
|
|
||||||
_selected={{
|
|
||||||
colorPalette,
|
|
||||||
layerStyle: "fill.solid",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
{label && <HStack textStyle="xs">{label}</HStack>}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function getColorPalette(percent: number) {
|
|
||||||
switch (true) {
|
|
||||||
case percent < 33:
|
|
||||||
return { label: "Low", colorPalette: "red" }
|
|
||||||
case percent < 66:
|
|
||||||
return { label: "Medium", colorPalette: "orange" }
|
|
||||||
default:
|
|
||||||
return { label: "High", colorPalette: "green" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
|
||||||
import {
|
|
||||||
ColorModeProvider,
|
|
||||||
type ColorModeProviderProps,
|
|
||||||
} from "./color-mode"
|
|
||||||
|
|
||||||
export function Provider(props: ColorModeProviderProps) {
|
|
||||||
return (
|
|
||||||
<ChakraProvider value={defaultSystem}>
|
|
||||||
<ColorModeProvider {...props} />
|
|
||||||
</ChakraProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Toaster as ChakraToaster,
|
|
||||||
Portal,
|
|
||||||
Spinner,
|
|
||||||
Stack,
|
|
||||||
Toast,
|
|
||||||
createToaster,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
|
|
||||||
export const toaster = createToaster({
|
|
||||||
placement: "bottom-end",
|
|
||||||
pauseOnPageIdle: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Toaster = () => {
|
|
||||||
return (
|
|
||||||
<Portal>
|
|
||||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
|
||||||
{(toast) => (
|
|
||||||
<Toast.Root width={{ md: "sm" }}>
|
|
||||||
{toast.type === "loading" ? (
|
|
||||||
<Spinner size="sm" color="blue.solid" />
|
|
||||||
) : (
|
|
||||||
<Toast.Indicator />
|
|
||||||
)}
|
|
||||||
<Stack gap="1" flex="1" maxWidth="100%">
|
|
||||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
|
||||||
{toast.description && (
|
|
||||||
<Toast.Description>{toast.description}</Toast.Description>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
{toast.action && (
|
|
||||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
|
||||||
)}
|
|
||||||
{toast.closable && <Toast.CloseTrigger />}
|
|
||||||
</Toast.Root>
|
|
||||||
)}
|
|
||||||
</ChakraToaster>
|
|
||||||
</Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
|
||||||
showArrow?: boolean
|
|
||||||
portalled?: boolean
|
|
||||||
portalRef?: React.RefObject<HTMLElement | null>
|
|
||||||
content: React.ReactNode
|
|
||||||
contentProps?: ChakraTooltip.ContentProps
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
|
||||||
function Tooltip(props, ref) {
|
|
||||||
const {
|
|
||||||
showArrow,
|
|
||||||
children,
|
|
||||||
disabled,
|
|
||||||
portalled = true,
|
|
||||||
content,
|
|
||||||
contentProps,
|
|
||||||
portalRef,
|
|
||||||
...rest
|
|
||||||
} = props
|
|
||||||
|
|
||||||
if (disabled) return children
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChakraTooltip.Root {...rest}>
|
|
||||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
|
||||||
<Portal disabled={!portalled} container={portalRef}>
|
|
||||||
<ChakraTooltip.Positioner>
|
|
||||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
|
||||||
{showArrow && (
|
|
||||||
<ChakraTooltip.Arrow>
|
|
||||||
<ChakraTooltip.ArrowTip />
|
|
||||||
</ChakraTooltip.Arrow>
|
|
||||||
)}
|
|
||||||
{content}
|
|
||||||
</ChakraTooltip.Content>
|
|
||||||
</ChakraTooltip.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</ChakraTooltip.Root>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text",
|
|
||||||
"SF Pro Display", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
|
||||||
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Regular.woff2")
|
|
||||||
format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Medium.woff2") format("woff2");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Display";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProDisplay-Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Regular.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Medium.woff2") format("woff2");
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "SF Pro Text";
|
|
||||||
src: url("/src/assets/fonts/sf-pro/SFProText-Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--font-sans: "SF Pro Text", "SF Pro Display", -apple-system,
|
|
||||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Provider } from "@/components/ui/provider";
|
|
||||||
import { StrictMode } from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import "./index.css";
|
|
||||||
import App from "./App.tsx";
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<Provider>
|
|
||||||
<App />
|
|
||||||
</Provider>
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import {
|
|
||||||
Container,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Spinner,
|
|
||||||
VStack,
|
|
||||||
Table,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { getBorrowableItems } from "@/utils/Fetcher";
|
|
||||||
import { useState } from "react";
|
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
|
||||||
import { borrowAbleItemsAtom } from "@/states/Atoms";
|
|
||||||
import { createLoan } from "@/utils/Fetcher";
|
|
||||||
import { Header } from "@/components/Header";
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
username: string;
|
|
||||||
role: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HomePage = () => {
|
|
||||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
|
||||||
const [startDate, setStartDate] = useState("");
|
|
||||||
const [endDate, setEndDate] = useState("");
|
|
||||||
const [isLoadingA, setIsLoadingA] = useState(false);
|
|
||||||
const [selectedItems, setSelectedItems] = useState<number[]>([]);
|
|
||||||
|
|
||||||
// Error handling states
|
|
||||||
const [isMsg, setIsMsg] = useState(false);
|
|
||||||
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
|
|
||||||
const [msgTitle, setMsgTitle] = useState("");
|
|
||||||
const [msgDescription, setMsgDescription] = useState("");
|
|
||||||
|
|
||||||
const handleCheckboxChange = (itemId: number) => {
|
|
||||||
setSelectedItems((prevSelected) =>
|
|
||||||
prevSelected.includes(itemId)
|
|
||||||
? prevSelected.filter((id) => id !== itemId)
|
|
||||||
: [...prevSelected, itemId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
|
|
||||||
<Header />
|
|
||||||
{isMsg && (
|
|
||||||
<MyAlert
|
|
||||||
status={msgStatus}
|
|
||||||
title={msgTitle}
|
|
||||||
description={msgDescription}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Stack as="main">
|
|
||||||
<label htmlFor="startDate">
|
|
||||||
<Text>Startdatum</Text>
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="startDate"
|
|
||||||
placeholder="Startdatum"
|
|
||||||
type="datetime-local"
|
|
||||||
value={startDate}
|
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="endDate">
|
|
||||||
<Text>Enddatum</Text>
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="endDate"
|
|
||||||
placeholder="Enddatum"
|
|
||||||
type="datetime-local"
|
|
||||||
value={endDate}
|
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoadingA(true);
|
|
||||||
if (!startDate || !endDate) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle("Fehlende Eingaben");
|
|
||||||
setMsgDescription("Bitte Start- und Enddatum angeben.");
|
|
||||||
setIsMsg(true);
|
|
||||||
setIsLoadingA(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await getBorrowableItems(startDate, endDate).then((response) => {
|
|
||||||
setIsLoadingA(false);
|
|
||||||
if (response && response.status === "error") {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(response.title || "Fehler");
|
|
||||||
setMsgDescription(
|
|
||||||
response.description || "Unbekannter Frontend Fehler"
|
|
||||||
);
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBorrowableItems(response.data);
|
|
||||||
setIsMsg(false);
|
|
||||||
console.log(borrowableItems);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Verfügbare Gegenstände anzeigen
|
|
||||||
</Button>
|
|
||||||
{isLoadingA && (
|
|
||||||
<VStack colorPalette="teal">
|
|
||||||
<Spinner color="colorPalette.600" />
|
|
||||||
<Text color="colorPalette.600">Loading...</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
{borrowableItems.length > 0 && (
|
|
||||||
<Table.ScrollArea borderWidth="1px" rounded="md">
|
|
||||||
<Table.Root size="sm" stickyHeader>
|
|
||||||
<Table.Header>
|
|
||||||
<Table.Row bg="bg.subtle">
|
|
||||||
<Table.ColumnHeader></Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Gegenstand</Table.ColumnHeader>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
|
|
||||||
<Table.Body>
|
|
||||||
{borrowableItems.map((item) => (
|
|
||||||
<Table.Row key={item.id}>
|
|
||||||
<Table.Cell>
|
|
||||||
<input
|
|
||||||
onChange={() => handleCheckboxChange(item.id)}
|
|
||||||
type="checkbox"
|
|
||||||
name={item.id}
|
|
||||||
id={item.id}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{item.item_name}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table.Root>
|
|
||||||
</Table.ScrollArea>
|
|
||||||
)}
|
|
||||||
{selectedItems.length >= 1 && (
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
createLoan(selectedItems, startDate, endDate).then((response) => {
|
|
||||||
if (response.status === "error") {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(response.title || "Fehler");
|
|
||||||
setMsgDescription(
|
|
||||||
response.description || "Unbekannter Frontend Fehler"
|
|
||||||
);
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMsgStatus("success");
|
|
||||||
setMsgTitle("Erfolg");
|
|
||||||
setMsgDescription("Gegenstände erfolgreich ausgeliehen.");
|
|
||||||
setIsMsg(true);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Gegenstände ausleihen
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import MyAlert from "../components/myChakra/MyAlert";
|
|
||||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
|
||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { Navigate, useNavigate } from "react-router-dom";
|
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
|
|
||||||
const API_BASE =
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
|
||||||
"http://localhost:8002";
|
|
||||||
|
|
||||||
export const LoginPage = () => {
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
|
||||||
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedIn) {
|
|
||||||
navigate("/", { replace: true });
|
|
||||||
}
|
|
||||||
}, [isLoggedIn, navigate]);
|
|
||||||
|
|
||||||
const loginFnc = async (username: string, password: string) => {
|
|
||||||
const response = await fetch(`${API_BASE}/api/login`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: data.message ?? "Login fehlgeschlagen",
|
|
||||||
description: data.description ?? "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Cookies.set("token", data.token);
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
return { success: true };
|
|
||||||
};
|
|
||||||
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [isError, setIsError] = useState(false);
|
|
||||||
const [errorMsg, setErrorMsg] = useState("");
|
|
||||||
const [errorDsc, setErrorDsc] = useState("");
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
const result = await loginFnc(username, password);
|
|
||||||
if (!result.success) {
|
|
||||||
setErrorMsg(result.message);
|
|
||||||
setErrorDsc(result.description);
|
|
||||||
setIsError(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTriggerLogout(false);
|
|
||||||
navigate("/", { replace: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
return <Navigate to="/" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center p-4">
|
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
|
||||||
<Card.Root maxW="sm">
|
|
||||||
<Card.Header>
|
|
||||||
<Card.Title>Login</Card.Title>
|
|
||||||
<Card.Description>
|
|
||||||
Bitte unten Ihre Zugangsdaten eingeben.
|
|
||||||
</Card.Description>
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body>
|
|
||||||
<Stack gap="4" w="full">
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>Benutzername</Field.Label>
|
|
||||||
<Input
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Field.Root>
|
|
||||||
<Field.Root>
|
|
||||||
<Field.Label>Passwort</Field.Label>
|
|
||||||
<PasswordInput
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Field.Root>
|
|
||||||
</Stack>
|
|
||||||
</Card.Body>
|
|
||||||
<Card.Footer justifyContent="flex-end">
|
|
||||||
{isError && (
|
|
||||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
|
||||||
)}
|
|
||||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
|
||||||
<Card.Footer justifyContent="flex-end">
|
|
||||||
{triggerLogout && (
|
|
||||||
<MyAlert
|
|
||||||
status="success"
|
|
||||||
title={"Logout erfolgreich!"}
|
|
||||||
description={"Sie wurden erfolgreich abgemeldet."}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
|
||||||
import {
|
|
||||||
Container,
|
|
||||||
VStack,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
Table,
|
|
||||||
Button,
|
|
||||||
CloseButton,
|
|
||||||
Dialog,
|
|
||||||
Portal,
|
|
||||||
Code,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { Header } from "@/components/Header";
|
|
||||||
import { Trash2 } from "lucide-react";
|
|
||||||
|
|
||||||
const API_BASE =
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
|
||||||
"http://localhost:8002";
|
|
||||||
|
|
||||||
export const MyLoansPage = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loans, setLoans] = useState<any[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const [delLoanCode, setDelLoanCode] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Error handling states
|
|
||||||
const [isMsg, setIsMsg] = useState(false);
|
|
||||||
const [msgStatus, setMsgStatus] = useState<"error" | "success">("error");
|
|
||||||
const [msgTitle, setMsgTitle] = useState("");
|
|
||||||
const [msgDescription, setMsgDescription] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!Cookies.get("token")) {
|
|
||||||
navigate("/login", { replace: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchLoans = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const res = await fetch(`${API_BASE}/api/userLoans`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle("Fehler");
|
|
||||||
setMsgDescription(
|
|
||||||
"Beim Laden der Ausleihen ist ein Fehler aufgetreten."
|
|
||||||
);
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
setLoans(data);
|
|
||||||
console.log("Fetched loans:", data);
|
|
||||||
} catch (e) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle("Fehler");
|
|
||||||
setMsgDescription("Netzwerkfehler beim Laden der Ausleihen.");
|
|
||||||
setIsMsg(true);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchLoans();
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
const deleteLoan = async (loanId: number) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE}/api/SETdeleteLoan/${loanId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle("Fehler");
|
|
||||||
setMsgDescription(
|
|
||||||
"Beim Löschen der Ausleihe ist ein Fehler aufgetreten."
|
|
||||||
);
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoans((prev) => prev.filter((loan) => loan.id !== loanId));
|
|
||||||
setMsgStatus("success");
|
|
||||||
setMsgTitle("Erfolg");
|
|
||||||
setMsgDescription("Ausleihe erfolgreich gelöscht.");
|
|
||||||
setIsMsg(true);
|
|
||||||
} catch (e) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle("Fehler");
|
|
||||||
setMsgDescription("Netzwerkfehler beim Löschen der Ausleihe.");
|
|
||||||
setIsMsg(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (iso: string | null) => {
|
|
||||||
if (!iso) return "-";
|
|
||||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
|
||||||
if (!m) return iso;
|
|
||||||
const [, y, M, d, h, min] = m;
|
|
||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Container maxW="7xl" className="px-6 sm:px-8 pt-10">
|
|
||||||
<Header />
|
|
||||||
{isMsg && (
|
|
||||||
<MyAlert
|
|
||||||
status={msgStatus}
|
|
||||||
title={msgTitle}
|
|
||||||
description={msgDescription}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLoading && (
|
|
||||||
<VStack colorPalette="teal">
|
|
||||||
<Spinner color="colorPalette.600" />
|
|
||||||
<Text color="colorPalette.600">Loading...</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
{loans && (
|
|
||||||
<Table.Root
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
style={{ tableLayout: "fixed", width: "100%" }}
|
|
||||||
>
|
|
||||||
<Table.ColumnGroup>
|
|
||||||
{/* Ausleihcode */}
|
|
||||||
<Table.Column style={{ width: "14%" }} />
|
|
||||||
{/* Startdatum */}
|
|
||||||
<Table.Column style={{ width: "14%" }} />
|
|
||||||
{/* Enddatum */}
|
|
||||||
<Table.Column style={{ width: "14%" }} />
|
|
||||||
{/* Geräte (flexibler) */}
|
|
||||||
<Table.Column style={{ width: "28%" }} />
|
|
||||||
{/* Ausleihdatum */}
|
|
||||||
<Table.Column style={{ width: "14%" }} />
|
|
||||||
{/* Rückgabedatum */}
|
|
||||||
<Table.Column style={{ width: "14%" }} />
|
|
||||||
{/* Aktionen */}
|
|
||||||
<Table.Column style={{ width: "8%" }} />
|
|
||||||
</Table.ColumnGroup>
|
|
||||||
<Table.Header>
|
|
||||||
<Table.Row>
|
|
||||||
<Table.ColumnHeader>Ausleihcode</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Startdatum</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Enddatum</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Geräte</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Ausleihdatum</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Rückgabedatum</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>Aktionen</Table.ColumnHeader>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{loans.map((loan) => (
|
|
||||||
<Table.Row key={loan.id}>
|
|
||||||
<Table.Cell>
|
|
||||||
<Text title={loan.loan_code}>
|
|
||||||
<Code variant="solid">{`${loan.loan_code}`}</Code>
|
|
||||||
</Text>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{formatDate(loan.start_date)}</Table.Cell>
|
|
||||||
<Table.Cell>{formatDate(loan.end_date)}</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Text title={loan.loaned_items_name}>
|
|
||||||
{loan.loaned_items_name}
|
|
||||||
</Text>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell>
|
|
||||||
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Dialog.Root role="alertdialog">
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
onClick={() => setDelLoanCode(loan.loan_code)}
|
|
||||||
aria-label="Ausleihe löschen"
|
|
||||||
style={{
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 />
|
|
||||||
</Button>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Portal>
|
|
||||||
<Dialog.Backdrop />
|
|
||||||
<Dialog.Positioner>
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Sicher?</Dialog.Title>
|
|
||||||
</Dialog.Header>
|
|
||||||
<Dialog.Body>
|
|
||||||
<Text>
|
|
||||||
Möchtest du die Ausleihe mit dem{" "}
|
|
||||||
<strong><Code>{delLoanCode}</Code></strong> Code wirklich
|
|
||||||
löschen?
|
|
||||||
<br />
|
|
||||||
Für den Admin bleibt sie weiterhin sichtbar.
|
|
||||||
</Text>
|
|
||||||
</Dialog.Body>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Dialog.ActionTrigger asChild>
|
|
||||||
<Button variant="outline">Abbrechen</Button>
|
|
||||||
</Dialog.ActionTrigger>
|
|
||||||
<Button
|
|
||||||
colorPalette="red"
|
|
||||||
onClick={() => deleteLoan(loan.id)}
|
|
||||||
>
|
|
||||||
<strong>Löschen</strong>
|
|
||||||
</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
<Dialog.CloseTrigger asChild>
|
|
||||||
<CloseButton size="sm" />
|
|
||||||
</Dialog.CloseTrigger>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table.Root>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
export const testAtom = atom<number>(0);
|
|
||||||
export const setIsLoggedInAtom = atom<boolean>(false);
|
|
||||||
export const triggerLogoutAtom = atom<boolean>(false);
|
|
||||||
export const borrowAbleItemsAtom = atom<any[]>([]);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
username: string;
|
|
||||||
role: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserContext = createContext<User | undefined>(undefined);
|
|
||||||
|
|
||||||
export function useUserContext() {
|
|
||||||
const user = useContext(UserContext);
|
|
||||||
|
|
||||||
if (user === undefined) {
|
|
||||||
throw new Error("useUserContext must be used with a UserContext")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# How to use Atoms
|
|
||||||
Atoms are the fundamental building blocks of state management in this system. They represent individual pieces of state that can be shared and manipulated across different components.
|
|
||||||
|
|
||||||
You can also name it global state.
|
|
||||||
|
|
||||||
## Creating an Atom
|
|
||||||
to create an atom you have to declare an atom like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
export const NAME_OF_YOUR_ATOM = atom<type_of_your_atom>(initial_value);
|
|
||||||
```
|
|
||||||
|
|
||||||
In this project we declare all atoms in the `states/Atoms.tsx`file. Which you can find above this README file.
|
|
||||||
|
|
||||||
## Using an Atom
|
|
||||||
To use an atom in your component, you can use the `useAtom` hook provided by Jotai. Here's an example of how to use an atom in a React component:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { NAME_OF_YOUR_ATOM } from '@/states/Atoms';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const [value, setValue] = useAtom(NAME_OF_YOUR_ATOM);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Current value: {value}</p>
|
|
||||||
<button onClick={() => setValue(newValue)}>Update Value</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
As you can see, you can use `useAtom` like `useState` but the state is global. In this example `value` is the current state of the atom, and `setValue` is a function to update the state, which is also known as the setter function.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import Cookies from "js-cookie";
|
|
||||||
const API_BASE =
|
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
|
||||||
import.meta.env.VITE_BACKEND_URL ||
|
|
||||||
"http://localhost:8002";
|
|
||||||
|
|
||||||
export const getBorrowableItems = async (
|
|
||||||
startDate: string,
|
|
||||||
endDate: string
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/api/borrowableItems`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ startDate, endDate }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
data: null,
|
|
||||||
status: "error",
|
|
||||||
title: "Server error",
|
|
||||||
description:
|
|
||||||
"Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(data);
|
|
||||||
return {
|
|
||||||
data: data,
|
|
||||||
status: "success",
|
|
||||||
title: null,
|
|
||||||
description: null,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
data: null,
|
|
||||||
status: "error",
|
|
||||||
title: "Netzwerkfehler",
|
|
||||||
description:
|
|
||||||
"Es konnte keine Verbindung zum Server hergestellt werden. Bitte überprüfe deine Internetverbindung.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createLoan = async (
|
|
||||||
itemIds: number[],
|
|
||||||
startDate: string,
|
|
||||||
endDate: string
|
|
||||||
) => {
|
|
||||||
const response = await fetch(`${API_BASE}/api/createLoan`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ items: itemIds, startDate, endDate }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return {
|
|
||||||
data: null,
|
|
||||||
status: "error",
|
|
||||||
title: "Server error",
|
|
||||||
description:
|
|
||||||
"Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return {
|
|
||||||
data: data,
|
|
||||||
status: "success",
|
|
||||||
title: null,
|
|
||||||
description: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { UserContext } from "@/states/Context";
|
|
||||||
|
|
||||||
export const ProtectedRoutes = () => {
|
|
||||||
const user = useContext(UserContext);
|
|
||||||
const location = useLocation();
|
|
||||||
const hasToken = Boolean(Cookies.get("token"));
|
|
||||||
|
|
||||||
if (hasToken && !user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user ? (
|
|
||||||
<Outlet />
|
|
||||||
) : (
|
|
||||||
<Navigate to="/login" replace state={{ from: location }} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,jsx,ts,tsx}",
|
|
||||||
// add other paths if needed
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true,
|
|
||||||
|
|
||||||
/* Path aliases */
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["node"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineConfig } from "vite";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import svgr from "vite-plugin-svgr";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
|
||||||
server: {
|
|
||||||
host: "0.0.0.0",
|
|
||||||
port: 8001,
|
|
||||||
watch: {
|
|
||||||
usePolling: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
276
Mock/AppMockup.tsx
Normal file
276
Mock/AppMockup.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// Beispiel-Daten für die Übersicht in der Seitenleiste
|
||||||
|
const allItems = [
|
||||||
|
{ id: 1, name: "Kamera" },
|
||||||
|
{ id: 2, name: "Mikrofon" },
|
||||||
|
{ id: 3, name: "Licht-Set" },
|
||||||
|
{ id: 4, name: "Stativ" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Beispiel-Ausleihen, später per API dynamisch!
|
||||||
|
const loans = [
|
||||||
|
{
|
||||||
|
itemId: 1,
|
||||||
|
username: "max",
|
||||||
|
start: "2025-01-01T08:00",
|
||||||
|
end: "2025-01-01T18:00",
|
||||||
|
loanCode: "123456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemId: 3,
|
||||||
|
username: "sara",
|
||||||
|
start: "2025-01-02T10:00",
|
||||||
|
end: "2025-01-02T16:00",
|
||||||
|
loanCode: "654321",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dummy: Für das Beispiel sind einige Items "nicht verfügbar" bei bestimmten Zeiträumen
|
||||||
|
function getAvailableItems(start: string, end: string) {
|
||||||
|
if (start.startsWith("2025-01-01")) {
|
||||||
|
return allItems.filter(
|
||||||
|
(item) => item.name === "Kamera" || item.name === "Stativ"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return allItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||||
|
const [startDate, setStartDate] = useState("");
|
||||||
|
const [endDate, setEndDate] = useState("");
|
||||||
|
const [availableItems, setAvailableItems] = useState<typeof allItems>([]);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Dummy Code für das Design
|
||||||
|
const loanCode = "123456";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex bg-gradient-to-r from-blue-50 via-white to-blue-100">
|
||||||
|
{/* Seitenleiste */}
|
||||||
|
<aside className="w-80 min-h-screen bg-white/90 backdrop-blur border-r border-blue-100 shadow-xl flex flex-col p-8">
|
||||||
|
<h2 className="text-2xl font-extrabold mb-6 text-blue-700 tracking-tight flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
className="w-7 h-7 text-blue-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M16.5 7.5V4.75A2.25 2.25 0 0 0 14.25 2.5h-4.5A2.25 2.25 0 0 0 7.5 4.75V7.5m9 0h-9m9 0v11.75A2.25 2.25 0 0 1 14.25 21.5h-4.5A2.25 2.25 0 0 1 7.5 19.25V7.5m9 0h-9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Ausleih-Übersicht
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-5 flex-1">
|
||||||
|
{allItems.map((item) => {
|
||||||
|
const itemLoans = loans.filter((loan) => loan.itemId === item.id);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white/80 rounded-xl p-4 shadow hover:shadow-md transition"
|
||||||
|
>
|
||||||
|
<div className="font-semibold text-gray-900 flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="inline-block w-2 h-2 rounded-full"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
itemLoans.length === 0 ? "#34d399" : "#60a5fa",
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
{itemLoans.length === 0 ? (
|
||||||
|
<div className="text-green-500 text-xs mt-1 font-medium">
|
||||||
|
Verfügbar
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
{itemLoans.map((loan, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="text-xs text-blue-800 bg-blue-100/60 p-1 rounded"
|
||||||
|
>
|
||||||
|
<span className="font-bold">{loan.username}</span>
|
||||||
|
<span className="ml-2">
|
||||||
|
{formatDateTime(loan.start)} –{" "}
|
||||||
|
{formatDateTime(loan.end)}
|
||||||
|
</span>
|
||||||
|
<span className="ml-2 text-gray-400">
|
||||||
|
({loan.loanCode})
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-10 text-xs text-gray-400 flex items-center gap-4">
|
||||||
|
<span className="inline-block w-3 h-3 bg-green-400 rounded-full mr-1"></span>
|
||||||
|
Verfügbar
|
||||||
|
<span className="inline-block w-3 h-3 bg-blue-400 rounded-full ml-4 mr-1"></span>
|
||||||
|
Verliehen
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Hauptbereich */}
|
||||||
|
<main className="flex-1 flex flex-col items-center py-14 px-4">
|
||||||
|
<header className="mb-12">
|
||||||
|
<h1 className="text-4xl font-extrabold text-blue-800 tracking-tight drop-shadow-sm">
|
||||||
|
Gegenstand ausleihen
|
||||||
|
</h1>
|
||||||
|
<p className="text-blue-400 mt-2 text-lg font-medium">
|
||||||
|
Schnell und unkompliziert Equipment reservieren
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div className="bg-white/90 shadow-2xl rounded-3xl p-10 w-full max-w-xl ring-1 ring-blue-100">
|
||||||
|
{step === 1 && (
|
||||||
|
<form
|
||||||
|
className="space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAvailableItems(getAvailableItems(startDate, endDate));
|
||||||
|
setStep(2);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-bold mb-2 text-blue-700">
|
||||||
|
1. Zeitraum wählen
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium mb-1 text-blue-900">
|
||||||
|
Startdatum
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="w-full border border-blue-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:outline-none"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block font-medium mb-1 text-blue-900">
|
||||||
|
Enddatum
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="w-full border border-blue-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:outline-none"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
required
|
||||||
|
min={startDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-blue-400 hover:from-blue-700 hover:to-blue-500 text-white font-bold py-2 px-4 rounded-xl shadow transition disabled:bg-gray-300"
|
||||||
|
disabled={!startDate || !endDate || endDate <= startDate}
|
||||||
|
>
|
||||||
|
Verfügbare Gegenstände anzeigen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-6 text-blue-700">
|
||||||
|
2. Gegenstand auswählen
|
||||||
|
</h2>
|
||||||
|
{availableItems.length === 0 ? (
|
||||||
|
<div className="text-red-600 mb-8 font-medium text-center">
|
||||||
|
Keine Gegenstände verfügbar für diesen Zeitraum.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="mb-8 space-y-3">
|
||||||
|
{availableItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={`flex justify-between items-center p-4 rounded-xl shadow-sm border ${
|
||||||
|
selectedItem === item.id
|
||||||
|
? "bg-blue-100 border-blue-400"
|
||||||
|
: "bg-green-50 border-green-100 hover:bg-blue-50"
|
||||||
|
} transition`}
|
||||||
|
>
|
||||||
|
<span className="font-medium text-lg">{item.name}</span>
|
||||||
|
<button
|
||||||
|
className={`px-4 py-1 rounded-lg bg-gradient-to-r from-blue-500 to-blue-400 text-white text-sm font-semibold shadow hover:from-blue-600 hover:to-blue-500 ${
|
||||||
|
selectedItem === item.id ? "ring-2 ring-blue-400" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedItem(item.id)}
|
||||||
|
>
|
||||||
|
{selectedItem === item.id ? "Ausgewählt" : "Auswählen"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<button
|
||||||
|
className="px-5 py-2 bg-gray-100 text-gray-600 rounded-xl hover:bg-gray-200 font-semibold shadow"
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-5 py-2 bg-gradient-to-r from-blue-600 to-blue-400 text-white rounded-xl hover:from-blue-700 hover:to-blue-500 font-bold shadow transition disabled:bg-gray-300"
|
||||||
|
disabled={selectedItem === null}
|
||||||
|
onClick={() => setStep(3)}
|
||||||
|
>
|
||||||
|
Ausleihen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="mt-2 p-8 bg-blue-50/80 border border-blue-200 rounded-2xl text-center shadow-lg">
|
||||||
|
<h3 className="font-extrabold text-blue-700 mb-3 text-2xl">
|
||||||
|
Ausleihe bestätigt!
|
||||||
|
</h3>
|
||||||
|
<p className="mb-2 text-lg">
|
||||||
|
Ihr Ausleih-Code lautet:{" "}
|
||||||
|
<span className="font-mono text-2xl text-blue-900 bg-white px-2 py-1 rounded shadow">
|
||||||
|
{loanCode}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-blue-600 text-sm">
|
||||||
|
Bitte merken Sie sich diesen Code, um das Schließfach zu öffnen.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="mt-8 px-6 py-2 bg-gradient-to-r from-blue-600 to-blue-400 text-white rounded-xl hover:from-blue-700 hover:to-blue-500 font-bold shadow"
|
||||||
|
onClick={() => {
|
||||||
|
setStep(1);
|
||||||
|
setStartDate("");
|
||||||
|
setEndDate("");
|
||||||
|
setSelectedItem(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Neue Ausleihe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion: Datumsformatierung (z.B. 01.01.2025 08:00)
|
||||||
|
function formatDateTime(dt: string) {
|
||||||
|
const d = new Date(dt);
|
||||||
|
return (
|
||||||
|
d.toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
}) +
|
||||||
|
" " +
|
||||||
|
d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })
|
||||||
|
);
|
||||||
|
}
|
||||||
20
Mock/frontendMockData.json
Normal file
20
Mock/frontendMockData.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Mock Book 1",
|
||||||
|
"author": "Author 1",
|
||||||
|
"description": "Description for Mock Book 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Mock Book 2",
|
||||||
|
"author": "Author 2",
|
||||||
|
"description": "Description for Mock Book 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Mock Book 3",
|
||||||
|
"author": "Author 3",
|
||||||
|
"description": "Description for Mock Book 3"
|
||||||
|
}
|
||||||
|
]
|
||||||
72
README.md
72
README.md
@@ -1,73 +1,7 @@
|
|||||||
# Borrow System
|
# Borrow System
|
||||||
|
|
||||||

|
**You have reached the `debian12` branch.**
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
A small full‑stack system to log in, view available items, reserve them for a time window, and manage personal loans.
|
Here you will find the source code of exactly the application that I have hosted.
|
||||||
|
|
||||||
- Frontend: React + TypeScript + Vite + Tailwind CSS
|
The main branch or the branch that I am developing on, is the `dev` branch.
|
||||||
- Backend: Node.js + Express + MySQL + JWT (jose)
|
|
||||||
- Orchestration: Docker Compose (backend + MySQL)
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
- Frontend: [frontend/](frontend)
|
|
||||||
- Vite/Tailwind config: [frontend/vite.config.ts](frontend/vite.config.ts), [frontend/tailwind.config.js](frontend/tailwind.config.js)
|
|
||||||
- App entry: [frontend/src/main.tsx](frontend/src/main.tsx), [frontend/src/App.tsx](frontend/src/App.tsx)
|
|
||||||
- UI: [frontend/src/layout/Layout.tsx](frontend/src/layout/Layout.tsx), [frontend/src/components](frontend/src/components)
|
|
||||||
- Data/utilities: [frontend/src/utils/fetchData.ts](frontend/src/utils/fetchData.ts), [frontend/src/utils/userHandler.ts](frontend/src/utils/userHandler.ts), [frontend/src/utils/toastify.ts](frontend/src/utils/toastify.ts)
|
|
||||||
- Backend: [backend/](backend)
|
|
||||||
- Server: [backend/server.js](backend/server.js)
|
|
||||||
- Routes: [backend/routes/api.js](backend/routes/api.js), [backend/routes/apiV2.js](backend/routes/apiV2.js)
|
|
||||||
- DB + services: [backend/services/database.js](backend/services/database.js), [backend/services/tokenService.js](backend/services/tokenService.js)
|
|
||||||
- Schema/seed: [backend/scheme.sql](backend/scheme.sql)
|
|
||||||
- Docs: [docs/](docs)
|
|
||||||
- API docs (see below): [docs/backend_API_docs/README.md](docs/backend_API_docs/README.md)
|
|
||||||
|
|
||||||
## Features (high‑level)
|
|
||||||
|
|
||||||
- Auth via JWT (login -> token cookie) using the backend route in [backend/routes/api.js](backend/routes/api.js).
|
|
||||||
- After login, the app loads items, loans, and user loans and keeps them in localStorage.
|
|
||||||
- Choose a date range to fetch borrowable items, select items, and create a loan.
|
|
||||||
- Manage personal loans list (and delete a loan).
|
|
||||||
|
|
||||||
Key frontend utilities:
|
|
||||||
|
|
||||||
- [`utils.fetchData.fetchAllData`](frontend/src/utils/fetchData.ts): loads items, loans, and user loans after login.
|
|
||||||
- [`utils.fetchData.getBorrowableItems`](frontend/src/utils/fetchData.ts): fetches borrowable items for the selected time range.
|
|
||||||
- [`utils.userHandler.createLoan`](frontend/src/utils/userHandler.ts): creates a new loan for selected items.
|
|
||||||
- [`utils.userHandler.handleDeleteLoan`](frontend/src/utils/userHandler.ts): deletes a loan and syncs local state.
|
|
||||||
- [`utils.toastify.myToast`](frontend/src/utils/toastify.ts): toast notifications.
|
|
||||||
|
|
||||||
UI flow (main screens):
|
|
||||||
|
|
||||||
- Period selection: [frontend/src/components/Form1.tsx](frontend/src/components/Form1.tsx)
|
|
||||||
- Borrowable items + selection: [frontend/src/components/Form2.tsx](frontend/src/components/Form2.tsx)
|
|
||||||
- User loans table: [frontend/src/components/Form4.tsx](frontend/src/components/Form4.tsx)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- Scripts: see [frontend/package.json](frontend/package.json) and [backend/package.json](backend/package.json)
|
|
||||||
- Frontend: `npm run dev`, `npm run build`, `npm run preview`, `npm run lint`
|
|
||||||
- Backend: `npm start`
|
|
||||||
- Linting: ESLint configured via [frontend/eslint.config.js](frontend/eslint.config.js)
|
|
||||||
- TypeScript configs: [frontend/tsconfig.app.json](frontend/tsconfig.app.json), [frontend/tsconfig.node.json](frontend/tsconfig.node.json)
|
|
||||||
|
|
||||||
## Configuration notes
|
|
||||||
|
|
||||||
- Vite/Tailwind integration via [frontend/vite.config.ts](frontend/vite.config.ts) and `@tailwindcss/vite`; CSS entry uses `@import "tailwindcss"` in [frontend/src/index.css](frontend/src/index.css).
|
|
||||||
- Toasts wired in [frontend/src/main.tsx](frontend/src/main.tsx) with `react-toastify`.
|
|
||||||
- Local state is stored in `localStorage` keys: `allItems`, `allLoans`, `userLoans`, `borrowableItems`. Cross‑component updates are signaled via window events from [`utils.fetchData`](frontend/src/utils/fetchData.ts).
|
|
||||||
|
|
||||||
## API documentation
|
|
||||||
|
|
||||||
Refer to the dedicated API docs:
|
|
||||||
|
|
||||||
`docs/backend_API_docs/README.md`
|
|
||||||
48
admin/package-lock.json
generated
48
admin/package-lock.json
generated
@@ -12,7 +12,6 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -4421,35 +4420,6 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jotai": {
|
|
||||||
"version": "2.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz",
|
|
||||||
"integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@babel/core": ">=7.0.0",
|
|
||||||
"@babel/template": ">=7.0.0",
|
|
||||||
"@types/react": ">=17.0.0",
|
|
||||||
"react": ">=17.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@babel/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@babel/template": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
@@ -5675,13 +5645,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.4.4",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -5879,9 +5849,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.11",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
|
||||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
"integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -5889,7 +5859,7 @@
|
|||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.14"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
export const testAtom = atom<number>(0);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# How to use Atoms
|
|
||||||
Atoms are the fundamental building blocks of state management in this system. They represent individual pieces of state that can be shared and manipulated across different components.
|
|
||||||
|
|
||||||
You can also name it global state.
|
|
||||||
|
|
||||||
## Creating an Atom
|
|
||||||
to create an atom you have to declare an atom like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
export const NAME_OF_YOUR_ATOM = atom<type_of_your_atom>(initial_value);
|
|
||||||
```
|
|
||||||
|
|
||||||
In this project we declare all atoms in the `States/Atoms.tsx`file. Which you can find above this README file.
|
|
||||||
|
|
||||||
## Using an Atom
|
|
||||||
To use an atom in your component, you can use the `useAtom` hook provided by Jotai. Here's an example of how to use an atom in a React component:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { NAME_OF_YOUR_ATOM } from '@/States/Atoms';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const [value, setValue] = useAtom(NAME_OF_YOUR_ATOM);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Current value: {value}</p>
|
|
||||||
<button onClick={() => setValue(newValue)}>Update Value</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
As you can see, you can use `useAtom` like `useState` but the state is global. In this example `value` is the current state of the atom, and `setValue` is a function to update the state, which is also known as the setter function.
|
|
||||||
@@ -11,15 +11,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Lock, LockOpen } from "lucide-react";
|
import { Lock, LockOpen } from "lucide-react";
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
import MyAlert from "../myChakra/MyAlert";
|
||||||
|
import { formatDateTime } from "@/utils/userFuncs";
|
||||||
export const formatDateTime = (value: string | null | undefined) => {
|
|
||||||
if (!value) return "N/A";
|
|
||||||
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
|
||||||
if (!m) return "N/A";
|
|
||||||
const [, y, M, d, h, min] = m;
|
|
||||||
return `${d}.${M}.${y} ${h}:${min} Uhr`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const API_BASE =
|
const API_BASE =
|
||||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||||
@@ -8,9 +8,13 @@ export default defineConfig({
|
|||||||
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 8003,
|
allowedHosts: ["admin.insta.the1s.de"],
|
||||||
watch: {
|
port: 8103,
|
||||||
usePolling: true,
|
watch: { usePolling: true },
|
||||||
|
hmr: {
|
||||||
|
host: "admin.insta.the1s.de",
|
||||||
|
port: 8103,
|
||||||
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8002
|
EXPOSE 8102
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
createAPIentry,
|
createAPIentry,
|
||||||
deleteAPKey,
|
deleteAPKey,
|
||||||
getLoanInfoWithID,
|
getLoanInfoWithID,
|
||||||
SETdeleteLoanFromDatabase,
|
|
||||||
} from "../services/database.js";
|
} from "../services/database.js";
|
||||||
import { authenticate, generateToken } from "../services/tokenService.js";
|
import { authenticate, generateToken } from "../services/tokenService.js";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -262,16 +261,6 @@ router.delete("/deleteLoan/:id", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/SETdeleteLoan/:id", authenticate, async (req, res) => {
|
|
||||||
const loanId = req.params.id;
|
|
||||||
const result = await SETdeleteLoanFromDatabase(loanId);
|
|
||||||
if (result.success) {
|
|
||||||
res.status(200).json({ message: "Loan deleted successfully" });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ message: "Failed to delete loan" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/borrowableItems", authenticate, async (req, res) => {
|
router.post("/borrowableItems", authenticate, async (req, res) => {
|
||||||
const { startDate, endDate } = req.body || {};
|
const { startDate, endDate } = req.body || {};
|
||||||
if (!startDate || !endDate) {
|
if (!startDate || !endDate) {
|
||||||
@@ -443,7 +432,7 @@ router.delete("/deleteUser/:id", authenticate, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/verifyToken", authenticate, async (req, res) => {
|
router.get("/verifyToken", authenticate, async (req, res) => {
|
||||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
res.status(200).json({ message: "Token is valid" });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/editUser/:id", authenticate, async (req, res) => {
|
router.post("/editUser/:id", authenticate, async (req, res) => {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ CREATE TABLE `loans` (
|
|||||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`loaned_items_id` json NOT NULL DEFAULT ('[]'),
|
`loaned_items_id` json NOT NULL DEFAULT ('[]'),
|
||||||
`loaned_items_name` json NOT NULL DEFAULT ('[]'),
|
`loaned_items_name` json NOT NULL DEFAULT ('[]'),
|
||||||
`deleted` bool NOT NULL DEFAULT false,
|
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `loan_code` (`loan_code`)
|
UNIQUE KEY `loan_code` (`loan_code`)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import apiRouter from "./routes/api.js";
|
|||||||
import apiRouterV2 from "./routes/apiV2.js";
|
import apiRouterV2 from "./routes/apiV2.js";
|
||||||
env.config();
|
env.config();
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 8002;
|
const port = 8102;
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
// Increase body size limits to support large CSV JSON payloads
|
// Increase body size limits to support large CSV JSON payloads
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const pool = mysql
|
|||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
})
|
})
|
||||||
.promise();
|
.promise();
|
||||||
|
|
||||||
@@ -126,10 +127,9 @@ export const getLoansFromDatabase = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getUserLoansFromDatabase = async (username) => {
|
export const getUserLoansFromDatabase = async (username) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query("SELECT * FROM loans WHERE username = ?;", [
|
||||||
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
username,
|
||||||
[username]
|
]);
|
||||||
);
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
} else if (result.length == 0) {
|
} else if (result.length == 0) {
|
||||||
@@ -150,18 +150,6 @@ export const deleteLoanFromDatabase = async (loanId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
|
||||||
[loanId]
|
|
||||||
);
|
|
||||||
if (result.affectedRows > 0) {
|
|
||||||
return { success: true };
|
|
||||||
} else {
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBorrowableItemsFromDatabase = async (
|
export const getBorrowableItemsFromDatabase = async (
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
|
|||||||
@@ -1,35 +1,45 @@
|
|||||||
services:
|
services:
|
||||||
# borrow_system-frontend:
|
borrow_system-frontend:
|
||||||
# container_name: borrow_system-frontend
|
container_name: borrow_system-frontend
|
||||||
# build: ./FrontendV2
|
build: ./frontend
|
||||||
# ports:
|
ports:
|
||||||
# - "8001:8001"
|
- "8101:8101"
|
||||||
# environment:
|
networks:
|
||||||
# - CHOKIDAR_USEPOLLING=true
|
- proxynet
|
||||||
# volumes:
|
- borrow_system-internal
|
||||||
# - ./FrontendV2:/app
|
environment:
|
||||||
# - /app/node_modules
|
- CHOKIDAR_USEPOLLING=true
|
||||||
# restart: unless-stopped
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
# admin-frontend:
|
admin-frontend:
|
||||||
# container_name: admin-frontend
|
container_name: admin-frontend
|
||||||
# build: ./admin
|
build: ./admin
|
||||||
# ports:
|
networks:
|
||||||
# - "8003:8003"
|
- proxynet
|
||||||
# environment:
|
- borrow_system-internal
|
||||||
# - CHOKIDAR_USEPOLLING=true
|
ports:
|
||||||
# volumes:
|
- "8103:8103"
|
||||||
# - ./admin:/app
|
environment:
|
||||||
# - /app/node_modules
|
- CHOKIDAR_USEPOLLING=true
|
||||||
# restart: unless-stopped
|
volumes:
|
||||||
|
- ./admin:/app
|
||||||
|
- /app/node_modules
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
borrow_system-backend:
|
borrow_system-backend:
|
||||||
container_name: borrow_system-backend
|
container_name: borrow_system-backend
|
||||||
build: ./backend
|
build: ./backend
|
||||||
ports:
|
ports:
|
||||||
- "8002:8002"
|
- "8102:8102"
|
||||||
|
networks:
|
||||||
|
- proxynet
|
||||||
|
- borrow_system-internal
|
||||||
environment:
|
environment:
|
||||||
DB_HOST: mysql
|
DB_HOST: mysql
|
||||||
|
DB_PORT: 3306
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
DB_NAME: borrow_system
|
DB_NAME: borrow_system
|
||||||
@@ -52,6 +62,14 @@ services:
|
|||||||
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
|
||||||
ports:
|
ports:
|
||||||
- "3309:3306"
|
- "3309:3306"
|
||||||
|
networks:
|
||||||
|
- borrow_system-internal
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxynet:
|
||||||
|
external: true
|
||||||
|
borrow_system-internal:
|
||||||
|
external: false
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ RUN npm install
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8001
|
EXPOSE 8101
|
||||||
|
|
||||||
CMD ["npm", "run", "dev"]
|
CMD ["npm", "run", "dev"]
|
||||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -10,7 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@@ -3165,35 +3164,6 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jotai": {
|
|
||||||
"version": "2.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz",
|
|
||||||
"integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@babel/core": ">=7.0.0",
|
|
||||||
"@babel/template": ">=7.0.0",
|
|
||||||
"@types/react": ">=17.0.0",
|
|
||||||
"react": ">=17.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@babel/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@babel/template": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
@@ -4322,13 +4292,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.4.4",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -4499,17 +4469,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.12",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
||||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.4.6",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup": "^4.43.0",
|
"rollup": "^4.43.0",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.14"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"jotai": "^2.15.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Layout from "./layout/Layout";
|
import Layout from "./layout/Layout";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AddLoan } from "./components/AddLoan";
|
import Form1 from "./components/Form1";
|
||||||
|
import Form2 from "./components/Form2";
|
||||||
|
import Form4 from "./components/Form4";
|
||||||
import LoginForm from "./components/LoginForm";
|
import LoginForm from "./components/LoginForm";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import {
|
import {
|
||||||
@@ -44,7 +46,13 @@ function App() {
|
|||||||
|
|
||||||
return isLoggedIn ? (
|
return isLoggedIn ? (
|
||||||
<Layout onLogout={handleLogout}>
|
<Layout onLogout={handleLogout}>
|
||||||
<AddLoan />
|
<div className="space-y-6">
|
||||||
|
<Form1 />
|
||||||
|
<div className="h-px bg-slate-200" />
|
||||||
|
<Form2 />
|
||||||
|
<div className="h-px bg-slate-200" />
|
||||||
|
<Form4 />
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
) : (
|
) : (
|
||||||
<LoginForm onLogin={() => setIsLoggedIn(true)} />
|
<LoginForm onLogin={() => setIsLoggedIn(true)} />
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
// Atoms to store the start and end dates for loans
|
|
||||||
export const startDate = atom<string | null>(null);
|
|
||||||
export const endDate = atom<string | null>(null);
|
|
||||||
export const getBorrowableItemsAtom = atom<string[] | boolean>(false);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# How to use Atoms
|
|
||||||
Atoms are the fundamental building blocks of state management in this system. They represent individual pieces of state that can be shared and manipulated across different components.
|
|
||||||
|
|
||||||
You can also name it global state.
|
|
||||||
|
|
||||||
## Creating an Atom
|
|
||||||
to create an atom you have to declare an atom like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
export const NAME_OF_YOUR_ATOM = atom<type_of_your_atom>(initial_value);
|
|
||||||
```
|
|
||||||
|
|
||||||
In this project we declare all atoms in the `States/Atoms.tsx`file. Which you can find above this README file.
|
|
||||||
|
|
||||||
## Using an Atom
|
|
||||||
To use an atom in your component, you can use the `useAtom` hook provided by Jotai. Here's an example of how to use an atom in a React component:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { NAME_OF_YOUR_ATOM } from '@/States/Atoms';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const [value, setValue] = useAtom(NAME_OF_YOUR_ATOM);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Current value: {value}</p>
|
|
||||||
<button onClick={() => setValue(newValue)}>Update Value</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
As you can see, you can use `useAtom` like `useState` but the state is global. In this example `value` is the current state of the atom, and `setValue` is a function to update the state, which is also known as the setter function.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { getBorrowableItems } from "../utils/fetchData";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { startDate, endDate } from "../States/Atoms";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
|
|
||||||
export const AddLoan = () => {
|
|
||||||
const [start, setStart] = useAtom(startDate);
|
|
||||||
const [end, setEnd] = useAtom(endDate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-lg sm:text-xl font-bold text-slate-900">
|
|
||||||
1. Zeitraum wählen
|
|
||||||
</h2>
|
|
||||||
<form
|
|
||||||
className="space-y-3"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.currentTarget as HTMLFormElement;
|
|
||||||
const fd = new FormData(form);
|
|
||||||
const start = (fd.get("startDate") as string) || "";
|
|
||||||
const end = (fd.get("endDate") as string) || "";
|
|
||||||
setStart(start);
|
|
||||||
setEnd(end);
|
|
||||||
Cookies.set("startDate", start);
|
|
||||||
Cookies.set("endDate", end);
|
|
||||||
getBorrowableItems();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="startDate"
|
|
||||||
className="block text-sm font-medium text-slate-700 mb-1"
|
|
||||||
>
|
|
||||||
Start
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
id="startDate"
|
|
||||||
name="startDate"
|
|
||||||
className="w-full border border-slate-300 rounded-lg px-3 py-2.5 focus:ring-2 focus:ring-indigo-500 focus:outline-none bg-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="endDate"
|
|
||||||
className="block text-sm font-medium text-slate-700 mb-1"
|
|
||||||
>
|
|
||||||
Ende
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
id="endDate"
|
|
||||||
name="endDate"
|
|
||||||
className="w-full border border-slate-300 rounded-lg px-3 py-2.5 focus:ring-2 focus:ring-indigo-500 focus:outline-none bg-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-indigo-600 text-white font-bold py-2.5 px-4 rounded-lg shadow hover:bg-indigo-700 transition"
|
|
||||||
>
|
|
||||||
Verfügbare Gegenstände anzeigen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { myToast } from "./toastify";
|
import { myToast } from "./toastify";
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { getBorrowableItemsAtom } from "../States/Atoms";
|
|
||||||
|
|
||||||
// Event name used to notify the app when the list of items has been updated
|
// Event name used to notify the app when the list of items has been updated
|
||||||
export const ALL_ITEMS_UPDATED_EVENT = "allItemsUpdated";
|
export const ALL_ITEMS_UPDATED_EVENT = "allItemsUpdated";
|
||||||
@@ -192,7 +190,6 @@ export const getBorrowableItems = async () => {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
localStorage.setItem("borrowableItems", JSON.stringify(data));
|
localStorage.setItem("borrowableItems", JSON.stringify(data));
|
||||||
|
|
||||||
window.dispatchEvent(new Event(BORROWABLE_ITEMS_UPDATED_EVENT)); // notify same-tab listeners
|
window.dispatchEvent(new Event(BORROWABLE_ITEMS_UPDATED_EVENT)); // notify same-tab listeners
|
||||||
console.log("Borrowable items fetched successfully");
|
console.log("Borrowable items fetched successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import svgr from "vite-plugin-svgr";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svgr(), tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 8001,
|
allowedHosts: ["insta.the1s.de"],
|
||||||
watch: {
|
port: 8101,
|
||||||
usePolling: true,
|
watch: { usePolling: true },
|
||||||
|
hmr: {
|
||||||
|
host: "insta.the1s.de",
|
||||||
|
port: 8101,
|
||||||
|
protocol: "wss",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
other/data_structure.xlsx
Normal file
BIN
other/data_structure.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user