Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

volunteer list in progress #42

Merged
merged 6 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/empty_list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions src/app/api/user/route.client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User, VolunteerDetails } from "@prisma/client";
import { Role, User, VolunteerDetails } from "@prisma/client";

type CreateUserInput = Omit<User, "id" | "events" | "eventIds">;
type CreateVolunteerDetailsInput = Omit<
Expand Down Expand Up @@ -47,8 +47,13 @@ export const getUser = async (userID: string) => {
export const getUserByEmail = async (email: string) => {
const url = `/api/user?email=${email}`;
return fetchApi(url, "GET");
};

export const getUsersByRole = async (role: Role) => {
const url = `/api/user?role=${role}`;
return fetchApi(url, "GET");
};

}
export const deleteUser = async (userID: string) => {
const url = `/api/user?id=${userID}`;
return fetchApi(url, "DELETE");
Expand Down
47 changes: 45 additions & 2 deletions src/app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export const POST = async (request: NextRequest) => {
},
});

await prisma.code.create({
data: {
codeString: "",
expire: new Date(),
userId: savedUser.id,
},
});

return NextResponse.json({
code: "SUCCESS",
message: `User created with email: ${savedUser.email}`,
Expand Down Expand Up @@ -68,6 +76,10 @@ export const DELETE = async (request: NextRequest) => {
}

try {
await prisma.code.delete({
where: { userId: id },
});

await prisma.volunteerDetails.delete({
where: { userId: id },
});
Expand Down Expand Up @@ -99,9 +111,9 @@ export const GET = async (request: NextRequest) => {
const { searchParams } = new URL(request.url);
const id: string | undefined = searchParams.get("id") || undefined;
const email: string | undefined = searchParams.get("email") || undefined;
const role: string | undefined = searchParams.get("role") || undefined;

// Check if id and email is null
if (!id && !email) {
if (!id && !email && !role) {
return NextResponse.json(
{
code: "BAD_REQUEST",
Expand All @@ -110,6 +122,37 @@ export const GET = async (request: NextRequest) => {
{ status: 400 }
);
}

if (role) {
try {
const users = await prisma.user.findMany({
where: { role: role === "ADMIN" ? "ADMIN" : "VOLUNTEER" },
include: { volunteerDetails: true },
});

if (!users || users.length === 0) {
return NextResponse.json(
{
code: "NOT_FOUND",
message: "No users found",
},
{ status: 404 }
);
}

return NextResponse.json({
code: "SUCCESS",
data: users,
});
} catch (error) {
console.error("Error:", error);
return NextResponse.json({
code: "ERROR",
message: error,
});
}
}

try {
const fetchedUser = await prisma.user.findUnique({
where: id ? { id } : { email },
Expand Down
176 changes: 174 additions & 2 deletions src/app/private/volunteers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,181 @@
"use client";

import VolunteerTable from "@components/VolunteerTable/VolunteerTable";
import SearchBar from "@components/SearchBar";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Button } from "@mui/material";
import React from "react";
import { Role, User } from "@prisma/client";
import { deleteUser, getUsersByRole } from "@api/user/route.client";
import Image from "next/image";

export default function VolunteersPage() {
const [users, setUsers] = React.useState<User[]>();
const [selected, setSelected] = React.useState<string[]>([]);
const [searchText, setSearchText] = React.useState("");
const [isModalOpen, setIsModalOpen] = React.useState(false);

React.useEffect(() => {
const fetchUsers = async () => {
try {
const response = await getUsersByRole(Role.VOLUNTEER);
setUsers(response.data);
} catch (error) {
console.error("Error fetching volunteers:", error);
}
};

fetchUsers();
}, []);

// Filter users based on the search text
const filteredUsers = users?.filter(
(user) =>
user.firstName.toLowerCase().includes(searchText.toLowerCase()) ||
user.lastName.toLowerCase().includes(searchText.toLowerCase()) ||
user.email.toLowerCase().includes(searchText.toLowerCase())
);

const deleteUsers = async () => {
try {
const deletePromises = selected.map((id) => deleteUser(id));
const responses = await Promise.all(deletePromises);
const allDeleted = responses.every(
(response) => response.code === "SUCCESS"
);

if (allDeleted) {
setUsers((prevUsers) =>
prevUsers
? prevUsers.filter((user) => !selected.includes(user.id))
: []
);
setSelected([]);
console.log("All users deleted successfully", responses);
} else {
console.error("Not all deletions succeeded");
}
setIsModalOpen(false);
} catch (error) {
console.error("Error deleting users:", error);
}
};

return (
<div>
<h1>Volunteers Page</h1>
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Icon icon="mdi:people" width="44" height="44" />
<div className="text-4xl font-['Kepler_Std'] font-semibold">
Volunteer List ({users ? users.length : 0})
</div>
</div>
{selected.length > 0 ? (
<div className="flex items-center gap-4">
<div>{selected.length} Selected</div>
<Button
sx={{
display: "flex",
padding: "10px 18px",
alignItems: "center",
gap: "8px",
borderRadius: "8px",
backgroundColor: "var(--Rose-600, #E61932)",
color: "white",
fontWeight: 600,
textTransform: "none",
"&:hover": {
backgroundColor: "var(--Rose-700, #C11429)",
},
}}
onClick={() => setIsModalOpen(true)}
>
<DeleteOutlineIcon sx={{ width: 20, color: "whitesmoke" }} />
<div>Delete</div>
</Button>
</div>
) : (
<div className="h-[44.5px]"></div>
)}
</div>
<SearchBar
onSearchChange={(value) => {
setSearchText(value);
setSelected([]);
}}
/>
{filteredUsers && filteredUsers.length > 0 ? (
<VolunteerTable
showPagination={true}
fromVolunteerPage
users={filteredUsers}
selected={selected}
setSelected={setSelected}
/>
) : (
<div className="text-center">
<div className="relative w-full h-[50vh]">
<Image
src="/empty_list.png"
alt="Empty List"
layout="fill"
objectFit="contain"
/>
</div>
<div className="text-[#344054] font-['Kepler_Std'] text-3xl font-semibold mt-8">
No volunteers found!
</div>
</div>
)}
{isModalOpen && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="fixed inset-0 bg-[#101828] opacity-40"></div>
<div className="bg-white p-6 rounded-2xl shadow-lg z-10 max-w-[512px]">
<div className="text-[#101828] text-center font-['Kepler_Std'] text-4xl font-semibold">
Are you sure you want to delete {selected.length}{" "}
{selected.length === 1 ? "user" : "users"}?
</div>
<div className="text-[#667085] text-center text-lg mt-2">
You will not be able to recover {selected.length === 1 ? "a" : ""}{" "}
deleted {selected.length === 1 ? "profile" : "profiles"}.
</div>
<div className="flex justify-end gap-5 mt-8">
<Button
variant="outlined"
sx={{
borderRadius: "8px",
border: "1px solid var(--Grey-300, #D0D5DD)",
padding: "10px 18px",
color: "var(--Teal-800, #145A5A)",
fontWeight: 600,
textTransform: "none",
fontSize: 16,
}}
onClick={() => setIsModalOpen(false)}
>
Cancel
</Button>
<Button
variant="outlined"
sx={{
borderRadius: "8px",
padding: "10px 18px",
backgroundColor: "var(--Teal-600, #138D8A)",
color: "white",
fontWeight: 600,
textTransform: "none",
fontSize: 16,
"&:hover": { backgroundColor: "var(--Teal-700, #1D7A7A)" },
}}
onClick={deleteUsers}
>
Delete
</Button>
</div>
</div>
</div>
)}
</div>
);
}
46 changes: 46 additions & 0 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import { Box, InputBase } from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";

interface SearchBarProps {
onSearchChange: (searchText: string) => void;
}

const SearchBar = ({ onSearchChange }: SearchBarProps) => {
return (
<Box
sx={{
display: "flex",
padding: "5px 7px",
alignItems: "center",
gap: "8px",
borderRadius: "8px",
border: "1px solid var(--Grey-300, #D0D5DD)",
background: "var(--White, #FFF)",
boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
width: "100%",
maxWidth: 400,
}}
>
<SearchIcon sx={{ color: "var(--Grey-500, #667085)" }} />
<InputBase
placeholder="Search"
onChange={(e) => onSearchChange(e.target.value)}
sx={{
width: "100%",
fontSize: "14px",
color: "var(--Grey-700, #344054)",
"& input::placeholder": {
color: "var(--Grey-500, #667085)",
fontFamily: "Inter, sans-serif",
fontSize: "16px",
fontWeight: 400,
lineHeight: "24px",
},
}}
/>
</Box>
);
};

export default SearchBar;
Loading
Loading