Skip to content

Commit 3d3abc3

Browse files
committed
Create edit user modal for changing roles within circle
1 parent 28f1e2a commit 3d3abc3

4 files changed

Lines changed: 212 additions & 7 deletions

File tree

frontend/app/components/Dashboard.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import "./Dashboard.css"
44
import React, { useEffect, useRef, useState } from "react";
55
import CreateCircleModal from "./CreateCircleModal";
6+
import EditModal from "./EditModal";
67
import InviteModal from "./InviteModal";
78
import { useFetchDashboard } from "../hooks/useFetchDashboard";
89
import { useWebSocketDashboard } from "../hooks/useWebsocketDashboard";
@@ -12,7 +13,7 @@ export default function Dashboard2() {
1213
const [isUserDataFetched, setIsUserDataFetched] = useState(false);
1314
const [selectedCircleID, setSelectedCircleID] = useState("");
1415
const [allMessages, setAllMessages] = useState<{ [key: string]: Message[] }>({});
15-
16+
1617
const [circles, setCircles] = useState<Circle[]>([]);
1718
const { userID, username, fetchUserData, handleDelete } = useFetchDashboard(setCircles);
1819
const { sendMessage } = useWebSocketDashboard(setCircles, allMessages, setAllMessages);
@@ -58,6 +59,17 @@ export default function Dashboard2() {
5859
}
5960
};
6061

62+
// Edit users in circle
63+
const [openEditModal, setOpenEditModal] = useState(false);
64+
function handleOpenEditModal() {
65+
setOpenEditModal(true);
66+
}
67+
const handleEditClose = (event: HandleCloseEvent) => {
68+
if (event.target.classList.contains('modal-container')) {
69+
setOpenEditModal(false);
70+
}
71+
};
72+
6173
const handleEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
6274
if (event.key === "Enter") {
6375
const content = (event.target as HTMLInputElement).value;
@@ -232,11 +244,38 @@ export default function Dashboard2() {
232244
<InviteModal isOpen={openInviteModal} setOpen={() => setOpenInviteModal(false)} circleId={selectedCircleID} />
233245
</div>
234246
</div>}
247+
{openEditModal && <div className="modal-container" onClick={handleEditClose}>
248+
<div className="modal inset-0 bg-black bg-opacity-50 z-50">
249+
<EditModal isOpen={openEditModal} setOpen={() => setOpenEditModal(false)} circleId={selectedCircleID} />
250+
</div>
251+
</div>}
235252
</div>
236253
<div className="chat-container">
237254
<div className="chat-title">
238255
<h1>{circles.find((circle) => circle.id === selectedCircleID)?.name}&nbsp;</h1>
239256
<div className="chat-menu">
257+
<button
258+
className="button-size group disabled:opacity-50 disabled:cursor-not-allowed"
259+
onClick={selectedCircleID !== "" ? handleOpenEditModal : undefined}
260+
disabled={selectedCircleID === ""}
261+
>
262+
<svg
263+
xmlns="http://www.w3.org/2000/svg"
264+
className="size-4 opacity-75"
265+
fill="currentColor"
266+
viewBox="0 0 24 24"
267+
stroke="currentColor"
268+
strokeWidth="1"
269+
>
270+
<path d="M3.5,24h15A3.51,3.51,0,0,0,22,20.487V12.95a1,1,0,0,0-2,0v7.537A1.508,1.508,0,0,1,18.5,22H3.5A1.508,1.508,0,0,1,2,20.487V5.513A1.508,1.508,0,0,1,3.5,4H11a1,1,0,0,0,0-2H3.5A3.51,3.51,0,0,0,0,5.513V20.487A3.51,3.51,0,0,0,3.5,24Z"></path>
271+
<path d="M9.455,10.544l-.789,3.614a1,1,0,0,0,.271.921,1.038,1.038,0,0,0,.92.269l3.606-.791a1,1,0,0,0,.494-.271l9.114-9.114a3,3,0,0,0,0-4.243,3.07,3.07,0,0,0-4.242,0l-9.1,9.123A1,1,0,0,0,9.455,10.544Zm10.788-8.2a1.022,1.022,0,0,1,1.414,0,1.009,1.009,0,0,1,0,1.413l-.707.707L19.536,3.05Zm-8.9,8.914,6.774-6.791,1.4,1.407-6.777,6.793-1.795.394Z"></path>
272+
</svg>
273+
<span
274+
className="invisible absolute start-1/2 top-full mt-5 -translate-x-1/2 rounded bg-gray-900 px-1.5 py-1.5 text-xs font-medium text-white group-hover:visible"
275+
>
276+
Edit Users
277+
</span>
278+
</button>
240279
<button
241280
className="button-size group disabled:opacity-50 disabled:cursor-not-allowed"
242281
onClick={selectedCircleID !== "" ? handleOpenInviteModal : undefined}
@@ -308,8 +347,7 @@ export default function Dashboard2() {
308347
<div className="analytics-placeholder">
309348
</div>
310349
</div>
311-
</div>
312-
}
350+
</div>}
313351
</>
314352

