initial commit

This commit is contained in:
undefined
2025-01-24 20:05:48 +01:00
commit db55c10f43
484 changed files with 118165 additions and 0 deletions
+18
View File
@@ -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;
+207
View File
@@ -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"
>
&#x2715;
</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>
);
}
+10
View File
@@ -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
+18
View File
@@ -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>
);
}
+18
View File
@@ -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>
);
}
+37
View File
@@ -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>
);
}
+30
View File
@@ -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>
);
}
+63
View File
@@ -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>
);
}
+75
View File
@@ -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();
}
}}
>
&#x2715;
</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>
);
}
+59
View File
@@ -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();
}
}}
>
&#x2715;
</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>
</>
);
}
+180
View File
@@ -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;
+82
View File
@@ -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);
}}
>
&#x2715;
</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>
);
}
View File
+74
View File
@@ -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>
);
}
+83
View File
@@ -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;
+116
View File
@@ -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>
);
}
+192
View File
@@ -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">&#36;{data?.rate?.toFixed(2)} </p>
<p className="text-xs mb-1 font-medium ">Tax</p>
<p className="mb-1 text-xs">&#36;{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">&#36;{data?.total?.toFixed(2)} </p>
<p className="text-xs mb-1 font-medium ">Commission</p>
<p className="mb-1 text-xs">&#36;{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;
+140
View File
@@ -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>
);
}
+120
View File
@@ -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>
);
}
+131
View File
@@ -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>
</>
);
}
+149
View File
@@ -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;
+24
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+13
View File
@@ -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
+38
View File
@@ -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
+16
View File
@@ -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
+23
View File
@@ -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
+21
View File
@@ -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;
+16
View File
@@ -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
+16
View File
@@ -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>
);
}
+13
View File
@@ -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
+13
View File
@@ -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
+16
View File
@@ -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
+13
View File
@@ -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
+13
View File
@@ -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
+13
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+15
View File
@@ -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
+13
View File
@@ -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;
+16
View File
@@ -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
+23
View File
@@ -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
+16
View File
@@ -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
+75
View File
@@ -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
+40
View File
@@ -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>
);
}
+205
View File
@@ -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}
</>
);
}
+85
View File
@@ -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>
);
}
+50
View File
@@ -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;
+40
View File
@@ -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;
+181
View File
@@ -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">&#36;{data?.total?.toFixed(2)} </p>
<p className="text-xs mb-1 font-medium ">Tax</p>
<p className="mb-1 text-sm">&#36;{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">&#36;{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>
);
}
+142
View File
@@ -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"
>
&#x2715;
</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"
>
&#x2715;
</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"
>
&#x2715;
</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>
</>
);
}
+166
View File
@@ -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"
>
&#x2715;
</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>
</>
);
}
+82
View File
@@ -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 youll 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"
>
&#x2715;
</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>
);
}
+104
View File
@@ -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;
+258
View File
@@ -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}
</>
);
}
+99
View File
@@ -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>
);
}
+89
View File
@@ -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;
+73
View File
@@ -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>
</>
);
}
+44
View File
@@ -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();
}
}}
>
&#x2715;
</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>
);
}
+22
View File
@@ -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>
);
}
+452
View File
@@ -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">&#36;{(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;
+52
View File
@@ -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;
+51
View File
@@ -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"
>
&#x2715;
</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;
+79
View File
@@ -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>
);
}
+51
View File
@@ -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();
}
}}
>
&#x2715;
</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>
);
}
+121
View File
@@ -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">&#10003;</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">&#10003;</span> : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
}
+97
View File
@@ -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();
}
}}
>
&#x2715;
</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;
+76
View File
@@ -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>
);
}
+211
View File
@@ -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"
>
&#x2715;
</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;
+46
View File
@@ -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;
+40
View File
@@ -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;
+37
View File
@@ -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>&#43;</span> : <span>&#8722;</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;
+207
View File
@@ -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);
}}
>
&#x2715;
</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);
}}
>
&#x2715;
</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;
+122
View File
@@ -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;
+60
View File
@@ -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>
}
</>
);
}
+44
View File
@@ -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>
);
}
+102
View File
@@ -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>
);
}
+68
View File
@@ -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>
);
}
+331
View File
@@ -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"
>
&#x2715;
</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