Skip to content

Commit 97bb679

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 97bb679

22 files changed

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

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