315353
)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { useAuth } from "../context/authContext";
3+
import { EditUser } from "../types";
4+
import "./InviteModal.css"
5+
6+
interface InviteModalProps {
7+
isOpen: boolean;
8+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
9+
circleId: string;
10+
}
11+
12+
function EditModal({ isOpen, setOpen, circleId }: InviteModalProps) {
13+
const authContext = useAuth();
14+
if (!authContext) {
15+
throw new Error("useAuth must be used within an AuthProvider");
16+
}
17+
const { getAccessToken } = authContext;
18+
const [users, setUsers] = useState<EditUser[]>([]);
19+
20+
useEffect(() => {
21+
async function fetchInviteUsers() {
22+
const token = await getAccessToken();
23+
const headers = {
24+
'Authorization': `Bearer ${token}`,
25+
'Content-Type': 'application/json'
26+
};
27+
const body = {
28+
circle_id: circleId,
29+
};
30+
fetch('http://localhost:8000/api/circles/edit', {
31+
method: 'POST',
32+
headers: headers,
33+
body: JSON.stringify(body),
34+
})
35+
.then(async (response) => {
36+
const data = await response.json();
37+
if (!response.ok) {
38+
console.log("Error:", data);
39+
} else {
40+
console.log("Data:", data);
41+
// Initialize each user with a default role of "member"
42+
const mappedUsers = data.map((user: any) => ({
43+
id: user.id,
44+
username: user.username,
45+
role: 'member'
46+
}));
47+
setUsers(mappedUsers);
48+
}
49+
})
50+
.catch(error => {
51+
console.log(error);
52+
});
53+
}
54+
fetchInviteUsers();
55+
}, [circleId, getAccessToken]);
56+
57+
// Update the role for the specified user
58+
const handleRoleChange = (userId: string, newRole: string) => {
59+
setUsers(prevUsers =>
60+
prevUsers.map(user =>
61+
user.id === userId ? { ...user, role: newRole } : user
62+
)
63+
);
64+
};
65+
66+
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
67+
e.preventDefault();
68+
const token = await getAccessToken();
69+
const headers = {
70+
'Authorization': `Bearer ${token}`,
71+
'Content-Type': 'application/json'
72+
};
73+
// Send each user's id and the chosen role
74+
const body = {
75+
circle_id: circleId,
76+
users: users.map(user => ({ id: user.id, role: user.role })),
77+
};
78+
console.log(body);
79+
fetch('http://localhost:8000/api/circles/edit/add', {
80+
method: 'POST',
81+
headers: headers,
82+
body: JSON.stringify(body),
83+
})
84+
.then(async (response) => {
85+
const data = await response.json();
86+
if (!response.ok) {
87+
console.log("Error:", data);
88+
} else {
89+
console.log("Data:", data);
90+
setOpen(false);
91+
}
92+
})
93+
.catch(error => {
94+
console.log(error);
95+
});
96+
};
97+
98+
if (!isOpen) return null;
99+
100+
return (
101+
<div className="mx-auto max-w-screen-xl px-4 py-2 sm:px-6 lg:px-8 relative z-10 focus:outline-none">
102+
<form action="#" className="mx-auto mb-4 mt-6 max-w-md space-y-4" onSubmit={handleSubmit}>
103+
<div>
104+
<div className="search w-full relative rounded-md">
105+
<input
106+
className="search-input w-full border-gray-200 p-2 text-sm shadow-sm text-black"
107+
placeholder="Search username"
108+
id="circleName"
109+
/>
110+
<svg xmlns="http://www.w3.org/2000/svg"
111+
className="size-11 search-icon hover:cursor-pointer"
112+
fill="hsl(0, 0%, 95%)"
113+
viewBox="0 0 24 24"
114+
stroke="hsl(0, 0%, 95%)"
115+
strokeWidth="1">
116+
<path d="M 9 2 C 5.1458514 2 2 5.1458514 2 9 C 2 12.854149 5.1458514 16 9 16 C 10.747998 16 12.345009 15.348024 13.574219 14.28125 L 14 14.707031 L 14 16 L 20 22 L 22 20 L 16 14 L 14.707031 14 L 14.28125 13.574219 C 15.348024 12.345009 16 10.747998 16 9 C 16 5.1458514 12.854149 2 9 2 z M 9 4 C 11.773268 4 14 6.2267316 14 9 C 14 11.773268 11.773268 14 9 14 C 6.2267316 14 4 11.773268 4 9 C 4 6.2267316 6.2267316 4 9 4 z"></path>
117+
</svg>
118+
</div>
119+
<div className="flex users my-4 rounded-md gap-3 shadow-sm">
120+
<ul>
121+
{users.length === 0 && (
122+
<li className="user-item flex items-center gap-2">
123+
<p className="font-medium text-sm text-gray-500">No users found</p>
124+
</li>
125+
)}
126+
{users.length != 0 && users.map(user => (
127+
<li key={user.id} className="user-item flex items-center gap-2">
128+
<p className="font-medium text-sm text-gray-500">{user.username}</p>
129+
<select
130+
value={user.role}
131+
onChange={(e) => handleRoleChange(user.id, e.target.value)}
132+
className="border border-gray-300 p-1 rounded"
133+
>
134+
<option value="member">Member</option>
135+
<option value="admin">Admin</option>
136+
</select>
137+
</li>
138+
))}
139+
</ul>
140+
</div>
141+
</div>
142+
143+
<div className="flex items-center justify-between">
144+
<button
145+
type="submit"
146+
className="inline-block rounded-lg bg-blue-500 px-5 py-3 text-sm font-medium text-white"
147+
>
148+
Submit
149+
</button>
150+
</div>
151+
</form>
152+
</div>
153+
);
154+
}
155+
156+
export default EditModal;

