399 lines
12 KiB
React
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|