diff --git a/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx b/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx index 2989c6d..63f6e7d 100644 --- a/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx +++ b/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx @@ -1,4 +1,4 @@ -import React from 'react' +import React from "react"; import { GlobalContext } from "@/globalContext"; import { callCustomAPI } from "@/utils/callCustomAPI"; import { Dialog, Transition } from "@headlessui/react"; @@ -8,19 +8,28 @@ import { useState } from "react"; import { Fragment } from "react"; import MkdSDK from "@/utils/MkdSDK"; -const PrivacyAndPolicyModal = ({ isOpen, closeModal }) => { +const PrivacyAndPolicyModal = ({ isOpen, closeModal, onReadToEnd }) => { const [privacy, setPrivacy] = useState(""); const { dispatch: globalDispatch } = useContext(GlobalContext); + const [hasReadToEnd, setHasReadToEnd] = useState(false); async function fetchPrivacyPolicy() { globalDispatch({ type: "START_LOADING" }); const sdk = new MkdSDK(); sdk.setTable("cms"); try { - const result = await callCustomAPI("cms", "post", { payload: { content_key: "privacy_policy" }, limit: 1000, page: 1 }, "PAGINATE"); + const result = await callCustomAPI( + "cms", + "post", + { payload: { content_key: "privacy_policy" }, limit: 1000, page: 1 }, + "PAGINATE" + ); if (Array.isArray(result.list) && result.list.length > 0) { - setPrivacy(result.list.find((stg) => stg.content_key == "privacy_policy")?.content_value); + setPrivacy( + result.list.find((stg) => stg.content_key == "privacy_policy") + ?.content_value + ); } } catch (err) { globalDispatch({ @@ -36,74 +45,79 @@ const PrivacyAndPolicyModal = ({ isOpen, closeModal }) => { useEffect(() => { fetchPrivacyPolicy(); - }, []); + setHasReadToEnd(false); + }, [isOpen]); + + function handleScroll(e) { + const { scrollTop, scrollHeight, clientHeight } = e.target; + if (!hasReadToEnd && scrollTop + clientHeight >= scrollHeight - 10) { + setHasReadToEnd(true); + if (onReadToEnd) onReadToEnd(); + } + } return ( <> -
+
- - - -
- + + + +
+ -
-
- - - - {" "} - {" "} - - -
-
-
-
-
+ {" "} + + +
+
+
+ + +
-
-
-
- - ) -} +
+
+ + ); +}; -export default PrivacyAndPolicyModal \ No newline at end of file +export default PrivacyAndPolicyModal; diff --git a/src/pages/Common/SignUp/SignUpDetailsForm.jsx b/src/pages/Common/SignUp/SignUpDetailsForm.jsx index 5b264c9..16394a5 100644 --- a/src/pages/Common/SignUp/SignUpDetailsForm.jsx +++ b/src/pages/Common/SignUp/SignUpDetailsForm.jsx @@ -16,6 +16,7 @@ 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(); @@ -28,6 +29,9 @@ export default function SignUpDetailsForm() { 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); @@ -36,11 +40,67 @@ export default function SignUpDetailsForm() { 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 m = now.getMonth() - dob.getMonth(); + if (m < 0 || (m === 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(), - lastName: yup.string(), - dob: yup.date(), - password: yup.string() + 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 { @@ -65,6 +125,34 @@ export default function SignUpDetailsForm() { 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]); + + // --- 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 { @@ -74,7 +162,15 @@ export default function SignUpDetailsForm() { // 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 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", @@ -85,11 +181,13 @@ export default function SignUpDetailsForm() { last_name: data.lastName, }, profile: { - dob: isSameDay(data.dob, initialDate.current) ? undefined : moment(data.dob).format("yyyy-MM-DD"), + dob: isSameDay(data.dob, initialDate.current) + ? undefined + : moment(data.dob).format("yyyy-MM-DD"), }, }, "", - result.token, + result.token ); localStorage.removeItem("token"); @@ -97,7 +195,7 @@ export default function SignUpDetailsForm() { authDispatch({ type: "ALLOW_CHECK_VERIFICATION" }); navigate("/check-verification"); localStorage.setItem("first_login", result.user_id); - setLoading(false); + setLoading(false); } else { setLoading(false); if (result.validation) { @@ -108,11 +206,9 @@ export default function SignUpDetailsForm() { type: "manual", message: result.validation[field], }); - } } } - } catch (err) { setLoading(false); setError("firstName", { @@ -126,122 +222,160 @@ export default function SignUpDetailsForm() { return ( <> -
+
-

Finish Signing Up

-
+

+ Finish Signing Up +

+
-

{errors.firstName?.message}

+

+ {errors.firstName?.message} +

-
+
-

{errors.lastName?.message}

+

+ {errors.lastName?.message} +

setValue("dob", v)} /> -
- { - trigger("password"); - }, - })} - className="flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none " - placeholder="Password" - />{" "} - +

+ {errors.dob?.message} +

+ +
0 && dirtyFields.password + ? "rounded-md border border-[#C42945]" + : "borde" + } relative mb-4 flex flex-col rounded-md bg-transparent`} + > +
+ { + trigger("password"); + }, + })} + className='flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none ' + placeholder='Password' + /> + +
+ {dirtyFields.password && passwordErrors.length > 0 && ( +
    + {passwordErrors.map((err, idx) => ( +
  • {err}
  • + ))} +
+ )}
- -

- Select and agree to {" "} +

+ Select and agree to{" "} - {" "} - to continue. - {" "} - {" "} + {" "} + Terms and Conditions + {" "} + to continue.{" "}

+ +
+ {" "} + {!agreedToTerms && ( + + You must agree to the Terms and Conditions. + + )} + {!privacyRead && ( + + You must read the Privacy Policy to the end. + + )} +
+ Continue
-
- - + 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' + >
+ setModalOpen(false)} + setIsAgreed={handleAgreeTerms} + /> + setPrivacyModalOpen(false)} + onReadToEnd={handlePrivacyRead} + /> ); } diff --git a/src/pages/Common/SignUp/TermsAndConditionsModal.jsx b/src/pages/Common/SignUp/TermsAndConditionsModal.jsx index 72363ef..3631634 100644 --- a/src/pages/Common/SignUp/TermsAndConditionsModal.jsx +++ b/src/pages/Common/SignUp/TermsAndConditionsModal.jsx @@ -7,14 +7,23 @@ import { useContext } from "react"; import { useState } from "react"; import { Fragment } from "react"; -export default function TermsAndConditionsModal({ isOpen, closeModal, setIsAgreed }) { +export default function TermsAndConditionsModal({ + isOpen, + closeModal, + setIsAgreed, +}) { const [termsAndConditions, setTermsAndCondition] = useState(""); const [agreed, setAgreed] = useState(false); const { dispatch: globalDispatch } = useContext(GlobalContext); async function fetchTermsAndConditions() { try { - const result = await callCustomAPI("cms", "post", { where: [`content_key = 'terms_and_conditions'`], limit: 1, page: 1 }, "PAGINATE"); + const result = await callCustomAPI( + "cms", + "post", + { where: [`content_key = 'terms_and_conditions'`], limit: 1, page: 1 }, + "PAGINATE" + ); if (Array.isArray(result.list) && result.list.length > 0) { setTermsAndCondition(result.list[0].content_value); @@ -35,73 +44,72 @@ export default function TermsAndConditionsModal({ isOpen, closeModal, setIsAgree }, []); return ( <> -
+
- - + + -
+
-
-
+
+
- + - {" "} {" "} -
+
-
+
{setAgreed((prev) => !prev); setIsAgreed((prev) => !prev); closeModal()}} + onChange={() => { + setAgreed((prev) => !prev); + setIsAgreed((prev) => !prev); + closeModal(); + }} />