Skip to content

Commit 368aabe

Browse files
committed
feat(invitation):admin should invite user by role and email
- ensure that a user should be invited by providing email and role. -ensure that a user that invited should recieve invitation email containing the link to register to any organization. [Deliver #421]
1 parent 6a8a673 commit 368aabe

22 files changed

+911
-629
lines changed

package-lock.json

+145-205
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Mutations/invitationMutation.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { gql } from '@apollo/client';
2+
3+
export const SEND_INVITATION = gql`
4+
mutation SendInvitation($invitees: [InviteeInput!]!, $orgToken: String!) {
5+
sendInvitation(invitees: $invitees, orgToken: $orgToken) {
6+
status
7+
invitees {
8+
email
9+
role
10+
}
11+
createdAt
12+
}
13+
}
14+
`;

src/components/invitationModel.tsx

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useMutation } from '@apollo/client';
3+
import {SEND_INVITATION} from '../Mutations/invitationMutation'
4+
import { toast } from 'react-toastify';
5+
import { IoIosCloseCircleOutline, IoIosArrowDown, IoIosArrowUp } from 'react-icons/io';
6+
import { BsExclamationCircleFill } from "react-icons/bs";
7+
8+
const roles = ['trainee', 'admin', 'ttl', 'coordinator'];
9+
10+
const InviteForm: React.FC<{ onClose: () => void }> = ({ onClose }) => {
11+
const [email, setEmail] = useState('');
12+
const [role, setRole] = useState('Role');
13+
const [orgToken, setOrgToken] = useState('');
14+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
15+
const [emailError, setEmailError] = useState('');
16+
const [sendInvitation, { loading, error }] = useMutation(SEND_INVITATION);
17+
const organisationToken = localStorage.getItem('orgToken');
18+
19+
useEffect(() => {
20+
if (organisationToken) {
21+
setOrgToken(organisationToken);
22+
}
23+
}, [organisationToken]);
24+
25+
const validateEmail = (email: string) => {
26+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
27+
return emailRegex.test(email);
28+
};
29+
30+
const handleSubmit = async (e: React.FormEvent) => {
31+
e.preventDefault();
32+
33+
if (!validateEmail(email)) {
34+
setEmailError('Please enter a valid email address.');
35+
return;
36+
}
37+
38+
setEmailError('');
39+
40+
try {
41+
await sendInvitation({
42+
variables: {
43+
invitees: [{ email, role }],
44+
orgToken: orgToken,
45+
},
46+
});
47+
toast.success('Invitation sent successfully!');
48+
setEmail('');
49+
setRole('Role');
50+
setOrgToken('');
51+
} catch (e: any) {
52+
toast.error(`Error sending invitation: ${e.message}`);
53+
}
54+
};
55+
56+
const toggleDropdown = () => {
57+
setIsDropdownOpen(!isDropdownOpen);
58+
};
59+
60+
const handleRoleSelect = (selectedRole: string) => {
61+
setRole(selectedRole);
62+
setIsDropdownOpen(false);
63+
};
64+
65+
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
66+
setEmail(e.target.value);
67+
setEmailError('');
68+
};
69+
70+
return (
71+
<div className="relative bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
72+
<h2 className="text-xl font-bold mb-4">Invite users</h2>
73+
<button
74+
className="absolute top-2 right-2 text-black hover:text-gray-800"
75+
onClick={onClose}
76+
>
77+
<IoIosCloseCircleOutline size={24} />
78+
</button>
79+
<form onSubmit={handleSubmit} className="pb-16">
80+
<div className="text-black py-2">
81+
Anyone you invite will be able to access the dashboard of this application.
82+
</div>
83+
<input
84+
value={email}
85+
onChange={handleEmailChange}
86+
placeholder="Email address"
87+
className="border border-[#d9d0fb] px-1 rounded-md w-full mb-1 focus:outline-none focus:border-[#d9d0fb]"
88+
/>
89+
{emailError && (
90+
<p className="text-red-600 text-sm mb-4">{emailError}</p>
91+
)}
92+
<div>
93+
<label className="pb-2">
94+
<div className='flex justify-between pt-4'>
95+
<div className="relative w-[50%]">
96+
<button
97+
type="button"
98+
onClick={toggleDropdown}
99+
className="border border-[#d9d0fb] rounded-md w-full focus:outline-none focus:border-[#d9d0fb] bg-white px-2 text-left flex justify-between items-center"
100+
>
101+
{role}
102+
{isDropdownOpen ? (
103+
<IoIosArrowUp size={20} />
104+
) : (
105+
<IoIosArrowDown size={20} />
106+
)}
107+
</button>
108+
{isDropdownOpen && (
109+
<div className="absolute mt-1 w-full bg-[#f3f0fe] border border-[#d9d0fb] rounded-md shadow-lg z-10">
110+
{roles.map((roleOption) => (
111+
<div
112+
key={roleOption}
113+
onClick={() => handleRoleSelect(roleOption)}
114+
className="px-4 hover:bg-[#8667f2] cursor-pointer"
115+
>
116+
{roleOption.charAt(0).toUpperCase() + roleOption.slice(1)}
117+
</div>
118+
))}
119+
</div>
120+
)}
121+
</div>
122+
<div>
123+
<button
124+
type="submit"
125+
disabled={loading}
126+
className="ml-4 flex justify-center bg-[#7a5edc] text-white px-4 rounded"
127+
>
128+
{loading ? `Loading...` : 'Invite'}
129+
</button>
130+
</div>
131+
</div>
132+
</label>
133+
</div>
134+
</form>
135+
<div className="border-t-[1px] border-[#d9d0fb] m-0"></div>
136+
<div className="flex justify-between pt-2">
137+
<p className='flex justify-center items-center text-gray-400'><BsExclamationCircleFill />Learn more</p>
138+
<button
139+
type="button"
140+
disabled={loading}
141+
className="flex justify-center bg-[#7a5edc] text-white px-4 rounded"
142+
>
143+
{'Upload users file'}
144+
</button>
145+
</div>
146+
</div>
147+
);
148+
};
149+
150+
export default InviteForm;

