Skip to content

Commit b2c6629

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 b2c6629

24 files changed

+1211
-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

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
orgToken
12+
createdAt
13+
}
14+
}
15+
`;

src/components/invitationModel.tsx

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