initial commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { PlusCircleIcon } from "@heroicons/react/24/outline";
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
const AddButton = ({ link, text }) => {
|
||||
return (
|
||||
<>
|
||||
<NavLink
|
||||
to={link}
|
||||
className="ml-5 mb-1 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
|
||||
>
|
||||
<PlusCircleIcon className="h-6 w-6" />
|
||||
<span className="ml-2">{text ? text : ""}</span>
|
||||
</NavLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React from "react";
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { useEffect } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { NOTIFICATION_STATUS } from "@/utils/constants";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { useContext } from "react";
|
||||
import adminNavigationItems from "@/utils/adminNavigationItems";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowLeftOnRectangleIcon, ArrowsRightLeftIcon } from "@heroicons/react/24/outline";
|
||||
import LogoIcon from "./Icons/LogoIcon";
|
||||
|
||||
export const AdminHeader = () => {
|
||||
const { state, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch, state: authState } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function fetchNotificationCount() {
|
||||
const sdk = new MkdSDK();
|
||||
sdk.setTable("notification");
|
||||
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ payload: { status: NOTIFICATION_STATUS.NOT_ADDRESSED } }, "GETALL");
|
||||
const g = result?.list?.filter((not) => Number(not?.status) == 0)
|
||||
|
||||
globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: g.length });
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message);
|
||||
tokenExpireError(dispatch, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function switchToHost() {
|
||||
dispatch({ type: "SWITCH_TO_HOST" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a host`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
function switchToCustomer() {
|
||||
dispatch({ type: "SWITCH_TO_CUSTOMER" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a customer`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let interval = setInterval(() => {
|
||||
fetchNotificationCount();
|
||||
}, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`sidebar-holder overflow-y-auto border-r-4 border-gray-100 ${!state.isOpen ? "open-nav" : ""}`}>
|
||||
<div className="sticky top-0 h-fit pb-8">
|
||||
<div className="mt-4 w-full p-4">
|
||||
<div className="mx-auto w-10/12 text-center text-2xl font-bold text-black">
|
||||
<LogoIcon fill={"#1D2939"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-list w-full">
|
||||
<ul className="flex flex-wrap">
|
||||
{adminNavigationItems.map((item) => {
|
||||
if (item.sub_categories) {
|
||||
return (
|
||||
<li
|
||||
key={item.path}
|
||||
style={{ display: "relative" }}
|
||||
className={`super-nav relative mx-auto my-auto block w-10/12 list-none justify-between rounded-lg ${item.sub_categories.length > 7 ? "larger" : ""} ${item.sub_categories.length > 2 ? "large" : ""} ${item.sub_categories.length < 2 ? "small" : ""
|
||||
}`}
|
||||
onClick={(e) => e.currentTarget.classList.toggle("open")}
|
||||
>
|
||||
<NavLink
|
||||
to={`/admin/${item.path}`}
|
||||
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
|
||||
>
|
||||
<span className="mr-3">{item.icon}</span>
|
||||
<span>{item.title}</span>
|
||||
<span className="flex flex-grow justify-end">
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</span>
|
||||
</NavLink>
|
||||
<div className="nav-item-dropdown absolute w-full">
|
||||
{item.sub_categories.map((sub, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`group mx-auto my-auto block w-10/12 list-none justify-between truncate rounded-lg`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<NavLink
|
||||
to={`/admin/${sub.path}`}
|
||||
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == sub.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
|
||||
>
|
||||
<span className="mr-3">{sub.icon}</span>
|
||||
<span>{sub.title}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.path == "notification") {
|
||||
return (
|
||||
<li
|
||||
key={item.path}
|
||||
className={`group relative mx-auto my-auto block w-10/12 list-none justify-between rounded-lg`}
|
||||
>
|
||||
<NavLink
|
||||
to={`/admin/${item.path}`}
|
||||
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
|
||||
>
|
||||
<span className="mr-3">{item.icon}</span>
|
||||
<strong
|
||||
className={`${state.adminNotificationCount > 0 ? "inline" : "hidden"
|
||||
} absolute right-1 flex h-8 w-8 items-center justify-center rounded-full border bg-red-400 px-2 text-xs text-white`}
|
||||
>
|
||||
{state.adminNotificationCount}
|
||||
</strong>
|
||||
<span>{item.title}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
key={item.path}
|
||||
className={`group mx-auto my-auto block w-10/12 list-none justify-between rounded-lg`}
|
||||
>
|
||||
<NavLink
|
||||
to={`/admin/${item.path}`}
|
||||
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
|
||||
>
|
||||
<span className="mr-3">{item.icon}</span>
|
||||
<span>{item.title}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
|
||||
<a
|
||||
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: "Are you sure?",
|
||||
modalShowMessage: "You are about to log out.",
|
||||
modalBtnText: "Yes, Log Out",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="mr-3">{<ArrowLeftOnRectangleIcon className="h-6 w-6" />}</span>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
|
||||
<a
|
||||
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
|
||||
onClick={switchToCustomer}
|
||||
>
|
||||
<span className="mr-3">
|
||||
<ArrowsRightLeftIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<span>Switch to customer</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
|
||||
<a
|
||||
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
|
||||
onClick={switchToHost}
|
||||
>
|
||||
<span className="mr-3">
|
||||
<ArrowsRightLeftIcon className="h-6 w-6" />
|
||||
</span>
|
||||
<span>Switch to Host</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHeader;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { CardCvcElement, CardExpiryElement, CardNumberElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { LoadingButton } from "../frontend";
|
||||
|
||||
export default function AddCardMethodModal({ modalOpen, closeModal, onSuccess }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const sdk = new MkdSDK();
|
||||
const [ctrl] = useState(new AbortController());
|
||||
|
||||
const addNewCard = async (e) => {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
// create stripe token
|
||||
try {
|
||||
const cardNum = elements.getElement("cardNumber");
|
||||
const result = await stripe.createToken(cardNum).then(async (r) => {
|
||||
if (r.error) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
message: r.error?.message ? r.error?.message : r?.trace?.raw?.message,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await sdk.createCustomerStripeCard({ sourceToken: r ? r.token.id : result.token.id }, ctrl.signal);
|
||||
closeModal();
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.name == "AbortError") {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
console.log(error)
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
message: error?.message ? error?.message : "Declined",
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Add Modal UI here to allow card to be created */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useContext, useState } from "react";
|
||||
import { LoadingButton } from "../frontend";
|
||||
|
||||
export default function DeleteCardMethodModal({ modalOpen, closeModal, onSuccess, card }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const sdk = new MkdSDK();
|
||||
const [ctrl] = useState(new AbortController());
|
||||
|
||||
async function onSubmit(e) {
|
||||
setLoading(true);
|
||||
e.preventDefault();
|
||||
try {
|
||||
await sdk.deleteCustomerStripeCard(card.id, ctrl.signal);
|
||||
closeModal();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
if (err.name == "AbortError") {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
tokenExpireError(dispatch, err.message);
|
||||
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Card Deletion Failed", message: err.message } });
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<div className="mb-[18px] flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="mb-[8px] text-2xl font-semibold"
|
||||
>
|
||||
Delete Payment Method
|
||||
</Dialog.Title>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p>You are about to remove card ending with {card.last4}</p>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 flex-grow rounded border-2 border-[#98A2B3] py-2 tracking-wide outline-none focus:outline-none"
|
||||
onClick={() => {
|
||||
ctrl.abort();
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`mt-4 flex-grow rounded bg-[#D92D20] ${loading ? "py-1 px-4" : "py-2"} tracking-wide text-white outline-none focus:outline-none`}
|
||||
>
|
||||
Yes, remove
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
const Button = ({ text, setResetClicked }) => {
|
||||
return (<button onClick={() => setResetClicked(false)} type="submit" className=" ml-2 rounded-md font-inter px-[66px] py-[10px] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent border border-[#33D4B7]">
|
||||
{text}
|
||||
</button>)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
export default function CaretDownIcon() {
|
||||
return (
|
||||
<svg
|
||||
className=" w-6 float-right inline-block"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
export default function CaretUpIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="w-6 inline-block float-right"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import React from "react";
|
||||
import { useContext } from "react";
|
||||
import GreenCheckIcon from "./frontend/icons/GreenCheckIcon";
|
||||
|
||||
export default function ConfirmationModal() {
|
||||
const { state, dispatch } = useContext(GlobalContext);
|
||||
|
||||
if (!state.confirmation) return null;
|
||||
|
||||
return (
|
||||
<div className={"popup-container z-100 flex items-center justify-center normal-case"}>
|
||||
<div
|
||||
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()}
|
||||
>
|
||||
<h2 className="mb-4 text-3xl font-semibold">
|
||||
<GreenCheckIcon />
|
||||
{state.confirmationHeading}
|
||||
</h2>
|
||||
<p className="mb-4 text-sm text-gray-500">{state.confirmationMsg}</p>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
export default function CounterV2({ setValue, name, maxCount, minCount, control }) {
|
||||
const { field, fieldState, formState } = useController({ control, name });
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-[10px] p-2 font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
className={"rounded-md border-2 border-black px-3 text-2xl disabled:border-[#D0D5DD]"}
|
||||
onClick={() => setValue(Number(field.value) - 1)}
|
||||
disabled={Number(field.value) <= (minCount || 0)}
|
||||
onBlur={field.onBlur}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{field.value}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={"rounded-md border-2 border-black px-3 text-2xl disabled:border-[#D0D5DD]"}
|
||||
onClick={() => setValue(Number(field.value) + 1)}
|
||||
disabled={Number(field.value) >= maxCount}
|
||||
onBlur={field.onBlur}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
export default function CustomComboBox({ control, name, setValue, containerClassName, valueField, labelField, items, ...restProps }) {
|
||||
const { field, fieldState, formState } = useController({ control, name });
|
||||
|
||||
const filteredItems =
|
||||
field.value === ""
|
||||
? items
|
||||
: items
|
||||
.filter((item) => item[labelField].toLowerCase().replace(/\s+/g, "").includes(field.value.toLowerCase().replace(/\s+/g, "")))
|
||||
.sort((a, b) => {
|
||||
if (a[labelField].toLowerCase().indexOf(field.value.toLowerCase()) > b[labelField].toLowerCase().indexOf(field.value.toLowerCase())) {
|
||||
return 1;
|
||||
} else if (a[labelField].toLowerCase().indexOf(field.value.toLowerCase()) < b[labelField].toLowerCase().indexOf(field.value.toLowerCase())) {
|
||||
return -1;
|
||||
} else {
|
||||
if (a[labelField] > b[labelField]) return 1;
|
||||
else return -1;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={"div"}
|
||||
className={`${containerClassName ?? ""}`}
|
||||
value={field.value}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Combobox.Input
|
||||
{...restProps}
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`tiny-scroll absolute left-0 right-0 top-full z-50 mt-2 max-h-60 w-full origin-top cursor-pointer divide-y divide-gray-100 overflow-y-auto rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none ${
|
||||
filteredItems.length > 0 ? "py-2 shadow-lg ring-1" : ""
|
||||
}`}
|
||||
>
|
||||
{filteredItems.map((item, idx) => (
|
||||
<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"
|
||||
key={idx}
|
||||
value={item[valueField]}
|
||||
>
|
||||
{item[labelField]}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
export default function CustomComboBoxV2({ control, name, setValue, className, valueField, labelField, getItems, ...restProps }) {
|
||||
const { field, fieldState, formState } = useController({ control, name });
|
||||
const [items, setItems] = useState([]);
|
||||
const [query, setQuery] = useState("");
|
||||
const [selected, setSelected] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
getItems("", setItems, field.value);
|
||||
}, [field.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected[labelField]) {
|
||||
setQuery(selected[labelField]);
|
||||
}
|
||||
}, [selected[valueField]]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(items.find((item) => item[valueField] == field.value) ?? {});
|
||||
}, [field.value, items.length]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={"div"}
|
||||
className={`${className ?? ""} ${fieldState.error ? "border-red-500" : ""} normal-case`}
|
||||
value={field.value}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Combobox.Input
|
||||
{...restProps}
|
||||
className="w-full truncate border-0 text-black focus:outline-none"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
if (e.target.value.trim() == "") {
|
||||
setValue("");
|
||||
}
|
||||
getItems(e.target.value, setItems, field.value);
|
||||
}}
|
||||
value={query}
|
||||
onBlur={field.onBlur}
|
||||
ref={field.ref}
|
||||
name={field.name}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Combobox.Options
|
||||
className={`tiny-scroll absolute left-0 right-0 top-full z-50 mt-2 max-h-60 w-full origin-top cursor-pointer divide-y divide-gray-100 overflow-y-auto rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none ${
|
||||
items.length > 0 ? "py-2 shadow-lg ring-1" : ""
|
||||
}`}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<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"
|
||||
key={idx}
|
||||
value={item[valueField]}
|
||||
>
|
||||
{item[labelField]}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService";
|
||||
import { useController } from "react-hook-form";
|
||||
import LocationIcon from "./frontend/icons/LocationIcon";
|
||||
|
||||
export default function CustomLocationAutoCompleteV2({ type, control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) {
|
||||
const { field } = useController({ control, name });
|
||||
|
||||
const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({
|
||||
apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
|
||||
options: { types: suggestionType ?? ["(region)"] },
|
||||
debounce: 200,
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={"div"}
|
||||
className={`relative w-full normal-case z-100 ${containerClassName ?? ""}`}
|
||||
value={(typeof field.value === "object") ? field.value : (field.value)?.replace(', undefined', '')}
|
||||
>
|
||||
{!hideIcons && <LocationIcon />}
|
||||
|
||||
<Combobox.Input
|
||||
{...restProps}
|
||||
autoComplete="off"
|
||||
className={`w-full truncate text-black ${className ?? ""}`}
|
||||
onBlur={field.onBlur}
|
||||
value={(typeof field.value === "object") ? field.value : (field.value)?.replace(', undefined', '')}
|
||||
onChange={(evt) => {
|
||||
field.onChange(evt);
|
||||
getPlacePredictions({ input: evt.target.value });
|
||||
}}
|
||||
/>
|
||||
{!hideIcons && field.value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue("");
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{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">
|
||||
<svg
|
||||
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
|
||||
width="36px"
|
||||
height="36px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#d0d5dd"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
repeatCount="indefinite"
|
||||
dur="1.6666666666666667s"
|
||||
keyTimes="0;1"
|
||||
values="0;256.58892822265625"
|
||||
></animate>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox.Options
|
||||
className={`${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) => (
|
||||
<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"
|
||||
key={idx}
|
||||
value={place.structured_formatting.main_text}
|
||||
onClick={() => 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>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Transition>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { useController } from "react-hook-form";
|
||||
import { ChevronUpDownIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function CustomSelectV2({ control, name, containerClassName, items, labelField, valueField, placeholder, shouldUnregister, ...restProps }) {
|
||||
const { field, fieldState } = useController({ control, name, shouldUnregister: shouldUnregister ?? true });
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const selected = items.find((item) => item[valueField] === (typeof field.value !== "number" ? field.value : +field.value));
|
||||
const defaultImage = items.find((item) => item["type"] === "1");
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-md focus:outline-none active:outline-none ${containerClassName}`}>
|
||||
<Listbox
|
||||
as={"fragment"}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
>
|
||||
<Listbox.Button
|
||||
className={`flex h-full w-full items-center justify-between ${field.value === "" ? "text-gray-500" : ""} ${restProps.className ?? ""} ${dropdownOpen ? restProps.openClassName ?? "" : ""}`}
|
||||
>
|
||||
<span className="block truncate">{selected ? selected[labelField] : defaultImage === undefined ? placeholder : defaultImage["name"]}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
afterEnter={field.onBlur}
|
||||
beforeEnter={() => setDropdownOpen(true)}
|
||||
afterLeave={() => setDropdownOpen(false)}
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute z-50 mt-1 w-full max-h-60 md:max-w-lg overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm`}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<Listbox.Option
|
||||
key={idx}
|
||||
className={`relative cursor-pointer select-none py-2 pr-4 pl-4 ui-active:bg-amber-100 ui-active:text-amber-900 ui-not-active:text-gray-900`}
|
||||
value={item[valueField]}
|
||||
>
|
||||
{item[labelField]}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useContext, useState } from "react";
|
||||
import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService";
|
||||
import { useController } from "react-hook-form";
|
||||
import LocationIcon from "./frontend/icons/LocationIcon";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
|
||||
export default function CustomStaticLocationAutoCompleteV2({ type, control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) {
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const [location, setLocation] = useState(globalState.location);
|
||||
|
||||
|
||||
const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({
|
||||
apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
|
||||
options: { types: suggestionType ?? ["(region)"] },
|
||||
debounce: 200,
|
||||
});
|
||||
return (
|
||||
<Combobox
|
||||
as={"div"}
|
||||
className={`relative w-full normal-case z-100 ${containerClassName ?? ""}`}
|
||||
value={location}
|
||||
>
|
||||
{!hideIcons && <LocationIcon />}
|
||||
|
||||
<Combobox.Input
|
||||
{...restProps}
|
||||
autoComplete="off"
|
||||
className={`w-full truncate text-black ${className ?? ""}`}
|
||||
value={globalState.location}
|
||||
onChange={(evt) => {
|
||||
setLocation(evt.target.value)
|
||||
getPlacePredictions({ input: evt.target.value });
|
||||
}}
|
||||
/>
|
||||
{!hideIcons && globalState.location && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue("");
|
||||
setLocation("");
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{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">
|
||||
<svg
|
||||
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
|
||||
width="36px"
|
||||
height="36px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#d0d5dd"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
repeatCount="indefinite"
|
||||
dur="1.6666666666666667s"
|
||||
keyTimes="0;1"
|
||||
values="0;256.58892822265625"
|
||||
></animate>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox.Options
|
||||
className={`${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) => (
|
||||
<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"
|
||||
key={idx}
|
||||
value={place.structured_formatting.main_text}
|
||||
onClick={() =>
|
||||
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>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Transition>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useContext, useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTour } from "@reactour/tour";
|
||||
|
||||
export default function CustomerGettingStartedTour() {
|
||||
const navigate = useNavigate();
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const [modalOpen, setModalOpen] = useState(true);
|
||||
const [gettingStarted, setGettingStarted] = useState();
|
||||
const { pathname } = useLocation();
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const { setIsOpen } = useTour()
|
||||
|
||||
async function markAsNotFirstTimeUser() {
|
||||
try {
|
||||
await sdk.callRawAPI("/v2/api/custom/ergo/edit-self", { profile: { getting_started: 1 } }, "POST");
|
||||
globalDispatch({
|
||||
type: "SET_USER_DATA",
|
||||
payload: {
|
||||
...globalState.user,
|
||||
getting_started: 1,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
console.log("err", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalState.user.id) return null;
|
||||
|
||||
const fetchUser = async () => {
|
||||
const result = await sdk.callRawAPI("/rest/profile/GETALL", {
|
||||
"payload": {
|
||||
"user_id": Number(globalState.user.id)
|
||||
},
|
||||
"selectStr": "*"
|
||||
},
|
||||
"POST");
|
||||
setGettingStarted(result.list[0]?.getting_started)
|
||||
}
|
||||
|
||||
fetchUser()
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen && gettingStarted == 0}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setModalOpen(false)}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
First time login?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Would you like a tour of the site?</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
markAsNotFirstTimeUser();
|
||||
}}
|
||||
>
|
||||
No thanks
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`login-btn-gradient inline-flex justify-center rounded-md py-2 px-4 text-sm font-medium text-white`}
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
setIsOpen(true)
|
||||
globalDispatch({ type: "START_TOUR" });
|
||||
}}
|
||||
>
|
||||
Yes please
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { sleep } from "@/utils/utils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AuthContext, tokenExpireError } from "../authContext";
|
||||
import LogoIcon from "./frontend/icons/LogoIcon";
|
||||
import NavMenu from "./frontend/NavMenu";
|
||||
import StaticSearchBar from "./frontend/StaticSearchBar";
|
||||
|
||||
const getNavBarVariant = (path) => {
|
||||
if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/help")) {
|
||||
return "light";
|
||||
}
|
||||
switch (path) {
|
||||
case "/contact-us":
|
||||
case "/faq":
|
||||
return "white";
|
||||
case "/search":
|
||||
case "/explore":
|
||||
case "/favorites":
|
||||
case "/become-a-host":
|
||||
case "/reset-password":
|
||||
return "light";
|
||||
default:
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
|
||||
export const CustomerHeader = () => {
|
||||
const { state: authState, dispatch } = React.useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [variant, setVariant] = useState(getNavBarVariant(pathname));
|
||||
|
||||
async function fetchProfile() {
|
||||
const sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.getProfileCustom();
|
||||
globalDispatch({ type: "SET_USER_DATA", payload: result });
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
tokenExpireError(dispatch, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
if (pathname == "/") {
|
||||
if (window.scrollY > 10) {
|
||||
setVariant("white");
|
||||
} else {
|
||||
setVariant("transparent");
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", onScroll);
|
||||
fetchProfile();
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setVariant(getNavBarVariant(pathname));
|
||||
}, [pathname]);
|
||||
|
||||
const joinAsHostAdmin = async () => {
|
||||
let newLogin = { ...authState, role: "host" };
|
||||
globalDispatch({ type: "START_LOADING" });
|
||||
await sleep(500);
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
navigate("/");
|
||||
dispatch({ type: "LOGOUT" });
|
||||
dispatch({ type: "LOGIN", payload: newLogin });
|
||||
showToast(globalDispatch, "Joined as Host", 2000);
|
||||
};
|
||||
|
||||
async function becomeAHost() {
|
||||
// check if all fields are ready to go
|
||||
if (!(globalState.verificationType && globalState.dob && globalState.city && globalState.country && globalState.about)) {
|
||||
navigate("/become-a-host");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
user: { role: "host" },
|
||||
},
|
||||
"",
|
||||
);
|
||||
dispatch({ type: "SWITCH_TO_HOST" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a host`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
function switchToHost() {
|
||||
dispatch({ type: "SWITCH_TO_HOST" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a host`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
if (pathname.includes("/login") || pathname.includes("/signup")) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`fixed top-0 left-0 z-50 flex w-screen flex-wrap items-center justify-between py-4 px-4 text-sm duration-500 md:flex-nowrap md:rounded-br-[32px] md:rounded-bl-[32px] md:px-12 header-${variant}`}
|
||||
>
|
||||
<nav className={`gap-6`}>
|
||||
<Link
|
||||
to="/"
|
||||
className=""
|
||||
>
|
||||
<LogoIcon fill={variant == "transparent" || variant == "light" ? undefined : "#101828"} />
|
||||
</Link>
|
||||
</nav>
|
||||
<StaticSearchBar className="hidden lg:block" />
|
||||
|
||||
<div className="flex gap-4 space-x-4">
|
||||
<nav className={`z-50 inline `}>
|
||||
{" "}
|
||||
{pathname.startsWith("/account") && (
|
||||
<button
|
||||
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} ${variant == "transparent" ? "" : "border-white"}`}
|
||||
onClick={()=>navigate("/search?location=&booking_start_time=&max_capacity=&capacity=&size=")}
|
||||
>
|
||||
<span>Explore Spaces</span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<nav className="z-50 flex items-center gap-6">
|
||||
{authState.originalRole != "customer" ? (
|
||||
<button
|
||||
onClick={switchToHost}
|
||||
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} hidden whitespace-nowrap md:inline`}
|
||||
>
|
||||
<span>Join as host</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={becomeAHost}
|
||||
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} hidden whitespace-nowrap md:inline`}
|
||||
>
|
||||
<span>Become a host</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<NavMenu variant={variant} />
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<StaticSearchBar className="flex w-full justify-center py-4 md:hidden" />
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerHeader;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { formatDate } from "@/utils/date-time-utils";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect } from "react";
|
||||
import { Calendar } from "react-calendar";
|
||||
import { useController } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import CalendarIcon from "./frontend/icons/CalendarIcon";
|
||||
import NextIcon from "./frontend/icons/NextIcon";
|
||||
import PrevIcon from "./frontend/icons/PrevIcon";
|
||||
|
||||
export default function DatePickerV3({ control, name, placeholder, labelClassName, reset, min }) {
|
||||
const { field, fieldState, formState } = useController({ control, name });
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<div className={`w-full`}>
|
||||
<Popover className="lg:relative">
|
||||
<Popover.Button className={`flex w-full justify-between focus:outline-none ui-open:text-opacity-90`}>
|
||||
<div className={`flex gap-2 ${labelClassName ?? ""}`}>
|
||||
{!fieldState.isDirty ? (
|
||||
<CalendarIcon />
|
||||
) : (
|
||||
<span
|
||||
className={`self-end`}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
reset();
|
||||
searchParams.delete(name);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
)}
|
||||
<span className={`${!fieldState.isDirty ? "text-gray-400" : ""}`}>{fieldState.isDirty || searchParams.get(name) ? formatDate(field.value) : placeholder}</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
afterEnter={field.onBlur}
|
||||
>
|
||||
<Popover.Panel className={`absolute left-1/2 z-10 mt-3 -translate-x-1/2 transform px-4 pb-12 sm:px-0`}>
|
||||
{({ close }) => (
|
||||
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<Calendar
|
||||
onChange={(val) => {
|
||||
field.onChange(val);
|
||||
close();
|
||||
}}
|
||||
value={field.value}
|
||||
className={`calendar date-picker`}
|
||||
nextLabel={<NextIcon />}
|
||||
prevLabel={<PrevIcon />}
|
||||
next2Label={
|
||||
<div
|
||||
className="h-full w-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
prev2Label={
|
||||
<div
|
||||
className="h-full w-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
maxDetail="month"
|
||||
minDate={min}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||
import React, { Fragment } from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function ErrorModal() {
|
||||
const { state, dispatch } = useContext(GlobalContext);
|
||||
|
||||
if (!state.error) return null;
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={state.error}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50"
|
||||
onClose={() => dispatch({ type: "CLOSE_ERROR" })}
|
||||
>
|
||||
<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 z-10 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-50 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-lg transform overflow-hidden rounded-2xl bg-white p-6 text-center align-middle z-1000 shadow-xl transition-all">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => dispatch({ type: "CLOSE_ERROR" })}
|
||||
className="text-gray-500 duration-100 hover:text-black"
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="mt-8 text-2xl"
|
||||
>
|
||||
{state.errorHeading}
|
||||
</Dialog.Title>
|
||||
|
||||
<p className="tiny-scroll mt-4 max-h-[300px] overflow-y-auto text-wrap break-normal text-sm text-gray-500">{state.errorMsg}</p>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Icon from "./Icons";
|
||||
import "suneditor/dist/css/suneditor.min.css";
|
||||
|
||||
const Faq = ({ data }) => {
|
||||
const [showAnswer, setShowAnswer] = useState(false);
|
||||
const { dispatch } = useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="w-full p-1">
|
||||
<div className="flex justify-between">
|
||||
<div></div>
|
||||
<div>
|
||||
<button
|
||||
className="pr-2 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent font-bold border-r border-gray-200"
|
||||
onClick={() => {
|
||||
navigate(`/admin/edit-faq/${data.id}`, {
|
||||
state: data,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="font-semibold text-sm py-2.5 text-center inline-flex items-center mr-2 mb-2"
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowMessage: "Are you sure you want to delete this question?",
|
||||
modalShowTitle: "Confirm Changes",
|
||||
type: "BaasDelete",
|
||||
modalBtnText: "Yes, Delete",
|
||||
itemId: data.id,
|
||||
table1: "faq",
|
||||
backTo: "/admin/faq",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="ml-2"> Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-2 px-4 bg-white bg-opacity-60 border-[1px] border-gray-200">
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<div className="flex-1 p-2">
|
||||
<div className={`${showAnswer ? " mb-4" : ""}`}>
|
||||
<h4 className="text-lg font-semibold leading-normal">{data.question}</h4>
|
||||
</div>
|
||||
{showAnswer && (
|
||||
<p
|
||||
className="text-gray-600 font-medium sun-editor-editable"
|
||||
dangerouslySetInnerHTML={{ __html: data.answer }}
|
||||
></p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-auto p-2">
|
||||
{showAnswer ? (
|
||||
<Icon
|
||||
type="minus"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
onClick={() => setShowAnswer(!showAnswer)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
type="plus"
|
||||
className="h-4 w-4 cursor-pointer"
|
||||
onClick={() => setShowAnswer(!showAnswer)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Faq;
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useController } from "react-hook-form";
|
||||
import StarIcon from "./frontend/icons/StarIcon";
|
||||
|
||||
export default function FilterCheckBoxesV2({ name, control, setValue, reset, title, labelField, valueField, options }) {
|
||||
const { field, fieldState, formState } = useController({ control, name });
|
||||
|
||||
return (
|
||||
<div className="mb-[34px]">
|
||||
<Disclosure defaultOpen>
|
||||
<div className="mb-[12px] flex justify-between">
|
||||
<h4 className="flex w-full justify-between text-[16px] font-semibold lg:block">
|
||||
<span className="lg:mr-2 lg:border-r lg:pr-2">{title}</span>
|
||||
<button
|
||||
className="text-sm font-normal lowercase lg:text-xs"
|
||||
onClick={reset}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</h4>
|
||||
<Disclosure.Button className="hidden duration-200 ui-open:rotate-180 lg:inline">
|
||||
{" "}
|
||||
<svg
|
||||
width="14"
|
||||
height="8"
|
||||
viewBox="0 0 14 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13 7L7 1L1 7"
|
||||
stroke="#475467"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition-all ease duration-500"
|
||||
enterFrom="max-h-0"
|
||||
enterTo="max-h-[900px]"
|
||||
leave="transition-all ease duration-500"
|
||||
leaveFrom="max-h-[900px]"
|
||||
leaveTo="max-h-0"
|
||||
>
|
||||
<Disclosure.Panel className="overflow-hidden text-gray-500 duration-500">
|
||||
{options.map((op, idx) => (
|
||||
<div
|
||||
className="checkbox-container flex gap-2 items-center mb-[12px]"
|
||||
key={idx}
|
||||
>
|
||||
<input
|
||||
type={`${name === "capacity" ? "radio" : "checkbox"}`}
|
||||
id={op[valueField]}
|
||||
value={op[valueField]}
|
||||
className={`text-xl w-5 h-8 rounded ${name === "capacity" ? "accent-[#0D9895]" : ""}`}
|
||||
name={name}
|
||||
checked={ field.value.includes(op[valueField])}
|
||||
onChange={() => {
|
||||
if (name === "capacity") {
|
||||
field.onChange(op[valueField]);
|
||||
} else {
|
||||
const exists = field.value.includes(op[valueField]);
|
||||
if (exists) {
|
||||
field.onChange(field.value.filter((item) => item !== op[valueField]));
|
||||
} else {
|
||||
field.onChange([...field.value, op[valueField]]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
// onChange={() => {
|
||||
// // remove if in array else add
|
||||
// const exists = field.value.includes(op[valueField]);
|
||||
// if (exists && op[name] !== "capacity") {
|
||||
// field.onChange(field.value.filter((item) => item != op[valueField]));
|
||||
// return;
|
||||
// }
|
||||
// field.onChange([...field.value, op[valueField]]);
|
||||
// }}
|
||||
onBlur={field.onBlur}
|
||||
/>
|
||||
|
||||
{name !== "capacity" ?
|
||||
<label htmlFor={op[valueField]}>
|
||||
{op[labelField]}{" "}
|
||||
{title == "Reviews"
|
||||
? Array(Number(op[valueField]))
|
||||
.fill("")
|
||||
.map((_, idx) => (
|
||||
<span
|
||||
className="ml-1"
|
||||
key={idx}
|
||||
>
|
||||
<StarIcon />
|
||||
</span>
|
||||
))
|
||||
: null}
|
||||
</label>
|
||||
:
|
||||
<span >
|
||||
{op[labelField]}{" "}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { Fragment } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import PaginationBar from "./PaginationBar";
|
||||
import PaginationHeader from "./PaginationHeader";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import Icon from "./Icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { secondsToHour } from "@/utils/utils";
|
||||
import moment from "moment";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
const History = ({ id, table }) => {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const statusMapping = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Upcoming" },
|
||||
{ key: "2", value: "Ongoing" },
|
||||
{ key: "3", value: "Complete" },
|
||||
{ key: "4", value: "Declined" },
|
||||
{ key: "5", value: "Cancelled" }
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/booking/PAGINATE",
|
||||
{
|
||||
where: [
|
||||
table ? `${table === "customer" ? `customer.id = ${id}` : "1"} AND ${table === "host" ? `ergo_user.id = ${id}` : "1"} AND ${table == "property" ? `ergo_property.id = ${id}` : "1"}` : 1
|
||||
],
|
||||
page: pageNum,
|
||||
limit: limitNum
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
await getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="overflow-x-auto p-5 bg-white shadow rounded">
|
||||
{data.map((data) => (
|
||||
<div
|
||||
key={data.id}
|
||||
className="border rounded px-5 py-4 flex justify-between flex-col lg:flex-row mb-4"
|
||||
>
|
||||
<div>{ID_PREFIX.BOOKINGS + data.id}</div>
|
||||
<img
|
||||
src={data.image_url}
|
||||
className="h-24 max-w-[135px]"
|
||||
alt="property_image"
|
||||
/>
|
||||
<div className="min-w-[219px] max-w-[219px] mb-4">
|
||||
<p className="font-semibold text-xl text-[#101828] mb-1">{data.property_name}</p>
|
||||
<p className="text-xs font-medium mb-1">{data.space_category}</p>
|
||||
<p className="bg-gray-200 text-xs p-2 w-fit rounded">{statusMapping.find((status) => status.key == data.status)?.value}</p>
|
||||
</div>
|
||||
<div className="min-w-[219px] max-w-[219px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Host</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.host_last_name}, {data.host_first_name}{" "}
|
||||
</p>
|
||||
<p className="text-xs mb-1 font-medium ">Customer</p>
|
||||
<p className="mb-1 text-xs">
|
||||
{data.customer_last_name}, {data.customer_first_name}{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-[72px] max-w-[72px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Date</p>
|
||||
<p className="mb-1 text-sm">{moment(data.booking_start_time).format("MM/DD/YY")} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Duration</p>
|
||||
<p className="mb-1 text-xs">{secondsToHour(data.duration)} </p>
|
||||
</div>
|
||||
<div className="min-w-[72px] max-w-[72px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Rate</p>
|
||||
<p className="mb-1 text-sm">${data?.rate?.toFixed(2)} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Tax</p>
|
||||
<p className="mb-1 text-xs">${data?.tax?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="min-w-[72px] max-w-[72px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Total</p>
|
||||
<p className="mb-1 text-xs">${data?.total?.toFixed(2)} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Commission</p>
|
||||
<p className="mb-1 text-xs">${data?.commission?.toFixed(2)}</p>
|
||||
</div>
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative min-w-[60px] max-w-[60px] inline-block text-left"
|
||||
>
|
||||
<div className="">
|
||||
<Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#33D4B7] focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||
<Icon type="dots" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-0 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => navigate(`/admin/edit-booking/${data.id}`)}
|
||||
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} w-full text-left block px-4 py-2 text-sm`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
@@ -0,0 +1,140 @@
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useContext } from "react";
|
||||
import GreenCheckIcon from "./frontend/icons/GreenCheckIcon";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { LoadingButton } from "./frontend";
|
||||
|
||||
export default function HostAddAddonsModal({setAddOnModal, getData}) {
|
||||
let sdk = new MkdSDK();
|
||||
const { state, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [loading, setLoading] = React.useState();
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
cost: yup.number().required().typeError("Cost must be a number"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
sdk.setTable("add_on");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
name: data.name,
|
||||
cost: data.cost,
|
||||
creator_id: Number(localStorage.getItem("user")),
|
||||
space_id: data.space_id || null,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
getData();
|
||||
showToast(globalDispatch, "Added");
|
||||
setLoading(false)
|
||||
setAddOnModal(false)
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"popup-container flex items-center justify-center normal-case"}>
|
||||
<div
|
||||
className={`w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="add_on_id"
|
||||
>
|
||||
Add-Ons
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("name")}
|
||||
placeholder="Addon Name"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="add_on_cost"
|
||||
>
|
||||
Add-On Cost
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("cost")}
|
||||
placeholder="Addon Cost"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.cost?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() =>setAddOnModal(false)}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"}`}>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useContext } from "react";
|
||||
import GreenCheckIcon from "./frontend/icons/GreenCheckIcon";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { LoadingButton } from "./frontend";
|
||||
|
||||
export default function HostAddAmenityModal({setAmenityModal,getData}) {
|
||||
let sdk = new MkdSDK();
|
||||
const { state, dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const [loading, setLoading] = React.useState();
|
||||
const schema = yup
|
||||
.object({
|
||||
name: yup.string().required("Name is required"),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
sdk.setTable("amenity");
|
||||
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
name: data.name,
|
||||
creator_id: Number(localStorage.getItem("user")),
|
||||
space_id: data.space_id || null,
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error) {
|
||||
getData();
|
||||
showToast(globalDispatch, "Amenity Added");
|
||||
setLoading(false)
|
||||
setAmenityModal(false)
|
||||
} else {
|
||||
if (result.validation) {
|
||||
const keys = Object.keys(result.validation);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const field = keys[i];
|
||||
setError(field, {
|
||||
type: "manual",
|
||||
message: result.validation[field],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
console.log("Error", error);
|
||||
setError("name", {
|
||||
type: "manual",
|
||||
message: error.message,
|
||||
});
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"popup-container flex items-center justify-center normal-case"}>
|
||||
<div
|
||||
className={`w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<form
|
||||
className=" w-full max-w-lg"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="mb-2 block text-sm font-bold text-gray-700"
|
||||
htmlFor="add_on_id"
|
||||
>
|
||||
Amenity
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
|
||||
{...register("name")}
|
||||
placeholder="Amenity Name"
|
||||
/>
|
||||
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() =>setAmenityModal(false)}
|
||||
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"}`}>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { Fragment, useContext, useEffect, useState } from "react";
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTour } from "@reactour/tour";
|
||||
|
||||
export default function HostGettingStartedTour() {
|
||||
const navigate = useNavigate();
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const [modalOpen, setModalOpen] = useState(true);
|
||||
const [gettingStarted, setGettingStarted] = useState();
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const { setIsOpen } = useTour()
|
||||
|
||||
async function markAsNotFirstTimeUser() {
|
||||
try {
|
||||
await sdk.callRawAPI("/v2/api/custom/ergo/edit-self", { profile: { getting_started: 1 } }, "POST");
|
||||
globalDispatch({
|
||||
type: "SET_USER_DATA",
|
||||
payload: {
|
||||
...globalState.user,
|
||||
getting_started: 1,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
tokenExpireError(dispatch, err.message);
|
||||
console.log("err", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalState.user.id) return null;
|
||||
|
||||
const fetchUser = async () => {
|
||||
const result = await sdk.callRawAPI("/rest/profile/GETALL", {
|
||||
"payload": {
|
||||
"user_id": Number(globalState.user.id)
|
||||
},
|
||||
"selectStr": "*"
|
||||
},
|
||||
"POST");
|
||||
setGettingStarted(result.list[0]?.getting_started)
|
||||
}
|
||||
|
||||
fetchUser()
|
||||
|
||||
const setTour = () => {
|
||||
setModalOpen(false);
|
||||
globalDispatch({ type: "START_TOUR" });
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen && gettingStarted == 0}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => setModalOpen(false)}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
First time login?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Would you like a tour of the site?</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
markAsNotFirstTimeUser();
|
||||
}}
|
||||
>
|
||||
No thanks
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`login-btn-gradient inline-flex justify-center rounded-md py-2 px-4 text-sm font-medium text-white`}
|
||||
onClick={() => {
|
||||
setTour()
|
||||
}}
|
||||
>
|
||||
Yes please
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import LogoIcon from "./frontend/icons/LogoIcon";
|
||||
import ReactTestUtils from "react-dom/test-utils";
|
||||
import StaticSearchBar from "./frontend/StaticSearchBar";
|
||||
import NavMenu from "./frontend/NavMenu";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
|
||||
const getNavBarVariant = (path) => {
|
||||
if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/spaces") || path.startsWith("/help")) {
|
||||
return "light";
|
||||
}
|
||||
switch (path) {
|
||||
case "/contact-us":
|
||||
case "/faq":
|
||||
return "white";
|
||||
case "/search":
|
||||
case "/explore":
|
||||
case "/favorites":
|
||||
case "/reset-password":
|
||||
return "light";
|
||||
default:
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
|
||||
export const HostHeader = () => {
|
||||
const { pathname } = useLocation();
|
||||
const [variant, setVariant] = useState(getNavBarVariant(pathname));
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
async function fetchProfile() {
|
||||
const sdk = new MkdSDK();
|
||||
try {
|
||||
const result = await sdk.getProfileCustom();
|
||||
globalDispatch({ type: "SET_USER_DATA", payload: result });
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
tokenExpireError(dispatch, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
if (pathname == "/") {
|
||||
if (window.scrollY > 10) {
|
||||
setVariant("white");
|
||||
} else {
|
||||
setVariant("transparent");
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
fetchProfile();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setVariant(getNavBarVariant(pathname));
|
||||
}, [pathname]);
|
||||
|
||||
const saveAsDraft = () => {
|
||||
console.log("clicked");
|
||||
const saveDraftBtn = document.getElementById("save-as-draft");
|
||||
if (saveDraftBtn) {
|
||||
ReactTestUtils.Simulate.click(saveDraftBtn);
|
||||
}
|
||||
};
|
||||
|
||||
if (pathname.includes("/login") || pathname.includes("/signup")) return null;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 z-50 flex w-screen flex-wrap items-center justify-between py-4 px-4 text-sm duration-500 md:flex-nowrap md:rounded-br-[32px] md:rounded-bl-[32px] md:px-12 header-${variant}`}
|
||||
>
|
||||
<nav className={`flex gap-6`}>
|
||||
<Link
|
||||
to="/"
|
||||
className=""
|
||||
>
|
||||
<LogoIcon fill={variant == "transparent" || variant == "light" ? undefined : "#101828"} />
|
||||
</Link>
|
||||
</nav>
|
||||
<StaticSearchBar className="hidden md:block" />
|
||||
|
||||
<div className="flex">
|
||||
<nav className={`z-50 inline `}>
|
||||
{" "}
|
||||
{pathname.startsWith("/account") && (
|
||||
<button
|
||||
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
|
||||
onClick={()=>navigate("/search?location=&booking_start_time=&max_capacity=&capacity=&size=")}
|
||||
>
|
||||
<span>Explore Spaces</span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
{["/spaces/add/4", "/spaces/add/5"].includes(pathname) || !pathname.startsWith("/spaces") ? (
|
||||
<>
|
||||
{" "}
|
||||
<Link
|
||||
to="/contact-us"
|
||||
className={`self-stretch rounded-md px-6 py-[5px] pb-[7px] font-normal my-border-${variant}`}
|
||||
>
|
||||
<span>Support</span>
|
||||
</Link>
|
||||
<NavMenu variant={variant} />
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
|
||||
onClick={saveAsDraft}
|
||||
>
|
||||
<span>Save as draft</span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
<nav className={`z-50 inline md:hidden`}>
|
||||
{" "}
|
||||
{pathname.startsWith("/spaces") && pathname != "/spaces/add/5" && pathname != "/spaces/add/4" ? (
|
||||
<button
|
||||
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
|
||||
onClick={saveAsDraft}
|
||||
>
|
||||
<span>Save as draft</span>
|
||||
</button>
|
||||
) : (
|
||||
<NavMenu variant={variant} />
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<StaticSearchBar className="flex w-full justify-center py-4 md:hidden" />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostHeader;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as ArrowNarrowLeft } from "../../assets/arrow-narrow-left.svg";
|
||||
import { ReactComponent as ArrowNarrowRight } from "../../assets/arrow-narrow-right.svg";
|
||||
|
||||
|
||||
const ArrowSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "narrow-right": return <ArrowNarrowRight
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "narrow-left": return <ArrowNarrowLeft
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ArrowSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as BankNoteOne } from "../../assets/bank-note-one.svg";
|
||||
|
||||
const BankNoteSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "one": return <BankNoteOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default BankNoteSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as BuildingOne } from "../../assets/building-one.svg";
|
||||
|
||||
const BuildingSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "one": return <BuildingOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default BuildingSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Calender } from "../../assets/calender.svg";
|
||||
|
||||
const CalenderSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
default: return <Calender
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default CalenderSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as ChevronDown } from "../../assets/chevron-down.svg";
|
||||
|
||||
const CalenderSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case 'down': return <ChevronDown
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default CalenderSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Dots } from "../../assets/dots.svg";
|
||||
|
||||
const DotsSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Dots
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default DotsSvg
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as FileCheckThree } from "../../assets/file-check-three.svg";
|
||||
import { ReactComponent as FilePlusThree } from "../../assets/file-plus-three.svg";
|
||||
import { ReactComponent as FileQuestionThree } from "../../assets/file-question-three.svg";
|
||||
import { ReactComponent as FileSearchOne } from "../../assets/file-search-one.svg";
|
||||
|
||||
|
||||
const BuildingSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "check-three": return <FileCheckThree
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "plus-three": return <FilePlusThree
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "question-three": return <FileQuestionThree
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "search-one": return <FileSearchOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default BuildingSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as GridOne } from "../../assets/grid-one.svg";
|
||||
|
||||
const GridSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "one": return <GridOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default GridSvg
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as HomeThree } from "../../assets/home-three.svg";
|
||||
import { ReactComponent as HomeLine } from "../../assets/home-line.svg";
|
||||
|
||||
const HomeSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "three": return <HomeThree
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "line": return <HomeLine
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default HomeSvg
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
const HouseIcon = () => (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.40156 1.8778C8.59363 1.48869 8.68967 1.29413 8.82004 1.23197C8.93347 1.17789 9.06525 1.17789 9.17869 1.23197C9.30906 1.29413 9.4051 1.48869 9.59717 1.8778L11.4194 5.56944C11.4761 5.68432 11.5045 5.74176 11.5459 5.78635C11.5826 5.82584 11.6266 5.85783 11.6754 5.88056C11.7306 5.90623 11.794 5.91549 11.9208 5.93402L15.9968 6.5298C16.4261 6.59253 16.6407 6.6239 16.74 6.72874C16.8264 6.81995 16.8671 6.94529 16.8506 7.06985C16.8317 7.21302 16.6763 7.36436 16.3656 7.66702L13.4172 10.5387C13.3253 10.6282 13.2794 10.673 13.2497 10.7263C13.2235 10.7734 13.2066 10.8252 13.2001 10.8788C13.1928 10.9393 13.2036 11.0025 13.2253 11.129L13.921 15.1851C13.9944 15.6129 14.031 15.8269 13.9621 15.9538C13.9021 16.0642 13.7955 16.1417 13.6719 16.1646C13.5299 16.1909 13.3378 16.0899 12.9536 15.8879L9.30966 13.9716C9.19613 13.9119 9.13936 13.882 9.07955 13.8703C9.0266 13.8599 8.97213 13.8599 8.91918 13.8703C8.85937 13.882 8.8026 13.9119 8.68906 13.9716L5.04512 15.8879C4.66095 16.0899 4.46886 16.1909 4.32683 16.1646C4.20325 16.1417 4.09662 16.0642 4.03663 15.9538C3.96768 15.8269 4.00437 15.6129 4.07774 15.1851L4.77342 11.129C4.79511 11.0025 4.80595 10.9393 4.79862 10.8788C4.79212 10.8252 4.77528 10.7734 4.74902 10.7263C4.71937 10.673 4.67341 10.6282 4.5815 10.5387L1.63315 7.66702C1.3224 7.36436 1.16703 7.21302 1.14812 7.06985C1.13167 6.94529 1.17231 6.81995 1.25872 6.72874C1.35804 6.6239 1.57266 6.59253 2.00189 6.5298L6.07794 5.93402C6.2047 5.91549 6.26808 5.90623 6.32328 5.88056C6.37215 5.85783 6.41615 5.82584 6.45284 5.78635C6.49427 5.74176 6.52262 5.68432 6.57933 5.56944L8.40156 1.8778Z"
|
||||
stroke="#667085"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default HouseIcon;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as ImageThree } from "../../assets/image-three.svg";
|
||||
|
||||
const ImageSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "three": return <ImageThree
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function LogoIcon({ fill }) {
|
||||
return (
|
||||
<svg
|
||||
width="69"
|
||||
height="25"
|
||||
viewBox="0 0 69 25"
|
||||
fill={fill ?? "black"}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.95426 19.1764C6.3634 19.1764 4.96896 18.8828 3.77091 18.2957C2.57286 17.7086 1.63995 16.8476 0.972187 15.7125C0.324062 14.5775 0 13.2076 0 11.6029C0 10.0764 0.324062 8.74567 0.972187 7.61063C1.63995 6.47558 2.56304 5.59494 3.74145 4.9687C4.91986 4.3229 6.30448 4 7.89534 4C9.40763 4 10.7333 4.27398 11.8725 4.82193C13.0312 5.35032 13.9249 6.13311 14.5533 7.1703C15.2015 8.18793 15.5255 9.4404 15.5255 10.9277C15.5255 11.1821 15.5157 11.4267 15.4961 11.6616C15.4764 11.8768 15.447 12.0921 15.4077 12.3074H2.03275V10.5461H13.316L12.4911 11.6322C12.5108 11.4365 12.5206 11.2506 12.5206 11.0745C12.5206 10.8788 12.5206 10.6831 12.5206 10.4874C12.5206 9.13707 12.1278 8.13901 11.3422 7.49321C10.5762 6.82783 9.40763 6.49515 7.83642 6.49515C6.08844 6.49515 4.84129 6.89633 4.09497 7.69869C3.34864 8.50105 2.97548 9.65567 2.97548 11.1625V11.9258C2.97548 13.4522 3.34864 14.6166 4.09497 15.419C4.84129 16.2213 6.09826 16.6225 7.86588 16.6225C9.39781 16.6225 10.4977 16.3877 11.1654 15.918C11.8528 15.4288 12.1965 14.7536 12.1965 13.8925V13.6577H15.3782V13.9219C15.3782 14.9591 15.0542 15.8789 14.406 16.6812C13.7776 17.464 12.9036 18.0805 11.7841 18.5306C10.6842 18.9611 9.40763 19.1764 7.95426 19.1764Z" />
|
||||
<path d="M21.6795 18.8828H18.4978V4.29355H21.4143V8.43256L21.6795 8.57933V18.8828ZM21.6795 10.6929H20.9724V8.22707H21.6206C21.7581 7.42471 22.033 6.71042 22.4455 6.08418C22.8579 5.43838 23.4078 4.92957 24.0952 4.55774C24.8023 4.18591 25.6566 4 26.6583 4C27.7778 4 28.691 4.23484 29.3981 4.70451C30.1051 5.17419 30.6158 5.80042 30.93 6.58321C31.2639 7.366 31.4308 8.21729 31.4308 9.13707V11.0451H28.2786V9.75352C28.2786 8.69675 28.0429 7.92374 27.5715 7.4345C27.1002 6.94525 26.3146 6.70063 25.2147 6.70063C23.9577 6.70063 23.0543 7.0431 22.5044 7.72804C21.9545 8.41299 21.6795 9.40126 21.6795 10.6929Z" />
|
||||
<path d="M41.7076 24.1667C40.2542 24.1667 38.9776 23.9416 37.8778 23.4915C36.7779 23.061 35.9138 22.4347 35.2853 21.6128C34.6764 20.7909 34.372 19.7928 34.372 18.6186H37.5243C37.5243 19.3231 37.6716 19.8907 37.9662 20.3212C38.2608 20.7517 38.7223 21.0551 39.3508 21.2312C39.9989 21.4269 40.8434 21.5247 41.8844 21.5247C43.0038 21.5247 43.8877 21.3975 44.5358 21.1431C45.2035 20.9083 45.6847 20.4875 45.9793 19.8809C46.2739 19.2742 46.4212 18.4327 46.4212 17.3564V8.81417L46.6569 8.60868V4.29355H49.5735V17.1803C49.5735 18.8045 49.2494 20.1255 48.6013 21.1431C47.9532 22.1803 47.0399 22.9436 45.8615 23.4328C44.6831 23.922 43.2984 24.1667 41.7076 24.1667ZM40.3819 17.5619C38.9285 17.5619 37.6814 17.2781 36.6404 16.7106C35.6192 16.1431 34.8237 15.3505 34.2542 14.3328C33.7042 13.3152 33.4293 12.1312 33.4293 10.7809C33.4293 9.43062 33.7141 8.24664 34.2836 7.22901C34.8728 6.21139 35.6977 5.41881 36.7583 4.85129C37.8385 4.28376 39.1151 4 40.5881 4C42.12 4 43.4163 4.34247 44.4769 5.02741C45.5571 5.69279 46.2248 6.66149 46.4802 7.93353H47.1577L46.981 10.4874H46.4212C46.4212 9.66545 46.2248 8.98051 45.832 8.43256C45.4392 7.86503 44.8795 7.44428 44.1528 7.1703C43.4261 6.89633 42.5423 6.75934 41.5014 6.75934C40.4997 6.75934 39.6257 6.88654 38.8794 7.14095C38.1527 7.39536 37.593 7.81611 37.2002 8.4032C36.827 8.97073 36.6404 9.7633 36.6404 10.7809C36.6404 11.779 36.827 12.5716 37.2002 13.1587C37.5733 13.7458 38.1135 14.1763 38.8205 14.4503C39.5472 14.7047 40.4113 14.8319 41.413 14.8319C43.0235 14.8319 44.2608 14.4992 45.125 13.8338C45.9891 13.1684 46.4212 12.19 46.4212 10.8983H46.981V13.7751H46.215C45.9597 14.8906 45.341 15.8006 44.359 16.5051C43.377 17.2096 42.0513 17.5619 40.3819 17.5619Z" />
|
||||
<path d="M60.5895 19.1764C58.979 19.1764 57.5551 18.8633 56.3178 18.237C55.1001 17.5912 54.1476 16.7008 53.4601 15.5657C52.7924 14.4111 52.4585 13.0902 52.4585 11.6029C52.4585 10.0764 52.7924 8.74567 53.4601 7.61063C54.1476 6.47558 55.1001 5.59494 56.3178 4.9687C57.5551 4.3229 58.979 4 60.5895 4C62.2393 4 63.673 4.3229 64.8907 4.9687C66.1084 5.59494 67.0511 6.47558 67.7189 7.61063C68.4063 8.74567 68.75 10.0764 68.75 11.6029C68.75 13.0902 68.4063 14.4111 67.7189 15.5657C67.0511 16.7008 66.1084 17.5912 64.8907 18.237C63.673 18.8633 62.2393 19.1764 60.5895 19.1764ZM60.5895 16.3583C62.3768 16.3583 63.6632 15.9571 64.4488 15.1548C65.2344 14.3328 65.6272 13.1489 65.6272 11.6029C65.6272 10.0568 65.2344 8.87288 64.4488 8.05095C63.6632 7.20944 62.3768 6.78869 60.5895 6.78869C58.8219 6.78869 57.5453 7.20944 56.7597 8.05095C55.9741 8.87288 55.5813 10.0568 55.5813 11.6029C55.5813 13.1489 55.9741 14.3328 56.7597 15.1548C57.5453 15.9571 58.8219 16.3583 60.5895 16.3583Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Logo } from "../../assets/logo.svg";
|
||||
|
||||
const LogoSvg = ({ className = "",fill, id, onClick, onKeyUp }) => (
|
||||
<Logo
|
||||
id={id}
|
||||
className={`${className || ""} ${fill || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default LogoSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Logout } from "../../assets/logout.svg";
|
||||
|
||||
const LogoutSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Logout
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default LogoutSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as MailOne } from "../../assets/mail-one.svg";
|
||||
|
||||
const SettingsSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
default : return <MailOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Minus } from "../../assets/minus.svg";
|
||||
|
||||
const MinusSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Minus
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default MinusSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Pencil } from "../../assets/pencil.svg";
|
||||
|
||||
const PencilSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Pencil
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PencilSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Plus } from "../../assets/plus.svg";
|
||||
|
||||
const PlusSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Plus
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default PlusSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as BookingReceipt } from "../../assets/booking-receipt.svg";
|
||||
|
||||
const ReceiptSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "booking": return <BookingReceipt
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ReceiptSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as SettingsTwo } from "../../assets/settings-two.svg";
|
||||
|
||||
const SettingsSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "two": return <SettingsTwo
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsSvg
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as ShareOne } from "../../assets/share-one.svg";
|
||||
|
||||
const ShareSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
switch (variant) {
|
||||
case "one": return <ShareOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ShareSvg
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as Star } from "../../assets/star.svg";
|
||||
|
||||
const StarSvg = ({ className = "", id, onClick, onKeyUp }) => (
|
||||
<Star
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
);
|
||||
|
||||
export default StarSvg;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as TrashTwo } from "../../assets/trash-two.svg";
|
||||
|
||||
const TrashSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "two": return <TrashTwo
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default TrashSvg
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as UserSquare } from "../../assets/user-square.svg";
|
||||
import { ReactComponent as UserCircle } from "../../assets/user-circle.svg";
|
||||
|
||||
const UserSvg = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "square": return <UserSquare
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
case "circle": return <UserCircle
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default UserSvg
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { ReactComponent as UsersOne } from "../../assets/users-one.svg";
|
||||
|
||||
const Users = ({ className = "", id, onClick, onKeyUp, variant }) => {
|
||||
|
||||
switch (variant) {
|
||||
case "one": return <UsersOne
|
||||
id={id}
|
||||
className={`${className || ""}`}
|
||||
onClick={onClick}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default Users
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import LogoSvg from "./LogoSvg"
|
||||
import LogoutSvg from "./LogoutSvg"
|
||||
import ShareSvg from "./ShareSvg"
|
||||
import GridSvg from "./GridSvg"
|
||||
import UserSvg from "./UserSvg"
|
||||
import ImageSvg from "./ImageSvg"
|
||||
import BankNoteSvg from "./BankNoteSvg"
|
||||
import BuildingSvg from "./BuildingSvg"
|
||||
import UsersSvg from "./UsersSvg"
|
||||
import HomeSvg from "./HomeSvg"
|
||||
import FileSvg from "./FileSvg"
|
||||
import CalenderSvg from "./Calender";
|
||||
import ReceiptSvg from "./ReceiptSvg";
|
||||
import MailSvg from "./MailSvg"
|
||||
import SettingsSvg from "./SettingsSvg"
|
||||
import ArrowSvg from "./ArrowSvg"
|
||||
import ChevronSvg from "./ChevronSvg"
|
||||
import TrashSvg from "./TrashSvg"
|
||||
import PencilSvg from "./PencilSvg"
|
||||
import PlusSvg from "./PlusSvg"
|
||||
import MinusSvg from "./MinusSvg"
|
||||
import DotsSvg from "./DotsSvg"
|
||||
import StarSvg from "./StarSvg"
|
||||
|
||||
|
||||
const getIcon = (type, className, id, fill, onClick, onKeyUp, variant) => {
|
||||
const icons = {
|
||||
logo: <LogoSvg onClick={onClick} onKeyUp={onKeyUp} className={className} fill={fill} />,
|
||||
logout: <LogoutSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />,
|
||||
pencil: <PencilSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />,
|
||||
share: <ShareSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
grid: <GridSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
user: <UserSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
image: <ImageSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
banknote: <BankNoteSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
building: <BuildingSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
users: <UsersSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
home: <HomeSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
file: <FileSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
calender: <CalenderSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
receipt: <ReceiptSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
mail: <MailSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
settings: <SettingsSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
arrow: <ArrowSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
chevron: <ChevronSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
trash: <TrashSvg onClick={onClick} onKeyUp={onKeyUp} className={className} variant={variant} />,
|
||||
plus: <PlusSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />,
|
||||
minus: <MinusSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />,
|
||||
dots: <DotsSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />,
|
||||
star: <StarSvg onClick={onClick} onKeyUp={onKeyUp} className={className} />
|
||||
}
|
||||
|
||||
return icons[type] || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Icon = ({ className, id, fill = '', onClick, onKeyUp, type, variant }) => {
|
||||
const [icon, setIcon] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (type) {
|
||||
// Remove all white space from the string, with the regex
|
||||
const iconType = type.toLocaleLowerCase().replace(/\s+/g, '');
|
||||
|
||||
// set the icon based on icon type change, useful for conditional icon renderings
|
||||
setIcon(getIcon(iconType, className, id, fill, onClick, onKeyUp, variant));
|
||||
}
|
||||
}, [type, className]);
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
export default Icon
|
||||
@@ -0,0 +1,40 @@
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import React from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function LoadingSpinner() {
|
||||
const { state } = useContext(GlobalContext);
|
||||
|
||||
if (!state.loading) return null;
|
||||
|
||||
return (
|
||||
<div className="popup-container flex items-center justify-center">
|
||||
<div className="">
|
||||
<svg
|
||||
style={{ margin: "auto", background: "transparent", display: " block", shapeRendering: "auto" }}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
className="md:w-[100px] md:h-[100px] w-[80px] h-[80px]"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#d0d5dd"
|
||||
strokeWidth="6"
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
style={{ transform: "scale(0.8)", transformOrigin: "50px 50px" }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
repeatCount="indefinite"
|
||||
dur="1.882051282051282s"
|
||||
keyTimes="0;1"
|
||||
values="0;256.58892822265625"
|
||||
></animate>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { AuthContext } from "../authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
export default function Modal({ showModal, modalShowTitle, modalShowMessage, type, modalBtnText, itemId, itemId2, table1, table2, backTo }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden outline-none focus:outline-none">
|
||||
<div className="relative my-6 mx-auto w-auto max-w-3xl md:min-w-[35rem]">
|
||||
{/*content*/}
|
||||
<div className="relative flex w-full flex-col rounded-lg border-0 bg-white shadow-lg outline-none focus:outline-none">
|
||||
{/*header*/}
|
||||
<div className="flex items-start justify-between rounded-t border-solid border-slate-200 px-5 pt-6">
|
||||
<h3 className="text-xl font-semibold">{modalShowTitle}</h3>
|
||||
<button
|
||||
className="float-right ml-auto border-0 bg-transparent p-1 text-3xl font-semibold leading-none text-black outline-none focus:outline-none"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: false,
|
||||
modalShowMessage: "",
|
||||
modalBtnText: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="block h-6 w-6 bg-transparent text-2xl text-black outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{/*body*/}
|
||||
<div className="relative flex-auto px-6 py-2">
|
||||
<p className="text-lg my-2 normal-case leading-relaxed text-slate-500">{modalShowMessage}</p>
|
||||
</div>
|
||||
{/*footer*/}
|
||||
<div className="flex items-center justify-end rounded-b border-solid border-slate-200 px-6 pb-6">
|
||||
<button
|
||||
className="background-transparent mr-1 mb-1 rounded border border-[##98A2B3] px-6 py-2 text-sm font-bold text-[#667085] outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: false,
|
||||
modalShowMessage: "",
|
||||
modalBtnText: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (type === "Delete") {
|
||||
return;
|
||||
}
|
||||
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: false,
|
||||
modalShowMessage: "You are about to log out.",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="ml-5 mb-1 rounded border border-[##98A2B3] !bg-gradient-to-r from-primary to-primary-dark px-6 py-2 text-sm font-medium text-white outline-none focus:outline-none"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: false,
|
||||
modalShowMessage: "",
|
||||
modalBtnText: "",
|
||||
itemId: "",
|
||||
itemId2: "",
|
||||
table1: "",
|
||||
table2: "",
|
||||
},
|
||||
});
|
||||
|
||||
// if (type === "Delete") {
|
||||
// try {
|
||||
// if (table1 == "user") {
|
||||
// sdk.setTable("device");
|
||||
// await sdk.callRestAPI({ user_id: itemId }, "DELETEALL");
|
||||
// sdk.setTable("notification");
|
||||
// await sdk.callRestAPI({ user_id: itemId }, "DELETEALL");
|
||||
// }
|
||||
// sdk.setTable(table1);
|
||||
// await sdk.callRestAPI(
|
||||
// {
|
||||
// id: itemId,
|
||||
// },
|
||||
// "DELETE",
|
||||
// );
|
||||
// if (table1 == "property_spaces_images") {
|
||||
// sdk.setTable("photo");
|
||||
// await sdk.callRestAPI({ id: itemId2 }, "DELETE");
|
||||
// }
|
||||
// showToast(globalDispatch, "Successful");
|
||||
// if (table2) {
|
||||
// sdk.setTable(table2);
|
||||
// await sdk.callRestAPI(
|
||||
// {
|
||||
// user_id: itemId,
|
||||
// },
|
||||
// "DELETEALL",
|
||||
// );
|
||||
// }
|
||||
// globalDispatch({
|
||||
// type: "DELETED",
|
||||
// payload: {
|
||||
// deleted: true,
|
||||
// },
|
||||
// });
|
||||
// if (backTo) {
|
||||
// navigate(backTo);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// showToast(globalDispatch, error.message, 4000, "ERROR");
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (type == "Delete") {
|
||||
try {
|
||||
// sdk.setTable(table1);
|
||||
// await sdk.callRestAPI({ id: itemId, deleted_at: moment().format("yyyy-MM-DD HH:mm:ss") }, "PUT");
|
||||
await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: itemId, entity: table1, type: "delete" }, "POST");
|
||||
showToast(globalDispatch, "Successful");
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
if (backTo) {
|
||||
navigate(backTo);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == "BaasDelete") {
|
||||
try {
|
||||
sdk.setTable(table1);
|
||||
await sdk.callRestAPI({ id: itemId, deleted_at: moment().format("yyyy-MM-DD HH:mm:ss") }, "PUT");
|
||||
// await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images", { id: itemId, entity: table1, type: "delete" }, "POST");
|
||||
showToast(globalDispatch, "Successful");
|
||||
globalDispatch({
|
||||
type: "DELETED",
|
||||
payload: {
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
if (backTo) {
|
||||
navigate(backTo);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(globalDispatch, err.message, 4000, "ERROR");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === "Edit") {
|
||||
globalDispatch({
|
||||
type: "SAVE_CHANGES",
|
||||
payload: {
|
||||
saveChanges: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: "LOGOUT",
|
||||
});
|
||||
|
||||
navigate("/admin/login");
|
||||
}}
|
||||
>
|
||||
{modalBtnText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed inset-0 z-40 bg-black opacity-25"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
export default function NotVerifiedModal() {
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const { state: authState } = useContext(AuthContext);
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={globalState.notVerifiedModal}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => globalDispatch({ type: "CLOSE_NOT_VERIFIED_MODAL" })}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
User not verified
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Please verify your account to proceed with booking</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={() => globalDispatch({ type: "CLOSE_NOT_VERIFIED_MODAL" })}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigate(`/account/verification?redirect_uri=${pathname}`);
|
||||
globalDispatch({ type: "CLOSE_NOT_VERIFIED_MODAL" });
|
||||
}}
|
||||
className={`login-btn-gradient inline-flex justify-center rounded-md py-2 px-4 text-sm font-medium text-white`}
|
||||
>
|
||||
Get verified
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import Icon from "./Icons";
|
||||
const PaginationBar = ({ currentPage, pageSize, canPreviousPage, canNextPage, previousPage, nextPage, totalNumber, className }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={`flex justify-between px-6 text-[#667085] font-medium py-2 items-center text-sm ${className ?? ""}`}>
|
||||
<div className="md:mt-2">
|
||||
<span>
|
||||
Showing{" "}
|
||||
<strong>
|
||||
{totalNumber < 1 ? 0 : currentPage > 1 ? (currentPage - 1) * pageSize + 1 : currentPage} - {currentPage * pageSize < totalNumber ? currentPage * pageSize : totalNumber} of {totalNumber}
|
||||
</strong>{" "}
|
||||
</span>
|
||||
</div>
|
||||
{/* */}
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={previousPage}
|
||||
disabled={!canPreviousPage}
|
||||
className="disabled:opacity-50 font-semibold text-sm md:px-5 px-3 py-2.5 text-center inline-flex items-center md:mr-2 md:mb-2"
|
||||
>
|
||||
<Icon
|
||||
type="arrow"
|
||||
variant="narrow-left"
|
||||
className="stroke-[#667085] h-4 w-4"
|
||||
/>{" "}
|
||||
<span className="ml-2">Prev</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextPage}
|
||||
disabled={!canNextPage}
|
||||
className="disabled:opacity-50 font-semibold text-sm md:px-5 px-3 py-2.5 text-center inline-flex items-center mr-2 md:mb-2"
|
||||
>
|
||||
<span className="mr-2">Next</span>
|
||||
<Icon
|
||||
type="arrow"
|
||||
variant="narrow-right"
|
||||
className="stroke-[#667085] h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginationBar;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
|
||||
const PaginationBar = ({ currentPage, pageSize, updatePageSize, totalNumber, noBorder }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={"flex justify-between bg-white py-4 font-medium items-center text-[#667085] " + (noBorder ? "" : "border px-6")}>
|
||||
<div className="">
|
||||
<p className="text-sm mb-0">
|
||||
Showing{" "}
|
||||
<span>
|
||||
{totalNumber < 1 ? 0 : currentPage > 1 ? (currentPage - 1) * pageSize + 1 : currentPage}-{currentPage * pageSize < totalNumber ? currentPage * pageSize : totalNumber} of {totalNumber}
|
||||
</span>{" "}
|
||||
</p>
|
||||
</div>
|
||||
{/* */}
|
||||
<div>
|
||||
<span className="mr-2 text-sm">Results per page:</span>
|
||||
<select
|
||||
className="md:mt-2 cursor-pointer border w-[53px] py-1 text-sm bg-white"
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
updatePageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 30, 40, 50, "ALL"].map((pageSize) => (
|
||||
<option
|
||||
key={pageSize}
|
||||
value={pageSize == "ALL" ? 100000 : pageSize}
|
||||
>
|
||||
{pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginationBar;
|
||||
@@ -0,0 +1,181 @@
|
||||
import React, { Fragment } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import PaginationBar from "./PaginationBar";
|
||||
import PaginationHeader from "./PaginationHeader";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import Icon from "./Icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { secondsToHour } from "@/utils/utils";
|
||||
import moment from "moment";
|
||||
import { ID_PREFIX } from "@/utils/constants";
|
||||
|
||||
const Payment = ({ id, table }) => {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [data, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
|
||||
const payoutMapping = [
|
||||
{ key: "0", value: "Pending" },
|
||||
{ key: "1", value: "Initiated" },
|
||||
{ key: "2", value: "Paid" },
|
||||
{ key: "3", value: "Cancelled" }
|
||||
];
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum) {
|
||||
try {
|
||||
let sdk = new MkdSDK();
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/payout/PAGINATE",
|
||||
{
|
||||
where: [table ? `${table === "host" ? `ergo_user.id = ${id}` : "1"}` : 1],
|
||||
page: pageNum,
|
||||
limit: limitNum
|
||||
},
|
||||
"POST"
|
||||
);
|
||||
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
} catch (error) {
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
await getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginationHeader
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
updatePageSize={updatePageSize}
|
||||
/>
|
||||
<div className="overflow-x-auto p-5 bg-white shadow rounded">
|
||||
{data.map((data, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border rounded px-5 py-4 flex justify-between flex-col lg:flex-row mb-4"
|
||||
>
|
||||
<div>{ID_PREFIX.PAYOUT + data.id}</div>
|
||||
<div className="min-w-[219px] max-w-[219px] mr-[22px]">
|
||||
<p className="text-xs mb-1 font-medium ">Host</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.host_last_name}, {data.host_first_name}{" "}
|
||||
</p>
|
||||
<p className="text-xs mb-1 font-medium ">Customer</p>
|
||||
<p className="mb-1 text-sm">
|
||||
{data.customer_last_name}, {data.customer_first_name}{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-[219px] max-w-[219px] mr-[22px]">
|
||||
<p className="text-xs mb-1 font-medium ">Booking Date</p>
|
||||
<p className="mb-1 text-sm">{data.create_at} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Order Number</p>
|
||||
<p className="mb-1 text-sm">{data.id}</p>
|
||||
</div>
|
||||
<div className="min-w-[72px] max-w-[72px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Total</p>
|
||||
<p className="mb-1 text-sm">${data?.total?.toFixed(2)} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Tax</p>
|
||||
<p className="mb-1 text-sm">${data?.tax?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="min-w-[72px] max-w-[72px] mb-4">
|
||||
<p className="text-xs mb-1 font-medium ">Commission</p>
|
||||
<p className="mb-1 text-sm">${data?.commission?.toFixed(2)} </p>
|
||||
<p className="text-xs mb-1 font-medium ">Payout Date</p>
|
||||
<p className="mb-1 text-sm">{data?.initiated_at ? moment(data.initiated_at).add(7, "days").format("MM/DD/YY") : ""}</p>
|
||||
</div>
|
||||
<div className="flex min-w-[60px] max-w-[60px] mr-[22px] items-center justify-center">
|
||||
<p>{payoutMapping.find((status) => status.key == data.status)?.value}</p>
|
||||
</div>
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative min-w-[60px] max-w-[60px] inline-block text-left"
|
||||
>
|
||||
<div className="h-full flex items-center">
|
||||
<Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#33D4B7] focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||
<Icon type="dots" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-0 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => navigate(`/admin/edit-payout/${data.id}`)}
|
||||
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} w-full text-left block px-4 py-2 text-sm`}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
totalNumber={dataTotal}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payment;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
|
||||
export default function DeleteAccountModal({ modalOpen, closeModal }) {
|
||||
const { dispatch: authDispatch, state: authState } = useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function requestAccountDelete() {
|
||||
setLoading(true);
|
||||
try {
|
||||
await callCustomAPI("confirm-delete-email", "post", { user_id: globalState.user.id, email: globalState.user.email, role: authState.role }, "");
|
||||
navigate("/account/delete/check");
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Are you sure you want to delete your account?
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">You will receive an email to confirm this action</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="button"
|
||||
className={`inline-flex justify-center rounded-md ${loading ? "py-1 px-6" : "py-2 px-4"} text-sm font-medium bg-red-500 text-white`}
|
||||
onClick={requestAccountDelete}
|
||||
>
|
||||
Proceed
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export default function EditAboutModal({ modalOpen, closeModal }) {
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { handleSubmit, register } = useForm({ defaultValues: { about: globalState.user.about } });
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSubmit(data) {
|
||||
console.log("submitting", data);
|
||||
setLoading(true);
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
profile: data,
|
||||
},
|
||||
"",
|
||||
);
|
||||
closeModal();
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: "About change successful",
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
globalDispatch({
|
||||
type: "SET_USER_DATA",
|
||||
payload: {
|
||||
...globalState.user,
|
||||
about: data.about,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-[18px]">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-semibold"
|
||||
>
|
||||
Edit About
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="p-1 border hover:bg-gray-200 duration-300 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>{" "}
|
||||
</div>
|
||||
<hr className="mb-4" />
|
||||
<textarea
|
||||
{...register("about")}
|
||||
cols="30"
|
||||
rows="10"
|
||||
className="w-full focus:outline-none border-2 p-2 resize-none text-sm text-gray-900"
|
||||
></textarea>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="tracking-wide outline-none focus:outline-none rounded py-2 border-2 border-[#98A2B3] mt-4 flex-grow"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`login-btn-gradient text-white tracking-wide outline-none focus:outline-none rounded ${loading ? "py-1 px-4" : "py-2"} mt-4 flex-grow`}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import CustomLocationAutoCompleteV2 from "../CustomLocationAutoCompleteV2";
|
||||
import StickyCustomLocationAutoComplete from "../StickyCustomLocationAutoComplete";
|
||||
|
||||
export default function EditLocationModal({ modalOpen, closeModal }) {
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { handleSubmit, register, setValue, control, formState: { errors } } = useForm({ defaultValues: { city: globalState.user.city, country: globalState.user.country } });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSubmit(data) {
|
||||
console.log("submitting", data);
|
||||
// const parts = data.city.split(", ");
|
||||
// data.city = parts[0]
|
||||
// data.country = parts[1]
|
||||
setLoading(true);
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
profile: data,
|
||||
},
|
||||
"",
|
||||
);
|
||||
closeModal();
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: "Location changed successful",
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
globalDispatch({
|
||||
type: "SET_USER_DATA",
|
||||
payload: {
|
||||
...globalState.user,
|
||||
city: data.city,
|
||||
country: data.country,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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 z-10 ">
|
||||
<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
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-[18px]">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-semibold"
|
||||
>
|
||||
Edit Location
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="p-1 border hover:bg-gray-200 duration-300 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>{" "}
|
||||
</div>
|
||||
<hr className="mb-4" />
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="city"
|
||||
>
|
||||
Location
|
||||
</label>
|
||||
<StickyCustomLocationAutoComplete
|
||||
control={control}
|
||||
setValue={(val) => setValue("city", val)}
|
||||
name="city"
|
||||
className={`w-full z-20 rounded relative border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`}
|
||||
placeholder=""
|
||||
hideIcons
|
||||
suggestionType={["(cities)"]}
|
||||
/>
|
||||
{/* <input
|
||||
autoComplete="off"
|
||||
id="city"
|
||||
type="text"
|
||||
{...register("city")}
|
||||
className={`resize-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none`}
|
||||
/> */}
|
||||
</div>
|
||||
{/* <div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="country"
|
||||
>
|
||||
Country
|
||||
</label>
|
||||
<input
|
||||
autoComplete="off"
|
||||
id="country"
|
||||
type="text"
|
||||
{...register("country")}
|
||||
className={`resize-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none`}
|
||||
/>
|
||||
</div> */}
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="tracking-wide outline-none focus:outline-none rounded py-2 border-2 border-[#98A2B3] mt-4 flex-grow"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`login-btn-gradient text-white tracking-wide outline-none focus:outline-none rounded ${loading ? "py-1 px-4" : "py-2"} mt-4 flex-grow`}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import * as yup from "yup";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import React, { Fragment } from "react";
|
||||
import { useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import commonPasswords from "@/assets/json/common-passwords.json";
|
||||
import moment from "moment";
|
||||
|
||||
export default function EditPasswordModal({ modalOpen, closeModal }) {
|
||||
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
|
||||
const [showOldPassword, setShowOldPassword] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const schema = yup.object({
|
||||
current_password: yup.string().required("This field is required"),
|
||||
password: yup
|
||||
.string()
|
||||
.required("Password is required")
|
||||
.min(10, "Password must be at least 10 characters long")
|
||||
.matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)")
|
||||
.matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter")
|
||||
.matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter")
|
||||
.matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol")
|
||||
.test("is-not-dictionary", "Password must not contain a common word", (val) => {
|
||||
return commonPasswords.every((pass) => !val.includes(pass));
|
||||
})
|
||||
.test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val) => {
|
||||
const d = moment(globalState.user.dob);
|
||||
return [
|
||||
globalState.user.first_name,
|
||||
globalState.user.last_name,
|
||||
d.format("yyyyMMDD"),
|
||||
d.format("DDMMyyyy"),
|
||||
d.format("MMDDyyyy"),
|
||||
d.format("YYMMDD"),
|
||||
d.format("MMDDYY"),
|
||||
d.format("DDMMYY"),
|
||||
].every((field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()));
|
||||
}),
|
||||
confirm_password: yup
|
||||
.string()
|
||||
.oneOf([yup.ref("password"), null], "Passwords don't match")
|
||||
.required("This field is required"),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
reset,
|
||||
trigger,
|
||||
formState: { errors, dirtyFields },
|
||||
} = useForm({ defaultValues: { current_password: "", password: "", confirm_password: "" }, resolver: yupResolver(schema), criteriaMode: "all" });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSubmit(data) {
|
||||
console.log("submitting", data);
|
||||
setLoading(true);
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
user: { password: data.password, oldPassword: data.current_password },
|
||||
},
|
||||
"",
|
||||
);
|
||||
closeModal();
|
||||
reset();
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: "Password change successful",
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function getPasswordErrors() {
|
||||
var arr = [];
|
||||
if (Array.isArray(errors.password?.types.matches)) {
|
||||
arr = [...errors.password.types.matches];
|
||||
}
|
||||
if (typeof errors.password?.types.matches === "string") {
|
||||
arr.push(errors.password.types.matches);
|
||||
}
|
||||
if (errors.password?.types.min) {
|
||||
arr.push(errors.password.types.min);
|
||||
}
|
||||
if (errors.password?.types["does-not-contain-user-info"]) {
|
||||
arr.push(errors.password?.types["does-not-contain-user-info"]);
|
||||
}
|
||||
if (errors.password?.types["is-not-dictionary"]) {
|
||||
arr.push(errors.password?.types["is-not-dictionary"]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
const passwordErrors = getPasswordErrors();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-[18px] flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-semibold"
|
||||
>
|
||||
Change Password
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="rounded-full border p-1 px-3 text-2xl font-normal duration-300 hover:bg-gray-200"
|
||||
>
|
||||
✕
|
||||
</button>{" "}
|
||||
</div>
|
||||
<hr className="mb-4" />
|
||||
|
||||
<p className="mb-4">In order to set new password provide the current one:</p>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="relative flex justify-between rounded-sm border bg-transparent">
|
||||
<input
|
||||
type={showOldPassword ? "text" : "password"}
|
||||
{...register("current_password")}
|
||||
className="flex-grow border-0 p-2 px-4 focus:outline-none active:outline-none "
|
||||
placeholder="Type current password"
|
||||
/>{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowOldPassword((prev) => !prev)}
|
||||
className="absolute right-1 top-[20%]"
|
||||
>
|
||||
{" "}
|
||||
{showOldPassword ? (
|
||||
<img
|
||||
src="/show.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/invisible.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-[32px] md:my-[32px]" />
|
||||
<div className="mb-8">
|
||||
<div className="relative mb-4 flex justify-between rounded-sm border bg-transparent">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
{...register("password", {
|
||||
onChange: (e) => {
|
||||
trigger("password");
|
||||
},
|
||||
})}
|
||||
className="flex-grow border-0 p-2 px-4 focus:outline-none active:outline-none "
|
||||
placeholder="Type new password"
|
||||
autoComplete="new-password"
|
||||
/>{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
className="absolute right-1 top-[20%]"
|
||||
>
|
||||
{" "}
|
||||
{showPassword ? (
|
||||
<img
|
||||
src="/show.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/invisible.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{dirtyFields.password && (
|
||||
<div className="fade-in mb-4 space-y-2 rounded-sm border border-[#C42945] p-3 text-sm normal-case text-[#C42945] empty:hidden">
|
||||
{passwordErrors.map((msg) => (
|
||||
<p>{msg}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="relative flex justify-between rounded-sm border bg-transparent">
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
{...register("confirm_password")}
|
||||
className="flex-grow border-0 p-2 px-4 focus:outline-none active:outline-none "
|
||||
placeholder="Retype new password"
|
||||
/>{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
||||
className="absolute right-1 top-[20%]"
|
||||
>
|
||||
{" "}
|
||||
{showConfirmPassword ? (
|
||||
<img
|
||||
src="/show.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src="/invisible.png"
|
||||
alt=""
|
||||
className="mr-2 ml-2 w-6"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{Object.entries(errors).length > 0 && dirtyFields.password && !errors.password ? (
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 flex-grow rounded border-2 border-[#98A2B3] py-2 tracking-wide outline-none focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`login-btn-gradient rounded tracking-wide text-white outline-none focus:outline-none ${loading ? "py-1 px-4" : "py-2"} mt-4 flex-grow`}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export default function EditProfileModal({ modalOpen, closeModal }) {
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { handleSubmit, register } = useForm({ defaultValues: { first_name: globalState.user.first_name, last_name: globalState.user.last_name } });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function onSubmit(data) {
|
||||
console.log("submitting", data);
|
||||
setLoading(true);
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
user: data,
|
||||
},
|
||||
"",
|
||||
);
|
||||
closeModal();
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: "Name change successful",
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
globalDispatch({
|
||||
type: "SET_USER_DATA",
|
||||
payload: {
|
||||
...globalState.user,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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
|
||||
as="form"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-[18px]">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-semibold"
|
||||
>
|
||||
Edit Profile
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="p-1 border hover:bg-gray-200 duration-300 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>{" "}
|
||||
</div>
|
||||
<hr className="mb-4" />
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="first_name"
|
||||
>
|
||||
First Name
|
||||
</label>
|
||||
<input
|
||||
autoComplete="off"
|
||||
id="first_name"
|
||||
type="text"
|
||||
{...register("first_name")}
|
||||
className={`resize-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4 ">
|
||||
<label
|
||||
className="block text-gray-700 text-sm font-bold mb-2"
|
||||
htmlFor="last_name"
|
||||
>
|
||||
Last Name
|
||||
</label>
|
||||
<input
|
||||
autoComplete="off"
|
||||
id="last_name"
|
||||
type="text"
|
||||
{...register("last_name")}
|
||||
className={`resize-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="tracking-wide outline-none focus:outline-none rounded py-2 border-2 border-[#98A2B3] mt-4 flex-grow"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="submit"
|
||||
className={`login-btn-gradient text-white tracking-wide outline-none focus:outline-none rounded ${loading ? "py-1 px-4" : "py-2"} mt-4 flex-grow`}
|
||||
>
|
||||
Update
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { parseJsonSafely, sleep } from "@/utils/utils";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function EnableEmailDialog({ isOpen, closeModal }) {
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const isEnabled = parseJsonSafely(globalState.user.settings, {}).email_on_booking_declined == true;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function toggleEmailPreference() {
|
||||
setLoading(true);
|
||||
let newSettings;
|
||||
if (!isEnabled) {
|
||||
newSettings = {
|
||||
...parseJsonSafely(globalState.user.settings, {}),
|
||||
email_on_space_image_declined: true,
|
||||
email_on_booking_declined: true,
|
||||
email_on_profile_photo_declined: true,
|
||||
email_on_new_chat_message: true,
|
||||
email_on_space_booked: true,
|
||||
email_on_booking_cancelled: true,
|
||||
email_on_booking_accepted: true,
|
||||
};
|
||||
} else {
|
||||
newSettings = {
|
||||
...parseJsonSafely(globalState.user.settings, {}),
|
||||
email_on_space_image_declined: false,
|
||||
email_on_booking_declined: false,
|
||||
email_on_profile_photo_declined: false,
|
||||
email_on_new_chat_message: false,
|
||||
email_on_space_booked: false,
|
||||
email_on_booking_cancelled: false,
|
||||
email_on_booking_accepted: false,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await callCustomAPI(
|
||||
"edit-self",
|
||||
"post",
|
||||
{
|
||||
profile: { settings: JSON.stringify(newSettings) },
|
||||
},
|
||||
"",
|
||||
);
|
||||
closeModal();
|
||||
await sleep(200);
|
||||
globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, settings: JSON.stringify(newSettings) } });
|
||||
} catch (err) {
|
||||
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Operation Failed", message: err.message } });
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <div className="fixed inset-0 flex items-center justify-center"></div>}
|
||||
<Transition
|
||||
appear
|
||||
show={isOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{isEnabled ? "Turn Off Email Notifications" : "Enable email notifications?"}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{!isEnabled ? "Enable email notifications on site activity such as booking when booking is declined by host" : "Disabling email notifications on site activity"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="button"
|
||||
className={`inline-flex justify-center rounded-md ${loading ? "py-1 px-6" : "py-2 px-4"} text-sm font-medium login-btn-gradient text-white`}
|
||||
onClick={toggleEmailPreference}
|
||||
>
|
||||
Proceed
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { LoadingButton } from "@/components/frontend";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export default function TwoFaDialog({ isOpen, closeModal, isEnabled, onProceed, loading }) {
|
||||
return (
|
||||
<>
|
||||
{isOpen && <div className="fixed inset-0 flex items-center justify-center"></div>}
|
||||
<Transition
|
||||
appear
|
||||
show={isOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{isEnabled ? "Turn Off 2-Step Verification?" : "Protect your account with 2-Step Verification"}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{!isEnabled
|
||||
? "Prevent hackers from accessing your account with an additional layer of security. When you sign in, 2-Step Verification helps make sure that your personal information stays private, safe and secure."
|
||||
: "Turning off 2-Step Verification will remove the extra security on your account, and you’ll only use your password to sign in."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="button"
|
||||
className={`inline-flex justify-center rounded-md ${loading ? "py-1 px-6" : "py-2 px-4"} text-sm font-medium login-btn-gradient text-white`}
|
||||
onClick={onProceed}
|
||||
>
|
||||
Proceed
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { isValidDate, parseSearchParams } from "@/utils/utils";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useContext } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import CustomLocationAutoCompleteV2 from "./CustomLocationAutoCompleteV2";
|
||||
import DatePickerV3 from "./DatePickerV3";
|
||||
|
||||
const prices = [
|
||||
{
|
||||
label: "All Prices",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
label: "$0 - $30",
|
||||
value: "$0 - $30",
|
||||
},
|
||||
{
|
||||
label: "$31 - $60",
|
||||
value: "$31 - $60",
|
||||
},
|
||||
{
|
||||
label: "$60 - $90",
|
||||
value: "$60 - $90",
|
||||
},
|
||||
{
|
||||
label: "$90 - $120",
|
||||
value: "$90 - $120",
|
||||
},
|
||||
{
|
||||
label: "$120 - $150",
|
||||
value: "$120 - $150",
|
||||
},
|
||||
{
|
||||
label: "$150 - $180",
|
||||
value: "$150 - $180",
|
||||
},
|
||||
];
|
||||
|
||||
export default function PropertySpaceFiltersModal({ modalOpen, closeModal }) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { state: globalState } = useContext(GlobalContext);
|
||||
const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({
|
||||
defaultValues: (() => {
|
||||
const params = parseSearchParams(searchParams);
|
||||
return {
|
||||
location: params.location ?? "",
|
||||
from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(),
|
||||
to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(),
|
||||
space_name: params.space_name ?? "",
|
||||
category: params.category ?? "",
|
||||
price_range: params.price_range ?? "",
|
||||
direction: "DESC",
|
||||
};
|
||||
})(),
|
||||
});
|
||||
|
||||
const { dirtyFields } = formState;
|
||||
|
||||
const fromDate = watch("from");
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
console.log("submitting ", data);
|
||||
searchParams.set("category", data.category);
|
||||
searchParams.set("price_range", data.price_range);
|
||||
searchParams.set("space_name", data.space_name);
|
||||
searchParams.set("location", data.location);
|
||||
searchParams.set("from", dirtyFields?.from ? data.from.toISOString() : "");
|
||||
searchParams.set("to", dirtyFields?.to ? data.to.toISOString() : "");
|
||||
setSearchParams(searchParams);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen && window.innerWidth < 700}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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="tiny-scroll max-h-fit w-full max-w-md transform overflow-hidden overflow-y-auto rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
as="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-[18px] flex items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-semibold"
|
||||
>
|
||||
Filters
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-gray-800 underline"
|
||||
onClick={() =>
|
||||
reset(
|
||||
{
|
||||
location: "",
|
||||
from: new Date(),
|
||||
to: new Date(),
|
||||
space_name: "",
|
||||
category: "",
|
||||
price_range: "",
|
||||
direction: "DESC",
|
||||
},
|
||||
{ keepDirty: false },
|
||||
)
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="text-lg rounded-full border p-1 px-3 font-normal duration-300 hover:bg-gray-200 md:text-2xl"
|
||||
>
|
||||
✕
|
||||
</button>{" "}
|
||||
</div>
|
||||
<hr className="my-[10px]" />
|
||||
<div className="space-y-6">
|
||||
<select
|
||||
className="w-full cursor-pointer border bg-white py-2 px-3 focus:outline-none"
|
||||
{...register("category")}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{globalState.spaceCategories.map((sp) => (
|
||||
<option
|
||||
key={sp.id}
|
||||
value={sp.category}
|
||||
>
|
||||
{sp.category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="w-full cursor-pointer border bg-white py-2 px-3 focus:outline-none"
|
||||
{...register("price_range")}
|
||||
>
|
||||
<option value="">All Prices</option>
|
||||
{prices.map((pr) => (
|
||||
<option
|
||||
key={pr.value}
|
||||
value={pr.value}
|
||||
>
|
||||
{pr.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<CustomLocationAutoCompleteV2
|
||||
control={control}
|
||||
setValue={(val) => setValue("location", val)}
|
||||
name="location"
|
||||
className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none`}
|
||||
placeholder="Location"
|
||||
hideIcons
|
||||
/>
|
||||
<div className="my-[16px] flex gap-2">
|
||||
<div className="flex w-1/2 items-center gap-2 whitespace-nowrap rounded-md border bg-white p-2">
|
||||
<DatePickerV3
|
||||
reset={() => resetField("from", { keepDirty: false, keepTouched: false })}
|
||||
setValue={(val) => setValue("from", val, { shouldDirty: true })}
|
||||
control={control}
|
||||
name="from"
|
||||
labelClassName="justify-between flex-grow flex-row-reverse"
|
||||
placeholder="From"
|
||||
min={new Date()}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-1/2 items-center gap-2 rounded-md border bg-white p-2">
|
||||
<DatePickerV3
|
||||
reset={() => resetField("to", { keepDirty: false, keepTouched: false })}
|
||||
setValue={(val) => setValue("to", val, { shouldDirty: true })}
|
||||
control={control}
|
||||
name="to"
|
||||
labelClassName="justify-between flex-grow flex-row-reverse"
|
||||
placeholder="To"
|
||||
min={fromDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Space name"
|
||||
className="my-[16px] w-full rounded-md border p-2 focus:outline-none active:outline-none"
|
||||
{...register("space_name")}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none"
|
||||
>
|
||||
Apply and close
|
||||
</button>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { isNotInViewport } from "@/utils/utils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { Link } from "react-router-dom";
|
||||
import LogoIcon from "./frontend/icons/LogoIcon";
|
||||
import StaticSearchBar from "./frontend/StaticSearchBar";
|
||||
|
||||
const getNavBarVariant = (path) => {
|
||||
if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/help")) {
|
||||
return "light";
|
||||
}
|
||||
switch (path) {
|
||||
case "/contact-us":
|
||||
case "/faq":
|
||||
return "white";
|
||||
case "/search":
|
||||
case "/explore":
|
||||
case "/favorites":
|
||||
case "/reset-password":
|
||||
case "/check-verification":
|
||||
return "light";
|
||||
default:
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
|
||||
export const PublicHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [variant, setVariant] = useState(getNavBarVariant(pathname));
|
||||
const [showStaticBar, setShowStaticBar] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
if (pathname == "/") {
|
||||
if (window.scrollY > 10) {
|
||||
setVariant("white");
|
||||
} else {
|
||||
setVariant("transparent");
|
||||
}
|
||||
}
|
||||
setShowStaticBar(isNotInViewport("search-bar"));
|
||||
};
|
||||
window.addEventListener("scroll", onScroll);
|
||||
setShowStaticBar(isNotInViewport("search-bar"));
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setVariant(getNavBarVariant(pathname));
|
||||
}, [pathname]);
|
||||
|
||||
if (pathname.includes("/login") || pathname.includes("/signup")) return null;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 z-50 flex w-screen flex-wrap items-center justify-between py-4 px-4 text-sm duration-500 md:rounded-br-[32px] md:rounded-bl-[32px] md:px-12 lg:flex-nowrap header-${variant}`}
|
||||
>
|
||||
<nav className={`lg:flex ${showStaticBar ? "hidden" : "flex"} gap-6`}>
|
||||
<Link to="/">
|
||||
<LogoIcon fill={variant == "transparent" || variant == "light" ? undefined : "#101828"} />
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="hidden lg:block">{showStaticBar && (pathname == "/search" || pathname == "/") && <StaticSearchBar />}</div>
|
||||
|
||||
<nav className="hidden items-center gap-6 lg:flex">
|
||||
<Link
|
||||
to="/login"
|
||||
className={`rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} whitespace-nowrap`}
|
||||
>
|
||||
<span>Login</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup/select-role"
|
||||
className={`rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} whitespace-nowrap`}
|
||||
>
|
||||
<span>Sign up</span>
|
||||
</Link>
|
||||
</nav>
|
||||
<nav className={`lg:hidden ${showStaticBar ? "hidden" : "flex"} items-center gap-4`}>
|
||||
<Link
|
||||
to="/login"
|
||||
className={`rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant}`}
|
||||
>
|
||||
<span>Login</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup/select-role"
|
||||
className={`rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant}`}
|
||||
>
|
||||
<span>Sign up</span>
|
||||
</Link>
|
||||
</nav>
|
||||
<div className={`${showStaticBar && (pathname == "/search" || pathname == "/") ? "py-4" : ""} flex w-full justify-center lg:hidden`}>
|
||||
{showStaticBar && (pathname == "/search" || pathname == "/") && <StaticSearchBar className="flex w-full justify-center py-4 md:hidden" />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicHeader;
|
||||
@@ -0,0 +1,258 @@
|
||||
import React from "react";
|
||||
import { GlobalContext, showToast } from "@/globalContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import Icon from "./Icons";
|
||||
import LoadingButton from "@/components/frontend/LoadingButton";
|
||||
|
||||
export default function ReviewPopUp({ showReview, review }) {
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
const stars = Array(5).fill(0);
|
||||
const [hashtags, setHashtags] = React.useState([]);
|
||||
const [reason, setReason] = React.useState("");
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [acceptLoading, setAcceptLoading] = React.useState(false);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
async function acceptReview(id) {
|
||||
sdk.setTable("review");
|
||||
setAcceptLoading(true);
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ id, status: 1 }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Review accepted", 4000, "Success")
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: false,
|
||||
review: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setAcceptLoading(false)
|
||||
}
|
||||
|
||||
async function declineReview(review) {
|
||||
if (reason.length < 1) {
|
||||
showToast(globalDispatch, "Please add a reason for declining review", 4000, "ERROR")
|
||||
} else {
|
||||
sdk.setTable("review");
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await sdk.callRestAPI({ "id": review.id, status: 2 }, "PUT");
|
||||
if (!result.error) {
|
||||
showToast(globalDispatch, "Review declined", 4000, "Error")
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: false,
|
||||
review: "",
|
||||
},
|
||||
})
|
||||
sendEmailAlert(review.given_by === "host" ? review.host_id : review.customer_id, review?.property_name ?? "property_name")
|
||||
}
|
||||
} catch (err) {
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function sendEmailAlert(to, property_name) {
|
||||
try {
|
||||
// get receiver preferences
|
||||
const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST");
|
||||
if (!result.error) {
|
||||
const tmpl = await sdk.getEmailTemplate("customer-review-declined");
|
||||
const body = tmpl.html
|
||||
?.replace(new RegExp("{{{reason}}}", "g"), reason)
|
||||
.replace(new RegExp("{{{property_name}}}"), property_name)
|
||||
|
||||
// send email
|
||||
await sdk.sendEmail(result.email, tmpl.subject, body);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log("ERROR", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getHashtags() {
|
||||
try {
|
||||
const result = await sdk.callRawAPI(
|
||||
"/v2/api/custom/ergo/review/get-hashtag",
|
||||
{
|
||||
where: [`review_id=${review?.id}`],
|
||||
},
|
||||
"POST",
|
||||
);
|
||||
if (!result.error && result?.list) {
|
||||
setHashtags(result.list);
|
||||
}
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
getHashtags();
|
||||
}, [review?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showReview ? (
|
||||
<>
|
||||
<div
|
||||
className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none"
|
||||
>
|
||||
<div className="relative my-6 mx-auto w-[510px] max-w-[510px]">
|
||||
{/*content*/}
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col bg-white outline-none focus:outline-none">
|
||||
{/*header*/}
|
||||
<div className="flex w-11/12 mx-auto items-center justify-between pt-6 border-solid border-slate-200 rounded-t">
|
||||
<h3 className="text-md font-bold">Review details</h3>
|
||||
<button
|
||||
className="ml-auto bg-transparent border-0 text-black text-3xl leading-none font-semibold outline-none focus:outline-none"
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: false,
|
||||
review: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="bg-transparent text-black h-6 w-6 text-2xl block outline-none focus:outline-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className=" w-11/12 my-3 mx-auto bg-gray-200 h-[1px]"></div>
|
||||
{/*body*/}
|
||||
<div className="relative px-6 py-2 flex-auto">
|
||||
{review?.type === "received" && <p className="text-[#475467] mb-[2px] text-sm font-medium">Reviewed By: {review?.given_by}</p>}
|
||||
<p className="text-[#475467] my-[2px] text-sm font-medium">Review posted on: {review?.create_at}</p>
|
||||
<p className="text-[#475467] my-[2px] text-sm font-medium">Space name: {review?.category}</p>
|
||||
<p className="text-[#475467] my-[2px] text-sm font-medium">
|
||||
Booking: #{review?.booking_id}
|
||||
<Link
|
||||
className="underline ml-1"
|
||||
to={`/admin/view-booking/${review?.booking_id}`}
|
||||
onClick={() =>
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: false,
|
||||
review: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
(view details)
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<p className="text-lg font-bold mt-[14px]">Rating</p>
|
||||
<p className="flex w-1/3 justify-between py-2">
|
||||
{stars.map((_, index) => (
|
||||
<Icon
|
||||
type="star"
|
||||
key={index}
|
||||
className={review?.rating > index ? "stroke-[#FEC84B] fill-[#FEC84B]" : "stroke-[#98A2B3]"}
|
||||
/>
|
||||
))}
|
||||
</p>
|
||||
|
||||
<p className="text-lg font-bold mt-[14px]">Hashtags</p>
|
||||
<div className="flex py-2">
|
||||
{hashtags.map((hashtag) => (
|
||||
<span
|
||||
key={hashtag.id}
|
||||
className="py-2 px-3 mr-2 rounded bg-[#EAECF0]"
|
||||
>
|
||||
{hashtag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-lg font-bold mt-[14px]">Comment</p>
|
||||
<p className="mt-[2px] text-sm text-[#475467]">{review?.comment}</p>
|
||||
<div className="grid mt-3">
|
||||
<label className="font-bold">Reason (optional for declining)</label>
|
||||
<input type="text" maxLength={50} value={reason} onChange={(e) => setReason(e.target.value)} required className=" border mt-2 rounded-lg mt-1 p-2 max-w-xs h-[200px]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center my-8 flex px-6 gap-4">
|
||||
{/* <button
|
||||
type="button"
|
||||
className="tracking-wide outline-none focus:outline-none rounded py-2 border-2 border-[#98A2B3] mt-4 flex-grow"
|
||||
onClick={(e) => {
|
||||
acceptReview(review?.id);
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button> */}
|
||||
<LoadingButton
|
||||
type="button"
|
||||
loading={acceptLoading}
|
||||
className="border-[#98A2B3] border-2 text-black tracking-wide outline-none focus:outline-none rounded py-2 mt-4 flex-grow"
|
||||
onClick={(e) => {
|
||||
acceptReview(review?.id);
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
type="button"
|
||||
loading={loading}
|
||||
className="bg-[#D92D20] text-white tracking-wide outline-none focus:outline-none rounded py-2 mt-4 flex-grow"
|
||||
onClick={(e) => {
|
||||
declineReview(review);
|
||||
}}
|
||||
>
|
||||
Decline
|
||||
</LoadingButton>
|
||||
</div>
|
||||
{/*footer*/}
|
||||
<div className="flex px-6 pb-6 border-solid border-slate-200 rounded-b">
|
||||
<button
|
||||
className="text-[#667085] background-transparent font-semibold flex-1 px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 border border-[##98A2B3] rounded"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOW_REVIEW",
|
||||
payload: {
|
||||
showReview: false,
|
||||
review: "",
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
export default function SessionExpiredModal() {
|
||||
const { state, dispatch } = useContext(AuthContext);
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(AuthContext);
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
let modalTimeout;
|
||||
if (state.sessionExpired) {
|
||||
modalTimeout = setTimeout(() => {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
if (["superadmin", "admin"].includes(localStorage.getItem("role"))) {
|
||||
location.href = `/login?redirect_uri=${pathname}`;
|
||||
} else {
|
||||
location.href = `/login?redirect_uri=${pathname}`;
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
return () => clearTimeout(modalTimeout);
|
||||
}, [state.sessionExpired]);
|
||||
|
||||
if (!state.sessionExpired) return null;
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full">
|
||||
<Transition
|
||||
appear
|
||||
show={true}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => { }}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Session Expired
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Your current login session has expired. Redirecting to login page shortly</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
dispatch({ type: "LOGOUT" });
|
||||
globalDispatch({ type: "CLOSE_ERROR" });
|
||||
navigate("/login")
|
||||
}}
|
||||
>
|
||||
Got it, thanks!
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import { debounce } from "@/utils/utils";
|
||||
|
||||
const SmartSearch = ({ selectedData, setSelectedData, data, field, field2, errorField, getData, setError, type, multiple = false }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
value={selectedData}
|
||||
onChange={(item) => {
|
||||
setSelectedData(item);
|
||||
setError(errorField, {
|
||||
type: "manual",
|
||||
message: null
|
||||
});
|
||||
}}
|
||||
disabled={type ? true : false}
|
||||
multiple={multiple}
|
||||
>
|
||||
<div className="relative mt-1">
|
||||
<div className="relative w-full cursor-default overflow-hidden rounded-lg bg-white text-left sm:text-sm">
|
||||
<Combobox.Input as="input"
|
||||
className="border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
displayValue={(item) =>
|
||||
!field2
|
||||
? multiple
|
||||
? item.map((it) => it[field]).join(",")
|
||||
: item[field]
|
||||
: item !== undefined && item[field] !== ""
|
||||
? multiple
|
||||
? item.map((it) => `${item[field]} - ${item[field2]}`)
|
||||
: `${item[field]} - ${item[field2]}`
|
||||
: ""
|
||||
}
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
let searchValue = event.target.value;
|
||||
if (multiple) {
|
||||
let splitResult = searchValue.split(",");
|
||||
let index = splitResult.length > 1 ? splitResult.length - 1 : 0;
|
||||
searchValue = splitResult[index];
|
||||
}
|
||||
debounce(() => getData(1, 10, { [field]: searchValue.trim() }));
|
||||
if (event.target.value === "") {
|
||||
const emptyParam = { [field]: "" };
|
||||
if (field2) {
|
||||
emptyParam[field2] = "";
|
||||
}
|
||||
setSelectedData(multiple ? [] : { ...emptyParam });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setQuery("")}
|
||||
>
|
||||
<Combobox.Options className="absolute mt-1 z-50 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{data && data.length === 0 && query !== "" ? (
|
||||
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">Nothing found.</div>
|
||||
) : (
|
||||
data &&
|
||||
data.map((item) => (
|
||||
<Combobox.Option
|
||||
disabled
|
||||
key={item.id}
|
||||
className={({ active }) => `relative normal-case cursor-default select-none py-2 pl-10 pr-4 ${active ? "bg-teal-600 text-white" : "text-gray-900"}`}
|
||||
value={item}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>{!field2 ? item[field] : `${item[field]} - ${item[field2]}`}</span>
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmartSearch;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
export default function SmartSearchV2({ data, fieldToDisplay, setSelected, selected, placeholder }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const filteredData =
|
||||
query === ""
|
||||
? data
|
||||
: data
|
||||
.filter((cat) => cat[fieldToDisplay].toLowerCase().replace(/\s+/g, "").includes(query.toLowerCase().replace(/\s+/g, "")))
|
||||
.sort((a, b) => {
|
||||
if (a[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase()) > b[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase())) {
|
||||
return 1;
|
||||
} else if (a[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase()) < b[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase())) {
|
||||
return -1;
|
||||
} else {
|
||||
if (a[fieldToDisplay] > b[fieldToDisplay]) return 1;
|
||||
else return -1;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
<div className="border w-full p-2">
|
||||
<Combobox
|
||||
value={selected}
|
||||
onChange={setSelected}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative w-full cursor-default overflow-hidden text-left focus:outline-none sm:text-sm">
|
||||
<Combobox.Input
|
||||
className="w-full border-none focus:outline-none"
|
||||
displayValue={(cat) => cat[fieldToDisplay]}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={placeholder ?? "Type to search.."}
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setQuery("")}
|
||||
>
|
||||
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm review-scroll custom-calendar-scroll">
|
||||
{filteredData.length === 0 && query !== "" ? (
|
||||
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">Other</div>
|
||||
) : (
|
||||
filteredData
|
||||
.filter((cat) => cat[fieldToDisplay] != "")
|
||||
.map((cat) => (
|
||||
<Combobox.Option
|
||||
key={cat.id}
|
||||
className={({ active }) => `relative cursor-default select-none py-2 px-4 ${active ? "bg-teal-600 text-white" : "text-gray-900"}`}
|
||||
value={cat}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>{cat[fieldToDisplay]}</span>
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
const SnackBar = () => {
|
||||
const { state, dispatch } = React.useContext(GlobalContext);
|
||||
const show = state.globalMessage.length > 0;
|
||||
|
||||
return show ? (
|
||||
<div
|
||||
id="mkd-toast"
|
||||
className={`toast-animation border-l-8 fixed ${state.globalMessageType == "ERROR" ? "border-red-500 text-red-600" : "border-green-600"
|
||||
} shadow-xl absolute top-5 right-5 flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg dark:text-gray-400`}
|
||||
role="alert"
|
||||
style={{ zIndex: "200" }}
|
||||
>
|
||||
<div className="text-sm font-normal">{state.globalMessage}</div>
|
||||
<div className="flex items-center ml-auto space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
aria-label="Close"
|
||||
onClick={() => {
|
||||
dispatch({ type: "SNACKBAR", payload: { message: "", type: "" } });
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SnackBar;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Combobox, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService";
|
||||
import { useController } from "react-hook-form";
|
||||
import LocationIcon from "./frontend/icons/LocationIcon";
|
||||
|
||||
export default function StickyCustomLocationAutoComplete({ control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) {
|
||||
const { field } = useController({ control, name });
|
||||
|
||||
const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({
|
||||
apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
|
||||
options: { types: suggestionType ?? ["(region)"] },
|
||||
debounce: 200,
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={"div"}
|
||||
className={`relative w-full normal-case z-50 ${containerClassName ?? ""}`}
|
||||
value={(field.value)?.replace(', undefined', '')}
|
||||
>
|
||||
{!hideIcons && <LocationIcon />}
|
||||
|
||||
<Combobox.Input
|
||||
{...restProps}
|
||||
autoComplete="off"
|
||||
className={`w-full truncate text-black ${className ?? ""}`}
|
||||
onBlur={field.onBlur}
|
||||
value={(field.value)?.replace(', undefined', '')}
|
||||
onChange={(evt) => {
|
||||
field.onChange(evt);
|
||||
getPlacePredictions({ input: evt.target.value });
|
||||
}}
|
||||
/>
|
||||
{!hideIcons && field.value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setValue("");
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
{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">
|
||||
<svg
|
||||
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
|
||||
width="36px"
|
||||
height="36px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#d0d5dd"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
repeatCount="indefinite"
|
||||
dur="1.6666666666666667s"
|
||||
keyTimes="0;1"
|
||||
values="0;256.58892822265625"
|
||||
></animate>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox.Options
|
||||
className={`${placePredictions.length > 0 ? "py-2 shadow-lg ring-1 sticky" : ""
|
||||
} 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) => (
|
||||
<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"
|
||||
key={idx}
|
||||
value={place.structured_formatting.main_text}
|
||||
onClick={() => 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>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</Transition>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
export default function SwitchBulkMode({ enabled, setEnabled }) {
|
||||
return (
|
||||
<div className="py-1">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={setEnabled}
|
||||
className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"}
|
||||
relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${enabled ? "translate-x-7" : "translate-x-0"}
|
||||
pointer-events-none inline-block h-[24px] w-[24px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import moment from "moment";
|
||||
import { IMAGE_STATUS } from "@/utils/constants";
|
||||
|
||||
const Table = ({
|
||||
columns,
|
||||
rows,
|
||||
actions,
|
||||
profile,
|
||||
tableType,
|
||||
type,
|
||||
table1,
|
||||
table2,
|
||||
deleteMessage,
|
||||
deleteTitle,
|
||||
showDelete = true,
|
||||
onSort,
|
||||
id,
|
||||
rejectImage,
|
||||
approveImage,
|
||||
setActivePicture,
|
||||
openPictureModal,
|
||||
baasDelete,
|
||||
emailActions,
|
||||
}) => {
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<table
|
||||
className="min-w-full divide-y divide-gray-200 border border-t-0 bg-white"
|
||||
id={id}
|
||||
>
|
||||
<thead className="cursor-pointer bg-gray-50">
|
||||
<tr className="cursor-pointer">
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
scope="col"
|
||||
className="cursor-pointer whitespace-nowrap px-6 py-4 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
onClick={() => onSort(column.accessor)}
|
||||
>
|
||||
{column.header}
|
||||
{column.isSorted}
|
||||
<span>{column.isSorted ? (column.isSortedDesc ? " ▼" : " ▲") : ""}</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{rows?.length > 0 && rows.map((row, i) => {
|
||||
return (
|
||||
<tr
|
||||
className="py-2 text-sm"
|
||||
key={i}
|
||||
>
|
||||
{columns.map((cell, index) => {
|
||||
if (cell.accessor.split(",").length > 1) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.accessor.split(",").map((accessor, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`mr-2 ${cell?.multiline ? "mb-1 block" : ""}`}
|
||||
>
|
||||
{row[accessor.trim()]}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "" && emailActions) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className={`bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-4 font-bold text-transparent ${showDelete ? "border-r border-gray-200" : ""}`}
|
||||
onClick={() => {
|
||||
navigate(`/admin/edit-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{showDelete && (
|
||||
<button
|
||||
className="ml-2 px-1 text-[#667085]"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: deleteTitle,
|
||||
modalShowMessage: deleteMessage,
|
||||
modalBtnText: "Delete",
|
||||
type: "Delete",
|
||||
itemId: row.id,
|
||||
table1: table1,
|
||||
table2: table2,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`ml-4 border-l border-gray-200 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pl-4 font-bold text-transparent`}
|
||||
onClick={() => {
|
||||
navigate(`/admin/view-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "" && profile && cell.viewProperty) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className={`bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-4 font-bold text-transparent ${showDelete ? "border-r border-gray-200" : ""}`}
|
||||
onClick={() => {
|
||||
navigate(`/${type === "host" ? 'host':'admin'}/edit-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{showDelete && (
|
||||
<button
|
||||
className="ml-2 px-1 text-[#667085]"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: deleteTitle,
|
||||
modalShowMessage: deleteMessage,
|
||||
modalBtnText: "Delete",
|
||||
type: baasDelete ? "BaasDelete" : "Delete",
|
||||
itemId: row.id,
|
||||
table1: table1,
|
||||
table2: table2,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`ml-4 border-l border-gray-200 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pl-4 font-bold text-transparent`}
|
||||
onClick={() => {
|
||||
navigate(`/admin/view-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.accessor === "" && actions) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
setActivePicture(row.photo);
|
||||
openPictureModal();
|
||||
}}
|
||||
>
|
||||
View Picture
|
||||
</button>
|
||||
{row?.is_photo_approved == IMAGE_STATUS.IN_REVIEW ? (
|
||||
<>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => rejectImage(row)}
|
||||
>
|
||||
Reject Photo
|
||||
</button>
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => approveImage(row.id)}
|
||||
>
|
||||
Approve Photo
|
||||
</button>
|
||||
</>
|
||||
) : row?.is_photo_approved === IMAGE_STATUS.APPROVED ? (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => rejectImage(row)}
|
||||
>
|
||||
Reject Photo
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => approveImage(row.id)}
|
||||
>
|
||||
Approve Photo
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ml-2 border-r border-gray-200 px-1 pr-4 text-[#667085]"
|
||||
onClick={() => {
|
||||
navigate(`/admin/view-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Profile
|
||||
</button>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "" && profile) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="gap-3 whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<button
|
||||
className={`bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-4 font-bold text-transparent ${showDelete ? "border-r border-gray-200" : ""}`}
|
||||
onClick={() => {
|
||||
navigate(`${type === "host" ? 'host':'admin'}/edit-${tableType.toLowerCase()}/${row.id}`, {
|
||||
state: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
{showDelete && (
|
||||
<button
|
||||
className="ml-2 px-1 text-[#667085]"
|
||||
onClick={() => {
|
||||
globalDispatch({
|
||||
type: "SHOWMODAL",
|
||||
payload: {
|
||||
showModal: true,
|
||||
modalShowTitle: deleteTitle,
|
||||
modalShowMessage: deleteMessage,
|
||||
modalBtnText: "Delete",
|
||||
type: baasDelete ? "BaasDelete" : "Delete",
|
||||
itemId: row.id,
|
||||
table1: table1,
|
||||
table2: table2,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.accessor == "image" || cell.accessor == "photo_url") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[80px] whitespace-nowrap px-6 py-2"
|
||||
>
|
||||
<img
|
||||
src={row[cell.accessor]}
|
||||
className="h-16 "
|
||||
alt="image"
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "icon") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="max-h-[80px] whitespace-nowrap object-cover px-6 py-2"
|
||||
>
|
||||
<img
|
||||
src={row[cell.accessor]}
|
||||
className="h-16 "
|
||||
alt="icon"
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.statusMapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
<span className={`${row[cell.accessor] === 1 ? "text-black" : "text-[#98A2B3]"} rounded-full border border-[#EAECF0] bg-[#F9FAFB] py-[2px] px-[10px]`}>
|
||||
{" "}
|
||||
{cell.statusMapping[row[cell.accessor]]}
|
||||
</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.mapping) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.mapping[row[cell.accessor]] ?? "N/A"}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.accessor == "dob") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor] ? moment(row[cell.accessor]).format("MM/DD/YY") : ""}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor?.includes("email")) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor == "num_properties") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case "
|
||||
>
|
||||
<button
|
||||
className="border-r border-gray-200 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text pr-2 font-bold text-transparent"
|
||||
onClick={() => {
|
||||
navigate(`/admin/property_spaces?host_id=${row.id}`);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
{/* <span className="ml-2"> {row[cell.accessor]}</span> */}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor.includes("payout") || cell.amountField) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case "
|
||||
>
|
||||
<span className="ml-2">${(row[cell.accessor] ? row[cell.accessor] : 0).toFixed(2)}</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.formatDate) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case "
|
||||
>
|
||||
<span className="ml-2">{new Date(row[cell.accessor]).toUTCString()}</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.nested) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{row[cell.nested][cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.idPrefix) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4 normal-case"
|
||||
>
|
||||
{cell.idPrefix + row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.format) {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{cell.format(row[cell.accessor])}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
if (cell.accessor === "cost") {
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
${row[cell.accessor]}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
key={index}
|
||||
className="whitespace-nowrap px-6 py-4"
|
||||
>
|
||||
{row[cell.accessor]}
|
||||
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
const TopHeader = () => {
|
||||
const { state, dispatch } = React.useContext(GlobalContext);
|
||||
let { isOpen } = state;
|
||||
let toggleOpen = (open) =>
|
||||
dispatch({
|
||||
type: "OPEN_SIDEBAR",
|
||||
payload: { isOpen: open },
|
||||
});
|
||||
return (
|
||||
<div className="page-header block lg:hidden shadow">
|
||||
<span onClick={() => toggleOpen(!isOpen)}>
|
||||
{!isOpen ? (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopHeader;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
const AddOnCounter = ({ data, register, singleName }) => {
|
||||
const [counter, setCounter] = useState(1);
|
||||
console.log(singleName)
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex justify-between mb-[12px]">
|
||||
<form className="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
className=""
|
||||
id={"cb" + data.id}
|
||||
value={data.cost}
|
||||
{...register(singleName ?? data.add_on_name)}
|
||||
/>
|
||||
<label className="text-black" htmlFor={"cb" + data.id}>{data.add_on_name}</label>
|
||||
</form>
|
||||
|
||||
<div className="flex gap-[32px] items-center">
|
||||
{data.showCounter && (
|
||||
<div className="border border-[#475467] rounded-xl p-2 flex gap-[10px] items-center text-white">
|
||||
<button
|
||||
className={"border rounded-full px-2 text-white" + (counter > 0 ? " border-[#475467]" : "")}
|
||||
onClick={() =>
|
||||
setCounter((prev) => {
|
||||
if (prev == 0) return prev;
|
||||
return prev - 1;
|
||||
})
|
||||
}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{counter}</span>
|
||||
<button
|
||||
className={"border rounded-full px-2" + " border-[#475467]"}
|
||||
onClick={() => setCounter((prev) => prev + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="font-semibold text-[#344054]"> ${data.cost * counter}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddOnCounter;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AddonCounterV2({ data, register, name }) {
|
||||
const [counter, setCounter] = useState(1);
|
||||
return (
|
||||
<div className="flex justify-between mb-[12px]">
|
||||
<form className="checkbox-container mb-[12px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={data.add_on_name}
|
||||
id={"cb" + data.id}
|
||||
{...register(name)}
|
||||
/>
|
||||
<label htmlFor={"cb" + data.id}>{data.add_on_name}</label>
|
||||
</form>
|
||||
<div className="flex gap-[32px] items-center">
|
||||
{data.showCounter && (
|
||||
<div className="border border-[#475467] rounded-xl p-2 flex gap-[10px] items-center">
|
||||
<button
|
||||
className={"border rounded-full px-2" + (counter > 0 ? " border-[#475467]" : "")}
|
||||
onClick={() =>
|
||||
setCounter((prev) => {
|
||||
if (prev == 0) return prev;
|
||||
return prev - 1;
|
||||
})
|
||||
}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{counter}</span>
|
||||
<button
|
||||
className={"border rounded-full px-2" + " border-[#475467]"}
|
||||
onClick={() => setCounter((prev) => prev + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="font-semibold text-[#344054]"> ${data.cost * counter}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import CustomSelect from "./CustomSelect";
|
||||
import ReviewCard from "./ReviewCard";
|
||||
|
||||
export default function AllReviewsModal({ modalOpen, closeModal, reviews, onDirectionChange }) {
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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-red-800" />
|
||||
</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-7xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<div className="flex md:flex-row flex-col gap-4 justify-between items-center">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-2xl font-bold leading-6 text-gray-900"
|
||||
>
|
||||
All Reviews ({reviews.length})
|
||||
</Dialog.Title>
|
||||
<div className="flex gap-8 items-start">
|
||||
<CustomSelect
|
||||
options={[
|
||||
{ label: "By Date: Newest First", value: "DESC" },
|
||||
{ label: "By Date: Oldest First", value: "ASC" },
|
||||
]}
|
||||
onChange={onDirectionChange}
|
||||
accessor="label"
|
||||
valueAccessor="value"
|
||||
className="min-w-[200px]"
|
||||
/>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="p-1 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<section className="overflow-y-auto h-[70vh] review-scroll pr-[13px]">
|
||||
{reviews.map((rw) => (
|
||||
<ReviewCard
|
||||
key={rw.id}
|
||||
data={rw}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { callCustomAPI } from "@/utils/callCustomAPI";
|
||||
import { parseJsonSafely } from "@/utils/utils";
|
||||
import PencilIcon from "./icons/PencilIcon";
|
||||
import TrashIcon from "./icons/TrashIcon";
|
||||
import { formatAMPM, daysMapping } from "@/utils/date-time-utils";
|
||||
import { useContext } from "react";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import ThreeDotsMenu from "./ThreeDotsMenu";
|
||||
import EditTemplateModal from "@/pages/Host/Spaces/Add/EditTemplateModal";
|
||||
|
||||
const AvailabilityTemplate = ({ data, forceRender, selectedTemplate, setSelectedTemplate }) => {
|
||||
const [editPopup, setEditPopup] = useState(false);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
|
||||
const parsedSlots = parseJsonSafely(data.slots, []);
|
||||
|
||||
async function deleteTemplate(id) {
|
||||
globalDispatch({ type: "START_LOADING" });
|
||||
try {
|
||||
await callCustomAPI("host/schedule-slot/template", "delete", { id }, "");
|
||||
if (forceRender) forceRender(new Date());
|
||||
} catch (err) {
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-[44px] flex items-center justify-between rounded-lg border border-[#EAECF0] bg-[#F9FAFB] p-[12px]">
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between lg:justify-start">
|
||||
<div className="lg:min-w-[370px]">
|
||||
<h3 className="text-xl font-semibold">{data.template_name}</h3>
|
||||
<p className="capitalize">
|
||||
(
|
||||
{daysMapping
|
||||
.filter((day) => data[day] == 1)
|
||||
.map((day, i, arr) => {
|
||||
return day + (i == arr.length - 1 ? "" : ", ");
|
||||
})}
|
||||
)
|
||||
</p>
|
||||
<div className="block md:hidden">
|
||||
<br />
|
||||
<ThreeDotsMenu
|
||||
items={[
|
||||
{
|
||||
label: "Edit",
|
||||
icon: <PencilIcon />,
|
||||
onClick: () => setEditPopup(true),
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: <TrashIcon />,
|
||||
onClick: () => deleteTemplate(data.id),
|
||||
},
|
||||
]}
|
||||
menuClassName="right-[unset] left-0 origin-top-left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-[32px] lg:flex-nowrap lg:justify-start">
|
||||
{Array.isArray(parsedSlots) &&
|
||||
parsedSlots.slice(0, 2).map((slot, idx) => (
|
||||
<div
|
||||
className="whitespace-nowrap"
|
||||
key={idx}
|
||||
>
|
||||
<p className="text-sm">Slot {idx + 1}:</p>
|
||||
<p className="font-semibold">
|
||||
{formatAMPM(slot.start)} - {formatAMPM(slot.end)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:flex">
|
||||
<button
|
||||
onClick={() => setEditPopup(true)}
|
||||
className={`inline-flex w-full items-center gap-2 px-4 py-2 text-center text-sm`}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteTemplate(data.id)}
|
||||
className={`inline-flex w-full items-center gap-2 px-4 py-2 text-center text-sm`}
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EditTemplateModal
|
||||
data={data}
|
||||
selectedTemplate={selectedTemplate}
|
||||
setSelectedTemplate={setSelectedTemplate}
|
||||
forceRender={forceRender}
|
||||
onSuccess={() => {
|
||||
if (forceRender) forceRender();
|
||||
}}
|
||||
modalOpen={editPopup}
|
||||
closeModal={() => setEditPopup(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailabilityTemplate;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { isNotInViewport } from "@/utils/utils";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import Icon from "../Icons";
|
||||
import HeartIcon from "./icons/HeartIcon";
|
||||
import LogoutIcon from "./icons/LogoutIcon";
|
||||
import SearchIcon from "./icons/SearchIcon";
|
||||
|
||||
export default function BottomNav({ scrollDir, showAccount }) {
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { dispatch } = useContext(AuthContext);
|
||||
|
||||
const [showStaticBar, setShowStaticBar] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setShowStaticBar(isNotInViewport("search-bar"));
|
||||
};
|
||||
window.addEventListener("scroll", onScroll);
|
||||
setShowStaticBar(isNotInViewport("search-bar"));
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
function logout() {
|
||||
dispatch({ type: "LOGOUT" });
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
const whiteList = ["/search", "/"];
|
||||
|
||||
if (!whiteList.some((path) => pathname == path)) return null;
|
||||
|
||||
return (
|
||||
<div className={`${scrollDir == "UP" && showStaticBar ? "block" : "hidden"} md:hidden bg-white py-1 fixed bottom-0 left-0 right-0 z-[200] bottom-nav border-t border-b slideUp`}>
|
||||
<div className="flex justify-center text-sm">
|
||||
<NavLink
|
||||
to="/explore"
|
||||
className="px-4 py-2 flex flex-col items-center justify-between"
|
||||
>
|
||||
<SearchIcon stroke={"black"} />
|
||||
Explore
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/favorites"
|
||||
className="px-4 py-2 flex flex-col items-center justify-between"
|
||||
>
|
||||
<HeartIcon stroke={"black"} />
|
||||
Favorites
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={showAccount ? "/account/profile" : "/login"}
|
||||
className="px-4 py-2 flex flex-col items-center justify-between"
|
||||
>
|
||||
<Icon
|
||||
type="user"
|
||||
fill=""
|
||||
variant="circle"
|
||||
className={"my-stroke-" + "white"}
|
||||
/>
|
||||
{showAccount ? "Account" : "Login"}
|
||||
</NavLink>
|
||||
<button
|
||||
className={`${showAccount ? "flex" : "hidden"} px-4 py-2 flex-col items-center justify-between`}
|
||||
onClick={logout}
|
||||
>
|
||||
<LogoutIcon />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
const Counter = ({ register, setValue, name, initialValue, maxCount, minCount }) => {
|
||||
const [counter, setCounter] = useState(Number(initialValue) || 0);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(name, counter);
|
||||
}, [counter]);
|
||||
|
||||
useEffect(() => {
|
||||
setCounter(initialValue || 0);
|
||||
}, [initialValue]);
|
||||
|
||||
return (
|
||||
<div className="p-2 flex gap-[10px] items-center font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
className={"border-2 rounded-md px-3 text-2xl" + (counter > (minCount || 0) ? " border-black" : " border-[#D0D5DD]")}
|
||||
onClick={() =>
|
||||
setCounter((prev) => {
|
||||
if (prev == (minCount || 0)) return prev;
|
||||
return prev - 1;
|
||||
})
|
||||
}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{counter}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={"border-2 text-2xl rounded-md px-3" + (counter >= maxCount ? " border-[#D0D5DD]" : " border-black")}
|
||||
onClick={() =>
|
||||
setCounter((prev) => {
|
||||
if (prev + 1 > Number(maxCount)) return prev;
|
||||
return prev + 1;
|
||||
})
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(name)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Counter;
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import { useState } from "react";
|
||||
import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService";
|
||||
import LocationIcon from "./icons/LocationIcon";
|
||||
|
||||
function truncateString(str, limit) {
|
||||
if (str.length > limit) return str.slice(0, limit) + "...";
|
||||
return str;
|
||||
}
|
||||
|
||||
export default function CustomLocationAutoComplete({ location, setLocation, className, truncateNum, onChange, onClear, hideIcon, detailMode, ...restProps }) {
|
||||
const [predictionsOpen, setPredictionsOpen] = useState(false);
|
||||
// const [predictions, setPredictions] = useState([]);
|
||||
|
||||
const { placesService, placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({
|
||||
apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// fetch place details for the first element in placePredictions array
|
||||
// if (placePredictions.length)
|
||||
// placesService?.getDetails(
|
||||
// {
|
||||
// placeId: placePredictions[0].place_id,
|
||||
// },
|
||||
// (placeDetails) => setPredictions(placeDetails),
|
||||
// );
|
||||
// }, [placePredictions]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
as={"div"}
|
||||
className={`relative ${className ?? ""}`}
|
||||
>
|
||||
{!hideIcon && <LocationIcon />}
|
||||
|
||||
<input
|
||||
{...restProps}
|
||||
className="border-0 focus:outline-none w-full truncate text-black"
|
||||
onChange={(evt) => {
|
||||
getPlacePredictions({ input: evt.target.value });
|
||||
setLocation(evt.target.value);
|
||||
if (onChange) {
|
||||
onChange(evt.target.value);
|
||||
}
|
||||
}}
|
||||
onFocus={() => setPredictionsOpen(true)}
|
||||
onBlur={() => setPredictionsOpen(false)}
|
||||
value={location}
|
||||
/>
|
||||
{location && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLocation("");
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Transition
|
||||
show={predictionsOpen}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className={`${placePredictions.length > 0 ? "py-2 shadow-lg ring-1" : ""
|
||||
} z-50 absolute left-0 right-0 top-full mt-2 w-full origin-top divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`}
|
||||
>
|
||||
{!detailMode &&
|
||||
placePredictions.map((place, idx) => (
|
||||
<div
|
||||
className="px-1 py-1"
|
||||
key={idx}
|
||||
>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`${active ? "bg-gray-100 text-black" : "text-gray-800"} pill flex w-full items-center rounded-md px-2 py-2 text-sm truncate`}
|
||||
onClick={() => {
|
||||
setLocation(place.description);
|
||||
if (onChange) {
|
||||
onChange(place.description);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{truncateString(place.description, truncateNum ?? 30)}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
))}
|
||||
{detailMode &&
|
||||
placePredictions.map((place, idx) => (
|
||||
<div
|
||||
className="px-1 py-1"
|
||||
key={idx}
|
||||
>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`${active ? "bg-gray-100 text-black" : "text-gray-800"} pill flex w-full items-center rounded-md px-3 pr-5 py-3 text-sm truncate`}
|
||||
onClick={() => {
|
||||
setLocation(place.structured_formatting.main_text);
|
||||
if (onChange) {
|
||||
onChange(place.structured_formatting.main_text);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
{place.structured_formatting.main_text.slice(
|
||||
place.structured_formatting.main_text_matched_substrings[0].offset,
|
||||
place.structured_formatting.main_text_matched_substrings[0].length,
|
||||
)}
|
||||
</span>
|
||||
{place.structured_formatting.main_text.slice(
|
||||
place.structured_formatting.main_text_matched_substrings[0].offset + place.structured_formatting.main_text_matched_substrings[0].length,
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import NextIcon from "./icons/NextIcon";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function CustomSelect({
|
||||
options,
|
||||
accessor,
|
||||
name,
|
||||
register,
|
||||
setValue,
|
||||
formMode,
|
||||
valueAccessor,
|
||||
defaultValue,
|
||||
className,
|
||||
optionsClassName,
|
||||
defaultOptionClassName,
|
||||
onChange,
|
||||
initialEditValue,
|
||||
buttonClassName,
|
||||
listOptionClassName,
|
||||
noSelectedHighlight,
|
||||
hideIcon,
|
||||
}) {
|
||||
const [selected, setSelected] = useState(defaultValue ?? options[0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formMode) {
|
||||
if (selected == defaultValue) {
|
||||
setValue(name, "");
|
||||
} else {
|
||||
setValue(name, valueAccessor ? selected[valueAccessor] : selected);
|
||||
}
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formMode && defaultValue) {
|
||||
setValue(name, "");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialEditValue) {
|
||||
setSelected(initialEditValue);
|
||||
}
|
||||
}, [JSON.stringify(initialEditValue)]);
|
||||
|
||||
return (
|
||||
<div className={`border p-2 rounded-md focus:outline-none active:outline-none ${className}`}>
|
||||
<Listbox
|
||||
value={selected}
|
||||
onChange={(v) => {
|
||||
setSelected(v);
|
||||
if (onChange) {
|
||||
onChange(valueAccessor ? v[valueAccessor] : v);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formMode && (
|
||||
<input
|
||||
{...register(name)}
|
||||
type="hidden"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button
|
||||
className={`flex items-center justify-between w-full ${(accessor && JSON.stringify(selected) == JSON.stringify(defaultValue)) || accessor == defaultValue ? defaultOptionClassName : ""}`}
|
||||
>
|
||||
<span className={`block truncate ${buttonClassName ?? ""}`}>{accessor ? selected[accessor] : selected}</span>
|
||||
<span className={`${hideIcon ? "hidden" : "inline"} pointer-events-none flex items-center pr-2`}>
|
||||
<NextIcon />
|
||||
</span>{" "}
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options
|
||||
className={`absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm ${optionsClassName} z-50 tiny-scroll`}
|
||||
>
|
||||
{defaultValue && (
|
||||
<Listbox.Option
|
||||
className={({ active }) => `relative cursor-default select-none py-2 pr-4 ${active ? "bg-amber-100 text-amber-900" : "text-gray-900"} ${listOptionClassName ?? "pl-10"}`}
|
||||
value={defaultValue}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>{accessor ? defaultValue[accessor] : defaultValue}</span>
|
||||
{selected && !noSelectedHighlight ? <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">✓</span> : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
)}
|
||||
{options.map((option, idx) => (
|
||||
<Listbox.Option
|
||||
key={idx}
|
||||
className={({ active }) => `relative cursor-default select-none py-2 ${listOptionClassName ?? "pl-10"} pr-4 ${active ? "bg-amber-100 text-amber-900" : "text-gray-900"}`}
|
||||
value={option}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>{accessor ? option[accessor] : option}</span>
|
||||
{selected && !noSelectedHighlight ? <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">✓</span> : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { formatDate, isSameDay } from "@/utils/date-time-utils";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Calendar } from "react-calendar";
|
||||
import CalendarIcon from "./icons/CalendarIcon";
|
||||
import NextIcon from "./icons/NextIcon";
|
||||
import PrevIcon from "./icons/PrevIcon";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
const DatePicker = ({ initialDate, searchDate, control, setSearchDate, className, placeHolder, min, max, onChange, onReset, labelClassName, xClassName, panelClassName, hideIcon }) => {
|
||||
let isInitial = isSameDay(searchDate, initialDate);
|
||||
const { field, fieldState } = useController({ control, name });
|
||||
const [date, setDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNaN(new Date(field.value))) setDate(new Date(field.value));
|
||||
}, [field.value]);
|
||||
return (
|
||||
<div className={`w-full ${className ?? ""}`}>
|
||||
<Popover className="lg:relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button className={`${open ? "" : "text-opacity-90"} flex justify-between w-full`}>
|
||||
<div className={`flex gap-2 ${labelClassName ?? ""}`}>
|
||||
{/* {!hideIcon ? <CalendarIcon /> : null} */}
|
||||
{isInitial ? (
|
||||
<CalendarIcon />
|
||||
) : (
|
||||
<button
|
||||
className={`self-end ${xClassName ?? ""}`}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSearchDate(initialDate);
|
||||
if (onReset) {
|
||||
onReset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<span className={`${isInitial ? "text-gray-400" : ""}`}>{!isInitial ? formatDate(searchDate) : placeHolder ?? "Select date"}</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
className="relativ"
|
||||
>
|
||||
<Popover.Panel className={`absolute left-1/2 z-10 mt-3 -translate-x-1/2 transform px-4 sm:px-0 ${panelClassName ?? ""}`}>
|
||||
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<Calendar
|
||||
onChange={(v) => {
|
||||
setSearchDate(v);
|
||||
|
||||
if (onChange) {
|
||||
onChange(v);
|
||||
}
|
||||
}}
|
||||
value={date}
|
||||
className={`calendar date-picker`}
|
||||
defaultValue={initialDate}
|
||||
nextLabel={<NextIcon />}
|
||||
prevLabel={<PrevIcon />}
|
||||
next2Label={
|
||||
<div
|
||||
className="w-full h-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
prev2Label={
|
||||
<div
|
||||
className="w-full h-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
minDate={min}
|
||||
maxDate={max}
|
||||
maxDetail="month"
|
||||
/>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import moment from "moment";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Calendar } from "react-calendar";
|
||||
import { useController } from "react-hook-form";
|
||||
import CalendarIcon from "./icons/CalendarIcon";
|
||||
import NextIcon from "./icons/NextIcon";
|
||||
import PrevIcon from "./icons/PrevIcon";
|
||||
|
||||
export default function DatePickerV2({ control, name, min, type, max, setValue, classNameCustomized }) {
|
||||
const { field, fieldState } = useController({ control, name });
|
||||
const [date, setDate] = useState(new Date());
|
||||
const [showCalender, setShowCalender] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNaN(new Date(field.value))) setDate(new Date(field.value));
|
||||
}, [field.value]);
|
||||
|
||||
return (
|
||||
<div className={`${classNameCustomized ? classNameCustomized : type ? "mb-0" : "mb-8 w-full relative"}`}>
|
||||
<div className="relative !min-h-[40px] gap-3 hover:cursor-pointer">
|
||||
<input
|
||||
type="date"
|
||||
max="9999-12-31"
|
||||
className={`${classNameCustomized ? "h-10" : "h-12"} text-left !min-h-[40px] w-full resize-non rounded-md border bg-transparent p-2 px-4 focus:outline-none active:outline-none`}
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
disabled={false}
|
||||
placeholder="ceremony"
|
||||
style={{ textAlignLast: 'left' }}
|
||||
/>
|
||||
<div className="flex items-center absolute -right-2 top-[7px]">
|
||||
<span>D.O.B</span>
|
||||
<button
|
||||
className={" h-[38px] bg-white px-3"}
|
||||
type="button"
|
||||
onClick={() => setShowCalender(!showCalender)}
|
||||
>
|
||||
<CalendarIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showCalender &&
|
||||
<div className="absolute z-50">
|
||||
<Calendar
|
||||
onChange={(v) => {
|
||||
setValue(moment(v).format("yyyy-MM-DD"));
|
||||
setShowCalender(false);
|
||||
}}
|
||||
value={date}
|
||||
className={`calendar date-picker`}
|
||||
nextLabel={<NextIcon />}
|
||||
prevLabel={<PrevIcon />}
|
||||
next2Label={
|
||||
<div
|
||||
className="h-full w-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
prev2Label={
|
||||
<div
|
||||
className="h-full w-full cursor-default"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
></div>
|
||||
}
|
||||
minDate={min}
|
||||
maxDate={max}
|
||||
maxDetail="month"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { fullMonthsMapping, hourlySlots, monthsMapping, daysMapping } from "@/utils/date-time-utils";
|
||||
import { formatScheduleDate, parseJsonSafely } from "@/utils/utils";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { Calendar } from "react-calendar";
|
||||
import CalendarIcon from "./icons/CalendarIcon";
|
||||
import NextIcon from "./icons/NextIcon";
|
||||
import PrevIcon from "./icons/PrevIcon";
|
||||
|
||||
const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalendar, setShowCalendar, toDefault, fromDefault, bookedSlots, scheduleTemplate, defaultMessage }) => {
|
||||
const [selectedDate, setSelectedDate] = useState(defaultDate ?? new Date());
|
||||
const [from, setFrom] = useState(fromDefault ?? "");
|
||||
const [to, setTo] = useState(toDefault ?? "");
|
||||
|
||||
const onApply = () => {
|
||||
setValue("from", from);
|
||||
setValue("to", to);
|
||||
setValue("selectedDate", selectedDate);
|
||||
setShowCalendar(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={showCalendar ? "popup-mobile z-50" : ""}
|
||||
onClick={() => setShowCalendar((prev) => !prev)}
|
||||
>
|
||||
{fieldNames.map((field, idx) => (
|
||||
<input
|
||||
key={idx}
|
||||
type="hidden"
|
||||
{...register(field)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`${showCalendar ? "" : "border-2"} md:border-2 p-2 w-full md:relative flex pr-16 gap-2 items-center`}
|
||||
onClick={(e) => {
|
||||
setShowCalendar((prev) => !prev);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={`md:inline ${showCalendar ? "hidden" : ""}`}>
|
||||
<CalendarIcon />
|
||||
</div>
|
||||
<span
|
||||
id="booking-time"
|
||||
className={showCalendar ? "hidden" : "inline whitespace-nowrap md:text-base text-sm"}
|
||||
>
|
||||
{from && to
|
||||
? monthsMapping[selectedDate.getMonth()] + " " + selectedDate.getDate() + "/" + selectedDate.getFullYear() + " - " + from + " to " + to
|
||||
: defaultMessage ?? "Select date and time"}
|
||||
</span>
|
||||
{
|
||||
<div
|
||||
className={`${showCalendar ? "block" : "hidden"
|
||||
} absolute md:w-[unset] w-[80vw] bottom-[15px] top-[0%] md:-top-[22.5rem] 2xl:-top-[20rem] md:-left-10 lg:left-[-150px] left-0 md:right-[unset] right-0 text-center mx-auto shadow-lg bg-white border-2 text-sm md:max-h-[unset] min-h-[55vh] overflow-y-auto`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center border-b p-[16px]">
|
||||
<h3 className="text-xl font-semibold">Select date and time</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCalendar(false)}
|
||||
className="p-1 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex md:flex-row flex-col">
|
||||
<div className="">
|
||||
<Calendar
|
||||
onChange={(newDate) => {
|
||||
setSelectedDate(newDate);
|
||||
setFrom("");
|
||||
setTo("");
|
||||
}}
|
||||
value={selectedDate}
|
||||
className={`custom-calendar`}
|
||||
nextLabel={<NextIcon />}
|
||||
prevLabel={<PrevIcon />}
|
||||
next2Label={<></>}
|
||||
prev2Label={<></>}
|
||||
tileDisabled={({ date }) => {
|
||||
let customSlots = [];
|
||||
try {
|
||||
if (scheduleTemplate?.custom_slots && (Object.keys(scheduleTemplate?.custom_slots))?.length > 0) {
|
||||
customSlots = JSON.parse(scheduleTemplate?.custom_slots || "[]");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Invalid JSON in custom_slots", e);
|
||||
}
|
||||
if (customSlots.length > 0 && customSlots[(formatScheduleDate(date)).toString()]?.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (scheduleTemplate?.id && scheduleTemplate[daysMapping[date.getDay()]] != 1) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
minDate={new Date()}
|
||||
maxDetail="month"
|
||||
/>
|
||||
<p className="text-left p-[16px] text-[#667085]">Pacific Time - US & Canada</p>
|
||||
<div className="md:flex hidden px-[16px] py-1 cursor-default text-left">
|
||||
<p className="min-w-[150px]">From - {from}</p>
|
||||
<p>Until - {to}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<p className="font-semibold mb-4 text-center">
|
||||
<span className="capitalize">{daysMapping[selectedDate.getDay()]}</span> , {fullMonthsMapping[selectedDate.getMonth()]} {selectedDate.getDate()}
|
||||
</p>
|
||||
<div className="flex flex-col gap-[12px] custom-calendar-scroll review-scroll overflow-y-auto overflow-x-hidden md:max-h-[270px] max-h-[150px] md:px-6 px-3 text-[#667085]">
|
||||
{hourlySlots.map((tm, idx) => {
|
||||
var formattedDate = moment(selectedDate).format("MM/DD/YY");
|
||||
var fromTime = new Date(formattedDate + " " + from);
|
||||
var toTime = new Date(formattedDate + " " + to);
|
||||
var slotTime = new Date(formattedDate + " " + tm);
|
||||
var slotTimeOnly = new Date("01/01/2001" + " " + tm);
|
||||
var json = scheduleTemplate.custom_slots ?? "[]";
|
||||
var custom_slots_obj = parseJsonSafely(json, {});
|
||||
var custom_slots = custom_slots_obj[formattedDate] ?? [];
|
||||
custom_slots = custom_slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) }));
|
||||
var template_slots = Array.isArray(scheduleTemplate.slots) ? scheduleTemplate.slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) })) : [];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={idx}
|
||||
className={`${from == tm || to == tm ? "border-black border-2" : "border disabled:bg-[#F2F4F7] disabled:line-through border-[#EAECF0]"
|
||||
} md:w-[152px] w-full text-center py-[8px] ${from && to && fromTime <= slotTime && toTime >= slotTime ? "font-semibold between-slots" : ""}`}
|
||||
onClick={(e) => {
|
||||
if (from == tm) {
|
||||
setFrom("");
|
||||
setTo("");
|
||||
return;
|
||||
}
|
||||
if (to == tm) {
|
||||
setTo("");
|
||||
return;
|
||||
}
|
||||
if (from == "") {
|
||||
setFrom(e.target.innerText);
|
||||
} else {
|
||||
setTo(e.target.innerText);
|
||||
}
|
||||
}}
|
||||
disabled={(() => {
|
||||
// disabled slots that are not available in template only if a custom slot was not defined for the selectedDay
|
||||
// if custom slots were defined for selectedDay then disable slots that are not included
|
||||
|
||||
// disable if time is < current time
|
||||
if (slotTime < new Date()) return true;
|
||||
|
||||
if (custom_slots.length > 0) {
|
||||
var shouldDisable = false;
|
||||
for (let i = 0; i < custom_slots.length; i++) {
|
||||
const slot = custom_slots[i];
|
||||
if (slot.fromTime <= slotTime && slot.toTime >= slotTime) {
|
||||
shouldDisable = false;
|
||||
break;
|
||||
} else {
|
||||
shouldDisable = true;
|
||||
}
|
||||
}
|
||||
if (shouldDisable) return true;
|
||||
}
|
||||
else {
|
||||
var shouldDisable = false;
|
||||
for (let i = 0; i < template_slots.length; i++) {
|
||||
const slot = template_slots[i];
|
||||
if (slot.fromTime <= slotTimeOnly && slot.toTime >= slotTimeOnly) {
|
||||
shouldDisable = false;
|
||||
break;
|
||||
} else {
|
||||
shouldDisable = true;
|
||||
}
|
||||
}
|
||||
if (shouldDisable) return true;
|
||||
}
|
||||
})()}
|
||||
>
|
||||
{tm}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-8 px-6">
|
||||
<button
|
||||
type="button"
|
||||
className="login-btn-gradient w-[152px] text-center py-[8px] rounded-sm text-white"
|
||||
disabled={from == "" || to == ""}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div className="md:hidden flex px-1 py-1 mt-2 cursor-default text-left">
|
||||
<p className="min-w-[150px]">From - {from}</p>
|
||||
<p>Until - {to}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimePicker;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DRAFT_STATUS } from "@/utils/constants";
|
||||
|
||||
const DraftProgress = ({ data, scheduleTemplate }) => {
|
||||
return (
|
||||
<div className="flex relative mx-auto md:max-w-lg max-w-[300px] items-center justify-between mb-40 normal-case">
|
||||
<div className="absolute left-0 absolute-middle bg-gray-300 right-0 flex">
|
||||
<div className={`${data.draft_status >= DRAFT_STATUS.IMAGES ? "login-btn-gradient" : ""} h-full flex-grow`}></div>
|
||||
<div className={`${data.draft_status > DRAFT_STATUS.SCHEDULING ? "login-btn-gradient" : ""} h-full flex-grow`}></div>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<Link
|
||||
to={`/account/my-spaces/${data.id}/edit-${data.draft_status >= DRAFT_STATUS.PROPERTY_SPACE ? "property-space?mode=edit" : "property-space?mode=create"}`}
|
||||
className={`draft-stage ${data.draft_status >= DRAFT_STATUS.PROPERTY_SPACE ? "complete" : ""}`}
|
||||
state={data}
|
||||
>
|
||||
1
|
||||
</Link>
|
||||
<p className="absolute -left-6">About location</p>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<Link
|
||||
to={`/account/my-spaces/${data.id}/edit-${data.draft_status >= DRAFT_STATUS.IMAGES ? "images?mode=edit" : "images?mode=create"}`}
|
||||
className={`draft-stage ${data.draft_status >= DRAFT_STATUS.IMAGES ? "complete" : ""}`}
|
||||
state={data}
|
||||
>
|
||||
2
|
||||
</Link>
|
||||
<p className="absolute md:w-[unset] -left-4 !w-[60px]">Images, Addons, FAQs etc</p>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<Link
|
||||
to={`/account/my-spaces/${data.id}/edit-${scheduleTemplate.id ? "scheduling?mode=edit" : "scheduling?mode=create"}`}
|
||||
className={`draft-stage ${data.draft_status > DRAFT_STATUS.SCHEDULING ? "complete" : ""}`}
|
||||
state={scheduleTemplate}
|
||||
>
|
||||
3
|
||||
</Link>
|
||||
<p className="absolute -left-8 md:!w-[unset] !w-[50px]">Templates & Scheduling</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraftProgress;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const FaqAccordion = ({ data }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="mb-[16px]">
|
||||
<div className="mb-[12px]">
|
||||
<button
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className="flex items-center"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="8"
|
||||
viewBox="0 0 14 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${open ? "rotate-180" : "rotate-90"} duration-200`}
|
||||
>
|
||||
<path
|
||||
d="M13 7L7 1L1 7"
|
||||
stroke="#475467"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-semibold ml-4 text-[16px]">{data.question}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={`ml-8 duration-500 overflow-hidden ${open ? `pointer-events-auto max-h-[300px]` : "max-h-0 pointer-events-none"}`}
|
||||
dangerouslySetInnerHTML={{ __html: data.answer }}
|
||||
></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqAccordion;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
const FaqTile = ({ data }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`mb-8 overflow-hidden`}>
|
||||
<div
|
||||
className={`mb-5 bg-[#F0F5F3] p-2 px-5 cursor-pointer rounded-xl overflow-hidden`}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1>{data.question}</h1>
|
||||
<button className="text-4xl">{!open ? <span>+</span> : <span>−</span>} </button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={open}
|
||||
enter="transition-all ease duration-500 overflow-hidden"
|
||||
enterFrom="max-h-0"
|
||||
enterTo="max-h-[400px]"
|
||||
leave="transition-all ease duration-500 overflow-hidden"
|
||||
leaveFrom="max-h-[400px]"
|
||||
leaveTo="max-h-0"
|
||||
>
|
||||
<p
|
||||
className={`sun-editor-editable pl-4 z-50`}
|
||||
dangerouslySetInnerHTML={{ __html: data.answer }}
|
||||
></p>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FaqTile;
|
||||
@@ -0,0 +1,207 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import HeartIcon from "@/components/frontend/icons/HeartIcon";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import React, { useContext, useState } from "react";
|
||||
import useDelayUnmount from "@/hooks/useDelayUnmount";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
function FavoriteButton({ space_id, user_property_spaces_id, reRender, withLoader, className, buttonClassName, stroke, favColor }) {
|
||||
const [unfavorite, setUnfavorite] = useState(false);
|
||||
const showUnfavorite = useDelayUnmount(unfavorite, 100);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { state: authState } = useContext(AuthContext);
|
||||
const sdk = new MkdSDK();
|
||||
async function favorite() {
|
||||
if (withLoader) {
|
||||
globalDispatch({ type: "START_LOADING" });
|
||||
}
|
||||
sdk.setTable("user_property_spaces");
|
||||
try {
|
||||
await sdk.callRestAPI({ property_spaces_id: space_id, user_id: authState.user }, "POST");
|
||||
if (reRender) {
|
||||
reRender(new Date());
|
||||
}
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
} catch (err) {
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function unFavorite() {
|
||||
if (withLoader) {
|
||||
globalDispatch({ type: "START_LOADING" });
|
||||
}
|
||||
sdk.setTable("user_property_spaces");
|
||||
try {
|
||||
await sdk.callRestAPI({ id: user_property_spaces_id }, "DELETE");
|
||||
if (reRender) {
|
||||
reRender(new Date());
|
||||
}
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
setUnfavorite(false);
|
||||
} catch (err) {
|
||||
globalDispatch({ type: "STOP_LOADING" });
|
||||
globalDispatch({
|
||||
type: "SHOW_ERROR",
|
||||
payload: {
|
||||
heading: "Operation Failed",
|
||||
message: err.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
if (user_property_spaces_id) {
|
||||
setUnfavorite(true);
|
||||
} else {
|
||||
favorite();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className ?? "flex flex-grow justify-end pt-2"}>
|
||||
<button
|
||||
className={buttonClassName ?? "pointer-auto flex h-[32px] w-[32px] items-center justify-center rounded-full bg-[#13131366] text-end"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleFavorite();
|
||||
}}
|
||||
id="favorite-button"
|
||||
>
|
||||
<HeartIcon
|
||||
isFav={user_property_spaces_id !== null && user_property_spaces_id !== 0}
|
||||
stroke={stroke}
|
||||
favColor={favColor}
|
||||
/>
|
||||
</button>
|
||||
{showUnfavorite && (
|
||||
<div
|
||||
className="popup-container flex items-center justify-center normal-case"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
{
|
||||
showUnfavorite && (
|
||||
<div
|
||||
className="popup-container flex items-center justify-center normal-case"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${unfavorite ? "pop-in" : "pop-out"} w-[400px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-[18px] flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">Are you sure?</h3>
|
||||
<button
|
||||
className="rounded-full border p-1 px-3 text-2xl font-normal duration-300 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p>Are you sure you want to remove this space from your favorites?</p>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 flex-grow rounded border-2 border-[#98A2B3] py-2 tracking-wide outline-none focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="login-btn-gradient mt-4 flex-grow rounded py-2 tracking-wide text-white outline-none focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
unFavorite();
|
||||
}}
|
||||
>
|
||||
Yes, remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${unfavorite ? "pop-in" : "pop-out"} w-[400px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-[18px] flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">Are you sure?</h3>
|
||||
<button
|
||||
className="rounded-full border p-1 px-3 text-2xl font-normal duration-300 hover:bg-gray-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p>Are you sure you want to remove this space from your favorites?</p>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 flex-grow rounded border-2 border-[#98A2B3] py-2 tracking-wide outline-none focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setUnfavorite(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="login-btn-gradient mt-4 flex-grow rounded py-2 tracking-wide text-white outline-none focus:outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
unFavorite();
|
||||
}}
|
||||
>
|
||||
Yes, remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <Tooltip
|
||||
anchorId="favorite-button"
|
||||
place="right"
|
||||
content={user_property_spaces_id ? "Remove from favorites" : "Add to favorites"}
|
||||
noArrow
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FavoriteButton;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import StarIcon from "./icons/StarIcon";
|
||||
|
||||
const FilterCheckBoxes = ({ name, options, searchField, query, optionFieldName, filterPopup }) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const uncheckAll = () => {
|
||||
searchParams.set(searchField, "");
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const updateSearchQuery = (e) => {
|
||||
e.preventDefault();
|
||||
var prev = searchParams.get(searchField);
|
||||
prev = prev?.split(",") || [];
|
||||
if (!prev.includes(e.target.name)) {
|
||||
prev.push(e.target.name);
|
||||
} else {
|
||||
prev.splice(prev.indexOf(e.target.name), 1);
|
||||
}
|
||||
searchParams.set(searchField, prev.join(","));
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (filterPopup) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [filterPopup]);
|
||||
|
||||
return (
|
||||
<div className="mb-[34px]">
|
||||
<div className="flex justify-between mb-[12px]">
|
||||
<h4 className="font-semibold text-[16px] lg:block flex justify-between w-full">
|
||||
<span className="lg:border-r lg:pr-2 lg:mr-2">{name}</span>
|
||||
<button
|
||||
className="lg:text-xs text-sm font-normal lowercase"
|
||||
onClick={uncheckAll}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</h4>
|
||||
<button
|
||||
className={`${open ? "" : "rotate-180"} duration-200 lg:inline hidden`}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="8"
|
||||
viewBox="0 0 14 8"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13 7L7 1L1 7"
|
||||
stroke="#475467"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className={`text-gray-500 text-[16px] duration-500 overflow-hidden ${open ? `pointer-events-auto max-h-[300px]` : "max-h-0 pointer-events-none"}`}>
|
||||
{options.map((op) => (
|
||||
<div
|
||||
className="checkbox-container mb-[12px]"
|
||||
key={op.id}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={op[optionFieldName] ?? op.name}
|
||||
onClick={updateSearchQuery}
|
||||
name={op[optionFieldName] ?? op.name}
|
||||
checked={Array.isArray(query[searchField]) ? query[searchField]?.includes(op[optionFieldName] ?? op.name) : false}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<label htmlFor={op[optionFieldName] ?? op.name}>{op[optionFieldName] ?? op.name} {name == "Reviews" ? Array(Number(op.name)).fill("").map(() => <span className="ml-1 mb-1"><StarIcon /></span>) : null}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterCheckBoxes;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useContext } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import LogoIcon from "./icons/LogoIcon";
|
||||
|
||||
const Footer = () => {
|
||||
const { state: authState, dispatch: authDispatch } = useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const blackList = useMemo(() => ["/admin", "/login", "/account/messages", "/signup"], []);
|
||||
|
||||
function switchToHost() {
|
||||
authDispatch({ type: "SWITCH_TO_HOST" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a host`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function switchToCustomer() {
|
||||
authDispatch({ type: "SWITCH_TO_CUSTOMER" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a customer`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function switchToHostOrCustomer() {
|
||||
if (authState.role == "host") {
|
||||
switchToCustomer();
|
||||
} else {
|
||||
switchToHost();
|
||||
}
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
if (blackList.some((path) => pathname.startsWith(path))) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<div className="header-light pb-10">
|
||||
<div className="container mx-auto px-4 py-[24px] text-white 2xl:px-16">
|
||||
<div className="mb-[17px] hidden justify-between md:flex">
|
||||
<Link to="/">
|
||||
<LogoIcon />
|
||||
</Link>
|
||||
<div className="flex gap-[24px]">
|
||||
{(authState.role == "host" || authState.role == "customer") && (
|
||||
<>
|
||||
{authState.originalRole != "customer" ? (
|
||||
<button
|
||||
className="duration-200 hover:underline"
|
||||
onClick={switchToHostOrCustomer}
|
||||
>
|
||||
Join as {authState.role == "host" ? "customer" : "host"}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
className="duration-200 hover:underline"
|
||||
to="/become-a-host"
|
||||
>
|
||||
Host Your Space
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-72 gap-[24px] pl-2">
|
||||
<Link
|
||||
className="duration-200 hover:underline"
|
||||
to="/faq"
|
||||
>
|
||||
FAQs
|
||||
</Link>
|
||||
<Link
|
||||
className="duration-200 hover:underline"
|
||||
to="/contact-us"
|
||||
>
|
||||
Contact us
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[17px] flex justify-between text-xs md:text-sm">
|
||||
<div className="flex gap-[24px]">
|
||||
<span>ergo © All rights reserved</span>
|
||||
</div>
|
||||
<div className="flex gap-[24px]">
|
||||
<Link
|
||||
className="duration-200 hover:underline"
|
||||
to="/help/terms_and_conditions"
|
||||
>
|
||||
Terms and conditions
|
||||
</Link>
|
||||
<Link
|
||||
className="duration-200 hover:underline"
|
||||
to="/help/privacy-policy"
|
||||
>
|
||||
Privacy and policy
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
import { IMAGE_STATUS } from "@/utils/constants";
|
||||
import React, { useEffect } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import StarIcon from "./icons/StarIcon";
|
||||
import { useState } from "react";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
|
||||
const HostCard = ({ data }) => {
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const [user, setUser] = useState()
|
||||
|
||||
const fetchUser = async () => {
|
||||
sdk.setTable("user")
|
||||
const users = await sdk.getAllUsers()
|
||||
let host_user = users?.find(user => user.id == data.id)
|
||||
setUser(host_user)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("token")) {
|
||||
fetchUser()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex items-start w-[400px] md:text-base text-sm remove-select">
|
||||
<img
|
||||
src={data.is_photo_approved == IMAGE_STATUS.APPROVED ? data.photo ?? "/default.png" : "/default.png"}
|
||||
alt={data.first_name}
|
||||
className="rounded-full cursor-pointer md:w-[80px] md:h-[80px] w-[60px] h-[60px] object-cover"
|
||||
/>
|
||||
<div className="px-[12px]">
|
||||
<h4 className="md:text-2xl text-lg font-semibold">
|
||||
{data.first_name || <Skeleton />} {data.last_name}
|
||||
</h4>
|
||||
<div className="flex items-center">
|
||||
<p className="text-gray-500 mb-[6px]">{data?.city && data?.city}</p>
|
||||
<p className="text-gray-500 mb-[6px]">{data?.country && ", " + data?.country}</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-end lowercase">
|
||||
<p className="flex gap-2 items-center">
|
||||
<StarIcon />
|
||||
<span>
|
||||
{(Number(data.avg_host_rating) || 0).toFixed(1)}
|
||||
{(typeof data?.rating_count === "number" && data?.rating_count > 0) &&
|
||||
<span>
|
||||
{" "}({data.rating_count})
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HostCard;
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useRef } from "react";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/css";
|
||||
|
||||
import { Mousewheel } from "swiper";
|
||||
import HostCard from "./HostCard";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function HostCardSlider({ hosts }) {
|
||||
const scrollTable = useRef(null);
|
||||
const navigate = useNavigate()
|
||||
|
||||
const moveTable = (ref) => {
|
||||
ref.scrollLeft += 160;
|
||||
};
|
||||
const moveTableBack = (ref) => {
|
||||
ref.scrollLeft += -160;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{hosts.length == 0 && (
|
||||
<p className="text-center flex items-center justify-center normal-case min-h-[200px] max-w-fit">
|
||||
<b>No Hosts found</b>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
<div
|
||||
ref={scrollTable}
|
||||
className="flex justify-between w-full overflow-auto sidebar-holdee">
|
||||
{hosts.length > 0 && hosts.map((host, idx) => (
|
||||
<div
|
||||
className=""
|
||||
key={idx}
|
||||
>
|
||||
<HostCard data={host} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* !["/"].includes(location.pathname) && */}
|
||||
{(hosts.length > 3 && window.innerWidth > 800) &&
|
||||
<div className="flex items-center pb-2 gap-3 justify-center mx-auto w-full pt-6">
|
||||
<div className="cursor-pointer"
|
||||
onClick={() =>
|
||||
moveTableBack(scrollTable.current)
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/${backTo}`)}
|
||||
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold"
|
||||
>
|
||||
<ArrowLeftIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="cursor-pointer" onClick={() => moveTable(scrollTable.current)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(`/admin/${backTo}`)}
|
||||
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold"
|
||||
>
|
||||
<ArrowRightIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
|
||||
export default function LoadingButton({ loading, loadingEl, children, ...restProps }) {
|
||||
return (
|
||||
<button
|
||||
{...restProps}
|
||||
style={{ pointerEvents: loading ? "none" : undefined, cursor: loading ? "not-allowed" : undefined }}
|
||||
>
|
||||
{loading ? (
|
||||
loadingEl ?? (
|
||||
<div className="flex justify-center">
|
||||
<svg
|
||||
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
|
||||
width="36px"
|
||||
height="36px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#d0d5dd"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
strokeLinecap="round"
|
||||
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
repeatCount="indefinite"
|
||||
dur="1.6666666666666667s"
|
||||
keyTimes="0;1"
|
||||
values="0;256.58892822265625"
|
||||
></animate>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<span>{children}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { AuthContext } from "@/authContext";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import LoadingButton from "./LoadingButton";
|
||||
|
||||
export default function LogoutModal({ modalOpen, closeModal }) {
|
||||
const { dispatch: authDispatch } = useContext(AuthContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function logout() {
|
||||
setLoading(true);
|
||||
const sdk = new MkdSDK();
|
||||
try {
|
||||
await sdk.logout();
|
||||
authDispatch({ type: "LOGOUT" });
|
||||
navigate("/");
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
// still logout if the token is already expired
|
||||
if (err.message == "TOKEN_EXPIRED") {
|
||||
authDispatch({ type: "LOGOUT" });
|
||||
navigate("/");
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Are you sure
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Are you sure you want to sign out?</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
type="button"
|
||||
className={`inline-flex justify-center rounded-md ${loading ? "py-1 px-6" : "py-2 px-4"} login-btn-gradient text-sm font-medium text-white`}
|
||||
onClick={logout}
|
||||
>
|
||||
Proceed
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from "react";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/css";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Mousewheel } from "swiper";
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "@/authContext";
|
||||
|
||||
export default function NavBarSlider() {
|
||||
const [swiper, setSwiper] = useState(null);
|
||||
const { state } = useContext(AuthContext);
|
||||
const role = state.role;
|
||||
|
||||
const customerNavItems = [
|
||||
{ name: "My Bookings", route: "/account/my-bookings" },
|
||||
{ name: "Messages", route: "/account/messages" },
|
||||
{ name: "Reviews", route: "/account/reviews" },
|
||||
{ name: "Profile", route: "/account/profile" },
|
||||
{ name: "Payment", route: "/account/payments" },
|
||||
{ name: "Billing", route: "/account/billing" },
|
||||
];
|
||||
const hostNavItems = [
|
||||
{ name: "My Bookings", route: "/account/my-bookings" },
|
||||
{ name: "Messages", route: "/account/messages" },
|
||||
{ name: "Reviews", route: "/account/reviews" },
|
||||
{ name: "My Spaces", route: "/account/my-spaces" },
|
||||
{ name: "My Addons", route: "/account/my-addons" },
|
||||
{ name: "My Amenities", route: "/account/my-amenities" },
|
||||
{ name: "Profile", route: "/account/profile" },
|
||||
{ name: "Payment", route: "/account/payments" },
|
||||
{ name: "Billing", route: "/account/billing" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<Swiper
|
||||
slidesPerView={"auto"}
|
||||
centeredSlides={true}
|
||||
spaceBetween={0}
|
||||
mousewheel={true}
|
||||
className="navbar-slider"
|
||||
initialSlide={1}
|
||||
centeredSlidesBounds={true}
|
||||
modules={[Mousewheel]}
|
||||
onSwiper={setSwiper}
|
||||
breakpoints={{
|
||||
640: { enabled: false },
|
||||
}}
|
||||
>
|
||||
{(role == "host" ? hostNavItems : customerNavItems).map((items, i) => (
|
||||
<SwiperSlide
|
||||
className="!w-[120px] slider-menu text-center pb-3"
|
||||
key={items.route}
|
||||
>
|
||||
<NavLink
|
||||
className={`${items.name === "Reviews" && "thirteenth-step"} ${items.name === "Payment" && "twelfth-step"} ${items.name === "My Bookings" && "seventeen-step"}`}
|
||||
to={items.route}
|
||||
onClick={() => swiper.slideTo(i)}
|
||||
>
|
||||
{items.name}
|
||||
</NavLink>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
<div className="mover"></div>
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { AuthContext, tokenExpireError } from "@/authContext";
|
||||
import { GlobalContext } from "@/globalContext";
|
||||
import { ID_VERIFICATION_STATUSES, IMAGE_STATUS } from "@/utils/constants";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useContext } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Icon from "../Icons";
|
||||
import LogoutModal from "./LogoutModal";
|
||||
import MkdSDK from "@/utils/MkdSDK";
|
||||
import { ChatBubbleBottomCenterIcon } from "@heroicons/react/24/outline";
|
||||
import { useTour } from "@reactour/tour";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
export default function NavMenu({ variant }) {
|
||||
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
|
||||
const { state: authState, dispatch: authDispatch } = useContext(AuthContext);
|
||||
const [unreadCount, setUnreadCount] = useState(globalState.unreadMessages);
|
||||
const [height, setHeight] = useState(window.innerHeight);
|
||||
const navigate = useNavigate();
|
||||
const menuRef = useRef(null);
|
||||
|
||||
function switchToHost() {
|
||||
authDispatch({ type: "SWITCH_TO_HOST" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a host`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
function switchToAdmin() {
|
||||
authDispatch({ type: "SWITCH_TO_ADMIN" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as an admin`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/admin/dashboard");
|
||||
}
|
||||
|
||||
function switchToCustomer() {
|
||||
authDispatch({ type: "SWITCH_TO_CUSTOMER" });
|
||||
globalDispatch({
|
||||
type: "SHOW_CONFIRMATION",
|
||||
payload: {
|
||||
heading: "Success",
|
||||
message: `You are now signed in as a customer`,
|
||||
btn: "Ok got it",
|
||||
},
|
||||
});
|
||||
navigate("/");
|
||||
}
|
||||
async function fetchUnreadMessagesCount() {
|
||||
try {
|
||||
const result = await sdk.getMyRoom();
|
||||
if (Array.isArray(result.messages)) {
|
||||
globalDispatch({
|
||||
type: "SET_UNREAD_MESSAGES_COUNT",
|
||||
payload: result.messages.filter((msg) => {
|
||||
const messageSenderId = JSON.parse(msg.chat).user_id;
|
||||
return Number(messageSenderId) != Number(authState.user);
|
||||
}).length,
|
||||
});
|
||||
}
|
||||
|
||||
setUnreadCount(result.messages.filter((msg) => {
|
||||
const messageSenderId = JSON.parse(msg.chat).user_id;
|
||||
return Number(messageSenderId) != Number(authState.user);
|
||||
}).length)
|
||||
} catch (err) {
|
||||
tokenExpireError(authDispatch, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUnreadMessagesCount();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setHeight(window.innerHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [logoutModal, setLogoutModal] = useState(false);
|
||||
|
||||
function getVerifiedColor(status) {
|
||||
switch (status) {
|
||||
case ID_VERIFICATION_STATUSES.PENDING:
|
||||
return "";
|
||||
case ID_VERIFICATION_STATUSES.VERIFIED:
|
||||
return "text-green-600";
|
||||
case ID_VERIFICATION_STATUSES.REJECTED:
|
||||
return "text-red-600";
|
||||
default:
|
||||
return "text-red-600";
|
||||
}
|
||||
}
|
||||
|
||||
const { setIsOpen } = useTour()
|
||||
|
||||
const verificationStatuses = ["Pending Verification", "Verified", "Not Verified"];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="z-10 text-black">
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative inline-block text-left"
|
||||
ref={menuRef}
|
||||
>
|
||||
<div>
|
||||
<Menu.Button
|
||||
className="eighth-step pointer-events-auto relative h-[36px] w-[36px] overflow-hidden rounded-full"
|
||||
id="menu-btn"
|
||||
>
|
||||
<Icon
|
||||
type="user"
|
||||
fill=""
|
||||
variant="circle"
|
||||
className={"my-stroke-" + variant}
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={globalState.menuIconOpen || undefined}
|
||||
className="overflow-y-auto"
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`absolute hidden-scrollbar ${(height < 720 && height > 560) && "max-h-[500px]"} ${(height < 560) && "max-h-[400px]"} overflow-y-auto right-0 mt-2 w-80 max-w-screen-sm origin-top-right rounded-3xl border bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}>
|
||||
<div className="flex flex-col items-center border-b p-4">
|
||||
<img
|
||||
src={globalState.user.is_photo_approved == IMAGE_STATUS.APPROVED ? globalState.user.photo ?? "/default.png" : "/default.png"}
|
||||
className="mb-3 h-[36px] w-[36px] rounded-full object-cover"
|
||||
/>
|
||||
<h3 className="mb-1 font-semibold">
|
||||
{globalState.user.first_name} {globalState.user.last_name}
|
||||
</h3>
|
||||
<p className="font-thin">You are signed in as {authState.role}</p>
|
||||
<span className={getVerifiedColor(globalState.user.verificationStatus)}>{verificationStatuses[globalState.user.verificationStatus] ?? "Not verified"}</span>
|
||||
</div>
|
||||
<Menu.Item>
|
||||
<>
|
||||
<div className={`block border-b px-4 py-2 md:hidden`}>
|
||||
<button
|
||||
onClick={() => navigate("/account/profile")}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Account & profile
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 py-2 md:hidden block">
|
||||
<Link
|
||||
to={"/account/messages"}
|
||||
className={`relative -mx-3 flex w-full justify-between items-center rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Messages{" "}
|
||||
{globalState.unreadMessages > 0 && (
|
||||
<strong className={`login-btn-gradient flex h-[23px] w-[23px] items-center justify-center rounded-full border p-2 text-xs text-white`}>{globalState.unreadMessages}</strong>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
</Menu.Item>
|
||||
<div className={`hidden border-b px-4 py-2 md:block`}>
|
||||
<Menu.Item>
|
||||
<>
|
||||
<Link
|
||||
to={"/account/my-bookings"}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
My bookings
|
||||
</Link>
|
||||
<Link
|
||||
to={"/account/messages"}
|
||||
className={`relative -mx-3 flex w-full justify-between items-center rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Messages{" "}
|
||||
{globalState.unreadMessages > 0 && (
|
||||
<strong className={`login-btn-gradient flex h-[23px] w-[23px] items-center justify-center rounded-full border p-2 text-xs text-white`}>{globalState.unreadMessages}</strong>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to={"/account/reviews"}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Reviews
|
||||
</Link>
|
||||
{authState.role == "host" && (
|
||||
<Link
|
||||
to={"/account/my-spaces"}
|
||||
data-tour='first-step-2'
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
My Spaces
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to={"/account/profile"}
|
||||
data-tour="first-step"
|
||||
// data-step={2}
|
||||
className="first-step -mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200"
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to={"/account/payments"}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Payment
|
||||
</Link>
|
||||
<Link
|
||||
to={"/account/billing"}
|
||||
className={`ninth-step -mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Billing
|
||||
</Link>
|
||||
</>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className={`border-t px-4 pt-2 pb-2`}>
|
||||
<Menu.Item>
|
||||
<button
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
onClick={() => { globalDispatch({ type: "START_TOUR" }); setIsOpen(true) }}
|
||||
>
|
||||
Help me get started
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link
|
||||
to="/faq"
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
FAQs
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link
|
||||
to="/favorites"
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Favorites
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{authState.role == "customer" ? (
|
||||
<>
|
||||
{authState.originalRole != "customer" ? (
|
||||
<button
|
||||
onClick={switchToHost}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Sign in as host
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={"/become-a-host"}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Become a host
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={switchToCustomer}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Sign in as customer
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
{["superadmin", "admin"].includes(authState.originalRole) && (
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={switchToAdmin}
|
||||
className={`-mx-3 flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
>
|
||||
Sign in as admin
|
||||
</button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className={`flex w-full justify-start rounded-pill p-2 px-3 duration-200 hover:bg-gray-200`}
|
||||
onClick={() => setLogoutModal(true)}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
<LogoutModal
|
||||
modalOpen={logoutModal}
|
||||
closeModal={() => setLogoutModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import Swiper from "swiper";
|
||||
import { SwiperSlide, Swiper as SwiperComponent } from "swiper/react";
|
||||
import { Navigation, Pagination, A11y } from "swiper";
|
||||
|
||||
export default function PropertyEditImageSlider({ modalOpen, closeModal, spaceImages }) {
|
||||
const [currentImageSlide, setCurrentImageSlide] = useState(0);
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
show={modalOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50"
|
||||
onClose={closeModal}
|
||||
>
|
||||
<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
|
||||
as="div"
|
||||
className="bg-white p-5 rounded-lg md:w-4/5 w-5/6 transform overflow-hidden shadow-xl transition-all"
|
||||
>
|
||||
<div className="flex justify-between md:mb-[24px] mb-4">
|
||||
<div></div>
|
||||
<p className="self-center normal-case">
|
||||
Images {currentImageSlide + 1} of {spaceImages.length}
|
||||
</p>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="p-1 border hover:bg-gray-200 active:bg-gray-300 duration-100 px-3 text-2xl font-normal rounded-full"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="">
|
||||
<SwiperComponent
|
||||
// install Swiper modules
|
||||
modules={[Navigation, Pagination, A11y]}
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
navigation
|
||||
pagination={{
|
||||
clickable: true,
|
||||
renderBullet: (i, className) => `<img src="${spaceImages[i].photo_url || "/default-property.jpg"}" draggable="false" class="pagination-image ${className}" />`,
|
||||
}}
|
||||
className="property-swiper-slid"
|
||||
>
|
||||
{spaceImages.map((img, i) => (
|
||||
<SwiperSlide
|
||||
key={i}
|
||||
className="md:pb-[120px]"
|
||||
>
|
||||
{({ isActive }) => {
|
||||
if (isActive) setCurrentImageSlide(i);
|
||||
return (
|
||||
<img
|
||||
src={img.photo_url || "/default-property.jpg"}
|
||||
draggable={"false"}
|
||||
className="w-full property-swiper-image md:h-[600px] h-[300px"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</SwiperComponent>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user