Files
irth-new-client-120/src/components/SearchFilters.tsx
T

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;