Files
Ergo/src/pages/Common/ExplorePage.jsx
T
2025-01-24 20:05:48 +01:00

627 lines
22 KiB
React

import React, { useEffect, useState } from "react";
import PropertySpaceCard from "@/components/frontend/PropertySpaceCard";
import { useForm } from "react-hook-form";
import { useSearchParams } from "react-router-dom";
import InfiniteScroll from "react-infinite-scroll-component";
import NoteIcon from "@/components/frontend/icons/NoteIcon";
import { isValidDate, parseSearchParams } from "@/utils/utils";
import { useContext } from "react";
import { GlobalContext } from "@/globalContext";
import HostCardSlider from "@/components/frontend/HostCardSlider";
import CustomSelectV2 from "@/components/CustomSelectV2";
import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2";
import DatePickerV3 from "@/components/DatePickerV3";
import { DRAFT_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants";
import { AuthContext, tokenExpireError } from "@/authContext";
import MkdSDK from "@/utils/MkdSDK";
import PropertySpaceFiltersModal from "@/components/PropertySpaceFiltersModal";
import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid";
const prices = [
{
label: "All Prices",
value: "",
},
{
label: "$0 - $30",
value: "$0 - $30",
},
{
label: "$31 - $60",
value: "$31 - $60",
},
{
label: "$60 - $90",
value: "$60 - $90",
},
{
label: "$90 - $120",
value: "$90 - $120",
},
{
label: "$120 - $150",
value: "$120 - $150",
},
{
label: "$150 - $180",
value: "$150 - $180",
},
];
const sdk = new MkdSDK();
const ExplorePage = () => {
const FETCH_PER_SCROLL = 12;
const [searchParams, setSearchParams] = useSearchParams();
const section = searchParams.get("section") ?? "all";
const [hosts, setHosts] = useState([]);
const [popularSpaces, setPopularSpaces] = useState([]);
const [newSpaces, setNewSpaces] = useState([]);
const [showFilter, setShowFilter] = useState(false);
const [forceRender, setForceRender] = useState("");
const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext);
const { dispatch } = useContext(AuthContext);
const [ctrl] = useState(new AbortController());
const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({
defaultValues: (() => {
const params = parseSearchParams(searchParams);
return {
location: params.location ?? "",
from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(),
to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(),
space_name: params.space_name ?? "",
category: params.category ?? "",
price_range: params.price_range ?? "",
direction: "DESC",
};
})(),
});
const { dirtyFields } = formState;
const direction = watch("direction");
const fromDate = watch("from");
const [popularTotal, setPopularTotal] = useState(10000);
const [newSpaceTotal, setNewSpaceTotal] = useState(10000);
async function fetchPopularSpaces(page) {
setPopularSpaces([]);
setPopularSpaces((prev) => {
const amountToFetch = popularTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(popularTotal - prev.length - FETCH_PER_SCROLL);
return [...prev, ...Array(amountToFetch).fill({})];
});
const data = parseSearchParams(searchParams);
const user_id = localStorage.getItem("user");
const location = (data.location?.split(","))
var from_price, to_price;
if (data.price_range) {
var arr = data.price_range.split("-");
if (arr.length > 1) {
from_price = arr[0].trim().slice(1);
to_price = arr[1].trim().slice(1);
}
}
let where = [
`ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND ergo_property_spaces_images.is_approved = 1 AND schedule_template_id IS NOT NULL AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.deleted_at IS NULL`,
];
if (data.category) {
where.push(`ergo_spaces.category = '${data.category}'`);
}
if (data.space_name) {
where.push(`ergo_property.name LIKE '%${data.space_name}%'`);
}
if (data.price_range) {
where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`);
}
if (data.location) {
where.push(
`(ergo_property.address_line_1 LIKE '%${data.location}%' OR ergo_property.address_line_2 LIKE '%${data.location}%' OR ergo_property.city LIKE '%${location[0]}%' OR ergo_property.country LIKE '%${location.length === 1 ? location[0] : location.length === 2 ? location[1] : location[2]}%' OR ergo_property.zip LIKE '%${data.location}%' OR ergo_property.name LIKE '%${data.location}%')`,
);
}
try {
const result = await sdk.callRawAPI(
"/v2/api/custom/ergo/popular/PAGINATE",
{
page: page ?? 1,
limit: FETCH_PER_SCROLL,
user_id: Number(user_id),
where,
booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined,
booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined,
sortId: direction == "NONE" ? undefined : "id",
direction: direction == "NONE" ? undefined : direction,
},
"POST",
ctrl.signal,
);
if (Array.isArray(result.list)) {
setPopularSpaces((prev) => {
return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i);
});
setPopularTotal(result.total);
}
} catch (err) {
tokenExpireError(dispatch, err.message);
if (err.name == "AbortError") return;
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed",
message: err.message,
},
});
}
}
async function fetchNewSpaces(page) {
setNewSpaces([]);
setNewSpaces((prev) => {
const amountToFetch = newSpaceTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(newSpaceTotal - prev.length - FETCH_PER_SCROLL);
return [...prev, ...Array(amountToFetch).fill({})];
});
const data = parseSearchParams(searchParams);
const user_id = localStorage.getItem("user");
var from_price, to_price;
if (data.price_range) {
var arr = data.price_range.split("-");
if (arr.length > 1) {
from_price = arr[0].trim().slice(1);
to_price = arr[1].trim().slice(1);
}
}
let where = [
`ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.availability = ${SPACE_VISIBILITY.VISIBLE} AND ergo_property_spaces_images.is_approved = 1`,
];
if (data.category) {
where.push(`ergo_spaces.category = '${data.category}'`);
}
if (data.space_name) {
where.push(`ergo_property.name LIKE '%${data.space_name}%'`);
}
if (data.price_range) {
where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`);
}
if (data.location) {
where.push(
`(ergo_property.address_line_1 LIKE '%${location}%' OR ergo_property.address_line_2 LIKE '%${location}%' OR ergo_property.city LIKE '%${location[0] ?? ""}%' OR ergo_property.country LIKE '%${location.length === 1 ? location[0] : location.length === 2 ? location[1] : location[2]}%' OR ergo_property.zip LIKE '%${location}%' OR ergo_property.name LIKE '%${location}%')`,
);
}
try {
const result = await sdk.callRawAPI(
"/v2/api/custom/ergo/popular/PAGINATE",
{
page: page ?? 1,
limit: FETCH_PER_SCROLL,
user_id: Number(user_id),
where,
sortId: "update_at",
direction: "DESC",
booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined,
booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined,
},
"POST",
ctrl.signal,
);
if (Array.isArray(result.list)) {
setNewSpaces((prev) => {
return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i);
});
setNewSpaceTotal(result.total);
}
} catch (err) {
tokenExpireError(dispatch, err.message);
if (err.name == "AbortError") return;
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed",
message: err.message,
},
});
}
}
async function fetchHosts() {
const filter = parseSearchParams(searchParams);
const data = parseSearchParams(searchParams);
const location = (data.location?.replace(', undefined', '')?.split(","))
const user_id = localStorage.getItem("user");
var from_price, to_price;
if (data.price_range) {
var arr = data.price_range.split("-");
if (arr.length > 1) {
from_price = arr[0].trim().slice(1);
to_price = arr[1].trim().slice(1);
}
}
let where = [];
where.push('ergo_property.id IS NOT NULL');
if (data.category) {
where.push(`ergo_spaces.category = '${data.category}'`);
}
if (data.space_name) {
where.push(`ergo_property.name LIKE '%${data.space_name}%'`);
}
if (data.from) {
where.push(`ergo_user.create_at BETWEEN '${data.from}' AND '${data.to}'`);
}
if (data.price_range) {
where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`);
}
if (data.location) {
where.push([
`(ergo_profile.address_line_1 LIKE '%${data.location}%' OR ergo_profile.address_line_2 LIKE '%${data.location}%' OR ergo_profile.city LIKE '%${location[0]}%' OR ergo_profile.country LIKE '%${location[1]}%' OR ergo_profile.zip LIKE '%${data.location}%')`,
]);
}
try {
const result = await sdk.callRawAPI("/v2/api/custom/ergo/top-hosts/PAGINATE",
{
page: 1,
limit: 1000,
sortId: "avg_host_rating",
direction: "DESC",
where,
booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined,
booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined,
}, "POST", ctrl.signal);
setHosts(result.list);
} catch (err) {
tokenExpireError(dispatch, err.message);
if (err.name == "AbortError") return;
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed",
message: err.message,
},
});
}
}
useEffect(() => {
switch (searchParams.get("section")) {
case "popular":
fetchPopularSpaces();
break;
case "hosts":
fetchHosts();
break;
case "new-spaces":
fetchNewSpaces();
break;
default:
fetchHosts();
fetchPopularSpaces();
fetchNewSpaces();
}
}, [searchParams]);
useEffect(() => {
if (forceRender) {
setPopularSpaces([]);
setNewSpaces([]);
fetchPopularSpaces();
fetchNewSpaces();
}
}, [forceRender]);
useEffect(() => {
return () => {
// TODO: abort this only when component unmounts
// console.log("aborting");
// ctrl.abort();
};
}, []);
const onSubmit = async (data) => {
if (window.innerWidth < 700) {
setShowFilter(false);
}
if (data.location.includes("undefined")) {
const parts = inputString.split(",");
const result = parts[0].trim();
data.location = result;
}
searchParams.set("category", data.category);
searchParams.set("price_range", data.price_range);
searchParams.set("space_name", data.space_name);
searchParams.set("location", data.location);
searchParams.set("from", dirtyFields?.from ? data.from.toISOString() : "");
searchParams.set("to", dirtyFields?.to ? data.to.toISOString() : "");
setSearchParams(searchParams);
};
const sortByDate = (a, b) => {
if (direction == "NONE") return 0;
if (direction == "DESC") {
return new Date(b.id) - new Date(a.id);
}
return new Date(a.id) - new Date(b.id);
};
return (
<div className="min-h-screen">
<section className="container mx-auto bg-white px-6 pt-[120px] normal-case 2xl:px-16">
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-8 text-sm md:text-base"
>
<div className="mb-[30px] flex justify-between gap-4 md:gap-0">
<button
type="button"
className="flex flex-grow items-center justify-between gap-2 rounded-md border p-2 md:max-w-[120px]"
onClick={() => setShowFilter((prev) => !prev)}
>
<span>Filters</span>
<AdjustmentsHorizontalIcon className="h-6 w-6" />
</button>
<CustomSelectV2
items={[
{ label: "By Date: Newest First", value: "DESC" },
{ label: "By Date: Oldest First", value: "ASC" },
]}
labelField="label"
valueField="value"
containerClassName="h-full w-full max-w-[12rem]"
className={`w-full border py-2 px-3`}
placeholder={"By Date: Newest First"}
control={control}
name="direction"
/>
</div>
<div className={` ${showFilter ? "md:flex" : "hidden"} animate-filter hidden flex-wrap gap-[12px] gap-y-[20px]`}>
<CustomSelectV2
items={[{ label: "All Categories", value: "" }, ...globalState.spaceCategories.map((sp) => ({ label: sp.category, value: sp.category }))]}
labelField="label"
valueField="value"
containerClassName="flex-grow max-w-xs min-w-[10rem]"
className={`w-full border py-2 px-3`}
placeholder={"All Categories"}
control={control}
name="category"
/>
<CustomSelectV2
items={prices}
labelField="label"
valueField="value"
containerClassName="flex-grow max-w-xs min-w-[10rem]"
className={`w-full border py-2 px-3`}
placeholder={"All Prices"}
control={control}
name="price_range"
/>
{/* <CustomLocationAutoCompleteV2
control={control}
setValue={(val) => setValue("location", val)}
name="location"
className={`rounded border py-3 px-3 leading-tight text-gray-700 focus:outline-none`}
containerClassName={"w-[unset] flex-gro max-w-xs"}
placeholder="Location"
suggestionType={["(regions)"]}
hideIcons
/> */}
<CustomLocationAutoCompleteV2
control={control}
setValue={(val) => setValue("location", val)}
name="location"
className={`rounded border py-3 px-3 leading-tight text-gray-700 focus:outline-none`}
containerClassName={"w-[unset] flex-gro max-w-xs"}
placeholder="Location"
suggestionType={["(regions)"]}
hideIcons
/>
<div className="z-10 flex min-w-[190px] items-center gap-2 rounded-md border bg-white px-2">
<DatePickerV3
reset={() => resetField("from", { keepDirty: false, keepTouched: false })}
setValue={(val) => setValue("from", val, { shouldDirty: true })}
control={control}
name="from"
labelClassName="justify-between flex-grow flex-row-reverse"
placeholder="From"
/>
</div>
<div className="z-10 flex min-w-[190px] items-center gap-2 rounded-md border bg-white px-2">
<DatePickerV3
reset={() => resetField("to", { keepDirty: false, keepTouched: false })}
setValue={(val) => setValue("to", val, { shouldDirty: true })}
control={control}
name="to"
labelClassName="justify-between flex-grow flex-row-reverse"
placeholder="To"
min={fromDate}
/>
</div>
<input
type="text"
placeholder="Space name"
className="max-w-[180px] rounded-md border p-2 focus:outline-none active:outline-none"
{...register("space_name")}
/>
<button
type="submit"
className="rounded-md border border-black p-2 px-6"
>
Search
</button>
</div>
</form>
</section>
{(section == "popular" || section == "all") && (
<section
className="container mx-auto pt-[40px] 2xl:px-16"
id="popular"
>
<div className="mb-[26px] flex items-end justify-between border-b border-gray-300 px-6 pb-[12px] md:px-0">
<h3 className="text-3xl font-bold">{searchParams.get("category") || "Popular" + " spaces"}</h3>
</div>
{popularSpaces.length == 0 && (
<div className="flex min-h-[300px] items-center justify-center normal-case text-[#667085]">
<h2 className="flex gap-3">
<NoteIcon /> No spaces found
</h2>
</div>
)}
<InfiniteScroll
dataLength={popularSpaces.length}
next={() => {
fetchPopularSpaces(Math.round(popularSpaces.length / FETCH_PER_SCROLL + 1));
}}
scrollThreshold={1}
hasMore={popularSpaces.length < popularTotal}
loader={<></>}
endMessage={
<p className="text-center normal-case">
<b></b>
</p>
}
>
{
<div className="property-space-grid pb-[100px]">
{popularSpaces.sort(sortByDate).map((property, idx) => (
<PropertySpaceCard
key={property.id ?? idx}
data={property}
forceRender={setForceRender}
/>
))}
{popularSpaces.length < 4 ? (
<>
<div className="hidden 2xl:block"></div>
<div className="hidden lg:block"></div>
<div className="hidden md:block"></div>
</>
) : null}
</div>
}
</InfiniteScroll>
</section>
)}
{section == "all" && (
<section className="container mx-auto flex flex-wrap pt-[40px] pb-[40px] md:pb-[140px] 2xl:px-16">
<div className="px-6 md:w-2/5 md:px-0">
<h3 className="mb-[70px] text-[30px] font-semibold md:text-center">Browse By Category</h3>
</div>
<div className="browse-grid md:w-3/5">
{globalState.spaceCategories.map((tab, idx) => (
<button
key={tab.id}
className={``}
onClick={() => {
setPopularSpaces(Array(FETCH_PER_SCROLL).fill({}));
reset();
window.scrollTo({ top: 0, left: 0 });
searchParams.set("category", tab.category);
searchParams.set("section", "popular");
searchParams.delete("price_range");
searchParams.delete("space_name");
setSearchParams(searchParams);
}}
>
<img
src={tab.image}
alt={tab.category}
className="h-24 w-full rounded-lg object-cover md:h-40"
/>
<p className="text-lg py-3 px-5 text-left font-semibold">{tab.category}</p>
</button>
))}
</div>
</section>
)}
{(section == "hosts" || section == "all") && (
<section
className="container mx-auto pt-[12px] pb-[64px] 2xl:px-16"
id="hosts"
>
<div className="mb-[26px] flex items-end justify-between border-b border-gray-300 px-6 pb-[12px] md:px-0">
<h3 className="text-3xl font-bold">Top rated hosts</h3>
</div>
<div className="px-2 md:px-0">
<HostCardSlider hosts={hosts} />
</div>
</section>
)}
{(section == "new-spaces" || section == "all") && (
<section
className="container mx-auto pt-[40px] 2xl:px-16"
id="new-spaces"
>
<div className="mb-[26px] flex items-end justify-between border-b border-gray-300 px-6 pb-[12px] md:px-0">
<h3 className="text-3xl font-bold">New Spaces</h3>
</div>
{newSpaces.length == 0 && (
<div className="flex min-h-[300px] items-center justify-center normal-case text-[#667085]">
<h2 className="flex gap-3">
<NoteIcon /> No spaces found
</h2>
</div>
)}
<InfiniteScroll
dataLength={newSpaces.length}
next={() => {
fetchNewSpaces(Math.round(newSpaces.length / FETCH_PER_SCROLL + 1));
}}
scrollThreshold={1}
hasMore={newSpaces.length < newSpaceTotal}
loader={<></>}
endMessage={
<p className="text-center normal-case">
<b></b>
</p>
}
>
{
<div className="property-space-grid pb-[100px]">
{newSpaces.sort(sortByDate).map((property, idx) => (
<PropertySpaceCard
key={property.id ?? idx}
data={property}
forceRender={setForceRender}
/>
))}
{newSpaces.length < 4 ? (
<>
<div className="hidden 2xl:block"></div>
<div className="hidden lg:block"></div>
<div className="hidden md:block"></div>
</>
) : null}
</div>
}
</InfiniteScroll>
</section>
)}
<PropertySpaceFiltersModal
modalOpen={showFilter}
closeModal={() => setShowFilter(false)}
/>
</div>
);
};
export default ExplorePage;