From b7b318b565f549e6911e0fa2b05ed85c0f2aeffa Mon Sep 17 00:00:00 2001 From: Lanskikh Date: Tue, 6 May 2025 18:27:07 +0500 Subject: [PATCH] search page completed, unit cards skeletons --- bun.lock | 3 + package.json | 1 + src/components/Map.tsx | 12 +- src/components/SearchFilters.tsx | 763 ++++++++++++++----------- src/components/UnitCard.tsx | 52 +- src/components/ui/MultiRangeSlider.tsx | 25 +- src/components/ui/Select.tsx | 8 +- src/pages/SearchPage.tsx | 72 +-- 8 files changed, 531 insertions(+), 405 deletions(-) diff --git a/bun.lock b/bun.lock index b67e06e..a99a667 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "react": "^19.0.0", "react-device-detect": "^2.2.3", "react-dom": "^19.0.0", + "react-loading-skeleton": "^3.5.0", "react-router": "^7.5.0", "react-swipeable": "^7.0.2", "tailwindcss": "^4.1.3", @@ -475,6 +476,8 @@ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + "react-loading-skeleton": ["react-loading-skeleton@3.5.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ=="], + "react-router": ["react-router@7.5.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g=="], "react-swipeable": ["react-swipeable@7.0.2", "", { "peerDependencies": { "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w=="], diff --git a/package.json b/package.json index 9b3aaa0..8ebff35 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react": "^19.0.0", "react-device-detect": "^2.2.3", "react-dom": "^19.0.0", + "react-loading-skeleton": "^3.5.0", "react-router": "^7.5.0", "react-swipeable": "^7.0.2", "tailwindcss": "^4.1.3", diff --git a/src/components/Map.tsx b/src/components/Map.tsx index f05e8ff..a16a19d 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -687,11 +687,13 @@ function Map({ maxZoom = 1 }: MapProps) { ))} - +
+ +
diff --git a/src/components/SearchFilters.tsx b/src/components/SearchFilters.tsx index 185bdac..bf8e279 100644 --- a/src/components/SearchFilters.tsx +++ b/src/components/SearchFilters.tsx @@ -50,13 +50,13 @@ function SearchFilters({ const debouncedArea = useDebounce(inModal ? areaInModal : area, 1000); const debouncedFloor = useDebounce(inModal ? floorInModal : floor, 1000); - const [costChanged, setCostChanged] = useState(true); - const [areaChanged, setAreaChanged] = useState(true); - const [floorChanged, setFloorChanged] = useState(true); + const [costTouched, setCostTouched] = useState(false); + const [areaTouched, setAreaTouched] = useState(false); + const [floorTouched, setFloorTouched] = useState(false); - const debouncedCostChanged = useDebounce(costChanged, 1000); - const debouncedAreaChanged = useDebounce(areaChanged, 1000); - const debouncedFloorChanged = useDebounce(floorChanged, 1000); + const debouncedCostChanged = useDebounce(costTouched, 1000); + const debouncedAreaChanged = useDebounce(areaTouched, 1000); + const debouncedFloorChanged = useDebounce(floorTouched, 1000); const [searchParams, setSearchParams] = useSearchParams(); @@ -65,62 +65,28 @@ function SearchFilters({ "filters", "unitTypes", project, - debouncedCostChanged ? debouncedCost : undefined, - debouncedAreaChanged ? debouncedArea : undefined, - debouncedFloorChanged ? debouncedFloor : undefined, + searchParams.get("cost"), + searchParams.get("floor"), + searchParams.get("area"), view, ], - enabled: !!project, + enabled: !!project && !searchParams.has("unitTypes"), + initialData: searchParams.has("unitTypes") + ? searchParams.getAll("unitTypes") + : undefined, queryFn: () => api .get( `units/filters/unitTypes?${project ? `project=${project}` : ""}${ view !== "Any view" ? `&view=${view}` : "" }${ - debouncedCost[0] >= 0 && debouncedCost[1] >= 0 - ? `&cost=${debouncedCost.map(Math.round).join()}` + searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : "" + }${ + searchParams.has("floor") + ? `&floor=${searchParams.get("floor")}` : "" }${ - debouncedFloor[0] >= 0 && debouncedFloor[1] >= 0 - ? `&floor=${debouncedFloor.map(Math.round).join()}` - : "" - }${ - debouncedArea[0] >= 0 && debouncedArea[1] >= 0 - ? `&area=${debouncedArea.map(Math.round).join()}` - : "" - }` - ) - .json(), - }); - - const { data: viewsInFilters } = useQuery({ - queryKey: [ - "filters", - "views", - project, - debouncedCostChanged ? debouncedCost : undefined, - debouncedAreaChanged ? debouncedArea : undefined, - debouncedFloorChanged ? debouncedFloor : undefined, - unitTypes, - ], - enabled: !!project, - queryFn: () => - api - .get( - `units/filters/views?${project ? `project=${project}` : ""}${unitTypes - .map((unitType) => `&unitTypes=${unitType}`) - .join("")}${ - debouncedCost[0] >= 0 && debouncedCost[1] >= 0 - ? `&cost=${debouncedCost.map(Math.round).join()}` - : "" - }${ - debouncedFloor[0] >= 0 && debouncedFloor[1] >= 0 - ? `&floor=${debouncedFloor.map(Math.round).join()}` - : "" - }${ - debouncedArea[0] >= 0 && debouncedArea[1] >= 0 - ? `&area=${debouncedArea.map(Math.round).join()}` - : "" + searchParams.has("area") ? `&area=${searchParams.get("area")}` : "" }` ) .json(), @@ -131,25 +97,29 @@ function SearchFilters({ "filters", "cost", project, - debouncedAreaChanged ? debouncedArea : undefined, - debouncedFloorChanged ? debouncedFloor : undefined, unitTypes, + searchParams.get("floor"), + searchParams.get("area"), view, ], - enabled: !!project, + enabled: !!project && !searchParams.has("cost") && !debouncedCostChanged, + initialData: searchParams.has("cost") + ? { + min: searchParams.get("cost")!.split(",").map(Number)[0], + max: searchParams.get("cost")!.split(",").map(Number)[1], + } + : undefined, queryFn: () => api .get( `units/filters/cost?${project ? `project=${project}` : ""}${unitTypes .map((unitType) => `&unitTypes=${unitType}`) .join("")}${view !== "Any view" ? `&view=${view}` : ""}${ - debouncedFloor[0] >= 0 && debouncedFloor[1] >= 0 - ? `&floor=${debouncedFloor.map(Math.round).join()}` + searchParams.has("floor") + ? `&floor=${searchParams.get("floor")}` : "" }${ - debouncedArea[0] >= 0 && debouncedArea[1] >= 0 - ? `&area=${debouncedArea.map(Math.round).join()}` - : "" + searchParams.has("area") ? `&area=${searchParams.get("area")}` : "" }` ) .json<{ min: number; max: number }>(), @@ -160,25 +130,32 @@ function SearchFilters({ "filters", "floor", project, - debouncedCostChanged ? debouncedCost : undefined, - debouncedAreaChanged ? debouncedArea : undefined, unitTypes, + searchParams.get("cost"), + searchParams.get("area"), view, ], - enabled: !!project, + enabled: !!project && !searchParams.has("floor") && !debouncedFloorChanged, + initialData: searchParams.has("floor") + ? { + min: searchParams.get("floor")!.split(",").map(Number)[0], + max: searchParams.get("floor")!.split(",").map(Number)[1], + } + : floorInModal.every((bound) => bound >= 0) + ? { + min: floorInModal[0], + max: floorInModal[1], + } + : undefined, queryFn: () => api .get( `units/filters/floor?${project ? `project=${project}` : ""}${unitTypes .map((unitType) => `&unitTypes=${unitType}`) .join("")}${view !== "Any view" ? `&view=${view}` : ""}${ - debouncedCost[0] >= 0 && debouncedCost[1] >= 0 - ? `&cost=${debouncedCost.map(Math.round).join()}` - : "" + searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : "" }${ - debouncedArea[0] >= 0 && debouncedArea[1] >= 0 - ? `&area=${debouncedArea.map(Math.round).join()}` - : "" + searchParams.has("area") ? `&area=${searchParams.get("area")}` : "" }` ) .json<{ min: number; max: number }>(), @@ -189,95 +166,67 @@ function SearchFilters({ "filters", "area", project, - debouncedCostChanged ? debouncedCost : undefined, - debouncedFloorChanged ? debouncedFloor : undefined, unitTypes, + searchParams.get("cost"), + searchParams.get("floor"), view, ], - enabled: !!project, + enabled: !!project && !searchParams.has("area") && !debouncedAreaChanged, + initialData: searchParams.has("area") + ? { + min: searchParams.get("area")!.split(",").map(Number)[0], + max: searchParams.get("area")!.split(",").map(Number)[1], + } + : areaInModal.every((bound) => bound >= 0) + ? { + min: areaInModal[0], + max: areaInModal[1], + } + : undefined, queryFn: () => api .get( `units/filters/area?${project ? `project=${project}` : ""}${unitTypes .map((unitType) => `&unitTypes=${unitType}`) .join("")}${view !== "Any view" ? `&view=${view}` : ""}${ - debouncedCost[0] >= 0 && debouncedCost[1] >= 0 - ? `&cost=${debouncedCost.map(Math.round).join()}` - : "" + searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : "" }${ - debouncedFloor[0] >= 0 && debouncedFloor[1] >= 0 - ? `&floor=${debouncedFloor.map(Math.round).join()}` + searchParams.has("floor") + ? `&floor=${searchParams.get("floor")}` : "" }` ) .json<{ min: number; max: number }>(), }); - 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"; - } - - useEffect(() => { - if (areaInFilters) { - setAreaInModal([areaInFilters.min, areaInFilters.max]); - setAreaChanged(false); - } - }, [areaInFilters]); - - useEffect(() => { - if (costInFilters) { - setCostInModal([costInFilters.min, costInFilters.max]); - return () => setCostChanged(false); - } - }, [costInFilters]); - - useEffect(() => { - if (floorInFilters) { - setFloorInModal([floorInFilters.min, floorInFilters.max]); - setFloorChanged(false); - } - }, [floorInFilters]); - - useEffect(() => { - if (inModal) return; - setCost(costInModal); - }, [costInModal, inModal]); - - useEffect(() => { - if (inModal) return; - setArea(areaInModal); - }, [areaInModal, inModal]); - - useEffect(() => { - if (inModal) return; - setFloor(floorInModal); - }, [floorInModal, inModal]); - - function handleCostChange([min, max]: [number, number]) { - setCostInModal([min, max]); - setCostChanged(true); - } - - function handleFloorChange([min, max]: [number, number]) { - setFloorInModal([min, max]); - setFloorChanged(true); - } - - function handleAreaChange([min, max]: [number, number]) { - setAreaInModal([min, max]); - setAreaChanged(true); - } + const { data: viewsInFilters } = useQuery({ + queryKey: [ + "filters", + "views", + project, + searchParams.get("cost"), + searchParams.get("floor"), + searchParams.get("area"), + unitTypes, + ], + enabled: !!project, + queryFn: () => + api + .get( + `units/filters/views?${project ? `project=${project}` : ""}${unitTypes + .map((unitType) => `&unitTypes=${unitType}`) + .join("")}${ + searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : "" + }${ + searchParams.has("floor") + ? `&floor=${searchParams.get("floor")}` + : "" + }${ + searchParams.has("area") ? `&area=${searchParams.get("area")}` : "" + }` + ) + .json(), + }); const { data: count } = useQuery({ queryKey: [ @@ -285,10 +234,10 @@ function SearchFilters({ "count", project, unitTypes, + searchParams.get("cost"), + searchParams.get("area"), + searchParams.get("floor"), view, - debouncedCost, - debouncedArea, - debouncedFloor, ], enabled: !!project && @@ -304,57 +253,137 @@ function SearchFilters({ `units/count?${project ? `project=${project}` : ""}${unitTypes .map((unitType) => `&unitTypes=${unitType}`) .join("")}${view !== "Any view" ? `&view=${view}` : ""}${ - debouncedCost ? `&cost=${debouncedCost.map(Math.round).join()}` : "" + searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : "" }${ - debouncedArea ? `&area=${debouncedArea.map(Math.round).join()}` : "" + searchParams.has("area") ? `&area=${searchParams.get("area")}` : "" }${ - debouncedFloor - ? `&floor=${debouncedFloor.map(Math.round).join()}` + searchParams.has("floor") + ? `&floor=${searchParams.get("floor")}` : "" }` ) .json(), }); - function handleClose() { + useEffect(() => { 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); - } + + const costValue = searchParams.get("cost"); + if (costValue) + setCostInModal(costValue.split(",").map(Number) as [number, number]); + + const floorValue = searchParams.get("floor"); + if (floorValue) + setFloorInModal(floorValue.split(",").map(Number) as [number, number]); + + const areaValue = searchParams.get("area"); + if (areaValue) + setAreaInModal(areaValue.split(",").map(Number) as [number, number]); + + const viewValue = searchParams.get("view"); + if (viewValue) setView(viewValue); + }, [searchParams]); + + useEffect(() => { + if (costInFilters) setCostInModal([costInFilters.min, costInFilters.max]); + }, [costInFilters]); + + useEffect(() => { + if (floorInFilters) + setFloorInModal([floorInFilters.min, floorInFilters.max]); + }, [floorInFilters]); + + useEffect(() => { + if (areaInFilters) setAreaInModal([areaInFilters.min, areaInFilters.max]); + }, [areaInFilters]); + + useEffect(() => { + if (inModal) return; + setCost(costInModal); + }, [costInModal, inModal]); + + useEffect(() => { + if (inModal) return; + setFloor(floorInModal); + }, [floorInModal, inModal]); + + useEffect(() => { + if (inModal) return; + setArea(areaInModal); + }, [areaInModal, inModal]); + + useEffect(() => { + if (debouncedCostChanged) + setSearchParams((prev) => { + prev.set("cost", `${debouncedCost[0]},${debouncedCost[1]}`); + return prev; + }); + }, [debouncedCost, debouncedCostChanged]); + + useEffect(() => { + if (debouncedAreaChanged) + setSearchParams((prev) => { + prev.set("area", `${debouncedArea[0]},${debouncedArea[1]}`); + return prev; + }); + }, [debouncedArea, debouncedAreaChanged]); + + useEffect(() => { + if (debouncedFloorChanged) + setSearchParams((prev) => { + prev.set("floor", `${debouncedFloor[0]},${debouncedFloor[1]}`); + return prev; + }); + }, [debouncedFloor, debouncedFloorChanged]); function handleSelectProject(project: Project) { setProject(project.title); - if (!inModal) - setSearchParams((prev) => { - prev.set("project", project.title); - return prev; - }); + setSearchParams((prev) => { + prev.set("project", project.title); + return prev; + }); } function handleSelectUnitTypes(unitTypes: string[]) { setUnitTypes(unitTypes); - if (!inModal) - setSearchParams((prev) => { - prev.delete("unitTypes"); - unitTypes.forEach((unitType) => prev.append("unitTypes", unitType)); - return prev; - }); + setSearchParams((prev) => { + prev.delete("unitTypes"); + unitTypes.forEach((unitType) => prev.append("unitTypes", unitType)); + 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; - }); + setSearchParams((prev) => { + if (view !== "Any view") prev.set("view", view); + else prev.delete("view"); + return prev; + }); + } + + function resetFilters() { + setCostTouched(false); + setFloorTouched(false); + setAreaTouched(false); + if (costInFilters) setCostInModal([costInFilters.min, costInFilters.max]); + if (floorInFilters) + setFloorInModal([floorInFilters.min, floorInFilters.max]); + if (areaInFilters) setAreaInModal([areaInFilters.min, areaInFilters.max]); + setView("Any view"); + setUnitTypes([]); + setSearchParams((prev) => { + prev.delete("cost"); + prev.delete("floor"); + prev.delete("area"); + prev.delete("view"); + prev.delete("unitTypes"); + return prev; + }); } function applyFilters() { @@ -378,7 +407,7 @@ function SearchFilters({ {inModal && (
setInModal(false)} /> )}
setInModal(false)} >
)} - {costInFilters && - areaInFilters && - floorInFilters && - viewsInFilters && - unitTypesInFilters && ( - <> -
-
-

- {inModal ? "Filters" : "Search"} -

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

Apartment type

- -
-
- - - - + + )} + +
+
+ {inModal ? ( + + ) : ( +

+ + {count ? ( + + {count} + + ) : ( + + ... + + )} + +  Apartments found +

+ )} + + +
); diff --git a/src/components/UnitCard.tsx b/src/components/UnitCard.tsx index e137007..0acc14a 100644 --- a/src/components/UnitCard.tsx +++ b/src/components/UnitCard.tsx @@ -1,8 +1,10 @@ -import { useFavoritesUnitsStore } from '../stores/useFavoritesUnitsStore'; -import { IUnit } from '../types/IUnit'; -import FilledHeartIcon from './icons/FilledHeartIcon'; -import HeartIcon from './icons/HeartIcon'; -import Button from './ui/Button'; +import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore"; +import { IUnit } from "../types/IUnit"; +import FilledHeartIcon from "./icons/FilledHeartIcon"; +import HeartIcon from "./icons/HeartIcon"; +import Button from "./ui/Button"; +import "react-loading-skeleton/dist/skeleton.css"; +import Skeleton from "react-loading-skeleton"; function UnitCard({ unit }: { unit: IUnit }) { const { favoriteUnits, setFavoriteUnits } = useFavoritesUnitsStore(); @@ -18,22 +20,26 @@ function UnitCard({ unit }: { unit: IUnit }) { } return ( -
-
-
-

