search pages with filters in process

This commit is contained in:
2025-04-23 17:26:28 +05:00
parent dfab85a424
commit f0a06a9053
29 changed files with 602 additions and 39 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=http://localhost:3000
+5 -5
View File
@@ -5,12 +5,12 @@
"name": "irth-new",
"dependencies": {
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.72.1",
"@tanstack/react-query": "^5.74.4",
"@tweenjs/tween.js": "^25.0.0",
"@uidotdev/usehooks": "^2.4.1",
"clsx": "^2.1.1",
"gsap": "^3.12.7",
"ky": "^1.8.0",
"ky": "^1.8.1",
"motion": "^12.6.3",
"openmeteo": "^1.2.0",
"react": "^19.0.0",
@@ -216,9 +216,9 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="],
"@tanstack/query-core": ["@tanstack/query-core@5.72.1", "", {}, "sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q=="],
"@tanstack/query-core": ["@tanstack/query-core@5.74.4", "", {}, "sha512-YuG0A0+3i9b2Gfo9fkmNnkUWh5+5cFhWBN0pJAHkHilTx6A0nv8kepkk4T4GRt4e5ahbtFj2eTtkiPcVU1xO4A=="],
"@tanstack/react-query": ["@tanstack/react-query@5.72.1", "", { "dependencies": { "@tanstack/query-core": "5.72.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.74.4", "", { "dependencies": { "@tanstack/query-core": "5.74.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-mAbxw60d4ffQ4qmRYfkO1xzRBPUEf/72Dgo3qqea0J66nIKuDTLEqQt0ku++SDFlMGMnB6uKDnEG1xD/TDse4Q=="],
"@tweenjs/tween.js": ["@tweenjs/tween.js@25.0.0", "", {}, "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A=="],
@@ -390,7 +390,7 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"ky": ["ky@1.8.0", "", {}, "sha512-DoKGmG27nT8t/1F9gV8vNzggJ3mLAyD49J8tTMWHeZvS8qLc7GlyTieicYtFzvDznMe/q2u38peOjkWc5/pjvw=="],
"ky": ["ky@1.8.1", "", {}, "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+2 -2
View File
@@ -11,12 +11,12 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.72.1",
"@tanstack/react-query": "^5.74.4",
"@tweenjs/tween.js": "^25.0.0",
"@uidotdev/usehooks": "^2.4.1",
"clsx": "^2.1.1",
"gsap": "^3.12.7",
"ky": "^1.8.0",
"ky": "^1.8.1",
"motion": "^12.6.3",
"openmeteo": "^1.2.0",
"react": "^19.0.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+5
View File
@@ -0,0 +1,5 @@
import ky from "ky";
export const api = ky.create({
prefixUrl: import.meta.env.VITE_API_URL,
});
+1 -1
View File
@@ -10,7 +10,7 @@ function Compass({ imgStyle }: CompassProps) {
<img
src="/images/map/compass.png"
className={clsx(
"2xl:w-[7.222vw] w-26 pointer-events-none absolute 2xl:left-[1.667vw] md:max-2xl:bottom-4 left-4 2xl:bottom-[1.667vw] max-md:hidden z-10"
"2xl:w-[7.222vw] w-26 pointer-events-none absolute 2xl:left-[2.222vw] md:max-2xl:bottom-4 left-4 2xl:bottom-[2.222vw] max-md:hidden"
)}
style={imgStyle}
/>
+48
View File
@@ -0,0 +1,48 @@
import { IUnit } from "../types/IUnit";
import HeartIcon from "./icons/HeartIcon";
import Button from "./ui/Button";
function FlatCard({
project,
unitNo,
floor,
unitType,
squareFt,
salesPrice,
}: IUnit) {
return (
<div className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] rounded-2xl flex flex-col 2xl:gap-[1.111vw] gap-4 bg-white">
<div className="flex items-center justify-between">
<div className="2xl:space-y-[0.278vw] space-y-1">
<p>{project}</p>
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
<p className="text-caption-m">
{(unitNo.split("-")[0] === "W" ? "West" : "East") + " Wing"}
</p>
<div className="2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full" />
<p className="text-caption-m">Floor {floor}</p>
<div className="2xl:w-[0.278vw] w-1 aspect-square bg-[#E2E2DC] rounded-full" />
<p className="text-caption-m">{unitNo}</p>
</div>
</div>
<Button onlyIcon variant="secondary">
<span className="2xl:w-[1.389vw] w-5 aspect-square text-[#0D1922]/70">
<HeartIcon />
</span>
</Button>
</div>
<div className="2xl:space-y-[0.278vw] space-y-1">
<p className="text-s">
{unitType},{" "}
{squareFt.toLocaleString(undefined, { maximumFractionDigits: 2 })}{" "}
Sqft
</p>
<p className="text-[#00BED7]">
AED {Intl.NumberFormat("en").format(salesPrice)}
</p>
</div>
</div>
);
}
export default FlatCard;
+1 -1
View File
@@ -7,7 +7,7 @@ import TwitterIcon from "./icons/TwitterIcon";
function Footer() {
return (
<footer className="lg:px-[2.222vw] lg:pb-[2.222vw] lg:pt-[2.778vw] md:max-lg:p-6 px-4 py-6 grid md:grid-cols-6 grid-cols-3 lg:grid-rows-2 lg:gap-x-[1.667vw] lg:gap-y-[1.111vw] max-lg:gap-y-6 lg:rounded-t-[1.667vw] rounded-t-3xl outline outline-[#E2E2DC]">
<footer className="lg:px-[2.222vw] lg:pb-[2.222vw] lg:pt-[2.778vw] md:max-lg:p-6 px-4 py-6 grid md:grid-cols-6 grid-cols-3 lg:grid-rows-2 lg:gap-x-[1.667vw] lg:gap-y-[1.111vw] max-lg:gap-y-6 lg:rounded-t-[1.667vw] rounded-t-3xl outline outline-[#E2E2DC] bg-white">
<img
src="/images/logo.svg"
className="lg:w-[5.972vw] w-[86px] cursor-pointer"
+1 -1
View File
@@ -34,7 +34,7 @@ function FullScreenButton({
onlyIcon
size="small"
variant="secondary"
className="absolute 2xl:top-[1.667vw] 2xl:right-[1.667vw] top-4 right-4"
className="absolute 2xl:top-[2.222vw] 2xl:right-[2.222vw] top-4 right-4"
onClick={handleClick}
>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
+2 -2
View File
@@ -32,7 +32,7 @@ function Header() {
</div>
</div>
<NavBar />
<div className="flex-1 flex justify-end">
<div className="flex justify-end flex-1">
<ProfileBar />
</div>
</header>
@@ -84,7 +84,7 @@ function NavItem({ href, title }: { href: string; title: string }) {
function ProfileBar() {
return (
<Button className="!bg-[#F3F3F2] 2xl:mr-[2.222vw] mr-4" variant="secondary">
<Button className="!bg-[#F3F3F2] 2xl:mr-[1.667vw] mr-4" variant="secondary">
Login
</Button>
);
+2 -2
View File
@@ -561,7 +561,7 @@ function Map({ maxZoom = 1 }: MapProps) {
return (
<div
ref={containerRef}
className="relative h-full overflow-hidden select-none touch-none"
className="touch-none relative h-full overflow-hidden select-none"
style={{ cursor: isDragging ? "grabbing" : "grab" }}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
@@ -692,7 +692,7 @@ function Map({ maxZoom = 1 }: MapProps) {
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
<div className="absolute 2xl:right-[1.667vw] 2xl:bottom-[1.667vw] right-4 bottom-4 flex 2xl:gap-[0.556vw] gap-2">
<div className="absolute 2xl:right-[2.222vw] 2xl:bottom-[2.222vw] right-4 bottom-4 flex 2xl:gap-[0.556vw] gap-2">
<DisclaimerButton />
<PrivacyPolicyButton />
</div>
+32
View File
@@ -0,0 +1,32 @@
import clsx from "clsx";
import { useSearchParams } from "react-router";
function ProjectFilter({ title, img }: { title: string; img: string }) {
const [searchParams, setSearchParams] = useSearchParams();
return (
<div
onClick={() => {
setSearchParams((prev) => {
prev.set("project", title);
return prev;
});
}}
className={clsx(
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:p-[0.278vw] p-1 flex items-center 2xl:gap-[0.556vw] gap-2 text-s 2xl:outline-[0.069vw] outline transition-colors duration-300 cursor-pointer",
searchParams.get("project") === title
? "outline-[#00BED7]"
: "outline-[#E2E2DC]"
)}
>
<img
src={img}
alt=""
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
/>
<p className="2xl:mr-[1.111vw] mr-6">{title}</p>
</div>
);
}
export default ProjectFilter;
+252
View File
@@ -0,0 +1,252 @@
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 ProjectFilter from "./ProjectFilter";
import { useNavigate, useSearchParams } from "react-router";
import UnitTypesFilter from "./UnitTypesFilter";
import { projects } from "../data/projects";
import { useDebounce } from "../hooks/useDebounce";
interface Filters {
unitTypes: string[];
views: string[];
area: [number, number];
cost: [number, number];
floor: [number, number];
}
function SearchFilters({
inModal = false,
ref,
}: {
inModal?: boolean;
ref: RefObject<HTMLDivElement | null>;
}) {
const [project, setProject] = useState<string>();
const [unitTypes, setUnitTypes] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const { data: filters } = useQuery({
queryKey: ["filters", project],
queryFn: () =>
api
.get(`units/filters?${project ? `project=${project}` : ""}`)
.json<Filters>(),
});
useEffect(() => {
const projectValue = searchParams.get("project");
const unitTypesValue = searchParams.getAll("unitTypes");
if (projectValue) setProject(projectValue);
if (unitTypesValue) setUnitTypes(unitTypesValue);
}, [searchParams]);
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]);
}
}, [filters]);
const [currentMinCost, setCurrentMinCost] = useState<number>();
const [currentMaxCost, setCurrentMaxCost] = useState<number>();
const [currentMinArea, setCurrentMinArea] = useState<number>();
const [currentMaxArea, setCurrentMaxArea] = useState<number>();
const [currentMinFloor, setCurrentMinFloor] = useState<number>();
const [currentMaxFloor, setCurrentMaxFloor] = useState<number>();
function resetFilters() {
setProject(undefined);
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 { data: count } = useQuery({
queryKey: [
"units",
"count",
project,
unitTypes,
debouncedMinCost,
debouncedMaxCost,
debouncedMinArea,
debouncedMaxArea,
debouncedMinFloor,
debouncedMaxFloor,
],
queryFn: () =>
api
.get(
`units/count?${project ? `project=${project}` : ""}${unitTypes
.map((unitType) => `&unitTypes=${unitType}`)
.join("")}${
debouncedMinCost && debouncedMaxCost
? `&cost=${Math.round(debouncedMinCost)},${Math.round(
debouncedMaxCost
)}`
: ""
}${
debouncedMinArea && debouncedMaxArea
? `&area=${Math.round(debouncedMinArea)},${Math.round(
debouncedMaxArea
)}`
: ""
}${
debouncedMinFloor && debouncedMaxFloor
? `&floor=${Math.round(debouncedMinFloor)},${Math.round(
debouncedMaxFloor
)}`
: ""
}`
)
.json<number>(),
});
useEffect(() => {
if (debouncedMinCost && debouncedMaxCost)
setSearchParams((prev) => {
prev.set(
"cost",
`${Math.round(debouncedMinCost)},${Math.round(debouncedMaxCost)}`
);
return prev;
});
if (debouncedMinArea && debouncedMaxArea)
setSearchParams((prev) => {
prev.set(
"area",
`${Math.round(debouncedMinArea)},${Math.round(debouncedMaxArea)}`
);
return prev;
});
if (debouncedMinFloor && debouncedMaxFloor)
setSearchParams((prev) => {
prev.set(
"floor",
`${Math.round(debouncedMinFloor)},${Math.round(debouncedMaxFloor)}`
);
return prev;
});
}, [
debouncedMinCost,
debouncedMaxCost,
debouncedMinArea,
debouncedMaxArea,
debouncedMinFloor,
debouncedMaxFloor,
setSearchParams,
]);
return (
<div
ref={ref}
className="2xl:p-[2.222vw] md:max-2xl:p-6 p-4 2xl:-mx-[2.222vw] md:max-2xl:-mx-6 -mx-4 bg-white 2xl:rounded-b-[1.667vw] rounded-b-3xl 2xl:space-y-[2.222vw] md:max-2xl:space-y-8 space-y-4"
>
<div className="2xl:space-y-[1.111vw] space-y-4">
<p className="2xl:text-[2.222vw] md:max-2xl:text-[32px] text-2xl font-semibold leading-[135%]">
Search
</p>
<div className="flex 2xl:gap-[0.556vw] gap-2">
{projects.map((project) => (
<ProjectFilter
key={project.title}
title={project.title}
img={project.img}
/>
))}
</div>
</div>
<hr className="2xl:h-[0.069vw] h-px border-[#E2E2DC]" />
{filters && (
<>
<div className="2xl:space-y-[0.556vw] space-y-2">
<p>Apartment type</p>
<div className="flex 2xl:gap-[0.556vw] gap-2">
{filters.unitTypes.map((unitType) => (
<UnitTypesFilter key={unitType} title={unitType} />
))}
</div>
</div>
<div className="grid 2xl:grid-cols-4 md:max-2xl:grid-cols-2 md:max-2xl:grid-rows-2 2xl:gap-[1.111vw] gap-4">
<MultiRangeSlider
min={filters?.cost[0]}
max={filters?.cost[1]}
currentMin={currentMinCost!}
currentMax={currentMaxCost!}
step={1}
onChangeMin={setCurrentMinCost}
onChangeMax={setCurrentMaxCost}
label="Cost, AED"
/>
<MultiRangeSlider
min={filters?.floor[0]}
max={filters?.floor[1]}
currentMin={currentMinFloor!}
currentMax={currentMaxFloor!}
step={1}
onChangeMin={setCurrentMinFloor}
onChangeMax={setCurrentMaxFloor}
label="Floor"
/>
<MultiRangeSlider
min={filters?.area[0]}
max={filters?.area[1]}
currentMin={currentMinArea!}
currentMax={currentMaxArea!}
step={1}
onChangeMin={setCurrentMinArea}
onChangeMax={setCurrentMaxArea}
label="Total Area, Sqft"
/>
</div>
<div className="flex items-center 2xl:gap-[1.111vw] md:max-2xl:gap-4">
{inModal ? (
<Button>Show {count} apartments</Button>
) : (
<p className="text-[#00BED7] text-s">{count} Apartments found</p>
)}
<Button variant="secondary" onClick={resetFilters}>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70">
<RestartIcon />
</span>
<p className="text-s">Reset filters</p>
</Button>
</div>
</>
)}
</div>
);
}
export default SearchFilters;
+29
View File
@@ -0,0 +1,29 @@
import clsx from "clsx";
import { useSearchParams } from "react-router";
function UnitTypesFilter({ title }: { title: string }) {
const [searchParams, setSearchParams] = useSearchParams();
return (
<div
onClick={() => {
setSearchParams((prev) => {
if (prev.getAll("unitTypes").includes(title))
prev.delete("unitTypes", title);
else prev.append("unitTypes", title);
return prev;
});
}}
className={clsx(
"2xl:px-[1.389vw] 2xl:py-[0.833vw] px-5 py-3 2xl:rounded-[2.778vw] rounded-[40px] outline transition-colors duration-300 cursor-pointer",
searchParams.getAll("unitTypes").includes(title)
? "outline-[#00BED7]"
: "outline-[#E2E2DC]"
)}
>
{title}
</div>
);
}
export default UnitTypesFilter;
+3 -3
View File
@@ -18,20 +18,20 @@ export default function WeatherWidget({
const formattedTime = `${hours}:${minutes}`;
return (
<div className="absolute left-[1.667vw] top-[1.667vw] rounded-2xl space-y-4 min-w-50 w-[8.333vw] p-4 font-medium text-white bg-black/40 pointer-events-none max-[1440px]:hidden backdrop-blur-2xl">
<div className="absolute left-[2.222vw] top-[2.222vw] rounded-2xl space-y-4 min-w-50 w-[8.333vw] p-4 font-medium text-white bg-black/40 pointer-events-none max-[1440px]:hidden backdrop-blur-2xl">
<div>
<div className="flex justify-between">
<p>{day}</p>
<p>{formattedTime}</p>
</div>
<div className="flex justify-between opacity-60">
<div className="opacity-60 flex justify-between">
<p>
{date.getDate()} {month}
</p>
<p>{dayPart}</p>
</div>
</div>
<hr className="border-white -mx-4" />
<hr className="-mx-4 border-white" />
<p className="text-[32px]">{temperature}°C</p>
</div>
);
+12
View File
@@ -0,0 +1,12 @@
function FiltersIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.692 8.333A.68.68 0 0 0 3 9c0 .368.31.667.692.667h1.029c.294.901 1.169 1.555 2.202 1.555s1.908-.654 2.202-1.555h11.183A.68.68 0 0 0 21 9a.68.68 0 0 0-.692-.667H9.125c-.294-.901-1.169-1.555-2.202-1.555s-1.908.654-2.202 1.555zm0 5.778a.68.68 0 0 0-.692.667c0 .368.31.666.692.666h9.414C13.4 16.346 14.274 17 15.308 17c1.033 0 1.908-.654 2.202-1.556h2.798a.68.68 0 0 0 .692-.666.68.68 0 0 0-.692-.667H17.51c-.294-.901-1.169-1.555-2.202-1.555-1.034 0-1.908.654-2.202 1.555z"
fill="currentColor"
/>
</svg>
);
}
export default FiltersIcon;
+15
View File
@@ -0,0 +1,15 @@
function HeartIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15.683 5.965a3.404 3.404 0 0 0-4.75 0L10 6.881l-.934-.916a3.404 3.404 0 0 0-4.75 0 3.25 3.25 0 0 0 0 4.66L10 16.666l5.683-6.043a3.25 3.25 0 0 0 0-4.659Z"
stroke="currentColor"
strokeOpacity={0.7}
strokeWidth={1.5}
strokeLinecap="round"
/>
</svg>
);
}
export default HeartIcon;
+14
View File
@@ -0,0 +1,14 @@
function RestartIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="m14.26 3.583.95-.557a.75.75 0 0 0-.758-1.294L11.848 3.26a1 1 0 0 0-.33 1.413l1.53 2.32a.75.75 0 0 0 1.252-.826l-.806-1.224c3.288.687 5.756 3.596 5.756 7.078 0 3.991-3.244 7.23-7.25 7.23s-7.25-3.239-7.25-7.23a7.2 7.2 0 0 1 1.838-4.812c.276-.309.302-.78.023-1.087s-.755-.331-1.037-.027A8.7 8.7 0 0 0 3.25 12.02c0 4.823 3.92 8.73 8.75 8.73s8.75-3.907 8.75-8.73c0-4.044-2.753-7.443-6.49-8.437"
fill="currentColor"
/>
</svg>
);
}
export default RestartIcon;
+12 -7
View File
@@ -15,6 +15,7 @@ interface IMultiRangeSlider {
currentMax: number;
step: number;
disabled?: boolean;
label: string;
onChangeMin: (min: number) => void;
onChangeMax: (max: number) => void;
}
@@ -27,6 +28,7 @@ function MultiRangeSlider({
onChangeMin,
onChangeMax,
step,
label,
disabled = false,
}: IMultiRangeSlider) {
const [current, setCurrent] = useState<"min" | "max" | null>(null);
@@ -85,22 +87,25 @@ function MultiRangeSlider({
});
return (
<div className="space-y-3">
<div className="bg-white/80 rounded-lg relative p-3 flex justify-between w-[267px] border !border-[#E2E2DC]">
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="text-s">{label}</p>
<div className="bg-white/80 rounded-lg relative p-3 flex justify-between border !border-[#E2E2DC]">
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
{Intl.NumberFormat("en-IN").format(Math.round(currentMin))}
{Intl.NumberFormat("en").format(Math.round(currentMin))}
</p>
<p className={clsx("text-s", disabled && "text-[#0D1922]/40")}>
{Intl.NumberFormat("en-IN").format(Math.round(currentMax))}
{Intl.NumberFormat("en").format(Math.round(currentMax))}
</p>
<div className="absolute bottom-0 left-0 translate-y-1/2 w-full px-4">
<div className="absolute bottom-0 left-0 w-full px-4 translate-y-1/2">
<div
className="relative h-4 flex"
className="relative flex h-4"
ref={rangeRef}
onMouseMove={
handleChange as React.MouseEventHandler<HTMLDivElement>
}
onTouchMove={handleChange as React.TouchEventHandler<HTMLDivElement>}
onTouchMove={
handleChange as React.TouchEventHandler<HTMLDivElement>
}
>
<div
style={{
+10
View File
@@ -0,0 +1,10 @@
export const projects = [
{
title: "Rove Home Marasi Drive",
img: "/images/search/rove_home_marasi_drive.png",
},
{
title: "Rove Home Dubai Marina",
img: "/images/search/rove_home_dubai_marina.png",
},
];
+13
View File
@@ -0,0 +1,13 @@
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
}
+7 -9
View File
@@ -5,16 +5,14 @@
--breakpoint-2xl: 1440px;
}
@layer base {
body {
font-family: "Usual", sans-serif;
color: #0d1922;
}
body {
font-family: "Usual", sans-serif;
color: #0d1922;
}
button {
cursor: pointer;
outline: none;
}
button {
cursor: pointer;
outline: none;
}
@layer utilities {
+2 -2
View File
@@ -4,9 +4,9 @@ import Footer from "../components/Footer";
function DefaultLayout() {
return (
<div className="select-none flex flex-col">
<div className="flex flex-col select-none bg-[#F3F3F2]">
<Header />
<div className="h-[calc(100dvh-64px)] md:max-2xl:h-[calc(100dvh-72px)] 2xl:h-[calc(100dvh-4.444vw)]">
<div className="min-h-[calc(100dvh-64px)] md:max-2xl:min-h-[calc(100dvh-72px)] 2xl:min-h-[calc(100dvh-4.444vw)] 2xl:px-[2.222vw] md:max-2xl:px-6 px-4">
<Outlet />
</div>
<Footer />
+5
View File
@@ -0,0 +1,5 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: false } },
});
+6 -2
View File
@@ -1,4 +1,5 @@
import "./index.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";
import DefaultLayout from "./layout/DefaultLayout.tsx";
@@ -11,6 +12,7 @@ import AboutPage from "./pages/AboutPages.tsx";
import FavouritesPage from "./pages/FavouritesPage.tsx";
import SearchPage from "./pages/SearchPage.tsx";
import LayoutWithoutFooter from "./layout/LayoutWithoutFooter.tsx";
import { queryClient } from "./lib/queryClient.ts";
const route = createBrowserRouter([
{
@@ -55,7 +57,9 @@ const route = createBrowserRouter([
createRoot(document.getElementById("root")!).render(
<>
<RouterProvider router={route} />
<ModalContainer />
<QueryClientProvider client={queryClient}>
<RouterProvider router={route} />
<ModalContainer />
</QueryClientProvider>
</>
);
+106 -1
View File
@@ -1,5 +1,110 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
import { IUnit } from "../types/IUnit";
import FlatCard from "../components/FlatCard";
import { useEffect, useRef, useState } from "react";
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";
const STEP = 12;
function SearchPage() {
return <div>SearchPage</div>;
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 { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
initialPageParam: 0,
queryKey: ["units", project, unitTypes, cost, floor, area],
queryFn: async ({ pageParam = 0 }) =>
await api
.get(
`units?offset=${pageParam}&limit=${STEP}${
project ? `&project=${project}` : ""
}${unitTypes.map((unitType) => `&unitTypes=${unitType}`).join("")}${
cost.length > 0 ? `&cost=${cost.join(",")}` : ""
}${floor.length > 0 ? `&floor=${floor.join(",")}` : ""}${
area.length > 0 ? `&area=${area.join(",")}` : ""
}`
)
.json<IUnit[]>(),
getNextPageParam: (lastPage, _, lastPageIndex) =>
lastPage.length < STEP ? undefined : lastPageIndex + STEP,
});
const filtersRef = useRef<HTMLDivElement>(null);
const observerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!hasNextPage || isFetchingNextPage) return;
const observerElement = observerRef.current;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) fetchNextPage();
},
{ rootMargin: "-200px" }
);
if (observerElement) observer.observe(observerElement);
return () => {
if (observerElement) observer.unobserve(observerElement);
};
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const [showButtons, setShowButtons] = useState(false);
useEffect(() => {
const observerElement = filtersRef.current;
const observer = new IntersectionObserver((entries) =>
setShowButtons(!entries[0].isIntersecting)
);
if (observerElement) observer.observe(observerElement);
return () => {
if (observerElement) observer.unobserve(observerElement);
};
}, []);
return (
<div className="2xl:pb-[2.222vw] md:max-2xl:pb-6 pb-4 relative">
<SearchFilters ref={filtersRef} />
<div className="2xl:grid-cols-4 md:max-2xl:grid-cols-2 grid 2xl:gap-[1.111vw] gap-4 2xl:pt-[1.111vw] pt-4">
{data?.pages.flat().map((unit) => (
<FlatCard key={unit.id} {...unit} />
))}
</div>
<div ref={observerRef} />
{showButtons && (
<div className="fixed 2xl:bottom-[2.222vw] left-1/2 -translate-x-1/2 flex 2xl:gap-[0.278vw] gap-2">
<Button>
<span className="2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-white">
<FiltersIcon />
</span>
<span className="text-caption-m">Filters</span>
</Button>
<Button
variant="secondary"
className="2xl:!outline-[0.069vw] !outline !outline-[#E2E2DC]"
onClick={() => navigate("/search")}
>
<span className="2xl:w-[1.111vw] 2xl:h-[1.111vw] w-4 h-4 text-[#0D1922]/70">
<RestartIcon />
</span>
<span className="text-caption-m">Reset</span>
</Button>
</div>
)}
</div>
);
}
export default SearchPage;
+15
View File
@@ -0,0 +1,15 @@
export interface IUnit {
id: string;
unitNo: string;
project: string;
floor: string;
unitType: string;
noOfBathrooms: number;
unitView: string;
suitsArea: number;
squareFt: number;
noOfParkingSpace: number;
salesPrice: number;
state: string;
balconyArea: number;
}
+1 -1
View File
@@ -6,6 +6,6 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
// host: true,
host: true,
},
});