2025-01-24 20:05:48 +01:00
|
|
|
import React, { useContext, useEffect } from "react";
|
|
|
|
|
import { useState } from "react";
|
2025-07-06 13:11:55 +01:00
|
|
|
import {
|
|
|
|
|
Link,
|
|
|
|
|
Navigate,
|
|
|
|
|
useLocation,
|
|
|
|
|
useNavigate,
|
|
|
|
|
useParams,
|
|
|
|
|
} from "react-router-dom";
|
2025-01-24 20:05:48 +01:00
|
|
|
import FaqAccordion from "@/components/frontend/FaqAccordion";
|
|
|
|
|
import ReviewCard from "@/components/frontend/ReviewCard";
|
|
|
|
|
|
|
|
|
|
import StarIcon from "@/components/frontend/icons/StarIcon";
|
|
|
|
|
import MkdSDK from "@/utils/MkdSDK";
|
|
|
|
|
import { callCustomAPI } from "@/utils/callCustomAPI";
|
|
|
|
|
import DateTimePicker from "@/components/frontend/DateTimePicker";
|
|
|
|
|
import { useForm } from "react-hook-form";
|
|
|
|
|
import { useBookingContext } from "./bookingContext";
|
|
|
|
|
import CustomSelect from "@/components/frontend/CustomSelect";
|
|
|
|
|
import { GlobalContext } from "@/globalContext";
|
|
|
|
|
import FavoriteButton from "@/components/frontend/FavoriteButton";
|
|
|
|
|
import Counter from "@/components/frontend/Counter";
|
|
|
|
|
import { Tooltip } from "react-tooltip";
|
2025-07-06 13:11:55 +01:00
|
|
|
import {
|
|
|
|
|
usePropertyAddons,
|
|
|
|
|
usePropertySpace,
|
|
|
|
|
usePropertySpaceImages,
|
|
|
|
|
usePublicUserData,
|
|
|
|
|
usePropertySpaceAmenities,
|
|
|
|
|
usePropertySpaceFaqs,
|
|
|
|
|
usePropertySpaceReviews,
|
|
|
|
|
} from "@/hooks/api";
|
2025-01-24 20:05:48 +01:00
|
|
|
import PropertyImageSlider from "@/components/frontend/PropertyImageSlider";
|
|
|
|
|
import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage";
|
|
|
|
|
import AllReviewsModal from "@/components/frontend/AllReviewsModal";
|
|
|
|
|
import { AuthContext } from "@/authContext";
|
|
|
|
|
import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon";
|
|
|
|
|
|
|
|
|
|
let sdk = new MkdSDK();
|
|
|
|
|
|
|
|
|
|
const PropertyPage = () => {
|
2025-07-06 13:11:55 +01:00
|
|
|
const { dispatch: globalDispatch, state: globalState } =
|
|
|
|
|
useContext(GlobalContext);
|
2025-01-24 20:05:48 +01:00
|
|
|
const { state: authState, dispatch: authDispatch } = useContext(AuthContext);
|
|
|
|
|
const { state: spaceData } = useLocation();
|
|
|
|
|
const { bookingData, dispatch } = useBookingContext();
|
|
|
|
|
const [galleryOpen, setGalleryOpen] = useState(false);
|
|
|
|
|
const [reviewsPopup, setReviewsPopup] = useState(false);
|
|
|
|
|
const [fetching, setFetching] = useState(true);
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { id } = useParams();
|
2025-07-06 13:11:55 +01:00
|
|
|
const bookingDetails =
|
|
|
|
|
bookingData?.from === ""
|
|
|
|
|
? bookingData
|
|
|
|
|
: JSON.parse(localStorage.getItem("booking_details"));
|
2025-01-24 20:05:48 +01:00
|
|
|
const [reviewDirection, setReviewDirection] = useState("DESC");
|
|
|
|
|
const [bookedSlots, setBookedSlots] = useState([]);
|
|
|
|
|
const [scheduleTemplate, setScheduleTemplate] = useState({});
|
|
|
|
|
const [render, forceRender] = useState(false);
|
|
|
|
|
const { handleSubmit, register, setValue } = useForm({
|
|
|
|
|
defaultValues: bookingDetails,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const [showCalendar, setShowCalendar] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { propertySpace, notFound } = usePropertySpace(id, render);
|
|
|
|
|
const hostData = usePublicUserData(propertySpace.host_id);
|
2025-07-06 13:11:55 +01:00
|
|
|
const spaceImages = usePropertySpaceImages(
|
|
|
|
|
propertySpace.id,
|
|
|
|
|
true,
|
|
|
|
|
setFetching
|
|
|
|
|
);
|
2025-01-24 20:05:48 +01:00
|
|
|
const spaceAddons = usePropertyAddons(propertySpace.property_id);
|
|
|
|
|
const spaceAmenities = usePropertySpaceAmenities(propertySpace.id);
|
|
|
|
|
const faqs = usePropertySpaceFaqs(propertySpace.id);
|
|
|
|
|
const reviews = usePropertySpaceReviews(propertySpace.id);
|
|
|
|
|
const [showMap, setShowMap] = useState(false);
|
|
|
|
|
const { pathname } = useLocation();
|
|
|
|
|
|
|
|
|
|
if (!fetching && spaceImages.length === 0) {
|
2025-07-06 13:11:55 +01:00
|
|
|
navigate("*");
|
2025-01-24 20:05:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchBookedSlots(id) {
|
|
|
|
|
try {
|
2025-07-06 13:11:55 +01:00
|
|
|
const result = await callCustomAPI(
|
|
|
|
|
"customer/schedule",
|
|
|
|
|
"post",
|
|
|
|
|
{ property_spaces_id: id },
|
|
|
|
|
"",
|
|
|
|
|
null,
|
|
|
|
|
"v3"
|
|
|
|
|
);
|
2025-01-24 20:05:48 +01:00
|
|
|
if (Array.isArray(result.list)) {
|
|
|
|
|
setBookedSlots(result.list);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
globalDispatch({
|
|
|
|
|
type: "SHOW_ERROR",
|
|
|
|
|
payload: {
|
|
|
|
|
heading: "Operation failed",
|
|
|
|
|
message: err.message,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchScheduleTemplate(id) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await callCustomAPI(
|
|
|
|
|
"property_spaces_schedule_template",
|
|
|
|
|
"post",
|
|
|
|
|
{
|
|
|
|
|
page: 1,
|
|
|
|
|
limit: 1,
|
|
|
|
|
where: [`property_spaces_id = ${id}`],
|
|
|
|
|
},
|
2025-07-06 13:11:55 +01:00
|
|
|
"PAGINATE"
|
2025-01-24 20:05:48 +01:00
|
|
|
);
|
|
|
|
|
if (Array.isArray(result.list) && result.list.length > 0) {
|
|
|
|
|
setScheduleTemplate({ custom_slots: result.list[0].custom_slots });
|
|
|
|
|
}
|
|
|
|
|
if (result.list[0]?.schedule_template_id) {
|
|
|
|
|
const templateResult = await callCustomAPI(
|
|
|
|
|
"schedule_template",
|
|
|
|
|
"post",
|
|
|
|
|
{
|
|
|
|
|
page: 1,
|
|
|
|
|
limit: 1,
|
|
|
|
|
where: [`id = ${result.list[0].schedule_template_id}`],
|
|
|
|
|
},
|
2025-07-06 13:11:55 +01:00
|
|
|
"PAGINATE"
|
2025-01-24 20:05:48 +01:00
|
|
|
);
|
2025-07-06 13:11:55 +01:00
|
|
|
if (
|
|
|
|
|
Array.isArray(templateResult.list) &&
|
|
|
|
|
(templateResult.list[0] ?? {})
|
|
|
|
|
) {
|
2025-01-24 20:05:48 +01:00
|
|
|
setScheduleTemplate((prev) => {
|
|
|
|
|
let updated = { ...prev, ...templateResult.list[0] };
|
|
|
|
|
return updated;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
globalDispatch({
|
|
|
|
|
type: "SHOW_ERROR",
|
|
|
|
|
payload: {
|
|
|
|
|
heading: "Operation failed",
|
|
|
|
|
message: err.message,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data) => {
|
|
|
|
|
if (!authState.isAuthenticated) {
|
|
|
|
|
navigate(`/login?redirect_uri=${pathname}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (authState.user == propertySpace.host_id) {
|
2025-07-06 13:11:55 +01:00
|
|
|
globalDispatch({
|
|
|
|
|
type: "SHOW_ERROR",
|
|
|
|
|
payload: {
|
|
|
|
|
heading: "error",
|
|
|
|
|
message: "Owners can't book their own spaces",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return;
|
2025-01-24 20:05:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (globalState.user.verificationStatus != 1) {
|
|
|
|
|
globalDispatch({ type: "OPEN_NOT_VERIFIED_MODAL" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-06 13:11:55 +01:00
|
|
|
dispatch({
|
|
|
|
|
type: "SET_BOOKING_DETAILS",
|
|
|
|
|
payload: { ...data, ...propertySpace },
|
|
|
|
|
});
|
2025-01-24 20:05:48 +01:00
|
|
|
navigate("booking-preview");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isNaN(id)) return;
|
|
|
|
|
fetchBookedSlots(id);
|
|
|
|
|
fetchScheduleTemplate(id);
|
|
|
|
|
|
|
|
|
|
// if (spaceImages.length === 0) {
|
|
|
|
|
// navigate("*")
|
|
|
|
|
// }
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const sortByPostDate = (a, b) => {
|
|
|
|
|
if (reviewDirection == "DESC") {
|
|
|
|
|
return new Date(b.post_date) - new Date(a.post_date);
|
|
|
|
|
}
|
|
|
|
|
return new Date(a.post_date) - new Date(b.post_date);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-06 13:11:55 +01:00
|
|
|
if (notFound || isNaN(id)) return <Navigate to='/not-found' />;
|
2025-01-24 20:05:48 +01:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2025-07-06 13:11:55 +01:00
|
|
|
className='container mx-auto min-h-screen pt-[140px] text-sm normal-case md:text-base 2xl:px-16'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={() => {
|
|
|
|
|
setShowCalendar(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-07-06 13:11:55 +01:00
|
|
|
<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'>
|
|
|
|
|
<h2 className='text-3xl font-semibold'>
|
|
|
|
|
{propertySpace.name ?? spaceData?.name}
|
|
|
|
|
</h2>
|
2025-01-24 20:05:48 +01:00
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
className='whitespace-nowrap text-sm underline'
|
|
|
|
|
target='_blank'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={() => setShowMap(true)}
|
|
|
|
|
>
|
|
|
|
|
(view on map)
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<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]'>
|
2025-01-24 20:05:48 +01:00
|
|
|
<StarIcon />
|
2025-07-06 13:11:55 +01:00
|
|
|
<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>
|
2025-01-24 20:05:48 +01:00
|
|
|
</strong>
|
|
|
|
|
</p>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='flex flex-grow items-center justify-center gap-2 rounded-sm border bg-[#F3F9F7] px-[14px] py-[10px]'>
|
2025-01-24 20:05:48 +01:00
|
|
|
<FavoriteButton
|
|
|
|
|
space_id={propertySpace.id}
|
|
|
|
|
user_property_spaces_id={propertySpace.user_property_spaces_id}
|
|
|
|
|
reRender={forceRender}
|
|
|
|
|
withLoader={true}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='-mb-1'
|
|
|
|
|
buttonClassName=''
|
|
|
|
|
stroke='#344054'
|
2025-01-24 20:05:48 +01:00
|
|
|
favColor={"black"}
|
|
|
|
|
/>
|
|
|
|
|
<span>Save</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='snap-scroll relative mb-[66px] flex h-[381px] gap-[32px] px-[14px] md:px-0'>
|
|
|
|
|
{spaceImages[0]?.photo_url && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<img
|
|
|
|
|
src={spaceImages[0]?.photo_url}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='h-full rounded-lg object-cover xl:min-w-[616px]'
|
2025-01-24 20:05:48 +01:00
|
|
|
/>
|
2025-07-06 13:11:55 +01:00
|
|
|
)}
|
|
|
|
|
{spaceImages[1]?.photo_url && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<img
|
|
|
|
|
src={spaceImages[1]?.photo_url}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='h-full w-[292px] rounded-lg object-cover'
|
2025-01-24 20:05:48 +01:00
|
|
|
/>
|
2025-07-06 13:11:55 +01:00
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
className={`${
|
|
|
|
|
!spaceImages[3]?.photo_url ? "flex min-w-[550px] flex-col" : "block"
|
|
|
|
|
} "gap-4 md:gap-[32px]" overflow-hidden`}
|
|
|
|
|
>
|
|
|
|
|
{spaceImages[2]?.photo_url && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<img
|
|
|
|
|
src={spaceImages[2]?.photo_url}
|
2025-07-06 13:11:55 +01:00
|
|
|
className={`${
|
|
|
|
|
spaceImages[3]?.photo_url && "h-1/2"
|
|
|
|
|
} "rounded-lg md:w-full" object-cover`}
|
2025-01-24 20:05:48 +01:00
|
|
|
/>
|
2025-07-06 13:11:55 +01:00
|
|
|
)}
|
|
|
|
|
{spaceImages[3]?.photo_url && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<img
|
|
|
|
|
src={spaceImages[3]?.photo_url}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='h-1/2 rounded-lg object-cover md:w-full'
|
2025-01-24 20:05:48 +01:00
|
|
|
/>
|
2025-07-06 13:11:55 +01:00
|
|
|
)}
|
2025-01-24 20:05:48 +01:00
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
{spaceImages[4]?.photo_url && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<img
|
|
|
|
|
src={spaceImages[4]?.photo_url}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='h-full w-[292px] rounded-lg object-cover'
|
2025-01-24 20:05:48 +01:00
|
|
|
/>
|
2025-07-06 13:11:55 +01:00
|
|
|
)}
|
2025-01-24 20:05:48 +01:00
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
className='sticky right-6 mb-[8px] min-w-[170px] self-end border bg-[#00000080] px-3 py-1 text-center text-sm text-white'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={() => setGalleryOpen(true)}
|
|
|
|
|
>
|
|
|
|
|
View all photos ({spaceImages.length})
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<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 px-10 md:px-0 xl:w-3/5'>
|
|
|
|
|
<h3 className='mb-[8px] text-2xl font-semibold'>Description</h3>
|
|
|
|
|
<p className=''>
|
|
|
|
|
{propertySpace.description ?? spaceData?.description}
|
|
|
|
|
</p>
|
|
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<h3 className='mb-[8px] text-2xl font-semibold'>Amenities</h3>
|
|
|
|
|
<ul className='addons-grid list-disk-important'>
|
2025-01-24 20:05:48 +01:00
|
|
|
{spaceAmenities.map((am, idx) => (
|
|
|
|
|
<li
|
2025-07-06 13:11:55 +01:00
|
|
|
className='mb-4 flex w-fit items-center gap-2 sm:mb-0'
|
2025-01-24 20:05:48 +01:00
|
|
|
key={idx}
|
|
|
|
|
>
|
|
|
|
|
<CircleCheckIcon />
|
|
|
|
|
{am.amenity_name}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<h3 className='mb-[8px] text-2xl font-semibold'>Add ons</h3>
|
|
|
|
|
<ul className='addons-grid list-disk-important'>
|
2025-01-24 20:05:48 +01:00
|
|
|
{spaceAddons.map((addon) => (
|
|
|
|
|
<li
|
2025-07-06 13:11:55 +01:00
|
|
|
className='mb-4 flex w-fit items-center gap-2 sm:mb-0 sm:w-full'
|
2025-01-24 20:05:48 +01:00
|
|
|
key={addon.id}
|
|
|
|
|
>
|
2025-07-06 13:11:55 +01:00
|
|
|
<span className='w-fit'>
|
2025-01-24 20:05:48 +01:00
|
|
|
{" "}
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='flex gap-4'>
|
2025-01-24 20:05:48 +01:00
|
|
|
<CircleCheckIcon /> {addon.add_on_name}
|
|
|
|
|
</div>{" "}
|
|
|
|
|
</span>{" "}
|
2025-07-06 13:11:55 +01:00
|
|
|
<strong className='font-semibold'>${addon.cost}/h</strong>
|
2025-01-24 20:05:48 +01:00
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<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>
|
|
|
|
|
{authState.role == "customer" && propertySpace?.id && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<Link
|
|
|
|
|
to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`}
|
2025-07-06 13:11:55 +01:00
|
|
|
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'
|
2025-01-24 20:05:48 +01:00
|
|
|
>
|
|
|
|
|
Contact the host
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='flex items-center justify-between gap-4 md:justify-start md:gap-[24px]'>
|
|
|
|
|
<div className='w-max-content'>
|
|
|
|
|
<img
|
|
|
|
|
src={hostData.photo ?? "/default.png"}
|
|
|
|
|
className='h-[72px] w-[72px] rounded-full object-cover'
|
|
|
|
|
/>
|
2025-01-24 20:05:48 +01:00
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='w-[90%] space-y-3'>
|
|
|
|
|
<p className='hidden text-xl font-bold md:block'>
|
|
|
|
|
{propertySpace.first_name}
|
|
|
|
|
</p>
|
|
|
|
|
<p className='hidden md:block'>
|
|
|
|
|
{propertySpace.about ?? spaceData?.about}
|
|
|
|
|
</p>
|
2025-01-24 20:05:48 +01:00
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
{authState.role == "customer" && propertySpace?.id && (
|
2025-01-24 20:05:48 +01:00
|
|
|
<Link
|
|
|
|
|
to={`/account/messages?other_user_id=${propertySpace.host_id}&space=${propertySpace.id}`}
|
2025-07-06 13:11:55 +01:00
|
|
|
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'
|
2025-01-24 20:05:48 +01:00
|
|
|
>
|
|
|
|
|
Contact the host
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<p className='mt-4 block md:hidden'>
|
|
|
|
|
{propertySpace.about ?? spaceData?.about}
|
|
|
|
|
</p>
|
2025-01-24 20:05:48 +01:00
|
|
|
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<div className='mb-[18px] flex items-center justify-between'>
|
|
|
|
|
<h3 className='mb-[8px] text-2xl font-semibold'>Reviews</h3>
|
2025-01-24 20:05:48 +01:00
|
|
|
<CustomSelect
|
|
|
|
|
options={[
|
|
|
|
|
{ label: "By Date: Newest First", value: "DESC" },
|
|
|
|
|
{ label: "By Date: Oldest First", value: "ASC" },
|
|
|
|
|
]}
|
|
|
|
|
onChange={setReviewDirection}
|
2025-07-06 13:11:55 +01:00
|
|
|
accessor='label'
|
|
|
|
|
valueAccessor='value'
|
|
|
|
|
className='min-w-[200px]'
|
2025-01-24 20:05:48 +01:00
|
|
|
listOptionClassName={"pl-4"}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<section>
|
|
|
|
|
{reviews.length == 0 && <p>No reviews yet</p>}
|
|
|
|
|
{reviews
|
|
|
|
|
.sort(sortByPostDate)
|
|
|
|
|
.slice(0, 10)
|
|
|
|
|
.map((rw) => (
|
2025-07-06 13:11:55 +01:00
|
|
|
<ReviewCard key={rw.id} data={rw} />
|
2025-01-24 20:05:48 +01:00
|
|
|
))}
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='text-center'>
|
2025-01-24 20:05:48 +01:00
|
|
|
{reviews.length > 10 ? (
|
|
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
className='font-semibold underline'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={() => setReviewsPopup(true)}
|
|
|
|
|
>
|
|
|
|
|
View more ({reviews.length})
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<h3 className='mb-[8px] text-2xl font-semibold'>FAQs</h3>
|
2025-01-24 20:05:48 +01:00
|
|
|
{faqs.map((faq) => (
|
2025-07-06 13:11:55 +01:00
|
|
|
<FaqAccordion key={faq.id} data={faq} />
|
2025-01-24 20:05:48 +01:00
|
|
|
))}
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='my-[32px] md:my-[47px]' />
|
|
|
|
|
<h3 className='mb-4 text-2xl font-semibold'>Property rules</h3>
|
|
|
|
|
<p className='mb-32'>{propertySpace.rule ?? spaceData?.rule}</p>
|
2025-01-24 20:05:48 +01:00
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<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'>
|
|
|
|
|
<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>
|
2025-01-24 20:05:48 +01:00
|
|
|
<span>
|
|
|
|
|
{" "}
|
2025-07-06 13:11:55 +01:00
|
|
|
<strong className='font-semibold'>
|
|
|
|
|
{propertySpace.max_capacity ?? spaceData?.max_capacity}
|
|
|
|
|
</strong>{" "}
|
|
|
|
|
people
|
2025-01-24 20:05:48 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='mb-[13px] flex justify-between'>
|
|
|
|
|
<span className='text-lg'>Pricing from</span>
|
2025-01-24 20:05:48 +01:00
|
|
|
<span>
|
2025-07-06 13:11:55 +01:00
|
|
|
from:{" "}
|
|
|
|
|
<strong className='font-semibold'>
|
|
|
|
|
${propertySpace.rate ?? spaceData?.rate}
|
|
|
|
|
</strong>
|
|
|
|
|
/h
|
2025-01-24 20:05:48 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-06 13:11:55 +01:00
|
|
|
<form className='flex flex-col' onSubmit={handleSubmit(onSubmit)}>
|
|
|
|
|
<div className='mb-[13px] flex items-center justify-between'>
|
|
|
|
|
<span className='text-lg'>Number of guests</span>
|
2025-01-24 20:05:48 +01:00
|
|
|
<Counter
|
|
|
|
|
register={register}
|
2025-07-06 13:11:55 +01:00
|
|
|
name='num_guests'
|
2025-01-24 20:05:48 +01:00
|
|
|
setValue={setValue}
|
|
|
|
|
initialValue={bookingDetails.num_guests || 1}
|
2025-07-06 13:11:55 +01:00
|
|
|
maxCount={
|
|
|
|
|
propertySpace.max_capacity ?? spaceData?.max_capacity
|
|
|
|
|
}
|
2025-01-24 20:05:48 +01:00
|
|
|
minCount={1}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='mb-[24px] hidden md:block' />
|
|
|
|
|
<div className='z-50 mb-3'>
|
2025-01-24 20:05:48 +01:00
|
|
|
<DateTimePicker
|
|
|
|
|
register={register}
|
|
|
|
|
setValue={setValue}
|
|
|
|
|
fieldNames={["selectedDate", "from", "to"]}
|
|
|
|
|
showCalendar={showCalendar}
|
|
|
|
|
setShowCalendar={setShowCalendar}
|
|
|
|
|
fromDefault={bookingDetails.from}
|
|
|
|
|
toDefault={bookingDetails.to}
|
2025-07-06 13:11:55 +01:00
|
|
|
bookedSlots={bookedSlots}
|
2025-01-24 20:05:48 +01:00
|
|
|
scheduleTemplate={scheduleTemplate}
|
|
|
|
|
defaultDate={bookingDetails.selectedDate || undefined}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{authState.role != "customer" && authState.isAuthenticated ? (
|
|
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
type='button'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={switchToCustomer}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
|
2025-01-24 20:05:48 +01:00
|
|
|
>
|
|
|
|
|
Join as customer to book
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
type='submit'
|
|
|
|
|
id='proceed-to-preview'
|
|
|
|
|
className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
|
2025-01-24 20:05:48 +01:00
|
|
|
disabled={(() => {
|
|
|
|
|
const el = document.getElementById("booking-time");
|
|
|
|
|
return !(el && !el.innerText.includes("Select"));
|
|
|
|
|
})()}
|
|
|
|
|
>
|
2025-07-06 13:11:55 +01:00
|
|
|
{window.innerWidth > 500
|
|
|
|
|
? "Continue"
|
|
|
|
|
: "Continue to checkout"}
|
2025-01-24 20:05:48 +01:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<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>
|
|
|
|
|
<div className='mb-[13px] flex justify-between'>
|
|
|
|
|
<span className='text-lg'>Max capacity</span>
|
2025-01-24 20:05:48 +01:00
|
|
|
<span>
|
|
|
|
|
{" "}
|
2025-07-06 13:11:55 +01:00
|
|
|
<strong className='font-semibold'>
|
|
|
|
|
{propertySpace.max_capacity ?? spaceData?.max_capacity}
|
|
|
|
|
</strong>{" "}
|
|
|
|
|
people
|
2025-01-24 20:05:48 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<div className='mb-[13px] flex justify-between'>
|
|
|
|
|
<span className='text-lg'>Pricing from</span>
|
2025-01-24 20:05:48 +01:00
|
|
|
<span>
|
2025-07-06 13:11:55 +01:00
|
|
|
from:{" "}
|
|
|
|
|
<strong className='font-semibold'>
|
|
|
|
|
${propertySpace.rate ?? spaceData?.rate}
|
|
|
|
|
</strong>
|
|
|
|
|
/h
|
2025-01-24 20:05:48 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-06 13:11:55 +01:00
|
|
|
<form className='flex flex-col' onSubmit={handleSubmit(onSubmit)}>
|
|
|
|
|
<div className='mb-[13px] flex items-center justify-between'>
|
|
|
|
|
<span className='text-lg'>Number of guests</span>
|
2025-01-24 20:05:48 +01:00
|
|
|
<Counter
|
|
|
|
|
register={register}
|
2025-07-06 13:11:55 +01:00
|
|
|
name='num_guests'
|
2025-01-24 20:05:48 +01:00
|
|
|
setValue={setValue}
|
|
|
|
|
initialValue={bookingDetails.num_guests || 1}
|
|
|
|
|
maxCount={propertySpace.max_capacity ?? spaceData?.max_capacity}
|
|
|
|
|
minCount={1}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-06 13:11:55 +01:00
|
|
|
<hr className='mb-[24px] hidden md:block' />
|
|
|
|
|
<div className='z-50 mb-3'>
|
2025-01-24 20:05:48 +01:00
|
|
|
<DateTimePicker
|
|
|
|
|
register={register}
|
|
|
|
|
setValue={setValue}
|
|
|
|
|
fieldNames={["selectedDate", "from", "to"]}
|
|
|
|
|
showCalendar={showCalendar}
|
|
|
|
|
setShowCalendar={setShowCalendar}
|
|
|
|
|
fromDefault={bookingDetails.from}
|
|
|
|
|
toDefault={bookingDetails.to}
|
2025-07-06 13:11:55 +01:00
|
|
|
bookedSlots={bookedSlots}
|
2025-01-24 20:05:48 +01:00
|
|
|
scheduleTemplate={scheduleTemplate}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{authState.role != "customer" && authState.isAuthenticated ? (
|
|
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
type='button'
|
2025-01-24 20:05:48 +01:00
|
|
|
onClick={switchToCustomer}
|
2025-07-06 13:11:55 +01:00
|
|
|
className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
|
2025-01-24 20:05:48 +01:00
|
|
|
>
|
|
|
|
|
Join as customer to book
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
2025-07-06 13:11:55 +01:00
|
|
|
type='submit'
|
|
|
|
|
id='proceed-to-preview'
|
|
|
|
|
className='login-btn-gradient gap-2 rounded-sm px-2 py-3 text-center tracking-wide text-white outline-none focus:outline-none'
|
2025-01-24 20:05:48 +01:00
|
|
|
disabled={(() => {
|
|
|
|
|
const els = document.querySelectorAll("#booking-time");
|
2025-07-06 13:11:55 +01:00
|
|
|
return Array.from(els).every((el) =>
|
|
|
|
|
el.innerText.includes("Select")
|
|
|
|
|
);
|
2025-01-24 20:05:48 +01:00
|
|
|
})()}
|
|
|
|
|
>
|
|
|
|
|
{window.innerWidth > 500 && "Continue"}
|
|
|
|
|
{window.innerWidth < 500 && "Continue to checkout"}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
<PropertyImageSlider
|
|
|
|
|
spaceImages={spaceImages}
|
|
|
|
|
modalOpen={galleryOpen}
|
|
|
|
|
closeModal={() => setGalleryOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
<AllReviewsModal
|
|
|
|
|
modalOpen={reviewsPopup}
|
|
|
|
|
closeModal={() => setReviewsPopup(false)}
|
|
|
|
|
reviews={reviews}
|
|
|
|
|
onDirectionChange={setReviewDirection}
|
|
|
|
|
/>
|
|
|
|
|
<Tooltip
|
2025-07-06 13:11:55 +01:00
|
|
|
anchorId='proceed-to-preview'
|
|
|
|
|
place='bottom'
|
2025-01-24 20:05:48 +01:00
|
|
|
content={"Proceed to book"}
|
|
|
|
|
noArrow
|
|
|
|
|
/>
|
|
|
|
|
<Tooltip
|
2025-07-06 13:11:55 +01:00
|
|
|
anchorId='contact-host'
|
|
|
|
|
place='bottom'
|
2025-01-24 20:05:48 +01:00
|
|
|
content={"Chat with Host"}
|
|
|
|
|
noArrow
|
|
|
|
|
/>
|
|
|
|
|
<PropertySpaceMapImage
|
2025-07-06 13:11:55 +01:00
|
|
|
modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${
|
|
|
|
|
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 ?? ""}
|
2025-01-24 20:05:48 +01:00
|
|
|
&key=${import.meta.env.VITE_GOOGLE_API_KEY}`}
|
|
|
|
|
modalOpen={showMap}
|
|
|
|
|
closeModal={() => setShowMap(false)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default PropertyPage;
|