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:
2026-01-21 17:21:38 +05:00
parent 6b81b2995b
commit e5b0dc99a6
262 changed files with 685 additions and 488 deletions
+33 -32
View File
@@ -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
View File
@@ -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>
);
}
+84 -40
View File
@@ -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"] : []),
+9 -5
View File
@@ -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 && (
+5 -4
View File
@@ -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>
);
}
+18
View File
@@ -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>
);
}
+121
View File
@@ -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>
);
}
+32
View File
@@ -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>
);
}