Remove unused components and files from the frontend, including Form4, Header, LoginForm, Object, Sidebar, and related utility functions. Clean up the project structure by deleting unnecessary CSS, TypeScript configuration files, and Vite configuration. This refactor aims to streamline the codebase and improve maintainability.
This commit is contained in:
@@ -1 +1 @@
|
||||
<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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shapes-icon lucide-shapes"><path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"/><rect x="3" y="14" width="7" height="7" rx="1"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 420 B |
@@ -1,6 +1,29 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { useEffect } from "react";
|
||||
import { infoAtom } from "@/states/Atoms";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const Footer = () => {
|
||||
const [info, setInfo] = useAtom(infoAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const metaData = async () => {
|
||||
const response = await fetch(`${API_BASE}/server-info`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setInfo(data);
|
||||
}
|
||||
};
|
||||
metaData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="footer"
|
||||
@@ -13,7 +36,8 @@ export const Footer = () => {
|
||||
>
|
||||
Made with ❤️ by Theis Gaedigk - Year 2019 at MCS-Bochum
|
||||
<br />
|
||||
v2.0
|
||||
Frontend-Version: {info ? info["frontend-info"].version : "N/A"} |
|
||||
Backend-Version: {info ? info["backend-info"].version : "N/A"}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export const HomePage = () => {
|
||||
/>
|
||||
)}
|
||||
<Stack as="main">
|
||||
<Text>{t("timezone-info")}</Text>
|
||||
<label htmlFor="startDate">
|
||||
<Text>{t("start-date")}</Text>
|
||||
</label>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
interface Meta {
|
||||
"backend-info": {
|
||||
version: String;
|
||||
};
|
||||
"frontend-info": {
|
||||
version: String;
|
||||
};
|
||||
}
|
||||
|
||||
export const testAtom = atom<number>(0);
|
||||
export const setIsLoggedInAtom = atom<boolean>(false);
|
||||
export const triggerLogoutAtom = atom<boolean>(false);
|
||||
export const borrowAbleItemsAtom = atom<any[]>([]);
|
||||
export const infoAtom = atom<Meta | undefined>(undefined);
|
||||
|
||||
@@ -59,5 +59,6 @@
|
||||
"sure-delete-loan-1": " Ausleihcode wirklich löschen?",
|
||||
"sure-delete-loan-2": "Für den Admin bleibt sie weiterhin sichtbar.",
|
||||
"delete": "Löschen",
|
||||
"change-language": "Sprache ändern"
|
||||
"change-language": "Sprache ändern",
|
||||
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden."
|
||||
}
|
||||
@@ -59,5 +59,6 @@
|
||||
"sure-delete-loan-1": " loan code?",
|
||||
"sure-delete-loan-2": "It will remain visible to the admin.",
|
||||
"delete": "Delete",
|
||||
"change-language": "Change language"
|
||||
"change-language": "Change language",
|
||||
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such."
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"backend-info": {
|
||||
"version": "v2.0"
|
||||
"version": "v2.0 (dev)"
|
||||
},
|
||||
"frontend-info": {
|
||||
"version": "v2.0"
|
||||
"version": "v2.0 (dev)"
|
||||
}
|
||||
}
|
||||
24
frontend/.gitignore
vendored
24
frontend/.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,69 +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/) 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
|
||||
|
||||
## 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 tseslint.config([
|
||||
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 tseslint.config([
|
||||
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 { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
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="/shapes.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ausleihsystem</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4664
frontend/package-lock.json
generated
4664
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shapes-icon lucide-shapes"><path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"/><rect x="3" y="14" width="7" height="7" rx="1"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
||||
|
Before Width: | Height: | Size: 420 B |
@@ -1 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,54 +0,0 @@
|
||||
import "./App.css";
|
||||
import Layout from "./layout/Layout";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AddLoan } from "./components/AddLoan";
|
||||
import LoginForm from "./components/LoginForm";
|
||||
import Cookies from "js-cookie";
|
||||
import {
|
||||
fetchAllData,
|
||||
ALL_ITEMS_UPDATED_EVENT,
|
||||
AUTH_LOGOUT_EVENT,
|
||||
} from "./utils/fetchData";
|
||||
import { myToast } from "./utils/toastify";
|
||||
|
||||
function App() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const token = Cookies.get("token");
|
||||
if (token) {
|
||||
setIsLoggedIn(true);
|
||||
fetchAllData(token);
|
||||
}
|
||||
localStorage.setItem("borrowableItems", JSON.stringify([]));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onAuthLogout = () => {
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
window.addEventListener(AUTH_LOGOUT_EVENT, onAuthLogout);
|
||||
return () => window.removeEventListener(AUTH_LOGOUT_EVENT, onAuthLogout);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
Cookies.remove("token");
|
||||
localStorage.removeItem("allItems");
|
||||
localStorage.removeItem("allLoans");
|
||||
localStorage.removeItem("userLoans");
|
||||
localStorage.removeItem("borrowableItems");
|
||||
window.dispatchEvent(new Event(ALL_ITEMS_UPDATED_EVENT));
|
||||
myToast("Logged out successfully!", "success");
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
|
||||
return isLoggedIn ? (
|
||||
<Layout onLogout={handleLogout}>
|
||||
<AddLoan />
|
||||
</Layout>
|
||||
) : (
|
||||
<LoginForm onLogin={() => setIsLoggedIn(true)} />
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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 +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,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,12 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="fixed bottom-0 left-0 text-sm w-full bg-slate-100 text-center py-2 border-t border-slate-200 z-50">
|
||||
<p>Made with ❤️ by Theis Gaedigk - Jahrgang 2019</p>
|
||||
<p>v1.1</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { getBorrowableItems } from "../utils/fetchData";
|
||||
|
||||
const Form1: React.FC = () => {
|
||||
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) || "";
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form1;
|
||||
@@ -1,186 +0,0 @@
|
||||
import React from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { createLoan, addToRemove, rmFromRemove } from "../utils/userHandler";
|
||||
import { BORROWABLE_ITEMS_UPDATED_EVENT } from "../utils/fetchData";
|
||||
|
||||
interface BorrowItem {
|
||||
id: number;
|
||||
item_name: string;
|
||||
can_borrow_role: string;
|
||||
inSafe: number;
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_KEY = "borrowableItems";
|
||||
|
||||
function normalizeBorrowable(data: any): BorrowItem[] {
|
||||
const rawArr = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray(data?.items)
|
||||
? data.items
|
||||
: Array.isArray(data?.data)
|
||||
? data.data
|
||||
: [];
|
||||
|
||||
return rawArr
|
||||
.map((raw: any) => {
|
||||
const idRaw =
|
||||
raw.id ?? raw.item_id ?? raw.itemId ?? raw.itemID ?? raw.itemIdPk;
|
||||
const id = Number(idRaw);
|
||||
const item_name = String(raw.item_name ?? raw.name ?? raw.title ?? "");
|
||||
const can_borrow_role = String(
|
||||
raw.can_borrow_role ?? raw.role ?? raw.requiredRole ?? ""
|
||||
);
|
||||
const inSafeRaw =
|
||||
raw.inSafe ?? raw.in_safe ?? raw.inLocker ?? raw.isInSafe ?? raw.safe;
|
||||
const inSafe =
|
||||
typeof inSafeRaw === "boolean"
|
||||
? Number(inSafeRaw)
|
||||
: Number(isNaN(Number(inSafeRaw)) ? 0 : Number(inSafeRaw));
|
||||
|
||||
if (!Number.isFinite(id) || !item_name) return null;
|
||||
return { id, item_name, can_borrow_role, inSafe };
|
||||
})
|
||||
.filter(Boolean) as BorrowItem[];
|
||||
}
|
||||
|
||||
function useBorrowableItems() {
|
||||
const [items, setItems] = React.useState<BorrowItem[]>([]);
|
||||
|
||||
const readFromStorage = React.useCallback(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(LOCAL_STORAGE_KEY) || "[]";
|
||||
const parsed = JSON.parse(raw);
|
||||
const arr = normalizeBorrowable(parsed);
|
||||
setItems(arr);
|
||||
} catch {
|
||||
setItems([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
readFromStorage();
|
||||
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === LOCAL_STORAGE_KEY) readFromStorage();
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
|
||||
const onBorrowableUpdated = () => readFromStorage();
|
||||
window.addEventListener(
|
||||
BORROWABLE_ITEMS_UPDATED_EVENT,
|
||||
onBorrowableUpdated
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", onStorage);
|
||||
window.removeEventListener(
|
||||
BORROWABLE_ITEMS_UPDATED_EVENT,
|
||||
onBorrowableUpdated
|
||||
);
|
||||
};
|
||||
}, [readFromStorage]);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const Form2: React.FC = () => {
|
||||
const items = useBorrowableItems();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg sm:text-xl font-bold text-slate-900">
|
||||
2. Gegenstand auswählen
|
||||
</h2>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-slate-700 text-center bg-slate-100 border border-slate-200 rounded-xl p-4">
|
||||
Keine Gegenstände verfügbar für diesen Zeitraum.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Mobile: card list */}
|
||||
<div className="sm:hidden space-y-2">
|
||||
{items.map((item) => (
|
||||
<label
|
||||
key={item.id}
|
||||
htmlFor={`item-${item.id}`}
|
||||
className="flex items-center justify-between gap-3 p-3 rounded-lg border border-slate-200 bg-white shadow-sm"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-slate-900 truncate">
|
||||
{item.item_name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{item.inSafe ? "Verfügbar" : "Nicht im Schließfach"}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`item-${item.id}`}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) addToRemove(item.id);
|
||||
else rmFromRemove(item.id);
|
||||
}}
|
||||
className="h-5 w-5 accent-indigo-600"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop: table */}
|
||||
<div className="hidden sm:block overflow-x-auto rounded-xl border border-slate-200 shadow-sm bg-white">
|
||||
<table className="min-w-full divide-y divide-slate-200">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-700">
|
||||
Gegenstand
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-semibold text-slate-700">
|
||||
<input type="checkbox" className="invisible" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{items.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-sm font-medium text-slate-900">
|
||||
{item.item_name}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-sm text-slate-700 text-right">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) addToRemove(item.id);
|
||||
else rmFromRemove(item.id);
|
||||
}}
|
||||
id={`item-${item.id}`}
|
||||
className="h-4 w-4 accent-indigo-600"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
createLoan(
|
||||
Cookies.get("startDate") ?? "",
|
||||
Cookies.get("endDate") ?? ""
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
className="w-full sm:w-44 bg-indigo-600 text-white font-bold py-2.5 px-4 rounded-lg shadow hover:bg-indigo-700 transition"
|
||||
>
|
||||
Ausleihen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form2;
|
||||
@@ -1,277 +0,0 @@
|
||||
import React from "react";
|
||||
import { Trash, ArrowLeftRight } from "lucide-react";
|
||||
import { handleDeleteLoan } from "../utils/userHandler";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import Cookies from "js-cookie";
|
||||
import { queryClient } from "../utils/queryClient";
|
||||
import { onTake, onReturn } from "../utils/userHandler";
|
||||
|
||||
type Loan = {
|
||||
id: number;
|
||||
username: string;
|
||||
loan_code: number;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
take_date: string | null;
|
||||
returned_date: string | null;
|
||||
created_at: string;
|
||||
loaned_items_id: number[];
|
||||
loaned_items_name: string[];
|
||||
};
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
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}`;
|
||||
};
|
||||
|
||||
async function fetchUserLoans(): Promise<Loan[]> {
|
||||
const res = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${Cookies.get("token") || ""}` },
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to fetch user loans");
|
||||
const data = await res.json();
|
||||
if (data === "No loans found for this user") return [];
|
||||
return Array.isArray(data) ? (data as Loan[]) : [];
|
||||
}
|
||||
|
||||
const Form4: React.FC = () => {
|
||||
const { data: userLoans = [], isFetching } = useQuery({
|
||||
queryKey: ["userLoans"],
|
||||
queryFn: fetchUserLoans,
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (loanID: number) => handleDeleteLoan(loanID),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userLoans"] });
|
||||
},
|
||||
});
|
||||
|
||||
const takeMutation = useMutation({
|
||||
mutationFn: (loanID: number) => onTake(loanID),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userLoans"] });
|
||||
},
|
||||
});
|
||||
|
||||
const returnMutation = useMutation({
|
||||
mutationFn: (loanID: number) => onReturn(loanID),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["userLoans"] });
|
||||
},
|
||||
});
|
||||
|
||||
const onDelete = (loanID: number) => deleteMutation.mutate(loanID);
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-center text-slate-600 shadow-sm">
|
||||
<p>Lade Ausleihen…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (userLoans.length === 0) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-center text-slate-600 shadow-sm">
|
||||
<p>Keine Ausleihen gefunden.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<p className="text-lg font-semibold tracking-tight text-slate-900">
|
||||
Meine Ausleihen
|
||||
</p>
|
||||
<p className="text-sm text-slate-600">
|
||||
Tippe auf das Papierkorb-Symbol, um eine Ausleihe zu löschen.
|
||||
</p>
|
||||
|
||||
{/* Mobile: cards */}
|
||||
<div className="space-y-2 sm:hidden">
|
||||
{userLoans.map((loan) => (
|
||||
<div
|
||||
key={loan.id}
|
||||
className="rounded-xl border border-slate-200 bg-white p-3 shadow-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
Leihcode: <span className="font-mono">{loan.loan_code}</span>
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-slate-700">
|
||||
<div>
|
||||
<span className="text-slate-500">Start:</span>{" "}
|
||||
{formatDate(loan.start_date)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Ende:</span>{" "}
|
||||
{formatDate(loan.end_date)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Abgeholt:</span>{" "}
|
||||
{loan.take_date ? (
|
||||
formatDate(loan.take_date)
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2 py-0.5 text-[11px] font-medium text-blue-700 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500/40 disabled:opacity-50"
|
||||
onClick={() => takeMutation.mutate(loan.id)}
|
||||
disabled={takeMutation.isPending}
|
||||
>
|
||||
{takeMutation.isPending ? "..." : "Abholen"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Zurück:</span>{" "}
|
||||
{loan.returned_date ? (
|
||||
formatDate(loan.returned_date)
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[11px] font-medium text-emerald-700 hover:bg-emerald-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/40 disabled:opacity-50"
|
||||
onClick={() => returnMutation.mutate(loan.id)}
|
||||
disabled={returnMutation.isPending || !loan.take_date}
|
||||
title={!loan.take_date ? "Erst abholen" : ""}
|
||||
>
|
||||
{returnMutation.isPending ? "..." : "Zurückgeben"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-slate-700">
|
||||
<span className="text-slate-500">Gegenstände:</span>{" "}
|
||||
{Array.isArray(loan.loaned_items_name)
|
||||
? loan.loaned_items_name.join(", ")
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDelete(loan.id)}
|
||||
aria-label="Ausleihe löschen"
|
||||
className="flex items-center justify-center rounded-md p-2 text-slate-600 hover:bg-red-50 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-500/30"
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop: table */}
|
||||
<div className="hidden sm:block rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-auto min-w-full text-sm text-slate-700">
|
||||
<thead className="sticky top-0 z-10 bg-slate-50">
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Leihcode
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Start
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Ende
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Abgeholt
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Zurückgegeben
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Erstellt
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Gegenstände
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-slate-600">
|
||||
Aktionen
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{userLoans.map((loan) => (
|
||||
<tr key={loan.id} className="odd:bg-white even:bg-slate-50">
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{loan.loan_code}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{formatDate(loan.start_date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{formatDate(loan.end_date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{loan.take_date ? (
|
||||
formatDate(loan.take_date)
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500/40 disabled:opacity-50"
|
||||
onClick={() => takeMutation.mutate(loan.id)}
|
||||
disabled={takeMutation.isPending}
|
||||
>
|
||||
{takeMutation.isPending ? "..." : "Abholen"}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{loan.returned_date ? (
|
||||
formatDate(loan.returned_date)
|
||||
) : (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 hover:bg-emerald-100 focus:outline-none focus:ring-2 focus:ring-emerald-500/40 disabled:opacity-50"
|
||||
onClick={() => returnMutation.mutate(loan.id)}
|
||||
disabled={returnMutation.isPending || !loan.take_date}
|
||||
title={!loan.take_date ? "Erst abholen" : ""}
|
||||
>
|
||||
{returnMutation.isPending ? "..." : "Zurückgeben"}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap font-mono tabular-nums text-slate-900">
|
||||
{formatDate(loan.created_at)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<div className="text-slate-900">
|
||||
{Array.isArray(loan.loaned_items_name)
|
||||
? loan.loaned_items_name.join(", ")
|
||||
: ""}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => onDelete(loan.id)}
|
||||
aria-label="Ausleihe löschen"
|
||||
className="inline-flex items-center rounded-md p-2 text-slate-600 hover:bg-red-50 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-red-500/30"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Scroll hint */}
|
||||
<div className="border-t border-gray-100 px-4 py-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<ArrowLeftRight className="h-4 w-4 text-gray-400" />
|
||||
<span>Hinweis: Horizontal scrollen, um alle Spalten zu sehen.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form4;
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from "react";
|
||||
import { changePW } from "../utils/userHandler";
|
||||
import { myToast } from "../utils/toastify";
|
||||
|
||||
type HeaderProps = {
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ onLogout }) => {
|
||||
const passwordForm = () => {
|
||||
const oldPW = window.prompt("Altes Passwort");
|
||||
const newPW = window.prompt("Neues Passwort");
|
||||
const repeatNewPW = window.prompt("Neues Passwort wiederholen");
|
||||
if (oldPW && newPW && repeatNewPW) {
|
||||
if (newPW === repeatNewPW) {
|
||||
changePW(oldPW, newPW);
|
||||
} else {
|
||||
myToast("Die neuen Passwörter stimmen nicht überein.", "error");
|
||||
}
|
||||
} else {
|
||||
myToast("Bitte alle Felder ausfüllen.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const btn =
|
||||
"inline-flex items-center h-9 px-3 rounded-md text-sm font-medium border border-slate-300 bg-white text-slate-700 hover:bg-slate-100 active:bg-slate-200 transition focus:outline-none focus:ring-2 focus:ring-slate-400/50";
|
||||
|
||||
return (
|
||||
<header className="mb-4 sm:mb-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl sm:text-3xl font-extrabold text-slate-900 tracking-tight">
|
||||
Gegenstand ausleihen
|
||||
</h1>
|
||||
<p className="text-slate-600 mt-1 text-sm sm:text-base">
|
||||
Schnell und unkompliziert Equipment reservieren
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
aria-label="Aktionen"
|
||||
className="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<a
|
||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/src/branch/dev/Docs/HELP.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={btn}
|
||||
>
|
||||
Hilfe
|
||||
</a>
|
||||
<a
|
||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={btn}
|
||||
>
|
||||
Source Code
|
||||
</a>
|
||||
<button type="button" onClick={passwordForm} className={btn}>
|
||||
Passwort ändern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
className={`${btn} border-rose-300 hover:bg-rose-50`}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from "react";
|
||||
import Footer from "./Footer";
|
||||
import { useState } from "react";
|
||||
import { loginUser } from "../utils/fetchData";
|
||||
import { myToast } from "../utils/toastify";
|
||||
|
||||
type LoginFormProps = {
|
||||
onLogin: () => void;
|
||||
};
|
||||
|
||||
const LoginForm: React.FC<LoginFormProps> = ({ onLogin }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const result = await loginUser(username, password);
|
||||
if (result.success) {
|
||||
onLogin();
|
||||
} else {
|
||||
myToast("Login failed. Please check your credentials.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-100 p-4">
|
||||
<div className="w-full max-w-sm bg-white rounded-2xl shadow-md p-6 sm:p-8 border border-slate-200">
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-6 text-center">
|
||||
Login
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-slate-700 mb-1"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
id="username"
|
||||
className="mt-1 block w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2.5 bg-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-slate-700 mb-1"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
id="password"
|
||||
className="mt-1 block w-full border border-slate-300 rounded-md shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 px-3 py-2.5 bg-white"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-indigo-600 text-white font-bold py-2.5 px-4 rounded-md shadow hover:bg-indigo-700 transition"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
type ObjectProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const Object: React.FC<ObjectProps> = ({ title, description }) => {
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-slate-900">{title}</h3>
|
||||
<p className="text-xs text-slate-600 line-clamp-2">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Object;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Object from "./Object";
|
||||
import { MonitorSmartphone } from "lucide-react";
|
||||
import { ALL_ITEMS_UPDATED_EVENT } from "../utils/fetchData";
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const [items, setItems] = useState<any[]>(
|
||||
JSON.parse(localStorage.getItem("allItems") || "[]")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const next = JSON.parse(localStorage.getItem("allItems") || "[]");
|
||||
setItems(next);
|
||||
};
|
||||
handler();
|
||||
window.addEventListener(ALL_ITEMS_UPDATED_EVENT, handler);
|
||||
return () => window.removeEventListener(ALL_ITEMS_UPDATED_EVENT, handler);
|
||||
}, []);
|
||||
|
||||
const outCount = items.reduce((n, it) => n + (it.inSafe ? 0 : 1), 0);
|
||||
const sorted = [...items].sort((a, b) => Number(a.inSafe) - Number(b.inSafe));
|
||||
|
||||
return (
|
||||
<aside className="w-full md:w-72 md:h-full flex flex-col rounded-2xl pt-0 px-3 pb-3 sm:pt-0 sm:px-4 sm:pb-4 bg-gradient-to-b from-white to-slate-50 ring-1 ring-slate-200/70 shadow-md overflow-hidden">
|
||||
<div className="sticky top-0 z-10 -mx-3 sm:-mx-4 px-3 sm:px-4 py-2.5 bg-white/85 backdrop-blur supports-[backdrop-filter]:backdrop-blur border-b border-slate-200/70 text-lg sm:text-xl font-bold mb-3 text-slate-900 tracking-tight flex items-center justify-between gap-2 rounded-t-2xl">
|
||||
<span className="flex items-center gap-2 min-w-0 flex-1 truncate">
|
||||
<MonitorSmartphone className="w-5 h-5 text-slate-700 shrink-0" />
|
||||
<span className="truncate">Geräte</span>
|
||||
</span>
|
||||
{outCount > 0 && (
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap tabular-nums text-[10px] sm:text-xs px-2.5 py-1 rounded-full bg-amber-50 text-amber-700 ring-1 ring-amber-200/70 shadow-sm font-medium">
|
||||
{outCount} außerhalb
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scroll area */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
|
||||
<div className="flex flex-col gap-3 md:space-y-3">
|
||||
{sorted.map((item: any) => (
|
||||
<div
|
||||
key={item.item_name}
|
||||
className={`group relative w-full bg-white rounded-xl p-3 sm:p-4 ring-1 ring-slate-200/70 duration-200 hover:shadow-md focus-within:ring-slate-300 ${
|
||||
item.inSafe
|
||||
? "border-l-4 border-emerald-400"
|
||||
: "border-l-4 border-red-400 ring-red-200/60 bg-red-50/40"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
className="relative mt-0.5 inline-flex"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{!item.inSafe && (
|
||||
<span className="absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75 animate-ping"></span>
|
||||
)}
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full ring-2 ring-white ${
|
||||
item.inSafe ? "bg-emerald-500" : "bg-red-500"
|
||||
}`}
|
||||
title={
|
||||
item.inSafe ? "Im Schließfach" : "Nicht im Schließfach"
|
||||
}
|
||||
aria-label={
|
||||
item.inSafe ? "Im Schließfach" : "Nicht im Schließfach"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<Object
|
||||
title={item.item_name}
|
||||
description={
|
||||
item.inSafe
|
||||
? "Aktuell im Schließfach"
|
||||
: "Aktuell nicht im Schließfach"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-slate-200/70 text-[10px] sm:text-xs text-slate-500 items-center gap-4 hidden md:flex">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 bg-emerald-500 rounded-full ring-2 ring-white shadow-sm"></span>
|
||||
Im Schließfach
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 bg-red-500 rounded-full ring-2 ring-white shadow-sm"></span>
|
||||
Außerhalb des Schließfachs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -1,12 +0,0 @@
|
||||
/* Tailwind (v4) */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Small touch target improvements */
|
||||
@layer base {
|
||||
html:focus-within {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import "../App.css";
|
||||
import Header from "../components/Header";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children, onLogout }) => {
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-slate-50 text-slate-800">
|
||||
{/* Main */}
|
||||
<main className="flex-1 min-h-0 overflow-hidden flex flex-col items-center px-3 sm:px-5 py-4 sm:py-8 pb-12">
|
||||
<div className="w-full max-w-5xl flex flex-col gap-3 md:flex-row md:gap-6 md:items-stretch min-h-0 h-full">
|
||||
<div className="hidden md:flex md:flex-col md:shrink-0 md:w-72 md:min-h-0 md:h-full">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 min-h-0 h-full flex flex-col overflow-hidden">
|
||||
<div className="w-full">
|
||||
<Header onLogout={onLogout} />
|
||||
</div>
|
||||
<div className="w-full bg-white shadow-md md:shadow-lg rounded-2xl p-4 sm:p-6 ring-1 ring-slate-200 flex-1 min-h-0 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { ToastContainer, Flip } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { queryClient } from "./utils/queryClient";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={3000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="colored"
|
||||
transition={Flip}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -1,201 +0,0 @@
|
||||
import Cookies from "js-cookie";
|
||||
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
|
||||
export const ALL_ITEMS_UPDATED_EVENT = "allItemsUpdated";
|
||||
export const BORROWABLE_ITEMS_UPDATED_EVENT = "borrowableItemsUpdated";
|
||||
export const AUTH_LOGOUT_EVENT = "authLogout";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
let sendError = false;
|
||||
|
||||
function logout() {
|
||||
Cookies.remove("token");
|
||||
Cookies.remove("startDate");
|
||||
Cookies.remove("endDate");
|
||||
localStorage.removeItem("allItems");
|
||||
localStorage.removeItem("allLoans");
|
||||
localStorage.removeItem("userLoans");
|
||||
localStorage.removeItem("borrowableItems");
|
||||
window.dispatchEvent(new Event(ALL_ITEMS_UPDATED_EVENT));
|
||||
window.dispatchEvent(new Event(BORROWABLE_ITEMS_UPDATED_EVENT));
|
||||
window.dispatchEvent(new Event(AUTH_LOGOUT_EVENT));
|
||||
}
|
||||
|
||||
export const fetchAllData = async (token: string | undefined) => {
|
||||
if (!token) return;
|
||||
// First we fetch all items that are potentially available for borrowing
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/items`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 500) {
|
||||
if (!sendError) {
|
||||
sendError = true;
|
||||
myToast("Session expired. Please log in again.", "error");
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Failed to fetch items", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
localStorage.setItem("allItems", JSON.stringify(data));
|
||||
// Notify listeners (e.g., Sidebar) that items have been updated
|
||||
window.dispatchEvent(new Event(ALL_ITEMS_UPDATED_EVENT));
|
||||
} catch (error) {
|
||||
myToast("An error occurred", "error");
|
||||
}
|
||||
|
||||
// get all loans
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/loans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 500) {
|
||||
if (!sendError) {
|
||||
sendError = true;
|
||||
myToast("Session expired. Please log in again.", "error");
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Failed to fetch loans!", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
localStorage.setItem("allLoans", JSON.stringify(data));
|
||||
// Notify listeners (e.g., Sidebar) that loans have been updated
|
||||
window.dispatchEvent(new Event(ALL_ITEMS_UPDATED_EVENT));
|
||||
} catch (error) {
|
||||
myToast("An error occurred", "error");
|
||||
}
|
||||
|
||||
// get user loans
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/userLoans`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 500) {
|
||||
if (!sendError) {
|
||||
sendError = true;
|
||||
myToast("Session expired. Please log in again.", "error");
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Failed to fetch user loans!", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
localStorage.setItem("userLoans", JSON.stringify(data));
|
||||
// Notify listeners (e.g., Sidebar) that loans have been updated
|
||||
window.dispatchEvent(new Event(ALL_ITEMS_UPDATED_EVENT));
|
||||
} catch (error) {
|
||||
myToast("An error occurred", "error");
|
||||
}
|
||||
};
|
||||
|
||||
export const loginUser = async (username: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false } as const;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data?.token) {
|
||||
Cookies.set("token", data.token);
|
||||
myToast("Login successful!", "success");
|
||||
fetchAllData(Cookies.get("token"));
|
||||
return { success: true, token: data.token } as const;
|
||||
}
|
||||
|
||||
return { success: false } as const;
|
||||
} catch (e) {
|
||||
return { success: false } as const;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBorrowableItems = async () => {
|
||||
const startDate = Cookies.get("startDate");
|
||||
const endDate = Cookies.get("endDate");
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
myToast("Bitte wähle einen Zeitraum aus.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
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.status === 500) {
|
||||
if (!sendError) {
|
||||
sendError = true;
|
||||
myToast("Session expired. Please log in again.", "error");
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Failed to fetch borrowable items", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
localStorage.setItem("borrowableItems", JSON.stringify(data));
|
||||
|
||||
window.dispatchEvent(new Event(BORROWABLE_ITEMS_UPDATED_EVENT)); // notify same-tab listeners
|
||||
console.log("Borrowable items fetched successfully");
|
||||
} catch (error) {
|
||||
myToast("An error occurred", "error");
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
// Central QueryClient instance so utilities (e.g. file upload) can invalidate queries.
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
import { toast, Flip, type ToastOptions } from "react-toastify";
|
||||
|
||||
export type ToastType = "success" | "error" | "info" | "warning";
|
||||
|
||||
export const myToast = (message: string, msgType: ToastType) => {
|
||||
let config: ToastOptions = {
|
||||
position: "top-right",
|
||||
autoClose: 3000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: "colored",
|
||||
transition: Flip,
|
||||
};
|
||||
toast[msgType](message, config);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import { myToast } from "./toastify";
|
||||
import Cookies from "js-cookie";
|
||||
import { queryClient } from "./queryClient";
|
||||
|
||||
const API_BASE =
|
||||
(import.meta as any).env?.VITE_BACKEND_URL ||
|
||||
import.meta.env.VITE_BACKEND_URL ||
|
||||
"http://localhost:8002";
|
||||
|
||||
export const handleDeleteLoan = async (loanID: number): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/deleteLoan/${loanID}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Löschen der Ausleihe", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
const raw = localStorage.getItem("userLoans");
|
||||
let current: Array<{ id: number }> = [];
|
||||
|
||||
try {
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
current = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
current = [];
|
||||
}
|
||||
|
||||
const updated = current.filter(
|
||||
(loan) => Number(loan.id) !== Number(loanID)
|
||||
);
|
||||
localStorage.setItem("userLoans", JSON.stringify(updated));
|
||||
|
||||
myToast("Ausleihe erfolgreich gelöscht!", "success");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting loan:", error);
|
||||
myToast("Fehler beim löschen der Ausleihe", "error");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse existing cookie and coerce to numbers
|
||||
let removeArr: number[] = (() => {
|
||||
try {
|
||||
const raw = Cookies.get("removeArr");
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(parsed)
|
||||
? parsed.map((v) => Number(v)).filter((n) => Number.isFinite(n))
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
const rawCookies = Cookies.withConverter({
|
||||
write: (value: string) => value, // store raw JSON
|
||||
});
|
||||
|
||||
export const addToRemove = (itemID: number) => {
|
||||
if (!Number.isFinite(itemID)) return;
|
||||
if (!removeArr.includes(itemID)) {
|
||||
removeArr.push(itemID);
|
||||
rawCookies.set("removeArr", JSON.stringify(removeArr));
|
||||
}
|
||||
};
|
||||
|
||||
export const rmFromRemove = (itemID: number) => {
|
||||
removeArr = removeArr.filter((item) => item !== itemID);
|
||||
rawCookies.set("removeArr", JSON.stringify(removeArr));
|
||||
};
|
||||
|
||||
export const createLoan = async (startDate: string, endDate: string) => {
|
||||
const items = removeArr;
|
||||
const response = await fetch(`${API_BASE}/api/createLoan`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
body: JSON.stringify({ items, startDate, endDate }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Erstellen der Ausleihe", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear selection on success
|
||||
removeArr = [];
|
||||
Cookies.set("removeArr", "[]");
|
||||
myToast("Ausleihe erfolgreich erstellt!", "success");
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["userLoans"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["allLoans"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["borrowableItems"] });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const onReturn = async (loanID: number) => {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/returnLoan/${loanID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Zurückgeben der Ausleihe", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
myToast("Ausleihe erfolgreich zurückgegeben!", "success");
|
||||
return true;
|
||||
};
|
||||
|
||||
export const onTake = async (loanID: number) => {
|
||||
const response = await fetch(`${API_BASE}/api/takeLoan/${loanID}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
myToast("Fehler beim Ausleihen der Ausleihe", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
myToast("Ausleihe erfolgreich ausgeliehen!", "success");
|
||||
return true;
|
||||
};
|
||||
|
||||
export const changePW = async (oldPassword: string, newPassword: string) => {
|
||||
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) {
|
||||
myToast("Fehler beim Ändern des Passworts", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
myToast("Passwort erfolgreich geändert!", "success");
|
||||
return true;
|
||||
};
|
||||
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
// add other paths if needed
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"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
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"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,15 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), svgr(), tailwindcss()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8001,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user