src/components/tests/__snapshots__/AdminTraineeDashboard.test.tsx.snap

+57-52
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Array [
2828
>
2929
<form
3030
className="px-8 py-3 "
31+
onSubmit={[Function]}
3132
>
3233
<div
3334
className="flex flex-wrap items-center justify-center w-full card-title "
@@ -48,6 +49,7 @@ Array [
4849
className="w-full px-5 py-2 font-sans text-xs text-black border rounded outline-none dark:bg-dark-tertiary border-primary"
4950
name="email"
5051
onChange={[Function]}
52+
onSubmit={[Function]}
5153
placeholder="email"
5254
type="email"
5355
value=""
@@ -461,7 +463,7 @@ Array [
461463
name="date"
462464
readOnly={true}
463465
type="text"
464-
value="2024-03-26"
466+
value="2024-08-30"
465467
/>
466468
</div>
467469
<div
@@ -890,7 +892,6 @@ Array [
890892
onClick={[Function]}
891893
type="button"
892894
>
893-
894895
add
895896
+
896897
@@ -1111,60 +1112,64 @@ Array [
11111112
colSpan={6}
11121113
>
11131114
<div
1114-
className="w-full justify-center flex mx-auto flex-row items-center overflow-x-auto"
1115+
className="w-full justify-between flex mx-auto flex-row items-center overflow-x-auto"
11151116
>
1116-
<button
1117-
aria-label="left-arrow"
1118-
className="page mx-2 text-white rounded-circle appearance-none font-bold flex items-center justify-center bg-primary h-[30px] w-[60px] cursor-pointer"
1119-
disabled={true}
1120-
onClick={[Function]}
1121-
type="button"
1117+
<div
1118+
className="flex"
11221119
>
1123-
<svg
1124-
aria-hidden="true"
1125-
className="w-5"
1126-
fill="none"
1127-
fontSize="sm"
1128-
stroke="currentColor"
1129-
strokeWidth={2}
1130-
viewBox="0 0 24 24"
1131-
xmlns="http://www.w3.org/2000/svg"
1120+
<button
1121+
aria-label="left-arrow"
1122+
className="page mx-2 text-white rounded-circle appearance-none font-bold flex items-center justify-center bg-primary h-[30px] w-[60px] cursor-pointer"
1123+
disabled={true}
1124+
onClick={[Function]}
1125+
type="button"
11321126
>
1133-
<path
1134-
d="M11 15l-3-3m0 0l3-3m-3 3h8M3 12a9 9 0 1118 0 9 9 0 01-18 0z"
1135-
strokeLinecap="round"
1136-
strokeLinejoin="round"
1137-
/>
1138-
</svg>
1139-
</button>
1140-
1141-
<button
1142-
aria-label="right-arrow"
1143-
className="page mx-2 text-white rounded-circle font-bold flex items-center justify-center bg-primary h-[30px] w-[60px] cursor-pointer"
1144-
disabled={true}
1145-
onClick={[Function]}
1146-
type="button"
1147-
>
1148-
<svg
1149-
aria-hidden="true"
1150-
className="w-5"
1151-
fill="none"
1152-
fontSize="sm"
1153-
stroke="currentColor"
1154-
strokeWidth={2}
1155-
viewBox="0 0 24 24"
1156-
xmlns="http://www.w3.org/2000/svg"
1127+
<svg
1128+
aria-hidden="true"
1129+
className="w-5"
1130+
fill="none"
1131+
fontSize="sm"
1132+
stroke="currentColor"
1133+
strokeWidth={2}
1134+
viewBox="0 0 24 24"
1135+
xmlns="http://www.w3.org/2000/svg"
1136+
>
1137+
<path
1138+
d="M11 15l-3-3m0 0l3-3m-3 3h8M3 12a9 9 0 1118 0 9 9 0 01-18 0z"
1139+
strokeLinecap="round"
1140+
strokeLinejoin="round"
1141+
/>
1142+
</svg>
1143+
</button>
1144+
1145+
<button
1146+
aria-label="right-arrow"
1147+
className="page mx-2 text-white rounded-circle font-bold flex items-center justify-center bg-primary h-[30px] w-[60px] cursor-pointer"
1148+
disabled={true}
1149+
onClick={[Function]}
1150+
type="button"
11571151
>
1158-
<path
1159-
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
1160-
strokeLinecap="round"
1161-
strokeLinejoin="round"
1162-
/>
1163-
</svg>
1164-
</button>
1165-
1152+
<svg
1153+
aria-hidden="true"
1154+
className="w-5"
1155+
fill="none"
1156+
fontSize="sm"
1157+
stroke="currentColor"
1158+
strokeWidth={2}
1159+
viewBox="0 0 24 24"
1160+
xmlns="http://www.w3.org/2000/svg"
1161+
>
1162+
<path
1163+
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
1164+
strokeLinecap="round"
1165+
strokeLinejoin="round"
1166+
/>
1167+
</svg>
1168+
</button>
1169+
1170+
</div>
11661171
<div
1167-
className="flex flex-row justify-center w-full sm:text-[12px] items-center "
1172+
className="flex flex-row justify-center w-1/3 sm:text-[12px] items-center"
11681173
>
11691174
<span
11701175
className="inline-block mx-2"
@@ -1200,7 +1205,7 @@ Array [
12001205
</span>
12011206
12021207
<select
1203-
className="px-1/2 font-raleway rounded-md border border-primary dark:bg-primary"
1208+
className="px-1/2 font-raleway rounded-md border border-dark dark:bg-primary focus:outline-none"
12041209
onChange={[Function]}
12051210
style={
12061211
Object {

0 commit comments

Comments
 (0)