Fix: issue 1b upcoming and ingoing bookings were not disabled on time selection

This commit is contained in:
Ayobami
2025-07-06 13:11:55 +01:00
parent 4de59de7e5
commit fed26eadc8
5 changed files with 752 additions and 411 deletions
+5 -3
View File
@@ -36,9 +36,11 @@ const DateTimePicker = ({
// Only check slots for the selected date // Only check slots for the selected date
const selectedDateStr = moment(selectedDate).format("YYYY-MM-DD"); const selectedDateStr = moment(selectedDate).format("YYYY-MM-DD");
return bookedSlots.some((booking) => { return bookedSlots.some((booking) => {
// booking should have start and end in ISO or parseable format // booking should have booking_start_time and booking_end_time in ISO or parseable format
const bookingStart = moment(booking.start); const bookingStart = moment(
const bookingEnd = moment(booking.end); booking.booking_start_time || booking.start_time
);
const bookingEnd = moment(booking.booking_end_time || booking.end_time);
// Only check if booking is on the same day // Only check if booking is on the same day
if (bookingStart.format("YYYY-MM-DD") !== selectedDateStr) return false; if (bookingStart.format("YYYY-MM-DD") !== selectedDateStr) return false;
// If slotTime is within booking range // If slotTime is within booking range
@@ -6,6 +6,7 @@ import SearchIcon from "./icons/SearchIcon";
import ReactTestUtils from "react-dom/test-utils"; import ReactTestUtils from "react-dom/test-utils";
import { isNotInViewport, sleep } from "@/utils/utils"; import { isNotInViewport, sleep } from "@/utils/utils";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import CustomLocationAutoCompleteV2 from "../CustomLocationAutoCompleteV2";
import CustomComboBox from "../CustomComboBox"; import CustomComboBox from "../CustomComboBox";
import { GlobalContext } from "@/globalContext"; import { GlobalContext } from "@/globalContext";
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
+241 -156
View File
@@ -1,6 +1,12 @@
import React, { useContext, useEffect } from "react"; import React, { useContext, useEffect } from "react";
import { useState } from "react"; import { useState } from "react";
import { Link, Navigate, useLocation, useNavigate, useParams } from "react-router-dom"; import {
Link,
Navigate,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import FaqAccordion from "@/components/frontend/FaqAccordion"; import FaqAccordion from "@/components/frontend/FaqAccordion";
import ReviewCard from "@/components/frontend/ReviewCard"; import ReviewCard from "@/components/frontend/ReviewCard";
@@ -15,7 +21,15 @@ import { GlobalContext } from "@/globalContext";
import FavoriteButton from "@/components/frontend/FavoriteButton"; import FavoriteButton from "@/components/frontend/FavoriteButton";
import Counter from "@/components/frontend/Counter"; import Counter from "@/components/frontend/Counter";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { usePropertyAddons, usePropertySpace, usePropertySpaceImages, usePublicUserData, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceReviews } from "@/hooks/api"; import {
usePropertyAddons,
usePropertySpace,
usePropertySpaceImages,
usePublicUserData,
usePropertySpaceAmenities,
usePropertySpaceFaqs,
usePropertySpaceReviews,
} from "@/hooks/api";
import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; import PropertyImageSlider from "@/components/frontend/PropertyImageSlider";
import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage";
import AllReviewsModal from "@/components/frontend/AllReviewsModal"; import AllReviewsModal from "@/components/frontend/AllReviewsModal";
@@ -25,7 +39,8 @@ import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon";
let sdk = new MkdSDK(); let sdk = new MkdSDK();
const PropertyPage = () => { const PropertyPage = () => {
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); const { dispatch: globalDispatch, state: globalState } =
useContext(GlobalContext);
const { state: authState, dispatch: authDispatch } = useContext(AuthContext); const { state: authState, dispatch: authDispatch } = useContext(AuthContext);
const { state: spaceData } = useLocation(); const { state: spaceData } = useLocation();
const { bookingData, dispatch } = useBookingContext(); const { bookingData, dispatch } = useBookingContext();
@@ -34,7 +49,10 @@ const PropertyPage = () => {
const [fetching, setFetching] = useState(true); const [fetching, setFetching] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams(); const { id } = useParams();
const bookingDetails = bookingData?.from === "" ? bookingData : JSON.parse(localStorage.getItem("booking_details")); const bookingDetails =
bookingData?.from === ""
? bookingData
: JSON.parse(localStorage.getItem("booking_details"));
const [reviewDirection, setReviewDirection] = useState("DESC"); const [reviewDirection, setReviewDirection] = useState("DESC");
const [bookedSlots, setBookedSlots] = useState([]); const [bookedSlots, setBookedSlots] = useState([]);
const [scheduleTemplate, setScheduleTemplate] = useState({}); const [scheduleTemplate, setScheduleTemplate] = useState({});
@@ -47,7 +65,11 @@ const PropertyPage = () => {
const { propertySpace, notFound } = usePropertySpace(id, render); const { propertySpace, notFound } = usePropertySpace(id, render);
const hostData = usePublicUserData(propertySpace.host_id); const hostData = usePublicUserData(propertySpace.host_id);
const spaceImages = usePropertySpaceImages(propertySpace.id, true, setFetching); const spaceImages = usePropertySpaceImages(
propertySpace.id,
true,
setFetching
);
const spaceAddons = usePropertyAddons(propertySpace.property_id); const spaceAddons = usePropertyAddons(propertySpace.property_id);
const spaceAmenities = usePropertySpaceAmenities(propertySpace.id); const spaceAmenities = usePropertySpaceAmenities(propertySpace.id);
const faqs = usePropertySpaceFaqs(propertySpace.id); const faqs = usePropertySpaceFaqs(propertySpace.id);
@@ -56,12 +78,19 @@ const PropertyPage = () => {
const { pathname } = useLocation(); const { pathname } = useLocation();
if (!fetching && spaceImages.length === 0) { if (!fetching && spaceImages.length === 0) {
navigate("*") navigate("*");
} }
async function fetchBookedSlots(id) { async function fetchBookedSlots(id) {
try { try {
const result = await callCustomAPI("customer/schedule", "post", { property_spaces_id: id }, "", null, "v3"); const result = await callCustomAPI(
"customer/schedule",
"post",
{ property_spaces_id: id },
"",
null,
"v3"
);
if (Array.isArray(result.list)) { if (Array.isArray(result.list)) {
setBookedSlots(result.list); setBookedSlots(result.list);
} }
@@ -86,11 +115,10 @@ const PropertyPage = () => {
limit: 1, limit: 1,
where: [`property_spaces_id = ${id}`], where: [`property_spaces_id = ${id}`],
}, },
"PAGINATE", "PAGINATE"
); );
if (Array.isArray(result.list) && result.list.length > 0) { if (Array.isArray(result.list) && result.list.length > 0) {
setScheduleTemplate({ custom_slots: result.list[0].custom_slots }); setScheduleTemplate({ custom_slots: result.list[0].custom_slots });
} }
if (result.list[0]?.schedule_template_id) { if (result.list[0]?.schedule_template_id) {
const templateResult = await callCustomAPI( const templateResult = await callCustomAPI(
@@ -101,9 +129,12 @@ const PropertyPage = () => {
limit: 1, limit: 1,
where: [`id = ${result.list[0].schedule_template_id}`], where: [`id = ${result.list[0].schedule_template_id}`],
}, },
"PAGINATE", "PAGINATE"
); );
if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { if (
Array.isArray(templateResult.list) &&
(templateResult.list[0] ?? {})
) {
setScheduleTemplate((prev) => { setScheduleTemplate((prev) => {
let updated = { ...prev, ...templateResult.list[0] }; let updated = { ...prev, ...templateResult.list[0] };
return updated; return updated;
@@ -140,15 +171,24 @@ const PropertyPage = () => {
} }
if (authState.user == propertySpace.host_id) { if (authState.user == propertySpace.host_id) {
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "error", message: "Owners can't book their own spaces" } }) globalDispatch({
return type: "SHOW_ERROR",
payload: {
heading: "error",
message: "Owners can't book their own spaces",
},
});
return;
} }
if (globalState.user.verificationStatus != 1) { if (globalState.user.verificationStatus != 1) {
globalDispatch({ type: "OPEN_NOT_VERIFIED_MODAL" }); globalDispatch({ type: "OPEN_NOT_VERIFIED_MODAL" });
return; return;
} }
dispatch({ type: "SET_BOOKING_DETAILS", payload: { ...data, ...propertySpace } }); dispatch({
type: "SET_BOOKING_DETAILS",
payload: { ...data, ...propertySpace },
});
navigate("booking-preview"); navigate("booking-preview");
}; };
@@ -169,99 +209,119 @@ const PropertyPage = () => {
return new Date(a.post_date) - new Date(b.post_date); return new Date(a.post_date) - new Date(b.post_date);
}; };
if (notFound || isNaN(id)) return <Navigate to="/not-found" />; if (notFound || isNaN(id)) return <Navigate to='/not-found' />;
return ( return (
<div <div
className="container mx-auto min-h-screen pt-[140px] text-sm normal-case md:text-base 2xl:px-16" className='container mx-auto min-h-screen pt-[140px] text-sm normal-case md:text-base 2xl:px-16'
onClick={() => { onClick={() => {
setShowCalendar(false); setShowCalendar(false);
}} }}
> >
<div className="mb-[18px] flex flex-col items-start justify-between px-[17px] md:flex-row md:items-center md:px-0"> <div className='mb-[18px] flex flex-col items-start justify-between px-[17px] md:flex-row md:items-center md:px-0'>
<div className="flex flex-col items-start gap-4 normal-case md:flex-row md:items-center"> <div className='flex flex-col items-start gap-4 normal-case md:flex-row md:items-center'>
<h2 className="text-3xl font-semibold">{propertySpace.name ?? spaceData?.name}</h2> <h2 className='text-3xl font-semibold'>
{propertySpace.name ?? spaceData?.name}
</h2>
<button <button
className="whitespace-nowrap text-sm underline" className='whitespace-nowrap text-sm underline'
target="_blank" target='_blank'
onClick={() => setShowMap(true)} onClick={() => setShowMap(true)}
> >
(view on map) (view on map)
</button> </button>
</div> </div>
<div className="mt-[19px] flex w-full justify-center gap-4 md:mt-0 md:w-[unset]"> <div className='mt-[19px] flex w-full justify-center gap-4 md:mt-0 md:w-[unset]'>
<p className="flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]"> <p className='flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]'>
<StarIcon /> <StarIcon />
<strong className="font-semibold"> <strong className='font-semibold'>
{(Number(propertySpace.average_space_rating ?? spaceData?.average_space_rating) || 0).toFixed(1)} {(
<span className="font-normal">({propertySpace.space_rating_count ?? spaceData?.space_rating_count})</span> Number(
propertySpace.average_space_rating ??
spaceData?.average_space_rating
) || 0
).toFixed(1)}
<span className='font-normal'>
(
{propertySpace.space_rating_count ??
spaceData?.space_rating_count}
)
</span>
</strong> </strong>
</p> </p>
<div className="flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]"> <div className='flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]'>
<FavoriteButton <FavoriteButton
space_id={propertySpace.id} space_id={propertySpace.id}
user_property_spaces_id={propertySpace.user_property_spaces_id} user_property_spaces_id={propertySpace.user_property_spaces_id}
reRender={forceRender} reRender={forceRender}
withLoader={true} withLoader={true}
className="-mb-1" className='-mb-1'
buttonClassName="" buttonClassName=''
stroke="#344054" stroke='#344054'
favColor={"black"} favColor={"black"}
/> />
<span>Save</span> <span>Save</span>
</div> </div>
</div> </div>
</div> </div>
<div className="snap-scroll relative mb-[66px] flex h-[381px] gap-[32px] px-[14px] md:px-0"> <div className='snap-scroll relative mb-[66px] flex h-[381px] gap-[32px] px-[14px] md:px-0'>
{spaceImages[0]?.photo_url && {spaceImages[0]?.photo_url && (
<img <img
src={spaceImages[0]?.photo_url} src={spaceImages[0]?.photo_url}
className="h-full rounded-lg object-cover xl:min-w-[616px]" className='h-full rounded-lg object-cover xl:min-w-[616px]'
/> />
} )}
{spaceImages[1]?.photo_url && {spaceImages[1]?.photo_url && (
<img <img
src={spaceImages[1]?.photo_url} src={spaceImages[1]?.photo_url}
className="h-full w-[292px] rounded-lg object-cover" className='h-full w-[292px] rounded-lg object-cover'
/> />
} )}
<div className={`${((!spaceImages[3]?.photo_url)) ? "flex min-w-[550px] flex-col" : "block"} "gap-4 overflow-hidden md:gap-[32px]"`}> <div
{spaceImages[2]?.photo_url && className={`${
!spaceImages[3]?.photo_url ? "flex min-w-[550px] flex-col" : "block"
} "gap-4 md:gap-[32px]" overflow-hidden`}
>
{spaceImages[2]?.photo_url && (
<img <img
src={spaceImages[2]?.photo_url} src={spaceImages[2]?.photo_url}
className={`${spaceImages[3]?.photo_url && "h-1/2"} "rounded-lg object-cover md:w-full"`} className={`${
spaceImages[3]?.photo_url && "h-1/2"
} "rounded-lg md:w-full" object-cover`}
/> />
} )}
{spaceImages[3]?.photo_url && {spaceImages[3]?.photo_url && (
<img <img
src={spaceImages[3]?.photo_url} src={spaceImages[3]?.photo_url}
className="h-1/2 rounded-lg object-cover md:w-full" className='h-1/2 rounded-lg object-cover md:w-full'
/> />
} )}
</div> </div>
{spaceImages[4]?.photo_url && {spaceImages[4]?.photo_url && (
<img <img
src={spaceImages[4]?.photo_url} src={spaceImages[4]?.photo_url}
className="h-full w-[292px] rounded-lg object-cover" className='h-full w-[292px] rounded-lg object-cover'
/> />
} )}
<button <button
className="sticky right-6 mb-[8px] min-w-[170px] self-end border bg-[#00000080] px-3 py-1 text-center text-sm text-white" className='sticky right-6 mb-[8px] min-w-[170px] self-end border bg-[#00000080] px-3 py-1 text-center text-sm text-white'
onClick={() => setGalleryOpen(true)} onClick={() => setGalleryOpen(true)}
> >
View all photos ({spaceImages.length}) View all photos ({spaceImages.length})
</button> </button>
</div> </div>
<section className="relative flex flex-col items-start xl:flex-row xl:gap-12 lg:w-[90%] w-full mx-auto"> <section className='relative mx-auto flex w-full flex-col items-start lg:w-[90%] xl:flex-row xl:gap-12'>
<div className="w-full md:px-0 xl:w-3/5 px-10"> <div className='w-full px-10 md:px-0 xl:w-3/5'>
<h3 className="mb-[8px] text-2xl font-semibold">Description</h3> <h3 className='mb-[8px] text-2xl font-semibold'>Description</h3>
<p className="">{propertySpace.description ?? spaceData?.description}</p> <p className=''>
<hr className="my-[32px] md:my-[47px]" /> {propertySpace.description ?? spaceData?.description}
<h3 className="mb-[8px] text-2xl font-semibold">Amenities</h3> </p>
<ul className="addons-grid list-disk-important"> <hr className='my-[32px] md:my-[47px]' />
<h3 className='mb-[8px] text-2xl font-semibold'>Amenities</h3>
<ul className='addons-grid list-disk-important'>
{spaceAmenities.map((am, idx) => ( {spaceAmenities.map((am, idx) => (
<li <li
className="flex w-fit items-center gap-2 mb-4 sm:mb-0" className='mb-4 flex w-fit items-center gap-2 sm:mb-0'
key={idx} key={idx}
> >
<CircleCheckIcon /> <CircleCheckIcon />
@@ -269,72 +329,80 @@ const PropertyPage = () => {
</li> </li>
))} ))}
</ul> </ul>
<hr className="my-[32px] md:my-[47px]" /> <hr className='my-[32px] md:my-[47px]' />
<h3 className="mb-[8px] text-2xl font-semibold">Add ons</h3> <h3 className='mb-[8px] text-2xl font-semibold'>Add ons</h3>
<ul className="addons-grid list-disk-important"> <ul className='addons-grid list-disk-important'>
{spaceAddons.map((addon) => ( {spaceAddons.map((addon) => (
<li <li
className="flex w-fit sm:w-full items-center gap-2 mb-4 sm:mb-0" className='mb-4 flex w-fit items-center gap-2 sm:mb-0 sm:w-full'
key={addon.id} key={addon.id}
> >
<span className="w-fit"> <span className='w-fit'>
{" "} {" "}
<div className="flex gap-4"> <div className='flex gap-4'>
<CircleCheckIcon /> {addon.add_on_name} <CircleCheckIcon /> {addon.add_on_name}
</div>{" "} </div>{" "}
</span>{" "} </span>{" "}
<strong className="font-semibold">${addon.cost}/h</strong> <strong className='font-semibold'>${addon.cost}/h</strong>
</li> </li>
))} ))}
</ul> </ul>
<hr className="my-[32px] md:my-[47px]" /> <hr className='my-[32px] md:my-[47px]' />
<div className="mb-[28px] flex flex-wrap items-center justify-between"> <div className='mb-[28px] flex flex-wrap items-center justify-between'>
<h3 className="mb-2 text-xl font-semibold md:mb-0 md:text-2xl">About the host</h3> <h3 className='mb-2 text-xl font-semibold md:mb-0 md:text-2xl'>
{(authState.role == "customer" && propertySpace?.id) && ( About the host
</h3>
{authState.role == "customer" && propertySpace?.id && (
<Link <Link
to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`} to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`}
className="my-text-gradient hidden w-[178px] whitespace-nowrap rounded-md border border-[#33D4B7] px-6 py-2 text-center font-semibold md:inline" className='my-text-gradient hidden w-[178px] whitespace-nowrap rounded-md border border-[#33D4B7] px-6 py-2 text-center font-semibold md:inline'
id="contact-host" id='contact-host'
> >
Contact the host Contact the host
</Link> </Link>
)} )}
</div> </div>
<div className="flex items-center justify-between gap-4 md:justify-start md:gap-[24px]"> <div className='flex items-center justify-between gap-4 md:justify-start md:gap-[24px]'>
<div className="w-max-content"> <div className='w-max-content'>
<img <img
src={hostData.photo ?? "/default.png"} src={hostData.photo ?? "/default.png"}
className="h-[72px] w-[72px] rounded-full object-cover" className='h-[72px] w-[72px] rounded-full object-cover'
/> />
</div> </div>
<div className="space-y-3 w-[90%]"> <div className='w-[90%] space-y-3'>
<p className="hidden text-xl font-bold md:block">{propertySpace.first_name}</p> <p className='hidden text-xl font-bold md:block'>
<p className="hidden md:block">{propertySpace.about ?? spaceData?.about}</p> {propertySpace.first_name}
</p>
<p className='hidden md:block'>
{propertySpace.about ?? spaceData?.about}
</p>
</div> </div>
{(authState.role == "customer" && propertySpace?.id) && ( {authState.role == "customer" && propertySpace?.id && (
<Link <Link
to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`} to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`}
className="my-text-gradient inline whitespace-nowrap rounded-md border border-[#33D4B7] px-4 py-1 text-center text-sm font-semibold md:hidden" className='my-text-gradient inline whitespace-nowrap rounded-md border border-[#33D4B7] px-4 py-1 text-center text-sm font-semibold md:hidden'
id="contact-host" id='contact-host'
> >
Contact the host Contact the host
</Link> </Link>
)} )}
</div> </div>
<p className="mt-4 block md:hidden">{propertySpace.about ?? spaceData?.about}</p> <p className='mt-4 block md:hidden'>
{propertySpace.about ?? spaceData?.about}
</p>
<hr className="my-[32px] md:my-[47px]" /> <hr className='my-[32px] md:my-[47px]' />
<div className="mb-[18px] flex items-center justify-between"> <div className='mb-[18px] flex items-center justify-between'>
<h3 className="mb-[8px] text-2xl font-semibold">Reviews</h3> <h3 className='mb-[8px] text-2xl font-semibold'>Reviews</h3>
<CustomSelect <CustomSelect
options={[ options={[
{ label: "By Date: Newest First", value: "DESC" }, { label: "By Date: Newest First", value: "DESC" },
{ label: "By Date: Oldest First", value: "ASC" }, { label: "By Date: Oldest First", value: "ASC" },
]} ]}
onChange={setReviewDirection} onChange={setReviewDirection}
accessor="label" accessor='label'
valueAccessor="value" valueAccessor='value'
className="min-w-[200px]" className='min-w-[200px]'
listOptionClassName={"pl-4"} listOptionClassName={"pl-4"}
/> />
</div> </div>
@@ -344,15 +412,12 @@ const PropertyPage = () => {
.sort(sortByPostDate) .sort(sortByPostDate)
.slice(0, 10) .slice(0, 10)
.map((rw) => ( .map((rw) => (
<ReviewCard <ReviewCard key={rw.id} data={rw} />
key={rw.id}
data={rw}
/>
))} ))}
<div className="text-center"> <div className='text-center'>
{reviews.length > 10 ? ( {reviews.length > 10 ? (
<button <button
className="font-semibold underline" className='font-semibold underline'
onClick={() => setReviewsPopup(true)} onClick={() => setReviewsPopup(true)}
> >
View more ({reviews.length}) View more ({reviews.length})
@@ -360,52 +425,57 @@ const PropertyPage = () => {
) : null} ) : null}
</div> </div>
</section> </section>
<hr className="my-[32px] md:my-[47px]" /> <hr className='my-[32px] md:my-[47px]' />
<h3 className="mb-[8px] text-2xl font-semibold">FAQs</h3> <h3 className='mb-[8px] text-2xl font-semibold'>FAQs</h3>
{faqs.map((faq) => ( {faqs.map((faq) => (
<FaqAccordion <FaqAccordion key={faq.id} data={faq} />
key={faq.id}
data={faq}
/>
))} ))}
<hr className="my-[32px] md:my-[47px]" /> <hr className='my-[32px] md:my-[47px]' />
<h3 className="mb-4 text-2xl font-semibold">Property rules</h3> <h3 className='mb-4 text-2xl font-semibold'>Property rules</h3>
<p className="mb-32">{propertySpace.rule ?? spaceData?.rule}</p> <p className='mb-32'>{propertySpace.rule ?? spaceData?.rule}</p>
</div> </div>
<div className="sticky bottom-0 hidden w-full flex-grow bg-white xl:top-16 xl:bottom-[unset] xl:block xl:w-[unset] ml-24"> <div className='sticky bottom-0 ml-24 hidden w-full flex-grow bg-white xl:bottom-[unset] xl:top-16 xl:block xl:w-[unset]'>
<div className="sticky-price-summary mx-auto max-w-2xl p-6 md:border-2"> <div className='sticky-price-summary mx-auto max-w-2xl p-6 md:border-2'>
<h3 className="mb-[8px] text-2xl font-semibold">Price and availability</h3> <h3 className='mb-[8px] text-2xl font-semibold'>
<div className="mb-[13px] flex justify-between"> Price and availability
<span className="text-lg">Max capacity</span> </h3>
<div className='mb-[13px] flex justify-between'>
<span className='text-lg'>Max capacity</span>
<span> <span>
{" "} {" "}
<strong className="font-semibold">{propertySpace.max_capacity ?? spaceData?.max_capacity}</strong> people <strong className='font-semibold'>
{propertySpace.max_capacity ?? spaceData?.max_capacity}
</strong>{" "}
people
</span> </span>
</div> </div>
<div className="mb-[13px] flex justify-between"> <div className='mb-[13px] flex justify-between'>
<span className="text-lg">Pricing from</span> <span className='text-lg'>Pricing from</span>
<span> <span>
from: <strong className="font-semibold">${propertySpace.rate ?? spaceData?.rate}</strong>/h from:{" "}
<strong className='font-semibold'>
${propertySpace.rate ?? spaceData?.rate}
</strong>
/h
</span> </span>
</div> </div>
<form <form className='flex flex-col' onSubmit={handleSubmit(onSubmit)}>
className="flex flex-col" <div className='mb-[13px] flex items-center justify-between'>
onSubmit={handleSubmit(onSubmit)} <span className='text-lg'>Number of guests</span>
>
<div className="mb-[13px] flex items-center justify-between">
<span className="text-lg">Number of guests</span>
<Counter <Counter
register={register} register={register}
name="num_guests" name='num_guests'
setValue={setValue} setValue={setValue}
initialValue={bookingDetails.num_guests || 1} initialValue={bookingDetails.num_guests || 1}
maxCount={propertySpace.max_capacity ?? spaceData?.max_capacity} maxCount={
propertySpace.max_capacity ?? spaceData?.max_capacity
}
minCount={1} minCount={1}
/> />
</div> </div>
<hr className="mb-[24px] hidden md:block" /> <hr className='mb-[24px] hidden md:block' />
<div className="z-50 mb-3"> <div className='z-50 mb-3'>
<DateTimePicker <DateTimePicker
register={register} register={register}
setValue={setValue} setValue={setValue}
@@ -414,68 +484,76 @@ const PropertyPage = () => {
setShowCalendar={setShowCalendar} setShowCalendar={setShowCalendar}
fromDefault={bookingDetails.from} fromDefault={bookingDetails.from}
toDefault={bookingDetails.to} toDefault={bookingDetails.to}
bookedSlots={bookedSlots.map((slot) => ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} bookedSlots={bookedSlots}
scheduleTemplate={scheduleTemplate} scheduleTemplate={scheduleTemplate}
defaultDate={bookingDetails.selectedDate || undefined} defaultDate={bookingDetails.selectedDate || undefined}
/> />
</div> </div>
{authState.role != "customer" && authState.isAuthenticated ? ( {authState.role != "customer" && authState.isAuthenticated ? (
<button <button
type="button" type='button'
onClick={switchToCustomer} onClick={switchToCustomer}
className="login-btn-gradient gap-2 rounded-sm py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
> >
Join as customer to book Join as customer to book
</button> </button>
) : ( ) : (
<button <button
type="submit" type='submit'
id="proceed-to-preview" id='proceed-to-preview'
className="login-btn-gradient gap-2 rounded-sm py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
disabled={(() => { disabled={(() => {
const el = document.getElementById("booking-time"); const el = document.getElementById("booking-time");
return !(el && !el.innerText.includes("Select")); return !(el && !el.innerText.includes("Select"));
})()} })()}
> >
{window.innerWidth > 500 ? "Continue" : "Continue to checkout"} {window.innerWidth > 500
? "Continue"
: "Continue to checkout"}
</button> </button>
)} )}
</form> </form>
</div> </div>
</div> </div>
<div className="mx-auto -mt-16 block w-full max-w-xl p-6 xl:hidden"> <div className='mx-auto -mt-16 block w-full max-w-xl p-6 xl:hidden'>
<h3 className="mb-[8px] text-2xl font-semibold">Price and availability</h3> <h3 className='mb-[8px] text-2xl font-semibold'>
<div className="mb-[13px] flex justify-between"> Price and availability
<span className="text-lg">Max capacity</span> </h3>
<div className='mb-[13px] flex justify-between'>
<span className='text-lg'>Max capacity</span>
<span> <span>
{" "} {" "}
<strong className="font-semibold">{propertySpace.max_capacity ?? spaceData?.max_capacity}</strong> people <strong className='font-semibold'>
{propertySpace.max_capacity ?? spaceData?.max_capacity}
</strong>{" "}
people
</span> </span>
</div> </div>
<div className="mb-[13px] flex justify-between"> <div className='mb-[13px] flex justify-between'>
<span className="text-lg">Pricing from</span> <span className='text-lg'>Pricing from</span>
<span> <span>
from: <strong className="font-semibold">${propertySpace.rate ?? spaceData?.rate}</strong>/h from:{" "}
<strong className='font-semibold'>
${propertySpace.rate ?? spaceData?.rate}
</strong>
/h
</span> </span>
</div> </div>
<form <form className='flex flex-col' onSubmit={handleSubmit(onSubmit)}>
className="flex flex-col" <div className='mb-[13px] flex items-center justify-between'>
onSubmit={handleSubmit(onSubmit)} <span className='text-lg'>Number of guests</span>
>
<div className="mb-[13px] flex items-center justify-between">
<span className="text-lg">Number of guests</span>
<Counter <Counter
register={register} register={register}
name="num_guests" name='num_guests'
setValue={setValue} setValue={setValue}
initialValue={bookingDetails.num_guests || 1} initialValue={bookingDetails.num_guests || 1}
maxCount={propertySpace.max_capacity ?? spaceData?.max_capacity} maxCount={propertySpace.max_capacity ?? spaceData?.max_capacity}
minCount={1} minCount={1}
/> />
</div> </div>
<hr className="mb-[24px] hidden md:block" /> <hr className='mb-[24px] hidden md:block' />
<div className="z-50 mb-3"> <div className='z-50 mb-3'>
<DateTimePicker <DateTimePicker
register={register} register={register}
setValue={setValue} setValue={setValue}
@@ -484,26 +562,28 @@ const PropertyPage = () => {
setShowCalendar={setShowCalendar} setShowCalendar={setShowCalendar}
fromDefault={bookingDetails.from} fromDefault={bookingDetails.from}
toDefault={bookingDetails.to} toDefault={bookingDetails.to}
bookedSlots={bookedSlots.map((slot) => ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} bookedSlots={bookedSlots}
scheduleTemplate={scheduleTemplate} scheduleTemplate={scheduleTemplate}
/> />
</div> </div>
{authState.role != "customer" && authState.isAuthenticated ? ( {authState.role != "customer" && authState.isAuthenticated ? (
<button <button
type="button" type='button'
onClick={switchToCustomer} onClick={switchToCustomer}
className="login-btn-gradient gap-2 rounded-sm py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
> >
Join as customer to book Join as customer to book
</button> </button>
) : ( ) : (
<button <button
type="submit" type='submit'
id="proceed-to-preview" id='proceed-to-preview'
className="login-btn-gradient gap-2 rounded-sm py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
disabled={(() => { disabled={(() => {
const els = document.querySelectorAll("#booking-time"); const els = document.querySelectorAll("#booking-time");
return Array.from(els).every((el) => el.innerText.includes("Select")); return Array.from(els).every((el) =>
el.innerText.includes("Select")
);
})()} })()}
> >
{window.innerWidth > 500 && "Continue"} {window.innerWidth > 500 && "Continue"}
@@ -525,20 +605,25 @@ const PropertyPage = () => {
onDirectionChange={setReviewDirection} onDirectionChange={setReviewDirection}
/> />
<Tooltip <Tooltip
anchorId="proceed-to-preview" anchorId='proceed-to-preview'
place="bottom" place='bottom'
content={"Proceed to book"} content={"Proceed to book"}
noArrow noArrow
/> />
<Tooltip <Tooltip
anchorId="contact-host" anchorId='contact-host'
place="bottom" place='bottom'
content={"Chat with Host"} content={"Chat with Host"}
noArrow noArrow
/> />
<PropertySpaceMapImage <PropertySpaceMapImage
modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${propertySpace.address_line_1 ?? ""},${propertySpace.address_line_2 ?? ""},${propertySpace.city ?? ""},${propertySpace.country ?? "" modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${
}&zoom=15&size=600x400&maptype=roadmap&markers=color:red|${propertySpace.address_line_1 ?? ""},${propertySpace.address_line_2 ?? ""} propertySpace.address_line_1 ?? ""
},${propertySpace.address_line_2 ?? ""},${propertySpace.city ?? ""},${
propertySpace.country ?? ""
}&zoom=15&size=600x400&maptype=roadmap&markers=color:red|${
propertySpace.address_line_1 ?? ""
},${propertySpace.address_line_2 ?? ""}
&key=${import.meta.env.VITE_GOOGLE_API_KEY}`} &key=${import.meta.env.VITE_GOOGLE_API_KEY}`}
modalOpen={showMap} modalOpen={showMap}
closeModal={() => setShowMap(false)} closeModal={() => setShowMap(false)}
+188 -94
View File
@@ -6,7 +6,11 @@ import { useNavigate, useParams } from "react-router";
import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon";
import Icon from "@/components/Icons"; import Icon from "@/components/Icons";
import { callCustomAPI } from "@/utils/callCustomAPI"; import { callCustomAPI } from "@/utils/callCustomAPI";
import { formatAMPM, fullMonthsMapping, getDuration } from "@/utils/date-time-utils"; import {
formatAMPM,
fullMonthsMapping,
getDuration,
} from "@/utils/date-time-utils";
import { useContext } from "react"; import { useContext } from "react";
import { GlobalContext } from "@/globalContext"; import { GlobalContext } from "@/globalContext";
import FavoriteButton from "@/components/frontend/FavoriteButton"; import FavoriteButton from "@/components/frontend/FavoriteButton";
@@ -30,7 +34,9 @@ export default function EditBookingPage() {
const { id: booking_id } = useParams(); const { id: booking_id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { register, watch, handleSubmit, setValue } = useForm({ defaultValues: { selectedAddons: [] } }); const { register, watch, handleSubmit, setValue } = useForm({
defaultValues: { selectedAddons: [] },
});
const formValues = watch(); const formValues = watch();
const { dispatch: globalDispatch } = useContext(GlobalContext); const { dispatch: globalDispatch } = useContext(GlobalContext);
const { dispatch } = useContext(AuthContext); const { dispatch } = useContext(AuthContext);
@@ -39,16 +45,25 @@ export default function EditBookingPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { tax, commission } = useTaxAndCommission(); const { tax, commission } = useTaxAndCommission();
const { bookedSlots, scheduleTemplate } = useSchedulingData({ property_space_id: booking.property_space_id }); const { bookedSlots, scheduleTemplate } = useSchedulingData({
property_space_id: booking.property_space_id,
});
const addons = usePropertyAddons(booking.property_id); const addons = usePropertyAddons(booking.property_id);
const [showCharges, setShowCharges] = useState(false); const [showCharges, setShowCharges] = useState(false);
async function fetchBooking() { async function fetchBooking() {
globalDispatch({ type: "START_LOADING" }); globalDispatch({ type: "START_LOADING" });
const where = [`ergo_booking.id = ${booking_id} AND ergo_booking.deleted_at IS NULL`]; const where = [
`ergo_booking.id = ${booking_id} AND ergo_booking.deleted_at IS NULL`,
];
try { try {
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/details", { where }, "POST", ctrl.signal); const result = await sdk.callRawAPI(
"/v2/api/custom/ergo/booking/details",
{ where },
"POST",
ctrl.signal
);
setBooking(result.list ?? {}); setBooking(result.list ?? {});
setValue("from", formatAMPM(result.list.booking_start_time)); setValue("from", formatAMPM(result.list.booking_start_time));
setValue("to", formatAMPM(result.list.booking_end_time)); setValue("to", formatAMPM(result.list.booking_end_time));
@@ -78,7 +93,7 @@ export default function EditBookingPage() {
if (addons.length > 0 && booking.property_space_id) { if (addons.length > 0 && booking.property_space_id) {
setValue( setValue(
"selectedAddons", "selectedAddons",
booking.add_ons.map((addon) => addon.name), booking.add_ons.map((addon) => addon.name)
); );
} }
}, [addons.length, booking]); }, [addons.length, booking]);
@@ -95,7 +110,9 @@ export default function EditBookingPage() {
{ {
id: booking.id, id: booking.id,
booked_unit: 1, booked_unit: 1,
booking_start_time: new Date(dateFormat + " " + data.from).toISOString(), booking_start_time: new Date(
dateFormat + " " + data.from
).toISOString(),
booking_end_time: new Date(dateFormat + " " + data.to).toISOString(), booking_end_time: new Date(dateFormat + " " + data.to).toISOString(),
commission_rate: Number(commission), commission_rate: Number(commission),
customer_id: Number(user_id), customer_id: Number(user_id),
@@ -108,13 +125,21 @@ export default function EditBookingPage() {
tax_rate: Number(tax ?? booking?.tax), tax_rate: Number(tax ?? booking?.tax),
}, },
"POST", "POST",
ctrl.signal, ctrl.signal
); );
// get addons to delete and addons to create // get addons to delete and addons to create
let addons_to_delete = booking.add_ons.filter((addon) => !data.selectedAddons.includes(addon.name)).map((addon) => addon.booking_addons_id); let addons_to_delete = booking.add_ons
.filter((addon) => !data.selectedAddons.includes(addon.name))
.map((addon) => addon.booking_addons_id);
let addons_to_create = addons let addons_to_create = addons
.filter((addon) => data.selectedAddons.includes(addon.add_on_name) && !booking.add_ons.map((addon) => addon.name).includes(addon.add_on_name)) .filter(
(addon) =>
data.selectedAddons.includes(addon.add_on_name) &&
!booking.add_ons
.map((addon) => addon.name)
.includes(addon.add_on_name)
)
.map((addon) => addon.id); .map((addon) => addon.id);
sdk.setTable("booking_addons"); sdk.setTable("booking_addons");
@@ -124,7 +149,10 @@ export default function EditBookingPage() {
} }
for (const property_add_on_id of addons_to_create) { for (const property_add_on_id of addons_to_create) {
await sdk.callRestAPI({ booking_id: booking.id, property_add_on_id }, "POST"); await sdk.callRestAPI(
{ booking_id: booking.id, property_add_on_id },
"POST"
);
} }
navigate(`/account/my-bookings/${booking.id}`); navigate(`/account/my-bookings/${booking.id}`);
} catch (err) { } catch (err) {
@@ -141,7 +169,11 @@ export default function EditBookingPage() {
// notify host // notify host
if (booking.status == BOOKING_STATUS.UPCOMING) { if (booking.status == BOOKING_STATUS.UPCOMING) {
const r = await sdk.sendEmail(booking.host_email, "Booking Changed", `The structure for this email will be changed shortly`); const r = await sdk.sendEmail(
booking.host_email,
"Booking Changed",
`The structure for this email will be changed shortly`
);
} }
} }
const bookingStartDate = new Date(formValues.selectedDate); const bookingStartDate = new Date(formValues.selectedDate);
@@ -150,27 +182,31 @@ export default function EditBookingPage() {
return ( return (
<div <div
className="container mx-auto min-h-screen bg-white px-6 normal-case 2xl:px-16" className='container mx-auto min-h-screen bg-white px-6 normal-case 2xl:px-16'
onClick={() => setShowCalendar(false)} onClick={() => setShowCalendar(false)}
> >
<button <button
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center font-semibold" className='mb-2 mr-2 inline-flex items-center py-2.5 pr-5 text-center font-semibold'
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
<Icon <Icon
type="arrow" type='arrow'
variant="narrow-left" variant='narrow-left'
className="h-4 w-4 stroke-[#667085]" className='h-4 w-4 stroke-[#667085]'
/>{" "} />{" "}
<span className="ml-2">Back</span> <span className='ml-2'>Back</span>
</button> </button>
<h2 className="mb-[20px] text-3xl font-semibold">Edit Booking</h2> <h2 className='mb-[20px] text-3xl font-semibold'>Edit Booking</h2>
<div className="flex flex-col items-start justify-between md:flex-row"> <div className='flex flex-col items-start justify-between md:flex-row'>
<div className="w-full md:w-[43%]"> <div className='w-full md:w-[43%]'>
<div className="mb-[40px] flex flex-col gap-[24px] md:flex-row"> <div className='mb-[40px] flex flex-col gap-[24px] md:flex-row'>
<div <div
className="h-[150px] rounded-lg bg-cover bg-center pr-2 md:w-[204px]" className='h-[150px] rounded-lg bg-cover bg-center pr-2 md:w-[204px]'
style={{ backgroundImage: `url(${booking.image_url ?? "/default-property.jpg"})` }} style={{
backgroundImage: `url(${
booking.image_url ?? "/default-property.jpg"
})`,
}}
> >
<FavoriteButton <FavoriteButton
space_id={booking.property_space_id} space_id={booking.property_space_id}
@@ -178,38 +214,52 @@ export default function EditBookingPage() {
reRender={null} reRender={null}
/> />
</div> </div>
<div className=""> <div className=''>
<h3 className="mb-[6px] text-[18px] font-semibold">{booking.property_name}</h3> <h3 className='mb-[6px] text-[18px] font-semibold'>
<p className="mb-[6px] text-[#475467]">{booking.address_line_1}</p> {booking.property_name}
<p className="mb-[6px] text-[#475467]">{booking.city + ", " + booking.address_line_2}</p> </h3>
</div> <p className='mb-[6px] text-[#475467]'>
</div> {booking.address_line_1}
<div className="mb-[12px] flex justify-between"> </p>
<div className="flex gap-[10px]"> <p className='mb-[6px] text-[#475467]'>
<DateTimeIcon /> {booking.city + ", " + booking.address_line_2}
<h4 className="text-lg font-semibold">Date & time</h4>
</div>
</div>
<div className="mb-[12px] flex justify-between">
<p>Date</p>
<p className="font-semibold text-[#344054]">
{" "}
{(bookingStartDate instanceof Date ? fullMonthsMapping[bookingStartDate.getMonth()] : "") + " " + bookingStartDate.getDate() + "/" + bookingStartDate.getFullYear()}
</p> </p>
</div> </div>
<div className="mb-[12px] flex justify-between"> </div>
<div className='mb-[12px] flex justify-between'>
<div className='flex gap-[10px]'>
<DateTimeIcon />
<h4 className='text-lg font-semibold'>Date & time</h4>
</div>
</div>
<div className='mb-[12px] flex justify-between'>
<p>Date</p>
<p className='font-semibold text-[#344054]'>
{" "}
{(bookingStartDate instanceof Date
? fullMonthsMapping[bookingStartDate.getMonth()]
: "") +
" " +
bookingStartDate.getDate() +
"/" +
bookingStartDate.getFullYear()}
</p>
</div>
<div className='mb-[12px] flex justify-between'>
<p>Time</p> <p>Time</p>
<p className="font-semibold text-[#344054]"> <p className='font-semibold text-[#344054]'>
{formValues.from} - {formValues.to} {formValues.from} - {formValues.to}
</p> </p>
</div> </div>
<div className="mb-[12px] flex justify-between"> <div className='mb-[12px] flex justify-between'>
<p>Duration</p> <p>Duration</p>
<p className="font-semibold text-[#344054]">{getDuration(formValues.from, formValues.to)} hour(s)</p> <p className='font-semibold text-[#344054]'>
{getDuration(formValues.from, formValues.to)} hour(s)
</p>
</div> </div>
<div className="mt-[40px] mb-[16px] flex gap-[10px]"> <div className='mb-[16px] mt-[40px] flex gap-[10px]'>
<AddIcon /> <AddIcon />
<h4 className="text-lg font-semibold">Add Ons</h4> <h4 className='text-lg font-semibold'>Add Ons</h4>
</div> </div>
{addons.map((addon) => { {addons.map((addon) => {
return ( return (
@@ -217,41 +267,50 @@ export default function EditBookingPage() {
key={addon.id} key={addon.id}
data={addon} data={addon}
register={register} register={register}
name="selectedAddons" name='selectedAddons'
/> />
); );
})} })}
</div> </div>
<div className={`${showCharges ? "hidden" : "block"} sticky-price-summary ml-auto w-full bg-white p-4 md:w-[473px] md:border md:p-[32px]`}> <div
<h3 className="mb-[8px] text-2xl font-semibold">Price and availability</h3> className={`${
<div className="mb-[13px] flex justify-between"> showCharges ? "hidden" : "block"
<span className="text-lg">Max capacity</span> } sticky-price-summary ml-auto w-full bg-white p-4 md:w-[473px] md:border md:p-[32px]`}
>
<h3 className='mb-[8px] text-2xl font-semibold'>
Price and availability
</h3>
<div className='mb-[13px] flex justify-between'>
<span className='text-lg'>Max capacity</span>
<span> <span>
{" "} {" "}
<strong className="font-semibold">{booking.max_capacity}</strong> people <strong className='font-semibold'>
{booking.max_capacity}
</strong>{" "}
people
</span> </span>
</div> </div>
<div className="mb-[13px] flex justify-between"> <div className='mb-[13px] flex justify-between'>
<span className="text-lg">Pricing from</span> <span className='text-lg'>Pricing from</span>
<span> <span>
from: <strong className="font-semibold">${booking.rate}</strong>/h from: <strong className='font-semibold'>${booking.rate}</strong>/h
</span> </span>
</div> </div>
<div className="flex flex-col"> <div className='flex flex-col'>
<div className="mb-[13px] flex items-center justify-between"> <div className='mb-[13px] flex items-center justify-between'>
<span className="text-lg">Number of guests</span> <span className='text-lg'>Number of guests</span>
<Counter <Counter
register={register} register={register}
name="num_guests" name='num_guests'
setValue={setValue} setValue={setValue}
initialValue={(booking.num_guests ?? 0) + 1} initialValue={(booking.num_guests ?? 0) + 1}
maxCount={booking.max_capacity} maxCount={booking.max_capacity}
/> />
</div> </div>
<hr className="mb-[24px] hidden md:block" /> <hr className='mb-[24px] hidden md:block' />
<div className="z-50 mb-3"> <div className='z-50 mb-3'>
<DateTimePicker <DateTimePicker
register={register} register={register}
setValue={setValue} setValue={setValue}
@@ -260,15 +319,15 @@ export default function EditBookingPage() {
setShowCalendar={setShowCalendar} setShowCalendar={setShowCalendar}
fromDefault={formatAMPM(booking.booking_start_time)} fromDefault={formatAMPM(booking.booking_start_time)}
toDefault={formatAMPM(booking.booking_end_time)} toDefault={formatAMPM(booking.booking_end_time)}
bookedSlots={bookedSlots.map((slot) => ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} bookedSlots={bookedSlots}
scheduleTemplate={scheduleTemplate} scheduleTemplate={scheduleTemplate}
defaultDate={bookingStartDate} defaultDate={bookingStartDate}
/> />
</div> </div>
<button <button
type="button" type='button'
id="proceed-to-preview" id='proceed-to-preview'
className="login-btn-gradient gap-2 rounded-tr rounded-br py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-br rounded-tr px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
disabled={(() => { disabled={(() => {
const el = document.getElementById("booking-time"); const el = document.getElementById("booking-time");
return !(el && !el.innerText.includes("Select")); return !(el && !el.innerText.includes("Select"));
@@ -280,70 +339,105 @@ export default function EditBookingPage() {
</div> </div>
</div> </div>
<form <form
className={`${showCharges ? "block" : "hidden"} fadeIn w-full md:w-[40%]`} className={`${
showCharges ? "block" : "hidden"
} fadeIn w-full md:w-[40%]`}
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<div className="flex flex-col rounded-sm border-2 border-[#33D4B7] p-[12px] pb-0 md:p-[32px]"> <div className='flex flex-col rounded-sm border-2 border-[#33D4B7] p-[12px] pb-0 md:p-[32px]'>
<div className="mb-[16px] flex justify-between text-[#101828]"> <div className='mb-[16px] flex justify-between text-[#101828]'>
<h4 className="text-2xl font-semibold">Charges</h4> <h4 className='text-2xl font-semibold'>Charges</h4>
</div> </div>
<div className="tiny-scroll mb-2 max-h-[200px] overflow-y-auto pr-3"> <div className='tiny-scroll mb-2 max-h-[200px] overflow-y-auto pr-3'>
<div className="mb-[12px] flex justify-between"> <div className='mb-[12px] flex justify-between'>
<p>Rate</p> <p>Rate</p>
<p className="font-semibold text-[#344054]">${booking.rate.toFixed(2)}/h</p> <p className='font-semibold text-[#344054]'>
${booking.rate.toFixed(2)}/h
</p>
</div> </div>
<div className="mb-[12px] flex justify-between"> <div className='mb-[12px] flex justify-between'>
<p>Price</p> <p>Price</p>
<p className="font-semibold text-[#344054]"> ${(booking.rate * getDuration(formValues.from, formValues.to)).toFixed(2)}</p> <p className='font-semibold text-[#344054]'>
{" "}
$
{(
booking.rate * getDuration(formValues.from, formValues.to)
).toFixed(2)}
</p>
</div> </div>
{formValues.selectedAddons.map((addon_name, idx) => { {formValues.selectedAddons.map((addon_name, idx) => {
let price = addons.find((addon) => addon.add_on_name == addon_name)?.cost; let price = addons.find(
(addon) => addon.add_on_name == addon_name
)?.cost;
if (!price) return null; if (!price) return null;
return ( return (
<div <div className='mb-[12px] flex justify-between' key={idx}>
className="mb-[12px] flex justify-between"
key={idx}
>
<p>{addon_name}</p> <p>{addon_name}</p>
<p className="font-semibold text-[#344054]"> ${Number(price).toFixed(2)}</p> <p className='font-semibold text-[#344054]'>
{" "}
${Number(price).toFixed(2)}
</p>
</div> </div>
); );
})} })}
<div className="mb-[12px] flex justify-between"> <div className='mb-[12px] flex justify-between'>
<p>Tax</p> <p>Tax</p>
<p className="font-semibold text-[#344054]"> ${Number((booking.rate * getDuration(formValues.from, formValues.to) * tax) / 100).toFixed(2)}</p> <p className='font-semibold text-[#344054]'>
{" "}
$
{Number(
(booking.rate *
getDuration(formValues.from, formValues.to) *
tax) /
100
).toFixed(2)}
</p>
</div> </div>
<div className="mb-[12px] flex justify-between"> <div className='mb-[12px] flex justify-between'>
<p>Total</p> <p>Total</p>
<p className="font-semibold text-[#344054]"> <p className='font-semibold text-[#344054]'>
{" "} {" "}
$ $
{( {(
Number( Number(
addons.reduce((acc, curr) => { addons.reduce((acc, curr) => {
if (!formValues.selectedAddons.includes(curr.add_on_name)) return acc; if (
!formValues.selectedAddons.includes(curr.add_on_name)
)
return acc;
return Number(acc) + (Number(curr.cost) ?? 0); return Number(acc) + (Number(curr.cost) ?? 0);
}, 0), }, 0)
) + ) +
Number((booking.rate * getDuration(formValues.from, formValues.to) * tax) / 100) + Number(
Number(booking.rate * getDuration(formValues.from, formValues.to)) (booking.rate *
getDuration(formValues.from, formValues.to) *
tax) /
100
) +
Number(
booking.rate * getDuration(formValues.from, formValues.to)
)
).toFixed(2)} ).toFixed(2)}
</p> </p>
</div> </div>
</div> </div>
<LoadingButton <LoadingButton
type="submit" type='submit'
loading={loading} loading={loading}
className={`login-btn-gradient mb-[12px] gap-2 rounded-tr rounded-br px-2 text-center tracking-wide text-white outline-none focus:outline-none ${loading ? "loading py-2" : "py-3"}`} className={`login-btn-gradient mb-[12px] gap-2 rounded-br rounded-tr px-2 text-center tracking-wide text-white outline-none focus:outline-none ${
loading ? "loading py-2" : "py-3"
}`}
disabled={tax == null || commission == null} disabled={tax == null || commission == null}
> >
Save Save
</LoadingButton> </LoadingButton>
<p className="text-center text-sm">(Note: this will affect the rates)</p> <p className='text-center text-sm'>
(Note: this will affect the rates)
</p>
</div> </div>
<a <a
href="/" href='/'
className="mt-[12px] block text-center text-sm text-[#667085] underline" className='mt-[12px] block text-center text-sm text-[#667085] underline'
> >
Cancellation Policy Cancellation Policy
</a> </a>
+317 -158
View File
@@ -1,6 +1,11 @@
import React, { Fragment, useEffect } from "react"; import React, { Fragment, useEffect } from "react";
import { useState } from "react"; import { useState } from "react";
import { Navigate, useLocation, useNavigate, useParams } from "react-router-dom"; import {
Navigate,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import FaqAccordion from "@/components/frontend/FaqAccordion"; import FaqAccordion from "@/components/frontend/FaqAccordion";
import ReviewCard from "@/components/frontend/ReviewCard"; import ReviewCard from "@/components/frontend/ReviewCard";
import StarIcon from "@/components/frontend/icons/StarIcon"; import StarIcon from "@/components/frontend/icons/StarIcon";
@@ -13,12 +18,24 @@ import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu";
import MySpaceBookingHistoryPage from "./MySpaceBookingHistoryPage"; import MySpaceBookingHistoryPage from "./MySpaceBookingHistoryPage";
import CustomSelect from "@/components/frontend/CustomSelect"; import CustomSelect from "@/components/frontend/CustomSelect";
import DraftProgress from "@/components/frontend/DraftProgress"; import DraftProgress from "@/components/frontend/DraftProgress";
import { DRAFT_STATUS, IMAGE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; import {
DRAFT_STATUS,
IMAGE_STATUS,
SPACE_VISIBILITY,
} from "@/utils/constants";
import { useContext } from "react"; import { useContext } from "react";
import { GlobalContext } from "@/globalContext"; import { GlobalContext } from "@/globalContext";
import FavoriteButton from "@/components/frontend/FavoriteButton"; import FavoriteButton from "@/components/frontend/FavoriteButton";
import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; import PropertyImageSlider from "@/components/frontend/PropertyImageSlider";
import { usePropertyAddons, usePropertySpace, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceImages, usePropertySpaceReviews, usePublicUserData } from "@/hooks/api"; import {
usePropertyAddons,
usePropertySpace,
usePropertySpaceAmenities,
usePropertySpaceFaqs,
usePropertySpaceImages,
usePropertySpaceReviews,
usePublicUserData,
} from "@/hooks/api";
import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import AllReviewsModal from "@/components/frontend/AllReviewsModal"; import AllReviewsModal from "@/components/frontend/AllReviewsModal";
@@ -32,7 +49,11 @@ let sdk = new MkdSDK();
let ctrl = new AbortController(); let ctrl = new AbortController();
const statusMapping = ["Under Review", "Active", "Rejected"]; const statusMapping = ["Under Review", "Active", "Rejected"];
const statusColorMapping = ["text-[#DC6803]", "my-text-gradient", "text-[#D92D20]"]; const statusColorMapping = [
"text-[#DC6803]",
"my-text-gradient",
"text-[#D92D20]",
];
const MySpaceDetailsPage = () => { const MySpaceDetailsPage = () => {
const { state: spaceData } = useLocation(); const { state: spaceData } = useLocation();
@@ -55,10 +76,9 @@ const MySpaceDetailsPage = () => {
const [fetching, setFetching] = useState(true); const [fetching, setFetching] = useState(true);
const { propertySpace, notFound } = usePropertySpace(id, render); const { propertySpace, notFound } = usePropertySpace(id, render);
const hostData = usePublicUserData(propertySpace.host_id); const hostData = usePublicUserData(propertySpace.host_id);
const spaceImages = usePropertySpaceImagesV2(propertySpace.id, false,); const spaceImages = usePropertySpaceImagesV2(propertySpace.id, false);
const spaceAddons = usePropertyAddons(propertySpace.property_id); const spaceAddons = usePropertyAddons(propertySpace.property_id);
const spaceAmenities = usePropertySpaceAmenities(propertySpace.id); const spaceAmenities = usePropertySpaceAmenities(propertySpace.id);
@@ -66,10 +86,16 @@ const MySpaceDetailsPage = () => {
const reviews = usePropertySpaceReviews(propertySpace.id); const reviews = usePropertySpaceReviews(propertySpace.id);
const [deleteSpace, setDeleteSpace] = useState(false); const [deleteSpace, setDeleteSpace] = useState(false);
async function fetchBookedSlots(id) { async function fetchBookedSlots(id) {
try { try {
const result = await callCustomAPI("customer/schedule", "post", { property_spaces_id: id }, "", null, "v3"); const result = await callCustomAPI(
"customer/schedule",
"post",
{ property_spaces_id: id },
"",
null,
"v3"
);
if (Array.isArray(result.list)) { if (Array.isArray(result.list)) {
setBookedSlots(result.list); setBookedSlots(result.list);
} }
@@ -94,11 +120,14 @@ const MySpaceDetailsPage = () => {
limit: 1, limit: 1,
where: [`property_spaces_id = ${id}`], where: [`property_spaces_id = ${id}`],
}, },
"PAGINATE", "PAGINATE"
); );
if (Array.isArray(result.list) && result.list.length > 0) { if (Array.isArray(result.list) && result.list.length > 0) {
setScheduleTemplate({ custom_slots: result.list[0].custom_slots, schedule_id: result.list[0].id }); setScheduleTemplate({
custom_slots: result.list[0].custom_slots,
schedule_id: result.list[0].id,
});
} }
if (result.list[0]?.schedule_template_id) { if (result.list[0]?.schedule_template_id) {
const templateResult = await callCustomAPI( const templateResult = await callCustomAPI(
@@ -109,9 +138,12 @@ const MySpaceDetailsPage = () => {
limit: 1, limit: 1,
where: [`id = ${result.list[0].schedule_template_id}`], where: [`id = ${result.list[0].schedule_template_id}`],
}, },
"PAGINATE", "PAGINATE"
); );
if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { if (
Array.isArray(templateResult.list) &&
(templateResult.list[0] ?? {})
) {
setScheduleTemplate((prev) => { setScheduleTemplate((prev) => {
let updated = { ...prev, ...templateResult.list[0] }; let updated = { ...prev, ...templateResult.list[0] };
return updated; return updated;
@@ -131,9 +163,16 @@ const MySpaceDetailsPage = () => {
async function fetchMySpaceBookings() { async function fetchMySpaceBookings() {
const user_id = localStorage.getItem("user"); const user_id = localStorage.getItem("user");
var where = [`ergo_booking.host_id = ${user_id} AND ergo_booking.property_space_id = ${id} AND ergo_booking.deleted_at IS NULL`]; var where = [
`ergo_booking.host_id = ${user_id} AND ergo_booking.property_space_id = ${id} AND ergo_booking.deleted_at IS NULL`,
];
try { try {
const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1, limit: 10000, where }, "POST", ctrl.signal); const result = await sdk.callRawAPI(
"/v2/api/custom/ergo/booking/PAGINATE",
{ page: 1, limit: 10000, where },
"POST",
ctrl.signal
);
if (Array.isArray(result.list)) { if (Array.isArray(result.list)) {
setMyBookings(result.list); setMyBookings(result.list);
} }
@@ -153,7 +192,12 @@ const MySpaceDetailsPage = () => {
async function hidePropertySpace(id) { async function hidePropertySpace(id) {
globalDispatch({ type: "START_LOADING" }); globalDispatch({ type: "START_LOADING" });
try { try {
await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.HIDDEN }, "POST", ctrl.signal); await sdk.callRawAPI(
"/rest/property_spaces/PUT",
{ id, availability: SPACE_VISIBILITY.HIDDEN },
"POST",
ctrl.signal
);
forceRender(new Date()); forceRender(new Date());
} catch (err) { } catch (err) {
tokenExpireError(dispatch, err.message); tokenExpireError(dispatch, err.message);
@@ -175,7 +219,12 @@ const MySpaceDetailsPage = () => {
async function showPropertySpace(id) { async function showPropertySpace(id) {
globalDispatch({ type: "START_LOADING" }); globalDispatch({ type: "START_LOADING" });
try { try {
await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.VISIBLE }, "POST", ctrl.signal); await sdk.callRawAPI(
"/rest/property_spaces/PUT",
{ id, availability: SPACE_VISIBILITY.VISIBLE },
"POST",
ctrl.signal
);
forceRender(new Date()); forceRender(new Date());
} catch (err) { } catch (err) {
tokenExpireError(dispatch, err.message); tokenExpireError(dispatch, err.message);
@@ -215,54 +264,90 @@ const MySpaceDetailsPage = () => {
return ( return (
<div <div
className="-mt-2 text-sm normal-case md:-mt-10 md:text-base" className='-mt-2 text-sm normal-case md:-mt-10 md:text-base'
onClick={() => { onClick={() => {
setShowCalendar(false); setShowCalendar(false);
}} }}
> >
<div> <div>
<button <button
type="button" type='button'
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold" className='mb-2 mr-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold'
> >
<Icon <Icon
type="arrow" type='arrow'
variant="narrow-left" variant='narrow-left'
className="h-4 w-4 stroke-[#667085]" className='h-4 w-4 stroke-[#667085]'
/>{" "} />{" "}
<span className="ml-2">Back</span> <span className='ml-2'>Back</span>
</button> </button>
</div> </div>
<div className="mb-[22px] flex items-center justify-between"> <div className='mb-[22px] flex items-center justify-between'>
<h1 className="mr-3 text-2xl font-semibold text-[#101828] md:text-3xl">Space Details</h1> <h1 className='mr-3 text-2xl font-semibold text-[#101828] md:text-3xl'>
<div className="flex items-center gap-[16px]"> Space Details
<span className={`${"bg-[#F2F4F7]"} rounded-sm px-[16px] py-[8px] ${statusColorMapping[propertySpace.space_status ?? spaceData?.space_status ?? 0]} border text-sm font-semibold uppercase`}> </h1>
<div className='flex items-center gap-[16px]'>
<span
className={`${"bg-[#F2F4F7]"} rounded-sm px-[16px] py-[8px] ${
statusColorMapping[
propertySpace.space_status ?? spaceData?.space_status ?? 0
]
} border text-sm font-semibold uppercase`}
>
{" "} {" "}
{(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED ? "DRAFT" : statusMapping[propertySpace.space_status ?? spaceData?.space_status ?? 0]} {(propertySpace.draft_status ?? spaceData?.draft_status) <
DRAFT_STATUS.COMPLETED
? "DRAFT"
: statusMapping[
propertySpace.space_status ?? spaceData?.space_status ?? 0
]}
</span> </span>
<button onClick={() => setDeleteSpace(true)} className={`${(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED ? 'block' : 'hidden'}`}>Delete draft</button> <button
onClick={() => setDeleteSpace(true)}
className={`${
(propertySpace.draft_status ?? spaceData?.draft_status) <
DRAFT_STATUS.COMPLETED
? "block"
: "hidden"
}`}
>
Delete draft
</button>
<ThreeDotsMenu <ThreeDotsMenu
disabled={(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED} disabled={
hidden={(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED} (propertySpace.draft_status ?? spaceData?.draft_status) <
DRAFT_STATUS.COMPLETED
}
hidden={
(propertySpace.draft_status ?? spaceData?.draft_status) <
DRAFT_STATUS.COMPLETED
}
items={[ items={[
{ {
label: "Activate Space", label: "Activate Space",
icon: <></>, icon: <></>,
onClick: () => showPropertySpace(propertySpace.id), onClick: () => showPropertySpace(propertySpace.id),
notShow: (propertySpace.space_status ?? spaceData?.space_status) == 0 || (propertySpace.availability ?? spaceData?.availability) == 1, notShow:
(propertySpace.space_status ?? spaceData?.space_status) ==
0 ||
(propertySpace.availability ?? spaceData?.availability) == 1,
}, },
{ {
label: "Deactivate Space", label: "Deactivate Space",
icon: <></>, icon: <></>,
onClick: () => hidePropertySpace(propertySpace.id), onClick: () => hidePropertySpace(propertySpace.id),
notShow: (propertySpace.availability ?? spaceData?.availability) == 0, notShow:
(propertySpace.availability ?? spaceData?.availability) == 0,
}, },
{ {
label: "Edit Scheduling", label: "Edit Scheduling",
icon: <></>, icon: <></>,
onClick: () => { onClick: () => {
navigate(`/account/my-spaces/${id}/edit-scheduling?mode=edit`, { state: scheduleTemplate }); navigate(
`/account/my-spaces/${id}/edit-scheduling?mode=edit`,
{ state: scheduleTemplate }
);
}, },
}, },
{ {
@@ -276,7 +361,9 @@ const MySpaceDetailsPage = () => {
label: "Edit Space Details", label: "Edit Space Details",
icon: <></>, icon: <></>,
onClick: () => { onClick: () => {
navigate(`/account/my-spaces/${id}/edit-property-space?mode=edit`); navigate(
`/account/my-spaces/${id}/edit-property-space?mode=edit`
);
}, },
}, },
{ {
@@ -289,17 +376,22 @@ const MySpaceDetailsPage = () => {
</div> </div>
</div> </div>
<Tab.Group> <Tab.Group>
<Tab.List <Tab.List as={"div"} className='two-tab-menu mb-[32px] border-b'>
as={"div"} <Tab as={Fragment}>
className="two-tab-menu mb-[32px] border-b" <button
className={`px-4 py-3 text-xl focus:outline-none ui-selected:font-semibold`}
> >
<Tab as={Fragment}> Space Listing
<button className={`py-3 px-4 text-xl focus:outline-none ui-selected:font-semibold`}>Space Listing</button> </button>
</Tab>{" "} </Tab>{" "}
<Tab as={Fragment}> <Tab as={Fragment}>
<button className={`py-3 px-4 text-xl focus:outline-none ui-selected:font-semibold`}>Booking History</button> <button
className={`px-4 py-3 text-xl focus:outline-none ui-selected:font-semibold`}
>
Booking History
</button>
</Tab>{" "} </Tab>{" "}
<div className="mover"></div> <div className='mover'></div>
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel as={Fragment}> <Tab.Panel as={Fragment}>
@@ -307,126 +399,163 @@ const MySpaceDetailsPage = () => {
{propertySpace.id ?? spaceData?.id ? ( {propertySpace.id ?? spaceData?.id ? (
<> <>
{" "} {" "}
{(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED ? ( {(propertySpace.draft_status ?? spaceData?.draft_status) <
DRAFT_STATUS.COMPLETED ? (
<> <>
<p className="mb-4">Finish creating your space</p> <p className='mb-4'>Finish creating your space</p>
<DraftProgress <DraftProgress
data={propertySpace ?? spaceData} data={propertySpace ?? spaceData}
scheduleTemplate={scheduleTemplate} scheduleTemplate={scheduleTemplate}
/> />
</> </>
) : ( ) : (
<div className={"mb-[40px] flex grid-cols-3 flex-col gap-[16px] md:grid"}> <div
<div className="flex justify-between border px-[16px] py-[8px]"> className={
"mb-[40px] flex grid-cols-3 flex-col gap-[16px] md:grid"
}
>
<div className='flex justify-between border px-[16px] py-[8px]'>
<p>Space ID</p> <p>Space ID</p>
<p className="font-semibold">{id}</p> <p className='font-semibold'>{id}</p>
</div> </div>
<div className="flex justify-between border px-[16px] py-[8px]"> <div className='flex justify-between border px-[16px] py-[8px]'>
<p>Total Revenue</p> <p>Total Revenue</p>
<p className="font-semibold"> <p className='font-semibold'>
${" "} ${" "}
{myBookings {myBookings
.reduce((acc, curr) => { .reduce((acc, curr) => {
return acc + (curr.total ?? 0) + (curr.addon_cost ?? 0); return (
acc + (curr.total ?? 0) + (curr.addon_cost ?? 0)
);
}, 0) }, 0)
.toFixed(2)} .toFixed(2)}
</p> </p>
</div> </div>
<div className="flex justify-between border px-[16px] py-[8px]"> <div className='flex justify-between border px-[16px] py-[8px]'>
<p>Total Bookings</p> <p>Total Bookings</p>
<p className="font-semibold">{myBookings.length}</p> <p className='font-semibold'>{myBookings.length}</p>
</div> </div>
</div> </div>
)} )}
</> </>
) : null} ) : null}
<div className="mb-[18px] flex flex-col items-start justify-between md:flex-row md:items-center"> <div className='mb-[18px] flex flex-col items-start justify-between md:flex-row md:items-center'>
<div className="flex flex-col items-start gap-4 normal-case md:flex-row md:items-center"> <div className='flex flex-col items-start gap-4 normal-case md:flex-row md:items-center'>
<h2 className="text-3xl font-semibold">{propertySpace.name ?? spaceData?.name}</h2> <h2 className='text-3xl font-semibold'>
<p className="text-[#475467]">{propertySpace.address_line_1 ?? spaceData?.address_line_1}</p> {propertySpace.name ?? spaceData?.name}
</h2>
<p className='text-[#475467]'>
{propertySpace.address_line_1 ?? spaceData?.address_line_1}
</p>
<button <button
className="whitespace-nowrap text-sm underline" className='whitespace-nowrap text-sm underline'
target="_blank" target='_blank'
onClick={() => setShowMap(true)} onClick={() => setShowMap(true)}
> >
(view on map) (view on map)
</button> </button>
</div> </div>
<div className="mt-[19px] flex w-full justify-center gap-4 md:mt-0 md:w-[unset]"> <div className='mt-[19px] flex w-full justify-center gap-4 md:mt-0 md:w-[unset]'>
<p className="flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]"> <p className='flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]'>
<StarIcon /> <StarIcon />
<strong className="font-semibold"> <strong className='font-semibold'>
{(Number(propertySpace.average_space_rating ?? spaceData?.average_space_rating) || 0).toFixed(1)} {(
<span className="font-normal">({propertySpace.space_rating_count ?? spaceData?.space_rating_count})</span> Number(
propertySpace.average_space_rating ??
spaceData?.average_space_rating
) || 0
).toFixed(1)}
<span className='font-normal'>
(
{propertySpace.space_rating_count ??
spaceData?.space_rating_count}
)
</span>
</strong> </strong>
</p> </p>
<div className="flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]"> <div className='flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]'>
<FavoriteButton <FavoriteButton
space_id={propertySpace.id ?? spaceData?.id} space_id={propertySpace.id ?? spaceData?.id}
user_property_spaces_id={propertySpace.user_property_spaces_id ?? spaceData?.user_property_spaces_id} user_property_spaces_id={
propertySpace.user_property_spaces_id ??
spaceData?.user_property_spaces_id
}
reRender={forceRender} reRender={forceRender}
withLoader={true} withLoader={true}
className="-mb-1" className='-mb-1'
buttonClassName="" buttonClassName=''
stroke="#344054" stroke='#344054'
favColor={"black"} favColor={"black"}
/> />
<span>Save</span> <span>Save</span>
</div> </div>
</div> </div>
</div> </div>
<div className="snap-scroll relative mb-[66px] flex h-[381px] gap-[32px] px-[14px] md:px-0"> <div className='snap-scroll relative mb-[66px] flex h-[381px] gap-[32px] px-[14px] md:px-0'>
{spaceImages[0]?.photo_url && (
{spaceImages[0]?.photo_url &&
<img <img
src={spaceImages[0]?.photo_url} src={spaceImages[0]?.photo_url}
className="h-full rounded-lg object-cover xl:min-w-[616px]" className='h-full rounded-lg object-cover xl:min-w-[616px]'
/> />
} )}
{spaceImages[1]?.photo_url && {spaceImages[1]?.photo_url && (
<img <img
src={spaceImages[1]?.photo_url} src={spaceImages[1]?.photo_url}
className="h-full w-[292px] rounded-lg object-cover" className='h-full w-[292px] rounded-lg object-cover'
/> />
} )}
<div className={`${((!spaceImages[3]?.photo_url)) ? "flex min-w-[550px] flex-col" : "block"} "gap-4 overflow-hidden md:gap-[32px]"`}> <div
{spaceImages[2]?.photo_url && className={`${
!spaceImages[3]?.photo_url
? "flex min-w-[550px] flex-col"
: "block"
} "gap-4 md:gap-[32px]" overflow-hidden`}
>
{spaceImages[2]?.photo_url && (
<img <img
src={spaceImages[2]?.photo_url} src={spaceImages[2]?.photo_url}
className={`${spaceImages[3]?.photo_url && "h-1/2"} "rounded-lg object-cover md:w-full"`} className={`${
spaceImages[3]?.photo_url && "h-1/2"
} "rounded-lg md:w-full" object-cover`}
/> />
} )}
{spaceImages[3]?.photo_url && {spaceImages[3]?.photo_url && (
<img <img
src={spaceImages[3]?.photo_url} src={spaceImages[3]?.photo_url}
className="h-1/2 rounded-lg object-cover md:w-full" className='h-1/2 rounded-lg object-cover md:w-full'
/> />
} )}
</div> </div>
{spaceImages[4]?.photo_url && {spaceImages[4]?.photo_url && (
<img <img
src={spaceImages[4]?.photo_url} src={spaceImages[4]?.photo_url}
className="h-full w-[292px] rounded-lg object-cover" className='h-full w-[292px] rounded-lg object-cover'
/> />
} )}
<button <button
className="sticky right-6 mb-[8px] min-w-[170px] self-end border bg-[#00000080] px-3 py-1 text-center text-sm text-white" className='sticky right-6 mb-[8px] min-w-[170px] self-end border bg-[#00000080] px-3 py-1 text-center text-sm text-white'
onClick={() => setGalleryOpen(true)} onClick={() => setGalleryOpen(true)}
> >
View all photos ({spaceImages.length}) View all photos ({spaceImages.length})
</button> </button>
</div> </div>
<section className="relative flex flex-col items-start xl:flex-row xl:gap-12"> <section className='relative flex flex-col items-start xl:flex-row xl:gap-12'>
<div className="w-full px-2 md:px-0 xl:w-3/5"> <div className='w-full px-2 md:px-0 xl:w-3/5'>
<h3 className="mb-[8px] text-2xl font-semibold">Description</h3> <h3 className='mb-[8px] text-2xl font-semibold'>
<p className="">{propertySpace.description ?? spaceData?.description}</p> Description
<hr className="my-[47px]" /> </h3>
<h3 className="mb-[12px] text-2xl font-semibold">Amenities</h3> <p className=''>
<ul className="addons-grid list-disk-important"> {propertySpace.description ?? spaceData?.description}
</p>
<hr className='my-[47px]' />
<h3 className='mb-[12px] text-2xl font-semibold'>
Amenities
</h3>
<ul className='addons-grid list-disk-important'>
{spaceAmenities.map((am, idx) => ( {spaceAmenities.map((am, idx) => (
<li <li
className="flex w-fit items-center gap-2 mb-4 sm:mb-0" className='mb-4 flex w-fit items-center gap-2 sm:mb-0'
key={idx} key={idx}
> >
<CircleCheckIcon /> <CircleCheckIcon />
@@ -434,58 +563,66 @@ const MySpaceDetailsPage = () => {
</li> </li>
))} ))}
</ul> </ul>
<hr className="my-[47px]" /> <hr className='my-[47px]' />
<h3 className="mb-[8px] text-2xl font-semibold">Add ons</h3> <h3 className='mb-[8px] text-2xl font-semibold'>Add ons</h3>
<ul className="addons-grid list-disk-important"> <ul className='addons-grid list-disk-important'>
{spaceAddons.map((addon) => ( {spaceAddons.map((addon) => (
<li <li
className="flex w-fit sm:w-full items-center gap-2 mb-4 sm:mb-0" className='mb-4 flex w-fit items-center gap-2 sm:mb-0 sm:w-full'
key={addon.id} key={addon.id}
> >
<span className="w-fit"> <span className='w-fit'>
{" "} {" "}
<div className="flex gap-4"> <div className='flex gap-4'>
<CircleCheckIcon /> {addon.add_on_name} <CircleCheckIcon /> {addon.add_on_name}
</div>{" "} </div>{" "}
</span>{" "} </span>{" "}
<strong className="font-semibold">${addon.cost}/h</strong> <strong className='font-semibold'>
${addon.cost}/h
</strong>
</li> </li>
))} ))}
</ul> </ul>
<hr className="my-[47px]" /> <hr className='my-[47px]' />
<div className="mb-[28px] flex flex-wrap items-center justify-between"> <div className='mb-[28px] flex flex-wrap items-center justify-between'>
<h3 className="mb-2 text-xl font-semibold md:mb-0 md:text-2xl">About the host</h3> <h3 className='mb-2 text-xl font-semibold md:mb-0 md:text-2xl'>
About the host
</h3>
</div> </div>
<div className="flex items-center justify-between gap-4 md:justify-start md:gap-[24px]"> <div className='flex items-center justify-between gap-4 md:justify-start md:gap-[24px]'>
<div className="w-max-content"> <div className='w-max-content'>
<img <img
src={hostData.photo ?? "/default.png"} src={hostData.photo ?? "/default.png"}
className="h-[72px] w-[72px] rounded-full object-cover" className='h-[72px] w-[72px] rounded-full object-cover'
/> />
</div> </div>
<div className="space-y-3 w-[90%]"> <div className='w-[90%] space-y-3'>
<div className="flex text-xl font-bold gap-1"> <div className='flex gap-1 text-xl font-bold'>
<p className="md:block">{hostData.first_name}</p> <p className='md:block'>{hostData.first_name}</p>
<p className="md:block">{hostData.last_name}</p> <p className='md:block'>{hostData.last_name}</p>
</div> </div>
<p className="hidden md:block">{propertySpace.about ?? spaceData?.about}</p> <p className='hidden md:block'>
{propertySpace.about ?? spaceData?.about}
</p>
</div> </div>
</div> </div>
<p className="mt-4 block md:hidden">{propertySpace.about ?? spaceData?.about}</p> <p className='mt-4 block md:hidden'>
<hr className="my-[47px]" /> {propertySpace.about ?? spaceData?.about}
<div className="mb-[18px] flex items-center justify-between"> </p>
<h3 className="mb-[8px] text-2xl font-semibold">Reviews</h3> <hr className='my-[47px]' />
<div className='mb-[18px] flex items-center justify-between'>
<h3 className='mb-[8px] text-2xl font-semibold'>Reviews</h3>
<CustomSelect <CustomSelect
options={[ options={[
{ label: "By Date: Newest First", value: "DESC" }, { label: "By Date: Newest First", value: "DESC" },
{ label: "By Date: Oldest First", value: "ASC" }, { label: "By Date: Oldest First", value: "ASC" },
]} ]}
onChange={setReviewDirection} onChange={setReviewDirection}
accessor="label" accessor='label'
valueAccessor="value" valueAccessor='value'
className="min-w-[200px]" className='min-w-[200px]'
listOptionClassName={"pl-4"} listOptionClassName={"pl-4"}
/> />
</div> </div>
@@ -495,15 +632,12 @@ const MySpaceDetailsPage = () => {
.sort(sortByPostDate) .sort(sortByPostDate)
.slice(0, 10) .slice(0, 10)
.map((rw) => ( .map((rw) => (
<ReviewCard <ReviewCard key={rw.id} data={rw} />
key={rw.id}
data={rw}
/>
))} ))}
<div className="text-center"> <div className='text-center'>
{reviews.length > 10 ? ( {reviews.length > 10 ? (
<button <button
className="font-semibold underline" className='font-semibold underline'
onClick={() => setReviewsPopup(true)} onClick={() => setReviewsPopup(true)}
> >
View more ({reviews.length}) View more ({reviews.length})
@@ -511,49 +645,65 @@ const MySpaceDetailsPage = () => {
) : null} ) : null}
</div> </div>
</section> </section>
<hr className="my-[47px]" /> <hr className='my-[47px]' />
<h3 className="mb-[8px] text-2xl font-semibold">FAQs</h3> <h3 className='mb-[8px] text-2xl font-semibold'>FAQs</h3>
{faqs.map((faq) => ( {faqs.map((faq) => (
<FaqAccordion <FaqAccordion key={faq.id} data={faq} />
key={faq.id}
data={faq}
/>
))} ))}
<hr className="my-[47px]" /> <hr className='my-[47px]' />
<h3 className="mb-[8px] text-2xl font-semibold">Property rules</h3> <h3 className='mb-[8px] text-2xl font-semibold'>
<p className="mb-32">{propertySpace.rule ?? spaceData?.rule}</p> Property rules
</h3>
<p className='mb-32'>
{propertySpace.rule ?? spaceData?.rule}
</p>
</div> </div>
<div className="sticky bottom-0 w-full flex-grow bg-white xl:top-16 xl:bottom-[unset] xl:w-[unset]"> <div className='sticky bottom-0 w-full flex-grow bg-white xl:bottom-[unset] xl:top-16 xl:w-[unset]'>
<div className="sticky-price-summary mx-auto max-w-2xl p-6 md:border-2"> <div className='sticky-price-summary mx-auto max-w-2xl p-6 md:border-2'>
<h3 className="mb-[8px] text-2xl font-semibold">Price and availability</h3> <h3 className='mb-[8px] text-2xl font-semibold'>
<div className="mb-[13px] flex justify-between"> Price and availability
<span className="text-lg">Max capacity</span> </h3>
<div className='mb-[13px] flex justify-between'>
<span className='text-lg'>Max capacity</span>
<span> <span>
{" "} {" "}
<strong className="font-semibold">{propertySpace.max_capacity ?? spaceData?.max_capacity}</strong> people <strong className='font-semibold'>
{propertySpace.max_capacity ??
spaceData?.max_capacity}
</strong>{" "}
people
</span> </span>
</div> </div>
<div className="mb-[13px] flex justify-between"> <div className='mb-[13px] flex justify-between'>
<span className="text-lg">Pricing from</span> <span className='text-lg'>Pricing from</span>
<span> <span>
from: <strong className="font-semibold">${propertySpace.rate ?? spaceData?.rate}</strong>/h from:{" "}
<strong className='font-semibold'>
${propertySpace.rate ?? spaceData?.rate}
</strong>
/h
</span> </span>
</div> </div>
{propertySpace.additional_guest_rate && propertySpace.max_capacity > 1 ? ( {propertySpace.additional_guest_rate &&
<div className="mb-[13px] flex justify-between"> propertySpace.max_capacity > 1 ? (
<span className="text-lg">Additional guest</span> <div className='mb-[13px] flex justify-between'>
<span className='text-lg'>Additional guest</span>
<span> <span>
from: <strong className="font-semibold">${propertySpace.additional_guest_rate}</strong>/h from:{" "}
<strong className='font-semibold'>
${propertySpace.additional_guest_rate}
</strong>
/h
</span> </span>
</div> </div>
) : null} ) : null}
<hr className="my-[24px] hidden md:block" /> <hr className='my-[24px] hidden md:block' />
<form <form
className="flex flex-col" className='flex flex-col'
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<div className="z-50 mb-3"> <div className='z-50 mb-3'>
<DateTimePicker <DateTimePicker
register={register} register={register}
setValue={setValue} setValue={setValue}
@@ -562,17 +712,19 @@ const MySpaceDetailsPage = () => {
setShowCalendar={setShowCalendar} setShowCalendar={setShowCalendar}
fromDefault={""} fromDefault={""}
toDefault={""} toDefault={""}
bookedSlots={bookedSlots.map((slot) => ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} bookedSlots={bookedSlots}
scheduleTemplate={scheduleTemplate} scheduleTemplate={scheduleTemplate}
defaultMessage="Check Availability" defaultMessage='Check Availability'
/> />
</div> </div>
<button <button
type="submit" type='submit'
className="login-btn-gradient gap-2 rounded-tr rounded-br py-3 px-2 text-center tracking-wide text-white outline-none focus:outline-none" className='login-btn-gradient gap-2 rounded-br rounded-tr px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
disabled={true} disabled={true}
> >
{window.innerWidth > 500 ? "Continue" : "Check Availability"} {window.innerWidth > 500
? "Continue"
: "Check Availability"}
</button> </button>
</form> </form>
</div> </div>
@@ -601,8 +753,15 @@ const MySpaceDetailsPage = () => {
</Tab.Group> </Tab.Group>
<PropertySpaceMapImage <PropertySpaceMapImage
modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${propertySpace.address_line_1 || ""}, ${propertySpace.address_line_2 || ""}, ${propertySpace.city || ""}, ${propertySpace.country || "" modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${
}&zoom=15&size=600x400&maptype=roadmap&markers=color:red|${propertySpace.address_line_1 || ""}, ${propertySpace.address_line_2 || ""} propertySpace.address_line_1 || ""
}, ${propertySpace.address_line_2 || ""}, ${
propertySpace.city || ""
}, ${
propertySpace.country || ""
}&zoom=15&size=600x400&maptype=roadmap&markers=color:red|${
propertySpace.address_line_1 || ""
}, ${propertySpace.address_line_2 || ""}
&key=${import.meta.env.VITE_GOOGLE_API_KEY}`} &key=${import.meta.env.VITE_GOOGLE_API_KEY}`}
modalOpen={showMap} modalOpen={showMap}
closeModal={() => setShowMap(false)} closeModal={() => setShowMap(false)}