Skip to content

Commit

Permalink
Bulk email send feature (#115)
Browse files Browse the repository at this point in the history
Co-authored-by: Anjula Shanaka <[email protected]>
  • Loading branch information
mayura-andrew and anjula-sack authored Jun 11, 2024
1 parent c2188cf commit 9aba2f9
Show file tree
Hide file tree
Showing 15 changed files with 537 additions and 119 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MenteeDashboard from './pages/MenteeDashboard';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import MenteeApplications from './pages/MenteeApplicationsView/MenteeApplications.component.tsx';
import { LoginModalProvider } from './contexts/LoginModalContext.tsx';
import PasswordReset from './pages/PasswordReset/index.tsx';

const queryClient = new QueryClient();

Expand Down Expand Up @@ -45,6 +46,7 @@ const App: React.FC = () => {
<Route path="/mentors" element={<Mentors />} />
<Route path="/mentors/:mentorId" element={<MentorProfile />} />
<Route path="/settings" element={<EditProfileForm />} />
<Route path="/resetpassword" element={<PasswordReset />} />
</Routes>
</MainLayout>
</BrowserRouter>
Expand Down
117 changes: 117 additions & 0 deletions src/components/ForgotPasswordModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useResetPassword } from '../../hooks/useForgotPassword';
import { type PasswordResetData } from '../../types';
import { z } from 'zod';
import Loading from '../../assets/svg/Loading';
import closeIcon from '../../assets/svg/closeIcon.svg';

const ForgotPasswordDataSchema = z.object({
email: z.string(),
});

interface ForgotPasswordModalProps {
handleClose: () => void;
}

const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({
handleClose,
}) => {
const { register, handleSubmit } = useForm<PasswordResetData>();

const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState('default');
const { requestPasswordReset } = useResetPassword();
const [loading, setLoading] = useState(false);

const onSubmit = (data: PasswordResetData) => {
setLoading(true);

const validatedData = ForgotPasswordDataSchema.parse(data);
requestPasswordReset(validatedData, {
onSuccess: () => {
setMessage(
`A password reset link has been sent to your email address. Please check your email and follow the instructions to reset your password. `
);
setMessageType('success');
setLoading(false);
},
onError: () => {
setMessage('Error sending email, Check your email and try again.');
setMessageType('error');
setLoading(false);
},
});
};

return (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0 flex-col items-center justify-center h-screen w-full p-5 ">
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
<div
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<button
className="absolute top-2 right-2 p-2 text-gray-700 hover:text-gray-900 focus:outline-none"
onClick={handleClose}
>
<img className="w-6 h-6" src={closeIcon} alt="Modal Close Icon" />
</button>

<div className="bg-white p-6 space-y-8 rounded-lg shadow-xl">
<div className="m-5">
<h2 className="text-2xl mb-5 font-semibold text-gray-900 text-center">
Reset Password
</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
{...register('email')}
placeholder="Enter your email address"
className="bg-gray-50 border mb-5 border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
/>
{message.length > 0 && (
<div
className={`px-4 py-3 my-3 text-center ${
messageType === 'success'
? 'text-green-500'
: 'text-red-500'
}`}
>
{message}
</div>
)}
<div>
{loading ? (
<div className="flex justify-center items-center px-4 py-2">
<Loading />
</div>
) : (
messageType !== 'success' && (
<div className="flex justify-center items-center">
<button
type="submit"
className="w-full px-4 py-2 text-base font-semibold text-center text-white bg-blue-500 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50 cursor-pointer shadow-md transition-all md:w-auto"
>
Submit
</button>
</div>
)
)}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export default ForgotPasswordModal;
8 changes: 8 additions & 0 deletions src/components/Layout/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@ import {
import LogoutModal from '../../LogoutModal';
import { Link, useNavigate } from 'react-router-dom';
import { useLoginModalContext } from '../../../contexts/LoginModalContext';
import ForgotPasswordModal from '../../ForgotPasswordModal';

const Navbar: React.FC = () => {
const [openMenu, setOpenMenu] = useState(false);
const {
isLoginModalVisible,
isRegisterModalVisible,
isLogoutModalVisible,
isForgotPasswordModalVisible,
handleLoginModalClose,
handleLoginModalOpen,
handleRegisterModalClose,
handleRegisterModalOpen,
handleLogoutModalClose,
handleLogoutModalOpen,
handleForgotPasswordModalOpen,
handleForgotPasswordModalClose,
} = useLoginModalContext();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -285,6 +289,7 @@ const Navbar: React.FC = () => {
<LoginModal
handleClose={handleLoginModalClose}
onRegistrationClick={handleRegisterModalOpen}
onForgotPasswordClick={handleForgotPasswordModalOpen}
/>
) : null}
{isRegisterModalVisible ? (
Expand All @@ -294,6 +299,9 @@ const Navbar: React.FC = () => {
/>
) : null}
{isLogoutModalVisible && <LogoutModal onClose={handleLogoutModalClose} />}
{isForgotPasswordModalVisible ? (
<ForgotPasswordModal handleClose={handleForgotPasswordModalClose} />
) : null}
</>
);
};
Expand Down
10 changes: 9 additions & 1 deletion src/components/LoginModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import GoogleLoginButton from '../OAuth/Google';
interface LoginModalProps {
handleClose: () => void;
onRegistrationClick: () => void;
onForgotPasswordClick: () => void;
}

const LoginModal: React.FC<LoginModalProps> = ({
handleClose,
onRegistrationClick,
onForgotPasswordClick,
}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
Expand Down Expand Up @@ -124,7 +126,13 @@ const LoginModal: React.FC<LoginModalProps> = ({
</label>
</div>
</div>
<p className="text-sm font-normal text-black hover:underline">
<p
className="text-sm font-normal text-black hover:underline cursor-pointer"
onClick={() => {
onForgotPasswordClick();
handleClose();
}}
>
Forgot Password?
</p>
<button
Expand Down
26 changes: 13 additions & 13 deletions src/components/MenteeApplication/MenteeApplication.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ const MenteeApplication: React.FC = () => {
)}
</span>
</div>
<div className="ml-auto flex overflow-hidden">
{mentee?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateChange('approved');
}}
reject={() => {
handleStateChange('rejected');
}}
/>
)}
</div>
</div>
<div className="ml-auto flex overflow-hidden">
{mentee?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateChange('approved');
}}
reject={() => {
handleStateChange('rejected');
}}
/>
)}
</div>
<div className="grid grid-cols-5 gap-10">
<div className="col-span-3">
Expand Down
26 changes: 13 additions & 13 deletions src/components/MenteeProfile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ const MenteeProfile: React.FC = () => {
)}
</div>
</div>
<div className="ml-auto flex overflow-hidden mt-5 md:mt-0">
{mentee?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateUpdate(ApplicationStatus.APPROVED);
}}
reject={() => {
handleStateUpdate(ApplicationStatus.REJECTED);
}}
/>
)}
</div>
</div>
<div className="ml-auto flex overflow-hidden mt-4 md:mt-0">
{mentee?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateUpdate(ApplicationStatus.APPROVED);
}}
reject={() => {
handleStateUpdate(ApplicationStatus.REJECTED);
}}
/>
)}
</div>
<div className="md:hidden">
<a
Expand Down
26 changes: 13 additions & 13 deletions src/components/MentorApplication/MentorApplication.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,19 @@ const MentorApplication: React.FC = () => {
{mentor?.application.institution}
</span>
</div>
<div className="ml-auto flex overflow-hidden">
{mentor?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateChange('approved');
}}
reject={() => {
handleStateChange('rejected');
}}
/>
)}
</div>
</div>
<div className="ml-auto flex overflow-hidden">
{mentor?.state === ApplicationStatus.PENDING && (
<ApproveRejectButtons
isLoading={isPending}
approve={() => {
handleStateChange('approved');
}}
reject={() => {
handleStateChange('rejected');
}}
/>
)}
</div>
<div className="grid grid-cols-4 gap-10">
<div className="col-span-3">
Expand Down
19 changes: 19 additions & 0 deletions src/contexts/LoginModalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ const LoginModalContext = createContext<{
handleRegisterModalOpen: () => void;
handleRegisterModalClose: () => void;
handleLogoutModalOpen: () => void;
handleForgotPasswordModalOpen: () => void;
handleForgotPasswordModalClose: () => void;
isLoginModalVisible: boolean;
isRegisterModalVisible: boolean;
isLogoutModalVisible: boolean;
isForgotPasswordModalVisible: boolean;
}>({
isLogoutModalVisible: false,
isRegisterModalVisible: false,
isLoginModalVisible: false,
isForgotPasswordModalVisible: false,
handleLoginModalOpen: () => {},
handleLoginModalClose: () => {},
handleLogoutModalClose: () => {},
handleRegisterModalOpen: () => {},
handleRegisterModalClose: () => {},
handleLogoutModalOpen: () => {},
handleForgotPasswordModalOpen: () => {},
handleForgotPasswordModalClose: () => {},
});

