Compare commits
1 Commits
demoDev
...
dev_next-j
| Author | SHA1 | Date | |
|---|---|---|---|
| 233778a8ad |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -112,8 +112,4 @@ backend/public/uploads/
|
|||||||
secrets/
|
secrets/
|
||||||
keys/
|
keys/
|
||||||
|
|
||||||
ToDo.txt
|
ToDo.txt
|
||||||
|
|
||||||
|
|
||||||
# only in development branch
|
|
||||||
next-env.d.ts
|
|
||||||
@@ -1,177 +1,373 @@
|
|||||||
# Borrow System API Documentation
|
# Borrow System API Documentation
|
||||||
|
|
||||||
## Overview
|
**Frontend:** https://insta.the1s.de
|
||||||
|
**Backend base URL:** `https://backend.insta.the1s.de/api`
|
||||||
|
|
||||||
The Borrow System API provides endpoints for managing items, loans, and door access for a borrowing/locker system. All endpoints require authentication via an 8-digit API key passed as a URL parameter.
|
---
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
All requests must include a valid API key in the URL path as the `:key` parameter. API keys are 8-digit numeric strings.
|
All API endpoints require **either**:
|
||||||
|
|
||||||
|
### 1. Bearer Token (JWT)
|
||||||
|
|
||||||
|
Send an `Authorization` header:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Used for user-based access.
|
||||||
|
- Token must be valid and not expired.
|
||||||
|
|
||||||
|
### 2. API Key (for devices / machine-to-machine)
|
||||||
|
|
||||||
|
Include an API key in the route as `:key` parameter:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/api/.../:key/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/items/ABC123
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `ABC123` is your API key.
|
||||||
|
The API key is validated server-side.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Response Codes
|
||||||
|
|
||||||
|
- `200 OK` – Request was successful.
|
||||||
|
- `401 Unauthorized` – Missing or malformed credentials.
|
||||||
|
- `403 Forbidden` – Credentials invalid or not allowed to access this resource.
|
||||||
|
- `404 Not Found` – Resource (e.g., loan) not found.
|
||||||
|
- `500 Internal Server Error` – Unexpected server error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
The Base URL for all endpoints is: `https://insta.the1s.de/backend/api`
|
### 1. Get All Items
|
||||||
|
|
||||||
### Get All Items
|
**GET** `/api/items/:key`
|
||||||
|
|
||||||
`GET /items/:key`
|
Returns a list of all items.
|
||||||
|
|
||||||
Returns all items in the system.
|
#### Path Parameters
|
||||||
|
|
||||||
**Response 200:**
|
- `:key` – API key (string)
|
||||||
|
|
||||||
|
#### Authentication
|
||||||
|
|
||||||
|
- Either:
|
||||||
|
- Valid `Authorization: Bearer <token>`
|
||||||
|
- Or valid `:key` path parameter
|
||||||
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/items/ABC123 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/items/dummyKey HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Successful Response (200)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"item_name": "Laptop",
|
"item_name": "DJI 1er Mikro",
|
||||||
"can_borrow_role": 1,
|
"can_borrow_role": 4,
|
||||||
"in_safe": true,
|
"inSafe": 1,
|
||||||
"safe_nr": 3,
|
"safe_nr": 3,
|
||||||
"door_key": 101,
|
"door_key": "123",
|
||||||
"last_borrowed_person": "jdoe",
|
"entry_created_at": "2025-08-19T22:02:16.000Z",
|
||||||
|
"entry_updated_at": "2025-08-19T22:02:16.000Z",
|
||||||
|
"last_borrowed_person": "alice",
|
||||||
"currently_borrowing": null
|
"currently_borrowing": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 500:**
|
#### Error Response (500)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to fetch items" }
|
{
|
||||||
|
"message": "Failed to fetch items"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Change Item Safe State
|
### 2. Toggle Item Safe State
|
||||||
|
|
||||||
`POST /change-state/:key/:itemId`
|
Toggles `in_safe` between `0` and `1` for a given item.
|
||||||
|
|
||||||
Toggles the `in_safe` boolean state of an item.
|
**Keep in mind that when you return a loan by code, the item states are automatically updated.**
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/change-state/:key/:itemId`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **itemId** - The item's ID
|
|
||||||
|
|
||||||
**Response 200:** Returns on successful toggle.
|
- `:key` – API key (string)
|
||||||
|
- `:itemId` – Item ID (integer)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
|
- Either Bearer token or `:key` API key.
|
||||||
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/change-state/ABC123/42 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Successful Response (200)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to update item state" }
|
{
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_(Implementation currently only returns `{ success: true }`, so `data` may be empty.)_
|
||||||
|
|
||||||
|
#### Error Response (500)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Failed to update item state"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Get Loan by Code
|
### 3. Get Loan by Code
|
||||||
|
|
||||||
`GET /get-loan-by-code/:key/:loan_code`
|
Fetch loan information by `loan_code`.
|
||||||
|
|
||||||
Retrieves loan details by its 6-digit loan code.
|
**GET** `/api/get-loan-by-code/:key/:loan_code`
|
||||||
|
|
||||||
**URL Parameters:**
|
#### Path Parameters
|
||||||
|
|
||||||
- **key** - API key
|
- `:key` – API key (string)
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 200:**
|
#### Authentication
|
||||||
|
|
||||||
|
- Either Bearer token or `:key` API key.
|
||||||
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/get-loan-by-code/ABC123/12345 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Successful Response (200)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"username": "jdoe",
|
"username": "john",
|
||||||
"returned_date": null,
|
"returned_date": null,
|
||||||
"take_date": "2024-01-15T10:30:00.000Z",
|
"take_date": "2025-01-01T10:00:00.000Z",
|
||||||
"lockers": [1, 3]
|
"lockers": "[1, 2, 3]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 404:**
|
#### Error Response (404)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Loan not found" }
|
{
|
||||||
|
"message": "Loan not found"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Set Take Date
|
### 4. Set Loan Return Date
|
||||||
|
|
||||||
`POST /set-take-date/:key/:loan_code`
|
Sets `returned_date = NOW()` on a loan and updates related items:
|
||||||
|
|
||||||
Records when items are physically taken by setting `take_date` to the current timestamp. Updates associated items to `in_safe = false` and sets `currently_borrowing` to the loan's username.
|
- `in_safe = 1`
|
||||||
|
- `currently_borrowing = NULL`
|
||||||
|
- `last_borrowed_person = username`
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/set-return-date/:key/:loan_code`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
|
||||||
|
|
||||||
**Response 200:** Empty JSON object on success.
|
- `:key` – API key (string)
|
||||||
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
```json
|
- Either Bearer token or `:key` API key.
|
||||||
{ "message": "Loan not found or already taken" }
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/set-return-date/ABC123/12345 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** This endpoint will fail if the loan has already been taken or does not exist.
|
#### Successful Response (200)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Response (500)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Failed to set return date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Set Return Date
|
### 5. Set Loan Take Date
|
||||||
|
|
||||||
`POST /set-return-date/:key/:loan_code`
|
Sets `take_date = NOW()` on a loan and updates related items:
|
||||||
|
|
||||||
Marks a loan as returned by setting `returned_date` to the current timestamp. Also updates all associated items to `in_safe = true`, clears `currently_borrowing`, and sets `last_borrowed_person`. Therefore, keep in mind that you must not call other endpoints that will change the safe state of an item after or before calling this endpoint, otherwise the state of the items will be inconsistent.
|
- `in_safe = 0`
|
||||||
|
- `currently_borrowing = username`
|
||||||
|
|
||||||
**URL Parameters:**
|
**POST** `/api/set-take-date/:key/:loan_code`
|
||||||
|
|
||||||
- **key** - API key
|
#### Path Parameters
|
||||||
- **loan_code** - A 6-digit numeric loan code
|
|
||||||
|
|
||||||
**Response 200:** Empty JSON object on success.
|
- `:key` – API key (string)
|
||||||
|
- `:loan_code` – Loan code (string)
|
||||||
|
|
||||||
**Response 500:**
|
#### Authentication
|
||||||
|
|
||||||
```json
|
- Either Bearer token or `:key` API key.
|
||||||
{ "message": "Failed to set return date" }
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/set-take-date/ABC123/LOAN-12345 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** This endpoint will fail if the loan has already been returned (i.e., `returned_date` is not `NULL`).
|
#### Successful Response (200)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Response (500)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Failed to set take date"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Open Door
|
### 6. Open Door by Door Key
|
||||||
|
|
||||||
`GET /open-door/:key/:doorKey`
|
Looks up an item by its `door_key`, toggles `in_safe`, and returns safe information.
|
||||||
|
|
||||||
Toggles the safe state of an item identified by its door key and returns the associated safe number.
|
**GET** `/api/open-door/:key/:doorKey`
|
||||||
|
|
||||||
**URL Parameters:**
|
#### Path Parameters
|
||||||
|
|
||||||
- **key** - API key
|
- `:key` – API key (string)
|
||||||
- **doorKey** - The door key identifier assigned to an item
|
- `:doorKey` – Door key/token (string) used by hardware to identify the locker.
|
||||||
|
|
||||||
**Response 200:**
|
#### Authentication
|
||||||
|
|
||||||
|
- Either Bearer token or `:key` API key.
|
||||||
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/open-door/ABC123/123 HTTP/1.1
|
||||||
|
Host: backend.insta.the1s.de
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Successful Response (200)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"safe_nr": 3,
|
"safe_nr": 5,
|
||||||
"id": 1
|
"id": 42
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response 500:**
|
#### Error Response (500)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "message": "Failed to open door" }
|
{
|
||||||
|
"message": "Failed to open door"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Error Handling
|
---
|
||||||
|
|
||||||
All endpoints return a `500` status code for server-side failures and a JSON body with a `message` field, except for **Get Loan by Code** which returns `404` when no matching loan is found.
|
## Authentication Error Messages
|
||||||
|
|
||||||
|
### Missing credentials
|
||||||
|
|
||||||
|
Status: `401`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Unauthorized"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid JWT
|
||||||
|
|
||||||
|
Status: `403`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Present token invalid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invalid API Key
|
||||||
|
|
||||||
|
Status: `403`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "API Key invalid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All responses are JSON.
|
||||||
|
- Time fields like `take_date` and `returned_date` are in the format returned by MySQL (usually ISO-like strings).
|
||||||
|
- `loaned_items_id` in the database is stored as a JSON array string (e.g. `"[1,2,3]"`) and is parsed internally; clients do not interact with this field directly via current endpoints.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Ausleihsystem</title>
|
<title>frontendv2</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /backend {
|
|
||||||
return 301 /backend/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /backend/ {
|
|
||||||
proxy_pass http://borrow_system-backend_v2:8004/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import { triggerLogoutAtom } from "@/states/Atoms";
|
|||||||
import { MyLoansPage } from "./pages/MyLoansPage";
|
import { MyLoansPage } from "./pages/MyLoansPage";
|
||||||
import Landingpage from "./pages/Landingpage";
|
import Landingpage from "./pages/Landingpage";
|
||||||
import { changeLanguage } from "i18next";
|
import { changeLanguage } from "i18next";
|
||||||
import { Flex } from "@chakra-ui/react";
|
import { Box, Flex } from "@chakra-ui/react";
|
||||||
import { Footer } from "./components/footer/Footer";
|
import { Footer } from "./components/footer/Footer";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
import { ContactPage } from "./pages/ContactPage";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -72,8 +71,8 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Flex direction="column" minH="100dvh">
|
<Flex direction="column" minH="100vh">
|
||||||
<Flex as="main" flex="1" direction="column">
|
<Box as="main" flex="1">
|
||||||
<UserContext.Provider value={user}>
|
<UserContext.Provider value={user}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -81,14 +80,13 @@ function App() {
|
|||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/my-loans" element={<MyLoansPage />} />
|
<Route path="/my-loans" element={<MyLoansPage />} />
|
||||||
<Route path="/landingpage" element={<Landingpage />} />
|
<Route path="/landingpage" element={<Landingpage />} />
|
||||||
<Route path="/contact" element={<ContactPage />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
</Flex>
|
</Box>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flex>
|
</Flex>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
50
FrontendV2/src/components/Changelog.json
Normal file
50
FrontendV2/src/components/Changelog.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"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)."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
263
FrontendV2/src/components/Changelog.tsx
Normal file
263
FrontendV2/src/components/Changelog.tsx
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,41 +4,98 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
CloseButton,
|
||||||
|
Dialog,
|
||||||
|
Portal,
|
||||||
HStack,
|
HStack,
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
Box,
|
Box,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
CircleUserRound,
|
CircleUserRound,
|
||||||
|
RotateCcwKey,
|
||||||
|
Code,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
LogOut,
|
LogOut,
|
||||||
CalendarPlus,
|
CalendarPlus,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Languages,
|
Languages,
|
||||||
Table,
|
Table,
|
||||||
ContactRound,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useUserContext } from "@/states/Context";
|
import { useUserContext } from "@/states/Context";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import MyAlert from "./myChakra/MyAlert";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserDialogue } from "./UserDialogue";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const userData = useUserContext();
|
const userData = useUserContext();
|
||||||
|
console.log(userData);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// 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 [, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||||
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
const [, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||||
|
|
||||||
|
// Dialog control
|
||||||
|
const [isPwOpen, setPwOpen] = useState(false);
|
||||||
const [userDialog, setUserDialog] = useState(false);
|
const [userDialog, setUserDialog] = useState(false);
|
||||||
|
|
||||||
|
const changePassword = async () => {
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setMsgTitle(t("err_pw_change"));
|
||||||
|
setMsgDescription(t("pw_mismatch"));
|
||||||
|
setMsgStatus("error");
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/api/users/change-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ oldPassword, newPassword }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setMsgTitle(t("err_pw_change"));
|
||||||
|
setMsgDescription(t("pw_mismatch"));
|
||||||
|
setMsgStatus("error");
|
||||||
|
setIsMsg(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMsgTitle(t("pw_success"));
|
||||||
|
setMsgDescription(t("pw_success_desc"));
|
||||||
|
setMsgStatus("success");
|
||||||
|
setIsMsg(true);
|
||||||
|
|
||||||
|
setOldPassword("");
|
||||||
|
setNewPassword("");
|
||||||
|
setConfirmPassword("");
|
||||||
|
};
|
||||||
|
|
||||||
const username = userData.first_name ? userData.first_name : "N/A";
|
const username = userData.first_name ? userData.first_name : "N/A";
|
||||||
const fullname = userData.first_name + " " + userData.last_name;
|
const fullname = userData.first_name + " " + userData.last_name;
|
||||||
const randomColor = [
|
const randomColor = [
|
||||||
@@ -142,9 +199,9 @@ export const Header = () => {
|
|||||||
value="help"
|
value="help"
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
window.open(
|
window.open(
|
||||||
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages",
|
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener,noreferrer",
|
"noopener,noreferrer"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
children={
|
children={
|
||||||
@@ -155,12 +212,18 @@ export const Header = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
value="contact"
|
value="source-code"
|
||||||
onSelect={() => navigate("/contact", { replace: true })}
|
onSelect={() =>
|
||||||
|
window.open(
|
||||||
|
"https://git.the1s.de/Matthias-Claudius-Schule/borrow-system",
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
)
|
||||||
|
}
|
||||||
children={
|
children={
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<ContactRound size={16} />
|
<Code size={16} />
|
||||||
<Text as="span">{t("contact")}</Text>
|
<Text as="span">{t("source-code")}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -279,7 +342,7 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki/?action=_pages"
|
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system/wiki"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
@@ -290,15 +353,17 @@ export const Header = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<Button
|
<a
|
||||||
variant={"outline"}
|
href="https://git.the1s.de/Matthias-Claudius-Schule/borrow-system"
|
||||||
onClick={() => navigate("/contact", { replace: true })}
|
target="_blank"
|
||||||
>
|
>
|
||||||
<HStack gap={2}>
|
<Button variant="ghost">
|
||||||
<ContactRound size={18} />
|
<HStack gap={2}>
|
||||||
<Text as="span">{t("contact")}</Text>
|
<Code size={18} />
|
||||||
</HStack>
|
<Text as="span">{t("source-code")}</Text>
|
||||||
</Button>
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
|
||||||
<Button onClick={logout} variant="outline" colorScheme="red">
|
<Button onClick={logout} variant="outline" colorScheme="red">
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
@@ -311,12 +376,145 @@ export const Header = () => {
|
|||||||
|
|
||||||
{/* User Info Dialoge */}
|
{/* User Info Dialoge */}
|
||||||
{userDialog && (
|
{userDialog && (
|
||||||
<UserDialogue
|
<Flex
|
||||||
setUserDialog={setUserDialog}
|
position="fixed"
|
||||||
fullname={fullname}
|
inset={0}
|
||||||
randomColor={randomColor}
|
zIndex={1000}
|
||||||
/>
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
bg="blackAlpha.400"
|
||||||
|
backdropFilter="blur(6px)"
|
||||||
|
>
|
||||||
|
<Card.Root maxW="sm" w="full" mx={4}>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>
|
||||||
|
<Flex justify="center" align="center" w="100%">
|
||||||
|
<Avatar.Root
|
||||||
|
size={"2xl"}
|
||||||
|
colorPalette={randomColor[Math.floor(Math.random() * 10)]}
|
||||||
|
>
|
||||||
|
<Avatar.Fallback name={fullname} />
|
||||||
|
</Avatar.Root>
|
||||||
|
</Flex>
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>{t("user-info-desc")}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body>
|
||||||
|
<Stack gap="4" w="full">
|
||||||
|
<Box as="dl">
|
||||||
|
<Grid
|
||||||
|
templateColumns="auto 1fr"
|
||||||
|
rowGap={2}
|
||||||
|
columnGap={4}
|
||||||
|
alignItems="start"
|
||||||
|
>
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("first-name")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.first_name}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("last-name")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.last_name}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("username")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.username}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("role")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">{userData.role}</Text>
|
||||||
|
|
||||||
|
<Text as="dt" fontWeight="bold" textAlign="left">
|
||||||
|
{t("admin-status")}:
|
||||||
|
</Text>
|
||||||
|
<Text as="dd">
|
||||||
|
{userData.is_admin ? t("yes") : t("no")}
|
||||||
|
</Text>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button variant="solid" onClick={() => setPwOpen(true)}>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<RotateCcwKey size={18} />
|
||||||
|
<Text as="span">{t("change-password")}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer justifyContent="flex-end">
|
||||||
|
<Button variant="outline" onClick={() => setUserDialog(false)}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
</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>{t("change-password")}</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={t("old-password")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder={t("new-password")}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder={t("confirm-password")}
|
||||||
|
/>
|
||||||
|
</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">{t("cancel")}</Button>
|
||||||
|
</Dialog.ActionTrigger>
|
||||||
|
<Button type="submit" colorScheme="teal">
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
<Dialog.CloseTrigger asChild>
|
||||||
|
<CloseButton size="sm" />
|
||||||
|
</Dialog.CloseTrigger>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Positioner>
|
||||||
|
</Portal>
|
||||||
|
</Dialog.Root>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
CloseButton,
|
|
||||||
Dialog,
|
|
||||||
Portal,
|
|
||||||
HStack,
|
|
||||||
Box,
|
|
||||||
Avatar,
|
|
||||||
Card,
|
|
||||||
Grid,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
import { RotateCcwKey } from "lucide-react";
|
|
||||||
import MyAlert from "./myChakra/MyAlert";
|
|
||||||
import { API_BASE } from "@/config/api.config";
|
|
||||||
import { useUserContext } from "@/states/Context";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
|
|
||||||
type UserDialogueProps = {
|
|
||||||
setUserDialog: (value: boolean) => void;
|
|
||||||
fullname: string;
|
|
||||||
randomColor: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UserDialogue = (props: UserDialogueProps) => {
|
|
||||||
const userData = useUserContext();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
// 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("");
|
|
||||||
|
|
||||||
// Dialog control
|
|
||||||
const [isPwOpen, setPwOpen] = useState(false);
|
|
||||||
|
|
||||||
const changePassword = async () => {
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
setMsgTitle(t("err_pw_change"));
|
|
||||||
setMsgDescription(t("pw_mismatch"));
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/api/users/change-password`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ oldPassword, newPassword }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
setMsgTitle(t("err_pw_change"));
|
|
||||||
setMsgDescription(t("pw_mismatch"));
|
|
||||||
setMsgStatus("error");
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMsgTitle(t("pw_success"));
|
|
||||||
setMsgDescription(t("pw_success_desc"));
|
|
||||||
setMsgStatus("success");
|
|
||||||
setIsMsg(true);
|
|
||||||
|
|
||||||
setOldPassword("");
|
|
||||||
setNewPassword("");
|
|
||||||
setConfirmPassword("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
position="fixed"
|
|
||||||
inset={0}
|
|
||||||
zIndex={1000}
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
bg="blackAlpha.400"
|
|
||||||
backdropFilter="blur(6px)"
|
|
||||||
>
|
|
||||||
<Card.Root maxW="sm" w="full" mx={4}>
|
|
||||||
<Card.Header>
|
|
||||||
<Card.Title>
|
|
||||||
<Flex justify="center" align="center" w="100%">
|
|
||||||
<Avatar.Root
|
|
||||||
size={"2xl"}
|
|
||||||
colorPalette={props.randomColor[Math.floor(Math.random() * 10)]}
|
|
||||||
>
|
|
||||||
<Avatar.Fallback name={props.fullname} />
|
|
||||||
</Avatar.Root>
|
|
||||||
</Flex>
|
|
||||||
</Card.Title>
|
|
||||||
<Card.Description>{t("user-info-desc")}</Card.Description>
|
|
||||||
</Card.Header>
|
|
||||||
<Card.Body>
|
|
||||||
<Stack gap="4" w="full">
|
|
||||||
<Box as="dl">
|
|
||||||
<Grid
|
|
||||||
templateColumns="auto 1fr"
|
|
||||||
rowGap={2}
|
|
||||||
columnGap={4}
|
|
||||||
alignItems="start"
|
|
||||||
>
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("first-name")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.first_name}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("last-name")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.last_name}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("username")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.username}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("role")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.role}</Text>
|
|
||||||
|
|
||||||
<Text as="dt" fontWeight="bold" textAlign="left">
|
|
||||||
{t("admin-status")}:
|
|
||||||
</Text>
|
|
||||||
<Text as="dd">{userData.is_admin ? t("yes") : t("no")}</Text>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Button variant="solid" onClick={() => setPwOpen(true)}>
|
|
||||||
<HStack gap={2}>
|
|
||||||
<RotateCcwKey size={18} />
|
|
||||||
<Text as="span">{t("change-password")}</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Card.Body>
|
|
||||||
<Card.Footer justifyContent="flex-end">
|
|
||||||
<Button variant="outline" onClick={() => props.setUserDialog(false)}>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</Card.Footer>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
{/* 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>{t("change-password")}</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={t("old-password")}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
placeholder={t("new-password")}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
placeholder={t("confirm-password")}
|
|
||||||
/>
|
|
||||||
</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">{t("cancel")}</Button>
|
|
||||||
</Dialog.ActionTrigger>
|
|
||||||
<Button type="submit" colorScheme="teal">
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</form>
|
|
||||||
<Dialog.CloseTrigger asChild>
|
|
||||||
<CloseButton size="sm" />
|
|
||||||
</Dialog.CloseTrigger>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Positioner>
|
|
||||||
</Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -9,9 +9,10 @@ export const Footer = () => {
|
|||||||
as="footer"
|
as="footer"
|
||||||
py={4}
|
py={4}
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
width="100%"
|
position="fixed"
|
||||||
flexShrink={0}
|
bottom="0"
|
||||||
fontSize="sm"
|
left="0"
|
||||||
|
right="0"
|
||||||
>
|
>
|
||||||
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
Made with ❤️ by Theis Gaedigk - Class of 2019 at MCS-Bochum
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import {
|
|
||||||
Field,
|
|
||||||
Textarea,
|
|
||||||
Button,
|
|
||||||
Alert,
|
|
||||||
Container,
|
|
||||||
Text,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { API_BASE } from "@/config/api.config";
|
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { Header } from "@/components/Header";
|
|
||||||
|
|
||||||
interface Alert {
|
|
||||||
type: "info" | "warning" | "success" | "error" | "neutral";
|
|
||||||
headline: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContactPage = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const [alert, setAlert] = useState<Alert | null>(null);
|
|
||||||
|
|
||||||
const sendMessage = async () => {
|
|
||||||
// Logic to send the message
|
|
||||||
const result = await fetch(`${API_BASE}/api/users/contact`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
setAlert({
|
|
||||||
type: "success",
|
|
||||||
headline: t("contactPage_successHeadline"),
|
|
||||||
text: t("contactPage_successText"),
|
|
||||||
});
|
|
||||||
setMessage("");
|
|
||||||
} else {
|
|
||||||
setAlert({
|
|
||||||
type: "error",
|
|
||||||
headline: t("contactPage_errorHeadline"),
|
|
||||||
text: t("contactPage_errorText"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
|
||||||
<Header />
|
|
||||||
<Field.Root invalid={message === ""}>
|
|
||||||
<Field.Label>
|
|
||||||
<Text>{t("contactPage_messageDescription")}</Text>
|
|
||||||
<Field.RequiredIndicator />
|
|
||||||
</Field.Label>
|
|
||||||
<Textarea
|
|
||||||
placeholder={t("contactPage_messagePlaceholder")}
|
|
||||||
variant="subtle"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
/>
|
|
||||||
{message === "" && (
|
|
||||||
<Field.ErrorText>{t("contactPage_messageErrorText")}</Field.ErrorText>
|
|
||||||
)}
|
|
||||||
</Field.Root>
|
|
||||||
{alert && (
|
|
||||||
<Alert.Root status={alert.type}>
|
|
||||||
<Alert.Indicator />
|
|
||||||
<Alert.Content>
|
|
||||||
<Alert.Title>{alert.headline}</Alert.Title>
|
|
||||||
<Alert.Description>{alert.text}</Alert.Description>
|
|
||||||
</Alert.Content>
|
|
||||||
</Alert.Root>
|
|
||||||
)}
|
|
||||||
<Button onClick={sendMessage}>{t("contactPage_sendButton")}</Button>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -108,6 +108,7 @@ export const HomePage = () => {
|
|||||||
}
|
}
|
||||||
setBorrowableItems(response.data);
|
setBorrowableItems(response.data);
|
||||||
setIsMsg(false);
|
setIsMsg(false);
|
||||||
|
console.log(borrowableItems);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Button,
|
Button,
|
||||||
Container,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import MyAlert from "@/components/myChakra/MyAlert";
|
import MyAlert from "@/components/myChakra/MyAlert";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Header } from "@/components/Header";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const formatDateTime = (value: string | null | undefined) => {
|
export const formatDateTime = (value: string | null | undefined) => {
|
||||||
if (!value) return "N/A";
|
if (!value) return "N/A";
|
||||||
@@ -33,7 +32,6 @@ type Loan = {
|
|||||||
returned_date: string | null;
|
returned_date: string | null;
|
||||||
take_date: string | null;
|
take_date: string | null;
|
||||||
loaned_items_name: string[] | string;
|
loaned_items_name: string[] | string;
|
||||||
note: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Device = {
|
type Device = {
|
||||||
@@ -48,6 +46,7 @@ type Device = {
|
|||||||
|
|
||||||
const Landingpage: React.FC = () => {
|
const Landingpage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loans, setLoans] = useState<Loan[]>([]);
|
const [loans, setLoans] = useState<Loan[]>([]);
|
||||||
@@ -60,7 +59,7 @@ const Landingpage: React.FC = () => {
|
|||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
description: string,
|
description: string
|
||||||
) => {
|
) => {
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
setErrorStatus(status);
|
setErrorStatus(status);
|
||||||
@@ -86,7 +85,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_loan"),
|
t("unexpected-date-format_loan")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ const Landingpage: React.FC = () => {
|
|||||||
setError(
|
setError(
|
||||||
"error",
|
"error",
|
||||||
t("error-by-loading"),
|
t("error-by-loading"),
|
||||||
t("unexpected-date-format_device"),
|
t("unexpected-date-format_device")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -116,8 +115,14 @@ const Landingpage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<>
|
||||||
<Header />
|
<Heading as="h1" size="lg" mb={2}>
|
||||||
|
Matthias-Claudius-Schule Technik
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Button onClick={() => navigate("/", { replace: true })}>
|
||||||
|
{t("back")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Heading as="h2" size="md" mb={4}>
|
<Heading as="h2" size="md" mb={4}>
|
||||||
{t("all-loans")}
|
{t("all-loans")}
|
||||||
@@ -163,9 +168,6 @@ const Landingpage: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>{t("return-date")}</strong>
|
<strong>{t("return-date")}</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
|
||||||
<strong>{t("note")}</strong>
|
|
||||||
</Table.ColumnHeader>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -182,7 +184,6 @@ const Landingpage: React.FC = () => {
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.take_date)}</Table.Cell>
|
||||||
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
<Table.Cell>{formatDateTime(loan.returned_date)}</Table.Cell>
|
||||||
<Table.Cell>{loan.note}</Table.Cell>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
))}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
@@ -259,7 +260,7 @@ const Landingpage: React.FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Container>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
|||||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
import { Navigate, useNavigate } from "react-router-dom";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Footer } from "@/components/footer/Footer";
|
||||||
import { API_BASE } from "@/config/api.config";
|
import { API_BASE } from "@/config/api.config";
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
@@ -15,15 +16,13 @@ export const LoginPage = () => {
|
|||||||
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||||
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
|
||||||
const from = location.state?.from?.pathname || "/";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
navigate(from, { replace: true });
|
navigate("/", { replace: true });
|
||||||
window.location.reload(); // if deleted, the user context is not updated in time
|
window.location.reload(); // Wenn entfernt: Seite bleibt schwarz und muss manuell neu geladen werden
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, navigate, from]);
|
}, [isLoggedIn, navigate]);
|
||||||
|
|
||||||
const loginFnc = async (username: string, password: string) => {
|
const loginFnc = async (username: string, password: string) => {
|
||||||
const response = await fetch(`${API_BASE}/api/users/login`, {
|
const response = await fetch(`${API_BASE}/api/users/login`, {
|
||||||
@@ -62,15 +61,15 @@ export const LoginPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTriggerLogout(false);
|
setTriggerLogout(false);
|
||||||
navigate(from, { replace: true });
|
navigate("/", { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to={from} replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center p-4">
|
<div className="min-h-screen flex items-center justify-center p-4">
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<Card.Root maxW="sm">
|
<Card.Root maxW="sm">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
@@ -114,6 +113,7 @@ export const LoginPage = () => {
|
|||||||
</Card.Footer>
|
</Card.Footer>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</form>
|
</form>
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,86 +112,6 @@ export const MyLoansPage = () => {
|
|||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
return `${d}.${M}.${y} ${h}:${min}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTakeAction = async (loanCode: string) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`${API_BASE}/api/loans/set-take-date/${loanCode}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(t("error"));
|
|
||||||
setMsgDescription(t("error-take-loan"));
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the loan in state
|
|
||||||
setLoans((prev) =>
|
|
||||||
prev.map((loan) =>
|
|
||||||
loan.loan_code === loanCode
|
|
||||||
? { ...loan, take_date: new Date().toISOString() }
|
|
||||||
: loan,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setMsgStatus("success");
|
|
||||||
setMsgTitle(t("success"));
|
|
||||||
setMsgDescription(t("take-loan-success"));
|
|
||||||
setIsMsg(true);
|
|
||||||
} catch (e) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(t("error"));
|
|
||||||
setMsgDescription(t("network-error"));
|
|
||||||
setIsMsg(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReturnAction = async (loanCode: string) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`${API_BASE}/api/loans/set-return-date/${loanCode}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(t("error"));
|
|
||||||
setMsgDescription(t("error-return-loan"));
|
|
||||||
setIsMsg(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the loan in state
|
|
||||||
setLoans((prev) =>
|
|
||||||
prev.map((loan) =>
|
|
||||||
loan.loan_code === loanCode
|
|
||||||
? { ...loan, returned_date: new Date().toISOString() }
|
|
||||||
: loan,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setMsgStatus("success");
|
|
||||||
setMsgTitle(t("success"));
|
|
||||||
setMsgDescription(t("return-loan-success"));
|
|
||||||
setIsMsg(true);
|
|
||||||
} catch (e) {
|
|
||||||
setMsgStatus("error");
|
|
||||||
setMsgTitle(t("error"));
|
|
||||||
setMsgDescription(t("network-error"));
|
|
||||||
setIsMsg(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container className="px-6 sm:px-8 pt-10">
|
<Container className="px-6 sm:px-8 pt-10">
|
||||||
@@ -270,33 +190,8 @@ export const MyLoansPage = () => {
|
|||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>{formatDate(loan.take_date)}</Table.Cell>
|
||||||
{loan.take_date ? (
|
<Table.Cell>{formatDate(loan.returned_date)}</Table.Cell>
|
||||||
formatDate(loan.take_date)
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
colorPalette="teal"
|
|
||||||
onClick={() => handleTakeAction(loan.loan_code)}
|
|
||||||
>
|
|
||||||
{t("take")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{loan.returned_date ? (
|
|
||||||
formatDate(loan.returned_date)
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
colorPalette="blue"
|
|
||||||
onClick={() => handleReturnAction(loan.loan_code)}
|
|
||||||
disabled={!loan.take_date}
|
|
||||||
>
|
|
||||||
{t("return")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{loan.note}</Table.Cell>
|
<Table.Cell>{loan.note}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Dialog.Root role="alertdialog">
|
<Dialog.Root role="alertdialog">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { API_BASE } from "@/config/api.config";
|
|||||||
|
|
||||||
export const getBorrowableItems = async (
|
export const getBorrowableItems = async (
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
const response = await fetch(`${API_BASE}/api/loans/borrowable-items`, {
|
||||||
@@ -22,7 +22,7 @@ export const getBorrowableItems = async (
|
|||||||
status: "error",
|
status: "error",
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description:
|
description:
|
||||||
"An error occurred on the server. Sometimes reloading the page helps. Otherwise, please contact the administrator.",
|
"Ein Fehler ist auf dem Server aufgetreten. Manchmal hilft es, die Seite neu zu laden.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export const createLoan = async (
|
|||||||
itemIds: number[],
|
itemIds: number[],
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate: string,
|
||||||
note: string | null,
|
note: string | null
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
|
const response = await fetch(`${API_BASE}/api/loans/createLoan`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
||||||
"optional-note": "Optionale Notiz",
|
"optional-note": "Optionale Notiz",
|
||||||
"note": "Notiz",
|
"note": "Notiz",
|
||||||
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und ändern.",
|
||||||
"role": "Rolle",
|
"role": "Rolle",
|
||||||
"admin-status": "Admin-Status",
|
"admin-status": "Admin-Status",
|
||||||
"first-name": "Vorname",
|
"first-name": "Vorname",
|
||||||
@@ -72,21 +72,5 @@
|
|||||||
"last-borrowed-person": "Zuletzt ausgeliehen von",
|
"last-borrowed-person": "Zuletzt ausgeliehen von",
|
||||||
"currently-borrowed-by": "Derzeit ausgeliehen von",
|
"currently-borrowed-by": "Derzeit ausgeliehen von",
|
||||||
"back": "Zurückgehen",
|
"back": "Zurückgehen",
|
||||||
"landingpage": "Übersichtsseite",
|
"landingpage": "Übersichtsseite"
|
||||||
"contactPage_successHeadline": "Nachricht erfolgreich gesendet",
|
|
||||||
"contactPage_successText": "Vielen Dank, dass Sie uns kontaktiert haben. Wir werden uns so schnell wie möglich bei Ihnen melden.",
|
|
||||||
"contactPage_errorHeadline": "Fehler beim Senden der Nachricht",
|
|
||||||
"contactPage_errorText": "Beim Senden Ihrer Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
|
||||||
"contactPage_sendButton": "Nachricht senden",
|
|
||||||
"contactPage_messageLabel": "Nachricht",
|
|
||||||
"contactPage_messagePlaceholder": "Geben Sie hier Ihre Nachricht ein...",
|
|
||||||
"contactPage_messageErrorText": "Dieses Feld darf nicht leer sein.",
|
|
||||||
"contact": "Kontakt",
|
|
||||||
"take": "Abholen",
|
|
||||||
"return": "Zurückgeben",
|
|
||||||
"serverError": "Serverfehler. Bitte versuchen Sie es später erneut, oder laden Sie die Seite neu.",
|
|
||||||
"take-loan-success": "Ausleihe erfolgreich abgeholt",
|
|
||||||
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
|
|
||||||
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
|
|
||||||
"contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden."
|
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
||||||
"optional-note": "Optional note",
|
"optional-note": "Optional note",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
"user-info-desc": "Here you can view and edit your personal information.",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"admin-status": "Admin status",
|
"admin-status": "Admin status",
|
||||||
"first-name": "First name",
|
"first-name": "First name",
|
||||||
@@ -72,21 +72,5 @@
|
|||||||
"last-borrowed-person": "Last borrowed by",
|
"last-borrowed-person": "Last borrowed by",
|
||||||
"currently-borrowed-by": "Currently borrowed by",
|
"currently-borrowed-by": "Currently borrowed by",
|
||||||
"back": "Go back",
|
"back": "Go back",
|
||||||
"landingpage": "Overview page",
|
"landingpage": "Overview page"
|
||||||
"contactPage_successHeadline": "Message sent successfully",
|
|
||||||
"contactPage_successText": "Thank you for contacting us. We will get back to you as soon as possible.",
|
|
||||||
"contactPage_errorHeadline": "Error sending message",
|
|
||||||
"contactPage_errorText": "An error occurred while sending your message. Please try again later.",
|
|
||||||
"contactPage_sendButton": "Send message",
|
|
||||||
"contactPage_messageLabel": "Message",
|
|
||||||
"contactPage_messagePlaceholder": "Enter your message here...",
|
|
||||||
"contactPage_messageErrorText": "This field cannot be empty.",
|
|
||||||
"contact": "Contact",
|
|
||||||
"serverError": "Server error. Please try again later, or refresh the page.",
|
|
||||||
"take": "Take",
|
|
||||||
"return": "Return",
|
|
||||||
"take-loan-success": "Loan taken successfully",
|
|
||||||
"return-loan-success": "Loan returned successfully",
|
|
||||||
"network-error": "Network error. Please contact the administrator.",
|
|
||||||
"contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible."
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/user-star.svg" />
|
<link rel="icon" type="image/svg+xml" href="/user-star.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Adminpanel</title>
|
<title>Admin panel</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /backend {
|
|
||||||
return 301 /backend/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /backend/ {
|
|
||||||
proxy_pass http://borrow_system-backend_v2:8004/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
68
admin/package-lock.json
generated
68
admin/package-lock.json
generated
@@ -3675,16 +3675,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "1.1.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
@@ -4470,9 +4466,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -4908,9 +4904,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
"node_modules/minizlib": {
|
||||||
"version": "3.1.0",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
||||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minipass": "^7.1.2"
|
"minipass": "^7.1.2"
|
||||||
@@ -4919,6 +4915,21 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "dist/cjs/src/bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -5296,9 +5307,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.13.0",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz",
|
||||||
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
|
"integrity": "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
@@ -5318,12 +5329,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.13.0",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.2.tgz",
|
||||||
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
|
"integrity": "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.13.0"
|
"react-router": "7.8.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
@@ -5481,9 +5492,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
@@ -5638,15 +5649,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.7",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
"chownr": "^3.0.0",
|
"chownr": "^3.0.0",
|
||||||
"minipass": "^7.1.2",
|
"minipass": "^7.1.2",
|
||||||
"minizlib": "^3.1.0",
|
"minizlib": "^3.0.1",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
"yallist": "^5.0.0"
|
"yallist": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useState } from "react";
|
|||||||
import { loginFunc } from "@/utils/loginUser";
|
import { loginFunc } from "@/utils/loginUser";
|
||||||
import MyAlert from "../components/myChakra/MyAlert";
|
import MyAlert from "../components/myChakra/MyAlert";
|
||||||
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||||
import { PasswordInput } from "@/components/ui/password-input";
|
|
||||||
|
|
||||||
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
@@ -44,7 +43,8 @@ const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
|
|||||||
</Field.Root>
|
</Field.Root>
|
||||||
<Field.Root>
|
<Field.Root>
|
||||||
<Field.Label>password</Field.Label>
|
<Field.Label>password</Field.Label>
|
||||||
<PasswordInput
|
<Input
|
||||||
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const APIKeyTable: React.FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log(data);
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError("error", "Failed to fetch items", "There is an error");
|
setError("error", "Failed to fetch items", "There is an error");
|
||||||
|
|||||||
@@ -193,12 +193,7 @@ const ItemTable: React.FC = () => {
|
|||||||
|
|
||||||
{/* make table fill available width, like UserTable */}
|
{/* make table fill available width, like UserTable */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Table.Root
|
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
|
||||||
size="sm"
|
|
||||||
striped
|
|
||||||
w="100%"
|
|
||||||
style={{ tableLayout: "auto" }} // Spalten nach Content
|
|
||||||
>
|
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
@@ -213,10 +208,10 @@ const ItemTable: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Im Schließfach</strong>
|
<strong>Im Schließfach</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
<Table.ColumnHeader>
|
||||||
<strong>Schließfachnummer</strong>
|
<strong>Schließfachnummer</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
<Table.ColumnHeader>
|
||||||
<strong>Schlüssel</strong>
|
<strong>Schlüssel</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
@@ -231,7 +226,7 @@ const ItemTable: React.FC = () => {
|
|||||||
<Table.ColumnHeader>
|
<Table.ColumnHeader>
|
||||||
<strong>Dav **</strong>
|
<strong>Dav **</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
<Table.ColumnHeader>
|
||||||
<strong>Aktionen</strong>
|
<strong>Aktionen</strong>
|
||||||
</Table.ColumnHeader>
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
@@ -319,7 +314,7 @@ const ItemTable: React.FC = () => {
|
|||||||
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
||||||
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
||||||
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
||||||
<Table.Cell whiteSpace="nowrap">
|
<Table.Cell>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleEditItems(
|
handleEditItems(
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const UserTable: React.FC = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchUserData();
|
const data = await fetchUserData();
|
||||||
|
console.log(data);
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setUsers(data);
|
setUsers(data);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -167,6 +167,7 @@ export const createItem = async (
|
|||||||
can_borrow_role: number,
|
can_borrow_role: number,
|
||||||
lockerNumber: string | null
|
lockerNumber: string | null
|
||||||
) => {
|
) => {
|
||||||
|
console.log(JSON.stringify({ item_name, can_borrow_role, lockerNumber }));
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE}/api/admin/item-data/create-item`,
|
`${API_BASE}/api/admin/item-data/create-item`,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
@@ -24,10 +23,14 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
/* Path aliases */
|
/* Chakra / Pfad Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"ignoreDeprecations": "5.0"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"backend-info": {
|
"backend-info": {
|
||||||
"version": "v2.1.1 (demo)"
|
"version": "v2.0.1 (dev)"
|
||||||
},
|
},
|
||||||
"frontend-info": {
|
"frontend-info": {
|
||||||
"version": "v2.1.2 (demo)"
|
"version": "v2.0 (dev)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.3.2 (demo)"
|
"version": "v1.3 (dev)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,11 +18,11 @@ export const createUser = async (
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
email,
|
email,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name
|
||||||
) => {
|
) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
[username, role, password, isAdmin, email, first_name, last_name],
|
[username, role, password, isAdmin, email, first_name, last_name]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -34,10 +34,10 @@ export const deleteUserById = async (userId) => {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePassword = async (username, newPassword) => {
|
export const changePassword = async (userId, newPassword) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
|
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE id = ?",
|
||||||
[newPassword, username],
|
[newPassword, userId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -49,11 +49,11 @@ export const editUserById = async (
|
|||||||
last_name,
|
last_name,
|
||||||
role,
|
role,
|
||||||
email,
|
email,
|
||||||
is_admin,
|
is_admin
|
||||||
) => {
|
) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
|
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
|
||||||
[first_name, last_name, role, email, is_admin, userId],
|
[first_name, last_name, role, email, is_admin, userId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) return { success: true };
|
if (result.affectedRows > 0) return { success: true };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -61,7 +61,7 @@ export const editUserById = async (
|
|||||||
|
|
||||||
export const getAllUsers = async () => {
|
export const getAllUsers = async () => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users",
|
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users"
|
||||||
);
|
);
|
||||||
if (result.length > 0) return { success: true, data: result };
|
if (result.length > 0) return { success: true, data: result };
|
||||||
return { success: false };
|
return { success: false };
|
||||||
@@ -70,7 +70,7 @@ export const getAllUsers = async () => {
|
|||||||
export const getUserById = async (userId) => {
|
export const getUserById = async (userId) => {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
|
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
|
||||||
[userId],
|
[userId]
|
||||||
);
|
);
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const getItemsFromDatabaseV2 = async () => {
|
|||||||
export const getLoanByCodeV2 = async (loan_code) => {
|
export const getLoanByCodeV2 = async (loan_code) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
|
||||||
[loan_code],
|
[loan_code]
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, data: result[0] };
|
return { success: true, data: result[0] };
|
||||||
@@ -33,7 +33,7 @@ export const getLoanByCodeV2 = async (loan_code) => {
|
|||||||
export const changeInSafeStateV2 = async (itemId) => {
|
export const changeInSafeStateV2 = async (itemId) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||||
[itemId],
|
[itemId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -42,62 +42,50 @@ export const changeInSafeStateV2 = async (itemId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setReturnDateV2 = async (loanCode) => {
|
export const setReturnDateV2 = async (loanCode) => {
|
||||||
try {
|
|
||||||
const [items] = await pool.query(
|
|
||||||
"SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (items.length === 0)
|
|
||||||
return { success: false, message: "No items found for loan" };
|
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
|
||||||
? items[0].loaned_items_id
|
|
||||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows === 0) return { success: false };
|
|
||||||
|
|
||||||
if (itemIds.length > 0) {
|
|
||||||
await pool.query(
|
|
||||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)",
|
|
||||||
[items[0].username, itemIds],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, data: { returned: true } };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("setReturnDateV2 error:", error);
|
|
||||||
return { success: false, message: "Failed to set return date" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setTakeDateV2 = async (loanCode) => {
|
|
||||||
const [isTaken] = await pool.query(
|
|
||||||
"SELECT take_date FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTaken.length === 0 || isTaken[0].take_date !== null) {
|
|
||||||
return { success: false, message: "Loan not found or already taken" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [items] = await pool.query(
|
const [items] = await pool.query(
|
||||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [owner] = await pool.query(
|
const [owner] = await pool.query(
|
||||||
"SELECT username FROM loans WHERE loan_code = ?",
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (items.length === 0)
|
if (items.length === 0) return { success: false };
|
||||||
return { success: false, message: "No items found for loan" };
|
|
||||||
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
|
? items[0].loaned_items_id
|
||||||
|
: JSON.parse(items[0].loaned_items_id || "[]");
|
||||||
|
|
||||||
|
const [setItemStates] = await pool.query(
|
||||||
|
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
||||||
|
[owner[0].username, itemIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await pool.query(
|
||||||
|
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
||||||
|
[loanCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setTakeDateV2 = async (loanCode) => {
|
||||||
|
const [items] = await pool.query(
|
||||||
|
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [owner] = await pool.query(
|
||||||
|
"SELECT username FROM loans WHERE loan_code = ?",
|
||||||
|
[loanCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length === 0) return { success: false };
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
const itemIds = Array.isArray(items[0].loaned_items_id)
|
||||||
? items[0].loaned_items_id
|
? items[0].loaned_items_id
|
||||||
@@ -105,18 +93,18 @@ export const setTakeDateV2 = async (loanCode) => {
|
|||||||
|
|
||||||
const [setItemStates] = await pool.query(
|
const [setItemStates] = await pool.query(
|
||||||
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
||||||
[owner[0].username, itemIds],
|
[owner[0].username, itemIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
|
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
||||||
[loanCode],
|
[loanCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { message: "Failed to set take date", success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllLoansV2 = async () => {
|
export const getAllLoansV2 = async () => {
|
||||||
@@ -130,12 +118,12 @@ export const getAllLoansV2 = async () => {
|
|||||||
export const openDoor = async (doorKey) => {
|
export const openDoor = async (doorKey) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
|
||||||
[doorKey],
|
[doorKey]
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
const [changeItemSate] = await pool.query(
|
const [changeItemSate] = await pool.query(
|
||||||
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
|
||||||
[result[0].id],
|
[result[0].id]
|
||||||
);
|
);
|
||||||
if (changeItemSate.affectedRows > 0) {
|
if (changeItemSate.affectedRows > 0) {
|
||||||
return { success: true, data: result[0] };
|
return { success: true, data: result[0] };
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ router.get(
|
|||||||
} else {
|
} else {
|
||||||
res.status(404).json({ message: "Loan not found" });
|
res.status(404).json({ message: "Loan not found" });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to set the return date by the loan code
|
// Route for API to set the return date by the loan code
|
||||||
@@ -58,11 +58,11 @@ router.post(
|
|||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setReturnDateV2(loanCode);
|
const result = await setReturnDateV2(loanCode);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({});
|
res.status(200).json({ data: result.data });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
res.status(500).json({ message: "Failed to set return date" });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to set the take away date by the loan code
|
// Route for API to set the take away date by the loan code
|
||||||
@@ -73,11 +73,11 @@ router.post(
|
|||||||
const loanCode = req.params.loan_code;
|
const loanCode = req.params.loan_code;
|
||||||
const result = await setTakeDateV2(loanCode);
|
const result = await setTakeDateV2(loanCode);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
res.status(200).json({});
|
res.status(200).json({ data: result.data });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: result.message });
|
res.status(500).json({ message: "Failed to set take date" });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Route for API to open a door
|
// Route for API to open a door
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const createLoanInDatabase = async (
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
note,
|
note,
|
||||||
itemIds,
|
itemIds
|
||||||
) => {
|
) => {
|
||||||
if (!username)
|
if (!username)
|
||||||
return { success: false, code: "BAD_REQUEST", message: "Missing username" };
|
return { success: false, code: "BAD_REQUEST", message: "Missing username" };
|
||||||
@@ -52,7 +52,7 @@ export const createLoanInDatabase = async (
|
|||||||
// Ensure all items exist and collect names + lockers
|
// Ensure all items exist and collect names + lockers
|
||||||
const [itemsRows] = await conn.query(
|
const [itemsRows] = await conn.query(
|
||||||
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
|
"SELECT id, item_name, safe_nr FROM items WHERE id IN (?)",
|
||||||
[itemIds],
|
[itemIds]
|
||||||
);
|
);
|
||||||
if (!itemsRows || itemsRows.length !== itemIds.length) {
|
if (!itemsRows || itemsRows.length !== itemIds.length) {
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
@@ -65,7 +65,7 @@ export const createLoanInDatabase = async (
|
|||||||
|
|
||||||
const itemNames = itemIds
|
const itemNames = itemIds
|
||||||
.map(
|
.map(
|
||||||
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name,
|
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name
|
||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
@@ -80,9 +80,9 @@ export const createLoanInDatabase = async (
|
|||||||
sn !== undefined &&
|
sn !== undefined &&
|
||||||
Number.isInteger(Number(sn)) &&
|
Number.isInteger(Number(sn)) &&
|
||||||
Number(sn) >= 0 &&
|
Number(sn) >= 0 &&
|
||||||
Number(sn) <= 99,
|
Number(sn) <= 99
|
||||||
)
|
)
|
||||||
.map((sn) => Number(sn)),
|
.map((sn) => Number(sn))
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export const createLoanInDatabase = async (
|
|||||||
AND l.start_date < ?
|
AND l.start_date < ?
|
||||||
AND COALESCE(l.returned_date, l.end_date) > ?
|
AND COALESCE(l.returned_date, l.end_date) > ?
|
||||||
`,
|
`,
|
||||||
[itemIds, end, start],
|
[itemIds, end, start]
|
||||||
);
|
);
|
||||||
if (confRows?.[0]?.conflicts > 0) {
|
if (confRows?.[0]?.conflicts > 0) {
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
@@ -115,7 +115,7 @@ export const createLoanInDatabase = async (
|
|||||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
||||||
const [exists] = await conn.query(
|
const [exists] = await conn.query(
|
||||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
||||||
[candidate],
|
[candidate]
|
||||||
);
|
);
|
||||||
if (exists.length === 0) {
|
if (exists.length === 0) {
|
||||||
loanCode = candidate;
|
loanCode = candidate;
|
||||||
@@ -146,7 +146,7 @@ export const createLoanInDatabase = async (
|
|||||||
JSON.stringify(itemIds.map((n) => Number(n))),
|
JSON.stringify(itemIds.map((n) => Number(n))),
|
||||||
JSON.stringify(itemNames),
|
JSON.stringify(itemNames),
|
||||||
note,
|
note,
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
@@ -189,7 +189,7 @@ export const getLoanInfoWithID = async (loanId) => {
|
|||||||
export const getLoansFromDatabase = async (username) => {
|
export const getLoansFromDatabase = async (username) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
"SELECT * FROM loans WHERE username = ? AND deleted = 0;",
|
||||||
[username],
|
[username]
|
||||||
);
|
);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
return { success: true, status: true, data: result };
|
return { success: true, status: true, data: result };
|
||||||
@@ -202,7 +202,7 @@ export const getLoansFromDatabase = async (username) => {
|
|||||||
export const getBorrowableItemsFromDatabase = async (
|
export const getBorrowableItemsFromDatabase = async (
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
role = 0,
|
role = 0
|
||||||
) => {
|
) => {
|
||||||
// Overlap if: loan.start < end AND effective_end > start
|
// Overlap if: loan.start < end AND effective_end > start
|
||||||
// effective_end is returned_date if set, otherwise end_date
|
// effective_end is returned_date if set, otherwise end_date
|
||||||
@@ -236,7 +236,7 @@ export const getBorrowableItemsFromDatabase = async (
|
|||||||
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
||||||
[loanId],
|
[loanId]
|
||||||
);
|
);
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -260,69 +260,3 @@ export const getItems = async () => {
|
|||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setReturnDate = async (loanCode) => {
|
|
||||||
const [items] = await pool.query(
|
|
||||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [owner] = await pool.query(
|
|
||||||
"SELECT username FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (items.length === 0) return { success: false };
|
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
|
||||||
? items[0].loaned_items_id
|
|
||||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
|
||||||
|
|
||||||
const [setItemStates] = await pool.query(
|
|
||||||
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
|
|
||||||
[owner[0].username, itemIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
return { success: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setTakeDate = async (loanCode) => {
|
|
||||||
const [items] = await pool.query(
|
|
||||||
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [owner] = await pool.query(
|
|
||||||
"SELECT username FROM loans WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (items.length === 0) return { success: false };
|
|
||||||
|
|
||||||
const itemIds = Array.isArray(items[0].loaned_items_id)
|
|
||||||
? items[0].loaned_items_id
|
|
||||||
: JSON.parse(items[0].loaned_items_id || "[]");
|
|
||||||
|
|
||||||
const [setItemStates] = await pool.query(
|
|
||||||
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
|
|
||||||
[owner[0].username, itemIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [result] = await pool.query(
|
|
||||||
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
|
|
||||||
[loanCode],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
return { success: false };
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
getALLLoans,
|
getALLLoans,
|
||||||
getItems,
|
getItems,
|
||||||
SETdeleteLoanFromDatabase,
|
SETdeleteLoanFromDatabase,
|
||||||
setReturnDate,
|
|
||||||
setTakeDate,
|
|
||||||
} from "./database/loansMgmt.database.js";
|
} from "./database/loansMgmt.database.js";
|
||||||
import { sendMailLoan } from "./services/mailer.js";
|
import { sendMailLoan } from "./services/mailer.js";
|
||||||
|
|
||||||
@@ -50,7 +48,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
note,
|
note,
|
||||||
itemIds,
|
itemIds
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -61,8 +59,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
|||||||
mailInfo.data.loaned_items_name,
|
mailInfo.data.loaned_items_name,
|
||||||
mailInfo.data.start_date,
|
mailInfo.data.start_date,
|
||||||
mailInfo.data.end_date,
|
mailInfo.data.end_date,
|
||||||
mailInfo.data.created_at,
|
mailInfo.data.created_at
|
||||||
mailInfo.data.note,
|
|
||||||
);
|
);
|
||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
message: "Loan created successfully",
|
message: "Loan created successfully",
|
||||||
@@ -99,26 +96,6 @@ router.get("/loans", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
|
||||||
const loanCode = req.params.loan_code;
|
|
||||||
const result = await setReturnDate(loanCode);
|
|
||||||
if (result.success) {
|
|
||||||
res.status(200).json({ data: result.data });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ message: "Failed to set return date" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
|
||||||
const loanCode = req.params.loan_code;
|
|
||||||
const result = await setTakeDate(loanCode);
|
|
||||||
if (result.success) {
|
|
||||||
res.status(200).json({ data: result.data });
|
|
||||||
} else {
|
|
||||||
res.status(500).json({ message: "Failed to set take date" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/all-items", authenticate, async (req, res) => {
|
router.get("/all-items", authenticate, async (req, res) => {
|
||||||
const result = await getItems();
|
const result = await getItems();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -158,7 +135,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
|
|||||||
const result = await getBorrowableItemsFromDatabase(
|
const result = await getBorrowableItemsFromDatabase(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
req.user.role,
|
req.user.role
|
||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// return the array directly for consistency with /items
|
// return the array directly for consistency with /items
|
||||||
|
|||||||
@@ -34,21 +34,14 @@ const formatDateTime = (value) => {
|
|||||||
return "N/A";
|
return "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildLoanEmail({
|
function buildLoanEmail({ user, items, startDate, endDate, createdDate }) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}) {
|
|
||||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||||
const itemsList =
|
const itemsList =
|
||||||
Array.isArray(items) && items.length
|
Array.isArray(items) && items.length
|
||||||
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
||||||
.map(
|
.map(
|
||||||
(i) =>
|
(i) =>
|
||||||
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`,
|
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`
|
||||||
)
|
)
|
||||||
.join("")}</ul>`
|
.join("")}</ul>`
|
||||||
: "<span style='color:#111827;'>N/A</span>";
|
: "<span style='color:#111827;'>N/A</span>";
|
||||||
@@ -108,27 +101,21 @@ function buildLoanEmail({
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
startDate,
|
startDate
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
endDate,
|
endDate
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
||||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
||||||
createdDate,
|
createdDate
|
||||||
)}</td>
|
)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td style="padding:10px 14px; color:#6b7280; vertical-align:top;">Notiz</td>
|
|
||||||
<td style="padding:10px 14px; font-weight:600; color:#111827;">${
|
|
||||||
note || "Keine Notiz"
|
|
||||||
}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p style="margin:22px 0 0 0; font-size:14px;">
|
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||||
@@ -147,14 +134,7 @@ function buildLoanEmail({
|
|||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLoanEmailText({
|
function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}) {
|
|
||||||
const itemsText =
|
const itemsText =
|
||||||
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
|
||||||
return [
|
return [
|
||||||
@@ -165,18 +145,10 @@ function buildLoanEmailText({
|
|||||||
`Start: ${formatDateTime(startDate)}`,
|
`Start: ${formatDateTime(startDate)}`,
|
||||||
`Ende: ${formatDateTime(endDate)}`,
|
`Ende: ${formatDateTime(endDate)}`,
|
||||||
`Erstellt am: ${formatDateTime(createdDate)}`,
|
`Erstellt am: ${formatDateTime(createdDate)}`,
|
||||||
`Notiz: ${note || "Keine Notiz"}`,
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMailLoan(
|
export function sendMailLoan(user, items, startDate, endDate, createdDate) {
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
) {
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: process.env.MAIL_HOST,
|
host: process.env.MAIL_HOST,
|
||||||
port: process.env.MAIL_PORT,
|
port: process.env.MAIL_PORT,
|
||||||
@@ -198,18 +170,12 @@ export function sendMailLoan(
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
createdDate,
|
createdDate,
|
||||||
note,
|
|
||||||
}),
|
|
||||||
html: buildLoanEmail({
|
|
||||||
user,
|
|
||||||
items,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
createdDate,
|
|
||||||
note,
|
|
||||||
}),
|
}),
|
||||||
|
html: buildLoanEmail({ user, items, startDate, endDate, createdDate }),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Loan message sent:", info.messageId);
|
// debugging logs
|
||||||
|
// console.log("Message sent:", info.messageId);
|
||||||
})();
|
})();
|
||||||
|
// console.log("sendMailLoan called");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import nodemailer from "nodemailer";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
export function sendMail(username, message) {
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: process.env.MAIL_HOST,
|
|
||||||
port: process.env.MAIL_PORT,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: process.env.MAIL_USER,
|
|
||||||
pass: process.env.MAIL_PASSWORD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const mailText = `Neue Kontaktanfrage im Ausleihsystem.\n\nBenutzername: ${username}\n\nNachricht:\n${message}`;
|
|
||||||
|
|
||||||
const mailHtml = `<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Neue Nachricht im Ausleihsystem</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; line-height: 1.5; color: #222;">
|
|
||||||
<h2>Neue Nachricht im Ausleihsystem</h2>
|
|
||||||
<p><strong>Benutzername:</strong> ${username}</p>
|
|
||||||
<p><strong>Nachricht:</strong></p>
|
|
||||||
<p style="white-space: pre-line;">${message}</p>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
|
||||||
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
|
||||||
to: process.env.MAIL_SENDEES_CONTACT,
|
|
||||||
subject: "Sie haben eine neue Nachricht!",
|
|
||||||
text: mailText,
|
|
||||||
html: mailHtml,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Contact message sent: %s", info.messageId);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ dotenv.config();
|
|||||||
|
|
||||||
// database funcs import
|
// database funcs import
|
||||||
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
||||||
import { sendMail } from "./services/mailer_v2.js";
|
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
const result = await loginFunc(req.body.username, req.body.password);
|
const result = await loginFunc(req.body.username, req.body.password);
|
||||||
@@ -36,13 +35,4 @@ router.post("/change-password", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/contact", authenticate, async (req, res) => {
|
|
||||||
const message = req.body.message;
|
|
||||||
const username = req.user.username;
|
|
||||||
|
|
||||||
sendMail(username, message);
|
|
||||||
|
|
||||||
res.status(200).json({ message: "Contact message sent successfully" });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
USE borrow_system_new;
|
|
||||||
|
|
||||||
-- USERS
|
|
||||||
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin)
|
|
||||||
VALUES
|
|
||||||
('user1', 'passwordhash1', 'user1@example.com', 'First1', 'Last1', 1, false),
|
|
||||||
('user2', 'passwordhash2', 'user2@example.com', 'First2', 'Last2', 1, false),
|
|
||||||
('user3', 'passwordhash3', 'user3@example.com', 'First3', 'Last3', 2, false),
|
|
||||||
('admin1', 'passwordhash4', 'admin1@example.com', 'Admin', 'One', 9, true),
|
|
||||||
('admin2', 'passwordhash5', 'admin2@example.com', 'Admin', 'Two', 9, true);
|
|
||||||
|
|
||||||
-- ITEMS
|
|
||||||
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, door_key, last_borrowed_person, currently_borrowing)
|
|
||||||
VALUES
|
|
||||||
('Item1', 1, true, 1, 101, NULL, NULL),
|
|
||||||
('Item2', 1, true, 2, 102, 'user1', 'user1'),
|
|
||||||
('Item3', 2, true, 3, 103, 'user2', NULL),
|
|
||||||
('Item4', 1, false, NULL, NULL, NULL, NULL),
|
|
||||||
('Item5', 2, false, NULL, NULL, 'user3', 'user3');
|
|
||||||
|
|
||||||
-- LOANS
|
|
||||||
INSERT INTO loans (
|
|
||||||
username,
|
|
||||||
lockers,
|
|
||||||
loan_code,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
take_date,
|
|
||||||
returned_date,
|
|
||||||
created_at,
|
|
||||||
loaned_items_id,
|
|
||||||
loaned_items_name,
|
|
||||||
deleted,
|
|
||||||
note
|
|
||||||
)
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
'user1',
|
|
||||||
JSON_ARRAY('Locker1', 'Locker2'),
|
|
||||||
'123456',
|
|
||||||
'2026-02-01 09:00:00',
|
|
||||||
'2026-02-10 17:00:00',
|
|
||||||
'2026-02-01 09:15:00',
|
|
||||||
NULL,
|
|
||||||
'2026-02-01 09:00:00',
|
|
||||||
JSON_ARRAY(1, 2),
|
|
||||||
JSON_ARRAY('Item1', 'Item2'),
|
|
||||||
false,
|
|
||||||
'Erste allgemeine Ausleihe'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user2',
|
|
||||||
JSON_ARRAY('Locker3'),
|
|
||||||
'234567',
|
|
||||||
'2026-02-02 10:00:00',
|
|
||||||
'2026-02-05 16:00:00',
|
|
||||||
'2026-02-02 10:05:00',
|
|
||||||
'2026-02-05 15:30:00',
|
|
||||||
'2026-02-02 10:00:00',
|
|
||||||
JSON_ARRAY(3),
|
|
||||||
JSON_ARRAY('Item3'),
|
|
||||||
false,
|
|
||||||
'Zurückgegeben vor Enddatum'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user3',
|
|
||||||
JSON_ARRAY(),
|
|
||||||
'345678',
|
|
||||||
'2026-02-03 08:30:00',
|
|
||||||
'2026-02-15 18:00:00',
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
'2026-02-03 08:30:00',
|
|
||||||
JSON_ARRAY(5),
|
|
||||||
JSON_ARRAY('Item5'),
|
|
||||||
false,
|
|
||||||
'Noch ausgeliehen'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'user1',
|
|
||||||
JSON_ARRAY('Locker4'),
|
|
||||||
'456789',
|
|
||||||
'2025-12-01 09:00:00',
|
|
||||||
'2025-12-03 17:00:00',
|
|
||||||
'2025-12-01 09:10:00',
|
|
||||||
'2025-12-03 16:45:00',
|
|
||||||
'2025-12-01 09:00:00',
|
|
||||||
JSON_ARRAY(1),
|
|
||||||
JSON_ARRAY('Item1'),
|
|
||||||
true,
|
|
||||||
'Alte, gelöschte Ausleihe'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- API KEYS
|
|
||||||
INSERT INTO apiKeys (api_key, entry_name)
|
|
||||||
VALUES
|
|
||||||
('10000001', 'Entry1'),
|
|
||||||
('10000002', 'Entry2'),
|
|
||||||
('10000003', 'Entry3'),
|
|
||||||
('10000004', 'Entry4');
|
|
||||||
120
backendV2/schemeV2.mock_data.sql
Normal file
120
backendV2/schemeV2.mock_data.sql
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
USE borrow_system_new;
|
||||||
|
|
||||||
|
-- Reset tables (no FKs defined, so order is safe)
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
TRUNCATE TABLE loans;
|
||||||
|
TRUNCATE TABLE apiKeys;
|
||||||
|
TRUNCATE TABLE items;
|
||||||
|
TRUNCATE TABLE users;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- Users (roles 1–6, plain-text passwords; is_admin is BOOL)
|
||||||
|
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin) VALUES
|
||||||
|
('admin', 'adminpass', 'admin@example.com', 'System', 'Admin', 6, TRUE),
|
||||||
|
('alice', 'alice123', 'alice@example.com', 'Alice', 'Andersen',1, FALSE),
|
||||||
|
('bob', 'bob12345', 'bob@example.com', 'Bob', 'Berg', 2, FALSE),
|
||||||
|
('carol', 'carol123', 'carol@example.com', 'Carol', 'Christensen', 3, FALSE),
|
||||||
|
('dave', 'dave123', 'dave@example.com', 'Dave', 'Dahl', 4, FALSE),
|
||||||
|
('erin', 'erin123', 'erin@example.com', 'Erin', 'Enevoldsen', 5, FALSE),
|
||||||
|
('frank', 'frank123', 'frank@example.com', 'Frank', 'Fisher', 2, FALSE),
|
||||||
|
('grace', 'grace123', 'grace@example.com', 'Grace', 'Gundersen',1, FALSE),
|
||||||
|
('heidi', 'heidi123', 'heidi@example.com', 'Heidi', 'Hansen', 4, FALSE),
|
||||||
|
('tech', 'techpass', 'tech@example.com', 'Tech', 'User', 5, TRUE);
|
||||||
|
|
||||||
|
-- Items (safe_nr is two digits or NULL; matches CHECK and UNIQUE constraint)
|
||||||
|
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, last_borrowed_person, currently_borrowing) VALUES
|
||||||
|
('Laptop A', 2, FALSE, NULL, 'grace', 'bob'),
|
||||||
|
('Laptop B', 2, TRUE, '01', NULL, NULL),
|
||||||
|
('Camera Canon', 3, TRUE, '02', 'erin', NULL),
|
||||||
|
('Microphone Rode', 1, TRUE, '03', 'grace', NULL),
|
||||||
|
('Tripod Manfrotto', 1, TRUE, '04', 'frank', NULL),
|
||||||
|
('Oscilloscope Tek', 4, TRUE, '05', NULL, NULL),
|
||||||
|
('VR Headset', 3, FALSE, NULL, 'heidi', 'carol'),
|
||||||
|
('Keycard Programmer', 6, TRUE, '06', 'admin', NULL);
|
||||||
|
|
||||||
|
-- Loans (JSON strings, 6-digit numeric loan_code per CHECK)
|
||||||
|
-- Assumes the items above have ids 1..8 in insert order
|
||||||
|
INSERT INTO loans (
|
||||||
|
username,
|
||||||
|
lockers,
|
||||||
|
loan_code,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
take_date,
|
||||||
|
returned_date,
|
||||||
|
loaned_items_id,
|
||||||
|
loaned_items_name,
|
||||||
|
deleted,
|
||||||
|
note
|
||||||
|
) VALUES
|
||||||
|
-- Active loan: bob has Laptop A (item id 1, locker "01")
|
||||||
|
('bob',
|
||||||
|
'["01"]',
|
||||||
|
'123456',
|
||||||
|
'2025-11-15 09:00:00',
|
||||||
|
'2025-11-22 17:00:00',
|
||||||
|
'2025-11-15 09:15:00',
|
||||||
|
NULL,
|
||||||
|
'[1]',
|
||||||
|
'["Laptop A"]',
|
||||||
|
FALSE,
|
||||||
|
'Active loan - Laptop A'
|
||||||
|
),
|
||||||
|
-- Returned loan: frank had Tripod Manfrotto (item id 5, locker "04")
|
||||||
|
('frank',
|
||||||
|
'["04"]',
|
||||||
|
'234567',
|
||||||
|
'2025-10-01 10:00:00',
|
||||||
|
'2025-10-07 16:00:00',
|
||||||
|
'2025-10-01 10:05:00',
|
||||||
|
'2025-10-05 15:30:00',
|
||||||
|
'[5]',
|
||||||
|
'["Tripod Manfrotto"]',
|
||||||
|
FALSE,
|
||||||
|
'Completed loan'
|
||||||
|
),
|
||||||
|
-- Future reservation: dave will take Oscilloscope Tek (item id 6, locker "05")
|
||||||
|
('dave',
|
||||||
|
'["05"]',
|
||||||
|
'345678',
|
||||||
|
'2025-12-10 09:00:00',
|
||||||
|
'2025-12-12 17:00:00',
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'[6]',
|
||||||
|
'["Oscilloscope Tek"]',
|
||||||
|
FALSE,
|
||||||
|
'Reserved'
|
||||||
|
),
|
||||||
|
-- Active loan: carol has VR Headset (item id 7, locker "02")
|
||||||
|
('carol',
|
||||||
|
'["02"]',
|
||||||
|
'456789',
|
||||||
|
'2025-11-10 13:00:00',
|
||||||
|
'2025-11-20 12:00:00',
|
||||||
|
'2025-11-10 13:10:00',
|
||||||
|
NULL,
|
||||||
|
'[7]',
|
||||||
|
'["VR Headset"]',
|
||||||
|
FALSE,
|
||||||
|
'Active loan - VR Headset'
|
||||||
|
),
|
||||||
|
-- Soft-deleted historic loan: grace had Microphone + Tripod (item ids 4,5; lockers "03","04")
|
||||||
|
('grace',
|
||||||
|
'["03","04"]',
|
||||||
|
'567890',
|
||||||
|
'2025-09-01 09:00:00',
|
||||||
|
'2025-09-03 17:00:00',
|
||||||
|
'2025-09-01 09:10:00',
|
||||||
|
'2025-09-03 16:45:00',
|
||||||
|
'[4,5]',
|
||||||
|
'["Microphone Rode","Tripod Manfrotto"]',
|
||||||
|
TRUE,
|
||||||
|
'Canceled/soft-deleted record'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- API keys (8-digit numeric keys per CHECK)
|
||||||
|
INSERT INTO apiKeys (api_key, entry_name, last_used_at) VALUES
|
||||||
|
('12345678', 'CI token', '2025-11-15 08:00:00'),
|
||||||
|
('87654321', 'Local dev', NULL),
|
||||||
|
('00000001', 'Monitoring', '2025-11-10 12:30:00');
|
||||||
41
next-js/.gitignore
vendored
Normal file
41
next-js/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
next-js/README.md
Normal file
36
next-js/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
BIN
next-js/app/favicon.ico
Normal file
BIN
next-js/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
next-js/app/globals.css
Normal file
26
next-js/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
34
next-js/app/layout.tsx
Normal file
34
next-js/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
next-js/app/page.tsx
Normal file
65
next-js/app/page.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||||
|
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js logo"
|
||||||
|
width={100}
|
||||||
|
height={20}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||||
|
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||||
|
To get started, edit the page.tsx file.
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||||
|
Looking for a starting point or more instructions? Head over to{" "}
|
||||||
|
<a
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Templates
|
||||||
|
</a>{" "}
|
||||||
|
or the{" "}
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||||
|
>
|
||||||
|
Learning
|
||||||
|
</a>{" "}
|
||||||
|
center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel logomark"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Deploy Now
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex h-12 w-full items-center justify-center rounded-full border border-solid px-5 transition-colors hover:border-transparent dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
next-js/eslint.config.mjs
Normal file
18
next-js/eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
8
next-js/next.config.ts
Normal file
8
next-js/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
reactCompiler: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6557
next-js/package-lock.json
generated
Normal file
6557
next-js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
next-js/package.json
Normal file
27
next-js/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "next-js",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.0.5",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-dom": "19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.0.5",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
next-js/postcss.config.mjs
Normal file
7
next-js/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
next-js/public/file.svg
Normal file
1
next-js/public/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
next-js/public/globe.svg
Normal file
1
next-js/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
next-js/public/next.svg
Normal file
1
next-js/public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
next-js/public/vercel.svg
Normal file
1
next-js/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
next-js/public/window.svg
Normal file
1
next-js/public/window.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
34
next-js/tsconfig.json
Normal file
34
next-js/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user