{unit.project}

-
-

- {(unit.unitNo.split('-')[0] === 'W' ? 'West' : 'East') + ' Wing'} +

+
+
+

+ {unit.project || } +

+
+

+ + {`${unit.unitNo.split("-")[0] === "W" ? "West" : "East"} Wing`} +

-
-

Floor {unit.floor}

-
-

{unit.unitNo}

+
+

Floor {unit.floor}

+
+

{unit.unitNo}

-
-
-

+

+

{`${unit.unitType}, ${unit.squareFt.toLocaleString(undefined, { maximumFractionDigits: 2, })} Sqft`}

-

- {`AED ${Intl.NumberFormat('ar-AE', { - currency: 'AED', +

+ {`AED ${Intl.NumberFormat("ar-AE", { + currency: "AED", minimumFractionDigits: 0, }).format(unit.salesPrice)}`}

diff --git a/src/components/ui/MultiRangeSlider.tsx b/src/components/ui/MultiRangeSlider.tsx index 53f33b3..54958f8 100644 --- a/src/components/ui/MultiRangeSlider.tsx +++ b/src/components/ui/MultiRangeSlider.tsx @@ -17,6 +17,7 @@ interface IMultiRangeSlider { disabled?: boolean; label: string; onChange: (value: [number, number]) => void; + setTouched?: (value: boolean) => void; } function MultiRangeSlider({ @@ -27,6 +28,7 @@ function MultiRangeSlider({ onChange, offset, label, + setTouched, disabled = false, }: IMultiRangeSlider) { const [current, setCurrent] = useState<"min" | "max" | null>(null); @@ -59,19 +61,26 @@ function MultiRangeSlider({ } function handleMouseUp() { - setCurrent(null); + if (current) { + setCurrent(null); + setTouched?.(true); + } } useEffect(() => { - if (current) { - document.addEventListener("mousemove", handleChange as EventListener); - document.addEventListener("mouseup", handleMouseUp); - document.addEventListener("mouseleave", handleMouseUp); - } + document.addEventListener("mousemove", handleChange as EventListener); + document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("mouseleave", handleMouseUp); + document.addEventListener("touchmove", handleChange as EventListener); + document.addEventListener("touchend", handleMouseUp); + document.addEventListener("touchcancel", handleMouseUp); return () => { document.removeEventListener("mousemove", handleChange as EventListener); document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("mouseleave", handleMouseUp); + document.removeEventListener("touchmove", handleChange as EventListener); + document.removeEventListener("touchend", handleMouseUp); + document.removeEventListener("touchcancel", handleMouseUp); }; }, [current]); @@ -84,10 +93,10 @@ function MultiRangeSlider({

{label}

- {Intl.NumberFormat("en").format(Math.round(currentMin))} + {Intl.NumberFormat("en").format(Math.ceil(currentMin))}

- {Intl.NumberFormat("en").format(Math.round(currentMax))} + {Intl.NumberFormat("en").format(Math.ceil(currentMax))}

{ + if (el) + el.style.maxHeight = `calc(100vh - ${ + el?.getBoundingClientRect().y + }px - 0.278vw)`; + }} + className="absolute 2xl:mt-[0.278vw] 2xl:pt-[0.278vw] mt-1 p-1 2xl:space-y-[0.139vw] space-y-0.5 shadow-[0px_2px_8px_rgba(0,0,0,0.15)] overflow-auto rounded-xl bg-white w-full z-10" > {options.map((option, index) => (
{showButtons && ( @@ -229,11 +233,11 @@ function SearchPage() { Filters - {/* {!!activeFiltersCount && ( + {!!activeFiltersCount && (
{activeFiltersCount}
- )} */} + )}