338 lines
11 KiB
TypeScript
338 lines
11 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 MultiRangeSlider from "./ui/MultiRangeSlider";
|
|
import { api } from "../api/ky";
|
|
import { RefObject, useEffect, useState } from "react";
|
|
import { useSearchParams } from "react-router";
|
|
import { projects } from "../data/projects";
|
|
import { useDebounce } from "../hooks/useDebounce";
|
|
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";
|
|
|
|
export interface Filters {
|
|
unitTypes: string[];
|
|
views: string[];
|
|
area: [number, number];
|
|
cost: [number, number];
|
|
floor: [number, number];
|
|
}
|
|
|
|
function SearchFilters({
|
|
inModal = false,
|
|
setInModal,
|
|
ref,
|
|
cost,
|
|
floor,
|
|
area,
|
|
setCost,
|
|
setFloor,
|
|
setArea,
|
|
}: {
|
|
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;
|
|
}) {
|
|
const [project, setProject] = useState<string>();
|
|
const [unitTypes, setUnitTypes] = useState<string[]>([]);
|
|
const [view, setView] = useState<string>("Any view");
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const { data: filters } = useQuery({
|
|
queryKey: ["filters", project],
|
|
enabled: !!project,
|
|
queryFn: () =>
|
|
api
|
|
.get(`units/filters?${project ? `project=${project}` : ""}`)
|
|
.json<Filters>(),
|
|
});
|
|
|
|
useEffect(() => {
|
|
const projectValue = searchParams.get("project") || projects[0].title;
|
|
if (projectValue) setProject(projectValue);
|
|
|
|
const viewValue = searchParams.get("view");
|
|
if (viewValue) setView(viewValue);
|
|
|
|
const unitTypesValue = searchParams.getAll("unitTypes");
|
|
if (unitTypesValue) setUnitTypes(unitTypesValue);
|
|
}, [searchParams]);
|
|
|
|
function resetFilters() {
|
|
window.location.href = "/search";
|
|
}
|
|
|
|
const [costInModal, setCostInModal] = useState<[number, number]>(cost);
|
|
const [areaInModal, setAreaInModal] = useState<[number, number]>(area);
|
|
const [floorInModal, setFloorInModal] = useState<[number, number]>(floor);
|
|
|
|
useEffect(() => {
|
|
if (filters) {
|
|
setCostInModal(filters.cost);
|
|
setAreaInModal(filters.area);
|
|
setFloorInModal(filters.floor);
|
|
}
|
|
}, [filters, setAreaInModal, setCostInModal, setFloorInModal]);
|
|
|
|
useEffect(() => {
|
|
if (inModal) return;
|
|
setCost(costInModal);
|
|
setArea(areaInModal);
|
|
setFloor(floorInModal);
|
|
}, [setCost, costInModal, setArea, areaInModal, setFloor, floorInModal]);
|
|
|
|
const debouncedCost = useDebounce(inModal ? costInModal : cost, 500);
|
|
const debouncedArea = useDebounce(inModal ? areaInModal : area, 500);
|
|
const debouncedFloor = useDebounce(inModal ? floorInModal : floor, 500);
|
|
|
|
const { data: count } = useQuery({
|
|
queryKey: [
|
|
"units",
|
|
"count",
|
|
project,
|
|
unitTypes,
|
|
view,
|
|
debouncedCost,
|
|
debouncedArea,
|
|
debouncedFloor,
|
|
],
|
|
enabled:
|
|
!!project &&
|
|
debouncedCost[0] >= 0 &&
|
|
debouncedCost[1] >= 0 &&
|
|
debouncedArea[0] >= 0 &&
|
|
debouncedArea[1] >= 0 &&
|
|
debouncedFloor[0] >= 0 &&
|
|
debouncedFloor[1] >= 0,
|
|
queryFn: () =>
|
|
api
|
|
.get(
|
|
`units/count?${project ? `project=${project}` : ""}${unitTypes
|
|
.map((unitType) => `&unitTypes=${unitType}`)
|
|
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
|
|
debouncedCost
|
|
? `&cost=${Math.round(debouncedCost[0])},${Math.round(
|
|
debouncedCost[1]
|
|
)}`
|
|
: ""
|
|
}${
|
|
debouncedArea
|
|
? `&area=${Math.round(debouncedArea[0])},${Math.round(
|
|
debouncedArea[1]
|
|
)}`
|
|
: ""
|
|
}${
|
|
debouncedFloor
|
|
? `&floor=${Math.round(debouncedFloor[0])},${Math.round(
|
|
debouncedFloor[1]
|
|
)}`
|
|
: ""
|
|
}`
|
|
)
|
|
.json<number>(),
|
|
});
|
|
|
|
function handleClose() {
|
|
const projectValue = searchParams.get("project") || projects[0].title;
|
|
if (projectValue) setProject(projectValue);
|
|
|
|
const viewValue = searchParams.get("view");
|
|
setView(viewValue || "Any view");
|
|
|
|
const unitTypesValue = searchParams.getAll("unitTypes");
|
|
if (unitTypesValue) setUnitTypes(unitTypesValue);
|
|
setInModal(false);
|
|
}
|
|
|
|
function handleSelectProject(project: Project) {
|
|
setProject(project.title);
|
|
if (!inModal)
|
|
setSearchParams((prev) => {
|
|
prev.set("project", project.title);
|
|
return prev;
|
|
});
|
|
}
|
|
|
|
function handleSelectView(view: string) {
|
|
setView(view);
|
|
if (!inModal)
|
|
setSearchParams((prev) => {
|
|
if (view !== "Any view") prev.set("view", view);
|
|
else prev.delete("view");
|
|
return prev;
|
|
});
|
|
}
|
|
|
|
function handleSelectUnitTypes(unitTypes: string[]) {
|
|
setUnitTypes(unitTypes);
|
|
if (!inModal)
|
|
setSearchParams((prev) => {
|
|
prev.delete("unitTypes");
|
|
unitTypes.forEach((unitType) => prev.append("unitTypes", unitType));
|
|
return prev;
|
|
});
|
|
}
|
|
|
|
function applyFilters() {
|
|
setInModal(false);
|
|
setSearchParams((prev) => {
|
|
prev.set("project", project!);
|
|
if (view !== "Any view") prev.set("view", view);
|
|
else prev.delete("view");
|
|
prev.delete("unitTypes");
|
|
unitTypes.forEach((unitType) => prev.append("unitTypes", unitType));
|
|
return prev;
|
|
});
|
|
setCost(costInModal);
|
|
setArea(areaInModal);
|
|
setFloor(floorInModal);
|
|
window.scroll({ top: 0, behavior: "smooth" });
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{inModal && (
|
|
<div
|
|
className="fixed inset-0 2xl:top-[4.444vw] bg-[#0D1922]/40 cursor-pointer"
|
|
onClick={handleClose}
|
|
/>
|
|
)}
|
|
<div
|
|
ref={ref}
|
|
className={clsx(
|
|
"2xl:p-[2.222vw] md:max-2xl:p-6 p-4 bg-white 2xl:rounded-b-[1.667vw] rounded-b-3xl 2xl:space-y-[2.222vw] md:max-2xl:space-y-8 space-y-4",
|
|
inModal &&
|
|
"fixed 2xl:top-[calc(2.778vw+4.444vw)] 2xl:left-[2.778vw] 2xl:right-[2.778vw] 2xl:rounded-[1.667vw] rounded-3xl md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-24"
|
|
)}
|
|
>
|
|
{inModal && (
|
|
<Button
|
|
onlyIcon
|
|
className="absolute right-[2.222vw] !bg-[#F3F3F2]"
|
|
onClick={handleClose}
|
|
>
|
|
<div className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]">
|
|
<CloseIcon />
|
|
</div>
|
|
</Button>
|
|
)}
|
|
<div className="2xl:space-y-[1.111vw] space-y-4">
|
|
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-semibold leading-[135%]">
|
|
Search
|
|
</p>
|
|
{project && (
|
|
<ProjectSelect
|
|
projects={projects}
|
|
onSelect={handleSelectProject}
|
|
defaultProject={projects.find(({ title }) => title === project)!}
|
|
/>
|
|
)}
|
|
</div>
|
|
<hr className="2xl:h-[0.069vw] h-px border-[#E2E2DC]" />
|
|
{filters && (
|
|
<>
|
|
<div className="2xl:space-y-[0.556vw] space-y-2">
|
|
<p className="text-s text-[#0D1922]/70">Apartment type</p>
|
|
<UnitTypesSelect
|
|
unitTypes={filters.unitTypes}
|
|
onSelect={handleSelectUnitTypes}
|
|
defaultSelected={unitTypes}
|
|
/>
|
|
</div>
|
|
<div className="grid 2xl:grid-cols-4 md:max-2xl:grid-cols-2 md:max-2xl:grid-rows-2 2xl:gap-[1.111vw] gap-4">
|
|
<MultiRangeSlider
|
|
min={filters?.cost[0]}
|
|
max={filters?.cost[1]}
|
|
currentMin={inModal ? costInModal[0] : cost[0]}
|
|
currentMax={inModal ? costInModal[1] : cost[1]}
|
|
offset={1}
|
|
onChange={setCostInModal}
|
|
label="Cost, AED"
|
|
/>
|
|
<MultiRangeSlider
|
|
min={filters?.floor[0]}
|
|
max={filters?.floor[1]}
|
|
currentMin={inModal ? floorInModal[0] : floor[0]}
|
|
currentMax={inModal ? floorInModal[1] : floor[1]}
|
|
offset={1}
|
|
onChange={setFloorInModal}
|
|
label="Floor"
|
|
/>
|
|
<MultiRangeSlider
|
|
min={filters?.area[0]}
|
|
max={filters?.area[1]}
|
|
currentMin={inModal ? areaInModal[0] : area[0]}
|
|
currentMax={inModal ? areaInModal[1] : area[1]}
|
|
offset={1}
|
|
onChange={setAreaInModal}
|
|
label="Total Area, Sqft"
|
|
/>
|
|
<Select
|
|
defaultOption={view}
|
|
label="View"
|
|
options={["Any view", ...filters.views]}
|
|
onSelect={handleSelectView}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4">
|
|
{inModal ? (
|
|
<Button onClick={applyFilters}>
|
|
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>
|
|
) : (
|
|
<AnimatePresence mode="wait">
|
|
{count && (
|
|
<motion.p
|
|
key={count}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="text-[#00BED7] text-s"
|
|
>
|
|
{count} Apartments found
|
|
</motion.p>
|
|
)}
|
|
</AnimatePresence>
|
|
)}
|
|
<Button variant="secondary" onClick={resetFilters}>
|
|
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70">
|
|
<RestartIcon />
|
|
</span>
|
|
<p className="text-s">Reset filters</p>
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default SearchFilters;
|