Files
Ergo/src/pages/Common/SignUp/SignUpDetailsForm.jsx
T

399 lines
12 KiB
React

import React from "react";
import { Navigate, useNavigate } from "react-router";
import { useSignUpContext } from "./signUpContext";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { AuthContext } from "@/authContext";
import MkdSDK from "@/utils/MkdSDK";
import { Link } from "react-router-dom";
import { callCustomAPI } from "@/utils/callCustomAPI";
import { useRef } from "react";
import { isSameDay } from "@/utils/date-time-utils";
import moment from "moment/moment";
import TermsAndConditionsModal from "./TermsAndConditionsModal";
import DatePickerV2 from "@/components/frontend/DatePickerV2";
import { LoadingButton } from "@/components/frontend";
import PrivacyAndPolicyModal from "./PrivacyAndPolicyModal";
import commonPasswords from "@/assets/json/common-passwords.json";
export default function SignUpDetailsForm() {
const navigate = useNavigate();
const { signUpData } = useSignUpContext();
const role = signUpData.role;
const { dispatch: authDispatch } = React.useContext(AuthContext);
const [showPassword, setShowPassword] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const sdk = new MkdSDK();
const [modalOpen, setModalOpen] = React.useState(false);
const [privacyOpen, setPrivacyModalOpen] = React.useState(false);
const initialDate = useRef(new Date());
const [agreedToTerms, setAgreedToTerms] = React.useState(false);
const [privacyRead, setPrivacyRead] = React.useState(false);
const [passwordErrors, setPasswordErrors] = React.useState([]);
function closeModal() {
setModalOpen(false);
}
function closePrivacyModal() {
setPrivacyModalOpen(false);
}
// --- DOB must be at least 18 years ---
function isAtLeast18YearsOld(date) {
if (!date) return false;
const now = new Date();
const dob = new Date(date);
let age = now.getFullYear() - dob.getFullYear();
const monthDiff = now.getMonth() - dob.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < dob.getDate())) {
age--;
}
return age >= 18;
}
// --- Password validation ---
function validatePassword(password, firstName, lastName, dob) {
const errors = [];
if (!password) errors.push("Password is required.");
if (password && password.length < 10)
errors.push("Password must be at least 10 characters long.");
if (password && !/[a-z]/.test(password))
errors.push("Password must contain at least one lowercase letter.");
if (password && !/[A-Z]/.test(password))
errors.push("Password must contain at least one uppercase letter.");
if (password && !/[0-9]/.test(password))
errors.push("Password must contain at least one digit.");
if (password && !/[^A-Za-z0-9]/.test(password))
errors.push("Password must contain at least one symbol.");
if (
password &&
firstName &&
password.toLowerCase().includes(firstName.toLowerCase())
)
errors.push("Password must not contain your first name.");
if (
password &&
lastName &&
password.toLowerCase().includes(lastName.toLowerCase())
)
errors.push("Password must not contain your last name.");
if (password && dob) {
const dobStr = new Date(dob).toISOString().slice(0, 10).replace(/-/g, "");
if (dobStr && password.includes(dobStr))
errors.push("Password must not contain your date of birth.");
}
if (
password &&
commonPasswords.some((w) => password.toLowerCase().includes(w))
)
errors.push("Password must not contain common password words.");
return errors;
}
// --- Validation schema ---
const schema = yup.object({
firstName: yup.string().required("First name is required."),
lastName: yup.string().required("Last name is required."),
dob: yup
.date()
.required("Date of birth is required.")
.test("age", "You must be at least 18 years old.", isAtLeast18YearsOld),
password: yup.string().required("Password is required."),
});
const {
register,
setError,
handleSubmit,
trigger,
watch,
setValue,
control,
formState: { errors, dirtyFields },
} = useForm({
resolver: yupResolver(schema),
defaultValues: {
firstName: signUpData.firstName,
lastName: signUpData.lastName,
dob: initialDate.current,
password: signUpData.password,
},
criteriaMode: "all",
});
const data = watch();
// --- Password error display ---
React.useEffect(() => {
setPasswordErrors(
validatePassword(data.password, data.firstName, data.lastName, data.dob)
);
}, [data.password, data.firstName, data.lastName, data.dob]);
// --- Trigger DOB validation when date changes ---
React.useEffect(() => {
if (data.dob && !isSameDay(data.dob, initialDate.current)) {
trigger("dob");
}
}, [data.dob, trigger]);
// --- Terms and Privacy Modal handlers ---
function handleAgreeTerms() {
setAgreedToTerms(true);
setModalOpen(false);
}
function handlePrivacyRead() {
setPrivacyRead(true);
setPrivacyModalOpen(false);
}
// --- Disable submit logic ---
const canSubmit =
data.firstName &&
data.lastName &&
data.dob &&
isAtLeast18YearsOld(data.dob) &&
data.password &&
passwordErrors.length === 0 &&
agreedToTerms &&
privacyRead;
async function onSubmit() {
setLoading(true);
try {
const result = await sdk.register(signUpData.email, data.password, role);
if (!result.error) {
localStorage.setItem("token", result.token);
// register device
sdk.setTable("device");
await sdk.callRestAPI(
{
active: 1,
user_id: result.user_id,
last_login_time: new Date().toISOString().split("T")[0],
uid: localStorage.getItem("device-uid"),
},
"POST"
);
await callCustomAPI(
"edit-self",
"post",
{
user: {
first_name: data.firstName,
last_name: data.lastName,
},
profile: {
dob: isSameDay(data.dob, initialDate.current)
? undefined
: moment(data.dob).format("yyyy-MM-DD"),
},
},
"",
result.token
);
localStorage.removeItem("token");
authDispatch({ type: "ALLOW_CHECK_VERIFICATION" });
navigate("/check-verification");
localStorage.setItem("first_login", result.user_id);
setLoading(false);
} else {
setLoading(false);
if (result.validation) {
const keys = Object.keys(result.validation);
for (let i = 0; i < keys.length; i++) {
const field = keys[i];
setError(field, {
type: "manual",
message: result.validation[field],
});
}
}
}
} catch (err) {
setLoading(false);
setError("firstName", {
type: "manual",
message: err.message,
});
}
}
if (!signUpData.email) return <Navigate to={`/signup`} />;
return (
<>
<section className='flex flex-col items-center justify-center bg-white md:w-1/2'>
<form
className='flex w-full max-w-md flex-col px-6'
onSubmit={handleSubmit(onSubmit)}
autoComplete='off'
>
<h1 className='mb-8 text-center text-5xl font-bold'>
Finish Signing Up
</h1>
<div className='mb-8'>
<input
type='text'
{...register("firstName")}
className='w-full resize-none rounded-md border bg-transparent px-4 py-2 focus:outline-none active:outline-none'
placeholder='First name'
autoComplete='off'
/>
<p className='mt-2 block text-xs italic text-red-500'>
{errors.firstName?.message}
</p>
</div>
<div className='mb-8'>
<input
type='text'
{...register("lastName")}
className='w-full resize-none rounded-md border bg-transparent px-4 py-2 focus:outline-none active:outline-none'
placeholder='Last name'
autoComplete='off'
/>
<p className='mt-2 block text-xs italic text-red-500'>
{errors.lastName?.message}
</p>
</div>
<div className='flex flex-col'>
<DatePickerV2
control={control}
name='dob'
min={new Date("1950-01-01")}
max={initialDate.current}
setValue={(v) => {
setValue("dob", v);
trigger("dob");
}}
/>
{errors.dob && (
<p className='mb-1 block text-xs italic text-red-500'>
{errors.dob.message}
</p>
)}
</div>
<div
className={`${
passwordErrors.length > 0 && dirtyFields.password
? "rounded-md border border-[#C42945]"
: "borde"
} relative mb-4 flex flex-col rounded-md bg-transparent`}
>
<div className='flex items-center justify-between'>
<input
autoComplete={showPassword ? "off" : "new-password"}
type={showPassword ? "text" : "password"}
{...register("password", {
onChange: () => {
trigger("password");
},
})}
className='flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none '
placeholder='Password'
/>
<button
type='button'
onClick={() => setShowPassword((prev) => !prev)}
className='absolute right-1 top-[20%]'
>
{showPassword ? (
<img src='/show.png' alt='' className='mr-2 w-6' />
) : (
<img src='/invisible.png' alt='' className='mr-2 w-6' />
)}
</button>
</div>
{dirtyFields.password && passwordErrors.length > 0 && (
<ul className='ml-6 mt-2 list-disc text-xs text-red-500'>
{passwordErrors.map((err, idx) => (
<li key={idx}>{err}</li>
))}
</ul>
)}
</div>
<p className='mb-4 text-sm normal-case text-gray-500'>
Select and agree to{" "}
<button
type='button'
onClick={() => setModalOpen(true)}
className='underline'
// target={"_blank"}
>
{" "}
Terms and Conditions
</button>{" "}
to continue.{" "}
<button
type='button'
onClick={() => setPrivacyModalOpen(true)}
className='underline'
>
Privacy Policy
</button>
</p>
<div className='mb-1 flex flex-col'>
{" "}
{!agreedToTerms && (
<span className='text-xs text-red-500'>
You must agree to the Terms and Conditions.
</span>
)}
{!privacyRead && (
<span className='text-xs text-red-500'>
You must read the Privacy Policy to the end.
</span>
)}
</div>
<LoadingButton
loading={loading}
type='submit'
className={`login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none disabled:cursor-not-allowed ${
loading ? "py-1" : "py-2"
}`}
disabled={!canSubmit}
>
Continue
</LoadingButton>
</form>
</section>
<section
style={{
backgroundImage: `url(${
role == "host" ? "/host-sign-up.jpg" : "/sign-up-bg.jpg"
})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
}}
className='hidden w-1/2 md:block'
></section>
<TermsAndConditionsModal
isOpen={modalOpen}
closeModal={() => setModalOpen(false)}
setIsAgreed={handleAgreeTerms}
/>
<PrivacyAndPolicyModal
isOpen={privacyOpen}
closeModal={() => setPrivacyModalOpen(false)}
onReadToEnd={handlePrivacyRead}
/>
</>
);
}