search filters in modal, todo mobile
This commit is contained in:
@@ -1 +1 @@
|
||||
VITE_API_URL=http://localhost:3000
|
||||
VITE_API_URL=http://192.168.1.250:3000
|
||||
@@ -11,6 +11,7 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -15,6 +15,8 @@ function ProjectSelect({
|
||||
}) {
|
||||
const [selectedProject, setSelectedProject] = useState(defaultProject);
|
||||
|
||||
useEffect(() => setSelectedProject(defaultProject), [defaultProject]);
|
||||
|
||||
useEffect(() => onSelect(selectedProject), [selectedProject]);
|
||||
|
||||
return (
|
||||
@@ -36,7 +38,14 @@ function ProjectSelect({
|
||||
alt={project.title}
|
||||
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
|
||||
/>
|
||||
<p className="2xl:mr-[1.111vw] mr-6">{project.title}</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"2xl:mr-[1.111vw] mr-6",
|
||||
selectedProject.title !== project.title && "text-[#0D1922]/70"
|
||||
)}
|
||||
>
|
||||
{project.title}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+242
-160
@@ -1,18 +1,20 @@
|
||||
/* 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 { useNavigate, useSearchParams } from "react-router";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { projects } from "../data/projects";
|
||||
import { useDebounce } from "../hooks/useDebounce";
|
||||
import clsx from "clsx";
|
||||
import ProjectSelect from "./ProjectSelect";
|
||||
import Project from "../types/Project";
|
||||
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[];
|
||||
@@ -24,12 +26,24 @@ export interface Filters {
|
||||
|
||||
function SearchFilters({
|
||||
inModal = false,
|
||||
filters,
|
||||
setInModal,
|
||||
ref,
|
||||
cost,
|
||||
floor,
|
||||
area,
|
||||
setCost,
|
||||
setFloor,
|
||||
setArea,
|
||||
}: {
|
||||
inModal?: boolean;
|
||||
filters?: Filters;
|
||||
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[]>([]);
|
||||
@@ -37,57 +51,52 @@ function SearchFilters({
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
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");
|
||||
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) {
|
||||
setCurrentMinCost(filters.cost[0]);
|
||||
setCurrentMaxCost(filters.cost[1]);
|
||||
setCurrentMinArea(filters.area[0]);
|
||||
setCurrentMaxArea(filters.area[1]);
|
||||
setCurrentMinFloor(filters.floor[0]);
|
||||
setCurrentMaxFloor(filters.floor[1]);
|
||||
setCostInModal(filters.cost);
|
||||
setAreaInModal(filters.area);
|
||||
setFloorInModal(filters.floor);
|
||||
}
|
||||
}, [filters]);
|
||||
}, [filters, setAreaInModal, setCostInModal, setFloorInModal]);
|
||||
|
||||
const [currentMinCost, setCurrentMinCost] = useState<number>();
|
||||
const [currentMaxCost, setCurrentMaxCost] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (inModal) return;
|
||||
setCost(costInModal);
|
||||
setArea(areaInModal);
|
||||
setFloor(floorInModal);
|
||||
}, [setCost, costInModal, setArea, areaInModal, setFloor, floorInModal]);
|
||||
|
||||
const [currentMinArea, setCurrentMinArea] = useState<number>();
|
||||
const [currentMaxArea, setCurrentMaxArea] = useState<number>();
|
||||
|
||||
const [currentMinFloor, setCurrentMinFloor] = useState<number>();
|
||||
const [currentMaxFloor, setCurrentMaxFloor] = useState<number>();
|
||||
|
||||
function resetFilters() {
|
||||
setUnitTypes([]);
|
||||
if (filters) {
|
||||
setCurrentMinCost(filters.cost[0]);
|
||||
setCurrentMaxCost(filters.cost[1]);
|
||||
setCurrentMinArea(filters.area[0]);
|
||||
setCurrentMaxArea(filters.area[1]);
|
||||
setCurrentMinFloor(filters.floor[0]);
|
||||
setCurrentMaxFloor(filters.floor[1]);
|
||||
}
|
||||
navigate("/search");
|
||||
}
|
||||
|
||||
const debouncedMinCost = useDebounce(currentMinCost, 500);
|
||||
const debouncedMaxCost = useDebounce(currentMaxCost, 500);
|
||||
|
||||
const debouncedMinArea = useDebounce(currentMinArea, 500);
|
||||
const debouncedMaxArea = useDebounce(currentMaxArea, 500);
|
||||
|
||||
const debouncedMinFloor = useDebounce(currentMinFloor, 500);
|
||||
const debouncedMaxFloor = useDebounce(currentMaxFloor, 500);
|
||||
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: [
|
||||
@@ -96,35 +105,39 @@ function SearchFilters({
|
||||
project,
|
||||
unitTypes,
|
||||
view,
|
||||
debouncedMinCost,
|
||||
debouncedMaxCost,
|
||||
debouncedMinArea,
|
||||
debouncedMaxArea,
|
||||
debouncedMinFloor,
|
||||
debouncedMaxFloor,
|
||||
debouncedCost,
|
||||
debouncedArea,
|
||||
debouncedFloor,
|
||||
],
|
||||
enabled: !!project,
|
||||
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}` : ""}${
|
||||
debouncedMinCost && debouncedMaxCost
|
||||
? `&cost=${Math.round(debouncedMinCost)},${Math.round(
|
||||
debouncedMaxCost
|
||||
debouncedCost
|
||||
? `&cost=${Math.round(debouncedCost[0])},${Math.round(
|
||||
debouncedCost[1]
|
||||
)}`
|
||||
: ""
|
||||
}${
|
||||
debouncedMinArea && debouncedMaxArea
|
||||
? `&area=${Math.round(debouncedMinArea)},${Math.round(
|
||||
debouncedMaxArea
|
||||
debouncedArea
|
||||
? `&area=${Math.round(debouncedArea[0])},${Math.round(
|
||||
debouncedArea[1]
|
||||
)}`
|
||||
: ""
|
||||
}${
|
||||
debouncedMinFloor && debouncedMaxFloor
|
||||
? `&floor=${Math.round(debouncedMinFloor)},${Math.round(
|
||||
debouncedMaxFloor
|
||||
debouncedFloor
|
||||
? `&floor=${Math.round(debouncedFloor[0])},${Math.round(
|
||||
debouncedFloor[1]
|
||||
)}`
|
||||
: ""
|
||||
}`
|
||||
@@ -132,123 +145,192 @@ function SearchFilters({
|
||||
.json<number>(),
|
||||
});
|
||||
|
||||
function handleProjectSelect(project: Project) {
|
||||
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);
|
||||
setSearchParams((prev) => {
|
||||
prev.set("project", project.title);
|
||||
return prev;
|
||||
});
|
||||
if (!inModal)
|
||||
setSearchParams((prev) => {
|
||||
prev.set("project", project.title);
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
function handleUnitTypesSelect(unitTypes: string[]) {
|
||||
setUnitTypes(unitTypes);
|
||||
}
|
||||
|
||||
function handleViewSelect(view: string) {
|
||||
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 (
|
||||
<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 top-[7.222vw] w-[calc(100vw-4.444vw)]"
|
||||
<>
|
||||
{inModal && (
|
||||
<div
|
||||
className="fixed inset-0 2xl:top-[4.444vw] bg-[#0D1922]/40 cursor-pointer"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<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={handleProjectSelect}
|
||||
defaultProject={projects.find(({ title }) => title === project)!}
|
||||
/>
|
||||
<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>
|
||||
<hr className="2xl:h-[0.069vw] h-px border-[#E2E2DC]" />
|
||||
{filters && (
|
||||
<>
|
||||
<div className="2xl:space-y-[0.556vw] space-y-2">
|
||||
<p>Apartment type</p>
|
||||
<UnitTypesSelect
|
||||
filters={filters}
|
||||
onSelect={handleUnitTypesSelect}
|
||||
/>
|
||||
</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={currentMinCost!}
|
||||
currentMax={currentMaxCost!}
|
||||
offset={1}
|
||||
onChangeMin={setCurrentMinCost}
|
||||
onChangeMax={setCurrentMaxCost}
|
||||
label="Cost, AED"
|
||||
/>
|
||||
<MultiRangeSlider
|
||||
min={filters?.floor[0]}
|
||||
max={filters?.floor[1]}
|
||||
currentMin={currentMinFloor!}
|
||||
currentMax={currentMaxFloor!}
|
||||
offset={1}
|
||||
onChangeMin={setCurrentMinFloor}
|
||||
onChangeMax={setCurrentMaxFloor}
|
||||
label="Floor"
|
||||
/>
|
||||
<MultiRangeSlider
|
||||
min={filters?.area[0]}
|
||||
max={filters?.area[1]}
|
||||
currentMin={currentMinArea!}
|
||||
currentMax={currentMaxArea!}
|
||||
offset={1}
|
||||
onChangeMin={setCurrentMinArea}
|
||||
onChangeMax={setCurrentMaxArea}
|
||||
label="Total Area, Sqft"
|
||||
/>
|
||||
<Select
|
||||
label="View"
|
||||
options={["Any view", ...filters.views]}
|
||||
onSelect={handleViewSelect}
|
||||
defaultOption={view}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4">
|
||||
{inModal ? (
|
||||
<Button>Show {count} apartments</Button>
|
||||
) : (
|
||||
<AnimatePresence mode="wait">
|
||||
{count && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
key={count}
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ function UnitCard({
|
||||
<div className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl flex flex-col justify-between 2xl:gap-[1.111vw] gap-4 bg-white 2xl:aspect-[332/396] md:max-2xl:aspect-[352/396] aspect-[328/396]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="2xl:space-y-[0.278vw] space-y-1">
|
||||
<p>{project}</p>
|
||||
<p className="text-s text-[#00BED7]">{project}</p>
|
||||
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
|
||||
<p className="text-caption-m">
|
||||
{(unitNo.split("-")[0] === "W" ? "West" : "East") + " Wing"}
|
||||
@@ -37,7 +37,7 @@ function UnitCard({
|
||||
{squareFt.toLocaleString(undefined, { maximumFractionDigits: 2 })}{" "}
|
||||
Sqft
|
||||
</p>
|
||||
<p className="text-[#00BED7]">
|
||||
<p className="text-[#00BED7] text-subheadline-s font-medium">
|
||||
AED {Intl.NumberFormat("en").format(salesPrice)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useState } from "react";
|
||||
import { Filters } from "./SearchFilters";
|
||||
import clsx from "clsx";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
function UnitTypesSelect({
|
||||
filters,
|
||||
unitTypes,
|
||||
onSelect,
|
||||
defaultSelected = [],
|
||||
}: {
|
||||
filters: Filters;
|
||||
unitTypes: string[];
|
||||
onSelect: (unitTypes: string[]) => void;
|
||||
defaultSelected: string[];
|
||||
}) {
|
||||
const [selectedUnitTypes, setSelectedUnitTypes] = useState<string[]>([]);
|
||||
const [selectedUnitTypes, setSelectedUnitTypes] =
|
||||
useState<string[]>(defaultSelected);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
useEffect(() => setSelectedUnitTypes(defaultSelected), [defaultSelected]);
|
||||
|
||||
useEffect(() => onSelect(selectedUnitTypes), [onSelect, selectedUnitTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
const unitTypesValue = searchParams.getAll("unitTypes");
|
||||
if (unitTypesValue) setSelectedUnitTypes(unitTypesValue);
|
||||
}, [searchParams]);
|
||||
useEffect(() => onSelect(selectedUnitTypes), [selectedUnitTypes]);
|
||||
|
||||
return (
|
||||
<div className="flex 2xl:gap-[0.556vw] gap-2">
|
||||
{filters.unitTypes.map((unitType) => (
|
||||
<div
|
||||
{unitTypes.map((unitType) => (
|
||||
<p
|
||||
key={unitType}
|
||||
onClick={() => {
|
||||
setSelectedUnitTypes((prev) =>
|
||||
@@ -32,22 +29,16 @@ function UnitTypesSelect({
|
||||
? prev.filter((type) => type !== unitType)
|
||||
: [...prev, unitType]
|
||||
);
|
||||
setSearchParams((prev) => {
|
||||
if (prev.getAll("unitTypes").includes(unitType))
|
||||
prev.delete("unitTypes", unitType);
|
||||
else prev.append("unitTypes", unitType);
|
||||
return prev;
|
||||
});
|
||||
}}
|
||||
className={clsx(
|
||||
"2xl:px-[1.389vw] 2xl:py-[0.833vw] px-5 py-3 2xl:rounded-[2.778vw] rounded-[40px] 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer",
|
||||
"2xl:px-[1.389vw] 2xl:py-[0.833vw] px-5 py-3 2xl:rounded-[2.778vw] rounded-[40px] 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer text-s",
|
||||
selectedUnitTypes.includes(unitType)
|
||||
? "ring-[#00BED7]"
|
||||
: "ring-[#E2E2DC]"
|
||||
: "ring-[#E2E2DC] text-[#0D1922]/70"
|
||||
)}
|
||||
>
|
||||
{unitType}
|
||||
</div>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,7 @@ interface IMultiRangeSlider {
|
||||
offset: number;
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
onChangeMin: (min: number) => void;
|
||||
onChangeMax: (max: number) => void;
|
||||
onChange: (value: [number, number]) => void;
|
||||
}
|
||||
|
||||
function MultiRangeSlider({
|
||||
@@ -25,8 +24,7 @@ function MultiRangeSlider({
|
||||
currentMin,
|
||||
max,
|
||||
min,
|
||||
onChangeMin,
|
||||
onChangeMax,
|
||||
onChange,
|
||||
offset,
|
||||
label,
|
||||
disabled = false,
|
||||
@@ -55,8 +53,8 @@ function MultiRangeSlider({
|
||||
const value = calculateValue(clientX, current === "min");
|
||||
|
||||
if (value !== undefined) {
|
||||
if (current === "min") onChangeMin(value);
|
||||
else onChangeMax(value);
|
||||
if (current === "min") onChange([value, currentMax]);
|
||||
else onChange([currentMin, value]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ function Select({
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsShow(false));
|
||||
|
||||
useEffect(() => setSelectedOption(defaultOption), [defaultOption]);
|
||||
|
||||
useEffect(() => onSelect(selectedOption), [selectedOption]);
|
||||
|
||||
return (
|
||||
|
||||
+72
-53
@@ -1,15 +1,16 @@
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery } 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, { Filters } from "../components/SearchFilters";
|
||||
import SearchFilters from "../components/SearchFilters";
|
||||
import { useNavigate, 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 { useDebounce } from "../hooks/useDebounce";
|
||||
|
||||
const STEP = 12;
|
||||
|
||||
@@ -17,42 +18,40 @@ function SearchPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// const project = searchParams.get("project");
|
||||
// const unitTypes = searchParams.getAll("unitTypes");
|
||||
// const cost = searchParams.getAll("cost");
|
||||
// const floor = searchParams.getAll("floor");
|
||||
// const area = searchParams.getAll("area");
|
||||
// const view = searchParams.get("view");
|
||||
const project = searchParams.get("project");
|
||||
const unitTypes = searchParams.getAll("unitTypes");
|
||||
const view = searchParams.get("view");
|
||||
|
||||
const [project, setProject] = useState<string>();
|
||||
const [unitTypes, setUnitTypes] = useState<string[]>([]);
|
||||
const [cost, setCost] = useState<string[]>([]);
|
||||
const [floor, setFloor] = useState<string[]>([]);
|
||||
const [area, setArea] = useState<string[]>([]);
|
||||
const [view, setView] = useState<string>();
|
||||
const [filtersInModal, setFiltersInModal] = useState(false);
|
||||
|
||||
const { data: filters } = useQuery({
|
||||
queryKey: ["filters", project],
|
||||
enabled: !!project,
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(`units/filters?${project ? `project=${project}` : ""}`)
|
||||
.json<Filters>(),
|
||||
});
|
||||
const [cost, setCost] = useState<[number, number]>([-1, -1]);
|
||||
const [floor, setFloor] = useState<[number, number]>([-1, -1]);
|
||||
const [area, setArea] = useState<[number, number]>([-1, -1]);
|
||||
|
||||
useEffect(() => {
|
||||
setProject(searchParams.get("project"));
|
||||
setUnitTypes(searchParams.getAll("unitTypes"));
|
||||
setCost(searchParams.getAll("cost"));
|
||||
setFloor(searchParams.getAll("floor"));
|
||||
setArea(searchParams.getAll("area"));
|
||||
setView(searchParams.get("view"));
|
||||
}, [searchParams]);
|
||||
const debouncedCost = useDebounce(cost, 500);
|
||||
const debouncedFloor = useDebounce(floor, 500);
|
||||
const debouncedArea = useDebounce(area, 500);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
initialPageParam: 0,
|
||||
queryKey: ["units", project, unitTypes, view, cost, floor, area],
|
||||
queryKey: [
|
||||
"units",
|
||||
project,
|
||||
unitTypes,
|
||||
view,
|
||||
debouncedCost,
|
||||
debouncedFloor,
|
||||
debouncedArea,
|
||||
],
|
||||
enabled:
|
||||
!!project &&
|
||||
debouncedCost[0] >= 0 &&
|
||||
debouncedCost[1] >= 0 &&
|
||||
debouncedFloor[0] >= 0 &&
|
||||
debouncedFloor[1] >= 0 &&
|
||||
debouncedArea[0] >= 0 &&
|
||||
debouncedArea[1] >= 0,
|
||||
queryFn: async ({ pageParam = 0 }) =>
|
||||
await api
|
||||
.get(
|
||||
@@ -60,9 +59,19 @@ function SearchPage() {
|
||||
project ? `&project=${project}` : ""
|
||||
}${unitTypes.map((unitType) => `&unitTypes=${unitType}`).join("")}${
|
||||
view ? `&view=${view}` : ""
|
||||
}${cost.length > 0 ? `&cost=${cost.join(",")}` : ""}${
|
||||
floor.length > 0 ? `&floor=${floor.join(",")}` : ""
|
||||
}${area.length > 0 ? `&area=${area.join(",")}` : ""}`
|
||||
}${
|
||||
debouncedCost.length > 0
|
||||
? `&cost=${debouncedCost.map(Math.round).join(",")}`
|
||||
: ""
|
||||
}${
|
||||
debouncedFloor.length > 0
|
||||
? `&floor=${debouncedFloor.map(Math.round).join(",")}`
|
||||
: ""
|
||||
}${
|
||||
debouncedArea.length > 0
|
||||
? `&area=${debouncedArea.map(Math.round).join(",")}`
|
||||
: ""
|
||||
}`
|
||||
)
|
||||
.json<IUnit[]>(),
|
||||
getNextPageParam: (lastPage, _, lastPageIndex) =>
|
||||
@@ -72,8 +81,6 @@ function SearchPage() {
|
||||
const filtersRef = useRef<HTMLDivElement>(null);
|
||||
const observerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [filtersInModal, setFiltersInModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
const observerElement = observerRef.current;
|
||||
@@ -119,27 +126,39 @@ function SearchPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<SearchFilters
|
||||
ref={filtersRef}
|
||||
inModal={filtersInModal}
|
||||
filters={filters}
|
||||
setInModal={setFiltersInModal}
|
||||
{...{ area, cost, floor, setArea, setCost, setFloor }}
|
||||
/>
|
||||
<div className="2xl:p-[2.222vw] p-4">
|
||||
<div className="2xl:p-[2.222vw] p-4 min-h-dvh">
|
||||
<AnimatePresence mode="wait">
|
||||
{project && unitTypes && cost && area && floor && (
|
||||
<motion.div
|
||||
key={project + unitTypes + view + cost + area + floor}
|
||||
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} />)
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
{project &&
|
||||
unitTypes &&
|
||||
debouncedCost &&
|
||||
debouncedArea &&
|
||||
debouncedFloor && (
|
||||
<motion.div
|
||||
key={
|
||||
project +
|
||||
unitTypes +
|
||||
view +
|
||||
debouncedCost +
|
||||
debouncedArea +
|
||||
debouncedFloor
|
||||
}
|
||||
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} />)
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{showButtons && (
|
||||
@@ -170,7 +189,7 @@ function SearchPage() {
|
||||
</div>
|
||||
)}
|
||||
<div ref={observerRef} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user