Refactor FloorSelect, SequenceSlider, and ProjectSelect components to use ComplexName type; update buildingType in projects data and add new HQ project. Enhance SearchFilters and FloorsPage for better UI consistency and functionality.
This commit is contained in:
@@ -16,6 +16,7 @@ import { isMobile } from "react-device-detect";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import ButtonGroup from "./ButtonGroup";
|
||||
import { SPECIAL_FLOORS } from "../constants/floors";
|
||||
import { ComplexName } from "../types/ComplexName";
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
@@ -31,7 +32,7 @@ const constrainPosition = (
|
||||
position: Position,
|
||||
containerSize: Size,
|
||||
imageSize: Size,
|
||||
zoom: number
|
||||
zoom: number,
|
||||
): Position => {
|
||||
const scaledWidth = imageSize.width * zoom;
|
||||
const scaledHeight = imageSize.height * zoom;
|
||||
@@ -57,7 +58,7 @@ const constrainPosition = (
|
||||
};
|
||||
|
||||
const getEventPosition = (
|
||||
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent
|
||||
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent,
|
||||
): Position => {
|
||||
if ("touches" in e)
|
||||
return {
|
||||
@@ -82,7 +83,7 @@ const calculateMinZoom = (containerSize: Size, imageSize: Size): number => {
|
||||
const calculateCenterPosition = (
|
||||
containerSize: Size,
|
||||
imageSize: Size,
|
||||
zoom: number
|
||||
zoom: number,
|
||||
): Position => {
|
||||
const scaledWidth = imageSize.width * zoom;
|
||||
const scaledHeight = imageSize.height * zoom;
|
||||
@@ -114,7 +115,7 @@ function FloorSelect({
|
||||
selectedFloor,
|
||||
onSelect,
|
||||
}: {
|
||||
complexName: string;
|
||||
complexName: ComplexName;
|
||||
selectedFloor: string | null;
|
||||
onSelect: (floor: string) => void;
|
||||
}) {
|
||||
@@ -160,7 +161,7 @@ function FloorSelect({
|
||||
newPosition,
|
||||
{ width: containerRect.width, height: containerRect.height },
|
||||
originalSize,
|
||||
newZoom
|
||||
newZoom,
|
||||
);
|
||||
|
||||
setImagePosition(constrainedPosition);
|
||||
@@ -193,7 +194,7 @@ function FloorSelect({
|
||||
// Check if we've moved beyond the threshold
|
||||
const distanceMoved = Math.hypot(
|
||||
x - dragStartPosition.current.x,
|
||||
y - dragStartPosition.current.y
|
||||
y - dragStartPosition.current.y,
|
||||
);
|
||||
|
||||
if (distanceMoved > dragThreshold) {
|
||||
@@ -250,7 +251,7 @@ function FloorSelect({
|
||||
const touch2 = e.touches[1];
|
||||
const distance = Math.hypot(
|
||||
touch1.clientX - touch2.clientX,
|
||||
touch1.clientY - touch2.clientY
|
||||
touch1.clientY - touch2.clientY,
|
||||
);
|
||||
initialTouchDistance.current = distance;
|
||||
previousTouchDistance.current = distance;
|
||||
@@ -284,7 +285,7 @@ function FloorSelect({
|
||||
const touch2 = e.touches[1];
|
||||
const distance = Math.hypot(
|
||||
touch1.clientX - touch2.clientX,
|
||||
touch1.clientY - touch2.clientY
|
||||
touch1.clientY - touch2.clientY,
|
||||
);
|
||||
|
||||
if (initialTouchDistance.current === null) {
|
||||
@@ -310,7 +311,7 @@ function FloorSelect({
|
||||
|
||||
const newZoom = Math.min(
|
||||
maxZoomRef.current,
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor)
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor),
|
||||
);
|
||||
|
||||
// Prevent zoom if at limits or change is too small
|
||||
@@ -352,7 +353,7 @@ function FloorSelect({
|
||||
// Check if we've moved beyond the threshold
|
||||
const distanceMoved = Math.hypot(
|
||||
x - dragStartPosition.current.x,
|
||||
y - dragStartPosition.current.y
|
||||
y - dragStartPosition.current.y,
|
||||
);
|
||||
|
||||
if (distanceMoved > dragThreshold) {
|
||||
@@ -425,7 +426,7 @@ function FloorSelect({
|
||||
|
||||
const newZoom = Math.min(
|
||||
maxZoomRef.current,
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor)
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor),
|
||||
);
|
||||
|
||||
// Prevent zoom if at limits or change is too small
|
||||
@@ -458,7 +459,7 @@ function FloorSelect({
|
||||
const centerPosition = calculateCenterPosition(
|
||||
{ width, height },
|
||||
originalSize,
|
||||
newMinZoom // Сбрасываем к минимальному зуму (изображение на всю высоту)
|
||||
newMinZoom, // Сбрасываем к минимальному зуму (изображение на всю высоту)
|
||||
);
|
||||
|
||||
setZoom(newMinZoom);
|
||||
@@ -492,7 +493,7 @@ function FloorSelect({
|
||||
const centerPosition = calculateCenterPosition(
|
||||
{ width, height },
|
||||
originalSize,
|
||||
newMinZoom // Используем вычисленный минимальный зум
|
||||
newMinZoom, // Используем вычисленный минимальный зум
|
||||
);
|
||||
setZoom(newMinZoom);
|
||||
setImagePosition(centerPosition);
|
||||
@@ -587,7 +588,7 @@ function FloorSelect({
|
||||
`units/get-floors-data/Rove Home ${complexName
|
||||
.split("-")
|
||||
.map((w) => w[0].toUpperCase() + w.slice(1))
|
||||
.join(" ")}`
|
||||
.join(" ")}`,
|
||||
)
|
||||
.json<FloorsData[]>(),
|
||||
});
|
||||
@@ -608,7 +609,7 @@ function FloorSelect({
|
||||
data.some(
|
||||
(floorData) =>
|
||||
floorData.floor === +floor!.split(" ").at(-1)! ||
|
||||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0]
|
||||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0],
|
||||
) ||
|
||||
SPECIAL_FLOORS.includes(floor)
|
||||
)
|
||||
@@ -620,11 +621,11 @@ function FloorSelect({
|
||||
data.find(
|
||||
(floorData) =>
|
||||
floorData.floor === +floor!.split(" ").at(-1)! ||
|
||||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0]
|
||||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0],
|
||||
)!
|
||||
}
|
||||
onSelect={handleFloorClick}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -649,7 +650,7 @@ function FloorSelect({
|
||||
<div
|
||||
className={clsx(
|
||||
"overflow-hidden h-full w-full relative transition-transform duration-300",
|
||||
selectedFloor && "2xl:-translate-x-1/4"
|
||||
selectedFloor && "2xl:-translate-x-1/4",
|
||||
)}
|
||||
ref={rootRef}
|
||||
>
|
||||
@@ -663,7 +664,7 @@ function FloorSelect({
|
||||
"touch-none absolute inset-0 select-none will-change-[opacity,scale,transform] transition-opacity duration-300",
|
||||
isImageLoaded && originalSize.width !== 0
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
: "opacity-0",
|
||||
)}
|
||||
style={{
|
||||
cursor: isMobile ? (isDragging ? "grabbing" : "grab") : "default",
|
||||
@@ -724,10 +725,10 @@ function FloorSelect({
|
||||
? hoveredFloor
|
||||
: hoveredFloor.split(" ").at(-1)!)) ||
|
||||
selectedFloor?.split(" ").at(-1) === floorTitle) &&
|
||||
"fill-[#00BED7]"
|
||||
"fill-[#00BED7]",
|
||||
)}
|
||||
/>
|
||||
<path d={d[0]} className="fill-white pointer-events-none" />
|
||||
<path d={d[0]} className="pointer-events-none fill-white" />
|
||||
<rect
|
||||
x={x[1]}
|
||||
y={y}
|
||||
@@ -745,10 +746,10 @@ function FloorSelect({
|
||||
? hoveredFloor
|
||||
: hoveredFloor.split(" ").at(-1)!)) ||
|
||||
selectedFloor?.split(" ").at(-1) === floorTitle) &&
|
||||
"fill-[#00BED7]"
|
||||
"fill-[#00BED7]",
|
||||
)}
|
||||
/>
|
||||
<path d={d[1]} className="fill-white pointer-events-none" />
|
||||
<path d={d[1]} className="pointer-events-none fill-white" />
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment key={floorTitle}>
|
||||
@@ -769,18 +770,18 @@ function FloorSelect({
|
||||
? hoveredFloor
|
||||
: hoveredFloor.split(" ").at(-1)!)) ||
|
||||
selectedFloor?.split(" ").at(-1) === floorTitle) &&
|
||||
"fill-[#00BED7]"
|
||||
"fill-[#00BED7]",
|
||||
)}
|
||||
/>
|
||||
<path
|
||||
d={d as string}
|
||||
className="fill-white pointer-events-none"
|
||||
className="pointer-events-none fill-white"
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
),
|
||||
)}
|
||||
{Object.entries(
|
||||
floorsMasks[complexName as keyof typeof floorsMasks]
|
||||
floorsMasks[complexName as keyof typeof floorsMasks],
|
||||
).map(([floorTitle, d]) => (
|
||||
<path
|
||||
onMouseMove={!isMobile ? handleFloorMouseMove : undefined}
|
||||
@@ -804,7 +805,7 @@ function FloorSelect({
|
||||
SPECIAL_FLOORS.includes(floorTitle) ||
|
||||
complexName === "marasi-drive"
|
||||
? floorTitle
|
||||
: floorTitle.split(" ").at(-1)!
|
||||
: floorTitle.split(" ").at(-1)!,
|
||||
);
|
||||
|
||||
openPopup(
|
||||
@@ -812,7 +813,7 @@ function FloorSelect({
|
||||
!SPECIAL_FLOORS.includes(floorTitle) &&
|
||||
complexName === "marasi-drive"
|
||||
? (floorTitle.split(" ")[0] as "West" | "East")
|
||||
: undefined
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -821,7 +822,7 @@ function FloorSelect({
|
||||
SPECIAL_FLOORS.includes(floorTitle) ||
|
||||
complexName === "marasi-drive"
|
||||
? floorTitle
|
||||
: floorTitle.split(" ").at(-1)!
|
||||
: floorTitle.split(" ").at(-1)!,
|
||||
);
|
||||
if (!isMobile)
|
||||
openPopup(
|
||||
@@ -829,7 +830,7 @@ function FloorSelect({
|
||||
!SPECIAL_FLOORS.includes(floorTitle) &&
|
||||
complexName === "marasi-drive"
|
||||
? (floorTitle.split(" ")[0] as "West" | "East")
|
||||
: undefined
|
||||
: undefined,
|
||||
);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
@@ -853,7 +854,7 @@ function FloorSelect({
|
||||
? floorTitle
|
||||
: floorTitle.split(" ").at(-1)!)
|
||||
? "opacity-60"
|
||||
: "opacity-20"
|
||||
: "opacity-20",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
+9
-354
@@ -1,18 +1,15 @@
|
||||
import { Link, NavLink, useLocation } from "react-router";
|
||||
import { useLocation } from "react-router";
|
||||
import LocationIcon from "./icons/LocationIcon";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Button from "./ui/Button";
|
||||
import BurgerIcon from "./icons/BurgerIcon";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
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";
|
||||
import { useFavoritesUnitsStore } from "../stores/useFavoritesUnitsStore";
|
||||
import BrochureButton from "./ui/BrochureButton";
|
||||
import Logo from "./header/Logo";
|
||||
import NavItem from "./header/NavItem";
|
||||
import BrochuresDropdown from "./header/BrochuresDropdown";
|
||||
import MobileMenu from "./header/MobileMenu";
|
||||
|
||||
function Header() {
|
||||
function handleLogoClick() {
|
||||
@@ -20,9 +17,7 @@ function Header() {
|
||||
}
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const { setModal, modal } = useModalStore();
|
||||
|
||||
const burgerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const menuRef = useClickAway<HTMLDivElement>((e) => {
|
||||
@@ -36,23 +31,14 @@ function Header() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 left-0 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white ring-[0.069vw] ring-[#E2E2DC] z-[2]">
|
||||
<header className="sticky top-0 left-0 w-full h-14 md:max-2xl:h-16 2xl:h-[4.444vw] flex items-center justify-center bg-white ring-[0.069vw] ring-[#E2E2DC] z-20">
|
||||
<div className="flex 2xl:gap-[1.111vw] gap-4 flex-1">
|
||||
<div
|
||||
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
|
||||
onClick={handleLogoClick}
|
||||
>
|
||||
<img
|
||||
src="/images/logo.svg"
|
||||
alt="logo"
|
||||
className="2xl:w-[5.972vw] w-[88px]"
|
||||
/>
|
||||
</div>
|
||||
<Logo onClick={handleLogoClick} />
|
||||
<div className="flex 2xl:gap-[0.278vw] gap-1 items-center max-md:hidden">
|
||||
<span className="2xl:size-[1.389vw] size-5 opacity-40">
|
||||
<LocationIcon />
|
||||
</span>
|
||||
<p className="text-s opacity-40">Dubai</p>
|
||||
<p className="opacity-40 text-s">Dubai</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-2xl:order-2">
|
||||
@@ -82,340 +68,9 @@ function Header() {
|
||||
</div>
|
||||
<div className="flex flex-1 justify-end">{/* <ProfileBar /> */}</div>
|
||||
</header>
|
||||
<AnimatePresence mode="wait">
|
||||
{opened && (
|
||||
<>
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
initial={{ opacity: 0, y: "-100%" }}
|
||||
animate={{ opacity: 1, y: "0%" }}
|
||||
exit={{ opacity: 0, y: "-100%" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="2xl:hidden fixed z-[1] left-0 md:top-16 top-14 md:p-4 p-3 w-full md:rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)] pointer-events-auto ring-[0.069vw] ring-[#E2E2DC]"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-h3 font-medium">Projects</p>
|
||||
<div className="max-md:flex-col flex flex-wrap gap-2 items-start">
|
||||
{projects.map(({ img, title }, index) => {
|
||||
const name = title
|
||||
.split(" ")
|
||||
.slice(-2)
|
||||
.join("-")
|
||||
.toLowerCase();
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
to={`/complex/${name}`}
|
||||
className={clsx(
|
||||
"p-1 pr-5 flex gap-2 items-center flex-nowrap ring-[0.069vw] rounded-[40px] transition-[box-shadow,opacity,color]",
|
||||
pathname.endsWith(name)
|
||||
? "ring-[#00BED7] text-[#00BED7]"
|
||||
: "ring-[#E2E2DC] opacity-70"
|
||||
)}
|
||||
>
|
||||
<img src={img} alt={title} className="size-10" />
|
||||
<span className="text-s">{title}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<Link
|
||||
to="/"
|
||||
className={clsx(
|
||||
"px-5 py-3.5 content-center ring-[0.069vw] rounded-[40px] text-s opacity-70 transition-[box-shadow,color]",
|
||||
pathname === "/"
|
||||
? "ring-[#00BED7] text-[#00BED7]"
|
||||
: "ring-[#E2E2DC]"
|
||||
)}
|
||||
>
|
||||
Show on Map
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="md:grid-cols-2 md:gap-4 grid 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"} />
|
||||
</nav>
|
||||
<hr className="border-[#E2E2DC]" />
|
||||
<div className="space-y-6">
|
||||
<p className="text-h3 font-medium">Brochures</p>
|
||||
<div className="p-[0.278vw] flex md:gap-[1.111vw] gap-6 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 flex-col gap-2">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/marasi-drive/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/marasi-drive/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Technical Brochure"}
|
||||
link="/files/brochures/marasi-drive/Technical Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/marasi-drive/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Reasons to buy"}
|
||||
link="/files/brochures/marasi-drive/Reasons to buy.pdf"
|
||||
/>
|
||||
</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-2">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/downtown/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/downtown/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Unit Plans"}
|
||||
link="/files/brochures/downtown/Unit Plan.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Typical Floor plan 2-13"}
|
||||
link="/files/brochures/downtown/Typical Floor plan 2-13.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Typical Floor plan 14-19"}
|
||||
link="/files/brochures/downtown/Typical Floor plan 14-19.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/downtown/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Reasons to buy"}
|
||||
link="/files/brochures/downtown/Reasons to buy.pdf"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4">
|
||||
<p className="text-s font-medium">Rove Home Dubai Marina</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/dubai-marina/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/dubai-marina/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Technical Brochure"}
|
||||
link="/files/brochures/dubai-marina/Technical Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/dubai-marina/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Arabic"}
|
||||
link="/files/brochures/dubai-marina/RHDM Arabic.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Turkish"}
|
||||
link="/files/brochures/dubai-marina/RHDM Turkish.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Russian"}
|
||||
link="/files/brochures/dubai-marina/RHDM Russian.pdf"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex bottom-0 left-0 justify-between items-end p-4 pt-6 w-full bg-white">
|
||||
<p className="text-s w-fit opacity-40">
|
||||
For more information, visit our
|
||||
<br />
|
||||
website:{" "}
|
||||
<Link
|
||||
to="https://www.irth.ae"
|
||||
className="underline text-[#00BED7]"
|
||||
>
|
||||
www.irth.ae
|
||||
</Link>
|
||||
</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={() => setModal(<PrivacyPolicyModal />)}
|
||||
>
|
||||
<span className="text-btn-s">Privacy Policy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="fixed inset-0" />
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<MobileMenu opened={opened} onClose={() => setOpened(false)} menuRef={menuRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
||||
function NavItem({ href, title }: { href: string; title: string }) {
|
||||
const { favoriteUnits } = useFavoritesUnitsStore();
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={href}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
"text-btn-m 2xl:px-[1.25vw] 2xl:py-[0.903vw] p-4 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none max-2xl:text-center max-2xl:bg-[#F3F3F2] relative",
|
||||
isActive &&
|
||||
"!bg-[#00BED7] text-[#FFFFFF] [&_>div]:bg-white [&_>div]:text-[#00BED7]"
|
||||
)
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{title === "Favorites" && !!favoriteUnits.length && (
|
||||
<div className="absolute top-1 right-1 rounded-full min-w-4 min-h-4 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s text-center font-mono">
|
||||
{favoriteUnits.length}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
function BrochuresDropdown() {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setOpened(false));
|
||||
|
||||
return (
|
||||
<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)}
|
||||
>
|
||||
<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
|
||||
key={"menu"}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ bounce: 0, duration: 0.3 }}
|
||||
className="max-2xl:hidden p-[1.667vw] flex gap-[1.111vw] justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[40vw] w-[50vw] 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]">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/marasi-drive/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/marasi-drive/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Technical Brochure"}
|
||||
link="/files/brochures/marasi-drive/Technical Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/marasi-drive/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Reasons to buy"}
|
||||
link="/files/brochures/marasi-drive/Reasons to buy.pdf"
|
||||
/>
|
||||
</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]">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/downtown/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/downtown/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Unit Plans"}
|
||||
link="/files/brochures/downtown/Unit Plan.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Typical Floor plan 2-13"}
|
||||
link="/files/brochures/downtown/Typical Floor plan 2-13.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Typical Floor plan 14-19"}
|
||||
link="/files/brochures/downtown/Typical Floor plan 14-19.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/downtown/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Reasons to buy"}
|
||||
link="/files/brochures/downtown/Reasons to buy.pdf"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4">
|
||||
<p className="text-s font-medium">Rove Home Dubai Marina</p>
|
||||
<div className="flex flex-col gap-[0.556vw]">
|
||||
<BrochureButton
|
||||
title={"Main Brochure"}
|
||||
link="/files/brochures/dubai-marina/Main Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Amenities Brochure"}
|
||||
link="/files/brochures/dubai-marina/Amenities Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Technical Brochure"}
|
||||
link="/files/brochures/dubai-marina/Technical Brochure.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"Factsheet"}
|
||||
link="/files/brochures/dubai-marina/Factsheet.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Arabic"}
|
||||
link="/files/brochures/dubai-marina/RHDM Arabic.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Turkish"}
|
||||
link="/files/brochures/dubai-marina/RHDM Turkish.pdf"
|
||||
/>
|
||||
<BrochureButton
|
||||
title={"RHDM Russian"}
|
||||
link="/files/brochures/dubai-marina/RHDM Russian.pdf"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,49 +23,93 @@ function ProjectSelect<T extends boolean = false>({
|
||||
|
||||
useEffect(() => onSelect(selectedProject), [selectedProject]);
|
||||
|
||||
// Get unique building types and group projects by type
|
||||
const uniqueBuildingTypes = [...new Set(projects.map((p) => p.buildingType))];
|
||||
|
||||
const getProjectsByType = (buildingType: string) => {
|
||||
return projects.filter((p) => p.buildingType === buildingType);
|
||||
};
|
||||
|
||||
const capitalizeFirstLetter = (str: string) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
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] md:max-2xl:px-5 md:max-2xl:py-3.5 text-s 2xl:ring-[0.069vw] ring-1 transition-[box-shadow,opacity] cursor-pointer",
|
||||
!selectedProject ? "ring-[#00BED7]" : "ring-[#E2E2DC]/70"
|
||||
)}
|
||||
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-1 transition-[box-shadow] cursor-pointer",
|
||||
selectedProject && project.title === selectedProject.title
|
||||
? "ring-[#00BED7]"
|
||||
: "ring-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
>
|
||||
<img
|
||||
src={project.img}
|
||||
alt={project.title}
|
||||
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
|
||||
/>
|
||||
<p
|
||||
className={clsx(
|
||||
"2xl:mr-[1.111vw] mr-6 transition-opacity",
|
||||
((selectedProject && selectedProject.title !== project.title) ||
|
||||
!selectedProject) &&
|
||||
"bg-opacity-70"
|
||||
)}
|
||||
>
|
||||
{project.title}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex 2xl:gap-[2.222vw] gap-8 max-md:hidden">
|
||||
{/* Dynamic Building Type Sections */}
|
||||
{uniqueBuildingTypes.map((buildingType) => {
|
||||
const projectsOfType = getProjectsByType(buildingType);
|
||||
|
||||
if (projectsOfType.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={buildingType}
|
||||
className="flex flex-col 2xl:gap-[0.556vw] gap-2"
|
||||
>
|
||||
<p className="text-s text-[#0D1922]/70">
|
||||
{capitalizeFirstLetter(buildingType)}
|
||||
</p>
|
||||
<div className="flex 2xl:gap-[0.556vw] gap-2">
|
||||
{projectsOfType.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-1 transition-[box-shadow] cursor-pointer bg-white",
|
||||
selectedProject &&
|
||||
project.title === selectedProject.title
|
||||
? "ring-[#00BED7]"
|
||||
: "ring-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
>
|
||||
<img
|
||||
src={project.img}
|
||||
alt={project.title}
|
||||
className="object-cover 2xl:w-[2.778vw] w-10 aspect-square rounded-full"
|
||||
/>
|
||||
<p
|
||||
className={clsx(
|
||||
"2xl:mr-[1.111vw] mr-6 transition-opacity",
|
||||
selectedProject &&
|
||||
selectedProject.title !== project.title &&
|
||||
"opacity-70"
|
||||
)}
|
||||
>
|
||||
{project.title}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex 2xl:gap-[2.222vw] gap-8 max-md:hidden">
|
||||
{/* All Projects Option (when withAll is true) */}
|
||||
{withAll && (
|
||||
<div className="flex flex-col 2xl:gap-[0.556vw] gap-2">
|
||||
<p className="text-s text-[#0D1922]/70 opacity-0 pointer-events-none">
|
||||
All
|
||||
</p>
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:rounded-[2.778vw] rounded-[40px] 2xl:py-[0.972vw] 2xl:px-[1.389vw] md:max-2xl:px-5 md:max-2xl:py-3.5 text-s 2xl:ring-[0.069vw] ring-1 transition-[box-shadow,opacity] cursor-pointer bg-white",
|
||||
!selectedProject ? "ring-[#00BED7]" : "ring-[#E2E2DC]"
|
||||
)}
|
||||
onClick={() => setSelectedProject(null)}
|
||||
>
|
||||
All Projects
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Select - keep as is for now */}
|
||||
<Select
|
||||
options={[
|
||||
...(withAll ? ["All"] : []),
|
||||
|
||||
@@ -140,16 +140,16 @@ function SearchFilters({
|
||||
<>
|
||||
{inModal && (
|
||||
<div
|
||||
className="fixed inset-0 bg-[#0D1922]/40 cursor-pointer z-5"
|
||||
className="fixed inset-0 bg-[#0D1922]/40 cursor-pointer z-10"
|
||||
onClick={() => setInModal(false)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"2xl:p-[2.222vw] md:max-2xl:p-6 p-4 bg-white 2xl:rounded-b-[1.667vw] md:rounded-t-3xl 2xl:space-y-[2.222vw] space-y-8",
|
||||
"2xl:p-[2.222vw] md:max-2xl:p-6 p-4 bg-white 2xl:rounded-b-[1.667vw] md:rounded-t-3xl 2xl:space-y-[2.222vw] space-y-8 z-10",
|
||||
inModal &&
|
||||
"z-5 fixed max-md:pb-0 max-md:pt-6 2xl:top-[calc(2.778vw+7.5vh)] max-md:w-full 2xl:left-[2.222vw] 2xl:right-[2.222vw] 2xl:rounded-[1.667vw] md:max-2xl:rounded-3xl md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-24 max-md:bottom-0 max-md:overflow-auto max-md:!rounded-t-3xl max-md:max-h-[calc(100dvh-40px)]"
|
||||
"fixed z-10 max-md:pb-0 max-md:pt-6 2xl:top-[calc(2.778vw+7.5vh)] max-md:w-full 2xl:left-[2.222vw] 2xl:right-[2.222vw] 2xl:rounded-[1.667vw] md:max-2xl:rounded-3xl md:max-2xl:left-6 md:max-2xl:right-6 md:max-2xl:top-24 max-md:bottom-0 max-md:overflow-auto max-md:!rounded-t-3xl max-md:max-h-[calc(100dvh-40px)]"
|
||||
)}
|
||||
>
|
||||
{inModal && (
|
||||
@@ -165,7 +165,7 @@ function SearchFilters({
|
||||
)}
|
||||
<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">
|
||||
<p className="font-medium text-h2">
|
||||
{inModal ? "Filters" : "Search"}
|
||||
</p>
|
||||
<div className={clsx(!inModal && "max-md:hidden")}>
|
||||
@@ -302,7 +302,11 @@ function SearchFilters({
|
||||
)}
|
||||
>
|
||||
{inModal ? (
|
||||
<Button variant="cta" onClick={applyFilters} className="max-md:w-full">
|
||||
<Button
|
||||
variant="cta"
|
||||
onClick={applyFilters}
|
||||
className="max-md:w-full"
|
||||
>
|
||||
Show{" "}
|
||||
<AnimatePresence mode="wait">
|
||||
{count !== undefined && (
|
||||
|
||||
@@ -11,9 +11,10 @@ import InfoIcon from "./icons/InfoIcon";
|
||||
import { markers } from "../data/markers";
|
||||
import { masks } from "../data/masks";
|
||||
import ButtonGroup from "./ButtonGroup";
|
||||
import { ComplexName } from "../types/ComplexName";
|
||||
|
||||
interface SequenceSliderProps {
|
||||
complexName: string;
|
||||
complexName: ComplexName;
|
||||
}
|
||||
|
||||
const FRAME_COUNT = 120;
|
||||
@@ -104,7 +105,7 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
|
||||
<img
|
||||
src={`/images/loader.png`}
|
||||
alt=""
|
||||
className="size-16 animate-spin"
|
||||
className="animate-spin size-16"
|
||||
/>
|
||||
<p className="text-[#00BED7] text-m">
|
||||
{Math.round((imageLoaded / FRAME_COUNT) * 100)}%
|
||||
@@ -160,7 +161,7 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 4096 1752"
|
||||
className="max-2xl:hidden absolute top-0 left-0 w-full h-full"
|
||||
className="absolute top-0 left-0 w-full h-full max-2xl:hidden"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
>
|
||||
<path
|
||||
@@ -239,7 +240,7 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
|
||||
<Button
|
||||
variant="cta"
|
||||
size={innerWidth < 768 ? "medium" : "small"}
|
||||
className="md:bottom-6 2xl:hidden absolute bottom-4 left-1/2 -translate-x-1/2"
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2 md:bottom-6 2xl:hidden"
|
||||
onClick={() => navigate("floors")}
|
||||
>
|
||||
Select a floor
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import clsx from "clsx";
|
||||
import Button from "../ui/Button";
|
||||
import ChevronDownIcon from "../icons/ChevronDownIcon";
|
||||
import ProjectBrochuresList from "./ProjectBrochuresList";
|
||||
import { projectBrochures } from "../../data/brochures";
|
||||
|
||||
export default function BrochuresDropdown() {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setOpened(false));
|
||||
|
||||
return (
|
||||
<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)}
|
||||
>
|
||||
<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
|
||||
key={"menu"}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ bounce: 0, duration: 0.3 }}
|
||||
className="max-2xl:hidden p-[1.667vw] flex gap-[1.111vw] justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[40vw] w-[50vw] rounded-[1.111vw] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]"
|
||||
>
|
||||
{projectBrochures.map((project, index) => (
|
||||
<ProjectBrochuresList
|
||||
key={index}
|
||||
projectTitle={project.projectTitle}
|
||||
brochures={project.brochures}
|
||||
variant="desktop"
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
interface LogoProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export default function Logo({ onClick }: LogoProps) {
|
||||
return (
|
||||
<div
|
||||
className="2xl:px-[2.222vw] 2xl:py-[1.111vw] md:max-2xl:px-6 max-md:px-4 py-4 cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
src="/images/logo.svg"
|
||||
alt="logo"
|
||||
className="2xl:w-[5.972vw] w-[88px]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Link, useLocation, useNavigate } from "react-router";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import clsx from "clsx";
|
||||
import Button from "../ui/Button";
|
||||
import NavItem from "./NavItem";
|
||||
import ProjectBrochuresList from "./ProjectBrochuresList";
|
||||
import { projects } from "../../data/projects";
|
||||
import { projectBrochures } from "../../data/brochures";
|
||||
import useModalStore from "../../stores/useModalStore";
|
||||
import PrivacyPolicyModal from "../modals/PrivacyPolicyModal";
|
||||
|
||||
interface MobileMenuProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
menuRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default function MobileMenu({
|
||||
opened,
|
||||
onClose,
|
||||
menuRef,
|
||||
}: MobileMenuProps) {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{opened && (
|
||||
<>
|
||||
<motion.div
|
||||
ref={menuRef}
|
||||
initial={{ opacity: 0, y: "-100%" }}
|
||||
animate={{ opacity: 1, y: "0%" }}
|
||||
exit={{ opacity: 0, y: "-100%" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="2xl:hidden fixed z-[1] left-0 md:top-16 top-14 md:p-4 p-3 w-full md:rounded-b-2xl flex flex-col gap-10 bg-white overflow-y-auto max-h-[calc(100dvh-56px)] pointer-events-auto ring-[0.069vw] ring-[#E2E2DC]"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="font-medium text-h3">Projects</p>
|
||||
<div className="flex flex-wrap gap-2 items-start max-md:flex-col">
|
||||
{projects.map(({ img, title, slug }, index) => {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
to={`/complex/${slug}`}
|
||||
className={clsx(
|
||||
"p-1 pr-5 flex gap-2 items-center flex-nowrap rounded-full transition-[box-shadow,opacity,color] text-s md:ring-[0.069vw] ring-1",
|
||||
pathname.endsWith(slug)
|
||||
? "ring-[#00BED7] text-[#00BED7]"
|
||||
: "ring-[#E2E2DC] text-[#0D1922]/70"
|
||||
)}
|
||||
>
|
||||
<img src={img} alt={title} className="size-10" />
|
||||
<span className="text-s">{title}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
className={clsx(
|
||||
"px-5 py-3.5 rounded-full text-s md:ring-[0.069vw] ring-1",
|
||||
pathname === "/"
|
||||
? "ring-[#00BED7] text-[#00BED7]"
|
||||
: "ring-[#E2E2DC] text-[#0D1922]/70"
|
||||
)}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
Show on Map
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="grid gap-2 md:grid-cols-2 md:gap-4">
|
||||
<NavItem href={"/unit-types"} title={"Unit Types"} />
|
||||
<NavItem href={"/about"} title={"About IRTH"} />
|
||||
<NavItem href={"/favorites"} title={"Favorites"} />
|
||||
<NavItem href={"/search"} title={"Search"} />
|
||||
</nav>
|
||||
<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 justify-stretch items-stretch max-md:flex-col">
|
||||
{projectBrochures.map((project, index) => (
|
||||
<ProjectBrochuresList
|
||||
key={index}
|
||||
projectTitle={project.projectTitle}
|
||||
brochures={project.brochures}
|
||||
variant="mobile"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex bottom-0 left-0 justify-between items-end p-4 pt-6 w-full bg-white">
|
||||
<p className="opacity-40 text-s w-fit">
|
||||
For more information, visit our
|
||||
<br />
|
||||
website:{" "}
|
||||
<Link
|
||||
to="https://www.irth.ae"
|
||||
className="underline text-[#00BED7]"
|
||||
>
|
||||
www.irth.ae
|
||||
</Link>
|
||||
</p>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={() => setModal(<PrivacyPolicyModal />)}
|
||||
>
|
||||
<span className="text-btn-s">Privacy Policy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="fixed inset-0" />
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NavLink } from "react-router";
|
||||
import clsx from "clsx";
|
||||
import { useFavoritesUnitsStore } from "../../stores/useFavoritesUnitsStore";
|
||||
|
||||
interface NavItemProps {
|
||||
href: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function NavItem({ href, title }: NavItemProps) {
|
||||
const { favoriteUnits } = useFavoritesUnitsStore();
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={href}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
"text-btn-m 2xl:px-[1.25vw] 2xl:py-[0.903vw] p-4 2xl:rounded-[0.833vw] rounded-xl transition-colors duration-300 !leading-none max-2xl:text-center max-2xl:bg-[#F3F3F2] relative",
|
||||
isActive &&
|
||||
"!bg-[#00BED7] text-[#FFFFFF] [&_>div]:bg-white [&_>div]:text-[#00BED7]"
|
||||
)
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{title === "Favorites" && !!favoriteUnits.length && (
|
||||
<div className="absolute top-1 right-1 rounded-full min-w-4 min-h-4 flex items-center justify-center aspect-square bg-[#00BED7] text-white text-caption-s text-center font-mono">
|
||||
{favoriteUnits.length}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import BrochureButton from "../ui/BrochureButton";
|
||||
import { Brochure } from "../../data/brochures";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ProjectBrochuresListProps {
|
||||
projectTitle: string;
|
||||
brochures: Brochure[];
|
||||
variant?: "mobile" | "desktop";
|
||||
}
|
||||
|
||||
export default function ProjectBrochuresList({
|
||||
projectTitle,
|
||||
brochures,
|
||||
variant = "desktop",
|
||||
}: ProjectBrochuresListProps) {
|
||||
const isMobile = variant === "mobile";
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4">
|
||||
<p className="font-medium text-s">{projectTitle}</p>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-col",
|
||||
isMobile ? "gap-2" : "gap-[0.556vw]"
|
||||
)}
|
||||
>
|
||||
{brochures.map((brochure, index) => (
|
||||
<BrochureButton
|
||||
key={index}
|
||||
title={brochure.title}
|
||||
link={brochure.link}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user