implement admin panel with login functionality and dashboard layout

This commit is contained in:
2025-08-31 18:07:49 +02:00
parent 8fb70ccccd
commit 217803ba8f
16 changed files with 409 additions and 2 deletions

View File

@@ -1,9 +1,12 @@
import "./App.css";
import Layout from "./Layout/Layout";
function App() {
return (
<>
<p>Admin panel</p>
<Layout>
<p></p>
</Layout>
</>
);
}

View File

@@ -0,0 +1,60 @@
import React from "react";
import { useState } from "react";
import { Box, Heading, Text, Flex, Button } from "@chakra-ui/react";
import Sidebar from "./Sidebar";
import UserTable from "../components/UserTable";
import ItemTable from "../components/ItemTable";
import LockerTable from "../components/LockerTable";
import LoanTable from "../components/LoanTable";
type DashboardProps = {
onLogout?: () => void;
};
const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
const userName = localStorage.getItem("userName") || "Admin";
const [activeView, setActiveView] = useState("");
return (
<Flex h="100vh">
<Sidebar
viewAusleihen={() => setActiveView("Ausleihen")}
viewGegenstaende={() => setActiveView("Gegenstände")}
viewSchliessfaecher={() => setActiveView("Schließfächer")}
viewUser={() => setActiveView("User")}
/>
<Box flex="1" display="flex" flexDirection="column">
<Flex
as="header"
align="center"
justify="space-between"
px={6}
py={4}
borderBottom="1px"
borderColor="gray.200"
bg="gray.900"
>
<Heading size="md">Dashboard</Heading>
<Flex align="center" gap={6}>
<Text fontSize="sm" color="white">
Willkommen {userName}, im Admin-Dashboard!
</Text>
<Button variant="solid" onClick={onLogout}>
Logout
</Button>
</Flex>
</Flex>
<Box as="main" flex="1" p={6}>
{activeView === "" && <Text>Bitte wählen Sie eine Ansicht aus.</Text>}
{activeView === "User" && <UserTable />}
{activeView === "Ausleihen" && <LoanTable />}
{activeView === "Gegenstände" && <ItemTable />}
{activeView === "Schließfächer" && <LockerTable />}
</Box>
</Box>
</Flex>
);
};
export default Dashboard;

View File

@@ -0,0 +1,39 @@
import React, { useState } from "react";
import { useEffect } from "react";
import Dashboard from "./Dashboard";
import Login from "./Login";
import Cookies from "js-cookie";
type LayoutProps = {
children: React.ReactNode;
};
const Layout: React.FC<LayoutProps> = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
if (Cookies.get("token")) {
setIsLoggedIn(true);
}
}, []);
const handleLogout = () => {
Cookies.remove("token");
setIsLoggedIn(false);
};
return (
<>
<main>
{isLoggedIn ? (
<Dashboard onLogout={() => handleLogout()} />
) : (
<Login onSuccess={() => setIsLoggedIn(true)} />
)}
</main>
{children}
</>
);
};
export default Layout;

View File

@@ -0,0 +1,62 @@
import React from "react";
import { useState } from "react";
import { loginFunc } from "@/utils/loginUser";
import MyAlert from "@/components/myChakra/MyAlert";
import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
const Login: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isError, setIsError] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [errorDsc, setErrorDsc] = useState("");
const handleLogin = async () => {
const result = await loginFunc(username, password);
if (!result.success) {
setErrorMsg(result.message);
setErrorDsc(result.description);
setIsError(true);
return;
}
onSuccess();
};
return (
<Card.Root maxW="sm">
<Card.Header>
<Card.Title>Login</Card.Title>
<Card.Description>
Bitte unten Ihre Admin Zugangsdaten eingeben.
</Card.Description>
</Card.Header>
<Card.Body>
<Stack gap="4" w="full">
<Field.Root>
<Field.Label>username</Field.Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</Field.Root>
<Field.Root>
<Field.Label>password</Field.Label>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Field.Root>
</Stack>
</Card.Body>
<Card.Footer justifyContent="flex-end">
{isError && <MyAlert title={errorMsg} description={errorDsc} />}
<Button onClick={() => handleLogin()} variant="solid">
Login
</Button>
</Card.Footer>
</Card.Root>
);
};
export default Login;

View File