export const LoginModalProvider: React.FC<{ children: React.ReactNode }> = ({
Expand All @@ -28,6 +34,8 @@ export const LoginModalProvider: React.FC<{ children: React.ReactNode }> = ({
const [isLoginModalVisible, setIsLoginModalVisible] = useState(false);
const [isRegisterModalVisible, setIsRegisterModalVisible] = useState(false);
const [isLogoutModalVisible, setIsLogoutModalVisible] = useState(false);
const [isForgotPasswordModalVisible, setIsForgotPasswordModalVisible] =
useState(false);

const handleLoginModalClose = () => {
setIsLoginModalVisible(false);
Expand All @@ -53,18 +61,29 @@ export const LoginModalProvider: React.FC<{ children: React.ReactNode }> = ({
setIsLogoutModalVisible(true);
};

const handleForgotPasswordModalClose = () => {
setIsForgotPasswordModalVisible(false);
};

const handleForgotPasswordModalOpen = () => {
setIsForgotPasswordModalVisible(true);
};

return (
<LoginModalContext.Provider
value={{
isLoginModalVisible,
isRegisterModalVisible,
isLogoutModalVisible,
isForgotPasswordModalVisible,
handleLoginModalClose,
handleLoginModalOpen,
handleRegisterModalClose,
handleRegisterModalOpen,
handleLogoutModalClose,
handleLogoutModalOpen,
handleForgotPasswordModalClose,
handleForgotPasswordModalOpen,
}}
>
{children}
Expand Down
Loading

0 comments on commit 9aba2f9

Please sign in to comment.