This commit is contained in:
2025-05-14 16:41:16 +05:00
40 changed files with 1089 additions and 605 deletions
+3 -9
View File
@@ -1,5 +1,3 @@
import clsx from "clsx";
interface CompassProps {
imgStyle?: React.CSSProperties;
}
@@ -7,13 +5,9 @@ interface CompassProps {
function Compass({ imgStyle }: CompassProps) {
return (
<div>
<img
src="/images/map/compass.png"
className={clsx(
"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}
/>
<div className="bg-[#0D1922]/40 2xl:w-[7.222vw] w-26 aspect-square pointer-events-none absolute 2xl:left-[2.222vw] md:max-2xl:bottom-4 left-4 2xl:bottom-[2.222vw] max-md:hidden rounded-full backdrop-blur-lg">
<img src="/images/map/compass.png" style={imgStyle} />
</div>
<img
src="/images/map/compass-mobile.png"
className="min-w-10 w-10 pointer-events-none absolute left-4 bottom-4 md:hidden"
+22 -24
View File
@@ -4,6 +4,7 @@ import InstagramIcon from './icons/InstagramIcon';
import FacebookIcon from './icons/FacebookIcon';
import LinkedInIcon from './icons/LinkedInIcon';
import TwitterIcon from './icons/TwitterIcon';
import ChevronDownIcon from './icons/ChevronDownIcon';
function Footer() {
return (
@@ -17,14 +18,11 @@ function Footer() {
alt='IRTH'
/>
<p className='2xl:max-w-[17.083vw] text-s text-[#0D1922]/40 2xl:col-start-1 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-2 max-md:row-start-3 max-md:mt-12'>
<p className='2xl:max-w-[17.083vw] text-s text-[#0D1922]/40 2xl:col-start-1 md:max-2xl:col-start-3 max-2xl:col-span-3 md:max-2xl:row-start-2 max-md:row-start-3 md:max-2xl:mt-[52px] max-md:mt-12'>
For more information, visit
<br />
our website: 
<Link
className='text-[#00BED7] md:underline'
to={'https://www.irth.ae'}
>
<Link className='text-[#00BED7] underline' to={'https://www.irth.ae'}>
www.irth.ae
</Link>
</p>
@@ -33,79 +31,79 @@ function Footer() {
<p className='text-s text-[#0D1922]/40'>Follow us for more:</p>
<div className='flex 2xl:gap-[0.278vw] gap-1'>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<YoutubeIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<InstagramIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<FacebookIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<LinkedInIcon />
</div>
</div>
<div className='2xl:p-[0.417vw] p-1.5 bg-[#E2E2DC] 2xl:rounded-[0.278vw] rounded'>
<div className='2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70'>
<div className='2xl:w-[2.222vw] 2xl:h-[2.222vw] md:max-2xl:w-8 md:max-2xl:h-8 w-9 h-9 text-[#0D1922]/70'>
<TwitterIcon />
</div>
</div>
</div>
</div>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] pl-4 flex flex-col 2xl:col-start-4 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-3 col-start-1 max-md:mt-4'>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] pl-4 flex flex-col items-start 2xl:col-start-4 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-3 col-start-1'>
<Link
to={'/'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
>
Map
</Link>
<Link
to={'/unit-types'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
>
Unit Types
</Link>
<Link
to={'/about-irth'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
>
About IRTH
</Link>
</div>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] pl-4 flex flex-col 2xl:col-start-5 2xl:row-start-1 2xl:row-span-2 max-2xl:col-span-2 max-md:mt-4'>
<div className='2xl:border-l-[0.069vw] border-l border-[#E2E2DC] 2xl:pl-[1.111vw] md:max-2xl:pl-6 pl-3.5 flex flex-col items-start 2xl:col-start-5 2xl:row-start-1 2xl:row-span-2'>
<Link
to={'/favorites'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
className='text-btn-l flex-1 content-center md:my-4 my-[13px] text-[#0D1922]/70'
>
Favorites
</Link>
<Link
to={'/search'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
className='text-btn-l flex-1 content-center md:my-4 my-2.5 text-[#0D1922]/70'
>
Search
</Link>
<Link
to={'/'}
className='text-m flex-1 content-center max-2xl:py-2.5 text-[#0D1922]/70 w-fit'
>
Brochures
</Link>
<button className='text-btn-l flex-1 content-center md:my-3 text-[#0D1922]/70 flex items-center gap-2'>
<span>Brochures</span>
<div className='2xl:w-[1.667vw] 2xl:h-[1.667vw] md:max-2xl:w-6 md:max-2xl:h-6 w-5 h-5'>
<ChevronDownIcon />
</div>
</button>
</div>
<div className='content-end 2xl:text-right 2xl:col-start-6 2xl:row-start-1 2xl:row-span-2 md:max-2xl:col-start-1 md:max-2xl:row-start-3 max-md:col-span-3 max-md:pt-3 max-md:border-t border-[#E2E2DC]'>
<Link
to={'/'}
className='2xl:text-caption-m text-caption-s max-2xl:text-[#73787C] text-[#0D1922]/70'
className='md:text-caption-m text-caption-s max-2xl:text-[#73787C] text-[#0D1922]/70'
>
Privacy Policy
</Link>
+212 -42
View File
@@ -1,10 +1,17 @@
import { NavLink } from "react-router";
import { Link, NavLink } from "react-router";
import LocationIcon from "./icons/LocationIcon";
import clsx from "clsx";
import ArrowDownIcon from "./icons/ArrowDownIcon";
import { useState } from "react";
import Button from "./ui/Button";
import BurgerIcon from "./icons/BurgerIcon";
import { AnimatePresence, motion } from "motion/react";
import DownloadIcon from "./icons/DownloadIcon";
import { useClickAway } from "@uidotdev/usehooks";
import CloseIcon from "./icons/CloseIcon";
import { projects } from "../data/projects";
import useModalStore from "../stores/useModalStore";
import PrivacyPolicyModal from "./modals/PrivacyPolicyModal";
import ChevronDownIcon from "./icons/ChevronDownIcon";
function Header() {
function handleLogoClick() {
@@ -12,10 +19,10 @@ function Header() {
}
return (
<header className="sticky top-0 left-0 z-1 w-full h-16 md:max-2xl:h-18 2xl:h-[4.444vw] flex items-center justify-center bg-white outline outline-[#E2E2DC]">
<header className="sticky top-0 left-0 z-1 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white outline outline-[#E2E2DC]">
<div className="flex 2xl:gap-[1.111vw] gap-4 flex-1">
<div
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:p-4 max-md:px-4 max-md:py-5 cursor-pointer"
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
onClick={handleLogoClick}
>
<img
@@ -31,7 +38,7 @@ function Header() {
<p className="text-s text-[#0D1922]/40">Dubai</p>
</div>
</div>
<NavBar />
<Menu />
<div className="flex justify-end flex-1">
<ProfileBar />
</div>
@@ -41,28 +48,130 @@ function Header() {
export default Header;
function NavBar() {
function Menu() {
const [opened, setOpened] = useState(false);
const { setModal } = useModalStore();
return (
<div className="max-2xl:order-2">
<nav className="flex 2xl:gap-[0.556vw] gap-2 items-center max-2xl:hidden">
<NavItem href={"/"} title={"Map"} />
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<BrochuresDropdown />
<NavItem href={"/search"} title={"Search"} />
</nav>
<Button
onlyIcon
variant="secondary"
size="small"
className="2xl:hidden !outline !outline-[#E2E2DC] mr-4"
>
<span className="w-5 h-5 text-[#0D1922]">
<BurgerIcon />
</span>
</Button>
</div>
<>
<div className="max-2xl:order-2 z-10">
<nav className="flex 2xl:gap-[0.278vw] gap-1 items-center max-2xl:hidden">
<NavItem href={"/"} title={"Map"} />
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<BrochuresDropdown />
<NavItem href={"/search"} title={"Search"} />
</nav>
<Button
onlyIcon
variant="secondary"
className="2xl:hidden !outline !outline-[#E2E2DC] md:mr-6 mr-4"
onClick={() => setOpened((prev) => !prev)}
>
<div className="w-5 h-5">
{opened ? <CloseIcon /> : <BurgerIcon />}
</div>
</Button>
</div>
<AnimatePresence mode="wait">
{opened && (
<motion.div
initial={{ opacity: 0, y: "-100%" }}
animate={{ opacity: 1, y: "0%" }}
exit={{ opacity: 0, y: "-100%" }}
transition={{ bounce: 0, duration: 0.3 }}
className="2xl:hidden absolute top-full md:p-4 p-3 w-full rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)]"
>
<div className="space-y-4">
<p className="text-h3 font-medium">Projects</p>
<div className="flex gap-2 flex-wrap max-md:flex-col items-start">
{projects.map(({ img, title }, index) => (
<Link
key={index}
to={
title.endsWith("Dubai Marina")
? "/"
: `/complex/${title
.split(" ")
.slice(-2)
.join("-")
.toLowerCase()}`
}
className="p-1 pr-5 flex gap-2 items-center flex-nowrap ring-[#E2E2DC] ring-1 rounded-[40px]"
>
<img src={img} alt={title} className="w-10 h-10" />
<span className="text-s text-[#0D1922]/70">{title}</span>
</Link>
))}
<Link
to="/"
className="px-5 py-3.5 content-center ring-[#E2E2DC] ring rounded-[40px] text-s text-[#0D1922]/70"
>
Show on Map
</Link>
</div>
</div>
<div className="grid md:grid-cols-2 md:gap-4 gap-2">
<NavItem href={"/unit-types"} title={"Unit Types"} />
<NavItem href={"/about"} title={"About IRTH"} />
<NavItem href={"/favorites"} title={"Favorites"} />
<NavItem href={"/search"} title={"Search"} />
</div>
<hr className="border-[#E2E2DC]" />
<div className="space-y-6">
<p className="font-medium text-h3">Brochures</p>
<div className="p-[0.278vw] flex md:gap-[1.111vw] gap-6 z-0 justify-stretch items-stretch max-md:flex-col">
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Marasi Drive</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Downtown</p>
<div className="flex gap-2 flex-col">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
</div>
</div>
<div className="pt-6 p-4 flex justify-between items-end bottom-0 left-0 w-full bg-white">
<p className="text-s text-[#0D1922]/40 max-w-[246px]">
For more information, visit our website:{" "}
<Link
to="https://www.irth.ae"
className="underline text-[#00BED7]"
>
www.irth.ae
</Link>
</p>
<Button
variant="tertiary"
size="small"
className="!px-3"
onClick={() => setModal(<PrivacyPolicyModal />)}
>
Privacy Policy
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}
@@ -72,8 +181,8 @@ function NavItem({ href, title }: { href: string; title: string }) {
to={href}
className={({ isActive }) =>
clsx(
"text-[0.972vw] 2xl:px-[1.25vw] 2xl:py-[0.903vw] px-4.5 py-[13px] 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none",
isActive && "bg-[#00BED7] text-[#FFFFFF]"
"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]",
isActive && "2xl:bg-[#00BED7] 2xl:text-[#FFFFFF]"
)
}
>
@@ -84,7 +193,10 @@ function NavItem({ href, title }: { href: string; title: string }) {
function ProfileBar() {
return (
<Button className="!bg-[#F3F3F2] 2xl:mr-[2.222vw] mr-4" variant="secondary">
<Button
variant="secondary"
className="2xl:mr-[2.222vw] mr-4 text-[#0D1922]/70 !bg-[#F3F3F2]"
>
Login
</Button>
);
@@ -93,20 +205,78 @@ function ProfileBar() {
function BrochuresDropdown() {
const [opened, setOpened] = useState(false);
const ref = useClickAway<HTMLDivElement>(() => setOpened(false));
return (
<button
className="2xl:px-[0.972vw] 2xl:py-[0.694vw] px-3.5 py-2.5 flex items-center"
onClick={() => setOpened((prev) => !prev)}
>
<span className="text-[0.972vw] leading-none">Brochures</span>
<span
className={clsx(
"text-[#0D1922] 2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 transition-transform duration-300",
opened && "rotate-180"
)}
<div ref={ref}>
<Button
variant="secondary"
className="2xl:px-[0.972vw] 2xl:py-[0.694vw] px-3.5 py-2.5 flex items-center max-2xl:hidden"
onClick={() => setOpened((prev) => !prev)}
>
<ArrowDownIcon />
</span>
</button>
<span className="text-btn-m text-[#0D1922]">Brochures</span>
<span
className={clsx(
"2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 transition-transform duration-300",
opened && "rotate-180"
)}
>
<ChevronDownIcon />
</span>
</Button>
<AnimatePresence>
{opened && (
<motion.div
initial={{ opacity: 0, x: "100%" }}
animate={{ opacity: 1, x: "0%" }}
exit={{ opacity: 0, x: "100%" }}
transition={{ bounce: 0, duration: 0.3 }}
className="max-2xl:hidden p-[1.667vw] flex gap-[1.111vw] z-0 justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[58.264vw] w-[32.222vw] rounded-[1.111vw] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]"
>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Marasi Drive</p>
<div className="flex flex-col gap-[0.556vw]">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
<div className="flex-1 space-y-4">
<p className="text-s font-medium">Rove Home Downtown</p>
<div className="flex flex-col gap-[0.556vw]">
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export function BrochureButton({ title }: { title: string }) {
return (
<Button
variant="secondary"
size="large"
className="w-full !bg-[#F3F3F2] !justify-between group hover:!bg-[#F3F3F2]"
>
<span className="text-nowrap text-caption-m group-hover:text-[#0D1922] transition-colors duration-300">
{title}
</span>
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 text-[#0D1922]/70 group-hover:text-[#0D1922] transition-colors duration-300">
<DownloadIcon />
</span>
</Button>
);
}
+22 -21
View File
@@ -15,7 +15,6 @@ import PrivacyPolicyButton from "./PrivacyPolicyButton";
import { getWeather } from "../api/weather";
import { isMobile } from "react-device-detect";
import SelectedComplexCard from "./SelectedComplexCard";
import FullScreenButton from "./FullScreenButton";
import useWindowSize from "../hooks/useWindowSize";
import TouchIcon from "./icons/map/TouchIcon";
@@ -505,23 +504,23 @@ function Map({ maxZoom = 1 }: MapProps) {
});
}, []);
const [isFullScreen, setIsFullScreen] = useState(false);
// const [isFullScreen, setIsFullScreen] = useState(false);
function handleFullScreenClick() {
if (!containerRef.current) return;
// function handleFullScreenClick() {
// if (!containerRef.current) return;
const { width, height } = containerRef.current.getBoundingClientRect();
// const { width, height } = containerRef.current.getBoundingClientRect();
containerSizeRef.current = { width, height };
// containerSizeRef.current = { width, height };
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
minZoomRef.current = newMinZoom;
// const newMinZoom = calculateMinZoom({ width, height }, originalSize);
// minZoomRef.current = newMinZoom;
setZoom(newMinZoom);
setIsFullScreen((prev) => !prev);
if (isFullScreen) document.exitFullscreen();
else containerRef.current.requestFullscreen();
}
// setZoom(newMinZoom);
// setIsFullScreen((prev) => !prev);
// if (isFullScreen) document.exitFullscreen();
// else containerRef.current.requestFullscreen();
// }
const cloudAnimationRef = useRef<number | null>(null);
const [cloudOffset, setCloudOffset] = useState(0);
@@ -576,7 +575,7 @@ function Map({ maxZoom = 1 }: MapProps) {
{containerRef.current?.clientWidth && (
<img
ref={mapRef}
src={`/images/map/map-${
src={`/images/map/map-new-${
containerRef.current.clientWidth < 768 ? "mobile" : "desktop"
}.jpg`}
alt="map"
@@ -662,7 +661,7 @@ function Map({ maxZoom = 1 }: MapProps) {
<TouchIcon />
</span>
</div>
<p className="text-sm text-center">Tap to move</p>
<p className="text-s text-center">Tap to move</p>
</div>
</motion.div>
) : (
@@ -682,16 +681,18 @@ function Map({ maxZoom = 1 }: MapProps) {
<MoveIcon />
</span>
</div>
<p className="text-sm">Zoom and Move to select a location</p>
<p className="text-s">Zoom and Move to select a location</p>
</div>
</motion.div>
))}
</AnimatePresence>
<FullScreenButton
isFullScreen={isFullScreen}
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
{/* <div className="absolute 2xl:right-[2.222vw] 2xl:top-[2.222vw] right-8 top-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 />
+24 -8
View File
@@ -4,16 +4,20 @@ import { useEffect, useState } from "react";
import Project from "../types/Project";
import Select from "./ui/Select";
function ProjectSelect({
function ProjectSelect<T extends boolean = false>({
projects,
onSelect,
defaultProject,
withAll,
}: {
projects: Project[];
onSelect: (project: Project) => void;
defaultProject: Project;
onSelect: (project: Project | null) => void;
defaultProject: T extends false ? Project : null;
withAll?: T;
}) {
const [selectedProject, setSelectedProject] = useState(defaultProject);
const [selectedProject, setSelectedProject] = useState<Project | null>(
defaultProject
);
useEffect(() => setSelectedProject(defaultProject), [defaultProject]);
@@ -22,12 +26,23 @@ function ProjectSelect({
return (
<>
<div className="flex 2xl:gap-[0.556vw] gap-2 max-md:hidden">
{withAll && (
<div
className={clsx(
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:py-[0.972vw] 2xl:px-[1.389vw] p-1 flex items-center 2xl:gap-[0.556vw] gap-2 text-s 2xl:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer",
!selectedProject ? "ring-[#00BED7]" : "ring-[#E2E2DC]"
)}
onClick={() => setSelectedProject(null)}
>
All Projects
</div>
)}
{projects.map((project) => (
<div
key={project.title}
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:ring-[0.069vw] ring transition-[box-shadow] cursor-pointer",
project.title === selectedProject.title
selectedProject && project.title === selectedProject.title
? "ring-[#00BED7]"
: "ring-[#E2E2DC]"
)}
@@ -41,7 +56,9 @@ function ProjectSelect({
<p
className={clsx(
"2xl:mr-[1.111vw] mr-6",
selectedProject.title !== project.title && "text-[#0D1922]/70"
selectedProject &&
selectedProject.title !== project.title &&
"text-[#0D1922]/70"
)}
>
{project.title}
@@ -49,7 +66,6 @@ function ProjectSelect({
</div>
))}
</div>
<Select
options={projects.map((project) => project.title)}
onSelect={(option) =>
@@ -58,7 +74,7 @@ function ProjectSelect({
defaultProject
)
}
defaultOption={defaultProject.title}
defaultOption={defaultProject ? defaultProject.title : "All"}
className="md:hidden"
/>
</>
+429 -333
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 debouncedCostTouched = useDebounce(costTouched, 1000);
const debouncedAreaTouched = useDebounce(areaTouched, 1000);
const debouncedFloorTouched = 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") && !debouncedCostTouched,
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") && !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,
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") && !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,
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,138 @@ 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);
}
function handleSelectProject(project: Project) {
setProject(project.title);
if (!inModal)
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 (debouncedCostTouched)
setSearchParams((prev) => {
prev.set("project", project.title);
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;
});
}
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 +408,7 @@ function SearchFilters({
{inModal && (
<div
className="fixed inset-0 z-20 bg-[#0D1922]/40 cursor-pointer"
onClick={handleClose}
onClick={() => setInModal(false)}
/>
)}
<div
@@ -393,175 +423,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="text-h2 font-medium">
{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>
</>
);
+10 -4
View File
@@ -5,14 +5,20 @@ import { motion } from "motion/react";
import IMarker from "../types/IMarker";
import { useEffect, useState } from "react";
import clsx from "clsx";
import { useQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
export default function SelectedComplexCard({ marker }: { marker: IMarker }) {
const navigate = useNavigate();
const [isImageLoaded, setIsImageLoaded] = useState(false);
useEffect(() => {
setIsImageLoaded(false);
}, [marker]);
useEffect(() => setIsImageLoaded(false), [marker]);
const { data: count } = useQuery({
queryKey: ["units", "count", `Rove Home ${marker.title}`],
queryFn: () =>
api.get(`units/count?project=Rove Home ${marker.title}`).json<number>(),
});
return (
<motion.div
@@ -51,7 +57,7 @@ export default function SelectedComplexCard({ marker }: { marker: IMarker }) {
{marker.title}
</p>
<p className="text-[#00BED7] text-[10px] leading-[135%]">
{marker.unitsCount} units
{count} units
</p>
</div>
<Button
+20
View File
@@ -1,4 +1,5 @@
// import { sequenceVideos } from "../data/sequenceVideos";
import { useState, useRef } from 'react';
import gsap from 'gsap';
import { useSwipeable } from 'react-swipeable';
@@ -12,6 +13,7 @@ import InfoIcon from './icons/InfoIcon';
import FullScreenButton from './FullScreenButton';
import PrivacyPolicyButton from './PrivacyPolicyButton';
import DisclaimerButton from './DisclaimerButton';
import { masks } from '../data/masks';
interface SequenceSliderProps {
complexName: string;
@@ -164,6 +166,24 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
/>
)}
</AnimatePresence>
{!isAnimating && (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 4096 1752'
className='absolute top-0 left-0 hidden w-full h-full md:block'
preserveAspectRatio='xMidYMid slice'
>
<path
d={`${
masks[complexName as keyof typeof masks][
Math.floor(currentIndex / FRAME_STEP)
]
}`}
className='fill-[#00BED7] cursor-pointer transition-opacity duration-300 opacity-0 hover:opacity-40'
onClick={() => navigate('floors')}
/>
</svg>
)}
{imageLoaded === FRAME_COUNT && (
<>
<div className='absolute flex 2xl:gap-[0.556vw] justify-between gap-2 2xl:left-[2.222vw] 2xl:right-[2.222vw] 2xl:top-[2.222vw] max-w-full md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-6 left-4 right-4 top-4'>
+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-h4 font-medium">
{`AED ${Intl.NumberFormat("ar-AE", {
currency: "AED",
minimumFractionDigits: 0,
}).format(unit.salesPrice)}`}
</p>
+1 -1
View File
@@ -18,7 +18,7 @@ function UnitTypeCard({ project, type }: { project: string; type: UnitType }) {
<img src={type.img} alt="" />
<div className="space-y-1 2xl:space-y-[0.278vw]">
<p className="text-s text-[#0D1922]/70">{type.area}</p>
<p className="text-subheadline-s font-medium">{type.name}</p>
<p className="text-h4 font-medium">{type.name}</p>
</div>
</div>
);
+26 -26
View File
@@ -1,29 +1,29 @@
import clsx from "clsx";
import { useSearchParams } from "react-router";
// import clsx from "clsx";
// import { useSearchParams } from "react-router";
function UnitTypesFilter({ title }: { title: string }) {
const [searchParams, setSearchParams] = useSearchParams();
// 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>
);
}
// 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;
// export default UnitTypesFilter;
+2 -7
View File
@@ -1,16 +1,11 @@
export default function ArrowLeftIcon() {
return (
<svg
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.625 10A.625.625 0 0 0 15 9.375H6.574l4.158-3.92a.625.625 0 0 0-.858-.91l-5.303 5a.625.625 0 0 0 0 .91l5.303 5a.625.625 0 0 0 .858-.91l-4.158-3.92H15c.345 0 .625-.28.625-.625"
d="M18.75 12a.75.75 0 0 0-.75-.75H7.889l4.99-4.704a.75.75 0 1 0-1.03-1.092l-6.364 6a.75.75 0 0 0 0 1.092l6.364 6a.75.75 0 0 0 1.03-1.092l-4.99-4.704H18a.75.75 0 0 0 .75-.75"
fill="currentColor"
fillOpacity={0.7}
/>
</svg>
);
+14
View File
@@ -0,0 +1,14 @@
function ArrowRightIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.25 12c0 .414.336.75.75.75h10.111l-4.99 4.704a.75.75 0 1 0 1.03 1.092l6.364-6a.75.75 0 0 0 0-1.092l-6.364-6a.75.75 0 1 0-1.03 1.092l4.99 4.704H6a.75.75 0 0 0-.75.75"
fill="currentColor"
/>
</svg>
);
}
export default ArrowRightIcon;
+15
View File
@@ -0,0 +1,15 @@
function ChevronLeftIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="m14 17-5-5 5-5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default ChevronLeftIcon;
+15
View File
@@ -0,0 +1,15 @@
function ChevronRightIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="m10 17 5-5-5-5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default ChevronRightIcon;
+15
View File
@@ -0,0 +1,15 @@
function DownloadIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 16v1a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-1m-4-4-4 4m0 0-4-4m4 4V4"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default DownloadIcon;
+14
View File
@@ -0,0 +1,14 @@
function EntranceIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.75 20c0 .414.336.75.75.75H17A2.75 2.75 0 0 0 19.75 18V6A2.75 2.75 0 0 0 17 3.25H8.5a.75.75 0 0 0 0 1.5H17c.69 0 1.25.56 1.25 1.25v12c0 .69-.56 1.25-1.25 1.25H8.5a.75.75 0 0 0-.75.75m3.724-3.91a.75.75 0 0 0 1.06-.008l3.5-3.556a.75.75 0 0 0 0-1.052l-3.5-3.556a.75.75 0 1 0-1.069 1.053l2.244 2.279H5a.75.75 0 0 0 0 1.5h8.71l-2.245 2.28a.75.75 0 0 0 .009 1.06"
fill="currentColor"
/>
</svg>
);
}
export default EntranceIcon;
+3 -4
View File
@@ -1,12 +1,11 @@
export default function ArrowRightIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 8a.5.5 0 0 0 .5.5h6.74l-3.325 3.136a.5.5 0 1 0 .686.728l4.242-4a.5.5 0 0 0 0-.728l-4.242-4a.5.5 0 1 0-.686.728L10.74 7.5H4a.5.5 0 0 0-.5.5"
fill="currentColor"
fillOpacity={0.7}
d="M5.25 12c0 .414.336.75.75.75h10.111l-4.99 4.704a.75.75 0 1 0 1.03 1.092l6.364-6a.75.75 0 0 0 0-1.092l-6.364-6a.75.75 0 1 0-1.03 1.092l4.99 4.704H6a.75.75 0 0 0-.75.75"
fill="#fff"
/>
</svg>
);
+6 -6
View File
@@ -1,24 +1,24 @@
export default function DisclaimerModal() {
return (
<div className="bg-white z-40 2xl:rounded-[0.556vw] rounded-lg py-[37px] px-8 2xl:w-[29.236vw] md:max-2xl:w-[54.818vw] w-full">
<h2 className="text-subheadline-m font-medium py-6 2xl:border-t-[0.139vw] border-t-2 border-[#00BED7] w-fit">
<h3 className="text-h3 font-medium py-6 2xl:border-t-[0.139vw] border-t-2 border-[#00BED7] w-fit">
Disclaimer
</h2>
</h3>
<div className="flex flex-col gap-4">
<p className="text-caption-m">
<p className="text-s">
This masterplan has been designed solely to provide an impression of
the Rove Home projects as well as the approximate location of existing
and proposed facilities, services, and destinations and is not
intended for any other purpose.
</p>
<p className="text-caption-m">
<p className="text-s">
All elements including the interior design used in the units and
images shown in the virtual tour are only for illustration. The
pictures of the proposed residential units, furniture, landscaping,
amenities, color schemes, fixtures, and accessories among all other
items are illustrative to showcase the units.
</p>
<p className="text-caption-m">
<p className="text-s">
IRTH does not make any representation or give any warranty concerning
the future developments shown, or the current or future amenities,
location, or existence of any facilities, services, and destinations.
@@ -26,7 +26,7 @@ export default function DisclaimerModal() {
information are approximate and for indicative purposes only and are
not to scale.
</p>
<p className="text-caption-m">
<p className="text-s">
IRTH gives notice that this virtual tour (including units, amenities,
plans of the property) does not constitute any part of a sale offer or
sale and purchase contract.
+2 -2
View File
@@ -2,9 +2,9 @@ function PrivacyPolicyModal() {
return (
<div className="2xl:rounded-[1.111vw] bg-white rounded-2xl 2xl:p-[2.222vw] 2xl:w-[38.889vw] p-8">
<div className="bg-[#00BED7] 2xl:h-[0.139vw] h-0.5 2xl:w-[8.646vw] 2xl:rounded-[0.208vw] rounded-[3px]" />
<h2 className="text-subheadline-m font-medium py-6">
<h3 className="text-h3 font-medium py-6">
Privacy Policy for IRTH Group and its companies:
</h2>
</h3>
<div className="space-y-4">
<p className="text-caption-s">
At IRTH Group and its companies, we are committed to protecting the
+15 -7
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);
@@ -60,18 +62,24 @@ function MultiRangeSlider({
function handleMouseUp() {
setCurrent(null);
setTouched?.(true);
}
useEffect(() => {
if (current) {
document.addEventListener("mousemove", handleChange as EventListener);
document.addEventListener("mouseup", handleMouseUp);
document.addEventListener("mouseleave", handleMouseUp);
}
if (!current) return;
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 +92,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
+19 -2
View File
@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { motion } from "motion/react";
import { AnimatePresence } from "motion/react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useClickAway } from "@uidotdev/usehooks";
import clsx from "clsx";
import ChevronDownIcon from "../icons/ChevronDownIcon";
@@ -25,10 +25,26 @@ function Select({
const ref = useClickAway<HTMLDivElement>(() => setIsShow(false));
const dropDownRef = useRef<HTMLDivElement>(null);
useEffect(() => setSelectedOption(defaultOption), [defaultOption]);
useEffect(() => onSelect(selectedOption), [selectedOption]);
function handleScroll() {
if (!dropDownRef.current) return;
dropDownRef.current.style.maxHeight = `calc(100vh - ${
dropDownRef.current?.getBoundingClientRect().y
}px - 0.278vw)`;
}
useEffect(() => {
handleScroll();
document.addEventListener("scroll", handleScroll);
return () => document.removeEventListener("scroll", handleScroll);
}, [isShow]);
return (
<div ref={ref} className={clsx("relative", className)}>
{label && (
@@ -58,7 +74,8 @@ 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={dropDownRef}
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