feat: refactor user handling and integrate toast notifications for login feedback
This commit is contained in:
@@ -50,7 +50,7 @@ app.get("/api/getAllUsers", authenticate, async (req, res) => {
|
||||
if (req.user.role === "admin") {
|
||||
getAllUsers()
|
||||
.then((users) => {
|
||||
res.status(200).json(users).reload();
|
||||
res.status(200).json(users);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching users:", err);
|
||||
|
14
frontend_admin/package-lock.json
generated
14
frontend_admin/package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.3.5"
|
||||
@@ -4010,6 +4011,19 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
@@ -19,6 +19,7 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.3.5"
|
||||
|
@@ -3,7 +3,9 @@ import Layout from "./layout/Layout";
|
||||
import { useUsers } from "./utils/useUsers";
|
||||
import UserTable from "./components/UserTable";
|
||||
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() {
|
||||
const users = useUsers();
|
||||
@@ -15,6 +17,7 @@ function App() {
|
||||
return (
|
||||
<Layout>
|
||||
<UserTable users={users} />
|
||||
<ToastContainer />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import React from "react";
|
||||
import LoginCard from "./LoginCard";
|
||||
import { greeting } from "../utils/functions";
|
||||
import { changeTheme } from "../utils/functions";
|
||||
import { greeting } from "../utils/frontendService";
|
||||
import { changeTheme } from "../utils/frontendService";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [loginCardVisible, setLoginCardVisible] = useState(false);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import Cookies from "js-cookie";
|
||||
import { logout } from "../utils/functions";
|
||||
import { logout, loginUser } from "../utils/userHandler";
|
||||
type LoginCardProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -25,38 +25,7 @@ const LoginCard: React.FC<LoginCardProps> = ({ onClose }) => {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
// Example: send login request
|
||||
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);
|
||||
});
|
||||
loginUser(username as string, password as string);
|
||||
}}
|
||||
className="space-y-4 text-black dark:text-white"
|
||||
>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { deleteUser, updateUserFunc } from "../utils/functions.ts";
|
||||
import { deleteUser, updateUserFunc } from "../utils/userHandler.ts";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -70,100 +70,105 @@ const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white text-black dark:bg-gray-900 dark:text-white divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{userList.map((user) => (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
>
|
||||
<td className="px-4 py-2 whitespace-nowrap">{user.id}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`username-${user.id}`}
|
||||
value={user.username}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "username", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`first_name-${user.id}`}
|
||||
value={user.first_name}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "first_name", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`last_name-${user.id}`}
|
||||
value={user.last_name}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "last_name", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`email-${user.id}`}
|
||||
value={user.email}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "email", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`password-${user.id}`}
|
||||
value={user.password}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<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">
|
||||
<button
|
||||
onClick={() => handleMenuClick(user.id)}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
aria-label="Open actions menu"
|
||||
>
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
{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}
|
||||
<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, idx) => {
|
||||
// If this is one of the last 2 rows, open menu upwards
|
||||
const openUp = idx >= userList.length - 2;
|
||||
return (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
>
|
||||
<td className="px-4 py-2 whitespace-nowrap">{user.id}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`username-${user.id}`}
|
||||
value={user.username}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "username", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`first_name-${user.id}`}
|
||||
value={user.first_name}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "first_name", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`last_name-${user.id}`}
|
||||
value={user.last_name}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "last_name", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`email-${user.id}`}
|
||||
value={user.email}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "email", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
id={`password-${user.id}`}
|
||||
value={user.password}
|
||||
onChange={(e) =>
|
||||
handleInputChange(user.id, "password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<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">
|
||||
<button
|
||||
onClick={() => handleMenuClick(user.id)}
|
||||
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
aria-label="Open actions menu"
|
||||
>
|
||||
<button
|
||||
onClick={() => updateUserFunc(user.id)}
|
||||
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
{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={() => deleteUser(user.id)}
|
||||
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<button
|
||||
onClick={() => updateUserFunc(user.id)}
|
||||
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteUser(user.id)}
|
||||
className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
52
frontend_admin/src/utils/frontendService.ts
Normal file
52
frontend_admin/src/utils/frontendService.ts
Normal 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 });
|
||||
};
|
@@ -1,54 +1,38 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
|
||||
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 });
|
||||
export const loginUser = (username: string, password: string) => {
|
||||
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 });
|
||||
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();
|
||||
toast("Login successful!");
|
||||
} else if (response.status === 401) {
|
||||
toast("Invalid credentials");
|
||||
} else if (response.status === 403) {
|
||||
toast("You are not an Admin!");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Login failed: ", error);
|
||||
});
|
||||
};
|
||||
|
||||
export const logout = () => {
|
Reference in New Issue
Block a user