@@ -0,0 +1,81 @@
import React from "react";
import { Box, Flex, VStack, Heading, Text, Link } from "@chakra-ui/react";
type SidebarProps = {
viewAusleihen: () => void;
viewGegenstaende: () => void;
viewSchliessfaecher: () => void;
viewUser: () => void;
};
const Sidebar: React.FC<SidebarProps> = ({
viewAusleihen,
viewGegenstaende,
viewSchliessfaecher,
viewUser,
}) => {
return (
<Box
as="aside"
w="260px"
minH="100vh"
bg="gray.800"
color="gray.100"
px={6}
py={8}
borderRight="1px solid"
borderColor="gray.700"
>
<Flex direction="column" h="full">
<Heading size="md" mb={8} letterSpacing="wide">
Borrow System
</Heading>
<VStack align="stretch" gap={4} fontSize="sm">
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewUser}
>
User
</Link>
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewAusleihen}
>
Ausleihen
</Link>
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewGegenstaende}
>
Gegenstände
</Link>
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewSchliessfaecher}
>
Schließfächer
</Link>
</VStack>
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
<Text>&copy; Made with by Theis Gaedigk</Text>
</Box>
</Flex>
</Box>
);
};
export default Sidebar;

View File

@@ -0,0 +1,7 @@
import React from "react";
const ItemTable: React.FC = () => {
return <>Item Table</>;
};
export default ItemTable;

View File

@@ -0,0 +1,7 @@
import React from "react";
const LoanTable: React.FC = () => {
return <>Loan Table</>;
};
export default LoanTable;

View File

@@ -0,0 +1,7 @@
import React from "react";
const LockerTable: React.FC = () => {
return <>Locker Table</>;
};
export default LockerTable;

View File

@@ -0,0 +1,7 @@
import React from "react";
const UserTable: React.FC = () => {
return <>User Table</>;
};
export default UserTable;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { Alert } from "@chakra-ui/react";
type MyAlertProps = {
title: string;
description: string;
};
const MyAlert: React.FC<MyAlertProps> = ({ title, description }) => {
return (
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Title>{title}</Alert.Title>
<Alert.Description>{description}</Alert.Description>
</Alert.Content>
</Alert.Root>
);
};
export default MyAlert;

View File

@@ -0,0 +1,43 @@
import Cookies from "js-cookie";
export type LoginSuccess = { success: true };
export type LoginFailure = {
success: false;
message: string;
description: string;
};
export type LoginResult = LoginSuccess | LoginFailure;
export const loginFunc = async (
username: string,
password: string
): Promise<LoginResult> => {
try {
const response = await fetch("http://localhost:8002/api/loginAdmin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
return {
success: false,
message: "Login failed!",
description: "Invalid username or password.",
};
}
// Successful login
const data = await response.json();
Cookies.set("token", data.token);
localStorage.setItem("userName", data.first_name);
return { success: true };
} catch (error) {
console.error("Error logging in:", error);
return {
success: false,
message: "Login failed!",
description: "Server error.",
};
}
};

View File

@@ -0,0 +1,18 @@
import { toast, Flip, type ToastOptions } from "react-toastify";
export type ToastType = "success" | "error" | "info" | "warning";
export const myToast = (message: string, msgType: ToastType) => {
let config: ToastOptions = {
position: "top-right",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "colored",
transition: Flip,
};
toast[msgType](message, config);
};

View File

@@ -27,7 +27,9 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}

View File

@@ -8,6 +8,7 @@ import {
getBorrowableItemsFromDatabase,
createLoanInDatabase,
onTake,
loginAdmin,
onReturn,
} from "../services/database.js";
import { authenticate, generateToken } from "../services/tokenService.js";
@@ -166,4 +167,33 @@ router.post("/createLoan", authenticate, async (req, res) => {
}
});
// Admin panel functions
router.post("/loginAdmin", async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res
.status(400)
.json({ message: "Username and password are required" });
}
const result = await loginAdmin(username, password);
if (result.success) {
const token = await generateToken({
username: result.data.username,
role: result.data.role,
});
return res.status(200).json({
message: "Login successful",
first_name: result.data.first_name,
token,
});
}
return res.status(401).json({ message: "Invalid credentials" });
});
export default router;

View File

@@ -12,6 +12,17 @@ CREATE TABLE `users` (
UNIQUE KEY `username` (`username`)
);
CREATE TABLE `admins` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL,
`first_name` varchar(255) NOT NULL,
`last_name` varchar(255) NOT NULL,
`entry_created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
);
CREATE TABLE `loans` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL,

View File

@@ -319,3 +319,12 @@ export const onReturn = async (loanId) => {
}
return { success: false };
};
export const loginAdmin = async (username, password) => {
const [result] = await pool.query(
"SELECT * FROM admins WHERE username = ? AND password = ?",
[username, password]
);
if (result.length > 0) return { success: true, data: result[0] };
return { success: false };
};