Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98834a9270 | |||
| 8d04465705 | |||
| 8589971dc8 | |||
| 6ec8e19737 | |||
| 9f44a4796d | |||
| dc0a68f7f1 | |||
| fe3a06e5ce | |||
| 179f5686d1 | |||
| 28373e0231 |
@@ -117,8 +117,3 @@ ToDo.txt
|
||||
|
||||
# only in development branch
|
||||
next-env.d.ts
|
||||
|
||||
# psd files from footage
|
||||
footage/*.psd
|
||||
|
||||
icon/
|
||||
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 416 KiB |
@@ -2,11 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/icon_borrow-system-frontend_dark.png"
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ausleihsystem</title>
|
||||
</head>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "v2.1.2 (dev)",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,7 +12,6 @@
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.28.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"i18next": "^25.6.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shapes-icon lucide-shapes"><path d="M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"/><rect x="3" y="14" width="7" height="7" rx="1"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
||||
|
After Width: | Height: | Size: 420 B |
@@ -1,28 +0,0 @@
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
|
||||
export const unlockAnimation = () => {
|
||||
return (
|
||||
<DotLottieReact
|
||||
src="https://lottie.host/f839baa1-9c64-44c4-9386-f0e4c87ab208/2Iw1m4k86d.lottie"
|
||||
autoplay
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const approvalAnimation = () => {
|
||||
return (
|
||||
<DotLottieReact
|
||||
src="https://lottie.host/b7257009-9e3f-43e2-8112-a176f4696e4c/iQxxqAVOGX.lottie"
|
||||
autoplay
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const logoutAnimation = () => {
|
||||
return (
|
||||
<DotLottieReact
|
||||
src="https://lottie.host/4975758c-de38-4d15-9f74-927709751d32/v8FtKpnD1y.lottie"
|
||||
autoplay
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,6 @@ import { borrowAbleItemsAtom } from "@/states/Atoms";
|
||||
import { createLoan } from "@/utils/Fetcher";
|
||||
import { Header } from "@/components/Header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { approvalAnimation } from "@/components/dotLottie";
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
@@ -28,8 +27,6 @@ export interface User {
|
||||
export const HomePage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
|
||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
||||
const [startDate, setStartDate] = useState("");
|
||||
const [endDate, setEndDate] = useState("");
|
||||
@@ -49,26 +46,11 @@ export const HomePage = () => {
|
||||
setSelectedItems((prevSelected) =>
|
||||
prevSelected.includes(itemId)
|
||||
? prevSelected.filter((id) => id !== itemId)
|
||||
: [...prevSelected, itemId],
|
||||
: [...prevSelected, itemId]
|
||||
);
|
||||
};
|
||||
|
||||
const showApprovalAnimation = (seconds: number) => {
|
||||
const milliseconds = seconds * 1000;
|
||||
|
||||
setShowAnimation(true);
|
||||
window.setTimeout(() => {
|
||||
setShowAnimation(false);
|
||||
}, milliseconds);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAnimation && (
|
||||
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||
<div>{approvalAnimation()}</div>
|
||||
</div>
|
||||
)}
|
||||
<Container className="px-6 sm:px-8 pt-10">
|
||||
<Header />
|
||||
{isMsg && (
|
||||
@@ -176,7 +158,7 @@ export const HomePage = () => {
|
||||
maxLength={MAX_CHARACTERS}
|
||||
onChange={(e) => {
|
||||
setNote(
|
||||
e.currentTarget.value.slice(0, MAX_CHARACTERS),
|
||||
e.currentTarget.value.slice(0, MAX_CHARACTERS)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -196,17 +178,16 @@ export const HomePage = () => {
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(response.title || t("error"));
|
||||
setMsgDescription(
|
||||
response.description || t("unknown-error"),
|
||||
response.description || t("unknown-error")
|
||||
);
|
||||
setIsMsg(true);
|
||||
return;
|
||||
}
|
||||
showApprovalAnimation(3);
|
||||
setMsgStatus("success");
|
||||
setMsgTitle(t("success"));
|
||||
setMsgDescription(t("loan-success"));
|
||||
setIsMsg(true);
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -215,6 +196,5 @@ export const HomePage = () => {
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,47 +4,26 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||
import { useAtom } from "jotai";
|
||||
import Cookies from "js-cookie";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import { unlockAnimation } from "@/components/dotLottie";
|
||||
import { logoutAnimation } from "@/components/dotLottie";
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [showLogout, setShowLogout] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const from = location.state?.from?.pathname || "/";
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerLogout) {
|
||||
setShowLogout(true);
|
||||
window.setTimeout(() => {
|
||||
setShowLogout(false);
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
// Existing sessions should redirect immediately, fresh logins wait for animation.
|
||||
if (!showAnimation) {
|
||||
if (isLoggedIn) {
|
||||
navigate(from, { replace: true });
|
||||
return;
|
||||
window.location.reload(); // if deleted, the user context is not updated in time
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
navigate(from, { replace: true });
|
||||
window.location.reload(); // keeps user context in sync after login
|
||||
}, 3000);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [isLoggedIn, showAnimation, navigate, from]);
|
||||
}, [isLoggedIn, navigate, from]);
|
||||
|
||||
const loginFnc = async (username: string, password: string) => {
|
||||
const response = await fetch(`${API_BASE}/api/users/login`, {
|
||||
@@ -63,8 +42,6 @@ export const LoginPage = () => {
|
||||
};
|
||||
}
|
||||
|
||||
setShowAnimation(true);
|
||||
|
||||
Cookies.set("token", data.token);
|
||||
setIsLoggedIn(true);
|
||||
return { success: true };
|
||||
@@ -85,22 +62,14 @@ export const LoginPage = () => {
|
||||
return;
|
||||
}
|
||||
setTriggerLogout(false);
|
||||
navigate(from, { replace: true });
|
||||
};
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to={from} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAnimation && (
|
||||
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||
<div>{unlockAnimation()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLogout && (
|
||||
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
|
||||
<div>{logoutAnimation()}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Card.Root maxW="sm">
|
||||
@@ -128,17 +97,9 @@ export const LoginPage = () => {
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
{isError && (
|
||||
<MyAlert
|
||||
status="error"
|
||||
title={errorMsg}
|
||||
description={errorDsc}
|
||||
/>
|
||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => handleLogin()}
|
||||
variant="solid"
|
||||
>
|
||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
||||
Login
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
@@ -154,6 +115,5 @@ export const LoginPage = () => {
|
||||
</Card.Root>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Copyright (c) 2026 Theis Gaedigk
|
||||
|
||||
All rights reserved.
|
||||
|
||||
This source code is not to be copied, modified, or distributed in any form
|
||||
without explicit written permission from the author.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "v1.3.2 (dev)",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"backend-info": {
|
||||
"version": "v2.1.1 (dev)"
|
||||
"version": "v2.1.1 (demo)"
|
||||
},
|
||||
"frontend-info": {
|
||||
"version": "v2.1.2 (dev)"
|
||||
"version": "v2.1.2 (demo)"
|
||||
},
|
||||
"admin-panel-info": {
|
||||
"version": "v1.3.2 (dev)"
|
||||
"version": "v1.3.2 (demo)"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backendv2",
|
||||
"version": "v2.1.1 (dev)",
|
||||
"version": "1.0.0",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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');
|
||||