feat: refactor user handling and integrate toast notifications for login feedback

This commit is contained in:
2025-07-24 14:12:06 +02:00
parent 06dd1fc80e
commit b69b446e3d
9 changed files with 207 additions and 179 deletions

View File

@@ -50,7 +50,7 @@ app.get("/api/getAllUsers", authenticate, async (req, res) => {
if (req.user.role === "admin") { if (req.user.role === "admin") {
getAllUsers() getAllUsers()
.then((users) => { .then((users) => {
res.status(200).json(users).reload(); res.status(200).json(users);
}) })
.catch((err) => { .catch((err) => {
console.error("Error fetching users:", err); console.error("Error fetching users:", err);

View File

@@ -16,6 +16,7 @@
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.5" "tw-animate-css": "^1.3.5"
@@ -4010,6 +4011,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

View File

@@ -19,6 +19,7 @@
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.5" "tw-animate-css": "^1.3.5"

View File

@@ -3,7 +3,9 @@ import Layout from "./layout/Layout";
import { useUsers } from "./utils/useUsers"; import { useUsers } from "./utils/useUsers";
import UserTable from "./components/UserTable"; import UserTable from "./components/UserTable";
import { useEffect } from "react"; import { useEffect } from "react";
import { loadTheme } from "./utils/functions"; import { loadTheme } from "./utils/frontendService";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
function App() { function App() {
const users = useUsers(); const users = useUsers();
@@ -15,6 +17,7 @@ function App() {
return ( return (
<Layout> <Layout>
<UserTable users={users} /> <UserTable users={users} />
<ToastContainer />
</Layout> </Layout>
); );
} }

View File

@@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import React from "react"; import React from "react";
import LoginCard from "./LoginCard"; import LoginCard from "./LoginCard";
import { greeting } from "../utils/functions"; import { greeting } from "../utils/frontendService";
import { changeTheme } from "../utils/functions"; import { changeTheme } from "../utils/frontendService";
const Header: React.FC = () => { const Header: React.FC = () => {
const [loginCardVisible, setLoginCardVisible] = useState(false); const [loginCardVisible, setLoginCardVisible] = useState(false);

View File

@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { logout } from "../utils/functions"; import { logout, loginUser } from "../utils/userHandler";
type LoginCardProps = { type LoginCardProps = {
onClose: () => void; onClose: () => void;
}; };
@@ -25,38 +25,7 @@ const LoginCard: React.FC<LoginCardProps> = ({ onClose }) => {
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
const username = formData.get("username"); const username = formData.get("username");
const password = formData.get("password"); const password = formData.get("password");
// Example: send login request loginUser(username as string, password as string);
await fetch("http://localhost:5002/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
})
.then(async (response) => {
if (response.ok) {
const data = await response.json();
Cookies.set("token", data.token, { expires: 7 });
onClose();
Cookies.set("name", data.user.first_name, { expires: 7 });
await fetch("http://localhost:5002/api/getAllUsers", {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
})
.then((res) => res.json())
.then((users) => {
localStorage.setItem("users", JSON.stringify(users));
});
document.location.reload();
} else if (response.status === 401) {
alert("Invalid credentials");
} else if (response.status === 403) {
alert("You are not an Admin!");
}
})
.catch((error) => {
console.log("Login failed: ", error);
});
}} }}
className="space-y-4 text-black dark:text-white" className="space-y-4 text-black dark:text-white"
> >

View File

@@ -1,6 +1,6 @@
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { deleteUser, updateUserFunc } from "../utils/functions.ts"; import { deleteUser, updateUserFunc } from "../utils/userHandler.ts";
interface User { interface User {
id: number; id: number;
@@ -70,100 +70,105 @@ const UserTable: React.FC<UserTableProps> = ({ users }) => {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white text-black dark:bg-gray-900 dark:text-white divide-y divide-gray-100 dark:divide-gray-800"> <tbody className="bg-white text-black dark:bg-gray-900 dark:text-white divide-y divide-gray-100 dark:divide-gray-800 z-10">
{userList.map((user) => ( {userList.map((user, idx) => {
<tr // If this is one of the last 2 rows, open menu upwards
key={user.id} const openUp = idx >= userList.length - 2;
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition" return (
> <tr
<td className="px-4 py-2 whitespace-nowrap">{user.id}</td> key={user.id}
<td className="px-4 py-2 whitespace-nowrap"> className="hover:bg-gray-50 dark:hover:bg-gray-800 transition"
<input >
type="text" <td className="px-4 py-2 whitespace-nowrap">{user.id}</td>
className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white" <td className="px-4 py-2 whitespace-nowrap">
id={`username-${user.id}`} <input
value={user.username} type="text"
onChange={(e) => className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white"
handleInputChange(user.id, "username", e.target.value) id={`username-${user.id}`}
} value={user.username}
/> onChange={(e) =>
</td> handleInputChange(user.id, "username", e.target.value)
<td className="px-4 py-2 whitespace-nowrap"> }
<input />
type="text" </td>
className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white" <td className="px-4 py-2 whitespace-nowrap">
id={`first_name-${user.id}`} <input
value={user.first_name} type="text"
onChange={(e) => className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white"
handleInputChange(user.id, "first_name", e.target.value) id={`first_name-${user.id}`}
} value={user.first_name}
/> onChange={(e) =>
</td> handleInputChange(user.id, "first_name", e.target.value)
<td className="px-4 py-2 whitespace-nowrap"> }
<input />
type="text" </td>
className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white" <td className="px-4 py-2 whitespace-nowrap">
id={`last_name-${user.id}`} <input
value={user.last_name} type="text"
onChange={(e) => className="w-20 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white"
handleInputChange(user.id, "last_name", e.target.value) id={`last_name-${user.id}`}
} value={user.last_name}
/> onChange={(e) =>
</td> handleInputChange(user.id, "last_name", e.target.value)
<td className="px-4 py-2 whitespace-nowrap"> }
<input />
type="text" </td>
className="w-60 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white" <td className="px-4 py-2 whitespace-nowrap">
id={`email-${user.id}`} <input
value={user.email} type="text"
onChange={(e) => className="w-60 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white"
handleInputChange(user.id, "email", e.target.value) id={`email-${user.id}`}
} value={user.email}
/> onChange={(e) =>
</td> handleInputChange(user.id, "email", e.target.value)
<td className="px-4 py-2 whitespace-nowrap"> }
<input />
type="text" </td>
className="w-25 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white" <td className="px-4 py-2 whitespace-nowrap">
id={`password-${user.id}`} <input
value={user.password} type="text"
onChange={(e) => className="w-25 bg-transparent border-b border-gray-200 dark:border-gray-600 focus:outline-none focus:border-blue-400 dark:focus:border-blue-300 text-black dark:text-white"
handleInputChange(user.id, "password", e.target.value) id={`password-${user.id}`}
} value={user.password}
/> onChange={(e) =>
</td> handleInputChange(user.id, "password", e.target.value)
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td> }
<td className="px-4 py-2 whitespace-nowrap">{user.role}</td> />
<td className="px-4 py-2 whitespace-nowrap relative"> </td>
<button <td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
onClick={() => handleMenuClick(user.id)} <td className="px-4 py-2 whitespace-nowrap">{user.role}</td>
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700" <td className="px-4 py-2 whitespace-nowrap relative">
aria-label="Open actions menu" <button
> onClick={() => handleMenuClick(user.id)}
<MoreVertical size={18} /> className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
</button> aria-label="Open actions menu"
{openMenu === user.id && (
<div
className="absolute right-0 mt-2 w-32 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded shadow-lg z-10"
onMouseLeave={handleMenuClose}
> >
<button <MoreVertical size={18} />
onClick={() => updateUserFunc(user.id)} </button>
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" {openMenu === user.id && (
<div
className={`absolute right-0 w-32 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded shadow-lg z-50 overflow-visible
${openUp ? "bottom-full mb-2" : "mt-2"}`}
onMouseLeave={handleMenuClose}
> >
Save <button
</button> onClick={() => updateUserFunc(user.id)}
<button className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={() => deleteUser(user.id)} >
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" Save
> </button>
Delete <button
</button> onClick={() => deleteUser(user.id)}
</div> className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
)} >
</td> Delete
</tr> </button>
))} </div>
)}
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
); );

View File

@@ -0,0 +1,52 @@
import Cookies from "js-cookie";
export const greeting = () => {
return Cookies.get("name") ?? "Login";
};
export const loadTheme = () => {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
// Switch to dark theme
console.log("dark");
document.documentElement.classList.add("dark");
document.body.classList.add("dark");
Cookies.set("theme", "dark", { expires: 365 });
} else {
// Switch to light theme
console.log("light");
document.documentElement.classList.remove("dark");
document.body.classList.remove("dark");
Cookies.set("theme", "light", { expires: 365 });
}
};
export const changeTheme = () => {
if (Cookies.get("theme") === "dark") {
// Switch to light theme
console.log("light");
removeDarkTheme();
} else if (Cookies.get("theme") === "light") {
// Switch to dark theme
console.log("dark");
setDarkTheme();
} else {
console.error("Theme not set or recognized");
}
};
export const removeDarkTheme = () => {
console.log("Removing dark theme");
document.documentElement.classList.remove("dark");
document.body.classList.remove("dark");
Cookies.set("theme", "light", { expires: 365 });
};
export const setDarkTheme = () => {
console.log("Setting dark theme");
document.documentElement.classList.add("dark");
document.body.classList.add("dark");
Cookies.set("theme", "dark", { expires: 365 });
};

View File

@@ -1,54 +1,38 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { ToastContainer, toast } from "react-toastify";
export const greeting = () => { export const loginUser = (username: string, password: string) => {
return Cookies.get("name") ?? "Login"; fetch("http://localhost:5002/api/login", {
}; method: "POST",
headers: { "Content-Type": "application/json" },
export const loadTheme = () => { body: JSON.stringify({ username, password }),
if ( })
window.matchMedia && .then(async (response) => {
window.matchMedia("(prefers-color-scheme: dark)").matches if (response.ok) {
) { const data = await response.json();
// Switch to dark theme Cookies.set("token", data.token, { expires: 7 });
console.log("dark"); Cookies.set("name", data.user.first_name, { expires: 7 });
document.documentElement.classList.add("dark"); await fetch("http://localhost:5002/api/getAllUsers", {
document.body.classList.add("dark"); method: "GET",
Cookies.set("theme", "dark", { expires: 365 }); headers: {
} else { Authorization: `Bearer ${Cookies.get("token")}`,
// Switch to light theme },
console.log("light"); })
document.documentElement.classList.remove("dark"); .then((res) => res.json())
document.body.classList.remove("dark"); .then((users) => {
Cookies.set("theme", "light", { expires: 365 }); localStorage.setItem("users", JSON.stringify(users));
} });
}; document.location.reload();
toast("Login successful!");
export const changeTheme = () => { } else if (response.status === 401) {
if (Cookies.get("theme") === "dark") { toast("Invalid credentials");
// Switch to light theme } else if (response.status === 403) {
console.log("light"); toast("You are not an Admin!");
removeDarkTheme(); }
} else if (Cookies.get("theme") === "light") { })
// Switch to dark theme .catch((error) => {
console.log("dark"); console.log("Login failed: ", error);
setDarkTheme(); });
} else {
console.error("Theme not set or recognized");
}
};
export const removeDarkTheme = () => {
console.log("Removing dark theme");
document.documentElement.classList.remove("dark");
document.body.classList.remove("dark");
Cookies.set("theme", "light", { expires: 365 });
};
export const setDarkTheme = () => {
console.log("Setting dark theme");
document.documentElement.classList.add("dark");
document.body.classList.add("dark");
Cookies.set("theme", "dark", { expires: 365 });
}; };
export const logout = () => { export const logout = () => {