557 lines
19 KiB
React
557 lines
19 KiB
React
import React from "react";
|
|
import { useEffect } from "react";
|
|
import { useState } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import PropertySpaceTile from "@/components/frontend/PropertySpaceTile";
|
|
import "react-calendar/dist/Calendar.css";
|
|
import { callCustomAPI } from "@/utils/callCustomAPI";
|
|
import PeopleIcon from "@/components/frontend/icons/PeopleIcon";
|
|
import { GlobalContext } from "@/globalContext";
|
|
import { useContext } from "react";
|
|
import CustomSelect from "@/components/frontend/CustomSelect";
|
|
import FilterIcon from "@/components/frontend/icons/FilterIcon";
|
|
import { Tooltip } from "react-tooltip";
|
|
import NoteIcon from "@/components/frontend/icons/NoteIcon";
|
|
import { formatDate } from "@/utils/date-time-utils";
|
|
import { DRAFT_STATUS, SPACE_STATUS } from "@/utils/constants";
|
|
import { useForm } from "react-hook-form";
|
|
// import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2";
|
|
import DatePickerV3 from "@/components/DatePickerV3";
|
|
import { isValidDate, parseSearchParams } from "@/utils/utils";
|
|
import FilterCheckBoxesV2 from "@/components/FilterCheckBoxesV2";
|
|
import MkdSDK from "@/utils/MkdSDK";
|
|
import useAmenityCategories from "@/hooks/api/useAmenityCategories";
|
|
import CustomStaticLocationAutoCompleteV2 from "@/components/CustomStaticLocationAutoCompleteV2";
|
|
|
|
const prices = [
|
|
{ name: "$0 - $30", id: 0 },
|
|
{ name: "$31 - $60", id: 1 },
|
|
{ name: "$60 - $90", id: 2 },
|
|
{ name: "$90 - $120", id: 3 },
|
|
{ name: "$120 - $150", id: 4 },
|
|
{ name: "$150 - $180", id: 5 },
|
|
];
|
|
const capacity = [
|
|
{ name: "0 - 4", id: 0 },
|
|
{ name: "5 - 9", id: 1 },
|
|
{ name: "10 - 14", id: 2 },
|
|
{ name: "15 - 19", id: 3 },
|
|
{ name: "20 - 24", id: 4 },
|
|
{ name: "25 - 30", id: 5 },
|
|
{ name: "Greater Than 30", id: 6 },
|
|
];
|
|
|
|
const reviews = [
|
|
{ name: "4", id: 0 },
|
|
{ name: "3", id: 1 },
|
|
{ name: "2", id: 2 },
|
|
{ name: "1", id: 3 },
|
|
];
|
|
|
|
const sdk = new MkdSDK();
|
|
const ctrl = new AbortController();
|
|
|
|
const SearchPage = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const { dispatch: globalDispatch, state: globalState } =
|
|
useContext(GlobalContext);
|
|
|
|
const [filterPopup, setFilterPopup] = useState(false);
|
|
|
|
const spaceCategories = globalState.spaceCategories;
|
|
const amenityCategories = useAmenityCategories();
|
|
const [propertySpaces, setPropertySpaces] = useState([]);
|
|
const [render, forceRender] = useState(false);
|
|
|
|
const { handleSubmit, control, setValue, resetField, register } = useForm({
|
|
defaultValues: (() => {
|
|
const params = parseSearchParams(searchParams);
|
|
return {
|
|
...params,
|
|
location: params.location ?? "",
|
|
booking_start_time: "",
|
|
category: params.category?.split(",") || [],
|
|
capacity: params.capacity?.split(",") || [],
|
|
price: params.price?.split(",") || [],
|
|
amenity: params.amenity?.split(",") || [],
|
|
review: params.review?.split(",") || [],
|
|
};
|
|
})(),
|
|
});
|
|
const [sortAsc, setSortAsc] = useState(false);
|
|
|
|
async function fetchSpaces() {
|
|
const params = parseSearchParams(searchParams);
|
|
const location = params.location?.split(",");
|
|
const d = new Date(params.booking_start_time || undefined);
|
|
|
|
const filter = {
|
|
...params,
|
|
booking_start_time: isNaN(d) ? undefined : d,
|
|
category: params.category?.split(",") || [],
|
|
price: params.price?.split(",") || [],
|
|
capacity: params.capacity?.split(",") || [],
|
|
amenity: params.amenity?.split(",") || [],
|
|
review: params.review?.split(",") || [],
|
|
};
|
|
globalDispatch({ type: "START_LOADING" });
|
|
|
|
// make sure only approved and non-draft spaces
|
|
var where = [
|
|
`ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND schedule_template_id IS NOT NULL AND ergo_property_spaces_images.is_approved = 1 AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.deleted_at IS NULL`,
|
|
];
|
|
|
|
// use data.location to search address, city, country and zip
|
|
if (filter.location) {
|
|
where.push(
|
|
`(ergo_property.address_line_1 LIKE '%${
|
|
filter.location
|
|
}%' OR ergo_property.address_line_2 LIKE '%${
|
|
filter.location
|
|
}%' OR ergo_property.city LIKE '%${
|
|
location[0] && location[0]
|
|
}%' OR ergo_property.country LIKE '%${
|
|
location.length === 1
|
|
? location[0]
|
|
: location.length === 2
|
|
? location[1]
|
|
: location[2]
|
|
}%' OR ergo_property.zip LIKE '%${
|
|
filter.location
|
|
}%' OR ergo_property.name LIKE '%${filter.location}%')`
|
|
);
|
|
}
|
|
|
|
if (filter.size) {
|
|
where.push(`ergo_property_spaces.size = ${filter.size}`);
|
|
}
|
|
|
|
if (filter.capacity.length > 0) {
|
|
if (filter.capacity[filter.capacity.length - 1] !== "Greater Than 30") {
|
|
const str = filter.capacity[filter.capacity.length - 1]; // Get the first (and only) element from the array
|
|
const numbers = str.split("-").map((num) => num.trim()); // Split the string and trim spaces
|
|
const [num1, num2] = numbers; // Destructure the resulting array to get the numbers
|
|
where.pop();
|
|
where.push(
|
|
`ergo_property_spaces.max_capacity BETWEEN ${num1} AND ${num2}`
|
|
);
|
|
} else {
|
|
where.push(`ergo_property_spaces.max_capacity > 30`);
|
|
}
|
|
}
|
|
|
|
if (filter.category.length > 0) {
|
|
where.push(
|
|
`(${filter.category
|
|
.map((cg) => `ergo_spaces.category LIKE '%${cg}%'`)
|
|
.join(" OR ")})`
|
|
);
|
|
}
|
|
|
|
if (filter.amenity.length > 0) {
|
|
where.push(
|
|
`(${filter.amenity
|
|
.map((am) => `ergo_amenity.name LIKE '%${am}%'`)
|
|
.join(" OR ")})`
|
|
);
|
|
}
|
|
|
|
if (filter.review.length > 0) {
|
|
where.push(
|
|
`(${filter.review
|
|
.map((rv) => `ER.average_space_rating >= ${rv.replace("+", "")}`)
|
|
.join(" OR ")})`
|
|
);
|
|
}
|
|
|
|
if (filter.price.length > 0) {
|
|
where.push(
|
|
`(${filter.price
|
|
.filter((pr) => pr.trim() != "")
|
|
.map((pr) => pr.split("-"))
|
|
.map(
|
|
([from, to]) =>
|
|
`ergo_property_spaces.rate BETWEEN ${from
|
|
.trim()
|
|
.slice(1)} AND ${to.trim().slice(1)} `
|
|
)
|
|
.join(" OR ")})`
|
|
);
|
|
}
|
|
|
|
try {
|
|
const user_id = Number(localStorage.getItem("user"));
|
|
const result = await sdk.callRawAPI(
|
|
"/v2/api/custom/ergo/popular/PAGINATE",
|
|
{
|
|
page: 1,
|
|
limit: 10000,
|
|
user_id: Number(user_id),
|
|
where,
|
|
booking_start_time: isValidDate(filter.booking_start_time || "")
|
|
? new Date(filter.booking_start_time).toISOString()
|
|
: undefined,
|
|
},
|
|
"POST",
|
|
ctrl.signal
|
|
);
|
|
setPropertySpaces(result.list);
|
|
} catch (err) {
|
|
console.log("err", err);
|
|
globalDispatch({
|
|
type: "SHOW_ERROR",
|
|
payload: {
|
|
heading: "Operation failed",
|
|
message: err.message,
|
|
},
|
|
});
|
|
}
|
|
globalDispatch({ type: "STOP_LOADING" });
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isValidDate(searchParams.get("booking_start_time"))) {
|
|
setValue(
|
|
"booking_start_time",
|
|
new Date(searchParams.get("booking_start_time")),
|
|
{ shouldDirty: true }
|
|
);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (render) {
|
|
fetchSpaces();
|
|
}
|
|
}, [render]);
|
|
|
|
const removeFilter = (searchField, arrEl) => {
|
|
if (!arrEl) {
|
|
setValue(searchField, "");
|
|
searchParams.set(searchField, "");
|
|
setSearchParams(searchParams);
|
|
} else {
|
|
const prev = searchParams.get(searchField) ?? "";
|
|
const arr = prev?.split(",") || [];
|
|
const removeElIndex = arr.indexOf(arrEl);
|
|
if (removeElIndex > -1) {
|
|
arr.splice(removeElIndex, 1);
|
|
setValue(searchField, arr);
|
|
searchParams.set(searchField, arr.join(","));
|
|
}
|
|
}
|
|
setSearchParams(searchParams);
|
|
};
|
|
|
|
async function onSubmit(data) {
|
|
if (globalState.location && globalState.location.includes("undefined")) {
|
|
const parts = globalState.location.split(",");
|
|
const result = parts[0].trim();
|
|
globalState.location = result;
|
|
}
|
|
searchParams.set("location", globalState.location);
|
|
searchParams.set(
|
|
"booking_start_time",
|
|
isValidDate(data.booking_start_time)
|
|
? data.booking_start_time.toISOString()
|
|
: ""
|
|
);
|
|
searchParams.set("category", data.category.join(","));
|
|
searchParams.set("price", data.price.join(","));
|
|
searchParams.set("amenity", data.amenity.join(","));
|
|
searchParams.set("review", data.review.join(","));
|
|
if (data.max_capacity !== "NaN") {
|
|
searchParams.set("max_capacity", Number(data.max_capacity));
|
|
}
|
|
searchParams.set("capacity", data.capacity);
|
|
setSearchParams(searchParams);
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchSpaces();
|
|
}, [searchParams]);
|
|
|
|
const sortRating = (a, b) => {
|
|
if (sortAsc == 1) {
|
|
return (a.average_space_rating ?? 0) - (b.average_space_rating ?? 0);
|
|
}
|
|
return (b.average_space_rating ?? 0) - (a.average_space_rating ?? 0);
|
|
};
|
|
|
|
return (
|
|
<div className='min-h-screen bg-white'>
|
|
<section className='container mx-auto mb-[24px] bg-white px-6 pt-[120px] normal-case 2xl:px-32'>
|
|
<form
|
|
className='flex w-full flex-wrap justify-center'
|
|
onSubmit={handleSubmit(onSubmit)}
|
|
id='search-bar'
|
|
>
|
|
<CustomStaticLocationAutoCompleteV2
|
|
// type={true}
|
|
// control={control}
|
|
// setValue={(val) => setValue("location", val)}
|
|
// name="location"
|
|
type='static'
|
|
setValue={(val) =>
|
|
globalDispatch({
|
|
type: "SETLOCATION",
|
|
payload: {
|
|
location: val,
|
|
},
|
|
})
|
|
}
|
|
containerClassName={
|
|
"mb-4 flex min-h-[45px] w-full max-w-full flex-grow items-center gap-2 border-2 px-4 lg:mb-0 lg:w-[unset] lg:border-r-0 lg:border-b-2"
|
|
}
|
|
className='border-0 focus:outline-none'
|
|
placeholder='Search by city or zip code'
|
|
suggestionType={["(regions)"]}
|
|
/>
|
|
|
|
<div className='relative mb-6 flex h-[45px] w-full cursor-pointer items-center gap-2 border-2 p-2 lg:mb-0 lg:w-1/2 lg:w-[unset] lg:min-w-[230px] lg:border-r-0'>
|
|
<DatePickerV3
|
|
reset={() => resetField("booking_start_time")}
|
|
setValue={(val) => setValue("booking_start_time", val)}
|
|
control={control}
|
|
name='booking_start_time'
|
|
labelClassName='justify-between flex-grow flex-row-reverse'
|
|
placeholder='Select Date'
|
|
min={new Date()}
|
|
/>
|
|
</div>
|
|
{/* <div className="flex h-[45px] w-1/2 items-center gap-2 border-2 px-4 lg:w-[unset] lg:border-r-0">
|
|
<PeopleIcon />
|
|
<input
|
|
type="number"
|
|
placeholder="2 People (Max Capacity)"
|
|
className="remove-arrow w-full focus:outline-none"
|
|
{...register("max_capacity")}
|
|
/>
|
|
</div> */}
|
|
<button
|
|
className='login-btn-gradient mb-4 w-full whitespace-nowrap p-2 px-6 text-white disabled:text-[#98A2B3] lg:mb-0 lg:w-[unset]'
|
|
type='submit'
|
|
id='update-search'
|
|
>
|
|
Update Search
|
|
</button>
|
|
</form>
|
|
<div className='block lg:hidden'>
|
|
<button
|
|
type='button'
|
|
className='mb-6 flex w-full items-center justify-center gap-2 border-2 py-2 text-center'
|
|
onClick={() => setFilterPopup(true)}
|
|
>
|
|
<FilterIcon />
|
|
Filters
|
|
</button>
|
|
<div className='snap-scroll flex gap-4'>
|
|
{Object.entries(parseSearchParams(searchParams)).map(([k, v]) => {
|
|
if (!v) return null;
|
|
if (k == "booking_start_time") {
|
|
return (
|
|
<span className='whitespace-nowrap rounded-[50px] bg-[#F2F4F7] px-[16px] py-[6px] text-[#475467]'>
|
|
{formatDate(v)}
|
|
<button
|
|
type='button'
|
|
className='ml-3'
|
|
onClick={() => removeFilter(k)}
|
|
>
|
|
✕
|
|
</button>
|
|
</span>
|
|
);
|
|
}
|
|
if (k == "max_capacity" && v == 0) {
|
|
return;
|
|
}
|
|
let vArr = v.split(",");
|
|
if (vArr.length > 0) {
|
|
return vArr.map((filter, i) => (
|
|
<span
|
|
key={i}
|
|
className='whitespace-nowrap rounded-[50px] bg-[#F2F4F7] px-[16px] py-[6px] text-[#475467]'
|
|
>
|
|
{filter}
|
|
<button
|
|
type='button'
|
|
className='ml-3'
|
|
onClick={() => removeFilter(k, filter)}
|
|
>
|
|
✕
|
|
</button>
|
|
</span>
|
|
));
|
|
}
|
|
|
|
return (
|
|
<span
|
|
key={k}
|
|
className='whitespace-nowrap rounded-[50px] bg-[#F2F4F7] px-[16px] py-[6px] text-[#475467]'
|
|
>
|
|
{v}
|
|
<button
|
|
type='button'
|
|
className='ml-3'
|
|
onClick={() => removeFilter(k)}
|
|
>
|
|
✕
|
|
</button>
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section className='search-page-container container mx-auto flex gap-[32px] bg-white px-6 normal-case 2xl:px-32'>
|
|
<aside
|
|
className={`hidden xl:block xl:w-1/5 ${
|
|
filterPopup ? "popup-tablet" : ""
|
|
}`}
|
|
onClick={() => setFilterPopup(false)}
|
|
>
|
|
<div
|
|
className={`${
|
|
filterPopup ? "w-[80%] max-w-[500px] rounded-xl p-5" : ""
|
|
} flex flex-col bg-white `}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{filterPopup ? (
|
|
<div className='mb-[18px] flex items-center justify-between border-b pb-2'>
|
|
<h3 className='text-2xl font-semibold'>Filters</h3>
|
|
<button
|
|
onClick={() => setFilterPopup(false)}
|
|
className='rounded-full border p-1 px-3 text-2xl font-normal duration-300 hover:bg-gray-200'
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className={`${filterPopup ? "snap-scroll h-[60vh]" : ""}`}>
|
|
<FilterCheckBoxesV2
|
|
control={control}
|
|
name='category'
|
|
title='Spaces'
|
|
labelField='category'
|
|
valueField='category'
|
|
options={spaceCategories}
|
|
reset={() => {
|
|
resetField("category");
|
|
searchParams.set("category", "");
|
|
setSearchParams(searchParams);
|
|
}}
|
|
/>
|
|
<FilterCheckBoxesV2
|
|
control={control}
|
|
name='price'
|
|
title='Prices'
|
|
labelField='name'
|
|
valueField='name'
|
|
options={prices}
|
|
reset={() => {
|
|
resetField("price");
|
|
searchParams.set("price", "");
|
|
setSearchParams(searchParams);
|
|
}}
|
|
/>
|
|
<FilterCheckBoxesV2
|
|
control={control}
|
|
name='capacity'
|
|
title='Capacity'
|
|
labelField='name'
|
|
valueField='name'
|
|
options={capacity}
|
|
reset={() => {
|
|
resetField("capacity");
|
|
searchParams.set("capacity", "");
|
|
setSearchParams(searchParams);
|
|
}}
|
|
/>
|
|
<FilterCheckBoxesV2
|
|
name='amenity'
|
|
control={control}
|
|
title='Amenities'
|
|
options={amenityCategories}
|
|
labelField='name'
|
|
valueField='name'
|
|
reset={() => {
|
|
resetField("amenity");
|
|
searchParams.set("amenity", "");
|
|
setSearchParams(searchParams);
|
|
}}
|
|
/>
|
|
<FilterCheckBoxesV2
|
|
name='review'
|
|
control={control}
|
|
title='Reviews'
|
|
options={reviews}
|
|
labelField='name'
|
|
valueField='name'
|
|
reset={() => {
|
|
resetField("review");
|
|
searchParams.set("review", "");
|
|
setSearchParams(searchParams);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
<div className='mb-16 max-w-full flex-grow xl:w-4/5'>
|
|
<div className='mb-[15px] flex items-center justify-between'>
|
|
<h5 className={propertySpaces.length == 0 ? "md:invisible" : ""}>
|
|
{propertySpaces.length == 0 ? (
|
|
"No results Found"
|
|
) : (
|
|
<>
|
|
{" "}
|
|
Results Found{" "}
|
|
<strong className='font-semibold'>
|
|
({propertySpaces.length})
|
|
</strong>
|
|
</>
|
|
)}
|
|
</h5>
|
|
<CustomSelect
|
|
options={[
|
|
{ label: "Rating (High to Low)", value: 0 },
|
|
{ label: "Rating (Low to High)", value: 1 },
|
|
]}
|
|
onChange={setSortAsc}
|
|
accessor='label'
|
|
valueAccessor='value'
|
|
className='min-w-[200px]'
|
|
listOptionClassName={"pl-4"}
|
|
/>
|
|
</div>
|
|
<div className='flex flex-wrap justify-center gap-6 lg:block'>
|
|
{propertySpaces.length == 0 && (
|
|
<div className='hidden min-h-[300px] items-center justify-center normal-case text-[#667085] md:flex'>
|
|
<h2 className='flex gap-3'>
|
|
<NoteIcon /> No results found
|
|
</h2>
|
|
</div>
|
|
)}
|
|
{propertySpaces.sort(sortRating).map((sp) => (
|
|
<PropertySpaceTile
|
|
key={sp.id}
|
|
data={sp}
|
|
forceRender={forceRender}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<Tooltip
|
|
anchorId='update-search'
|
|
place='right'
|
|
content={"Search"}
|
|
noArrow
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SearchPage;
|