feat: implement user management features including user deletion and role-based access
This commit is contained in:
@@ -23,7 +23,7 @@ app.use(cookieParser());
|
|||||||
app.post("/api/login", async (req, res) => {
|
app.post("/api/login", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await loginUser(req.body.username, req.body.password);
|
const result = await loginUser(req.body.username, req.body.password);
|
||||||
if (result.success && result.role === "admin") {
|
if (result.success && result.user.role === "admin") {
|
||||||
const userToken = await generateToken({
|
const userToken = await generateToken({
|
||||||
role: result.user.role,
|
role: result.user.role,
|
||||||
username: result.user.username,
|
username: result.user.username,
|
||||||
@@ -35,8 +35,10 @@ app.post("/api/login", async (req, res) => {
|
|||||||
token: userToken,
|
token: userToken,
|
||||||
...result,
|
...result,
|
||||||
});
|
});
|
||||||
} else if (result.success && result.role === "user") {
|
} else if (result.success && result.user.role === "user") {
|
||||||
|
// PROBLEM BELOW DOESNT WORK
|
||||||
|
// FIX LATER
|
||||||
|
res.redirect("http://localhost:5003");
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json(result, { message: "Invalid credentials" });
|
res.status(401).json(result, { message: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,28 @@ app.get("/api/getAllUsers", authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/api/deleteUser", authenticate, async (req, res) => {
|
||||||
|
if (req.user.role === "admin") {
|
||||||
|
deleteUser(req.body.id)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
res.status(200).json(result);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to delete user");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error deleting user:", err);
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ success: false, message: "Internal server error" });
|
||||||
|
});
|
||||||
|
console.log("User deleted successfully");
|
||||||
|
} else {
|
||||||
|
console.log("Access denied for user role");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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}`);
|
||||||
});
|
});
|
||||||
|
@@ -22,7 +22,7 @@ export async function loginUser(username, password) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// If a user is found, return success and user data
|
// If a user is found, return success and user data
|
||||||
if (result.length > 0) {
|
if (result.length > 0 && result[0].role === "admin") {
|
||||||
console.log("User found: ", result[0].username, " ", result[0].id);
|
console.log("User found: ", result[0].username, " ", result[0].id);
|
||||||
return { success: true, user: result[0] };
|
return { success: true, user: result[0] };
|
||||||
} else {
|
} else {
|
||||||
@@ -95,18 +95,12 @@ export async function updateUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to delete a user from the database
|
// Function to delete a user from the database
|
||||||
export async function deleteUser(
|
export async function deleteUser(id) {
|
||||||
username,
|
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
password,
|
|
||||||
email
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
// Delete user based on username and password
|
// Delete user based on username and password
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
"DELETE FROM users WHERE username = ? AND password = ?",
|
"DELETE FROM users WHERE id = ?",
|
||||||
[username, password]
|
[id]
|
||||||
);
|
);
|
||||||
const resultOfquery = result.affectedRows;
|
const resultOfquery = result.affectedRows;
|
||||||
|
|
||||||
|
44
frontend_admin/package-lock.json
generated
44
frontend_admin/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.3.5"
|
"tw-animate-css": "^1.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1843,6 +1845,39 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -4167,6 +4202,15 @@
|
|||||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss-animate": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tw-animate-css": "^1.3.5"
|
"tw-animate-css": "^1.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -1,56 +1,14 @@
|
|||||||
import "./App.css";
|
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";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const users = useUsers();
|
const users = useUsers();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<table className="min-w-full divide-y divide-gray-200 shadow rounded-lg overflow-hidden">
|
<UserTable users={users} />
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
#
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Username
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
First name
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Last name
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Email
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Password
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Created
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
||||||
Role
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
|
||||||
{users.map((user: any, idx: number) => (
|
|
||||||
<tr key={user.id} className="hover:bg-blue-50 transition">
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{idx + 1}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.username}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.first_name}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.last_name}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.email}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.password}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">{user.role}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
128
frontend_admin/src/components/UserTable.tsx
Normal file
128
frontend_admin/src/components/UserTable.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { MoreVertical } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { deleteUser } from "../utils/functions.ts";
|
||||||
|
|
||||||
|
const selectedUsers: Record<number, boolean> = {};
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
created: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserTableProps {
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserTable: React.FC<UserTableProps> = ({ users }) => {
|
||||||
|
const [openMenu, setOpenMenu] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleMenuClick = (userId: number) => {
|
||||||
|
setOpenMenu(openMenu === userId ? null : userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => setOpenMenu(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 shadow rounded-lg overflow-hidden">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<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>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
First name
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Last name
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Email
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Password
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Created
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{users.map((user, idx) => (
|
||||||
|
<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">
|
||||||
|
<input type="text" className="w-25" value={user.username} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<input type="text" className="w-20" value={user.first_name} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<input type="text" className="w-20" value={user.last_name} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<input type="text" className="w-50" value={user.email} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<input type="text" className="w-25" value={user.password} />
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
|
<input type="text" className="w-15" value={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"
|
||||||
|
aria-label="Open actions menu"
|
||||||
|
>
|
||||||
|
<MoreVertical size={18} />
|
||||||
|
</button>
|
||||||
|
{openMenu === user.id && (
|
||||||
|
<div
|
||||||
|
className="absolute right-0 mt-2 w-32 bg-white border rounded shadow-lg z-10"
|
||||||
|
onMouseLeave={handleMenuClose}
|
||||||
|
>
|
||||||
|
<button className="block w-full text-left px-4 py-2 hover:bg-gray-100">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button onClick={() => deleteUser(user.id)} className="block w-full text-left px-4 py-2 hover:bg-gray-100">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserTable;
|
@@ -10,3 +10,38 @@ export const logout = () => {
|
|||||||
localStorage.removeItem("users");
|
localStorage.removeItem("users");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteUser = (id: number) => {
|
||||||
|
fetch("http://localhost:5002/api/deleteUser", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ id: id }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
localStorage.removeItem("users");
|
||||||
|
document.location.reload();
|
||||||
|
} else {
|
||||||
|
alert("Failed to delete user");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Error deleting user: ", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchUsers = async () => {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@@ -8,6 +8,7 @@ export interface User {
|
|||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
created: string;
|
created: string;
|
||||||
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsers(): User[] {
|
export function useUsers(): User[] {
|
||||||
|
@@ -1,35 +1,11 @@
|
|||||||
import { useState } from 'react'
|
import "./App.css";
|
||||||
import reactLogo from './assets/react.svg'
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<p>Hello world</p>
|
||||||
<a href="https://vite.dev" target="_blank">
|
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://react.dev" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
Reference in New Issue
Block a user