From 5b09ff703bf913d68750afb48201f51f38ba570d Mon Sep 17 00:00:00 2001 From: Lanskikh Date: Tue, 29 Apr 2025 19:13:25 +0500 Subject: [PATCH] search filters in modal, todo mobile --- .env | 2 +- .gitignore | 1 + src/components/ProjectSelect.tsx | 11 +- src/components/SearchFilters.tsx | 402 +++++++++++++++---------- src/components/UnitCard.tsx | 4 +- src/components/UnitTypesSelect.tsx | 37 +-- src/components/ui/MultiRangeSlider.tsx | 10 +- src/components/ui/Select.tsx | 2 + src/pages/SearchPage.tsx | 125 ++++---- 9 files changed, 348 insertions(+), 246 deletions(-) diff --git a/.env b/.env index cd41370..d965dec 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -VITE_API_URL=http://localhost:3000 \ No newline at end of file +VITE_API_URL=http://192.168.1.250:3000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a547bf3..438657a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env # Editor directories and files .vscode/* diff --git a/src/components/ProjectSelect.tsx b/src/components/ProjectSelect.tsx index 7f79789..e809022 100644 --- a/src/components/ProjectSelect.tsx +++ b/src/components/ProjectSelect.tsx @@ -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" /> -

{project.title}

+

+ {project.title} +

))} diff --git a/src/components/SearchFilters.tsx b/src/components/SearchFilters.tsx index 0b557ea..b47c70f 100644 --- a/src/components/SearchFilters.tsx +++ b/src/components/SearchFilters.tsx @@ -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; + 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(); const [unitTypes, setUnitTypes] = useState([]); @@ -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(), + }); 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(); - const [currentMaxCost, setCurrentMaxCost] = useState(); + useEffect(() => { + if (inModal) return; + setCost(costInModal); + setArea(areaInModal); + setFloor(floorInModal); + }, [setCost, costInModal, setArea, areaInModal, setFloor, floorInModal]); - const [currentMinArea, setCurrentMinArea] = useState(); - const [currentMaxArea, setCurrentMaxArea] = useState(); - - const [currentMinFloor, setCurrentMinFloor] = useState(); - const [currentMaxFloor, setCurrentMaxFloor] = useState(); - - 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(), }); - 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 ( -
+ {inModal && ( +
)} - > -
-

- Search -

- {project && ( - title === project)!} - /> +
+ {inModal && ( + + )} +
+

+ Search +

+ {project && ( + title === project)!} + /> + )} +
+
+ {filters && ( + <> +
+

Apartment type

+ +
+
+ + + + -
-
- {inModal ? ( - - ) : ( - - {count && ( - - {count} Apartments found - - )} - - )} - -
- - )} -
+ ); } diff --git a/src/components/UnitCard.tsx b/src/components/UnitCard.tsx index 30bf05f..dc776bc 100644 --- a/src/components/UnitCard.tsx +++ b/src/components/UnitCard.tsx @@ -14,7 +14,7 @@ function UnitCard({
-

{project}

+

{project}

{(unitNo.split("-")[0] === "W" ? "West" : "East") + " Wing"} @@ -37,7 +37,7 @@ function UnitCard({ {squareFt.toLocaleString(undefined, { maximumFractionDigits: 2 })}{" "} Sqft

-

+

AED {Intl.NumberFormat("en").format(salesPrice)}

diff --git a/src/components/UnitTypesSelect.tsx b/src/components/UnitTypesSelect.tsx index 06a0d11..b858e95 100644 --- a/src/components/UnitTypesSelect.tsx +++ b/src/components/UnitTypesSelect.tsx @@ -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([]); + const [selectedUnitTypes, setSelectedUnitTypes] = + useState(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 (
- {filters.unitTypes.map((unitType) => ( -
( +

{ 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} -

+

))}
); diff --git a/src/components/ui/MultiRangeSlider.tsx b/src/components/ui/MultiRangeSlider.tsx index 3105eff..53f33b3 100644 --- a/src/components/ui/MultiRangeSlider.tsx +++ b/src/components/ui/MultiRangeSlider.tsx @@ -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]); } } diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx index 75f7bba..719d8a1 100644 --- a/src/components/ui/Select.tsx +++ b/src/components/ui/Select.tsx @@ -25,6 +25,8 @@ function Select({ const ref = useClickAway(() => setIsShow(false)); + useEffect(() => setSelectedOption(defaultOption), [defaultOption]); + useEffect(() => onSelect(selectedOption), [selectedOption]); return ( diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index b8b7a2e..59b0965 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -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(); - const [unitTypes, setUnitTypes] = useState([]); - const [cost, setCost] = useState([]); - const [floor, setFloor] = useState([]); - const [area, setArea] = useState([]); - const [view, setView] = useState(); + const [filtersInModal, setFiltersInModal] = useState(false); - const { data: filters } = useQuery({ - queryKey: ["filters", project], - enabled: !!project, - queryFn: () => - api - .get(`units/filters?${project ? `project=${project}` : ""}`) - .json(), - }); + 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(), getNextPageParam: (lastPage, _, lastPageIndex) => @@ -72,8 +81,6 @@ function SearchPage() { const filtersRef = useRef(null); const observerRef = useRef(null); - const [filtersInModal, setFiltersInModal] = useState(false); - useEffect(() => { if (!hasNextPage || isFetchingNextPage) return; const observerElement = observerRef.current; @@ -119,27 +126,39 @@ function SearchPage() { }, []); return ( -
+ <> -
+
- {project && unitTypes && cost && area && floor && ( - - {data?.pages.map((page) => - page.map((unit) => ) - )} - - )} + {project && + unitTypes && + debouncedCost && + debouncedArea && + debouncedFloor && ( + + {data?.pages.map((page) => + page.map((unit) => ) + )} + + )}
{showButtons && ( @@ -170,7 +189,7 @@ function SearchPage() {
)}
-
+ ); }