400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
/* eslint-disable react-hooks/exhaustive-deps */
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import RestartIcon from "./icons/RestartIcon";
|
|
import Button from "./ui/Button";
|
|
import { api } from "../api/ky";
|
|
import { RefObject } from "react";
|
|
import { projects } from "../data/projects";
|
|
import clsx from "clsx";
|
|
import ProjectSelect from "./ProjectSelect";
|
|
import UnitTypesSelect from "./UnitTypesSelect";
|
|
import Select from "./ui/Select";
|
|
import { AnimatePresence, motion } from "motion/react";
|
|
import CloseIcon from "./icons/CloseIcon";
|
|
import Project from "../types/Project";
|
|
import FiltersIcon from "./icons/FiltersIcon";
|
|
import MultiRangeSlider from "./ui/MultiRangeSlider";
|
|
|
|
function SearchFilters({
|
|
inModal = false,
|
|
setInModal,
|
|
cost,
|
|
setCost,
|
|
floor,
|
|
setFloor,
|
|
area,
|
|
setArea,
|
|
selectedUnitTypes,
|
|
setSelectedUnitTypes,
|
|
view,
|
|
setView,
|
|
ref,
|
|
project,
|
|
setProject,
|
|
allUnitTypes,
|
|
allCost,
|
|
allFloors,
|
|
allArea,
|
|
allViews,
|
|
}: {
|
|
inModal?: boolean;
|
|
setInModal: (inModal: boolean) => void;
|
|
ref?: RefObject<HTMLDivElement | null>;
|
|
cost: [number, number];
|
|
floor: [number, number];
|
|
area: [number, number];
|
|
setCost: (cost: [number, number]) => void;
|
|
setFloor: (floor: [number, number]) => void;
|
|
setArea: (area: [number, number]) => void;
|
|
selectedUnitTypes: string[];
|
|
setSelectedUnitTypes: (selectedUnitTypes: string[]) => void;
|
|
view: string;
|
|
setView: (view: string) => void;
|
|
project: string;
|
|
setProject: (project: string) => void;
|
|
allUnitTypes: string[];
|
|
allCost: { min: number; max: number };
|
|
allFloors: { min: number; max: number };
|
|
allArea: { min: number; max: number };
|
|
allViews: string[];
|
|
}) {
|
|
const { data: count } = useQuery({
|
|
queryKey: [
|
|
"units",
|
|
"count",
|
|
project,
|
|
cost,
|
|
area,
|
|
floor,
|
|
view,
|
|
selectedUnitTypes,
|
|
],
|
|
queryFn: () =>
|
|
api
|
|
.get(
|
|
`units/count?project=${project}${selectedUnitTypes
|
|
.map((unitType) => `&unitTypes=${unitType}`)
|
|
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
|
|
cost[0] >= 0 && cost[1] >= 0
|
|
? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
|
|
: ""
|
|
}${
|
|
area[0] >= 0 && area[1] >= 0
|
|
? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
|
|
: ""
|
|
}${
|
|
floor[0] >= 0 && floor[1] >= 0
|
|
? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
|
|
: ""
|
|
}`
|
|
)
|
|
.json<number>(),
|
|
enabled:
|
|
!!project &&
|
|
allUnitTypes !== undefined &&
|
|
allCost !== undefined &&
|
|
allFloors !== undefined &&
|
|
allArea !== undefined &&
|
|
allViews !== undefined &&
|
|
// Wait until filters are initialized to prevent duplicate requests
|
|
cost[0] >= 0 &&
|
|
cost[1] >= 0 &&
|
|
floor[0] >= 0 &&
|
|
floor[1] >= 0 &&
|
|
area[0] >= 0 &&
|
|
area[1] >= 0,
|
|
});
|
|
|
|
function handleSelectProject(project: Project | null) {
|
|
setProject(project?.slug || projects[0].slug);
|
|
}
|
|
|
|
function handleSelectUnitTypes(unitTypes: string[]) {
|
|
setSelectedUnitTypes(unitTypes);
|
|
}
|
|
|
|
function handleSelectView(view: string) {
|
|
setView(view);
|
|
}
|
|
|
|
function handleSelectCost(newCostRange: [number, number]) {
|
|
setCost(newCostRange);
|
|
}
|
|
|
|
function handleSelectFloor(newFloorRange: [number, number]) {
|
|
setFloor(newFloorRange);
|
|
}
|
|
|
|
function handleSelectArea(newAreaRange: [number, number]) {
|
|
setArea(newAreaRange);
|
|
}
|
|
|
|
function resetFilters() {
|
|
setView("Any view");
|
|
setSelectedUnitTypes([]);
|
|
// Use actual min/max values if available, otherwise use 0 to keep query enabled
|
|
if (allCost) setCost([allCost.min, allCost.max]);
|
|
if (allFloors) setFloor([allFloors.min, allFloors.max]);
|
|
if (allArea) setArea([allArea.min, allArea.max]);
|
|
if (inModal) setInModal(false);
|
|
}
|
|
|
|
function applyFilters() {
|
|
setInModal(false);
|
|
window.scroll({ top: 0, behavior: "smooth" });
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{inModal && (
|
|
<div
|
|
className="fixed inset-0 bg-[#0D1922]/40 cursor-pointer z-10"
|
|
onClick={() => setInModal(false)}
|
|
/>
|
|
)}
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
"2xl:p-[2.222vw] md:max-2xl:p-6 p-4 bg-white 2xl:rounded-b-[1.667vw] md:rounded-t-3xl 2xl:space-y-[2.222vw] space-y-8 z-10",
|
|
inModal &&
|
|
"fixed z-10 max-md:pb-0 max-md:pt-6 2xl:top-[calc(2.778vw+7.5vh)] max-md:w-full 2xl:left-[2.222vw] 2xl:right-[2.222vw] 2xl:rounded-[1.667vw] md:max-2xl:rounded-3xl md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-24 max-md:bottom-0 max-md:overflow-auto max-md:!rounded-t-3xl max-md:max-h-[calc(100dvh-40px)]"
|
|
)}
|
|
>
|
|
{inModal && (
|
|
<Button
|
|
onlyIcon
|
|
className="absolute right-[2.222vw] !bg-[#F3F3F2]"
|
|
onClick={() => setInModal(false)}
|
|
>
|
|
<div className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]">
|
|
<CloseIcon />
|
|
</div>
|
|
</Button>
|
|
)}
|
|
<div className="2xl:space-y-[2.222vw] space-y-8">
|
|
<div className="2xl:space-y-[1.111vw] space-y-4">
|
|
<p className="text-h2 font-medium">
|
|
{inModal ? "Filters" : "Search"}
|
|
</p>
|
|
<div className={clsx(!inModal && "max-md:hidden")}>
|
|
<motion.div
|
|
key={project}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<ProjectSelect
|
|
projects={projects}
|
|
onSelect={handleSelectProject}
|
|
defaultProject={
|
|
projects.find(({ slug }) => slug === project)!
|
|
}
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
<hr
|
|
className={clsx(
|
|
"2xl:h-[0.069vw] h-px border-[#E2E2DC]",
|
|
!inModal && "max-md:hidden"
|
|
)}
|
|
/>
|
|
</div>
|
|
<AnimatePresence mode="wait">
|
|
{allUnitTypes && (
|
|
<motion.div
|
|
key={allUnitTypes.join()}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className={clsx(
|
|
"2xl:space-y-[0.556vw] space-y-2",
|
|
!inModal && "max-md:hidden"
|
|
)}
|
|
>
|
|
<p className="text-s text-[#0D1922]/70">Apartment type</p>
|
|
<UnitTypesSelect
|
|
unitTypes={allUnitTypes}
|
|
onSelect={handleSelectUnitTypes}
|
|
defaultSelected={selectedUnitTypes}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<div
|
|
className={clsx(
|
|
"grid 2xl:grid-cols-4 md:max-2xl:grid-cols-2 md:max-2xl:grid-rows-2 2xl:gap-[1.111vw] gap-6",
|
|
!inModal && "max-md:hidden"
|
|
)}
|
|
>
|
|
<AnimatePresence mode="wait">
|
|
{allCost && (
|
|
<motion.div
|
|
key={`${allCost.min}-${allCost.max}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<MultiRangeSlider
|
|
{...allCost}
|
|
currentMin={cost[0] === -1 ? allCost.min : cost[0]}
|
|
currentMax={cost[1] === -1 ? allCost.max : cost[1]}
|
|
offset={0}
|
|
onChange={handleSelectCost}
|
|
label="Cost, AED"
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<AnimatePresence mode="wait">
|
|
{allFloors && (
|
|
<motion.div
|
|
key={`${allFloors.min}-${allFloors.max}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<MultiRangeSlider
|
|
{...allFloors}
|
|
currentMin={floor[0] === -1 ? allFloors.min : floor[0]}
|
|
currentMax={floor[1] === -1 ? allFloors.max : floor[1]}
|
|
offset={0}
|
|
onChange={handleSelectFloor}
|
|
label="Floor"
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<AnimatePresence mode="wait">
|
|
{allArea && (
|
|
<motion.div
|
|
key={`${allArea.min}-${allArea.max}`}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<MultiRangeSlider
|
|
{...allArea}
|
|
currentMin={area[0] === -1 ? allArea.min : area[0]}
|
|
currentMax={area[1] === -1 ? allArea.max : area[1]}
|
|
offset={0}
|
|
onChange={handleSelectArea}
|
|
label="Total Area, Sqft"
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<AnimatePresence mode="wait">
|
|
{allViews && (
|
|
<motion.div
|
|
key={allViews.join()}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
<Select
|
|
defaultOption={view}
|
|
label="View"
|
|
options={["Any view", ...allViews]}
|
|
onSelect={handleSelectView}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
<div
|
|
className={clsx(
|
|
"flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4 gap-2",
|
|
inModal &&
|
|
"max-md:flex-col max-md:sticky max-md:shadow-[0px_-4px_20px_rgba(0,0,0,0.05)] max-md:rounded-t-2xl max-md:-m-4 max-md:p-4 bottom-0 bg-white"
|
|
)}
|
|
>
|
|
{inModal ? (
|
|
<Button
|
|
variant="cta"
|
|
onClick={applyFilters}
|
|
className="max-md:w-full"
|
|
>
|
|
Show{" "}
|
|
<AnimatePresence mode="wait">
|
|
{count !== undefined && (
|
|
<motion.span
|
|
key={count}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
{count}
|
|
</motion.span>
|
|
)}
|
|
</AnimatePresence>{" "}
|
|
apartments
|
|
</Button>
|
|
) : (
|
|
<p
|
|
className={clsx(
|
|
"text-[#00BED7] text-s",
|
|
!inModal && "max-md:hidden"
|
|
)}
|
|
>
|
|
<AnimatePresence mode="wait">
|
|
{count !== undefined ? (
|
|
<motion.span
|
|
key={count}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
{count}
|
|
</motion.span>
|
|
) : (
|
|
<motion.span
|
|
key={count}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
>
|
|
...
|
|
</motion.span>
|
|
)}
|
|
</AnimatePresence>{" "}
|
|
Apartments found
|
|
</p>
|
|
)}
|
|
<Button
|
|
variant="secondary"
|
|
className={clsx(
|
|
"hidden",
|
|
!inModal && "max-md:flex !justify-center flex-1 !bg-[#F3F3F2]"
|
|
)}
|
|
onClick={() => setInModal(true)}
|
|
>
|
|
<div className="w-5 h-5">
|
|
<FiltersIcon />
|
|
</div>
|
|
<p className="leading-0 text-sm">Filters</p>
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onlyIcon={!inModal && innerWidth < 768}
|
|
onClick={resetFilters}
|
|
className={clsx(
|
|
!inModal ? "max-md:bg-[#F3F3F2]" : "max-md:w-full",
|
|
"max-md:!transition-none justify-center"
|
|
)}
|
|
>
|
|
<span className="2xl:size-[1.389vw] size-5 text-[#0D1922]/70">
|
|
<RestartIcon />
|
|
</span>
|
|
<p className={clsx("text-s", !inModal && "max-md:hidden")}>
|
|
Reset filters
|
|
</p>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default SearchFilters;
|