ISSUE 11: add sign up validation. and ensure users open and read T & C and privacy policy
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { GlobalContext } from "@/globalContext";
|
import { GlobalContext } from "@/globalContext";
|
||||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
@@ -8,19 +8,28 @@ import { useState } from "react";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import MkdSDK from "@/utils/MkdSDK";
|
import MkdSDK from "@/utils/MkdSDK";
|
||||||
|
|
||||||
const PrivacyAndPolicyModal = ({ isOpen, closeModal }) => {
|
const PrivacyAndPolicyModal = ({ isOpen, closeModal, onReadToEnd }) => {
|
||||||
const [privacy, setPrivacy] = useState("");
|
const [privacy, setPrivacy] = useState("");
|
||||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||||
|
const [hasReadToEnd, setHasReadToEnd] = useState(false);
|
||||||
|
|
||||||
async function fetchPrivacyPolicy() {
|
async function fetchPrivacyPolicy() {
|
||||||
globalDispatch({ type: "START_LOADING" });
|
globalDispatch({ type: "START_LOADING" });
|
||||||
const sdk = new MkdSDK();
|
const sdk = new MkdSDK();
|
||||||
sdk.setTable("cms");
|
sdk.setTable("cms");
|
||||||
try {
|
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) {
|
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) {
|
} catch (err) {
|
||||||
globalDispatch({
|
globalDispatch({
|
||||||
@@ -36,74 +45,79 @@ const PrivacyAndPolicyModal = ({ isOpen, closeModal }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPrivacyPolicy();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`${isOpen ? "flex" : "hidden"} fixed inset-0 items-center justify-center`}></div>
|
<div
|
||||||
|
className={`${
|
||||||
|
isOpen ? "flex" : "hidden"
|
||||||
|
} fixed inset-0 items-center justify-center`}
|
||||||
|
></div>
|
||||||
|
|
||||||
<Transition
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
appear
|
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
||||||
show={isOpen}
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
>
|
enter='ease-out duration-300'
|
||||||
<Dialog
|
enterFrom='opacity-0'
|
||||||
as="div"
|
enterTo='opacity-100'
|
||||||
className="relative z-10"
|
leave='ease-in duration-200'
|
||||||
onClose={closeModal}
|
leaveFrom='opacity-100'
|
||||||
>
|
leaveTo='opacity-0'
|
||||||
<Transition.Child
|
>
|
||||||
as={Fragment}
|
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||||
enter="ease-out duration-300"
|
</Transition.Child>
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className='fixed inset-0 overflow-y-auto'>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom='opacity-0 scale-95'
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo='opacity-100 scale-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-900 flex justify-between items-center"
|
className='text-lg flex items-center justify-between font-medium leading-6 text-gray-900'
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="py-2 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full flex justify-end"
|
|
||||||
>
|
>
|
||||||
✕
|
{" "}
|
||||||
</button>
|
<button
|
||||||
</Dialog.Title>
|
type='button'
|
||||||
<div className="mt-2">
|
onClick={closeModal}
|
||||||
<article
|
className='flex justify-end rounded-full border px-3 py-2 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300'
|
||||||
className="sun-editor-editable text-sm max-h-[600px] overflow-y-auto my-8"
|
>
|
||||||
dangerouslySetInnerHTML={{ __html: privacy }}
|
✕
|
||||||
></article>
|
</button>
|
||||||
</div>
|
</Dialog.Title>
|
||||||
</Dialog.Panel>
|
<div className='mt-2'>
|
||||||
</Transition.Child>
|
<article
|
||||||
|
className='sun-editor-editable my-8 max-h-[600px] overflow-y-auto text-sm'
|
||||||
|
dangerouslySetInnerHTML={{ __html: privacy }}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
></article>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Dialog>
|
||||||
</Dialog>
|
</Transition>
|
||||||
</Transition>
|
</>
|
||||||
</>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default PrivacyAndPolicyModal
|
export default PrivacyAndPolicyModal;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import TermsAndConditionsModal from "./TermsAndConditionsModal";
|
|||||||
import DatePickerV2 from "@/components/frontend/DatePickerV2";
|
import DatePickerV2 from "@/components/frontend/DatePickerV2";
|
||||||
import { LoadingButton } from "@/components/frontend";
|
import { LoadingButton } from "@/components/frontend";
|
||||||
import PrivacyAndPolicyModal from "./PrivacyAndPolicyModal";
|
import PrivacyAndPolicyModal from "./PrivacyAndPolicyModal";
|
||||||
|
import commonPasswords from "@/assets/json/common-passwords.json";
|
||||||
|
|
||||||
export default function SignUpDetailsForm() {
|
export default function SignUpDetailsForm() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -28,6 +29,9 @@ export default function SignUpDetailsForm() {
|
|||||||
const [modalOpen, setModalOpen] = React.useState(false);
|
const [modalOpen, setModalOpen] = React.useState(false);
|
||||||
const [privacyOpen, setPrivacyModalOpen] = React.useState(false);
|
const [privacyOpen, setPrivacyModalOpen] = React.useState(false);
|
||||||
const initialDate = useRef(new Date());
|
const initialDate = useRef(new Date());
|
||||||
|
const [agreedToTerms, setAgreedToTerms] = React.useState(false);
|
||||||
|
const [privacyRead, setPrivacyRead] = React.useState(false);
|
||||||
|
const [passwordErrors, setPasswordErrors] = React.useState([]);
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
@@ -36,11 +40,67 @@ export default function SignUpDetailsForm() {
|
|||||||
setPrivacyModalOpen(false);
|
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({
|
const schema = yup.object({
|
||||||
firstName: yup.string(),
|
firstName: yup.string().required("First name is required."),
|
||||||
lastName: yup.string(),
|
lastName: yup.string().required("Last name is required."),
|
||||||
dob: yup.date(),
|
dob: yup
|
||||||
password: yup.string()
|
.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 {
|
const {
|
||||||
@@ -65,6 +125,34 @@ export default function SignUpDetailsForm() {
|
|||||||
|
|
||||||
const data = watch();
|
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() {
|
async function onSubmit() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -74,7 +162,15 @@ export default function SignUpDetailsForm() {
|
|||||||
|
|
||||||
// register device
|
// register device
|
||||||
sdk.setTable("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(
|
await callCustomAPI(
|
||||||
"edit-self",
|
"edit-self",
|
||||||
@@ -85,11 +181,13 @@ export default function SignUpDetailsForm() {
|
|||||||
last_name: data.lastName,
|
last_name: data.lastName,
|
||||||
},
|
},
|
||||||
profile: {
|
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");
|
localStorage.removeItem("token");
|
||||||
@@ -97,7 +195,7 @@ export default function SignUpDetailsForm() {
|
|||||||
authDispatch({ type: "ALLOW_CHECK_VERIFICATION" });
|
authDispatch({ type: "ALLOW_CHECK_VERIFICATION" });
|
||||||
navigate("/check-verification");
|
navigate("/check-verification");
|
||||||
localStorage.setItem("first_login", result.user_id);
|
localStorage.setItem("first_login", result.user_id);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (result.validation) {
|
if (result.validation) {
|
||||||
@@ -108,11 +206,9 @@ export default function SignUpDetailsForm() {
|
|||||||
type: "manual",
|
type: "manual",
|
||||||
message: result.validation[field],
|
message: result.validation[field],
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError("firstName", {
|
setError("firstName", {
|
||||||
@@ -126,122 +222,160 @@ export default function SignUpDetailsForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-col items-center justify-center bg-white md:w-1/2">
|
<section className='flex flex-col items-center justify-center bg-white md:w-1/2'>
|
||||||
<form
|
<form
|
||||||
className="flex w-full max-w-md flex-col px-6"
|
className='flex w-full max-w-md flex-col px-6'
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
>
|
>
|
||||||
<h1 className="mb-8 text-center text-5xl font-bold">Finish Signing Up</h1>
|
<h1 className='mb-8 text-center text-5xl font-bold'>
|
||||||
<div className="mb-8">
|
Finish Signing Up
|
||||||
|
</h1>
|
||||||
|
<div className='mb-8'>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
{...register("firstName")}
|
{...register("firstName")}
|
||||||
className="w-full resize-none rounded-md border bg-transparent py-2 px-4 focus:outline-none active:outline-none"
|
className='w-full resize-none rounded-md border bg-transparent px-4 py-2 focus:outline-none active:outline-none'
|
||||||
placeholder="First name"
|
placeholder='First name'
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<p className="text-red-500 text-xs italic mt-2 block">{errors.firstName?.message}</p>
|
<p className='mt-2 block text-xs italic text-red-500'>
|
||||||
|
{errors.firstName?.message}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className='mb-8'>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
{...register("lastName")}
|
{...register("lastName")}
|
||||||
className="w-full resize-none rounded-md border bg-transparent py-2 px-4 focus:outline-none active:outline-none"
|
className='w-full resize-none rounded-md border bg-transparent px-4 py-2 focus:outline-none active:outline-none'
|
||||||
placeholder="Last name"
|
placeholder='Last name'
|
||||||
autoComplete="off"
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
<p className="text-red-500 text-xs italic mt-2 block">{errors.lastName?.message}</p>
|
<p className='mt-2 block text-xs italic text-red-500'>
|
||||||
|
{errors.lastName?.message}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DatePickerV2
|
<DatePickerV2
|
||||||
control={control}
|
control={control}
|
||||||
name="dob"
|
name='dob'
|
||||||
min={new Date("1950-01-01")}
|
min={new Date("1950-01-01")}
|
||||||
max={initialDate.current}
|
max={initialDate.current}
|
||||||
setValue={(v) => setValue("dob", v)}
|
setValue={(v) => setValue("dob", v)}
|
||||||
/>
|
/>
|
||||||
<div className={`${errors.password?.message && dirtyFields.password ? "border rounded-md border-[#C42945]" : "borde"} relative mb-4 flex justify-between rounded-md bg-transparent`}>
|
<p className='mt-2 block text-xs italic text-red-500'>
|
||||||
<input
|
{errors.dob?.message}
|
||||||
autoComplete={showPassword ? "off" : "new-password"}
|
</p>
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
{...register("password", {
|
<div
|
||||||
onChange: () => {
|
className={`${
|
||||||
trigger("password");
|
passwordErrors.length > 0 && dirtyFields.password
|
||||||
},
|
? "rounded-md border border-[#C42945]"
|
||||||
})}
|
: "borde"
|
||||||
className="flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none "
|
} relative mb-4 flex flex-col rounded-md bg-transparent`}
|
||||||
placeholder="Password"
|
>
|
||||||
/>{" "}
|
<div className='flex items-center justify-between'>
|
||||||
<button
|
<input
|
||||||
type="button"
|
autoComplete={showPassword ? "off" : "new-password"}
|
||||||
onClick={() => setShowPassword((prev) => !prev)}
|
type={showPassword ? "text" : "password"}
|
||||||
className="absolute right-1 top-[20%]"
|
{...register("password", {
|
||||||
>
|
onChange: () => {
|
||||||
{" "}
|
trigger("password");
|
||||||
{showPassword ? (
|
},
|
||||||
<img
|
})}
|
||||||
src="/show.png"
|
className='flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none '
|
||||||
alt=""
|
placeholder='Password'
|
||||||
className="mr-2 w-6"
|
/>
|
||||||
/>
|
<button
|
||||||
) : (
|
type='button'
|
||||||
<img
|
onClick={() => setShowPassword((prev) => !prev)}
|
||||||
src="/invisible.png"
|
className='absolute right-1 top-[20%]'
|
||||||
alt=""
|
>
|
||||||
className="mr-2 w-6"
|
{showPassword ? (
|
||||||
/>
|
<img src='/show.png' alt='' className='mr-2 w-6' />
|
||||||
)}
|
) : (
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<p className='mb-4 text-sm normal-case text-gray-500'>
|
||||||
<p className="mb-4 text-sm normal-case text-gray-500">
|
Select and agree to{" "}
|
||||||
Select and agree to {" "}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type='button'
|
||||||
onClick={() =>setModalOpen(true)}
|
onClick={() => setModalOpen(true)}
|
||||||
className="underline"
|
className='underline'
|
||||||
// target={"_blank"}
|
// target={"_blank"}
|
||||||
>
|
>
|
||||||
{" "} Terms and Conditions
|
{" "}
|
||||||
</button>
|
Terms and Conditions
|
||||||
{" "}
|
</button>{" "}
|
||||||
to continue.
|
to continue.{" "}
|
||||||
{" "}
|
|
||||||
{" "}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type='button'
|
||||||
onClick={() =>setPrivacyModalOpen(true)}
|
onClick={() => setPrivacyModalOpen(true)}
|
||||||
className="underline"
|
className='underline'
|
||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</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
|
<LoadingButton
|
||||||
loading={loading}
|
loading={loading}
|
||||||
type="submit"
|
type='submit'
|
||||||
className={`disabled:cursor-not-allowed login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"}`}
|
className={`login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none disabled:cursor-not-allowed ${
|
||||||
// disabled={!recaptchaValidated}
|
loading ? "py-1" : "py-2"
|
||||||
|
}`}
|
||||||
|
disabled={!canSubmit}
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
style={{ backgroundImage: `url(${role == "host" ? "/host-sign-up.jpg" : "/sign-up-bg.jpg"})`, backgroundSize: "cover", backgroundRepeat: "no-repeat", backgroundPosition: "center" }}
|
style={{
|
||||||
className="hidden w-1/2 md:block"
|
backgroundImage: `url(${
|
||||||
>
|
role == "host" ? "/host-sign-up.jpg" : "/sign-up-bg.jpg"
|
||||||
</section>
|
})`,
|
||||||
<TermsAndConditionsModal
|
backgroundSize: "cover",
|
||||||
isOpen={modalOpen}
|
backgroundRepeat: "no-repeat",
|
||||||
closeModal={closeModal}
|
backgroundPosition: "center",
|
||||||
/>
|
}}
|
||||||
<PrivacyAndPolicyModal
|
className='hidden w-1/2 md:block'
|
||||||
isOpen={privacyOpen}
|
></section>
|
||||||
closeModal={closePrivacyModal}
|
<TermsAndConditionsModal
|
||||||
/>
|
isOpen={modalOpen}
|
||||||
|
closeModal={() => setModalOpen(false)}
|
||||||
|
setIsAgreed={handleAgreeTerms}
|
||||||
|
/>
|
||||||
|
<PrivacyAndPolicyModal
|
||||||
|
isOpen={privacyOpen}
|
||||||
|
closeModal={() => setPrivacyModalOpen(false)}
|
||||||
|
onReadToEnd={handlePrivacyRead}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,23 @@ import { useContext } from "react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Fragment } 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 [termsAndConditions, setTermsAndCondition] = useState("");
|
||||||
const [agreed, setAgreed] = useState(false);
|
const [agreed, setAgreed] = useState(false);
|
||||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||||
|
|
||||||
async function fetchTermsAndConditions() {
|
async function fetchTermsAndConditions() {
|
||||||
try {
|
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) {
|
if (Array.isArray(result.list) && result.list.length > 0) {
|
||||||
setTermsAndCondition(result.list[0].content_value);
|
setTermsAndCondition(result.list[0].content_value);
|
||||||
@@ -35,73 +44,72 @@ export default function TermsAndConditionsModal({ isOpen, closeModal, setIsAgree
|
|||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`${isOpen ? "flex" : "hidden"} fixed inset-0 items-center justify-center`}></div>
|
<div
|
||||||
|
className={`${
|
||||||
|
isOpen ? "flex" : "hidden"
|
||||||
|
} fixed inset-0 items-center justify-center`}
|
||||||
|
></div>
|
||||||
|
|
||||||
<Transition
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
appear
|
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
||||||
show={isOpen}
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
className="relative z-10"
|
|
||||||
onClose={closeModal}
|
|
||||||
>
|
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0"
|
enterFrom='opacity-0'
|
||||||
enterTo="opacity-100"
|
enterTo='opacity-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100"
|
leaveFrom='opacity-100'
|
||||||
leaveTo="opacity-0"
|
leaveTo='opacity-0'
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className='fixed inset-0 overflow-y-auto'>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom='opacity-0 scale-95'
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo='opacity-100 scale-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-6xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-900 flex justify-between items-center"
|
className='text-lg flex items-center justify-between font-medium leading-6 text-gray-900'
|
||||||
>
|
>
|
||||||
{" "}
|
|
||||||
{" "}
|
{" "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type='button'
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
className="py-2 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full flex justify-end"
|
className='flex justify-end rounded-full border px-3 py-2 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300'
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className='mt-2'>
|
||||||
<article
|
<article
|
||||||
className="sun-editor-editable text-sm max-h-[600px] overflow-y-auto my-8"
|
className='sun-editor-editable my-8 max-h-[600px] overflow-y-auto text-sm'
|
||||||
dangerouslySetInnerHTML={{ __html: termsAndConditions }}
|
dangerouslySetInnerHTML={{ __html: termsAndConditions }}
|
||||||
></article>
|
></article>
|
||||||
</div>
|
</div>
|
||||||
<div className="checkbox-container">
|
<div className='checkbox-container'>
|
||||||
<input
|
<input
|
||||||
type={"checkbox"}
|
type={"checkbox"}
|
||||||
name="i-agree"
|
name='i-agree'
|
||||||
id="i-agree"
|
id='i-agree'
|
||||||
checked={agreed}
|
checked={agreed}
|
||||||
onChange={() => {setAgreed((prev) => !prev); setIsAgreed((prev) => !prev); closeModal()}}
|
onChange={() => {
|
||||||
|
setAgreed((prev) => !prev);
|
||||||
|
setIsAgreed((prev) => !prev);
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="i-agree"
|
htmlFor='i-agree'
|
||||||
className="items-center cursor-pointer remove-select"
|
className='remove-select cursor-pointer items-center'
|
||||||
>
|
>
|
||||||
Yeah, I agree to everything
|
Yeah, I agree to everything
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user