Skip to content

Commit 7a44f2e

Browse files
jmill-16wkim10
andauthored
volunteer list in progress (#42)
* put search bar and table on page * updates * added pagination feature * finish first draft * add delete modal * fix zipcode build issue --------- Co-authored-by: wkim10 <[email protected]>
1 parent f0006d7 commit 7a44f2e

File tree

6 files changed

+500
-61
lines changed

6 files changed

+500
-61
lines changed

public/empty_list.png

47.8 KB
Loading

src/app/api/user/route.client.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { User, VolunteerDetails } from "@prisma/client";
1+
import { Role, User, VolunteerDetails } from "@prisma/client";
22

33
type CreateUserInput = Omit<User, "id" | "events" | "eventIds">;
44
type CreateVolunteerDetailsInput = Omit<
@@ -47,8 +47,13 @@ export const getUser = async (userID: string) => {
4747
export const getUserByEmail = async (email: string) => {
4848
const url = `/api/user?email=${email}`;
4949
return fetchApi(url, "GET");
50+
};
51+
52+
export const getUsersByRole = async (role: Role) => {
53+
const url = `/api/user?role=${role}`;
54+
return fetchApi(url, "GET");
55+
};
5056

51-
}
5257
export const deleteUser = async (userID: string) => {
5358
const url = `/api/user?id=${userID}`;
5459
return fetchApi(url, "DELETE");

src/app/api/user/route.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export const DELETE = async (request: NextRequest) => {
7676
}
7777

7878
try {
79+
await prisma.code.delete({
80+
where: { userId: id },
81+
});
82+
7983
await prisma.volunteerDetails.delete({
8084
where: { userId: id },
8185
});
@@ -107,9 +111,9 @@ export const GET = async (request: NextRequest) => {
107111
const { searchParams } = new URL(request.url);
108112
const id: string | undefined = searchParams.get("id") || undefined;
109113
const email: string | undefined = searchParams.get("email") || undefined;
114+
const role: string | undefined = searchParams.get("role") || undefined;
110115

111-
// Check if id and email is null
112-
if (!id && !email) {
116+
if (!id && !email && !role) {
113117
return NextResponse.json(
114118
{
115119
code: "BAD_REQUEST",
@@ -118,6 +122,37 @@ export const GET = async (request: NextRequest) => {
118122
{ status: 400 }
119123
);
120124
}
125+
126+
if (role) {
127+
try {
128+
const users = await prisma.user.findMany({
129+
where: { role: role === "ADMIN" ? "ADMIN" : "VOLUNTEER" },
130+
include: { volunteerDetails: true },
131+
});
132+
133+
if (!users || users.length === 0) {
134+
return NextResponse.json(
135+
{
136+
code: "NOT_FOUND",
137+
message: "No users found",
138+
},
139+
{ status: 404 }
140+
);
141+
}
142+
143+
return NextResponse.json({
144+
code: "SUCCESS",
145+
data: users,
146+
});
147+
} catch (error) {
148+
console.error("Error:", error);
149+
return NextResponse.json({
150+
code: "ERROR",
151+
message: error,
152+
});
153+
}
154+
}
155+
121156
try {
122157
const fetchedUser = await prisma.user.findUnique({
123158
where: id ? { id } : { email },

src/app/private/volunteers/page.tsx

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,181 @@
1+
"use client";
2+
3+
import VolunteerTable from "@components/VolunteerTable/VolunteerTable";
4+
import SearchBar from "@components/SearchBar";
5+
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
6+
import { Icon } from "@iconify/react/dist/iconify.js";
7+
import { Button } from "@mui/material";
18
import React from "react";
9+
import { Role, User } from "@prisma/client";
10+
import { deleteUser, getUsersByRole } from "@api/user/route.client";
11+
import Image from "next/image";
212

313
export default function VolunteersPage() {
14+
const [users, setUsers] = React.useState<User[]>();
15+
const [selected, setSelected] = React.useState<string[]>([]);
16+
const [searchText, setSearchText] = React.useState("");
17+
const [isModalOpen, setIsModalOpen] = React.useState(false);
18+
19+
React.useEffect(() => {
20+
const fetchUsers = async () => {
21+
try {
22+
const response = await getUsersByRole(Role.VOLUNTEER);
23+
setUsers(response.data);
24+
} catch (error) {
25+
console.error("Error fetching volunteers:", error);
26+
}
27+
};
28+
29+
fetchUsers();
30+
}, []);
31+
32+
// Filter users based on the search text
33+
const filteredUsers = users?.filter(
34+
(user) =>
35+
user.firstName.toLowerCase().includes(searchText.toLowerCase()) ||
36+
user.lastName.toLowerCase().includes(searchText.toLowerCase()) ||
37+
user.email.toLowerCase().includes(searchText.toLowerCase())
38+
);
39+
40+
const deleteUsers = async () => {
41+
try {
42+
const deletePromises = selected.map((id) => deleteUser(id));
43+
const responses = await Promise.all(deletePromises);
44+
const allDeleted = responses.every(
45+
(response) => response.code === "SUCCESS"
46+
);
47+
48+
if (allDeleted) {
49+
setUsers((prevUsers) =>
50+
prevUsers
51+
? prevUsers.filter((user) => !selected.includes(user.id))
52+
: []
53+
);
54+
setSelected([]);
55+
console.log("All users deleted successfully", responses);
56+
} else {
57+
console.error("Not all deletions succeeded");
58+
}
59+
setIsModalOpen(false);
60+
} catch (error) {
61+
console.error("Error deleting users:", error);
62+
}
63+
};
64+
465
return (
5-
<div>
6-
<h1>Volunteers Page</h1>
66+
<div className="flex flex-col gap-8">
67+
<div className="flex items-center justify-between">
68+
<div className="flex items-center gap-3">
69+
<Icon icon="mdi:people" width="44" height="44" />
70+
<div className="text-4xl font-['Kepler_Std'] font-semibold">
71+
Volunteer List ({users ? users.length : 0})
72+
</div>
73+
</div>
74+
{selected.length > 0 ? (
75+
<div className="flex items-center gap-4">
76+
<div>{selected.length} Selected</div>
77+
<Button
78+
sx={{
79+
display: "flex",
80+
padding: "10px 18px",
81+
alignItems: "center",
82+
gap: "8px",
83+
borderRadius: "8px",
84+
backgroundColor: "var(--Rose-600, #E61932)",
85+
color: "white",
86+
fontWeight: 600,
87+
textTransform: "none",
88+
"&:hover": {
89+
backgroundColor: "var(--Rose-700, #C11429)",
90+
},
91+
}}
92+
onClick={() => setIsModalOpen(true)}
93+
>
94+
<DeleteOutlineIcon sx={{ width: 20, color: "whitesmoke" }} />
95+
<div>Delete</div>
96+
</Button>
97+
</div>
98+
) : (
99+
<div className="h-[44.5px]"></div>
100+
)}
101+
</div>
102+
<SearchBar
103+
onSearchChange={(value) => {
104+
setSearchText(value);
105+
setSelected([]);
106+
}}
107+
/>
108+
{filteredUsers && filteredUsers.length > 0 ? (
109+
<VolunteerTable
110+
showPagination={true}
111+
fromVolunteerPage
112+
users={filteredUsers}
113+
selected={selected}
114+
setSelected={setSelected}
115+
/>
116+
) : (
117+
<div className="text-center">
118+
<div className="relative w-full h-[50vh]">
119+
<Image
120+
src="/empty_list.png"
121+
alt="Empty List"
122+
layout="fill"
123+
objectFit="contain"
124+
/>
125+
</div>
126+
<div className="text-[#344054] font-['Kepler_Std'] text-3xl font-semibold mt-8">
127+
No volunteers found!
128+
</div>
129+
</div>
130+
)}
131+
{isModalOpen && (
132+
<div className="fixed inset-0 flex items-center justify-center z-50">
133+
<div className="fixed inset-0 bg-[#101828] opacity-40"></div>
134+
<div className="bg-white p-6 rounded-2xl shadow-lg z-10 max-w-[512px]">
135+
<div className="text-[#101828] text-center font-['Kepler_Std'] text-4xl font-semibold">
136+
Are you sure you want to delete {selected.length}{" "}
137+
{selected.length === 1 ? "user" : "users"}?
138+
</div>
139+
<div className="text-[#667085] text-center text-lg mt-2">
140+
You will not be able to recover {selected.length === 1 ? "a" : ""}{" "}
141+
deleted {selected.length === 1 ? "profile" : "profiles"}.
142+
</div>
143+
<div className="flex justify-end gap-5 mt-8">
144+
<Button
145+
variant="outlined"
146+
sx={{
147+
borderRadius: "8px",
148+
border: "1px solid var(--Grey-300, #D0D5DD)",
149+
padding: "10px 18px",
150+
color: "var(--Teal-800, #145A5A)",
151+
fontWeight: 600,
152+
textTransform: "none",
153+
fontSize: 16,
154+
}}
155+
onClick={() => setIsModalOpen(false)}
156+
>
157+
Cancel
158+
</Button>
159+
<Button
160+
variant="outlined"
161+
sx={{
162+
borderRadius: "8px",
163+
padding: "10px 18px",
164+
backgroundColor: "var(--Teal-600, #138D8A)",
165+
color: "white",
166+
fontWeight: 600,
167+
textTransform: "none",
168+
fontSize: 16,
169+
"&:hover": { backgroundColor: "var(--Teal-700, #1D7A7A)" },
170+
}}
171+
onClick={deleteUsers}
172+
>
173+
Delete
174+
</Button>
175+
</div>
176+
</div>
177+
</div>
178+
)}
7179
</div>
8180
);
9181
}

src/components/SearchBar.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react";
2+
import { Box, InputBase } from "@mui/material";
3+
import SearchIcon from "@mui/icons-material/Search";
4+
5+
interface SearchBarProps {
6+
onSearchChange: (searchText: string) => void;
7+
}
8+
9+
const SearchBar = ({ onSearchChange }: SearchBarProps) => {
10+
return (
11+
<Box
12+
sx={{
13+
display: "flex",
14+
padding: "5px 7px",
15+
alignItems: "center",
16+
gap: "8px",
17+
borderRadius: "8px",
18+
border: "1px solid var(--Grey-300, #D0D5DD)",
19+
background: "var(--White, #FFF)",
20+
boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
21+
width: "100%",
22+
maxWidth: 400,
23+
}}
24+
>
25+
<SearchIcon sx={{ color: "var(--Grey-500, #667085)" }} />
26+
<InputBase
27+
placeholder="Search"
28+
onChange={(e) => onSearchChange(e.target.value)}
29+
sx={{
30+
width: "100%",
31+
fontSize: "14px",
32+
color: "var(--Grey-700, #344054)",
33+
"& input::placeholder": {
34+
color: "var(--Grey-500, #667085)",
35+
fontFamily: "Inter, sans-serif",
36+
fontSize: "16px",
37+
fontWeight: 400,
38+
lineHeight: "24px",
39+
},
40+
}}
41+
/>
42+
</Box>
43+
);
44+
};
45+
46+
export default SearchBar;

0 commit comments

Comments
 (0)