upd
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user