This commit is contained in:
2026-01-22 15:08:31 +05:00
26 changed files with 1041 additions and 435 deletions
+8 -22
View File
@@ -26,7 +26,7 @@ export default function AboutHQ() {
);
return (
<div className="relative bg-white">
<section className="relative bg-white">
{/* Hero */}
<div className="2xl:sticky relative 2xl:top-[4.444vw] ">
<section className="w-full 2xl:h-[calc(100dvh-4.444vw)] relative md:h-[calc(100dvh-4rem)] h-[calc(100dvh-3.15rem)] md:max-h-[125vw] max-md:max-h-[162.222vw] flex flex-col justify-between bg-white 2xl:p-[2.222vw] 2xl:pt-[5vw] md:max-2xl:p-6 md:max-2xl:pt-10 p-4 pt-8 overflow-hidden">
@@ -40,8 +40,8 @@ export default function AboutHQ() {
<div className="2xl:space-y-[1.667vw] md:max-2xl:space-y-6 space-y-4 relative">
<h1 className="2xl:text-[5vw] md:max-2xl:text-7xl text-[32px] leading-none tracking-[-0.07em] font-mixcase-unmixed font-medium whitespace-pre-line">
{`Rove Home HQ —
Work looks different
here`}
Work looks different
here`}
</h1>
</div>
@@ -55,7 +55,7 @@ export default function AboutHQ() {
{`Welcome to the office you
actually want to show up for`}
</h4>
<p className="text-s opacity-70 2xl:w-1/4 md:max-2xl:w-1/3">
<p className="opacity-70 text-s 2xl:w-1/4 md:max-2xl:w-1/3">
HQ by Rove was born out of a question: what if the office could
feel alive again? Now, the first ever hospitality-branded office
building in Dubai is here to answer it. Starting in Marasi Bay
@@ -68,20 +68,6 @@ export default function AboutHQ() {
className="absolute 2xl:top-[1.806vw] 2xl:right-[2.222vw] 2xl:w-[6.111vw] md:max-2xl:hidden w-[17.778vw] max-md:left-1/2 max-md:top-[39.444vw] max-md:-translate-x-1/2"
/>
</section>
<section>
<div className="space-y-6 bg-white px-4 py-8 md:hidden">
<h4 className="text-h4 text-[#00BED7] font-medium whitespace-pre-line ">
{`Welcome to the office you
actually want to show up for`}
</h4>
<p className="text-s opacity-70 2xl:w-1/4 md:max-2xl:w-1/3">
HQ by Rove was born out of a question: what if the office could
feel alive again? Now, the first ever hospitality-branded office
building in Dubai is here to answer it. Starting in Marasi Bay
</p>
</div>
</section>
</div>
{/* Slider */}
@@ -96,10 +82,10 @@ export default function AboutHQ() {
<span className="opacity-40">More than an office,</span>{" "}
<br className="2xl:hidden" /> a lifestyle.
</h2>
<p className="text-s opacity-70 whitespace-pre-line text-center max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16">
{`Living rooms became boardrooms, kitchens became creative hubs.
But as the world returned, the office didnt keep up. HQ by Rove is the
answer - an office with a living touch.`}
But as the world returned, the office didnt keep up. HQ by Rove is the
answer - an office with a living touch.`}
</p>
</div>
<AboutHQScrollSlider scrollYProgress={scrollYProgress} />
@@ -313,6 +299,6 @@ export default function AboutHQ() {
</div>
</div>
</section>
</div>
</section>
);
}
+17 -17
View File
@@ -53,7 +53,7 @@ function AboutMarasiDrive() {
{`A home for the young
and young in heart`}
</h4>
<p className="text-s opacity-70 2xl:w-1/4 md:max-2xl:w-1/3">
<p className="opacity-70 text-s 2xl:w-1/4 md:max-2xl:w-1/3">
The dynamic essence of Rove comes to life at our new location in
Marasi Drive, Business Bay. Enjoy an urban living experience
beyond the ordinary.
@@ -62,12 +62,12 @@ function AboutMarasiDrive() {
</section>
<section>
<div className="space-y-6 bg-white px-4 py-8 md:hidden">
<div className="px-4 py-8 space-y-6 bg-white md:hidden">
<h4 className="text-h4 text-[#00BED7] font-medium whitespace-pre-line ">
{`A home for the young
and young in heart`}
</h4>
<p className="text-s opacity-70">
<p className="opacity-70 text-s">
The dynamic essence of Rove comes to life at our new location in
Marasi Drive, Business Bay. Enjoy an urban living experience
beyond the ordinary.
@@ -79,11 +79,11 @@ function AboutMarasiDrive() {
<div className="2xl:sticky relative 2xl:top-[-134vw]">
<section className="bg-white w-full overflow-clip 2xl:pt-[9.444vw] 2xl:px-[2.222vw] 2xl:pb-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 pb-8 flex flex-col 2xl:gap-[4.444vw] md:max-2xl:gap-12 gap-10">
<div className="flex flex-col 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 items-center">
<h2 className="font-mixcase-unmixed text-h1 text-center max-md:whitespace-pre-line">
<h2 className="text-center font-mixcase-unmixed text-h1 max-md:whitespace-pre-line">
{`What makes
a Rove Home?`}
</h2>
<p className="opacity-70 text-s whitespace-pre-line text-center max-md:whitespace-normal">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal">
{`Experience the difference with Rove Home where modern amenities, trendy
interiors, and smart features cater to your unique style. Rove Home is your
destination for artful inspiration and cleverly activated spaces.`}
@@ -118,11 +118,11 @@ function AboutMarasiDrive() {
</section>
<section className="bg-white w-full overflow-clip flex flex-col 2xl:gap-[4.444vw] gap-12 2xl:pt-[9.444vw] 2xl:px-[2.222vw] 2xl:pb-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 pb-8">
<div className="flex flex-col 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 items-center">
<h2 className="font-mixcase-unmixed text-h1 text-center max-md:whitespace-pre-line">
<h2 className="text-center font-mixcase-unmixed text-h1 max-md:whitespace-pre-line">
{`Expandable living
solutions`}
</h2>
<p className="text-s opacity-70 whitespace-pre-line text-center max-md:whitespace-normal">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal">
{`ORI introduces a revolutionary solution to apartment living,
where space is not just a constraint but an opportunity.`}
</p>
@@ -158,10 +158,10 @@ function AboutMarasiDrive() {
</section>
<section className="bg-white w-full overflow-clip flex flex-col 2xl:gap-[4.444vw] gap-12 2xl:pt-[9.444vw] 2xl:pb-[15vw] 2xl:px-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 max-md:pb-8 max-md:gap-0">
<div className="flex flex-col 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 items-center ">
<h2 className="font-mixcase-unmixed text-h1 text-center">
<h2 className="text-center font-mixcase-unmixed text-h1">
Inspired interiors
</h2>
<p className="text-s opacity-70 whitespace-pre-line text-center max-md:whitespace-normal max-md:mb-10">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal max-md:mb-10">
{`Smart, flexible designs maximize every inch. The ORI Cloud Bed
expands space by 33%, while the Flexibed transforms living areas
into bedrooms. Multipurpose layouts adapt effortlessly—blending
@@ -173,14 +173,14 @@ function AboutMarasiDrive() {
</section>
</div>
<div className="2xl:sticky relative">
<div className="relative 2xl:sticky">
<section className="bg-white w-full overflow-clip flex flex-col 2xl:gap-[4.444vw] gap-12 2xl:pt-[9.444vw] 2xl:px-[2.222vw] 2xl:pb-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 pb-8">
<div className="flex flex-col items-center 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6">
<h2 className="font-mixcase-unmixed text-h1 text-center whitespace-pre-line">
<h2 className="text-center whitespace-pre-line font-mixcase-unmixed text-h1">
{`A home for the young
and young in heart`}
</h2>
<p className="text-s opacity-70 whitespace-pre-line text-center max-md:whitespace-normal">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal">
{`The dynamic essence of Rove comes to life at our new
location in Marasi Drive, Business Bay. Enjoy an urban
living experience beyond the ordinary.`}
@@ -201,7 +201,7 @@ function AboutMarasiDrive() {
<h2 className="font-mixcase-unmixed text-h1 text-center whitespace-pre-line max-md:mb-[24px] md:max-2xl:mb-8">
{`Explore the neighbourhood`}
</h2>
<p className="text-s opacity-70 whitespace-pre-line text-center max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16">
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16">
{`With Dubai's trendiest spots right at your doorstep, explore nearby
entertainment and dining experiences in just 15 minutes. Live your best life
at Rove Home Marasi Drive!`}
@@ -229,7 +229,7 @@ function AboutMarasiDrive() {
{marasiDriveMapCards.map((card) => (
<MarasiDriveMapCard {...card} key={card.title} />
))}
<div className="col-start-3 col-span-full row-start-1 row-span-full">
<div className="col-span-full col-start-3 row-span-full row-start-1">
<img
src="/images/about-complex/marasi-drive/map/map.png"
alt=""
@@ -249,7 +249,7 @@ function AboutMarasiDrive() {
</div>
<div className="px-[2.778vw] flex flex-col gap-[3.333vw] max-2xl:px-0 md:max-2xl:gap-[6.25vw]">
<div className="space-y-[1.111vw]">
<h2 className="whitespace-pre-line text-h2 font-medium max-2xl:mb-4">
<h2 className="font-medium whitespace-pre-line text-h2 max-2xl:mb-4">
{`Live Different
with Rove Home`}
</h2>
@@ -268,8 +268,8 @@ function AboutMarasiDrive() {
link="/files/marasi-drive/Main Brochure.pdf"
/>
<BrochureButton
title={"Amenties Brochure"}
link="/files/marasi-drive/Amenties Brochure.pdf"
title={"Amenities Brochure"}
link="/files/marasi-drive/Amenities Brochure.pdf"
/>
<BrochureButton
title={"Technical Brochure"}
+20 -20
View File
@@ -14,19 +14,19 @@ interface FloorPopupProps {
onSelect: (floor: string) => void;
}
function getAmentiesCount(complexName: string, title: string) {
const amenties = projects.find(
function getAmenitiesCount(complexName: string, title: string) {
const amenities = projects.find(
(proj) => proj.slug === complexName
)?.amentiesFloors;
return amenties?.find((amenty) => amenty.title === title);
)?.amenitiesFloors;
return amenities?.find((amenity) => amenity.title === title);
}
function FloorPopup({ title, complexName, data, onSelect }: FloorPopupProps) {
const { setPopup } = usePopupStore();
const amentiesCount = useMemo(
const amenitiesCount = useMemo(
() =>
Number.isNaN(+title.at(-1)!)
? getAmentiesCount(complexName, title)
? getAmenitiesCount(complexName, title)
: null,
[title, complexName]
);
@@ -35,16 +35,16 @@ function FloorPopup({ title, complexName, data, onSelect }: FloorPopupProps) {
<div className="flex flex-col 2xl:gap-y-[0.556vw] gap-y-2">
<div className="flex 2xl:gap-[0.556vw] gap-2">
<p className="text-h5 font-medium">
{amentiesCount ? title : `${title.split(" ").at(-1)} floor`}
{amenitiesCount ? title : `${title.split(" ").at(-1)} floor`}
</p>
{complexName === "marasi-drive" && !amentiesCount && (
{complexName === "marasi-drive" && !amenitiesCount && (
<p className="text-s opacity-40">{title.split(" ")[0]} Wing</p>
)}
</div>
<div className="flex 2xl:gap-[0.278vw] gap-1">
<p className="2xl:px-[0.556vw] 2xl:py-[0.278vw] px-2 py-0.5 bg-[#F3F3F2] 2xl:rounded-[0.278vw] rounded text-caption-s opacity-70">
{amentiesCount !== null
? `${amentiesCount?.total} Amenties`
{amenitiesCount !== null
? `${amenitiesCount?.total} Amenities`
: `${
complexName === "marasi-drive" && data
? data[
@@ -57,7 +57,7 @@ function FloorPopup({ title, complexName, data, onSelect }: FloorPopupProps) {
: data?.others.totalUnits
} apartments`}
</p>
{!amentiesCount && (
{!amenitiesCount && (
<div className="2xl:px-[0.556vw] 2xl:py-[0.278vw] px-2 py-0.5 bg-[#30B216] bg-opacity-[8%] 2xl:rounded-[0.278vw] rounded flex 2xl:gap-[0.278vw] gap-1">
<span className="2xl:size-[0.833vw] size-3 text-[#30B216]">
<HumanIcon />
@@ -66,12 +66,12 @@ function FloorPopup({ title, complexName, data, onSelect }: FloorPopupProps) {
</div>
)}
</div>
{((amentiesCount?.indoor && amentiesCount?.outdoor) ||
!amentiesCount) && (
{((amenitiesCount?.indoor && amenitiesCount?.outdoor) ||
!amenitiesCount) && (
<hr className="border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
)}
<div className="flex flex-col 2xl:gap-y-[0.556vw] gap-y-2">
{!amentiesCount && data ? (
{!amenitiesCount && data ? (
Object.entries(
data[
title.split(" ")[0] === "East"
@@ -92,20 +92,20 @@ function FloorPopup({ title, complexName, data, onSelect }: FloorPopupProps) {
))
) : (
<>
{amentiesCount?.indoor && (
{amenitiesCount?.indoor && (
<div className="flex 2xl:gap-[0.556vw] gap-2">
<p className="bg-[#00BED7] rounded-full flex justify-center items-center font-mono text-caption-s text-white 2xl:size-[1.111vw] size-4">
{amentiesCount.indoor}
{amenitiesCount.indoor}
</p>
<p className="text-caption-m opacity-70">Indoor Amenties</p>
<p className="text-caption-m opacity-70">Indoor Amenities</p>
</div>
)}
{amentiesCount?.outdoor && (
{amenitiesCount?.outdoor && (
<div className="flex 2xl:gap-[0.556vw] gap-2">
<p className="bg-[#00BED7] rounded-full flex justify-center items-center font-mono text-caption-s text-white 2xl:size-[1.111vw] size-4">
{amentiesCount.outdoor}
{amenitiesCount.outdoor}
</p>
<p className="text-caption-m opacity-70">Outdoor Amenties</p>
<p className="text-caption-m opacity-70">Outdoor Amenities</p>
</div>
)}
</>
@@ -0,0 +1,142 @@
import { useState } from "react";
import { AmenitiesFloorData } from "../../types/Floor";
import Badge from "../ui/Badge";
import Button from "../ui/Button";
import PlayIcon from "../icons/PlayIcon";
import useModalStore from "../../stores/useModalStore";
import VideoModal from "../VideoModal";
import ViewToggleButtons from "./ViewToggleButtons";
import AmentitiesBadge from "../AmentitiesCard";
import AmenitiesBadge from "../icons/AmenitiesBadge";
import AmentitiesContentSlider from "../AmentitiesContentSlider";
interface AmenitiesFloorViewProps {
floor: AmenitiesFloorData;
}
function AmenitiesFloorView({ floor }: AmenitiesFloorViewProps) {
const { setModal } = useModalStore();
const [currentView, setCurrentView] = useState<"exterior" | "interior">(
"exterior"
);
const hasInteriorView = !!floor.images.interior;
const currentImage =
currentView === "interior" && floor.images.interior
? floor.images.interior
: floor.images.main;
// Split amenities into indoor and outdoor if counts are provided
const hasIndoorOutdoorSplit =
floor.amenitiesCount.indoor !== undefined &&
floor.amenitiesCount.outdoor !== undefined;
const indoorAmenities = hasIndoorOutdoorSplit
? floor.amenitiesList.slice(0, floor.amenitiesCount.indoor)
: [];
const outdoorAmenities = hasIndoorOutdoorSplit
? floor.amenitiesList.slice(floor.amenitiesCount.indoor)
: floor.amenitiesList;
return (
<div className="2xl:space-y-[2.222vw] space-y-8">
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">{floor.displayName}</p>
<Badge
variant="secondary"
text={`${floor.amenitiesCount.total} Amenities`}
/>
</div>
{hasIndoorOutdoorSplit && (
<div className="flex items-center 2xl:gap-[1.667vw] gap-6">
<AmenitiesBadge
count={floor.amenitiesCount.indoor!}
type="Indoor"
/>
<AmenitiesBadge
count={floor.amenitiesCount.outdoor!}
type="Outdoor"
/>
</div>
)}
<ViewToggleButtons
currentView={currentView}
onViewChange={setCurrentView}
hasInteriorView={hasInteriorView}
/>
<div className="bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-[1.111vw] p-4 relative">
<img
src="/images/floor-plans/compass.png"
className="absolute top-0 left-0 size-[7.222vw]"
alt=""
/>
<img src={currentImage} alt={floor.name} className="w-full" />
{floor.video && (
<Button
variant="cta"
className="absolute 2xl:top-[1.667vw] 2xl:right-[1.667vw] md:max-2xl:top-6 md:max-2xl:right-6 top-4 right-4"
onlyIcon
size="small"
onClick={() => setModal(<VideoModal src={floor.video!} />)}
>
<span className="2xl:size-[1.111vw] size-4">
<PlayIcon />
</span>
</Button>
)}
</div>
</div>
{hasIndoorOutdoorSplit && indoorAmenities.length > 0 && (
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Indoor Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 2xl:gap-[1.111vw] gap-4">
{indoorAmenities.map((amenity, index) => (
<AmentitiesBadge key={index} title={amenity.title} />
))}
</div>
</div>
)}
{hasIndoorOutdoorSplit && indoorAmenities.length > 0 && (
<hr className="border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
)}
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">
{hasIndoorOutdoorSplit ? "Outdoor Amenities" : "Amenities"}
</p>
<div
className={
hasIndoorOutdoorSplit
? "grid md:grid-cols-4 grid-cols-3 2xl:gap-x-[1.111vw] 2xl:gap-y-[1.667vw] gap-x-4 gap-y-6"
: "flex flex-wrap 2xl:gap-[0.556vw] gap-2"
}
>
{outdoorAmenities.map((amenity, index) => (
<AmentitiesBadge key={index} title={amenity.title} />
))}
</div>
</div>
{floor.images.content.length > 1 ? (
<AmentitiesContentSlider srcs={floor.images.content} />
) : (
floor.images.content.length === 1 && (
<img
src={floor.images.content[0]}
alt={floor.name}
className="w-full 2xl:rounded-[1.111vw] rounded-2xl select-none"
/>
)
)}
</div>
);
}
export default AmenitiesFloorView;
@@ -0,0 +1,46 @@
import { FloorData } from "../../types/Floor";
import { Unit } from "../../types/IUnit";
import { ComplexName } from "../../types/ComplexName";
import { FloorsData } from "../FloorSelect";
import ResidentialFloorView from "./ResidentialFloorView";
import AmenitiesFloorView from "./AmenitiesFloorView";
interface FloorPlanViewerProps {
floor: FloorData;
complexName: ComplexName;
unitsOnFloor?: Unit[];
floorsData?: FloorsData[];
selectedFloor: string;
onFloorSelect: (floor: string) => void;
}
function FloorPlanViewer({
floor,
complexName,
unitsOnFloor,
floorsData,
selectedFloor,
onFloorSelect,
}: FloorPlanViewerProps) {
if (floor.type === "residential") {
return (
<ResidentialFloorView
floor={floor}
complexName={complexName}
unitsOnFloor={unitsOnFloor}
floorsData={floorsData}
selectedFloor={selectedFloor}
onFloorSelect={onFloorSelect}
/>
);
}
if (floor.type === "amenities") {
return <AmenitiesFloorView floor={floor} />;
}
// This should never happen with proper TypeScript typing
return null;
}
export default FloorPlanViewer;
@@ -0,0 +1,283 @@
import { useState } from "react";
import { ResidentialFloorData } from "../../types/Floor";
import { Unit } from "../../types/IUnit";
import { ComplexName } from "../../types/ComplexName";
import { FloorsData } from "../FloorSelect";
import Badge from "../ui/Badge";
import Select from "../ui/Select";
import UnitTypeBadge from "../UnitTypeBadge";
import Button from "../ui/Button";
import { usePopupStore } from "../../stores/usePopupStore";
import { isMobile } from "react-device-detect";
// Import floor plan components
import FloorPlanMarasiDriveEast from "../FloorPlanMarasiDriveEast";
import FloorPlanMarasiDriveWestLower from "../FloorPlanMarasiDriveWestLower";
import FloorPlanMarasiDriveWestUpper from "../FloorPlanMarasiDriveWestUpper";
import FloorPlanDubaiMarina7_38 from "../FloorPlanDubaiMarina7_38";
import FloorPlanDubaiMarina7_38Comb from "../FloorPlanDubaiMarina7_38Comb";
import FloorPlanDubaiMarina39_40 from "../FloorPlanDubaiMarina39_40";
import FloorPlanDubaiMarina41_42 from "../FloorPlanDubaiMarina41_42";
interface ResidentialFloorViewProps {
floor: ResidentialFloorData;
complexName: ComplexName;
unitsOnFloor?: Unit[];
floorsData?: FloorsData[];
selectedFloor: string;
onFloorSelect: (floor: string) => void;
}
function ResidentialFloorView({
floor,
complexName,
unitsOnFloor,
floorsData,
selectedFloor,
onFloorSelect,
}: ResidentialFloorViewProps) {
const { setPopup, setPosition } = usePopupStore();
const [isCombinable, setIsCombinable] = useState(false);
// Marasi Drive specific logic
if (complexName === "marasi-drive") {
const floorNumber = floor.floorNumber;
const wing = floor.wing || selectedFloor.split(" ")[0];
const currentFloorData = floorsData?.find(
(item) => item.floor === floorNumber
);
const totalUnits =
(currentFloorData?.East?.totalUnits || 0) +
(currentFloorData?.West?.totalUnits || 0);
const wingData =
currentFloorData?.[selectedFloor.split(" ")[0] as "West" | "East"];
return (
<div className="2xl:space-y-[1.111vw] space-y-4" onScroll={() => setPopup(null)}>
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-4">
<p className="font-medium text-h4">{floorNumber} floor</p>
<div className="flex items-center 2xl:gap-[0.278vw] gap-1">
<Badge variant="secondary" text={`${totalUnits} Apartments`} />
</div>
</div>
<div className="2xl:space-y-[0.833vw] space-y-2">
<div className="flex items-center 2xl:gap-[1.111vw] gap-2">
<Select
options={
floorsData?.flatMap((item) => [
`East ${item.floor}`,
`West ${item.floor}`,
]) || []
}
defaultOption={selectedFloor?.toString() || ""}
onSelect={onFloorSelect}
className="2xl:w-[8.333vw] md:max-2xl:w-[120px] w-full"
maxOptionsCount={7}
/>
<div className="bg-[#E2E2DC] w-px 2xl:h-[1.667vw] h-1.5"></div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-2 max-md:hidden">
<UnitTypeBadge
type="Studio Flex"
count={wingData?.types["Studio Flex"] || 0}
/>
<UnitTypeBadge
type="Studio"
count={wingData?.types["Studio Squared"] || 0}
/>
<UnitTypeBadge
type="1 Bedroom"
count={wingData?.types["1 BR Squared"] || 0}
/>
<UnitTypeBadge
type="2 Bedroom"
count={wingData?.types["2 BR Squared"] || 0}
/>
</div>
</div>
</div>
<div
className="2xl:p-[4.444vw] p-4 bg-[#F3F3F2] 2xl:rounded-[0.833vw] rounded-lg"
onMouseMove={(e) =>
!isMobile && setPosition({ x: e.clientX, y: e.clientY })
}
>
{unitsOnFloor && wing === "East" && (
<FloorPlanMarasiDriveEast
unitsOnFloor={unitsOnFloor}
selectedFloor={floorNumber.toString()}
/>
)}
{wing === "West" && unitsOnFloor && (
<>
{floorNumber < 24 ? (
<FloorPlanMarasiDriveWestLower
unitsOnFloor={unitsOnFloor}
selectedFloor={floorNumber.toString()}
/>
) : (
<FloorPlanMarasiDriveWestUpper
unitsOnFloor={unitsOnFloor}
selectedFloor={floorNumber.toString()}
/>
)}
</>
)}
</div>
</div>
);
}
// Dubai Marina specific logic
if (complexName === "dubai-marina") {
const floorNumber = floor.floorNumber;
const currentFloorData = floorsData?.find(
(item) => item.floor === floorNumber
);
const isSpecialFloor =
selectedFloor === "39-40" || selectedFloor === "41-42";
return (
<div className="2xl:space-y-[1.111vw] space-y-4" onScroll={() => setPopup(null)}>
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-4">
<p className="font-medium text-h4">{selectedFloor} floor</p>
<div className="flex items-center 2xl:gap-[0.278vw] gap-1">
<Badge
variant="secondary"
text={`${currentFloorData?.others.totalUnits || 0} Apartments`}
/>
{!isSpecialFloor && <Badge variant="primary" text="Combinable" />}
</div>
</div>
<div className="2xl:space-y-[0.833vw] space-y-2">
<div className="flex items-center 2xl:gap-[1.111vw] gap-2">
<Select
options={
floorsData?.map((item) => {
if (item.floor === 39) {
return "39-40";
}
if (item.floor === 41) {
return "41-42";
}
return item.floor.toString();
}) || []
}
defaultOption={selectedFloor?.toString() || ""}
onSelect={onFloorSelect}
className="2xl:w-[8.333vw] md:max-xl:w-[120px] w-full"
maxOptionsCount={7}
/>
<div className="bg-[#E2E2DC] w-px 2xl:h-[1.667vw] h-1.5"></div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-2 max-md:hidden">
<UnitTypeBadge
type="Studio"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!)
)?.others.types["Studio2"] || 0
}
/>
<UnitTypeBadge
type="1 Bedroom"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!)
)?.others.types["One Bedroom2"] || 0
}
/>
<UnitTypeBadge
type="1 Bedroom Loft"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!)
)?.others.types["One Bedroom Loft"] || 0
}
/>
<UnitTypeBadge
type="2 Bedroom Loft"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!)
)?.others.types["Two Bedroom Loft"] || 0
}
/>
</div>
</div>
{!isSpecialFloor && (
<div className="flex gap-2 justify-center items-center">
<Button
variant={!isCombinable ? "cta" : "primary"}
onClick={() => setIsCombinable(false)}
>
Standard
</Button>
<Button
variant={isCombinable ? "cta" : "primary"}
onClick={() => setIsCombinable(true)}
>
Combinable
</Button>
</div>
)}
<div
className="2xl:py-[1.667vw] 2xl:px-[1.111vw] max-2xl:p-4 bg-[#F3F3F2] 2xl:rounded-[0.833vw] rounded-lg relative 2xl:space-y-[2.222vw] space-y-8"
onMouseMove={(e) =>
!isMobile && setPosition({ x: e.clientX, y: e.clientY })
}
>
{selectedFloor && unitsOnFloor && (
<>
{+selectedFloor >= 7 && +selectedFloor < 39 && (
<>
{!isCombinable ? (
<FloorPlanDubaiMarina7_38
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
/>
) : (
<FloorPlanDubaiMarina7_38Comb
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
/>
)}
</>
)}
{selectedFloor === "39-40" && (
<FloorPlanDubaiMarina39_40
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={null}
/>
)}
{selectedFloor === "41-42" && (
<FloorPlanDubaiMarina41_42
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={null}
/>
)}
</>
)}
</div>
</div>
</div>
);
}
// Default fallback
return <div>Unsupported complex: {complexName}</div>;
}
export default ResidentialFloorView;
@@ -0,0 +1,47 @@
import clsx from "clsx";
import Button from "../ui/Button";
interface ViewToggleButtonsProps {
currentView: "exterior" | "interior";
onViewChange: (view: "exterior" | "interior") => void;
hasInteriorView: boolean;
}
function ViewToggleButtons({
currentView,
onViewChange,
hasInteriorView,
}: ViewToggleButtonsProps) {
if (!hasInteriorView) {
return null;
}
return (
<div className="flex 2xl:gap-[0.556vw] gap-2">
<Button
variant={currentView === "exterior" ? "primary" : "secondary"}
size="small"
onClick={() => onViewChange("exterior")}
className={clsx(
"2xl:px-[1.111vw] 2xl:py-[0.556vw] px-4 py-2",
currentView === "exterior" && "!bg-[#0D1922] !text-white"
)}
>
Exterior View
</Button>
<Button
variant={currentView === "interior" ? "primary" : "secondary"}
size="small"
onClick={() => onViewChange("interior")}
className={clsx(
"2xl:px-[1.111vw] 2xl:py-[0.556vw] px-4 py-2",
currentView === "interior" && "!bg-[#0D1922] !text-white"
)}
>
Interior View
</Button>
</div>
);
}
export default ViewToggleButtons;
@@ -13,7 +13,7 @@ function GroundDubaiMarina() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Ground Level</p>
<Badge variant="secondary" text="14 Amenties" />
<Badge variant="secondary" text="14 Amenities" />
</div>
<div className="bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-[1.111vw] p-4 relative">
<img
@@ -15,7 +15,7 @@ function PodiumDubaiMarina() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Podium</p>
<Badge variant="secondary" text="14 Amenties" />
<Badge variant="secondary" text="14 Amenities" />
</div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-6">
<AmenitiesBadge count={3} type="Indoor" />
@@ -30,7 +30,7 @@ function RooftopDubaiMarina() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Sky 44 - Rooftop</p>
<Badge variant="secondary" text="14 Amenties" />
<Badge variant="secondary" text="14 Amenities" />
</div>
<div className="bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-[1.111vw] p-4 relative">
<img
@@ -21,7 +21,7 @@ function GroundMarasiDrive() {
<div className="space-y-[1.667vw]">
<div className="space-y-[0.556vw] border-b border-[#E2E2DC] pb-[1.667vw]">
<p className="text-h4 font-medium">Ground Level</p>
<Badge variant="secondary" text="7 Amenties" />
<Badge variant="secondary" text="7 Amenities" />
</div>
<div className="bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-[1.111vw] p-4 relative">
<img
@@ -52,7 +52,7 @@ function GroundMarasiDrive() {
</div>
</div>
<div className="space-y-[1.667vw]">
<p className="font-medium text-h4">Amenties</p>
<p className="font-medium text-h4">Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 gap-[1.111vw]">
<AmentitiesBadge icon={<RoveCafe />} title="Rove Café" />
<AmentitiesBadge icon={<LoungingSpaceIcon />} title="Lobby Lounge" />
@@ -41,7 +41,7 @@ function PodiumMarasiDrive() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Podium Level</p>
<Badge variant="secondary" text="27 Amenties" />
<Badge variant="secondary" text="27 Amenities" />
</div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-6">
<AmenitiesBadge count={13} type="Indoor" />
@@ -76,7 +76,7 @@ function PodiumMarasiDrive() {
</div>
</div>
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Indoor Amenties</p>
<p className="font-medium text-h4">Indoor Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 2xl:gap-[1.111vw] gap-4">
<AmentitiesBadge icon={<LoungeIcon />} title="Indoor Lounge" />
<AmentitiesBadge icon={<MonkeyBarsIcon />} title="Monkey Bars" />
@@ -110,7 +110,7 @@ function PodiumMarasiDrive() {
</div>
<hr className="border-[#E2E2DC] h-[0.069vw]" />
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Outdoor Amenties</p>
<p className="font-medium text-h4">Outdoor Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 2xl:gap-x-[1.111vw] 2xl:gap-y-[1.667vw] gap-x-4 gap-y-6">
<AmentitiesBadge
icon={<UrbanBeachPoolIcon />}
@@ -23,7 +23,7 @@ function RooftopMarasiDrive() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Rooftop</p>
<Badge variant="secondary" text="10 Amenties" />
<Badge variant="secondary" text="10 Amenities" />
</div>
<div className="bg-[#F3F3F2] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-[1.111vw] p-4 relative">
<img
@@ -54,7 +54,7 @@ function RooftopMarasiDrive() {
</div>
</div>
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Amenties</p>
<p className="font-medium text-h4">Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 2xl:gap-[1.111vw] gap-4">
<AmentitiesBadge icon={<StargazingIcon />} title="Stargazing Point" />
<AmentitiesBadge icon={<BBQTerraceIcon />} title="BBQ Terrace" />
@@ -32,7 +32,7 @@ function SkyGardenMarasiDrive() {
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-6">
<p className="text-h4 font-medium">Sky Garden</p>
<Badge variant="secondary" text="15 Amenties" />
<Badge variant="secondary" text="15 Amenities" />
</div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-4">
<AmenitiesBadge count={3} type="Indoor" />
@@ -67,7 +67,7 @@ function SkyGardenMarasiDrive() {
</div>
</div>
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Indoor Amenties</p>
<p className="font-medium text-h4">Indoor Amenities</p>
<div className="grid md:grid-cols-4 grid-cols-3 2xl:gap-[1.111vw] gap-4">
<AmentitiesBadge icon={<PoolIcon />} title="Indoor Lap Pool" />
<AmentitiesBadge icon={<WellnessIcon />} title="Wellness Features" />
@@ -76,7 +76,7 @@ function SkyGardenMarasiDrive() {
</div>
<hr className="border-[#E2E2DC] h-[0.069vw]" />
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Outdoor Amenties</p>
<p className="font-medium text-h4">Outdoor Amenities</p>
<div className="grid grid-cols-3 2xl:gap-x-[1.111vw] 2xl:gap-y-[1.667vw] gap-x-4 gap-y-6">
<AmentitiesBadge icon={<PingPongIcon />} title="Padel Pong" />
<AmentitiesBadge icon={<SunLoungeIcon />} title="Sun Lounging Deck" />
+1 -1
View File
@@ -37,7 +37,7 @@ export default function BrochuresDropdown() {
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)]"
className="max-2xl:hidden p-[1.111vw] flex gap-[1.111vw] justify-stretch items-stretch fixed top-[calc(3.889vw+20px)] left-[21%] rounded-[1.111vw] bg-white shadow-[0_2px_8px_rgba(0,0,0,0.15)]"
>
{projectBrochures.map((project, index) => (
<ProjectBrochuresList
@@ -16,13 +16,15 @@ export default function ProjectBrochuresList({
const isMobile = variant === "mobile";
return (
<div className="flex-1 space-y-4">
<p className="font-medium text-s">{projectTitle}</p>
<div
className={clsx(
"space-y-3",
isMobile ? "flex-1" : "2xl:w-[15.278vw] w-[220px]"
)}
>
<p className="font-medium whitespace-nowrap text-h5">{projectTitle}</p>
<div
className={clsx(
"flex flex-col",
isMobile ? "gap-2" : "gap-[0.556vw]"
)}
className={clsx("flex flex-col", isMobile ? "gap-2" : "gap-[0.556vw]")}
>
{brochures.map((brochure, index) => (
<BrochureButton
+2 -2
View File
@@ -22,10 +22,10 @@ export default function BrochureButton({
<Button
variant="primary"
size="large"
className="w-full !justify-between group"
className="w-full !justify-between !2xl:px-[1.389vw] !px-5 !2xl:py-[0.972vw] !py-3.5 group"
onClick={handleDownload}
>
<span className="text-nowrap text-caption-m duration-300">{title}</span>
<span className="duration-300 text-nowrap text-btn-m">{title}</span>
<span className="2xl:size-[1.389vw] size-5 opacity-70 group-hover:opacity-100 transition-opacity duration-300">
<DownloadIcon />
</span>
+2 -2
View File
@@ -8,8 +8,8 @@ export const dubaiMarinaDescriptionBadges = [
export const dubaiMarinaFeatures = [
{
name: "Life-enhancing Amenties",
image: "/images/about-complex/dubai-marina/amenties.jpg",
name: "Life-enhancing Amenities",
image: "/images/about-complex/dubai-marina/amenities.jpg",
},
{
name: "Community",
+2 -2
View File
@@ -14,8 +14,8 @@ export const marasiDriveFeatures = [
image: "/images/about-complex/marasi-drive/location.jpg",
},
{
name: "Fully Loaded Amenties",
image: "/images/about-complex/marasi-drive/amenties.jpg",
name: "Fully Loaded Amenities",
image: "/images/about-complex/marasi-drive/amenities.jpg",
},
{
name: "Rove-Inspired Design",
+11 -38
View File
@@ -24,14 +24,6 @@ export const projectBrochures: ProjectBrochures[] = [
title: "Technical Brochure",
link: "/files/brochures/marasi-drive/Technical Brochure.pdf",
},
{
title: "Factsheet",
link: "/files/brochures/marasi-drive/Factsheet.pdf",
},
{
title: "Reasons to buy",
link: "/files/brochures/marasi-drive/Reasons to buy.pdf",
},
],
},
{
@@ -46,24 +38,8 @@ export const projectBrochures: ProjectBrochures[] = [
link: "/files/brochures/downtown/Amenities Brochure.pdf",
},
{
title: "Unit Plans",
link: "/files/brochures/downtown/Unit Plan.pdf",
},
{
title: "Typical Floor plan 2-13",
link: "/files/brochures/downtown/Typical Floor plan 2-13.pdf",
},
{
title: "Typical Floor plan 14-19",
link: "/files/brochures/downtown/Typical Floor plan 14-19.pdf",
},
{
title: "Factsheet",
link: "/files/brochures/downtown/Factsheet.pdf",
},
{
title: "Reasons to buy",
link: "/files/brochures/downtown/Reasons to buy.pdf",
title: "Technical Brochure",
link: "/files/brochures/downtown/Technical Brochure.pdf",
},
],
},
@@ -82,21 +58,18 @@ export const projectBrochures: ProjectBrochures[] = [
title: "Technical Brochure",
link: "/files/brochures/dubai-marina/Technical Brochure.pdf",
},
],
},
{
projectTitle: "Rove Home HQ",
brochures: [
{
title: "Factsheet",
link: "/files/brochures/dubai-marina/Factsheet.pdf",
title: "Main Brochure",
link: "/files/brochures/hq/Main Brochure.pdf",
},
{
title: "RHDM Arabic",
link: "/files/brochures/dubai-marina/RHDM Arabic.pdf",
},
{
title: "RHDM Turkish",
link: "/files/brochures/dubai-marina/RHDM Turkish.pdf",
},
{
title: "RHDM Russian",
link: "/files/brochures/dubai-marina/RHDM Russian.pdf",
title: "Technical Brochure",
link: "/files/brochures/hq/Technical Brochure.pdf",
},
],
},
+156
View File
@@ -0,0 +1,156 @@
import { FloorData } from "../../types/Floor";
export const dubaiMarinaFloors: FloorData[] = [
// Ground Level
{
id: "ground-level",
name: "Ground Level",
displayName: "Ground Level",
type: "amenities",
amenitiesCount: {
total: 14,
},
amenitiesList: [
{ icon: "text", title: "Residential Entrance" },
{ icon: "text", title: "Multifunctional Feature Staircase" },
{ icon: "text", title: "Lobby Lounge & Concierge" },
{ icon: "text", title: "Outdoor Landscape Seating Area" },
{ icon: "text", title: "Lift Lobby" },
{ icon: "text", title: "Rove Cafe & Energize Bar" },
{ icon: "text", title: "Organic Smart Gardens & Seating" },
{ icon: "text", title: "Co-working Area " },
{ icon: "text", title: "24x7 Convenience Store" },
{ icon: "text", title: "WCs" },
{ icon: "text", title: "Visitor Parking" },
{ icon: "text", title: "EV Charging Stations" },
{ icon: "text", title: "Bicycle/Scooter Rental & Storage" },
{ icon: "text", title: "Drop-off Area" },
],
images: {
main: "/images/floor-plans/dubai-marina/ground.png",
content: ["/images/floor-plans/dubai-marina/ground/content.jpg"],
},
video: "/videos/dubai-marina/GroundDubaiMarina.mp4",
},
// Podium Level
{
id: "podium-level",
name: "Podium Level",
displayName: "Podium Level",
type: "amenities",
amenitiesCount: {
total: 14,
indoor: 3,
outdoor: 12,
},
amenitiesList: [
// Indoor
{ icon: "text", title: "Multipurpose Hall" },
{ icon: "text", title: "Gaming Lounge" },
{ icon: "text", title: "State-of-the-art Gym" },
{ icon: "text", title: "7m Climbing Wall" },
{ icon: "text", title: "Changing Rooms & Lockers" },
{ icon: "text", title: "Hydration Station" },
{ icon: "text", title: "Boutique Fitness Studio - Crank" },
{ icon: "text", title: "Rentable Guest Rooms" },
// Outdoor
{ icon: "text", title: "Semi-Olympic Leisure Pool" },
{ icon: "text", title: "Outdoor Cinema & Amphitheatre" },
{ icon: "text", title: "Water Feature Wall" },
{ icon: "text", title: "Multipurpose Fitness Pool" },
{ icon: "text", title: "Communal Gardens" },
{ icon: "text", title: "BBQ & Social Zone" },
{ icon: "text", title: "Popsicle Cart" },
{ icon: "text", title: "Gaming Lounge - Terrace" },
{ icon: "text", title: "Zen Library" },
{ icon: "text", title: "Co-working Area" },
{ icon: "text", title: "Multipurpose Hall with Terrace" },
{ icon: "text", title: "Marina View Chill Zone" },
{ icon: "text", title: "Outdoor Gym" },
],
images: {
main: "/images/floor-plans/dubai-marina/podium.png",
content: [
"/images/floor-plans/dubai-marina/podium/content1.jpg",
"/images/floor-plans/dubai-marina/podium/content2.jpg",
"/images/floor-plans/dubai-marina/podium/content3.jpg",
],
},
video: "/videos/dubai-marina/PodiumDubaiMarina.mp4",
},
// Residential floors 7-20
...Array.from({ length: 14 }, (_, i) => {
const floor = i + 7;
return {
id: `floor-${floor}`,
name: `${floor}`,
displayName: `${floor}`,
type: "residential" as const,
floorNumber: floor,
};
}),
// Residential floors 22-38
...Array.from({ length: 17 }, (_, i) => {
const floor = i + 22;
return {
id: `floor-${floor}`,
name: `${floor}`,
displayName: `${floor}`,
type: "residential" as const,
floorNumber: floor,
};
}),
// Residential floors 39-40 (special layout)
{
id: "floor-39-40",
name: "39-40",
displayName: "39-40",
type: "residential",
floorNumber: 39,
},
// Residential floors 41-42 (special layout)
{
id: "floor-41-42",
name: "41-42",
displayName: "41-42",
type: "residential",
floorNumber: 41,
},
// Rooftop (Sky 44)
{
id: "rooftop",
name: "Rooftop",
displayName: "Sky 44 - Rooftop",
type: "amenities",
amenitiesCount: {
total: 14,
},
amenitiesList: [
{ icon: "text", title: "Sky Viewing Lounges" },
{ icon: "text", title: "Convertible Indoor Infinity Pool" },
{ icon: "text", title: "Marina View Amphitheatre" },
{ icon: "text", title: "Ultra Shield Oxygen Pod" },
{ icon: "text", title: "Aroma Steam Pod" },
{ icon: "text", title: "Reflexology Pool" },
{ icon: "text", title: "Cold Bucket Experience Shower Pod" },
{ icon: "text", title: "Experience Shower Pod" },
{ icon: "text", title: "Cold Plunge Pool" },
{ icon: "text", title: "Salt Steam Pod" },
{ icon: "text", title: "Finnish Sauna Pod" },
{ icon: "text", title: "Water Feature Wall" },
{ icon: "text", title: "Vitality Pool" },
{ icon: "text", title: "Changing Rooms and Lockers" },
],
images: {
main: "/images/floor-plans/dubai-marina/rooftop.png",
content: ["/images/floor-plans/dubai-marina/rooftop/content.jpg"],
},
video: "/videos/dubai-marina/SkyDubaiMarina.mp4",
},
];
+207
View File
@@ -0,0 +1,207 @@
import { FloorData } from "../../types/Floor";
export const marasiDriveFloors: FloorData[] = [
// Ground Level
{
id: "ground-level",
name: "Ground Level",
displayName: "Ground Level",
type: "amenities",
amenitiesCount: {
total: 7,
},
amenitiesList: [
{ icon: "RoveCafe", title: "Rove Café" },
{ icon: "LoungingSpaceIcon", title: "Lobby Lounge" },
{ icon: "CoworkingIcon", title: "Coworking Space" },
{ icon: "LushLandscapeIcon", title: "Outdoor Terrace" },
{ icon: "PrivateMeetingRoomsIcon", title: "Private Meeting Rooms" },
{ icon: "ConvenienceIcon", title: "Convenience Store" },
{ icon: "SoundproofMeetingPodsIcon", title: "Soundproof Meeting Pods" },
],
images: {
main: "/images/floor-plans/marasi-drive/ground.png",
content: [
"/images/floor-plans/marasi-drive/ground/content1.jpg",
"/images/floor-plans/marasi-drive/ground/content2.jpg",
"/images/floor-plans/marasi-drive/ground/content3.jpg",
"/images/floor-plans/marasi-drive/ground/content4.jpg",
"/images/floor-plans/marasi-drive/ground/content5.jpg",
"/images/floor-plans/marasi-drive/ground/content6.jpg",
],
},
video: "/videos/marasi-drive/GroundMarasiDrive.mp4",
},
// Podium Level
{
id: "podium-level",
name: "Podium Level",
displayName: "Podium Level",
type: "amenities",
amenitiesCount: {
total: 27,
indoor: 13,
outdoor: 14,
},
amenitiesList: [
// Indoor
{ icon: "LoungeIcon", title: "Indoor Lounge" },
{ icon: "MonkeyBarsIcon", title: "Monkey Bars" },
{ icon: "KaraokeIcon", title: "Karaoke Room" },
{ icon: "ArcadeGameIcon", title: "Arcade Games" },
{ icon: "ClimbingWallIcon", title: "Climbing Wall" },
{ icon: "PlaystationIcon", title: "Playstation Deck" },
{ icon: "FullyEquippedGymIcon", title: "Fully Equipped Gym" },
{ icon: "ChangingRoomIcon", title: "Changing Rooms" },
{ icon: "HammockMovieLoungeIcon", title: "Hammock Movie Lounge" },
{ icon: "GuestRooms", title: "Guest Rooms" },
{ icon: "MultiballInteractiveGamingIcon", title: "Multi Ball Interactive Gaming" },
{ icon: "MultiPurposeRoomWithKitchenIcon", title: "Multi-purpose Room for Kitchen" },
{ icon: "GamingLoungeIcon", title: "Gaming Lounge" },
// Outdoor
{ icon: "UrbanBeachPoolIcon", title: "Urban Beach Pool" },
{ icon: "JacuzziIcon", title: "Jacuzzi" },
{ icon: "YogaLoungeIcon", title: "Yoga Lounge" },
{ icon: "SunLoungeIcon", title: "Sun Lounging Pool" },
{ icon: "CascadingLeisurePoolIcon", title: "Cascading Leisure Pool" },
{ icon: "AquaCyclingIcon", title: "AquaCycling" },
{ icon: "OpenAirGymIcon", title: "Open-Air Gym" },
{ icon: "RoveBeverageTruckIcon", title: "Rove Beverage Truck" },
{ icon: "CabanasWithDaybeds", title: "Cabanas with Daybeds" },
{ icon: "IntegratedLapPoolIcon", title: "Integrated Lap Pool" },
{ icon: "SunkenGardensIcon", title: "Sunken Gardens" },
{ icon: "MultiPurposeRoomWithKitchenIcon", title: "Outdoor Multi-Purpose Terrace" },
{ icon: "GamingTerraceIcon", title: "Outdoor Gaming Terrace" },
{ icon: "CoworkingIcon", title: "Outdoor Coworking Space" },
],
images: {
main: "/images/floor-plans/marasi-drive/podium.png",
content: [
"/images/floor-plans/marasi-drive/podium/content1.jpg",
"/images/floor-plans/marasi-drive/podium/content2.jpg",
"/images/floor-plans/marasi-drive/podium/content3.jpg",
"/images/floor-plans/marasi-drive/podium/content4.jpg",
"/images/floor-plans/marasi-drive/podium/content5.jpg",
"/images/floor-plans/marasi-drive/podium/content6.jpg",
"/images/floor-plans/marasi-drive/podium/content7.jpg",
"/images/floor-plans/marasi-drive/podium/content8.jpg",
"/images/floor-plans/marasi-drive/podium/content9.jpg",
"/images/floor-plans/marasi-drive/podium/content10.jpg",
"/images/floor-plans/marasi-drive/podium/content11.jpg",
],
},
video: "/videos/marasi-drive/PodiumMarasiDrive.mp4",
},
// Residential floors 5-21 East Wing
...Array.from({ length: 17 }, (_, i) => {
const floor = i + 5;
return {
id: `east-${floor}`,
name: `East ${floor}`,
displayName: `East Wing ${floor}`,
type: "residential" as const,
floorNumber: floor,
wing: "East" as const,
};
}),
// Residential floors 5-21 West Wing
...Array.from({ length: 17 }, (_, i) => {
const floor = i + 5;
return {
id: `west-${floor}`,
name: `West ${floor}`,
displayName: `West Wing ${floor}`,
type: "residential" as const,
floorNumber: floor,
wing: "West" as const,
};
}),
// Residential floors 24-31 West Wing (upper)
...Array.from({ length: 8 }, (_, i) => {
const floor = i + 24;
return {
id: `west-${floor}`,
name: `West ${floor}`,
displayName: `West Wing ${floor}`,
type: "residential" as const,
floorNumber: floor,
wing: "West" as const,
};
}),
// Sky Garden
{
id: "sky-garden",
name: "Sky Garden",
displayName: "Sky Garden",
type: "amenities",
amenitiesCount: {
total: 15,
indoor: 3,
outdoor: 12,
},
amenitiesList: [
// Indoor
{ icon: "PoolIcon", title: "Indoor Lap Pool" },
{ icon: "WellnessIcon", title: "Wellness Features" },
{ icon: "ChangingRoomIcon", title: "Changing Rooms" },
// Outdoor
{ icon: "PingPongIcon", title: "Padel Pong" },
{ icon: "SunLoungeIcon", title: "Sun Lounging Deck" },
{ icon: "CinemaIcon", title: "Outdoor Cinema" },
{ icon: "BoulderingWallIcon", title: "Bouldering Wall" },
{ icon: "PingPongInTubeIcon", title: "Ping Pong in a Tube" },
{ icon: "AmphitheatreIcon", title: "Amphitheatre" },
{ icon: "CommunalDiningTablesIcon", title: "Communal Dining Tables" },
{ icon: "SuspendedLoungingNetsIcon", title: "Suspended Lounging Nets " },
{ icon: "LushLandscapeIcon", title: "Lush Landscape" },
{ icon: "RunningWheelIcon", title: "Running Wheel" },
{ icon: "ChessIcon", title: "Chess Tables" },
{ icon: "ClimbingWallIcon", title: "Climbing Wall" },
{ icon: "CoworkingIcon", title: "Outdoor Coworking Space" },
{ icon: "MultiPurposeIcon", title: "Multi-purpose Court" },
],
images: {
main: "/images/floor-plans/marasi-drive/sky-garden.png",
content: [
"/images/floor-plans/marasi-drive/skygarden/content1.jpg",
"/images/floor-plans/marasi-drive/skygarden/content2.jpg",
"/images/floor-plans/marasi-drive/skygarden/content3.jpg",
"/images/floor-plans/marasi-drive/skygarden/content4.jpg",
],
},
video: "/videos/marasi-drive/SkyGardenMarasiDrive.mp4",
},
// Rooftop
{
id: "rooftop",
name: "Rooftop",
displayName: "Rooftop",
type: "amenities",
amenitiesCount: {
total: 10,
},
amenitiesList: [
{ icon: "StargazingIcon", title: "Stargazing Point" },
{ icon: "BBQTerraceIcon", title: "BBQ Terrace" },
{ icon: "OutdoorKitchenIcon", title: "Outdoor Kitchen" },
{ icon: "CabanasWithDaybeds", title: "Cabanas with Daybeds" },
{ icon: "ViewingDeckWithWingsIcon", title: "Viewing Deck with Wings" },
{ icon: "LoungingSpaceIcon", title: "Lounging Space" },
{ icon: "SunkenSeatingIcon", title: "Sunken Seating" },
{ icon: "FirePitIcon", title: "Firepit" },
{ icon: "RooftopGardenIcon", title: "Rooftop Garden" },
{ icon: "CommunalDiningTablesRoundedIcon", title: "Communal Dining Tables" },
],
images: {
main: "/images/floor-plans/marasi-drive/rooftop.png",
content: ["/images/floor-plans/marasi-drive/rooftop/content.jpg"],
},
video: "/videos/marasi-drive/RooftopMarasiDrive.mp4",
},
];
+4 -4
View File
@@ -572,7 +572,7 @@ export const projects: Project[] = [
tourAvailable: true,
},
],
amentiesFloors: [
amenitiesFloors: [
{
title: "Rooftop",
total: 10,
@@ -1971,7 +1971,7 @@ export const projects: Project[] = [
tourAvailable: true,
},
],
amentiesFloors: [
amenitiesFloors: [
{
title: "Rooftop",
total: 14,
@@ -1994,6 +1994,6 @@ export const projects: Project[] = [
img: "/images/search/rove_home_hq.png",
buildingType: "commercial",
types: [],
amentiesFloors: [],
}
amenitiesFloors: [],
},
];
+31 -305
View File
@@ -2,33 +2,16 @@ import FloorSelect, { FloorsData } from "../components/FloorSelect";
import { useParams } from "react-router";
import FloorSidebar from "../components/FloorSidebar";
import { useEffect, useState } from "react";
import Select from "../components/ui/Select";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
import UnitTypeBadge from "../components/UnitTypeBadge";
import FloorPlanMarasiDriveEast from "../components/FloorPlanMarasiDriveEast";
import RooftopMarasiDrive from "../components/floor-plans/marasi-drive/RooftopMarasiDrive";
import GroundMarasiDrive from "../components/floor-plans/marasi-drive/GroundMarasiDrive";
import PodiumMarasiDrive from "../components/floor-plans/marasi-drive/PodiumMarasiDrive";
import SkyGardenMarasiDrive from "../components/floor-plans/marasi-drive/SkyGardenMarasiDrive";
import Badge from "../components/ui/Badge";
import RooftopDubaiMarina from "../components/floor-plans/dubai-marina/RooftopDubaiMarina";
import GroundDubaiMarina from "../components/floor-plans/dubai-marina/GroundDubaiMarina";
import PodiumDubaiMarina from "../components/floor-plans/dubai-marina/PodiumDubaiMarina";
import FloorPlanMarasiDriveWestLower from "../components/FloorPlanMarasiDriveWestLower";
import FloorPlanMarasiDriveWestUpper from "../components/FloorPlanMarasiDriveWestUpper";
import { SPECIAL_FLOORS } from "../constants/floors";
import { Unit } from "../types/IUnit";
import slugToComplexName from "../utils/slugToComplexName";
import { usePopupStore } from "../stores/usePopupStore";
import { isMobile } from "react-device-detect";
import FloorPlanDubaiMarina41_42 from "../components/FloorPlanDubaiMarina41_42";
import FloorPlanDubaiMarina39_40 from "../components/FloorPlanDubaiMarina39_40";
import FloorPlanDubaiMarina7_38Comb from "../components/FloorPlanDubaiMarina7_38Comb";
import FloorPlanDubaiMarina7_38 from "../components/FloorPlanDubaiMarina7_38";
import Button from "../components/ui/Button";
import { ComplexName } from "../types/ComplexName";
import FloorPlanViewer from "../components/floor-plans/FloorPlanViewer";
import { marasiDriveFloors } from "../data/floors/marasi-drive";
import { dubaiMarinaFloors } from "../data/floors/dubai-marina";
function FloorsPage() {
const [selectedFloor, setSelectedFloor] = useState<string | null>(null);
@@ -66,12 +49,22 @@ function FloorsPage() {
.json<Unit[]>(),
});
const { setPosition, setPopup } = usePopupStore();
const [isCombinable, setIsCombinable] = useState(false);
// Get floor data based on complex
const allFloors = useMemo(() => {
if (complexName === "marasi-drive") {
return marasiDriveFloors;
}
if (complexName === "dubai-marina") {
return dubaiMarinaFloors;
}
return [];
}, [complexName]);
useEffect(() => {
setIsCombinable(false);
}, [selectedFloor]);
// Find current floor
const currentFloor = useMemo(() => {
if (!selectedFloor) return null;
return allFloors.find((floor) => floor.name === selectedFloor);
}, [selectedFloor, allFloors]);
return (
<div className="relative 2xl:h-[calc(100dvh-4.444vw)]md:max-2xl:h-[calc(100vh-64px)]h-[calc(100vh-56px)] h-full overflow-hidden">
@@ -84,287 +77,20 @@ function FloorsPage() {
isOpen={!!selectedFloor}
onClose={() => setSelectedFloor(null)}
>
{complexName === "dubai-marina" && (
<>
{selectedFloor === "Rooftop" && <RooftopDubaiMarina />}
{selectedFloor === "Ground Level" && <GroundDubaiMarina />}
{selectedFloor === "Podium Level" && <PodiumDubaiMarina />}
{!!parseInt(selectedFloor!) && (
<div
className="2xl:space-y-[1.111vw] space-y-4"
onScroll={() => setPopup(null)}
>
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-4">
<p className="font-medium text-h4">{selectedFloor} floor</p>
<div className="flex items-center 2xl:gap-[0.278vw] gap-1">
<Badge
variant="secondary"
text={`${
floorsData?.find(
(item) => item.floor === parseInt(selectedFloor!),
)?.others.totalUnits || 0
} Apartments`}
/>
<Badge variant="primary" text="Combinable" />
</div>
</div>
<div className="2xl:space-y-[0.833vw] space-y-2">
<div className="flex items-center 2xl:gap-[1.111vw] gap-2">
<Select
options={
floorsData?.map((item) => {
if (item.floor === 39) {
return "39-40";
}
if (item.floor === 41) {
return "41-42";
}
return item.floor.toString();
}) || []
}
defaultOption={selectedFloor?.toString() || ""}
onSelect={setSelectedFloor}
className="2xl:w-[8.333vw] md:max-xl:w-[120px] w-full"
maxOptionsCount={7}
/>
<div className="bg-[#E2E2DC] w-px 2xl:h-[1.667vw] h-1.5"></div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-2 max-md:hidden">
<UnitTypeBadge
type="Studio"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!),
)?.others.types["Studio2"] || 0
}
/>
<UnitTypeBadge
type="1 Bedroom"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!),
)?.others.types["One Bedroom2"] || 0
}
/>
<UnitTypeBadge
type="1 Bedroom Loft"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!),
)?.others.types["One Bedroom Loft"] || 0
}
/>
<UnitTypeBadge
type="2 Bedroom Loft"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor!.split(" ").at(-1)!),
)?.others.types["Two Bedroom Loft"] || 0
}
/>
</div>
</div>
{selectedFloor !== "39-40" && selectedFloor !== "41-42" && (
<div className="flex gap-2 justify-center items-center">
<Button
variant={!isCombinable ? "cta" : "primary"}
// className="w-full"
onClick={() => setIsCombinable(false)}
>
Standard
</Button>
<Button
variant={isCombinable ? "cta" : "primary"}
// className="w-full"
onClick={() => setIsCombinable(true)}
>
Combinable
</Button>
</div>
)}
<div
className="2xl:py-[1.667vw] 2xl:px-[1.111vw] max-2xl:p-4 bg-[#F3F3F2] 2xl:rounded-[0.833vw] rounded-lg relative 2xl:space-y-[2.222vw] space-y-8"
onMouseMove={(e) =>
!isMobile && setPosition({ x: e.clientX, y: e.clientY })
}
>
{selectedFloor && unitsOnFloor && (
<>
{+selectedFloor >= 7 && +selectedFloor < 39 && (
<>
{!isCombinable ? (
<FloorPlanDubaiMarina7_38
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
/>
) : (
<FloorPlanDubaiMarina7_38Comb
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
/>
)}
</>
)}
{selectedFloor === "39-40" && (
<FloorPlanDubaiMarina39_40
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={null}
/>
)}
{selectedFloor === "41-42" && (
<FloorPlanDubaiMarina41_42
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={null}
/>
)}
</>
)}
</div>
</div>
</div>
)}
</>
{currentFloor && (
<FloorPlanViewer
floor={currentFloor}
complexName={complexName!}
unitsOnFloor={unitsOnFloor}
floorsData={floorsData}
selectedFloor={selectedFloor!}
onFloorSelect={setSelectedFloor}
/>
)}
{complexName === "marasi-drive" && (
<>
{selectedFloor === "Rooftop" && <RooftopMarasiDrive />}
{selectedFloor === "Ground Level" && <GroundMarasiDrive />}
{selectedFloor === "Podium Level" && <PodiumMarasiDrive />}
{selectedFloor === "Sky Garden" && <SkyGardenMarasiDrive />}
{selectedFloor && !!parseInt(selectedFloor.split(" ").at(-1)!) && (
<div
className="2xl:space-y-[1.111vw] space-y-4"
onScroll={() => setPopup(null)}
>
<div className="2xl:space-y-[0.556vw] space-y-2 border-b border-[#E2E2DC] 2xl:pb-[1.667vw] pb-4">
<p className="font-medium text-h4">
{selectedFloor.split(" ").at(-1)} floor
</p>
<div className="flex items-center 2xl:gap-[0.278vw] gap-1">
<Badge
variant="secondary"
text={`${
(floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.East?.totalUnits || 0) +
(floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.West?.totalUnits || 0)
} Apartments`}
/>
{/* <Badge variant="primary" text="Combinable" /> */}
</div>
</div>
<div className="2xl:space-y-[0.833vw] space-y-2">
<div className="flex items-center 2xl:gap-[1.111vw] gap-2">
<Select
options={
floorsData?.flatMap((item) => [
`East ${item.floor}`,
`West ${item.floor}`,
]) || []
}
defaultOption={selectedFloor?.toString() || ""}
onSelect={setSelectedFloor}
className="2xl:w-[8.333vw] md:max-2xl:w-[120px] w-full"
maxOptionsCount={7}
/>
<div className="bg-[#E2E2DC] w-px 2xl:h-[1.667vw] h-1.5"></div>
<div className="flex items-center 2xl:gap-[1.667vw] gap-2 max-md:hidden">
<UnitTypeBadge
type="Studio Flex"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.[selectedFloor.split(" ")[0] as "West" | "East"]
.types["Studio Flex"] || 0
}
/>
<UnitTypeBadge
type="Studio"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.[selectedFloor.split(" ")[0] as "West" | "East"]
.types["Studio Squared"] || 0
}
/>
<UnitTypeBadge
type="1 Bedroom"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.[selectedFloor.split(" ")[0] as "West" | "East"]
.types["1 BR Squared"] || 0
}
/>
<UnitTypeBadge
type="2 Bedroom"
count={
floorsData?.find(
(item) =>
item.floor ===
parseInt(selectedFloor.split(" ").at(-1)!),
)?.[selectedFloor.split(" ")[0] as "West" | "East"]
.types["2 BR Squared"] || 0
}
/>
</div>
</div>
</div>
<div
className="2xl:p-[4.444vw] p-4 bg-[#F3F3F2] 2xl:rounded-[0.833vw] rounded-lg"
onMouseMove={(e) =>
!isMobile && setPosition({ x: e.clientX, y: e.clientY })
}
>
{unitsOnFloor && selectedFloor.split(" ")[0] === "East" && (
<FloorPlanMarasiDriveEast
unitsOnFloor={unitsOnFloor}
selectedFloor={selectedFloor.split(" ").at(-1)!}
/>
)}
{selectedFloor.split(" ")[0] === "West" && unitsOnFloor && (
<>
{+selectedFloor.split(" ")[1] < 24 ? (
<FloorPlanMarasiDriveWestLower
unitsOnFloor={unitsOnFloor}
selectedFloor={selectedFloor.split(" ").at(-1)!}
/>
) : (
<FloorPlanMarasiDriveWestUpper
unitsOnFloor={unitsOnFloor}
selectedFloor={selectedFloor.split(" ").at(-1)!}
/>
)}
</>
)}
</div>
</div>
)}
</>
{!currentFloor && complexName === "hq" && <>HQ</>}
{!currentFloor && selectedFloor && (
<div>Floor not found: {selectedFloor}</div>
)}
{complexName === "hq" && <>HQ</>}
</FloorSidebar>
</div>
);
+36
View File
@@ -0,0 +1,36 @@
// Базовый интерфейс для всех этажей
export interface BaseFloorData {
id: string;
name: string; // "1", "Rooftop", "Ground Level" и т.д.
displayName: string; // "1st Floor", "Rooftop"
}
// Жилой этаж с квартирами и SVG масками
export interface ResidentialFloorData extends BaseFloorData {
type: 'residential';
floorNumber: number;
wing?: 'East' | 'West'; // для Marasi Drive
}
// Этаж с удобствами
export interface AmenitiesFloorData extends BaseFloorData {
type: 'amenities';
amenitiesCount: {
total: number;
indoor?: number;
outdoor?: number;
};
amenitiesList: {
icon: string; // название компонента иконки
title: string;
}[];
images: {
main: string; // основное изображение (exterior)
interior?: string; // для переключения вида
content: string[]; // изображения для слайдера/галереи
};
video?: string; // путь к видео
}
// Discriminated union
export type FloorData = ResidentialFloorData | AmenitiesFloorData;
+5 -3
View File
@@ -1,4 +1,5 @@
import UnitType from "./UnitType";
import { FloorData } from "./Floor";
export default interface Project {
title: string;
@@ -6,12 +7,13 @@ export default interface Project {
img: string;
buildingType: "residential" | "commercial";
types: UnitType[];
amentiesFloors: AmentiesFloor[];
amenitiesFloors: AmenitiesFloor[];
floors?: FloorData[]; // New centralized floor data structure
}
export interface AmentiesFloor {
export interface AmenitiesFloor {
title: string;
total: number;
indoor?: number;
outdoor?: number;
}
}