feat: add user update functionality and theme management
This commit is contained in:
@@ -52,7 +52,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);
|
res.status(200).json(users).reload();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Error fetching users:", err);
|
console.error("Error fetching users:", err);
|
||||||
@@ -92,6 +92,33 @@ app.post("/api/deleteUser", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/api/updateUser", authenticate, async (req, res) => {
|
||||||
|
if (req.user.role === "admin") {
|
||||||
|
updateUser(
|
||||||
|
req.body.username,
|
||||||
|
req.body.first_name,
|
||||||
|
req.body.last_name,
|
||||||
|
req.body.password,
|
||||||
|
req.body.email,
|
||||||
|
req.body.id
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json(result);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to update user");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error updating user:", err);
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ success: false, message: "Internal server error" });
|
||||||
|
});
|
||||||
|
console.log("User updated successfully");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Express backend server is running at http://localhost:${port}`);
|
console.log(`Express backend server is running at http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
@@ -62,13 +62,14 @@ export async function updateUser(
|
|||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
password,
|
password,
|
||||||
email
|
email,
|
||||||
|
id
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Update user details based on username
|
// Update user details based on id
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"UPDATE users SET first_name = ?, last_name = ?, password = ?, email = ? WHERE username = ?",
|
"UPDATE users SET first_name = ?, last_name = ?, password = ?, email = ?, username = ? WHERE id = ?",
|
||||||
[first_name, last_name, password, email, username]
|
[first_name, last_name, password, email, username, id]
|
||||||
);
|
);
|
||||||
const resultOfquery = result.affectedRows;
|
const resultOfquery = result.affectedRows;
|
||||||
|
|
||||||
|
@@ -1 +1,17 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Example: App.css */
|
||||||
|
body.dark {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark body {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
@@ -2,10 +2,17 @@ import "./App.css";
|
|||||||
import Layout from "./layout/Layout";
|
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 { loadTheme } from "./utils/functions";
|
||||||
|
import { replaceUsers } from "./utils/functions";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const users = useUsers();
|
const users = useUsers();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTheme();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<UserTable users={users} />
|
<UserTable users={users} />
|
||||||
|
@@ -2,6 +2,7 @@ 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/functions";
|
||||||
|
import { changeTheme } from "../utils/functions";
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const [loginCardVisible, setLoginCardVisible] = useState(false);
|
const [loginCardVisible, setLoginCardVisible] = useState(false);
|
||||||
@@ -22,6 +23,9 @@ const Header: React.FC = () => {
|
|||||||
<ul className="flex space-x-4">
|
<ul className="flex space-x-4">
|
||||||
<li>
|
<li>
|
||||||
<a className="hover:underline">
|
<a className="hover:underline">
|
||||||
|
<button onClick={() => changeTheme()} className="bg-blue-700 shadow-md hover:bg-blue-800 transition padding px-4 py-2 rounded-md text-white font-semibold">
|
||||||
|
Change Theme
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLoginCardVisible(true)}
|
onClick={() => setLoginCardVisible(true)}
|
||||||
className="bg-blue-700 shadow-md hover:bg-blue-800 transition padding px-4 py-2 rounded-md text-white font-semibold"
|
className="bg-blue-700 shadow-md hover:bg-blue-800 transition padding px-4 py-2 rounded-md text-white font-semibold"
|
||||||
|
@@ -68,6 +68,7 @@ const LoginCard: React.FC<LoginCardProps> = ({ onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
|
required
|
||||||
id="username"
|
id="username"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +83,7 @@ const LoginCard: React.FC<LoginCardProps> = ({ onClose }) => {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
required
|
||||||
id="password"
|
id="password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
@@ -11,7 +11,7 @@ const Sidebar: React.FC = () => (
|
|||||||
className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center shadow-lg"
|
className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center shadow-lg"
|
||||||
/>
|
/>
|
||||||
<span className="font-extrabold text-2xl text-blue-700 tracking-wide drop-shadow">
|
<span className="font-extrabold text-2xl text-blue-700 tracking-wide drop-shadow">
|
||||||
Bikelanes
|
Web-Panel
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { deleteUser } from "../utils/functions.ts";
|
import { deleteUser, updateUserFunc } from "../utils/functions.ts";
|
||||||
|
|
||||||
const selectedUsers: Record<number, boolean> = {};
|
const selectedUsers: Record<number, boolean> = {};
|
||||||
|
|
||||||
@@ -22,6 +22,11 @@ interface UserTableProps {
|
|||||||
|
|
||||||
const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
||||||
const [openMenu, setOpenMenu] = useState<number | null>(null);
|
const [openMenu, setOpenMenu] = useState<number | null>(null);
|
||||||
|
const [userList, setUserList] = useState<User[]>(users);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUserList(users);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
const handleMenuClick = (userId: number) => {
|
const handleMenuClick = (userId: number) => {
|
||||||
setOpenMenu(openMenu === userId ? null : userId);
|
setOpenMenu(openMenu === userId ? null : userId);
|
||||||
@@ -29,13 +34,16 @@ const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
|||||||
|
|
||||||
const handleMenuClose = () => setOpenMenu(null);
|
const handleMenuClose = () => setOpenMenu(null);
|
||||||
|
|
||||||
|
const handleInputChange = (id: number, field: keyof User, value: string) => {
|
||||||
|
setUserList((prev) =>
|
||||||
|
prev.map((user) => (user.id === id ? { ...user, [field]: value } : user))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="min-w-full divide-y divide-gray-200 shadow rounded-lg overflow-hidden">
|
<table className="min-w-full divide-y divide-gray-200 shadow rounded-lg overflow-hidden">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
<input type="checkbox" name="checkAll" id="checkAll" />
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
#
|
#
|
||||||
</th>
|
</th>
|
||||||
@@ -66,35 +74,74 @@ const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
{users.map((user, idx) => (
|
{userList.map((user, idx) => (
|
||||||
<tr key={user.id} className="hover:bg-blue-50 transition">
|
<tr key={user.id} className="hover:bg-blue-50 transition">
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="checkUser"
|
|
||||||
id={`checkUser-${user.id}`}
|
|
||||||
checked={!!selectedUsers[user.id]}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.id}</td>
|
<td className="px-4 py-2 whitespace-nowrap">{user.id}</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-25" value={user.username} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-20"
|
||||||
|
id={`username-${user.id}`}
|
||||||
|
value={user.username}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "username", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-20" value={user.first_name} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-20"
|
||||||
|
id={`first_name-${user.id}`}
|
||||||
|
value={user.first_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "first_name", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-20" value={user.last_name} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-20"
|
||||||
|
id={`last_name-${user.id}`}
|
||||||
|
value={user.last_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "last_name", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-50" value={user.email} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-60"
|
||||||
|
id={`email-${user.id}`}
|
||||||
|
value={user.email}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "email", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-25" value={user.password} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-25"
|
||||||
|
id={`password-${user.id}`}
|
||||||
|
value={user.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "password", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
|
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<input type="text" className="w-15" value={user.role} />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-15"
|
||||||
|
value={user.role}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(user.id, "role", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap relative">
|
<td className="px-4 py-2 whitespace-nowrap relative">
|
||||||
<button
|
<button
|
||||||
@@ -109,10 +156,16 @@ const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
|||||||
className="absolute right-0 mt-2 w-32 bg-white border rounded shadow-lg z-10"
|
className="absolute right-0 mt-2 w-32 bg-white border rounded shadow-lg z-10"
|
||||||
onMouseLeave={handleMenuClose}
|
onMouseLeave={handleMenuClose}
|
||||||
>
|
>
|
||||||
<button className="block w-full text-left px-4 py-2 hover:bg-gray-100">
|
<button
|
||||||
Edit
|
onClick={() => updateUserFunc(user.id)}
|
||||||
|
className="block w-full text-left px-4 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => deleteUser(user.id)} className="block w-full text-left px-4 py-2 hover:bg-gray-100">
|
<button
|
||||||
|
onClick={() => deleteUser(user.id)}
|
||||||
|
className="block w-full text-left px-4 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -4,6 +4,43 @@ export const greeting = () => {
|
|||||||
return Cookies.get("name") ?? "Login";
|
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")
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
document.body.classList.remove("dark");
|
||||||
|
Cookies.set("theme", "light", { expires: 365 });
|
||||||
|
} else if (Cookies.get("theme") === "light") {
|
||||||
|
// Switch to dark theme
|
||||||
|
console.log("dark")
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
document.body.classList.add("dark");
|
||||||
|
Cookies.set("theme", "dark", { expires: 365 });
|
||||||
|
} else {
|
||||||
|
console.error("Theme not set or recognized");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
Cookies.remove("name");
|
Cookies.remove("name");
|
||||||
Cookies.remove("token");
|
Cookies.remove("token");
|
||||||
@@ -22,8 +59,7 @@ export const deleteUser = (id: number) => {
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
localStorage.removeItem("users");
|
replaceUsers();
|
||||||
document.location.reload();
|
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to delete user");
|
alert("Failed to delete user");
|
||||||
}
|
}
|
||||||
@@ -33,8 +69,9 @@ export const deleteUser = (id: number) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUsers = async () => {
|
export const replaceUsers = async () => {
|
||||||
fetch("http://localhost:5002/api/getAllUsers", {
|
localStorage.removeItem("users");
|
||||||
|
await fetch("http://localhost:5002/api/getAllUsers", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
@@ -43,5 +80,70 @@ export const fetchUsers = async () => {
|
|||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
localStorage.setItem("users", JSON.stringify(users));
|
localStorage.setItem("users", JSON.stringify(users));
|
||||||
|
window.location.reload();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateUserFunc = async (userID: number) => {
|
||||||
|
console.log("UpdateFunc" + userID);
|
||||||
|
|
||||||
|
// Validate that required DOM elements exist
|
||||||
|
const usernameEl = document.getElementById(
|
||||||
|
`username-${userID}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const firstNameEl = document.getElementById(
|
||||||
|
`first_name-${userID}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const lastNameEl = document.getElementById(
|
||||||
|
`last_name-${userID}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const emailEl = document.getElementById(
|
||||||
|
`email-${userID}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const passwordEl = document.getElementById(
|
||||||
|
`password-${userID}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
if (!usernameEl || !firstNameEl || !lastNameEl || !emailEl) {
|
||||||
|
console.error("Required form elements not found");
|
||||||
|
alert("Form elements not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: userID,
|
||||||
|
username: usernameEl.value,
|
||||||
|
first_name: firstNameEl.value,
|
||||||
|
last_name: lastNameEl.value,
|
||||||
|
email: emailEl.value,
|
||||||
|
password: passwordEl?.value || "", // password might be optional
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Sending user data:", userData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://localhost:5002/api/updateUser", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(userData),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Response status:", response.status);
|
||||||
|
console.log("Response ok:", response.ok);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log("User updated successfully");
|
||||||
|
replaceUsers();
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error("Server error:", response.status, errorText);
|
||||||
|
alert(`Failed to update user: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Network error updating user:", error);
|
||||||
|
alert("Network error occurred while updating user");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user