Compare commits

...

8 Commits

14 changed files with 3389 additions and 2187 deletions
+33 -16
View File
@@ -9,28 +9,45 @@ export default function ConfirmationModal() {
if (!state.confirmation) return null; if (!state.confirmation) return null;
return ( return (
<div className={"popup-container z-100 flex items-center justify-center normal-case"}> <div
className={
"popup-container z-100 flex items-center justify-center normal-case"
}
>
<div <div
className={`${state.confirmation ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`} className={`${
state.confirmation ? "pop-in" : "pop-out"
} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<h2 className="mb-4 text-3xl font-semibold"> <h2 className='mb-4 text-3xl font-semibold'>
<GreenCheckIcon /> <GreenCheckIcon />
{state.confirmationHeading} {state.confirmationHeading}
</h2> </h2>
<p className="mb-4 text-sm text-gray-500">{state.confirmationMsg}</p> <p className='mb-4 text-sm text-gray-500'>{state.confirmationMsg}</p>
<button <div className='flex gap-4 *:w-1/2'>
type="button" <button
className="login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none" type='button'
onClick={() => { onClick={() => {
if (state.confirmationCloseFn) { dispatch({ type: "CLOSE_CONFIRMATION" });
state.confirmationCloseFn(); }}
} className='mt-4 w-full rounded border-2 border-gray-300 py-2 tracking-wide text-black outline-none focus:outline-none'
dispatch({ type: "CLOSE_CONFIRMATION" }); >
}} Cancel
> </button>
{state.confirmationBtn} <button
</button> type='button'
className='login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none'
onClick={() => {
if (state.confirmationCloseFn) {
state.confirmationCloseFn();
}
dispatch({ type: "CLOSE_CONFIRMATION" });
}}
>
{state.confirmationBtn}
</button>
</div>
</div> </div>
</div> </div>
); );
@@ -5,37 +5,51 @@ import { useController } from "react-hook-form";
import LocationIcon from "./frontend/icons/LocationIcon"; import LocationIcon from "./frontend/icons/LocationIcon";
import { GlobalContext } from "@/globalContext"; import { GlobalContext } from "@/globalContext";
export default function CustomStaticLocationAutoCompleteV2({ type, control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) { export default function CustomStaticLocationAutoCompleteV2({
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); type,
control,
name,
setValue,
onClear,
className,
containerClassName,
hideIcons,
suggestionType,
...restProps
}) {
const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const [location, setLocation] = useState(globalState.location); const [location, setLocation] = useState(globalState.location);
const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } =
const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({ usePlacesService({
apiKey: import.meta.env.VITE_GOOGLE_API_KEY, apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
options: { types: suggestionType ?? ["(region)"] }, options: { types: suggestionType ?? ["(region)"] },
debounce: 200, debounce: 200,
}); });
return ( return (
<Combobox <Combobox
as={"div"} as={"div"}
className={`relative w-full normal-case z-100 ${containerClassName ?? ""}`} className={`z-100 relative w-full normal-case ${
containerClassName ?? ""
}`}
value={location} value={location}
> >
{!hideIcons && <LocationIcon />} {!hideIcons && <LocationIcon />}
<Combobox.Input <Combobox.Input
{...restProps} {...restProps}
autoComplete="off" autoComplete='off'
className={`w-full truncate text-black ${className ?? ""}`} className={`w-full truncate text-black ${className ?? ""}`}
value={globalState.location} value={location}
onChange={(evt) => { onChange={(evt) => {
setLocation(evt.target.value) setLocation(evt.target.value);
getPlacePredictions({ input: evt.target.value }); getPlacePredictions({ input: evt.target.value });
}} }}
/> />
{!hideIcons && globalState.location && ( {!hideIcons && globalState.location && (
<button <button
type="button" type='button'
onClick={() => { onClick={() => {
setValue(""); setValue("");
setLocation(""); setLocation("");
@@ -49,56 +63,72 @@ export default function CustomStaticLocationAutoCompleteV2({ type, control, name
)} )}
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter='transition ease-out duration-100'
enterFrom="transform opacity-0 scale-95" enterFrom='transform opacity-0 scale-95'
enterTo="transform opacity-100 scale-100" enterTo='transform opacity-100 scale-100'
leave="transition ease-in duration-75" leave='transition ease-in duration-75'
leaveFrom="transform opacity-100 scale-100" leaveFrom='transform opacity-100 scale-100'
leaveTo="transform opacity-0 scale-95" leaveTo='transform opacity-0 scale-95'
> >
{isPlacePredictionsLoading ? ( {isPlacePredictionsLoading ? (
<div className="absolute left-0 right-0 top-full z-50 mt-2 flex w-full origin-top justify-center rounded-xl border bg-white py-8"> <div className='absolute left-0 right-0 top-full z-50 mt-2 flex w-full origin-top justify-center rounded-xl border bg-white py-8'>
<svg <svg
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }} style={{
width="36px" margin: "auto",
height="36px" background: "none",
viewBox="0 0 100 100" display: "block",
preserveAspectRatio="xMidYMid" shapeRendering: "auto",
}}
width='36px'
height='36px'
viewBox='0 0 100 100'
preserveAspectRatio='xMidYMid'
> >
<path <path
fill="none" fill='none'
stroke="#d0d5dd" stroke='#d0d5dd'
strokeWidth="10" strokeWidth='10'
strokeDasharray="42.76482137044271 42.76482137044271" strokeDasharray='42.76482137044271 42.76482137044271'
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z" d='M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z'
strokeLinecap="round" strokeLinecap='round'
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }} style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
> >
<animate <animate
attributeName="stroke-dashoffset" attributeName='stroke-dashoffset'
repeatCount="indefinite" repeatCount='indefinite'
dur="1.6666666666666667s" dur='1.6666666666666667s'
keyTimes="0;1" keyTimes='0;1'
values="0;256.58892822265625" values='0;256.58892822265625'
></animate> ></animate>
</path> </path>
</svg> </svg>
</div> </div>
) : ( ) : (
<Combobox.Options <Combobox.Options
className={`${placePredictions.length > 0 ? "py-2 shadow-lg ring-1" : "" className={`${
} absolute left-0 right-0 top-full z-50 mt-2 w-full origin-top cursor-pointer divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`} placePredictions.length > 0 ? "py-2 shadow-lg ring-1" : ""
} absolute left-0 right-0 top-full z-50 mt-2 w-full origin-top cursor-pointer divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`}
> >
{placePredictions.map((place, idx) => ( {placePredictions.map((place, idx) => (
<Combobox.Option <Combobox.Option
className="flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800" className='flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800'
key={idx} key={idx}
value={place.structured_formatting.main_text} value={place.structured_formatting.main_text}
onClick={() => onClick={() =>
setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text) setValue(
place?.structured_formatting.main_text +
", " +
place.structured_formatting?.secondary_text
)
} }
> >
<span>{`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`}</span> <span>{`${place.structured_formatting.main_text} ${
place.structured_formatting?.secondary_text ? "," : ""
} ${
place.structured_formatting?.secondary_text
? place.structured_formatting?.secondary_text
: ""
}`}</span>
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>
@@ -0,0 +1,62 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react";
export default function ProfileImageConfirmModal({
modalOpen,
modalImage,
onConfirm,
onCancel,
}) {
return (
<Transition appear show={modalOpen} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={onCancel}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
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='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-sm transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all'>
<img
src={modalImage || "/default.png"}
alt='Profile Preview'
className='mb-4 h-64 w-full rounded object-cover'
/>
<div className='flex justify-center gap-4'>
<button
className='login-btn-gradient rounded px-4 py-2 text-white'
onClick={onConfirm}
>
Confirm
</button>
<button
className='rounded bg-gray-300 px-4 py-2 text-black'
onClick={onCancel}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
+1412 -1276
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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"
> >
&#x2715; {" "}
</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 }} &#x2715;
></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;
+225 -91
View File
@@ -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}
/>
</> </>
); );
} }
+73 -38
View File
@@ -8,6 +8,7 @@ import { useSignUpContext } from "./signUpContext";
import { callCustomAPI, oauthLoginApi } from "@/utils/callCustomAPI"; import { callCustomAPI, oauthLoginApi } from "@/utils/callCustomAPI";
import { LoadingButton } from "@/components/frontend"; import { LoadingButton } from "@/components/frontend";
import TLDs from "@/assets/json/email-tlds.json"; import TLDs from "@/assets/json/email-tlds.json";
import ReCAPTCHA from "react-google-recaptcha";
const SignUpForm = () => { const SignUpForm = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -15,9 +16,18 @@ const SignUpForm = () => {
const role = signUpData.role; const role = signUpData.role;
const schema = yup.object({ const schema = yup.object({
email: yup email: yup
.string(), .string()
.required("Email is required")
.email("Invalid email address")
.test("tld-check", "Invalid email TLD", (value) => {
if (!value) return false;
const tld = value.split(".").pop();
return TLDs.includes(tld);
}),
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [recaptchaValue, setRecaptchaValue] = useState(null);
const [recaptchaError, setRecaptchaError] = useState("");
const { const {
register, register,
@@ -32,12 +42,20 @@ const SignUpForm = () => {
}); });
const onSubmit = async (data) => { const onSubmit = async (data) => {
setRecaptchaError("");
if (!recaptchaValue) {
setRecaptchaError("Please complete the recaptcha.");
return;
}
setLoading(true); setLoading(true);
try { try {
const result = await callCustomAPI("email-exist", "post", { email: data.email }, ""); const result = await callCustomAPI(
"email-exist",
"post",
{ email: data.email },
""
);
if (result.error || result.exist) throw new Error("User already exists"); if (result.error || result.exist) throw new Error("User already exists");
dispatch({ type: "SET_EMAIL", payload: data.email }); dispatch({ type: "SET_EMAIL", payload: data.email });
navigate("/signup/details" + "?role=" + role); navigate("/signup/details" + "?role=" + role);
} catch (err) { } catch (err) {
@@ -61,77 +79,87 @@ const SignUpForm = () => {
window.open(result.data, "_self"); window.open(result.data, "_self");
}; };
if (!signUpData.role) return <Navigate to={"/signup/select-role"} />; if (!signUpData.role) return <Navigate to={"/signup/select-role"} />;
return ( return (
<> <>
<section className="flex w-full flex-col items-center justify-center bg-white md:w-1/2"> <section className='flex w-full 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-3xl font-semibold md:text-5xl md:font-bold">{role == "host" ? "Become a host" : "Sign up"}</h1> <h1 className='mb-8 text-center text-3xl font-semibold md:text-5xl md:font-bold'>
{role == "host" ? "Become a host" : "Sign up"}
</h1>
<input <input
autoComplete="off" autoComplete='off'
{...register("email")} {...register("email")}
type="text" type='text'
className="mb-8 resize-none rounded-sm border-2 bg-transparent p-2 px-4 focus:outline-none active:outline-none" className='mb-8 resize-none rounded-sm border-2 bg-transparent p-2 px-4 focus:outline-none active:outline-none'
placeholder="Email" placeholder='Email'
/> />
{Object.entries(errors).length > 0 ? ( {Object.entries(errors).length > 0 ? (
<p className="error-vibrate my-3 rounded-md border border-[#C42945] bg-white py-2 px-3 text-center text-sm normal-case text-[#C42945]">{Object.values(errors)[0].message}</p> <p className='error-vibrate my-3 rounded-md border border-[#C42945] bg-white px-3 py-2 text-center text-sm normal-case text-[#C42945]'>
{Object.values(errors)[0].message}
</p>
) : ( ) : (
<></> <></>
)} )}
<div className='mb-4 flex justify-center'>
<ReCAPTCHA
sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY}
onChange={(val) => {
setRecaptchaValue(val);
setRecaptchaError("");
}}
/>
</div>
{recaptchaError && (
<p className='mb-2 text-center text-xs italic text-red-500'>
{recaptchaError}
</p>
)}
<LoadingButton <LoadingButton
loading={loading} loading={loading}
type="submit" type='submit'
className={`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 ${
loading ? "py-1" : "py-2"
}`}
disabled={!recaptchaValue}
> >
Continue Continue
</LoadingButton> </LoadingButton>
</form> </form>
<div className="hr my-6 text-center">OR</div> <div className='hr my-6 text-center'>OR</div>
<div className="oauth flex w-full max-w-md flex-col gap-4 px-6 text-[#344054]"> <div className='oauth flex w-full max-w-md flex-col gap-4 px-6 text-[#344054]'>
<button <button
onClick={() => handleGoogleLogin()} onClick={() => handleGoogleLogin()}
className="flex items-center justify-center gap-2 border-2 py-[10px]" className='flex items-center justify-center gap-2 border-2 py-[10px]'
> >
<img <img src='/google-icon.png' className='h-[18px] w-[18px]' />
src="/google-icon.png"
className="h-[18px] w-[18px]"
/>
<span>Sign Up With Google</span> <span>Sign Up With Google</span>
</button> </button>
<button <button
onClick={() => handleFacebookLogin()} onClick={() => handleFacebookLogin()}
className="flex items-center justify-center gap-2 border-2 py-[10px]" className='flex items-center justify-center gap-2 border-2 py-[10px]'
> >
<img <img src='/facebook-icon.png' className='h-[16px] w-[16px]' />
src="/facebook-icon.png"
className="h-[16px] w-[16px]"
/>
<span>Sign Up With Facebook</span> <span>Sign Up With Facebook</span>
</button> </button>
<button <button
onClick={() => handleAppleLogin()} onClick={() => handleAppleLogin()}
className="flex items-center justify-center gap-2 border-2 py-[10px]" className='flex items-center justify-center gap-2 border-2 py-[10px]'
> >
<img <img src='/apple-icon.png' className='h-[16px] w-[16px]' />
src="/apple-icon.png"
className="h-[16px] w-[16px]"
/>
<span>Sign Up With Apple</span> <span>Sign Up With Apple</span>
</button> </button>
<div> <div>
<h3 className="text-center text-sm normal-case text-gray-800"> <h3 className='text-center text-sm normal-case text-gray-800'>
Already have an account?{" "} Already have an account?{" "}
<Link <Link
to={"/login" + "?role=" + role} to={"/login" + "?role=" + role}
className="my-text-gradient mb-8 self-end text-sm font-semibold" className='my-text-gradient mb-8 self-end text-sm font-semibold'
> >
Log In Log In
</Link>{" "} </Link>{" "}
@@ -140,8 +168,15 @@ const SignUpForm = () => {
</div> </div>
</section> </section>
<section <section
style={{ backgroundImage: `url(${role == "host" ? "/jumbotron1.jpg" : "/sign-up-bg.jpg"})`, backgroundSize: "cover", backgroundRepeat: "no-repeat", backgroundPosition: "center" }} style={{
className="hidden w-1/2 md:block bg-contain" backgroundImage: `url(${
role == "host" ? "/jumbotron1.jpg" : "/sign-up-bg.jpg"
})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
}}
className='hidden w-1/2 bg-contain md:block'
></section> ></section>
</> </>
); );
@@ -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'
> >
&#x2715; &#x2715;
</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>
@@ -8,7 +8,11 @@ import MkdSDK from "@/utils/MkdSDK";
import { callCustomAPI } from "@/utils/callCustomAPI"; import { callCustomAPI } from "@/utils/callCustomAPI";
import Skeleton from "react-loading-skeleton"; import Skeleton from "react-loading-skeleton";
import { formatDate } from "@/utils/date-time-utils"; import { formatDate } from "@/utils/date-time-utils";
import { IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; import {
IMAGE_STATUS,
NOTIFICATION_STATUS,
NOTIFICATION_TYPE,
} from "@/utils/constants";
import SwitchBulkMode from "@/components/SwitchBulkMode"; import SwitchBulkMode from "@/components/SwitchBulkMode";
import TwoFaDialog from "@/components/Profile/TwoFaDialog"; import TwoFaDialog from "@/components/Profile/TwoFaDialog";
import EditProfileModal from "@/components/Profile/EditProfileModal"; import EditProfileModal from "@/components/Profile/EditProfileModal";
@@ -18,6 +22,7 @@ import EditAboutModal from "@/components/Profile/EditAboutModal";
import { parseJsonSafely } from "@/utils/utils"; import { parseJsonSafely } from "@/utils/utils";
import EnableEmailDialog from "@/components/Profile/EnableEmailDialog"; import EnableEmailDialog from "@/components/Profile/EnableEmailDialog";
import DeleteAccountModal from "@/components/Profile/DeleteAccountModal"; import DeleteAccountModal from "@/components/Profile/DeleteAccountModal";
import ProfileImageConfirmModal from "@/components/Profile/ProfileImageConfirmModal";
function getProfilePhotoMessage(image_status) { function getProfilePhotoMessage(image_status) {
switch (image_status) { switch (image_status) {
@@ -33,7 +38,8 @@ function getProfilePhotoMessage(image_status) {
} }
export default function CustomerProfilePage() { export default function CustomerProfilePage() {
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [twoFa, setTwoFa] = useState(false); const [twoFa, setTwoFa] = useState(false);
const [twoFaDialog, setTwoFaDialog] = useState(false); const [twoFaDialog, setTwoFaDialog] = useState(false);
@@ -49,20 +55,29 @@ export default function CustomerProfilePage() {
const [deleteAccountModal, setDeleteAccountModal] = useState(false); const [deleteAccountModal, setDeleteAccountModal] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
const [selectedImage, setSelectedImage] = useState(null);
let sdk = new MkdSDK(); let sdk = new MkdSDK();
const changeProfilePic = async (e) => { const changeProfilePic = (e) => {
globalDispatch({ type: "START_LOADING" }); const file = e.target.files && e.target.files[0];
const file = e.target.files; if (file) {
const formData = new FormData(); setSelectedImage(file);
for (let i = 0; i < file.length; i++) { setShowImagePreview(true);
formData.append("file", file[i]);
} }
};
const handleConfirmUpload = async () => {
setShowImagePreview(false);
if (!selectedImage) return;
globalDispatch({ type: "START_LOADING" });
const formData = new FormData();
formData.append("file", selectedImage);
try { try {
const upload = await sdk.uploadImage(formData); const upload = await sdk.uploadImage(formData);
console.log("upload", upload);
sdk.setTable("user"); sdk.setTable("user");
const result = await callCustomAPI( await callCustomAPI(
"edit-self", "edit-self",
"post", "post",
{ {
@@ -71,9 +86,16 @@ export default function CustomerProfilePage() {
is_photo_approved: IMAGE_STATUS.IN_REVIEW, is_photo_approved: IMAGE_STATUS.IN_REVIEW,
}, },
}, },
"", ""
); );
globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: upload.url, is_photo_approved: IMAGE_STATUS.IN_REVIEW } }); globalDispatch({
type: "SET_USER_DATA",
payload: {
...globalState.user,
photo: upload.url,
is_photo_approved: IMAGE_STATUS.IN_REVIEW,
},
});
// create notification // create notification
sdk.setTable("notification"); sdk.setTable("notification");
await sdk.callRestAPI( await sdk.callRestAPI(
@@ -86,7 +108,7 @@ export default function CustomerProfilePage() {
type: NOTIFICATION_TYPE.EDIT_USER_PICTURE, type: NOTIFICATION_TYPE.EDIT_USER_PICTURE,
status: NOTIFICATION_STATUS.NOT_ADDRESSED, status: NOTIFICATION_STATUS.NOT_ADDRESSED,
}, },
"POST", "POST"
); );
} catch (err) { } catch (err) {
globalDispatch({ globalDispatch({
@@ -98,9 +120,15 @@ export default function CustomerProfilePage() {
}); });
} }
globalDispatch({ type: "STOP_LOADING" }); globalDispatch({ type: "STOP_LOADING" });
setSelectedImage(null);
}; };
const removeProfilePic = async (e) => { const handleCancelUpload = () => {
setShowImagePreview(false);
setSelectedImage(null);
};
const removeProfilePic = async () => {
try { try {
sdk.setTable("user"); sdk.setTable("user");
await callCustomAPI( await callCustomAPI(
@@ -112,9 +140,12 @@ export default function CustomerProfilePage() {
is_photo_approved: null, is_photo_approved: null,
}, },
}, },
"", ""
); );
globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: null, is_photo_approved: null } }); globalDispatch({
type: "SET_USER_DATA",
payload: { ...globalState.user, photo: null, is_photo_approved: null },
});
} catch (err) { } catch (err) {
globalDispatch({ globalDispatch({
type: "SHOW_ERROR", type: "SHOW_ERROR",
@@ -137,14 +168,16 @@ export default function CustomerProfilePage() {
two_factor_authentication: twoFa != 1 ? 1 : 0, two_factor_authentication: twoFa != 1 ? 1 : 0,
}, },
}, },
"", ""
); );
setTwoFaDialog(false); setTwoFaDialog(false);
globalDispatch({ globalDispatch({
type: "SHOW_CONFIRMATION", type: "SHOW_CONFIRMATION",
payload: { payload: {
heading: "Success", heading: "Success",
message: `Two factor Authentication ${twoFa == 1 ? "disabled" : "enabled"}`, message: `Two factor Authentication ${
twoFa == 1 ? "disabled" : "enabled"
}`,
btn: "Ok got it", btn: "Ok got it",
}, },
}); });
@@ -163,50 +196,66 @@ export default function CustomerProfilePage() {
} }
return ( return (
<div className="pt-[44px] pb-16 normal-case text-[#475467]"> <div className='pb-16 pt-[44px] normal-case text-[#475467]'>
<div className="flex flex-wrap-reverse justify-between "> <div className='flex flex-wrap-reverse justify-between '>
<div className="flex max-w-3xl flex-grow flex-col justify-between md:flex-row md:items-center"> <div className='flex max-w-3xl flex-grow flex-col justify-between md:flex-row md:items-center'>
<div className="mb-[16px] flex flex-col"> <div className='mb-[16px] flex flex-col'>
<h3 className="text-xl font-semibold">Your photo</h3> <h3 className='text-xl font-semibold'>Your photo</h3>
<small className="text-xs md:text-sm">{getProfilePhotoMessage(globalState.user.is_photo_approved)}</small> <small className='text-xs md:text-sm'>
{getProfilePhotoMessage(globalState.user.is_photo_approved)}
</small>
</div> </div>
<div <div
data-tour="photo-step" data-tour='photo-step'
className="flex items-center justify-between"> className='flex items-center justify-between'
>
<img <img
src={globalState.user.photo ?? "/default.png"} src={globalState.user.photo ?? "/default.png"}
alt="" alt=''
className="photo-step h-[56px] w-[56px] rounded-full object-cover md:mr-[65px] md:h-[64px] md:w-[64px]" className='photo-step h-[56px] w-[56px] rounded-full object-cover md:mr-[65px] md:h-[64px] md:w-[64px]'
/> />
<div> <div>
<label <label
className="third-step mr-3 cursor-pointer font-semibold underline" className='third-step mr-3 cursor-pointer font-semibold underline'
htmlFor="profilePic" htmlFor='profilePic'
> >
Update{" "} Update{" "}
<input <input
type="file" type='file'
className="hidden" className='hidden'
id="profilePic" id='profilePic'
onChange={changeProfilePic} onChange={changeProfilePic}
/> />
</label> </label>
<button <button
className="underline" className='underline'
onClick={removeProfilePic} onClick={() => {
globalDispatch({
type: "SHOW_CONFIRMATION",
payload: {
heading: "Remove Profile Picture?",
message:
"Are you sure you want to remove your profile picture?",
btn: "Yes, Remove",
onClose: () => {
removeProfilePic();
},
},
});
}}
> >
Remove Remove
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div className="mb-12 flex w-full justify-between md:mb-0 md:w-[unset] md:flex-col md:justify-start"> <div className='mb-12 flex w-full justify-between md:mb-0 md:w-[unset] md:flex-col md:justify-start'>
<p className="mb-2 self-end">Profile status</p> <p className='mb-2 self-end'>Profile status</p>
<div data-tour="fourth-step" className="flex fourth-step"> <div data-tour='fourth-step' className='fourth-step flex'>
{![0, 1].includes(globalState.user.verificationStatus) && ( {![0, 1].includes(globalState.user.verificationStatus) && (
<Link <Link
to="/account/verification" to='/account/verification'
className="mr-3 font-semibold text-[#1570EF]" className='mr-3 font-semibold text-[#1570EF]'
> >
Get verified Get verified
</Link> </Link>
@@ -214,7 +263,11 @@ export default function CustomerProfilePage() {
<button <button
className={ className={
`${globalState.user.verificationStatus == 1 ? "login-btn-gradient" : "bg-[#667085]"}` + `${
globalState.user.verificationStatus == 1
? "login-btn-gradient"
: "bg-[#667085]"
}` +
" flex min-w-[103px] items-center gap-1 rounded-md p-1 px-2 text-xs uppercase tracking-wider text-white" " flex min-w-[103px] items-center gap-1 rounded-md p-1 px-2 text-xs uppercase tracking-wider text-white"
} }
> >
@@ -255,7 +308,7 @@ export default function CustomerProfilePage() {
</div> </div>
</div> </div>
<hr className="my-[37px]" /> <hr className='my-[37px]' />
<EditProfileModal <EditProfileModal
closeModal={() => setUpdateName(false)} closeModal={() => setUpdateName(false)}
modalOpen={updateName} modalOpen={updateName}
@@ -284,6 +337,16 @@ export default function CustomerProfilePage() {
isOpen={enableEmailDialog} isOpen={enableEmailDialog}
closeModal={() => setEnableEmailDialog(false)} closeModal={() => setEnableEmailDialog(false)}
/> />
<ProfileImageConfirmModal
modalOpen={showImagePreview}
modalImage={
selectedImage instanceof File
? URL.createObjectURL(selectedImage)
: selectedImage
}
onConfirm={handleConfirmUpload}
onCancel={handleCancelUpload}
/>
</div> </div>
); );
} }
@@ -23,11 +23,13 @@ export default function CustomerVerificationPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const schema = yup.object({ const schema = yup.object({
expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => { expiry_date: yup
if (val == "") return false; .string()
const date = new Date(val); .test("is-not-in-past", "Not a valid date", (val) => {
return date.setDate(date.getDate() - 1) > new Date(); if (val == "") return false;
}), const date = new Date(val);
return date.setDate(date.getDate() - 1) > new Date();
}),
}); });
const { const {
@@ -35,14 +37,18 @@ export default function CustomerVerificationPage() {
register, register,
watch, watch,
formState: { errors }, formState: { errors },
} = useForm({ defaultValues: { selectedType: "Driver's License" }, resolver: yupResolver(schema) }); } = useForm({
defaultValues: { selectedType: "Driver's License" },
resolver: yupResolver(schema),
});
const [frontImage, setFrontImage] = useState(null); const [frontImage, setFrontImage] = useState(null);
const [backImage, setBackImage] = useState(null); const [backImage, setBackImage] = useState(null);
const [passport, setPassport] = useState(null); const [passport, setPassport] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const { dispatch: authDispatch } = useContext(AuthContext); const { dispatch: authDispatch } = useContext(AuthContext);
const [verified, setVerified] = useState(false); const [verified, setVerified] = useState(false);
@@ -51,7 +57,8 @@ export default function CustomerVerificationPage() {
const selectedType = watch("selectedType"); const selectedType = watch("selectedType");
const isDisabled = () => { const isDisabled = () => {
if (selectedType == "Driver's License" && frontImage && backImage) return false; if (selectedType == "Driver's License" && frontImage && backImage)
return false;
if (selectedType == "Passport" && passport) return false; if (selectedType == "Passport" && passport) return false;
return true; return true;
}; };
@@ -88,7 +95,7 @@ export default function CustomerVerificationPage() {
image_back: data.backImage ?? null, image_back: data.backImage ?? null,
user_id: Number(localStorage.getItem("user")), user_id: Number(localStorage.getItem("user")),
}, },
globalState.user.verificationId ? "PUT" : "POST", globalState.user.verificationId ? "PUT" : "POST"
); );
// create notification // create notification
@@ -103,7 +110,7 @@ export default function CustomerVerificationPage() {
type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION, type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION,
status: NOTIFICATION_STATUS.NOT_ADDRESSED, status: NOTIFICATION_STATUS.NOT_ADDRESSED,
}, },
"POST", "POST"
); );
setVerified(true); setVerified(true);
@@ -130,66 +137,67 @@ export default function CustomerVerificationPage() {
}; };
return ( return (
<div className="pb-16 normal-case"> <div className='pb-16 normal-case'>
<div> <div>
<button <button
type="button" type='button'
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold" className='mb-2 mr-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold'
> >
<Icon <Icon
type="arrow" type='arrow'
variant="narrow-left" variant='narrow-left'
className="h-4 w-4 stroke-[#667085]" className='h-4 w-4 stroke-[#667085]'
/>{" "} />{" "}
<span className="ml-2">Back</span> <span className='ml-2'>Back</span>
</button> </button>
</div> </div>
<h1 className="mb-4 text-2xl font-semibold md:text-5xl">Identity Verification</h1> <h1 className='mb-4 text-2xl font-semibold md:text-5xl'>
<div className="mb-[32px] max-w-3xl rounded-lg border border-[#EAECF0] bg-[#F9FAFB] px-[24px] py-[16px]"> Identity Verification
<h3 className="text-lg flex items-center gap-2 font-semibold"> </h1>
<div className='mb-[32px] max-w-3xl rounded-lg border border-[#EAECF0] bg-[#F9FAFB] px-[24px] py-[16px]'>
<h3 className='text-lg flex items-center gap-2 font-semibold'>
<SecurityIcon /> <SecurityIcon />
<span>Safety is our priority</span> <span>Safety is our priority</span>
</h3> </h3>
<p className="ml-5 max-w-xl text-sm leading-relaxed"> <p className='ml-5 max-w-xl text-sm leading-relaxed'>
To establish trust for all parties we verify both hosts and guests. Your personal information is secure. We will never share your information with third parties. To establish trust for all parties we verify both hosts and guests.
Your personal information is secure. We will never share your
information with third parties.
</p> </p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<p className="mb-2 font-semibold">Verification Documents.</p> <p className='mb-2 font-semibold'>Verification Documents.</p>
<div data-tour="fifth-step" className="fifth-step radio-container mb-8 flex justify-between md:max-w-lg"> <div
<label data-tour='sixth-step'
htmlFor="driversLicense" className='sixth-step radio-container mb-8 flex justify-between md:max-w-lg'
className="cursor-pointer" >
> <label htmlFor='driversLicense' className='cursor-pointer'>
<input <input
type="radio" type='radio'
id="driversLicense" id='driversLicense'
{...register("selectedType")} {...register("selectedType")}
className="mr-2" className='mr-2'
value="Driver's License" value="Driver's License"
/> />
<span></span> <span></span>
Driver's License Driver's License
</label> </label>
<label <label htmlFor='passport' className='cursor-pointer'>
htmlFor="passport"
className="cursor-pointer"
>
<input <input
type="radio" type='radio'
id="passport" id='passport'
{...register("selectedType")} {...register("selectedType")}
className="mr-2" className='mr-2'
value="Passport" value='Passport'
/> />
<span></span> <span></span>
Passport Passport
</label> </label>
</div> </div>
<div className="text-[#667085]"> <div className='text-[#667085]'>
{selectedType == "Driver's License" ? ( {selectedType == "Driver's License" ? (
<div className="flex flex-col items-center gap-[16px] md:flex-row"> <div className='flex flex-col items-center gap-[16px] md:flex-row'>
<FileUploader <FileUploader
multiple={false} multiple={false}
handleChange={(file) => { handleChange={(file) => {
@@ -197,18 +205,21 @@ export default function CustomerVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{frontImage?.name ? ( {frontImage?.name ? (
<img <img
src={readImage(frontImage, "front-preview")} src={readImage(frontImage, "front-preview")}
id="front-preview" id='front-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Front</h4> <h4 className='text-xl font-semibold'>Front</h4>
<p className="px-[20px]"> <p className='px-[20px]'>
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) <strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -221,18 +232,21 @@ export default function CustomerVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{backImage?.name ? ( {backImage?.name ? (
<img <img
src={readImage(backImage, "back-preview")} src={readImage(backImage, "back-preview")}
id="back-preview" id='back-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Back</h4> <h4 className='text-xl font-semibold'>Back</h4>
<p className="px-[20px]"> <p className='px-[20px]'>
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) <strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -247,18 +261,23 @@ export default function CustomerVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{passport?.name ? ( {passport?.name ? (
<img <img
src={readImage(passport, "passport-preview")} src={readImage(passport, "passport-preview")}
id="passport-preview" id='passport-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Passport page with photo</h4> <h4 className='text-xl font-semibold'>
<p className="px-[20px]"> Passport page with photo
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) </h4>
<p className='px-[20px]'>
<strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -266,43 +285,61 @@ export default function CustomerVerificationPage() {
</FileUploader> </FileUploader>
)} )}
</div> </div>
<div className="my-8 max-w-lg"> <div className='my-8 max-w-lg'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="expiry_date" htmlFor='expiry_date'
> >
Expiration Date Expiration Date
</label> </label>
<input <input
type="date" type='date'
placeholder="expiry_date" placeholder='expiry_date'
{...register("expiry_date")} {...register("expiry_date")}
className={`focus:shadow-outline !min-h-[40px] w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none`} className={`focus:shadow-outline !min-h-[40px] w-full rounded border px-3 py-2 leading-tight text-gray-700 focus:outline-none`}
/> />
{errors.expiry_date?.message && <p className="my-3 rounded-md border border-[#C42945] bg-white py-2 px-3 text-center text-sm normal-case text-[#C42945]">{errors.expiry_date?.message}</p>} {errors.expiry_date?.message && (
<p className='my-3 rounded-md border border-[#C42945] bg-white px-3 py-2 text-center text-sm normal-case text-[#C42945]'>
{errors.expiry_date?.message}
</p>
)}
</div> </div>
<LoadingButton <LoadingButton
loading={loading} loading={loading}
type="submit" type='submit'
className={`login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"} mt-8 w-[333px] max-w-full`} className={`login-btn-gradient submit-doc-btn rounded tracking-wide text-white outline-none focus:outline-none ${
loading ? "py-1" : "py-2"
} mt-8 w-[333px] max-w-full`}
disabled={isDisabled()} disabled={isDisabled()}
data-tour='submit-doc-btn'
> >
Submit Document Submit Document
</LoadingButton> </LoadingButton>
</form> </form>
<div className={showVerified ? "popup-container flex items-center justify-center normal-case" : "hidden"}> <div
className={
showVerified
? "popup-container flex items-center justify-center normal-case"
: "hidden"
}
>
<div <div
className={`${verified ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`} className={`${
verified ? "pop-in" : "pop-out"
} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<h2 className="mb-4 text-3xl font-semibold"> <h2 className='mb-4 text-3xl font-semibold'>
<GreenCheckIcon /> <GreenCheckIcon />
Document received Document received
</h2> </h2>
<p className="mb-4 text-sm text-gray-500">Once we verify your document you will receive an email. It usually takes up to 24 hours.</p> <p className='mb-4 text-sm text-gray-500'>
Once we verify your document you will receive an email. It usually
takes up to 24 hours.
</p>
<button <button
type="button" type='button'
className="login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none'
onClick={() => { onClick={() => {
setVerified(false); setVerified(false);
navigate(searchParams.get("redirect_uri") ?? -1); navigate(searchParams.get("redirect_uri") ?? -1);
+114 -51
View File
@@ -8,7 +8,11 @@ import MkdSDK from "@/utils/MkdSDK";
import { callCustomAPI } from "@/utils/callCustomAPI"; import { callCustomAPI } from "@/utils/callCustomAPI";
import Skeleton from "react-loading-skeleton"; import Skeleton from "react-loading-skeleton";
import { formatDate } from "@/utils/date-time-utils"; import { formatDate } from "@/utils/date-time-utils";
import { IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; import {
IMAGE_STATUS,
NOTIFICATION_STATUS,
NOTIFICATION_TYPE,
} from "@/utils/constants";
import SwitchBulkMode from "@/components/SwitchBulkMode"; import SwitchBulkMode from "@/components/SwitchBulkMode";
import TwoFaDialog from "@/components/Profile/TwoFaDialog"; import TwoFaDialog from "@/components/Profile/TwoFaDialog";
import EditProfileModal from "@/components/Profile/EditProfileModal"; import EditProfileModal from "@/components/Profile/EditProfileModal";
@@ -18,6 +22,7 @@ import EditAboutModal from "@/components/Profile/EditAboutModal";
import { parseJsonSafely } from "@/utils/utils"; import { parseJsonSafely } from "@/utils/utils";
import EnableEmailDialog from "@/components/Profile/EnableEmailDialog"; import EnableEmailDialog from "@/components/Profile/EnableEmailDialog";
import DeleteAccountModal from "@/components/Profile/DeleteAccountModal"; import DeleteAccountModal from "@/components/Profile/DeleteAccountModal";
import ProfileImageConfirmModal from "@/components/Profile/ProfileImageConfirmModal";
function getProfilePhotoMessage(image_status) { function getProfilePhotoMessage(image_status) {
switch (image_status) { switch (image_status) {
@@ -33,7 +38,8 @@ function getProfilePhotoMessage(image_status) {
} }
export default function HostProfilePage() { export default function HostProfilePage() {
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [twoFa, setTwoFa] = useState(false); const [twoFa, setTwoFa] = useState(false);
const [twoFaDialog, setTwoFaDialog] = useState(false); const [twoFaDialog, setTwoFaDialog] = useState(false);
@@ -49,20 +55,29 @@ export default function HostProfilePage() {
const [deleteAccountModal, setDeleteAccountModal] = useState(false); const [deleteAccountModal, setDeleteAccountModal] = useState(false);
const [showImagePreview, setShowImagePreview] = useState(false);
const [selectedImage, setSelectedImage] = useState(null);
let sdk = new MkdSDK(); let sdk = new MkdSDK();
const changeProfilePic = async (e) => { const changeProfilePic = (e) => {
globalDispatch({ type: "START_LOADING" }); const file = e.target.files && e.target.files[0];
const file = e.target.files; if (file) {
const formData = new FormData(); setSelectedImage(file);
for (let i = 0; i < file.length; i++) { setShowImagePreview(true);
formData.append("file", file[i]);
} }
};
const handleConfirmUpload = async () => {
setShowImagePreview(false);
if (!selectedImage) return;
globalDispatch({ type: "START_LOADING" });
const formData = new FormData();
formData.append("file", selectedImage);
try { try {
const upload = await sdk.uploadImage(formData); const upload = await sdk.uploadImage(formData);
console.log("upload", upload);
sdk.setTable("user"); sdk.setTable("user");
const result = await callCustomAPI( await callCustomAPI(
"edit-self", "edit-self",
"post", "post",
{ {
@@ -71,9 +86,16 @@ export default function HostProfilePage() {
is_photo_approved: IMAGE_STATUS.IN_REVIEW, is_photo_approved: IMAGE_STATUS.IN_REVIEW,
}, },
}, },
"", ""
); );
globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: upload.url, is_photo_approved: IMAGE_STATUS.IN_REVIEW } }); globalDispatch({
type: "SET_USER_DATA",
payload: {
...globalState.user,
photo: upload.url,
is_photo_approved: IMAGE_STATUS.IN_REVIEW,
},
});
// create notification // create notification
sdk.setTable("notification"); sdk.setTable("notification");
await sdk.callRestAPI( await sdk.callRestAPI(
@@ -86,7 +108,7 @@ export default function HostProfilePage() {
type: NOTIFICATION_TYPE.EDIT_USER_PICTURE, type: NOTIFICATION_TYPE.EDIT_USER_PICTURE,
status: NOTIFICATION_STATUS.NOT_ADDRESSED, status: NOTIFICATION_STATUS.NOT_ADDRESSED,
}, },
"POST", "POST"
); );
} catch (err) { } catch (err) {
globalDispatch({ globalDispatch({
@@ -98,9 +120,15 @@ export default function HostProfilePage() {
}); });
} }
globalDispatch({ type: "STOP_LOADING" }); globalDispatch({ type: "STOP_LOADING" });
setSelectedImage(null);
}; };
const removeProfilePic = async (e) => { const handleCancelUpload = () => {
setShowImagePreview(false);
setSelectedImage(null);
};
const removeProfilePic = async () => {
try { try {
sdk.setTable("user"); sdk.setTable("user");
await callCustomAPI( await callCustomAPI(
@@ -112,9 +140,12 @@ export default function HostProfilePage() {
is_photo_approved: null, is_photo_approved: null,
}, },
}, },
"", ""
); );
globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: null, is_photo_approved: null } }); globalDispatch({
type: "SET_USER_DATA",
payload: { ...globalState.user, photo: null, is_photo_approved: null },
});
} catch (err) { } catch (err) {
globalDispatch({ globalDispatch({
type: "SHOW_ERROR", type: "SHOW_ERROR",
@@ -137,14 +168,16 @@ export default function HostProfilePage() {
two_factor_authentication: twoFa != 1 ? 1 : 0, two_factor_authentication: twoFa != 1 ? 1 : 0,
}, },
}, },
"", ""
); );
setTwoFaDialog(false); setTwoFaDialog(false);
globalDispatch({ globalDispatch({
type: "SHOW_CONFIRMATION", type: "SHOW_CONFIRMATION",
payload: { payload: {
heading: "Success", heading: "Success",
message: `Two factor Authentication ${twoFa == 1 ? "disabled" : "enabled"}`, message: `Two factor Authentication ${
twoFa == 1 ? "disabled" : "enabled"
}`,
btn: "Ok got it", btn: "Ok got it",
}, },
}); });
@@ -163,51 +196,67 @@ export default function HostProfilePage() {
} }
return ( return (
<div className="pt-[44px] pb-16 normal-case text-[#475467]"> <div className='pb-16 pt-[44px] normal-case text-[#475467]'>
<div className="flex flex-wrap-reverse justify-between "> <div className='flex flex-wrap-reverse justify-between '>
<div className="flex max-w-3xl flex-grow flex-col justify-between md:flex-row md:items-center"> <div className='flex max-w-3xl flex-grow flex-col justify-between md:flex-row md:items-center'>
<div className="mb-[16px] flex flex-col"> <div className='mb-[16px] flex flex-col'>
<h3 className="text-xl font-semibold">Your photo</h3> <h3 className='text-xl font-semibold'>Your photo</h3>
<small className="text-xs md:text-sm">{getProfilePhotoMessage(globalState.user.is_photo_approved)}</small> <small className='text-xs md:text-sm'>
{getProfilePhotoMessage(globalState.user.is_photo_approved)}
</small>
</div> </div>
<div <div
data-tour="photo-step" data-tour='photo-step'
className="flex items-center justify-between"> className='flex items-center justify-between'
>
<img <img
src={globalState.user.photo ?? "/default.png"} src={globalState.user.photo ?? "/default.png"}
alt="" alt=''
className="h-[56px] w-[56px] rounded-full object-cover md:mr-[65px] md:h-[64px] md:w-[64px]" className='h-[56px] w-[56px] rounded-full object-cover md:mr-[65px] md:h-[64px] md:w-[64px]'
/> />
<div> <div>
<label <label
className="photo-step mr-3 cursor-pointer font-semibold underline" className='photo-step mr-3 cursor-pointer font-semibold underline'
htmlFor="profilePic" htmlFor='profilePic'
> >
Update{" "} Update{" "}
<input <input
type="file" type='file'
className="hidden" className='hidden'
id="profilePic" id='profilePic'
onChange={changeProfilePic} onChange={changeProfilePic}
/> />
</label> </label>
<button <button
className="underline" className='underline'
id="remove_profile_pic" id='remove_profile_pic'
onClick={removeProfilePic} onClick={() => {
globalDispatch({
type: "SHOW_CONFIRMATION",
payload: {
heading: "Remove Profile Picture?",
message:
"Are you sure you want to remove your profile picture?",
btn: "Yes, Remove",
onClose: () => {
removeProfilePic();
},
},
});
}}
> >
Remove Remove
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div className="mb-12 flex w-full justify-between md:mb-0 md:w-[unset] md:flex-col md:justify-start"> <div className='mb-12 flex w-full justify-between md:mb-0 md:w-[unset] md:flex-col md:justify-start'>
<p className="mb-2 self-end">Profile status</p> <p className='mb-2 self-end'>Profile status</p>
<div data-tour="fourth-step" className="flex fourth-step"> <div data-tour='fourth-step' className='fourth-step flex'>
{![0, 1].includes(globalState.user.verificationStatus) && ( {![0, 1].includes(globalState.user.verificationStatus) && (
<Link <Link
to="/account/verification" to='/account/verification'
className="mr-3 font-semibold text-[#1570EF]" className='mr-3 font-semibold text-[#1570EF]'
> >
Get verified Get verified
</Link> </Link>
@@ -215,7 +264,11 @@ export default function HostProfilePage() {
<button <button
className={ className={
`${globalState.user.verificationStatus == 1 ? "login-btn-gradient" : "bg-[#667085]"}` + `${
globalState.user.verificationStatus == 1
? "login-btn-gradient"
: "bg-[#667085]"
}` +
" flex min-w-[103px] items-center gap-1 rounded-md p-1 px-2 text-xs uppercase tracking-wider text-white" " flex min-w-[103px] items-center gap-1 rounded-md p-1 px-2 text-xs uppercase tracking-wider text-white"
} }
> >
@@ -225,28 +278,28 @@ export default function HostProfilePage() {
return ( return (
<> <>
<NotVerifiedIcon /> <NotVerifiedIcon />
<span className="">Pending</span> <span className=''>Pending</span>
</> </>
); );
case 1: case 1:
return ( return (
<> <>
<NotVerifiedIcon /> <NotVerifiedIcon />
<span className="">Verified</span> <span className=''>Verified</span>
</> </>
); );
case 2: case 2:
return ( return (
<> <>
<NotVerifiedIcon /> <NotVerifiedIcon />
<span className="">Verification Declined</span> <span className=''>Verification Declined</span>
</> </>
); );
default: default:
return ( return (
<> <>
<NotVerifiedIcon /> <NotVerifiedIcon />
<span className="">Not verified</span> <span className=''>Not verified</span>
</> </>
); );
} }
@@ -255,18 +308,18 @@ export default function HostProfilePage() {
</div> </div>
</div> </div>
</div> </div>
<hr className="my-[37px]" /> <hr className='my-[37px]' />
<div className="grid sm:flex flex-co items-start gap-4"> <div className='flex-co grid items-start gap-4 sm:flex'>
<Link <Link
to={"/account/profile/rules-templates"} to={"/account/profile/rules-templates"}
className="rounded-md border border-primary-dark px-4 py-2 text-sm text-black duration-200 hover:bg-primary-dark hover:text-white" className='rounded-md border border-primary-dark px-4 py-2 text-sm text-black duration-200 hover:bg-primary-dark hover:text-white'
> >
Manage Property Rules Template Manage Property Rules Template
</Link> </Link>
<Link <Link
to={"/account/profile/rules-templates/add"} to={"/account/profile/rules-templates/add"}
className="rounded-md border border-primary-dark px-4 py-2 text-sm text-black duration-200 hover:bg-primary-dark hover:text-white" className='rounded-md border border-primary-dark px-4 py-2 text-sm text-black duration-200 hover:bg-primary-dark hover:text-white'
> >
Add Property Rules Template Add Property Rules Template
</Link> </Link>
@@ -299,6 +352,16 @@ export default function HostProfilePage() {
isOpen={enableEmailDialog} isOpen={enableEmailDialog}
closeModal={() => setEnableEmailDialog(false)} closeModal={() => setEnableEmailDialog(false)}
/> />
<ProfileImageConfirmModal
modalOpen={showImagePreview}
modalImage={
selectedImage instanceof File
? URL.createObjectURL(selectedImage)
: selectedImage
}
onConfirm={handleConfirmUpload}
onCancel={handleCancelUpload}
/>
</div> </div>
); );
} }
@@ -9,7 +9,12 @@ import MkdSDK from "@/utils/MkdSDK";
import { useContext } from "react"; import { useContext } from "react";
import { GlobalContext } from "@/globalContext"; import { GlobalContext } from "@/globalContext";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { SPACE_CATEGORY_SIZES, NOTIFICATION_STATUS, NOTIFICATION_TYPE, SPACE_STATUS } from "@/utils/constants"; import {
SPACE_CATEGORY_SIZES,
NOTIFICATION_STATUS,
NOTIFICATION_TYPE,
SPACE_STATUS,
} from "@/utils/constants";
import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2";
import CustomSelectV2 from "@/components/CustomSelectV2"; import CustomSelectV2 from "@/components/CustomSelectV2";
import CounterV2 from "@/components/CounterV2"; import CounterV2 from "@/components/CounterV2";
@@ -24,7 +29,8 @@ const ctrl = new AbortController();
const EditPropertySpacePage = () => { const EditPropertySpacePage = () => {
const { dispatch: authDispatch } = useContext(AuthContext); const { dispatch: authDispatch } = useContext(AuthContext);
const { spaceData, dispatch } = useSpaceContext(); const { spaceData, dispatch } = useSpaceContext();
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const mode = searchParams.get("mode"); const mode = searchParams.get("mode");
const { id } = useParams(); const { id } = useParams();
@@ -41,7 +47,11 @@ const EditPropertySpacePage = () => {
rate: yup.number().typeError("Must be a number").positive().integer(), rate: yup.number().typeError("Must be a number").positive().integer(),
description: yup.string().required("This field is required"), description: yup.string().required("This field is required"),
rule: yup.string(), rule: yup.string(),
max_capacity: yup.number().required("This field is required").min(1).typeError("This field is required"), max_capacity: yup
.number()
.required("This field is required")
.min(1)
.typeError("This field is required"),
additional_guest_rate: yup.string(), additional_guest_rate: yup.string(),
}); });
@@ -79,7 +89,12 @@ const EditPropertySpacePage = () => {
const where = [`ergo_property_spaces.id = ${id}`]; const where = [`ergo_property_spaces.id = ${id}`];
const user_id = localStorage.getItem("user"); const user_id = localStorage.getItem("user");
try { try {
const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); const result = await sdk.callRawAPI(
"/v2/api/custom/ergo/popular/PAGINATE",
{ page: 1, limit: 1, user_id: Number(user_id), where, all: true },
"POST",
ctrl.signal
);
if (Array.isArray(result.list) && result.list.length > 0) { if (Array.isArray(result.list) && result.list.length > 0) {
setCurrSpace(result.list[0]); setCurrSpace(result.list[0]);
} }
@@ -102,10 +117,29 @@ const EditPropertySpacePage = () => {
} }
}, []); }, []);
useEffect(() => {
if (mode === "edit" && currSpace && Object.keys(currSpace).length > 0) {
setValue("category", currSpace.space_id ?? "");
setValue("id", currSpace.id ?? "");
setValue("name", currSpace.name ?? "");
setValue("rate", currSpace.rate ?? "");
setValue("max_capacity", currSpace.max_capacity ?? 0);
setValue("description", currSpace.description ?? "");
setValue("rule", currSpace.rule ?? "");
setValue("zip", currSpace.zip ?? "");
setValue("country", currSpace.country ?? "");
setValue("city", currSpace.city ?? "");
setValue("address_line_1", currSpace.address_line_1 ?? "");
setValue("address_line_2", currSpace.address_line_2 ?? "");
setValue("additional_guest_rate", currSpace.additional_guest_rate ?? "");
setValue("size", currSpace.size ?? 0);
}
}, [currSpace, mode, setValue]);
const onSubmit = async (data) => { const onSubmit = async (data) => {
const result = extractLocationInfo(data?.city) const result = extractLocationInfo(data?.city);
data.city = (result[0]); data.city = result[0];
data.country = (result[1]); data.country = result[1];
console.log("submitting", data); console.log("submitting", data);
const host_id = localStorage.getItem("user"); const host_id = localStorage.getItem("user");
globalDispatch({ type: "START_LOADING" }); globalDispatch({ type: "START_LOADING" });
@@ -127,7 +161,7 @@ const EditPropertySpacePage = () => {
name: data.name, name: data.name,
rule: data.rule, rule: data.rule,
}, },
"PUT", "PUT"
); );
sdk.setTable("property_spaces"); sdk.setTable("property_spaces");
@@ -143,7 +177,7 @@ const EditPropertySpacePage = () => {
additional_guest_rate: data.additional_guest_rate, additional_guest_rate: data.additional_guest_rate,
size: hasSizes ? data.size : SPACE_CATEGORY_SIZES.UNSET, size: hasSizes ? data.size : SPACE_CATEGORY_SIZES.UNSET,
}, },
"PUT", "PUT"
); );
// create notification // create notification
@@ -158,7 +192,7 @@ const EditPropertySpacePage = () => {
type: NOTIFICATION_TYPE.EDIT_PROPERTY_SPACE, type: NOTIFICATION_TYPE.EDIT_PROPERTY_SPACE,
status: NOTIFICATION_STATUS.NOT_ADDRESSED, status: NOTIFICATION_STATUS.NOT_ADDRESSED,
}, },
"POST", "POST"
); );
} }
if (draftType === "continue") { if (draftType === "continue") {
@@ -168,7 +202,10 @@ const EditPropertySpacePage = () => {
} }
} catch (err) { } catch (err) {
tokenExpireError(authDispatch, err.message); tokenExpireError(authDispatch, err.message);
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Edit Space Failed", message: err.message } }); globalDispatch({
type: "SHOW_ERROR",
payload: { heading: "Edit Space Failed", message: err.message },
});
} }
globalDispatch({ type: "STOP_LOADING" }); globalDispatch({ type: "STOP_LOADING" });
}; };
@@ -181,166 +218,268 @@ const EditPropertySpacePage = () => {
{ label: "X-Large", value: SPACE_CATEGORY_SIZES.X_LARGE }, { label: "X-Large", value: SPACE_CATEGORY_SIZES.X_LARGE },
]; ];
const category = watch("category"); const category = watch("category");
const hasSizes = globalState.spaceCategories.find((ctg) => ctg.id == Number(category))?.has_sizes == 1; const hasSizes =
globalState.spaceCategories.find((ctg) => ctg.id == Number(category))
?.has_sizes == 1;
return ( return (
<div className="min-h-screen pb-40"> <div className='min-h-screen pb-40'>
<form <form
className="text-sm md:max-w-lg" className='text-sm md:max-w-lg'
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
autoComplete="off" autoComplete='off'
> >
<h1 className="mb-8 text-3xl font-bold md:text-4xl">Space Details</h1> <h1 className='mb-8 text-3xl font-bold md:text-4xl'>Space Details</h1>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="name" htmlFor='name'
> >
Property name {errors.name?.message ? <span className="text-xs font-normal italic text-red-500">{errors.name?.message}</span> : ""} Property name{" "}
{errors.name?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.name?.message}
</span>
) : (
""
)}
</label> </label>
<input <input
autoComplete="off" autoComplete='off'
placeholder="" placeholder=''
{...register("name")} {...register("name")}
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.name?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full rounded border px-3 py-2 leading-tight text-gray-700 ${
errors.name?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="address_line_1" htmlFor='address_line_1'
> >
Address Line 1 {errors.address_line_1?.message ? <span className="text-xs font-normal italic text-red-500">{errors.address_line_1?.message}</span> : ""} Address Line 1{" "}
{errors.address_line_1?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.address_line_1?.message}
</span>
) : (
""
)}
</label> </label>
<CustomLocationAutoCompleteV2 <CustomLocationAutoCompleteV2
control={control} control={control}
setValue={(val) => setValue("address_line_1", val)} setValue={(val) => setValue("address_line_1", val)}
name="address_line_1" name='address_line_1'
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_1?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full rounded border px-3 py-2 leading-tight text-gray-700 ${
placeholder="" errors.address_line_1?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
placeholder=''
hideIcons hideIcons
suggestionType={["(cities)"]} suggestionType={["(cities)"]}
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="address_line_2" htmlFor='address_line_2'
> >
Address Line 2 {errors.address_line_2?.message ? <span className="text-xs font-normal italic text-red-500">{errors.address_line_2?.message}</span> : ""} Address Line 2{" "}
{errors.address_line_2?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.address_line_2?.message}
</span>
) : (
""
)}
</label> </label>
<CustomLocationAutoCompleteV2 <CustomLocationAutoCompleteV2
control={control} control={control}
setValue={(val) => setValue("address_line_2", val)} setValue={(val) => setValue("address_line_2", val)}
name="address_line_2" name='address_line_2'
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_2?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full rounded border px-3 py-2 leading-tight text-gray-700 ${
placeholder="" errors.address_line_2?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
placeholder=''
hideIcons hideIcons
suggestionType={["(cities)"]} suggestionType={["(cities)"]}
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="city" htmlFor='city'
> >
City {errors.city?.message ? <span className="text-xs font-normal italic text-red-500">{errors.city?.message}</span> : ""} City{" "}
{errors.city?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.city?.message}
</span>
) : (
""
)}
</label> </label>
<CustomLocationAutoCompleteV2 <CustomLocationAutoCompleteV2
control={control} control={control}
setValue={(val) => setValue("city", val)} setValue={(val) => setValue("city", val)}
name="city" name='city'
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full rounded border px-3 py-2 leading-tight text-gray-700 ${
placeholder="" errors.city?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
placeholder=''
hideIcons hideIcons
suggestionType={["(cities)"]} suggestionType={["(cities)"]}
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="zip" htmlFor='zip'
> >
Zip code {errors.zip?.message ? <span className="text-xs font-normal italic text-red-500">{errors.zip?.message}</span> : ""} Zip code{" "}
{errors.zip?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.zip?.message}
</span>
) : (
""
)}
</label> </label>
<input <input
placeholder="" placeholder=''
{...register("zip")} {...register("zip")}
className={` focus:shadow-outline $ w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.zip?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary" className={` focus:shadow-outline $ w-full rounded border px-3 py-2 leading-tight text-gray-700 ${
}`} errors.zip?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="category" htmlFor='category'
> >
Category {errors.category?.message ? <span className="text-xs font-normal italic text-red-500">{errors.category?.message}</span> : ""} Category{" "}
{errors.category?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.category?.message}
</span>
) : (
""
)}
</label> </label>
<CustomSelectV2 <CustomSelectV2
items={globalState.spaceCategories} items={globalState.spaceCategories}
labelField="category" labelField='category'
valueField="id" valueField='id'
containerClassName="" containerClassName=''
className={`w-full border py-2 px-3 ${errors.category?.message ? "ring-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full border px-3 py-2 ${
openClassName="ring-primary ring-2" errors.category?.message
? "ring-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
openClassName='ring-primary ring-2'
placeholder={"Select a category"} placeholder={"Select a category"}
control={control} control={control}
name="category" name='category'
/> />
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="rate" htmlFor='rate'
> >
Hourly rate {errors.rate?.message ? <span className="text-xs font-normal italic text-red-500">{errors.rate?.message}</span> : ""} Hourly rate{" "}
{errors.rate?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.rate?.message}
</span>
) : (
""
)}
</label> </label>
<div className="flex"> <div className='flex'>
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-100 px-4 text-sm">&#36;</span> <span className='inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-100 px-4 text-sm'>
&#36;
</span>
<input <input
placeholder="" placeholder=''
type="number" type='number'
{...register("rate")} {...register("rate")}
className={`remove-arrow focus:shadow-outline w-full rounded rounded-l-none border py-2 px-3 leading-tight text-gray-700 ${errors.rate?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary" className={`remove-arrow focus:shadow-outline w-full rounded rounded-l-none border px-3 py-2 leading-tight text-gray-700 ${
}`} errors.rate?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
/> />
</div> </div>
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="description" htmlFor='description'
> >
Description {errors.description?.message ? <span className="text-xs font-normal italic text-red-500">{errors.description?.message}</span> : ""} Description{" "}
{errors.description?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.description?.message}
</span>
) : (
""
)}
</label> </label>
<textarea <textarea
placeholder="" placeholder=''
{...register("description")} {...register("description")}
className={`w-full resize-none rounded border py-2 px-3 leading-tight text-gray-700 ${errors.description?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary" className={`w-full resize-none rounded border px-3 py-2 leading-tight text-gray-700 ${
}`} errors.description?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
rows={10} rows={10}
></textarea> ></textarea>
</div> </div>
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="rule" htmlFor='rule'
> >
Property rules Property rules
</label> </label>
<textarea <textarea
placeholder="" placeholder=''
{...register("rule")} {...register("rule")}
className={`w-full resize-none rounded border py-2 px-3 leading-tight text-gray-700 ${errors.rule?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full resize-none rounded border px-3 py-2 leading-tight text-gray-700 ${
errors.rule?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
rows={10} rows={10}
></textarea> ></textarea>
</div> </div>
<div className="mb-6 flex items-center justify-between"> <div className='mb-6 flex items-center justify-between'>
<p className="font-semibold">* Max number of guests {errors.max_capacity?.message ? <span className="text-xs font-normal italic text-red-500">{errors.max_capacity?.message}</span> : ""}</p> <p className='font-semibold'>
* Max number of guests{" "}
{errors.max_capacity?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.max_capacity?.message}
</span>
) : (
""
)}
</p>
<CounterV2 <CounterV2
name="max_capacity" name='max_capacity'
control={control} control={control}
setValue={(val) => setValue("max_capacity", val)} setValue={(val) => setValue("max_capacity", val)}
/> />
@@ -348,58 +487,82 @@ const EditPropertySpacePage = () => {
{hasSizes && ( {hasSizes && (
<div className={`mb-8`}> <div className={`mb-8`}>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="size" htmlFor='size'
> >
Size (<span className="text-sm font-normal italic">optional </span>) {errors.size?.message ? <span className="text-xs font-normal italic text-red-500">{errors.size?.message}</span> : ""} Size (
<span className='text-sm font-normal italic'>optional </span>){" "}
{errors.size?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.size?.message}
</span>
) : (
""
)}
</label> </label>
<CustomSelectV2 <CustomSelectV2
shouldUnregister={false} shouldUnregister={false}
items={SIZES} items={SIZES}
labelField="label" labelField='label'
valueField="value" valueField='value'
containerClassName="" containerClassName=''
className={`w-full border py-2 px-3 ${errors.size?.message ? "ring-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} className={`w-full border px-3 py-2 ${
openClassName="ring-primary ring-2" errors.size?.message
? "ring-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
openClassName='ring-primary ring-2'
placeholder={"Select size"} placeholder={"Select size"}
control={control} control={control}
name="size" name='size'
/> />
</div> </div>
)} )}
<div className="mb-8"> <div className='mb-8'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="additional_guest_rate" htmlFor='additional_guest_rate'
> >
Hourly rate for additional {hasSizes ? "guests" : "guests"} (<span className="text-sm font-normal italic">optional </span>){" "} Hourly rate for additional {hasSizes ? "guests" : "guests"} (
{errors.additional_guest_rate?.message ? <span className="text-xs font-normal italic text-red-500">{errors.additional_guest_rate?.message}</span> : ""} <span className='text-sm font-normal italic'>optional </span>){" "}
{errors.additional_guest_rate?.message ? (
<span className='text-xs font-normal italic text-red-500'>
{errors.additional_guest_rate?.message}
</span>
) : (
""
)}
</label> </label>
<div className="flex"> <div className='flex'>
<span className="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-100 px-4 text-sm">&#36;</span> <span className='inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-100 px-4 text-sm'>
&#36;
</span>
<input <input
placeholder="" placeholder=''
type="number" type='number'
{...register("additional_guest_rate")} {...register("additional_guest_rate")}
className={`remove-arrow w-full rounded rounded-l-none border py-2 px-3 leading-tight text-gray-700 ${errors.additional_guest_rate?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary" className={`remove-arrow w-full rounded rounded-l-none border px-3 py-2 leading-tight text-gray-700 ${
}`} errors.additional_guest_rate?.message
? "border-red-500 focus:outline-red-500"
: "focus-within:outline-primary"
}`}
/> />
</div> </div>
</div> </div>
<hr className="my-[48px]" /> <hr className='my-[48px]' />
<button <button
onClick={() => setDraftType("continue")} onClick={() => setDraftType("continue")}
type="submit" type='submit'
className="login-btn-gradient rounded py-2 px-4 tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient rounded px-4 py-2 tracking-wide text-white outline-none focus:outline-none'
> >
Continue Continue
</button> </button>
<br /> <br />
<button <button
onClick={() => setDraftType("submit")} onClick={() => setDraftType("submit")}
type="submit" type='submit'
className="login-btn-gradient rounded py-2 mt-3 px-4 tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient mt-3 rounded px-4 py-2 tracking-wide text-white outline-none focus:outline-none'
> >
Submit Submit
</button> </button>
@@ -23,11 +23,13 @@ export default function HostVerificationPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const schema = yup.object({ const schema = yup.object({
expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => { expiry_date: yup
if (val == "") return false; .string()
const date = new Date(val); .test("is-not-in-past", "Not a valid date", (val) => {
return date.setDate(date.getDate() - 1) > new Date(); if (val == "") return false;
}), const date = new Date(val);
return date.setDate(date.getDate() - 1) > new Date();
}),
}); });
const { const {
@@ -35,14 +37,18 @@ export default function HostVerificationPage() {
register, register,
watch, watch,
formState: { errors }, formState: { errors },
} = useForm({ defaultValues: { selectedType: "Driver's License" }, resolver: yupResolver(schema) }); } = useForm({
defaultValues: { selectedType: "Driver's License" },
resolver: yupResolver(schema),
});
const [frontImage, setFrontImage] = useState(null); const [frontImage, setFrontImage] = useState(null);
const [backImage, setBackImage] = useState(null); const [backImage, setBackImage] = useState(null);
const [passport, setPassport] = useState(null); const [passport, setPassport] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const { dispatch: authDispatch } = useContext(AuthContext); const { dispatch: authDispatch } = useContext(AuthContext);
const [verified, setVerified] = useState(false); const [verified, setVerified] = useState(false);
@@ -51,7 +57,8 @@ export default function HostVerificationPage() {
const selectedType = watch("selectedType"); const selectedType = watch("selectedType");
const isDisabled = () => { const isDisabled = () => {
if (selectedType == "Driver's License" && frontImage && backImage) return false; if (selectedType == "Driver's License" && frontImage && backImage)
return false;
if (selectedType == "Passport" && passport) return false; if (selectedType == "Passport" && passport) return false;
return true; return true;
}; };
@@ -88,7 +95,7 @@ export default function HostVerificationPage() {
image_back: data.backImage ?? null, image_back: data.backImage ?? null,
user_id: Number(localStorage.getItem("user")), user_id: Number(localStorage.getItem("user")),
}, },
globalState.user.verificationId ? "PUT" : "POST", globalState.user.verificationId ? "PUT" : "POST"
); );
// create notification // create notification
@@ -103,7 +110,7 @@ export default function HostVerificationPage() {
type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION, type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION,
status: NOTIFICATION_STATUS.NOT_ADDRESSED, status: NOTIFICATION_STATUS.NOT_ADDRESSED,
}, },
"POST", "POST"
); );
setVerified(true); setVerified(true);
@@ -130,66 +137,69 @@ export default function HostVerificationPage() {
}; };
return ( return (
<div className="pb-16 normal-case"> <div className='pb-16 normal-case'>
<div> <div>
<button <button
type="button" type='button'
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold" className='mb-2 mr-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold'
> >
<Icon <Icon
type="arrow" type='arrow'
variant="narrow-left" variant='narrow-left'
className="h-4 w-4 stroke-[#667085]" className='h-4 w-4 stroke-[#667085]'
/>{" "} />{" "}
<span className="ml-2">Back</span> <span className='ml-2'>Back</span>
</button> </button>
</div> </div>
<h1 className="mb-4 text-2xl font-semibold md:text-5xl">Identity Verification</h1> <h1 className='mb-4 text-2xl font-semibold md:text-5xl'>
<div className="mb-[32px] max-w-3xl rounded-lg border border-[#EAECF0] bg-[#F9FAFB] px-[24px] py-[16px]"> Identity Verification
<h3 className="text-lg flex items-center gap-2 font-semibold"> </h1>
<div className='mb-[32px] max-w-3xl rounded-lg border border-[#EAECF0] bg-[#F9FAFB] px-[24px] py-[16px]'>
<h3 className='text-lg flex items-center gap-2 font-semibold'>
<SecurityIcon /> <SecurityIcon />
<span>Safety is our priority</span> <span>Safety is our priority</span>
</h3> </h3>
<p className="ml-5 max-w-xl text-sm leading-relaxed"> <p className='ml-5 max-w-xl text-sm leading-relaxed'>
To establish trust for all parties we verify both hosts and guests. Your personal information is secure. We will never share your information with third parties. To establish trust for all parties we verify both hosts and guests.
Your personal information is secure. We will never share your
information with third parties.
</p> </p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<p className="mb-2 font-semibold">Explain what document(s) are allowed.</p> <p className='mb-2 font-semibold'>
<div data-tour="fifth-step" className="radio-container mb-8 flex justify-between md:max-w-lg"> Explain what document(s) are allowed.
<label </p>
htmlFor="driversLicense" <div
className="cursor-pointer" data-tour='sixth-step'
> className='radio-container sixth-step mb-8 flex justify-between md:max-w-lg'
>
<label htmlFor='driversLicense' className='cursor-pointer'>
<input <input
type="radio" type='radio'
id="driversLicense" id='driversLicense'
{...register("selectedType")} {...register("selectedType")}
className="mr-2" className='mr-2'
value="Driver's License" value="Driver's License"
/> />
<span></span> <span></span>
Driver's License Driver's License
</label> </label>
<label <label htmlFor='passport' className='cursor-pointer'>
htmlFor="passport"
className="cursor-pointer"
>
<input <input
type="radio" type='radio'
id="passport" id='passport'
{...register("selectedType")} {...register("selectedType")}
className="mr-2" className='mr-2'
value="Passport" value='Passport'
/> />
<span></span> <span></span>
Passport Passport
</label> </label>
</div> </div>
<div className="sixth-step text-[#667085]"> <div className='text-[#667085]'>
{selectedType == "Driver's License" ? ( {selectedType == "Driver's License" ? (
<div className="flex flex-col items-center gap-[16px] md:flex-row"> <div className='flex flex-col items-center gap-[16px] md:flex-row'>
<FileUploader <FileUploader
multiple={false} multiple={false}
handleChange={(file) => { handleChange={(file) => {
@@ -197,18 +207,21 @@ export default function HostVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{frontImage?.name ? ( {frontImage?.name ? (
<img <img
src={readImage(frontImage, "front-preview")} src={readImage(frontImage, "front-preview")}
id="front-preview" id='front-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Front</h4> <h4 className='text-xl font-semibold'>Front</h4>
<p className="px-[20px]"> <p className='px-[20px]'>
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) <strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -221,18 +234,21 @@ export default function HostVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{backImage?.name ? ( {backImage?.name ? (
<img <img
src={readImage(backImage, "back-preview")} src={readImage(backImage, "back-preview")}
id="back-preview" id='back-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Back</h4> <h4 className='text-xl font-semibold'>Back</h4>
<p className="px-[20px]"> <p className='px-[20px]'>
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) <strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -247,18 +263,23 @@ export default function HostVerificationPage() {
}} }}
types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} types={["SVG", "JPEG", "PNG", "GIF", "JPG"]}
> >
<div className="flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]"> <div className='flex h-[130px] w-full max-w-full cursor-pointer flex-col items-center justify-center gap-[12px] border-2 border-dashed border-[#D0D5DD] text-sm md:w-[333px]'>
{passport?.name ? ( {passport?.name ? (
<img <img
src={readImage(passport, "passport-preview")} src={readImage(passport, "passport-preview")}
id="passport-preview" id='passport-preview'
className="h-full w-full rounded-sm object-cover" className='h-full w-full rounded-sm object-cover'
/> />
) : ( ) : (
<> <>
<h4 className="text-xl font-semibold">Passport page with photo</h4> <h4 className='text-xl font-semibold'>
<p className="px-[20px]"> Passport page with photo
<strong className="font-semibold underline">Click to upload</strong> or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) </h4>
<p className='px-[20px]'>
<strong className='font-semibold underline'>
Click to upload
</strong>{" "}
or drag and drop SVG, PNG, JPG or GIF (max. 800x400px)
</p> </p>
</> </>
)} )}
@@ -266,43 +287,61 @@ export default function HostVerificationPage() {
</FileUploader> </FileUploader>
)} )}
</div> </div>
<div className="my-8 max-w-lg"> <div className='my-8 max-w-lg'>
<label <label
className="mb-2 block text-sm font-bold text-gray-700" className='mb-2 block text-sm font-bold text-gray-700'
htmlFor="expiry_date" htmlFor='expiry_date'
> >
Expiry Date Expiry Date
</label> </label>
<input <input
type="date" type='date'
placeholder="expiry_date" placeholder='expiry_date'
{...register("expiry_date")} {...register("expiry_date")}
className={`focus:shadow-outline !min-h-[40px] w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none`} className={`focus:shadow-outline !min-h-[40px] w-full rounded border px-3 py-2 leading-tight text-gray-700 focus:outline-none`}
/> />
{errors.expiry_date?.message && <p className="my-3 rounded-md border border-[#C42945] bg-white py-2 px-3 text-center text-sm normal-case text-[#C42945]">{errors.expiry_date?.message}</p>} {errors.expiry_date?.message && (
<p className='my-3 rounded-md border border-[#C42945] bg-white px-3 py-2 text-center text-sm normal-case text-[#C42945]'>
{errors.expiry_date?.message}
</p>
)}
</div> </div>
<LoadingButton <LoadingButton
loading={loading} loading={loading}
type="submit" type='submit'
className={`login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"} mt-8 w-[333px] max-w-full`} className={`login-btn-gradient submit-doc-btn rounded tracking-wide text-white outline-none focus:outline-none ${
loading ? "py-1" : "py-2"
} mt-8 w-[333px] max-w-full`}
disabled={isDisabled()} disabled={isDisabled()}
data-tour='submit-doc-btn'
> >
Submit Document Submit Document
</LoadingButton> </LoadingButton>
</form> </form>
<div className={showVerified ? "popup-container flex items-center justify-center normal-case" : "hidden"}> <div
className={
showVerified
? "popup-container flex items-center justify-center normal-case"
: "hidden"
}
>
<div <div
className={`${verified ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`} className={`${
verified ? "pop-in" : "pop-out"
} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<h2 className="mb-4 text-3xl font-semibold"> <h2 className='mb-4 text-3xl font-semibold'>
<GreenCheckIcon /> <GreenCheckIcon />
Document received Document received
</h2> </h2>
<p className="mb-4 text-sm text-gray-500">Once we verify your document you will receive an email. It usually takes up to 24 hours.</p> <p className='mb-4 text-sm text-gray-500'>
Once we verify your document you will receive an email. It usually
takes up to 24 hours.
</p>
<button <button
type="button" type='button'
className="login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none'
onClick={() => { onClick={() => {
setVerified(false); setVerified(false);
navigate(searchParams.get("redirect_uri") ?? -1); navigate(searchParams.get("redirect_uri") ?? -1);