frontend/app/components/InviteModal.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function InviteModal({ isOpen, setOpen, circleId }: InviteModalProps) {
6464
));
6565
};
6666

67-
async function handleSubmit (e: React.FormEvent<HTMLFormElement>) {
67+
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
6868
e.preventDefault();
6969
const invitedUsers = users.filter(user => user.checked).map(user => user.id);
7070
const token = await getAccessToken();
@@ -112,16 +112,21 @@ function InviteModal({ isOpen, setOpen, circleId }: InviteModalProps) {
112112
/>
113113
<svg xmlns="http://www.w3.org/2000/svg"
114114
className="size-11 search-icon hover:cursor-pointer"
115-
fill="hsl(0, 0%, 90%)"
115+
fill="hsl(0, 0%, 95%)"
116116
viewBox="0 0 24 24"
117-
stroke="hsl(0, 0%, 90%)"
117+
stroke="hsl(0, 0%, 95%)"
118118
strokeWidth="1">
119119
<path d="M 9 2 C 5.1458514 2 2 5.1458514 2 9 C 2 12.854149 5.1458514 16 9 16 C 10.747998 16 12.345009 15.348024 13.574219 14.28125 L 14 14.707031 L 14 16 L 20 22 L 22 20 L 16 14 L 14.707031 14 L 14.28125 13.574219 C 15.348024 12.345009 16 10.747998 16 9 C 16 5.1458514 12.854149 2 9 2 z M 9 4 C 11.773268 4 14 6.2267316 14 9 C 14 11.773268 11.773268 14 9 14 C 6.2267316 14 4 11.773268 4 9 C 4 6.2267316 6.2267316 4 9 4 z"></path>
120120
</svg>
121121
</div>
122122
<div className="flex users my-4 rounded-md gap-3 shadow-sm">
123123
<ul>
124-
{users.map((user) => (
124+
{users.length === 0 && (
125+
<li className="user-item flex items-center gap-2">
126+
<p className="font-medium text-sm text-gray-500">No users found</p>
127+
</li>
128+
)}
129+
{users.length != 0 && users.map((user) => (
125130
<label key={user.id} className="user-item flex items-center gap-2">
126131
<div className="flex items-center">
127132
<input

frontend/app/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ export interface User {
2323
id: string;
2424
username: string;
2525
checked: boolean;
26+
}
27+
28+
export interface EditUser {
29+
id: string;
30+
username: string;
31+
role: string;
2632
}

0 commit comments

Comments
 (0)