Files
irth-new-client/src/pages/SearchPage.tsx
T
2025-05-23 19:40:05 +05:00

328 lines
11 KiB
TypeScript

/* eslint-disable react-hooks/exhaustive-deps */
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
import { IUnit } from "../types/IUnit";
import UnitCard from "../components/UnitCard";
import { useEffect, useRef, useState } from "react";
import SearchFilters from "../components/SearchFilters";
import { useSearchParams } from "react-router";
import Button from "../components/ui/Button";
import FiltersIcon from "../components/icons/FiltersIcon";
import RestartIcon from "../components/icons/RestartIcon";
import clsx from "clsx";
import { AnimatePresence, motion } from "motion/react";
import Select from "../components/ui/Select";
import Skeleton from "react-loading-skeleton";
import { SORT_OPTIONS } from "../data/sortOptions";
const STEP = 12;
function SearchPage() {
const [searchParams] = useSearchParams();
const [project, setProject] = useState(
searchParams.get("project") || "Rove Home Marasi Drive"
);
const [selectedUnitTypes, setSelectedUnitTypes] = useState<string[]>([]);
// const unitTypes = searchParams.getAll("unitTypes");
// const view = searchParams.get("view");
const [filtersInModal, setFiltersInModal] = useState(false);
const [cost, setCost] = useState<[number, number]>([-1, -1]);
const [floor, setFloor] = useState<[number, number]>([-1, -1]);
const [area, setArea] = useState<[number, number]>([-1, -1]);
const [view, setView] = useState("Any view");
const [sort, setSort] = useState<keyof typeof SORT_OPTIONS>(
"Sort by ascending price"
);
const filtersRef = useRef<HTMLDivElement>(null);
const observerRef = useRef<HTMLDivElement>(null);
useEffect(() => window.scrollTo({ top: 0, behavior: "smooth" }), []);
const [showButtons, setShowButtons] = useState(false);
useEffect(() => {
const observerElement = filtersRef.current;
const observer = new IntersectionObserver((entries) =>
setShowButtons(!entries[0].isIntersecting)
);
if (observerElement) observer.observe(observerElement);
return () => {
if (observerElement) observer.unobserve(observerElement);
};
}, []);
const [footerReached, setFooterReached] = useState(false);
useEffect(() => {
const footer = document.querySelector("footer");
const observer = new IntersectionObserver((entries) =>
setFooterReached(entries[0].isIntersecting)
);
if (footer) observer.observe(footer);
return () => {
if (footer) observer.unobserve(footer);
};
}, []);
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
useEffect(
() =>
setActiveFiltersCount(
+searchParams.has("view") +
+searchParams.has("unitTypes") +
+searchParams.has("cost") +
+searchParams.has("floor") +
+searchParams.has("area")
),
[searchParams]
);
const { data: allUnitTypes } = useQuery({
queryKey: ["filters", "unitTypes", project],
queryFn: () =>
api.get(`units/filters/unitTypes?project=${project}`).json<string[]>(),
});
const { data: allViews } = useQuery({
queryKey: ["filters", "views", project],
queryFn: () =>
api.get(`units/filters/views?project=${project}`).json<string[]>(),
});
const { data: allFloors } = useQuery({
queryKey: ["filters", "floors", project],
queryFn: () =>
api.get(`units/filters/floor?project=${project}`).json<{
min: number;
max: number;
}>(),
});
const { data: allCost } = useQuery({
queryKey: ["filters", "cost", project],
queryFn: () =>
api.get(`units/filters/cost?project=${project}`).json<{
min: number;
max: number;
}>(),
});
const { data: allArea } = useQuery({
queryKey: ["filters", "area", project],
queryFn: () =>
api.get(`units/filters/area?project=${project}`).json<{
min: number;
max: number;
}>(),
});
useEffect(() => {
if (allFloors) setFloor([allFloors.min, allFloors.max]);
}, [allFloors]);
useEffect(() => {
if (allCost) setCost([allCost.min, allCost.max]);
}, [allCost]);
useEffect(() => {
if (allArea) setArea([allArea.min, allArea.max]);
}, [allArea]);
const { data, fetchNextPage, isLoading, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
initialPageParam: 0,
queryKey: [
"units",
project,
selectedUnitTypes,
view,
cost,
floor,
area,
sort,
],
enabled:
!!project &&
cost[0] >= 0 &&
cost[1] >= 0 &&
floor[0] >= 0 &&
floor[1] >= 0 &&
area[0] >= 0 &&
area[1] >= 0,
queryFn: async ({ pageParam = 0 }) =>
await api
.get(
`units?offset=${pageParam}&limit=${STEP}${
project ? `&project=${project}` : ""
}${selectedUnitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
cost.length > 0
? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
: ""
}${
floor.length > 0
? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
: ""
}${
area.length > 0
? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
: ""
}${sort ? `&order=${SORT_OPTIONS[sort].split(" ").join()}` : ""}`
)
.json<IUnit[]>(),
getNextPageParam: (lastPage, _, lastPageIndex) =>
lastPage.length < STEP ? undefined : lastPageIndex + STEP,
});
useEffect(() => {
if (!hasNextPage || isFetchingNextPage) return;
const observerElement = observerRef.current;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) fetchNextPage();
},
{ rootMargin: "-200px" }
);
if (observerElement) observer.observe(observerElement);
return () => {
if (observerElement) observer.unobserve(observerElement);
};
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
return (
<>
<SearchFilters
allUnitTypes={allUnitTypes || []}
allCost={allCost || { min: 0, max: 0 }}
allFloors={allFloors || { min: 0, max: 0 }}
allArea={allArea || { min: 0, max: 0 }}
allViews={allViews || []}
project={project}
setProject={setProject}
selectedUnitTypes={selectedUnitTypes}
setSelectedUnitTypes={setSelectedUnitTypes}
view={view}
setView={setView}
ref={filtersRef}
inModal={filtersInModal}
setInModal={setFiltersInModal}
{...{ area, cost, floor, setArea, setCost, setFloor }}
/>
<div
className="2xl:p-[2.222vw] p-4 min-h-dvh 2xl:space-y-[1.111vw] space-y-4"
style={
filtersInModal ? { marginTop: filtersRef.current?.clientHeight } : {}
}
>
<Select
options={Object.keys(SORT_OPTIONS)}
defaultOption={sort}
onSelect={(opt) => setSort(opt as keyof typeof SORT_OPTIONS)}
className="2xl:max-w-[22.778vw] md:max-2xl:max-w-[45.833vw]"
/>
<hr className="2xl:h-[0.069vw] border-[#E2E2DC]" />
<AnimatePresence mode="wait">
{isLoading ? (
<div className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4">
{Array.from({ length: STEP }).map((_, i) => (
<Skeleton
key={i}
borderRadius={16}
className="2xl:aspect-[332/396] md:max-2xl:aspect-[352/396] aspect-[328/396]"
/>
))}
</div>
) : data?.pages[0].length === 0 ? (
<div className="2xl:aspect-[1376/396] md:max-2xl:py-20 max-md:aspect-[328/240] flex justify-center items-center">
<p className="text-h3 font-medium text-[#00BED7] text-center">
No apartments found with the given parameters
</p>
</div>
) : (
project &&
selectedUnitTypes &&
cost &&
area &&
floor &&
sort && (
<motion.div
key={
project +
selectedUnitTypes +
view +
cost +
area +
floor +
sort
}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4"
>
{data?.pages.map((page) =>
page.map((unit) => <UnitCard key={unit.id} unit={unit} />)
)}
{isFetchingNextPage &&
Array.from({ length: STEP }).map((_, i) => (
<Skeleton
key={"fetching-" + i}
borderRadius={16}
className="2xl:aspect-[332/396] md:max-2xl:aspect-[352/396] aspect-[328/396]"
/>
))}
</motion.div>
)
)}
</AnimatePresence>
</div>
{showButtons && (
<div
className={clsx(
"fixed left-1/2 -translate-x-1/2 flex justify-center 2xl:gap-[0.278vw] gap-2 transition-all z-2",
footerReached && !hasNextPage
? "top-[calc(100dvh-17.222vw)] translate-y-0"
: "top-[calc(100dvh-2.222vw)] -translate-y-full"
)}
>
<Button onClick={() => setFiltersInModal(true)} className="relative">
<span className="2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-white">
<FiltersIcon />
</span>
<span className="text-caption-m">Filters</span>
{!!activeFiltersCount && (
<div className="absolute 2xl:top-[0.139vw] 2xl:right-[0.139vw] top-0.5 right-0.5 rounded-full w-4 h-4 text-caption-s bg-white text-[#00BED7] flex justify-center items-center font-mono ring ring-[#E2E2DC]">
{activeFiltersCount}
</div>
)}
</Button>
<Button
variant="secondary"
className="2xl:!outline-[0.069vw] !outline !outline-[#E2E2DC]"
onClick={() => {
window.location.href = "/search";
}}
>
<span className="2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-[#0D1922]/70">
<RestartIcon />
</span>
<span className="text-caption-m">Reset</span>
</Button>
</div>
)}
<div ref={observerRef} />
</>
);
}
export default SearchPage;