This commit is contained in:
2025-05-12 11:44:51 +05:00
parent b7b318b565
commit c2532c577d
16 changed files with 305 additions and 96 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

+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"
+164 -41
View File
@@ -5,6 +5,12 @@ import ArrowDownIcon from "./icons/ArrowDownIcon";
import { useState } from "react";
import Button from "./ui/Button";
import BurgerIcon from "./icons/BurgerIcon";
import EntranceIcon from "./icons/EntranceIcon";
import { AnimatePresence, motion } from "motion/react";
import DownloadIcon from "./icons/DownloadIcon";
import { useClickAway } from "@uidotdev/usehooks";
import CloseIcon from "./icons/CloseIcon";
import ChevronRightIcon from "./icons/ChevronRightIcon";
function Header() {
function handleLogoClick() {
@@ -12,10 +18,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,8 +37,8 @@ function Header() {
<p className="text-s text-[#0D1922]/40">Dubai</p>
</div>
</div>
<NavBar />
<div className="flex justify-end flex-1">
<Menu />
<div className="flex justify-end flex-1 max-md:hidden">
<ProfileBar />
</div>
</header>
@@ -41,28 +47,80 @@ function Header() {
export default Header;
function NavBar() {
function Menu() {
const [opened, setOpened] = useState(false);
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">
<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)}
>
<span className="w-5 h-5 text-[#0D1922]">
{opened ? <CloseIcon /> : <BurgerIcon />}
</span>
</Button>
</div>
{/* <AnimatePresence>
{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 bg-[#F3F3F2] w-full rounded-b-2xl flex flex-col gap-4 -z-20"
>
<div className="grid md:grid-cols-2 md:grid-rows-3 gap-2">
<NavItem href={"/"} title={"Map"} />
<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] border" />
<p className="font-medium">Brochures</p>
<div className="max-2xl:hidden p-[0.278vw] flex gap-[0.278vw] 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">
<p className="text-s font-medium px-[1.111vw] py-[0.833vw]">
Rove Home Marasi Drive
</p>
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
<div className="outline-[0.069vw] outline-[#E2E2DC]" />
<div className="flex-1">
<p className="text-s font-medium px-[1.111vw] py-[0.833vw]">
Rove Home Downtown
</p>
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence> */}
</>
);
}
@@ -72,19 +130,25 @@ 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-m 2xl:px-[1.25vw] 2xl:py-[0.903vw] p-4 max-2xl:bg-[#FFFFFF]/80 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none text-[#0D1922]/70 flex items-center justify-between",
isActive && "2xl:bg-[#00BED7] 2xl:text-[#FFFFFF]"
)
}
>
{title}
<span className="w-5 h-5 2xl:hidden">
<ChevronRightIcon />
</span>
</NavLink>
);
}
function ProfileBar() {
return (
<Button className="!bg-[#F3F3F2] 2xl:mr-[2.222vw] mr-4" variant="secondary">
<Button className="2xl:mr-[2.222vw] mr-4 text-[#0D1922]/70">
<span className="2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5">
<EntranceIcon />
</span>
Login
</Button>
);
@@ -93,20 +157,79 @@ 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-[0.972vw] leading-none">Brochures</span>
<span
className={clsx(
"text-[#0D1922]/70 2xl:w-[1.389vw] 2xl:h-[1.389vw] w-5 h-5 transition-transform duration-300",
opened && "rotate-180"
)}
>
<ArrowDownIcon />
</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-[0.278vw] flex gap-[0.278vw] 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">
<p className="text-s font-medium px-[1.111vw] py-[0.833vw]">
Rove Home Marasi Drive
</p>
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
<div className="outline-[0.069vw] outline-[#E2E2DC]" />
<div className="flex-1">
<p className="text-s font-medium px-[1.111vw] py-[0.833vw]">
Rove Home Downtown
</p>
{[
"Rove Main Brochure",
"Rove Amenties Brochure",
"Rove Technical Brochure",
].map((title) => (
<BrochureButton title={title} key={title} />
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export function BrochureButton({ title }: { title: string }) {
return (
<Button
variant="secondary"
size="large"
className="w-full !bg-[#FFFFFF]/80 !rounded-lg !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>
);
}
+14 -15
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);
@@ -687,13 +686,13 @@ function Map({ maxZoom = 1 }: MapProps) {
</motion.div>
))}
</AnimatePresence>
<div className="absolute 2xl:right-[2.222vw] 2xl:top-[2.222vw] right-8 bottom-8">
{/* <div className="absolute 2xl:right-[2.222vw] 2xl:top-[2.222vw] right-8 top-8">
<FullScreenButton
isFullScreen={isFullScreen}
onFullScreenChange={setIsFullScreen}
onClick={handleFullScreenClick}
/>
</div>
</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 />
+15 -15
View File
@@ -54,9 +54,9 @@ function SearchFilters({
const [areaTouched, setAreaTouched] = useState(false);
const [floorTouched, setFloorTouched] = useState(false);
const debouncedCostChanged = useDebounce(costTouched, 1000);
const debouncedAreaChanged = useDebounce(areaTouched, 1000);
const debouncedFloorChanged = useDebounce(floorTouched, 1000);
const debouncedCostTouched = useDebounce(costTouched, 1000);
const debouncedAreaTouched = useDebounce(areaTouched, 1000);
const debouncedFloorTouched = useDebounce(floorTouched, 1000);
const [searchParams, setSearchParams] = useSearchParams();
@@ -102,7 +102,7 @@ function SearchFilters({
searchParams.get("area"),
view,
],
enabled: !!project && !searchParams.has("cost") && !debouncedCostChanged,
enabled: !!project && !searchParams.has("cost") && !debouncedCostTouched,
initialData: searchParams.has("cost")
? {
min: searchParams.get("cost")!.split(",").map(Number)[0],
@@ -135,7 +135,7 @@ function SearchFilters({
searchParams.get("area"),
view,
],
enabled: !!project && !searchParams.has("floor") && !debouncedFloorChanged,
enabled: !!project && !searchParams.has("floor") && !debouncedFloorTouched,
initialData: searchParams.has("floor")
? {
min: searchParams.get("floor")!.split(",").map(Number)[0],
@@ -171,7 +171,7 @@ function SearchFilters({
searchParams.get("floor"),
view,
],
enabled: !!project && !searchParams.has("area") && !debouncedAreaChanged,
enabled: !!project && !searchParams.has("area") && !debouncedAreaTouched,
initialData: searchParams.has("area")
? {
min: searchParams.get("area")!.split(",").map(Number)[0],
@@ -317,28 +317,28 @@ function SearchFilters({
}, [areaInModal, inModal]);
useEffect(() => {
if (debouncedCostChanged)
if (debouncedCostTouched)
setSearchParams((prev) => {
prev.set("cost", `${debouncedCost[0]},${debouncedCost[1]}`);
prev.set("cost", debouncedCost.map(Math.ceil).join(","));
return prev;
});
}, [debouncedCost, debouncedCostChanged]);
}, [debouncedCost, debouncedCostTouched]);
useEffect(() => {
if (debouncedAreaChanged)
if (debouncedAreaTouched)
setSearchParams((prev) => {
prev.set("area", `${debouncedArea[0]},${debouncedArea[1]}`);
prev.set("area", debouncedArea.map(Math.ceil).join(","));
return prev;
});
}, [debouncedArea, debouncedAreaChanged]);
}, [debouncedArea, debouncedAreaTouched]);
useEffect(() => {
if (debouncedFloorChanged)
if (debouncedFloorTouched)
setSearchParams((prev) => {
prev.set("floor", `${debouncedFloor[0]},${debouncedFloor[1]}`);
prev.set("floor", debouncedFloor.map(Math.ceil).join(","));
return prev;
});
}, [debouncedFloor, debouncedFloorChanged]);
}, [debouncedFloor, debouncedFloorTouched]);
function handleSelectProject(project: Project) {
setProject(project.title);
+19
View File
@@ -12,6 +12,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 +165,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">
+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>
);
+3 -4
View File
@@ -61,13 +61,12 @@ function MultiRangeSlider({
}
function handleMouseUp() {
if (current) {
setCurrent(null);
setTouched?.(true);
}
setCurrent(null);
setTouched?.(true);
}
useEffect(() => {
if (!current) return;
document.addEventListener("mousemove", handleChange as EventListener);
document.addEventListener("mouseup", handleMouseUp);
document.addEventListener("mouseleave", handleMouseUp);
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@ function LayoutWithoutFooter() {
return (
<div className="flex flex-col select-none">
<Header />
<div className="2xl:h-[calc(100dvh-4.444vw)] md:max-2xl:h-[calc(100dvh-72px)] h-[calc(100dvh-64px)]">
<div className="2xl:h-[calc(100dvh-4.444vw)] md:max-2xl:h-[calc(100dvh-64px)] h-[calc(100dvh-56px)]">
<Outlet />
</div>
</div>