search page completed, unit cards skeletons

This commit is contained in:
2025-05-06 18:27:07 +05:00
parent 34e249b8f8
commit b7b318b565
8 changed files with 531 additions and 405 deletions
+3
View File
@@ -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=="],
+1
View File
@@ -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",
+7 -5
View File
@@ -687,11 +687,13 @@ function Map({ maxZoom = 1 }: MapProps) {
</motion.div>
))}
</AnimatePresence>
<FullScreenButton
isFullScreen={isFullScreen}
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
<div className="absolute 2xl:right-[2.222vw] 2xl:top-[2.222vw] right-8 bottom-8">
<FullScreenButton
isFullScreen={isFullScreen}
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
</div>
<div className="absolute 2xl:right-[2.222vw] 2xl:bottom-[2.222vw] right-4 bottom-4 flex 2xl:gap-[0.556vw] gap-2">
<DisclaimerButton />
<PrivacyPolicyButton />
+429 -334
View File
@@ -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<string[]>(),
});
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<string[]>(),
@@ -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<string[]>(),
});
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<number>(),
});
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 && (
<div
className="fixed inset-0 z-20 bg-[#0D1922]/40 cursor-pointer"
onClick={handleClose}
onClick={() => setInModal(false)}
/>
)}
<div
@@ -393,175 +422,241 @@ function SearchFilters({
<Button
onlyIcon
className="absolute right-[2.222vw] !bg-[#F3F3F2]"
onClick={handleClose}
onClick={() => setInModal(false)}
>
<div className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]">
<CloseIcon />
</div>
</Button>
)}
{costInFilters &&
areaInFilters &&
floorInFilters &&
viewsInFilters &&
unitTypesInFilters && (
<>
<div className="2xl:space-y-[2.222vw] space-y-8">
<div className="2xl:space-y-[1.111vw] space-y-4">
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-medium leading-[135%]">
{inModal ? "Filters" : "Search"}
</p>
<div className={clsx(!inModal && "max-md:hidden")}>
{project && (
<ProjectSelect
projects={projects}
onSelect={handleSelectProject}
defaultProject={
projects.find(({ title }) => title === project)!
}
/>
)}
</div>
</div>
<hr
className={clsx(
"2xl:h-[0.069vw] h-px border-[#E2E2DC]",
!inModal && "max-md:hidden"
)}
/>
</div>
<>
<div
className={clsx(
"2xl:space-y-[0.556vw] space-y-2",
!inModal && "max-md:hidden"
)}
>
<p className="text-s text-[#0D1922]/70">Apartment type</p>
<UnitTypesSelect
unitTypes={unitTypesInFilters}
onSelect={handleSelectUnitTypes}
defaultSelected={unitTypes}
/>
</div>
<div
className={clsx(
"grid 2xl:grid-cols-4 md:max-2xl:grid-cols-2 md:max-2xl:grid-rows-2 2xl:gap-[1.111vw] gap-6",
!inModal && "max-md:hidden"
)}
>
<MultiRangeSlider
{...costInFilters}
currentMin={inModal ? costInModal[0] : cost[0]}
currentMax={inModal ? costInModal[1] : cost[1]}
offset={1}
onChange={handleCostChange}
label="Cost, AED"
/>
<MultiRangeSlider
{...floorInFilters}
currentMin={inModal ? floorInModal[0] : floor[0]}
currentMax={inModal ? floorInModal[1] : floor[1]}
offset={1}
onChange={handleFloorChange}
label="Floor"
/>
<MultiRangeSlider
{...areaInFilters}
currentMin={inModal ? areaInModal[0] : area[0]}
currentMax={inModal ? areaInModal[1] : area[1]}
offset={1}
onChange={handleAreaChange}
label="Total Area, Sqft"
/>
<Select
defaultOption={view}
label="View"
options={["Any view", ...viewsInFilters]}
onSelect={handleSelectView}
/>
</div>
<div
className={clsx(
"flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4 gap-2",
inModal &&
"max-md:flex-col max-md:sticky max-md:shadow-[0px_-4px_20px_rgba(0,0,0,0.05)] max-md:rounded-t-2xl max-md:-m-4 max-md:p-4 bottom-0 bg-white"
)}
>
{inModal ? (
<Button onClick={applyFilters} className="max-md:w-full">
Show{" "}
<AnimatePresence mode="wait">
{count !== undefined && (
<motion.span
key={count}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{count}
</motion.span>
)}
</AnimatePresence>{" "}
apartments
</Button>
) : (
<AnimatePresence mode="wait">
{count && (
<motion.p
key={count}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"text-[#00BED7] text-s",
!inModal && "max-md:hidden"
)}
>
{count} Apartments found
</motion.p>
)}
</AnimatePresence>
)}
<Button
variant="secondary"
className={clsx(
"hidden",
!inModal &&
"max-md:flex !justify-center flex-1 !bg-[#F3F3F2]"
)}
onClick={() => setInModal(true)}
<div className="2xl:space-y-[2.222vw] space-y-8">
<div className="2xl:space-y-[1.111vw] space-y-4">
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-medium leading-[135%]">
{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 }}
>
<div className="w-5 h-5">
<FiltersIcon />
</div>
<p className="text-sm leading-0">Filters</p>
</Button>
<Button
variant="secondary"
onlyIcon={!inModal && innerWidth < 768}
onClick={resetFilters}
className={clsx(
!inModal && "max-md:bg-[#F3F3F2]",
"max-md:!transition-none"
)}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70">
<RestartIcon />
</span>
<p
className={clsx(
"text-s max-md:w-full",
!inModal && "max-md:hidden"
)}
>
Reset filters
</p>
</Button>
</div>
</>
</>
<ProjectSelect
projects={projects}
onSelect={handleSelectProject}
defaultProject={
projects.find(({ title }) => title === project)!
}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<hr
className={clsx(
"2xl:h-[0.069vw] h-px border-[#E2E2DC]",
!inModal && "max-md:hidden"
)}
/>
</div>
<AnimatePresence mode="wait">
{unitTypesInFilters && (
<motion.div
key={unitTypesInFilters.join()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"2xl:space-y-[0.556vw] space-y-2",
!inModal && "max-md:hidden"
)}
>
<p className="text-s text-[#0D1922]/70">Apartment type</p>
<UnitTypesSelect
unitTypes={unitTypesInFilters}
onSelect={handleSelectUnitTypes}
defaultSelected={unitTypes}
/>
</motion.div>
)}
</AnimatePresence>
<div
className={clsx(
"grid 2xl:grid-cols-4 md:max-2xl:grid-cols-2 md:max-2xl:grid-rows-2 2xl:gap-[1.111vw] gap-6",
!inModal && "max-md:hidden"
)}
>
<AnimatePresence mode="wait">
{costInFilters && (
<motion.div
key={`${costInFilters.min}-${costInFilters.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...costInFilters}
currentMin={inModal ? costInModal[0] : cost[0]}
currentMax={inModal ? costInModal[1] : cost[1]}
offset={0}
onChange={setCostInModal}
setTouched={setCostTouched}
label="Cost, AED"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{floorInFilters && (
<motion.div
key={`${floorInFilters.min}-${floorInFilters.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...floorInFilters}
currentMin={inModal ? floorInModal[0] : floor[0]}
currentMax={inModal ? floorInModal[1] : floor[1]}
offset={0}
onChange={setFloorInModal}
setTouched={setFloorTouched}
label="Floor"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{areaInFilters && (
<motion.div
key={`${areaInFilters.min}-${areaInFilters.max}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<MultiRangeSlider
{...areaInFilters}
currentMin={inModal ? areaInModal[0] : area[0]}
currentMax={inModal ? areaInModal[1] : area[1]}
offset={0}
onChange={setAreaInModal}
setTouched={setAreaTouched}
label="Total Area, Sqft"
/>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{viewsInFilters && (
<motion.div
key={viewsInFilters.join()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Select
defaultOption={view}
label="View"
options={["Any view", ...viewsInFilters]}
onSelect={handleSelectView}
/>
</motion.div>
)}
</AnimatePresence>
</div>
<div
className={clsx(
"flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4 gap-2",
inModal &&
"max-md:flex-col max-md:sticky max-md:shadow-[0px_-4px_20px_rgba(0,0,0,0.05)] max-md:rounded-t-2xl max-md:-m-4 max-md:p-4 bottom-0 bg-white"
)}
>
{inModal ? (
<Button onClick={applyFilters} className="max-md:w-full">
Show{" "}
<AnimatePresence mode="wait">
{count !== undefined && (
<motion.span
key={count}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{count}
</motion.span>
)}
</AnimatePresence>{" "}
apartments
</Button>
) : (
<p
className={clsx(
"text-[#00BED7] text-s",
!inModal && "max-md:hidden"
)}
>
<AnimatePresence mode="wait">
{count ? (
<motion.span
key={count}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{count}
</motion.span>
) : (
<motion.span
key={count}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
...
</motion.span>
)}
</AnimatePresence>
 Apartments found
</p>
)}
<Button
variant="secondary"
className={clsx(
"hidden",
!inModal && "max-md:flex !justify-center flex-1 !bg-[#F3F3F2]"
)}
onClick={() => setInModal(true)}
>
<div className="w-5 h-5">
<FiltersIcon />
</div>
<p className="text-sm leading-0">Filters</p>
</Button>
<Button
variant="secondary"
onlyIcon={!inModal && innerWidth < 768}
onClick={resetFilters}
className={clsx(
!inModal && "max-md:bg-[#F3F3F2]",
"max-md:!transition-none"
)}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70">
<RestartIcon />
</span>
<p
className={clsx(
"text-s max-md:w-full",
!inModal && "max-md:hidden"
)}
>
Reset filters
</p>
</Button>
</div>
</div>
</>
);
+29 -23
View File
@@ -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 (
<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 className='text-s text-[#00BED7]'>{unit.project}</p>
<div className='flex items-center 2xl:gap-[0.556vw] gap-2'>
<p className='text-caption-m'>
{(unit.unitNo.split('-')[0] === 'W' ? 'West' : 'East') + ' Wing'}
<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 className="text-s text-[#00BED7]">
{unit.project || <Skeleton />}
</p>
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
<p className="text-caption-m">
<span>
{`${unit.unitNo.split("-")[0] === "W" ? "West" : "East"} Wing`}
</span>
</p>
<div className='2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full' />
<p className='text-caption-m'>Floor {unit.floor}</p>
<div className='2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full' />
<p className='text-caption-m'>{unit.unitNo}</p>
<div className="2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full" />
<p className="text-caption-m">Floor {unit.floor}</p>
<div className="2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full" />
<p className="text-caption-m">{unit.unitNo}</p>
</div>
</div>
<Button onlyIcon variant='secondary' onClick={handleFavorite}>
<span className='2xl:w-[1.389vw] w-5 aspect-square text-[#0D1922]/70'>
<Button onlyIcon variant="secondary" onClick={handleFavorite}>
<span className="2xl:w-[1.389vw] w-5 aspect-square text-[#0D1922]/70">
{favoriteUnits.some(
(favoriteUnit) => favoriteUnit.id === unit.id
) ? (
@@ -44,15 +50,15 @@ function UnitCard({ unit }: { unit: IUnit }) {
</span>
</Button>
</div>
<div className='2xl:space-y-[0.278vw] space-y-1'>
<p className='text-s'>
<div className="2xl:space-y-[0.278vw] space-y-1">
<p className="text-s">
{`${unit.unitType}, ${unit.squareFt.toLocaleString(undefined, {
maximumFractionDigits: 2,
})} Sqft`}
</p>
<p className='text-[#00BED7] text-subheadline-s font-medium'>
{`AED ${Intl.NumberFormat('ar-AE', {
currency: 'AED',
<p className="text-[#00BED7] text-subheadline-s font-medium">
{`AED ${Intl.NumberFormat("ar-AE", {
currency: "AED",
minimumFractionDigits: 0,
}).format(unit.salesPrice)}`}
</p>
+17 -8
View File
@@ -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({
<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.round(currentMin))}
{Intl.NumberFormat("en").format(Math.ceil(currentMin))}
</p>
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
{Intl.NumberFormat("en").format(Math.round(currentMax))}
{Intl.NumberFormat("en").format(Math.ceil(currentMax))}
</p>
<div className="absolute bottom-0 left-0 w-full 2xl:px-[1.111vw] px-4 translate-y-1/2">
<div
+7 -1
View File
@@ -58,7 +58,13 @@ function Select({
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
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)] rounded-xl bg-white w-full z-10"
ref={(el) => {
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) => (
<button
+38 -34
View File
@@ -12,6 +12,7 @@ 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";
const SORT_OPTIONS = {
"Sort by ascending price": "cost asc",
@@ -43,7 +44,7 @@ function SearchPage() {
"Sort by ascending price"
);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
const { data, fetchNextPage, isLoading, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
initialPageParam: 0,
queryKey: [
@@ -139,36 +140,19 @@ function SearchPage() {
};
}, []);
// const [activeFiltersCount, setActiveFiltersCount] = useState(0);
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
// const queryClient = useQueryClient();
// useEffect(() => {
// const filters = queryClient.getQueryData<Filters>(["filters", project]);
// if (filters) {
// setActiveFiltersCount(
// +!!view +
// +!!unitTypes.length +
// +(debouncedCost[0] !== filters.minCost) +
// +(debouncedCost[1] !== filters.maxCost) +
// +(debouncedArea[0] !== filters.minArea) +
// +(debouncedArea[1] !== filters.maxArea) +
// +(debouncedFloor[0] !== filters.minFloor) +
// +(debouncedFloor[1] !== filters.maxFloor)
// );
// }
// }, [
// queryClient,
// unitTypes,
// view,
// project,
// debouncedCost,
// cost,
// debouncedArea,
// area,
// debouncedFloor,
// floor,
// ]);
useEffect(
() =>
setActiveFiltersCount(
+searchParams.has("view") +
+searchParams.has("unitTypes") +
+searchParams.has("cost") +
+searchParams.has("floor") +
+searchParams.has("area")
),
[searchParams]
);
return (
<>
@@ -187,7 +171,18 @@ function SearchPage() {
/>
<hr className="2xl:h-[0.069vw] border-[#E2E2DC]" />
<AnimatePresence mode="wait">
{project &&
{isLoading ? (
<div className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4">
{Array.from({ length: STEP }).map((_, i) => (
<Skeleton
key={i}
borderRadius={16}
className="2xl:aspect-[332/396] md:max-2xl:aspect-[352/396] aspect-[328/396]"
/>
))}
</div>
) : (
project &&
unitTypes &&
debouncedCost &&
debouncedArea &&
@@ -211,8 +206,17 @@ function SearchPage() {
{data?.pages.map((page) =>
page.map((unit) => <UnitCard key={unit.id} unit={unit} />)
)}
{isFetchingNextPage &&
Array.from({ length: STEP }).map((_, i) => (
<Skeleton
key={"fetching-" + i}
borderRadius={16}
className="2xl:aspect-[332/396] md:max-2xl:aspect-[352/396] aspect-[328/396]"
/>
))}
</motion.div>
)}
)
)}
</AnimatePresence>
</div>
{showButtons && (
@@ -229,11 +233,11 @@ function SearchPage() {
<FiltersIcon />
</span>
<span className="text-caption-m">Filters</span>
{/* {!!activeFiltersCount && (
{!!activeFiltersCount && (
<div className="absolute 2xl:top-[0.139vw] 2xl:right-[0.139vw] top-0.5 right-0.5 rounded-full w-4 text-caption-s aspect-square bg-white text-[#00BED7]">
{activeFiltersCount}
</div>
)} */}
)}
</Button>
<Button
variant="secondary"