Merge branch 'main' of http://192.168.1.163:3000/inmake/irth-new-client
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user