Frontend
The frontend part is very easy compared to the backend part. All I need to do is create a modal, and use it to send data twice.
- Firstly send email to send OTP to
- Then send the OTP and the new password to change it
To create the modal, I copied some code, the classNames for the encapsulation of a modal, from the MessageModal
component in my earlier project Chat-Nat.
Planning
I'll add a "Forgot Password?" button on the login page, and set the onClick
handler to open the modal
I need to use a boolean state to denote whether the OTP has been sent to the user's email before asking for it. I'm naming the state isOTPSent
- If
!isOTPSent
-> just ask for the email address, send api req, then if successfulsetOTPSent(true)
- If
isOTPSent
-> now also ask for the OTP and the new password, then if successful, close the modal
Here are a few components and hooks that I'm reusing from the existing frontend of this project:
-
Box
-> It neatly wrapped my login and register pages into a card, centered on the page, reusing here with title"Password Reset"
-
AuthForm
-> Just a form but I coded it to disable the submit button and set button text to"Loading..."
when we are waiting on a response from the server -
FormInput
-> Input field with a label of its own, with value setter and onChange handler, optionally with anisRequired
boolean -
useAxios
-> Custom hook to handle the responses from the server which needs a token refresh.apiReq
function for the normal request sending, some custom error handling to display analert()
and refresh token,refreshReq
function to refresh the auth token and try the initial request again.
Here's the entire code for the modal:
// src/components/PasswordResetModal.tsx
import React, { useState } from "react"
import AuthForm from "./AuthForm";
import FormInput from "./FormInput";
import Box from "./Box";
import { useAxios } from "../hooks/useAxios";
interface FormData {
email: string,
new_password: string,
otp: string,
}
interface Props {
isVisible: boolean,
onClose: () => void,
}
const PasswordResetModal: React.FC<Props> = ({ isVisible, onClose }) => {
const [formData, setFormData] = useState<FormData>({
email: "",
new_password: "",
otp: ""
});
const [isLoading, setLoading] = useState<boolean>(false);
const [isOTPSent, setOTPSent] = useState<boolean>(false);
const { apiReq } = useAxios();
const handleClose = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if ((e.target as HTMLElement).id === "wrapper") {
onClose();
// could have setOTPSent(false), but avoiding it in case user misclicks outside
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
if (!isOTPSent) { // first request for sending otp,
const response = await apiReq<unknown, FormData>("post", "/api/reset-password", formData)
if (response) {
alert("OTP has been sent to your email");
setOTPSent(true);
}
} else { // then using otp to change password
const response = await apiReq<unknown, FormData>("put", "/api/reset-password", formData)
if (response) {
alert("Password has been successfully reset\nPlease log in again");
// clear the form
setFormData({
email: "",
otp: "",
new_password: "",
})
// close modal
onClose();
}
}
setLoading(false);
};
if (!isVisible) return null;
return (
<div
id="wrapper"
className="fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm flex justify-center items-center"
onClick={handleClose}>
<Box title="Password Reset">
<AuthForm
submitHandler={handleSubmit}
isLoading={isLoading}
buttonText={isOTPSent ? "Change Password" : "Send OTP"}>
<FormInput
id="email"
label="Your email"
type="email"
value={formData.email}
changeHandler={handleChange}
isRequired />
{isOTPSent && (<>
<FormInput
id="otp"
label="OTP"
type="text"
value={formData.otp}
changeHandler={handleChange}
isRequired />
<FormInput
id="new_password"
label="New Password"
type="password"
value={formData.new_password}
changeHandler={handleChange}
isRequired />
</>)}
</AuthForm>
</Box>
</div>
)
}
export default PasswordResetModal
And here's how the conditional rendering of the modal is handled in the Login form
// src/pages/auth/Login.tsx
import PasswordResetModal from "../../components/PasswordResetModal";
const Login: React.FC = () => {
const [showModal, setShowModal] = useState<boolean>(false);
return (
<Section>
<Box title="Login">
<div className="grid grid-flow-col">
{/* link to the register page here */}
<button
type="button"
onClick={() => setShowModal(true)}
className="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center me-2 mb-2 dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-500 dark:focus:ring-blue-800">
Forgot Password?
</button>
<PasswordResetModal isVisible={showModal} onClose={() => setShowModal(false)} />
</div>
</Box>
</Section>
)
We're done! Or so I thought.
While running the app in my development environment, I discovered a bug where the emails wouldn't go through if the backend has been running since a long time.
We'll fix this bug in the next post
Top comments (0)