This commit is contained in:
2025-05-22 18:37:43 +05:00
6 changed files with 295 additions and 432 deletions
+4 -3
View File
@@ -36,7 +36,7 @@ function Header() {
return (
<>
<header className="sticky top-0 left-0 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white ring ring-[#E2E2DC] z-2">
<header className="sticky top-0 left-0 w-full h-14 md:max-2xl:h-16 2xl:h-[7.5vh] flex items-center justify-center bg-white ring ring-[#E2E2DC] z-2">
<div className="flex 2xl:gap-[1.111vw] gap-4 flex-1">
<div
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
@@ -208,13 +208,14 @@ function NavItem({ href, title }: { href: string; title: string }) {
className={({ isActive }) =>
clsx(
"text-btn-m 2xl:px-[1.25vw] 2xl:py-[0.903vw] p-4 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none max-2xl:text-center max-2xl:bg-[#F3F3F2] relative",
isActive && "!bg-[#00BED7] text-[#FFFFFF]"
isActive &&
"!bg-[#00BED7] text-[#FFFFFF] [&_>div]:bg-white [&_>div]:text-[#00BED7]"
)
}
>
{title}
{title === "Favorites" && !!favoriteUnits.length && (
<div className="absolute 2xl:top-0 2xl:right-0 top-0.5 right-0.5 rounded-full w-5 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s text-center font-mono">
<div className="absolute 2xl:top-1.5 2xl:right-1.5 top-1.5 right-1.5 rounded-full min-w-5 min-h-5 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s text-center font-mono">
{favoriteUnits.length}
</div>
)}
-3
View File
@@ -14,11 +14,9 @@ import Compass from "./Compass";
import PrivacyPolicyButton from "./PrivacyPolicyButton";
import { getWeather } from "../api/weather";
import { isMobile } from "react-device-detect";
// import SelectedComplexCard from "./SelectedComplexCard";
import useWindowSize from "../hooks/useWindowSize";
import TouchIcon from "./icons/map/TouchIcon";
import NewSelectedComplexCard from "./NewSelectedComplexCard";
// import SelectedComplexCard from "./SelectedComplexCard";
interface Position {
x: number;
@@ -702,7 +700,6 @@ function Map({ maxZoom = 1 }: MapProps) {
<Compass />
<AnimatePresence>
{isMobile && hoveredMarker && (
// <SelectedComplexCard marker={hoveredMarker} />
<NewSelectedComplexCard
onClose={() => setHoveredMarker(null)}
marker={hoveredMarker}
+228 -366
View File
@@ -4,9 +4,7 @@ import RestartIcon from "./icons/RestartIcon";
import Button from "./ui/Button";
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";
@@ -20,13 +18,17 @@ import MultiRangeSlider from "./ui/MultiRangeSlider";
function SearchFilters({
inModal = false,
setInModal,
ref,
cost,
floor,
area,
setCost,
floor,
setFloor,
area,
setArea,
selectedUnitTypes,
setSelectedUnitTypes,
view,
setView,
ref,
}: {
inModal?: boolean;
setInModal: (inModal: boolean) => void;
@@ -37,381 +39,248 @@ function SearchFilters({
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;
}) {
const [searchParams, setSearchParams] = useSearchParams();
const [project, setProject] = useState<string>("Rove Home Marasi Drive");
const [project, setProject] = useState<string>();
const [unitTypes, setUnitTypes] = useState<string[]>(
searchParams.getAll("unitTypes") ?? []
);
const [view, setView] = useState("Any view");
const [costInModal, setCostInModal] = useState(cost);
const [areaInModal, setAreaInModal] = useState(area);
const [floorInModal, setFloorInModal] = useState(floor);
const debouncedCost = useDebounce(inModal ? costInModal : cost, 1000);
const debouncedArea = useDebounce(inModal ? areaInModal : area, 1000);
const debouncedFloor = useDebounce(inModal ? floorInModal : floor, 1000);
const [costTouched, setCostTouched] = useState(false);
const [areaTouched, setAreaTouched] = useState(false);
const [floorTouched, setFloorTouched] = useState(false);
const debouncedCostTouched = useDebounce(costTouched, 1000);
const debouncedAreaTouched = useDebounce(areaTouched, 1000);
const debouncedFloorTouched = useDebounce(floorTouched, 1000);
const { data: unitTypesInFilters } = useQuery({
queryKey: [
"filters",
"unitTypes",
project,
searchParams.get("cost"),
searchParams.get("floor"),
searchParams.get("area"),
view,
],
enabled: !!project && !searchParams.has("unitTypes"),
initialData: searchParams.has("unitTypes")
? searchParams.getAll("unitTypes")
: undefined,
const { data: allUnitTypes } = useQuery({
queryKey: ["filters", "unitTypes", project],
queryFn: () =>
api
.get(
`units/filters/unitTypes?${project ? `project=${project}` : ""}${
view !== "Any view" ? `&view=${view}` : ""
}${
searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : ""
}${
searchParams.has("floor")
? `&floor=${searchParams.get("floor")}`
: ""
}${
searchParams.has("area") ? `&area=${searchParams.get("area")}` : ""
}`
)
.json<string[]>(),
api.get(`units/filters/unitTypes?project=${project}`).json<string[]>(),
});
const { data: costInFilters } = useQuery({
queryKey: [
"filters",
"cost",
project,
unitTypes,
searchParams.get("floor"),
searchParams.get("area"),
view,
],
enabled: !!project && !searchParams.has("cost") && !debouncedCostTouched,
initialData: searchParams.has("cost")
? {
min: searchParams.get("cost")!.split(",").map(Number)[0],
max: searchParams.get("cost")!.split(",").map(Number)[1],
}
: undefined,
const { data: allViews } = useQuery({
queryKey: ["filters", "views", project],
queryFn: () =>
api
.get(
`units/filters/cost?${project ? `project=${project}` : ""}${unitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
searchParams.has("floor")
? `&floor=${searchParams.get("floor")}`
: ""
}${
searchParams.has("area") ? `&area=${searchParams.get("area")}` : ""
}`
)
.json<{ min: number; max: number }>(),
api.get(`units/filters/views?project=${project}`).json<string[]>(),
});
const { data: floorInFilters } = useQuery({
queryKey: [
"filters",
"floor",
project,
unitTypes,
searchParams.get("cost"),
searchParams.get("area"),
view,
],
enabled: !!project && !searchParams.has("floor") && !debouncedFloorTouched,
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,
const { data: allFloors } = useQuery({
queryKey: ["filters", "floors", project],
queryFn: () =>
api
.get(
`units/filters/floor?${project ? `project=${project}` : ""}${unitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : ""
}${
searchParams.has("area") ? `&area=${searchParams.get("area")}` : ""
}`
)
.json<{ min: number; max: number }>(),
api.get(`units/filters/floor?project=${project}`).json<{
min: number;
max: number;
}>(),
});
const { data: areaInFilters } = useQuery({
queryKey: [
"filters",
"area",
project,
unitTypes,
searchParams.get("cost"),
searchParams.get("floor"),
view,
],
enabled: !!project && !searchParams.has("area") && !debouncedAreaTouched,
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,
const { data: allCost } = useQuery({
queryKey: ["filters", "cost", project],
queryFn: () =>
api
.get(
`units/filters/area?${project ? `project=${project}` : ""}${unitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : ""
}${
searchParams.has("floor")
? `&floor=${searchParams.get("floor")}`
: ""
}`
)
.json<{ min: number; max: number }>(),
api.get(`units/filters/cost?project=${project}`).json<{
min: number;
max: number;
}>(),
});
const { data: viewsInFilters } = useQuery({
queryKey: [
"filters",
"views",
project,
searchParams.get("cost"),
searchParams.get("floor"),
searchParams.get("area"),
unitTypes,
],
enabled: !!project,
const { data: allArea } = useQuery({
queryKey: ["filters", "area", 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<string[]>(),
api.get(`units/filters/area?project=${project}`).json<{
min: number;
max: number;
}>(),
});
useEffect(() => {
if (allFloors) setFloor([allFloors.min, allFloors.max]);
}, [allFloors]);
useEffect(() => {
if (allCost) setCost([allCost.min, allCost.max]);
}, [allCost]);
useEffect(() => {
if (allArea) setArea([allArea.min, allArea.max]);
}, [allArea]);
// useQuery({
// queryKey: ["filters", "unitTypes", project],
// initialData: allUnitTypes,
// enabled: !!allUnitTypes && !!project,
// queryFn: () =>
// api
// .get(
// `units/filters/unitTypes?project=${project}${
// view !== "Any view" ? `&view=${view}` : ""
// }${
// cost[0] >= 0 && cost[1] >= 0
// ? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
// : ""
// }${
// floor[0] >= 0 && floor[1] >= 0
// ? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
// : ""
// }${
// area[0] >= 0 && area[1] >= 0
// ? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
// : ""
// }`
// )
// .json<string[]>(),
// });
// const { data: updatedCost, isLoading: updatedCostLoading } = useQuery({
// queryKey: ["filters", "cost", project],
// enabled: !!allCost && !!project,
// initialData: allCost,
// queryFn: () =>
// api
// .get(
// `units/filters/cost?project=${project}${selectedUnitTypes
// .map((unitType) => `&unitTypes=${unitType}`)
// .join("")}${view !== "Any view" ? `&view=${view}` : ""}${
// floor[0] >= 0 && floor[1] >= 0
// ? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
// : ""
// }${
// area[0] >= 0 && area[1] >= 0
// ? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
// : ""
// }`
// )
// .json<{ min: number; max: number }>(),
// });
// const { data: updatedFloor, isLoading: updatedFloorLoading } = useQuery({
// queryKey: ["filters", "floor", project],
// enabled: !!allFloors && !!project,
// initialData: allFloors,
// queryFn: () =>
// api
// .get(
// `units/filters/floor?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])}`
// : ""
// }`
// )
// .json<{ min: number; max: number }>(),
// });
// const { data: updatedArea, isLoading: updatedAreaLoading } = useQuery({
// queryKey: ["filters", "area", project],
// enabled: !!allArea && !!project,
// initialData: allArea,
// queryFn: () =>
// api
// .get(
// `units/filters/area?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])}`
// : ""
// }${
// floor[0] >= 0 && floor[1] >= 0
// ? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
// : ""
// }`
// )
// .json<{ min: number; max: number }>(),
// });
// const { data: updatedViews, isLoading: updatedViewsLoading } = useQuery({
// queryKey: ["filters", "views", project],
// enabled: !!allViews && !!project,
// initialData: allViews,
// queryFn: () =>
// api
// .get(
// `units/filters/views?project=${project}${selectedUnitTypes
// .map((unitType) => `&unitTypes=${unitType}`)
// .join("")}${
// cost[0] >= 0 && cost[1] >= 0
// ? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
// : ""
// }${
// floor[0] >= 0 && floor[1] >= 0
// ? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
// : ""
// }${
// area[0] >= 0 && area[1] >= 0
// ? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
// : ""
// }`
// )
// .json<string[]>(),
// });
const { data: count } = useQuery({
queryKey: [
"units",
"count",
project,
unitTypes,
searchParams.get("cost"),
searchParams.get("area"),
searchParams.get("floor"),
cost,
area,
floor,
view,
selectedUnitTypes,
],
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
`units/count?project=${project}${selectedUnitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
searchParams.has("cost") ? `&cost=${searchParams.get("cost")}` : ""
cost[0] >= 0 && cost[1] >= 0
? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
: ""
}${
searchParams.has("area") ? `&area=${searchParams.get("area")}` : ""
area[0] >= 0 && area[1] >= 0
? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
: ""
}${
searchParams.has("floor")
? `&floor=${searchParams.get("floor")}`
floor[0] >= 0 && floor[1] >= 0
? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
: ""
}`
)
.json<number>(),
});
useEffect(() => {
const projectValue = searchParams.get("project") || projects[0].title;
if (projectValue) setProject(projectValue);
const unitTypesValue = searchParams.getAll("unitTypes");
if (unitTypesValue) setUnitTypes(unitTypesValue);
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) setCost(costInModal);
}, [costInModal, inModal]);
useEffect(() => {
if (!inModal) setFloor(floorInModal);
}, [floorInModal, inModal]);
useEffect(() => {
if (!inModal) setArea(areaInModal);
}, [areaInModal, inModal]);
useEffect(() => {
if (debouncedCostTouched)
setSearchParams((prev) => {
prev.set("cost", debouncedCost.map(Math.ceil).join(","));
return prev;
});
}, [debouncedCost, debouncedCostTouched]);
useEffect(() => {
if (debouncedAreaTouched)
setSearchParams((prev) => {
prev.set("area", debouncedArea.map(Math.ceil).join(","));
return prev;
});
}, [debouncedArea, debouncedAreaTouched]);
useEffect(() => {
if (debouncedFloorTouched)
setSearchParams((prev) => {
prev.set("floor", debouncedFloor.map(Math.ceil).join(","));
return prev;
});
}, [debouncedFloor, debouncedFloorTouched]);
function handleSelectProject(project: Project | null) {
setProject(project?.title);
setSearchParams((prev) => {
if (project) prev.set("project", project.title);
else prev.delete("project");
return prev;
});
setProject(project?.title || projects[0].title);
}
function handleSelectUnitTypes(unitTypes: string[]) {
setUnitTypes(unitTypes);
setSearchParams((prev) => {
prev.delete("unitTypes");
unitTypes.forEach((unitType) => prev.append("unitTypes", unitType));
return prev;
});
setSelectedUnitTypes(unitTypes);
}
function handleSelectView(view: string) {
setView(view);
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;
});
setSelectedUnitTypes([]);
setCost([allCost?.min || -1, allCost?.max || -1]);
setFloor([allFloors?.min || -1, allFloors?.max || -1]);
setArea([allArea?.min || -1, allArea?.max || -1]);
if (inModal) setInModal(false);
}
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" });
}
useEffect(() => {
resetFilters();
}, [project]);
useEffect(resetFilters, [project]);
return (
<>
{inModal && (
<div
className="fixed inset-0 bg-[#0D1922]/40 cursor-pointer"
className="fixed inset-0 bg-[#0D1922]/40 cursor-pointer z-5"
onClick={() => setInModal(false)}
/>
)}
@@ -420,7 +289,7 @@ function SearchFilters({
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",
inModal &&
"fixed max-md:pb-0 max-md:pt-6 2xl:top-[calc(2.778vw+4.444vw)] 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)]"
"z-5 fixed 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 && (
@@ -440,24 +309,20 @@ function SearchFilters({
{inModal ? "Filters" : "Search"}
</p>
<div className={clsx(!inModal && "max-md:hidden")}>
<AnimatePresence mode="wait">
{project && (
<motion.div
key={project}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<ProjectSelect
projects={projects}
onSelect={handleSelectProject}
defaultProject={
projects.find(({ title }) => title === project)!
}
/>
</motion.div>
)}
</AnimatePresence>
<motion.div
key={project}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<ProjectSelect
projects={projects}
onSelect={handleSelectProject}
defaultProject={
projects.find(({ title }) => title === project)!
}
/>
</motion.div>
</div>
</div>
<hr
@@ -468,9 +333,9 @@ function SearchFilters({
/>
</div>
<AnimatePresence mode="wait">
{unitTypesInFilters && (
{allUnitTypes && (
<motion.div
key={unitTypesInFilters.join()}
key={allUnitTypes.join()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -481,9 +346,9 @@ function SearchFilters({
>
<p className="text-s text-[#0D1922]/70">Apartment type</p>
<UnitTypesSelect
unitTypes={unitTypesInFilters}
unitTypes={allUnitTypes}
onSelect={handleSelectUnitTypes}
defaultSelected={unitTypes}
defaultSelected={selectedUnitTypes}
/>
</motion.div>
)}
@@ -495,69 +360,66 @@ function SearchFilters({
)}
>
<AnimatePresence mode="wait">
{costInFilters && (
{allCost && (
<motion.div
key={`${costInFilters.min}-${costInFilters.max}`}
key={`${allCost.min}-${allCost.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...costInFilters}
currentMin={inModal ? costInModal[0] : cost[0]}
currentMax={inModal ? costInModal[1] : cost[1]}
{...allCost}
currentMin={cost[0] === -1 ? allCost.min : cost[0]}
currentMax={cost[1] === -1 ? allCost.max : cost[1]}
offset={0}
onChange={setCostInModal}
setTouched={setCostTouched}
onChange={setCost}
label="Cost, AED"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{floorInFilters && (
{allFloors && (
<motion.div
key={`${floorInFilters.min}-${floorInFilters.max}`}
key={`${allFloors.min}-${allFloors.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...floorInFilters}
currentMin={inModal ? floorInModal[0] : floor[0]}
currentMax={inModal ? floorInModal[1] : floor[1]}
{...allFloors}
currentMin={floor[0] === -1 ? allFloors.min : floor[0]}
currentMax={floor[1] === -1 ? allFloors.max : floor[1]}
offset={0}
onChange={setFloorInModal}
setTouched={setFloorTouched}
onChange={setFloor}
label="Floor"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{areaInFilters && (
{allArea && (
<motion.div
key={`${areaInFilters.min}-${areaInFilters.max}`}
key={`${allArea.min}-${allArea.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...areaInFilters}
currentMin={inModal ? areaInModal[0] : area[0]}
currentMax={inModal ? areaInModal[1] : area[1]}
{...allArea}
currentMin={area[0] === -1 ? allArea.min : area[0]}
currentMax={area[1] === -1 ? allArea.max : area[1]}
offset={0}
onChange={setAreaInModal}
setTouched={setAreaTouched}
onChange={setArea}
label="Total Area, Sqft"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{viewsInFilters && (
{allViews && (
<motion.div
key={viewsInFilters.join()}
key={allViews.join()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
@@ -565,7 +427,7 @@ function SearchFilters({
<Select
defaultOption={view}
label="View"
options={["Any view", ...viewsInFilters]}
options={["Any view", ...allViews]}
onSelect={handleSelectView}
/>
</motion.div>
@@ -604,7 +466,7 @@ function SearchFilters({
)}
>
<AnimatePresence mode="wait">
{count ? (
{count !== undefined ? (
<motion.span
key={count}
initial={{ opacity: 0 }}
+19 -18
View File
@@ -17,7 +17,6 @@ interface IMultiRangeSlider {
disabled?: boolean;
label: string;
onChange: (value: [number, number]) => void;
setTouched?: (value: boolean) => void;
}
function MultiRangeSlider({
@@ -28,10 +27,10 @@ function MultiRangeSlider({
onChange,
offset,
label,
setTouched,
disabled = false,
}: IMultiRangeSlider) {
const [current, setCurrent] = useState<"min" | "max" | null>(null);
const rangeRef = useRef<HTMLDivElement>(null);
function calculateValue(clientX: number, isMin: boolean) {
@@ -39,30 +38,32 @@ function MultiRangeSlider({
const rect = rangeRef.current.getBoundingClientRect();
const percentage = (clientX - rect.x) / rect.width;
const value = min + percentage * (max - min);
const newValue = min + percentage * (max - min);
return isMin
? Math.max(Math.min(value, currentMax - offset), min)
: Math.min(Math.max(value, currentMin + offset), max);
? Math.max(Math.min(newValue, currentMax - offset), min)
: Math.min(Math.max(newValue, currentMin + offset), max);
}
const [value, setValue] = useState<[number, number]>([min, max]);
function handleChange(
e: MouseEvent | TouchEvent | ReactMouseEvent | ReactTouchEvent
) {
if (!current || disabled) return;
const { clientX } = "touches" in e ? e.touches[0] : e;
const value = calculateValue(clientX, current === "min");
const newValue = calculateValue(clientX, current === "min");
if (value !== undefined) {
if (current === "min") onChange([value, currentMax]);
else onChange([currentMin, value]);
if (newValue !== undefined) {
if (current === "min") setValue([newValue, currentMax]);
else setValue([currentMin, newValue]);
}
}
function handleMouseUp() {
setCurrent(null);
setTouched?.(true);
onChange(value);
}
useEffect(() => {
@@ -81,10 +82,10 @@ function MultiRangeSlider({
document.removeEventListener("touchend", handleMouseUp);
document.removeEventListener("touchcancel", handleMouseUp);
};
}, [current]);
}, [current, value]);
const getThumbStyle = (value: number) => ({
left: `${((value - min) / (max - min)) * 100}%`,
const getThumbStyle = (newValue: number) => ({
left: `${((newValue - min) / (max - min)) * 100}%`,
});
return (
@@ -92,10 +93,10 @@ function MultiRangeSlider({
<p className="text-s text-[#0D1922]/70">{label}</p>
<div className="bg-white/80 2xl:rounded-[0.833vw] rounded-xl relative 2xl:px-[1.111vw] 2xl:py-[0.972vw] px-4 py-3.5 flex justify-between 2xl:ring-[0.069vw] ring-1 ring-[#E2E2DC]">
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
{Intl.NumberFormat("en").format(Math.ceil(currentMin))}
{Intl.NumberFormat("en").format(Math.ceil(value[0]))}
</p>
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
{Intl.NumberFormat("en").format(Math.ceil(currentMax))}
{Intl.NumberFormat("en").format(Math.ceil(value[1]))}
</p>
<div className="absolute bottom-0 left-0 w-full 2xl:px-[1.111vw] px-4 translate-y-1/2">
<div
@@ -110,8 +111,8 @@ function MultiRangeSlider({
>
<div
style={{
width: `${((currentMax - currentMin) / (max - min)) * 100}%`,
left: `${((currentMin - min) / (max - min)) * 100}%`,
width: `${((value[1] - value[0]) / (max - min)) * 100}%`,
left: `${((value[0] - min) / (max - min)) * 100}%`,
}}
className={clsx(
"2xl:h-[0.139vw] h-0.5 self-center relative",
@@ -123,7 +124,7 @@ function MultiRangeSlider({
key={type}
onMouseDown={() => setCurrent(type as "min" | "max")}
onTouchStart={() => setCurrent(type as "min" | "max")}
style={getThumbStyle(type === "min" ? currentMin : currentMax)}
style={getThumbStyle(type === "min" ? value[0] : value[1])}
className={clsx(
"rounded-full 2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 absolute bottom-0 -translate-x-1/2",
current === type ? "cursor-grabbing" : "cursor-grab",
+1 -1
View File
@@ -5,7 +5,7 @@ function LayoutWithoutFooter() {
return (
<div className="flex flex-col select-none min-h-dvh">
<Header />
<div className="2xl:h-[calc(100dvh-4.444vw)] md:max-2xl:h-[calc(100dvh-64px)] h-[calc(100dvh-56px)]">
<div className="2xl:h-[calc(100dvh-7.5vh)] md:max-2xl:h-[calc(100dvh-64px)] h-[calc(100dvh-56px)]">
<Outlet />
</div>
</div>
+43 -41
View File
@@ -10,7 +10,6 @@ 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";
import Select from "../components/ui/Select";
import Skeleton from "react-loading-skeleton";
import { SORT_OPTIONS } from "../data/sortOptions";
@@ -20,19 +19,18 @@ const STEP = 12;
function SearchPage() {
const [searchParams] = useSearchParams();
const project = searchParams.get("project");
const unitTypes = searchParams.getAll("unitTypes");
const view = searchParams.get("view");
// const project = searchParams.get("project");
const [project] = useState("Rove Home Marasi Drive");
const [selectedUnitTypes, setSelectedUnitTypes] = useState<string[]>([]);
// const unitTypes = searchParams.getAll("unitTypes");
// const view = searchParams.get("view");
const [filtersInModal, setFiltersInModal] = useState(false);
const [cost, setCost] = useState<[number, number]>([-1, -1]);
const [floor, setFloor] = useState<[number, number]>([-1, -1]);
const [area, setArea] = useState<[number, number]>([-1, -1]);
const debouncedCost = useDebounce(cost, 1000);
const debouncedFloor = useDebounce(floor, 1000);
const debouncedArea = useDebounce(area, 1000);
const [view, setView] = useState("Any view");
const [sort, setSort] = useState<keyof typeof SORT_OPTIONS>(
"Sort by ascending price"
@@ -44,45 +42,39 @@ function SearchPage() {
queryKey: [
"units",
project,
unitTypes,
selectedUnitTypes,
view,
debouncedCost,
debouncedFloor,
debouncedArea,
cost,
floor,
area,
sort,
],
enabled:
!!project &&
debouncedCost[0] >= 0 &&
debouncedCost[1] >= 0 &&
debouncedFloor[0] >= 0 &&
debouncedFloor[1] >= 0 &&
debouncedArea[0] >= 0 &&
debouncedArea[1] >= 0,
cost[0] >= 0 &&
cost[1] >= 0 &&
floor[0] >= 0 &&
floor[1] >= 0 &&
area[0] >= 0 &&
area[1] >= 0,
queryFn: async ({ pageParam = 0 }) =>
await api
.get(
`units?offset=${pageParam}&limit=${STEP}${
project ? `&project=${project}` : ""
}${unitTypes.map((unitType) => `&unitTypes=${unitType}`).join("")}${
view ? `&view=${view}` : ""
}${
debouncedCost.length > 0
? `&cost=${Math.floor(debouncedCost[0])},${Math.ceil(
debouncedCost[1]
)}`
}${selectedUnitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${view !== "Any view" ? `&view=${view}` : ""}${
cost.length > 0
? `&cost=${Math.floor(cost[0])},${Math.ceil(cost[1])}`
: ""
}${
debouncedFloor.length > 0
? `&floor=${Math.floor(debouncedFloor[0])},${Math.ceil(
debouncedFloor[1]
)}`
floor.length > 0
? `&floor=${Math.floor(floor[0])},${Math.ceil(floor[1])}`
: ""
}${
debouncedArea.length > 0
? `&area=${Math.floor(debouncedArea[0])},${Math.ceil(
debouncedArea[1]
)}`
area.length > 0
? `&area=${Math.floor(area[0])},${Math.ceil(area[1])}`
: ""
}${sort ? `&order=${SORT_OPTIONS[sort].split(" ").join()}` : ""}`
)
@@ -157,6 +149,10 @@ function SearchPage() {
return (
<>
<SearchFilters
selectedUnitTypes={selectedUnitTypes}
setSelectedUnitTypes={setSelectedUnitTypes}
view={view}
setView={setView}
ref={filtersRef}
inModal={filtersInModal}
setInModal={setFiltersInModal}
@@ -186,21 +182,27 @@ function SearchPage() {
/>
))}
</div>
) : data?.pages[0].length === 0 ? (
<div className="2xl:aspect-[1376/396] md:max-2xl:py-20 max-md:aspect-[328/240] flex justify-center items-center">
<p className="text-h3 font-medium text-[#00BED7] text-center">
No apartments found with the given parameters
</p>
</div>
) : (
project &&
unitTypes &&
debouncedCost &&
debouncedArea &&
debouncedFloor &&
selectedUnitTypes &&
cost &&
area &&
floor &&
sort && (
<motion.div
key={
project +
unitTypes +
selectedUnitTypes +
view +
debouncedCost +
debouncedArea +
debouncedFloor +
cost +
area +
floor +
sort
}
initial={{ opacity: 0 }}