Add new popup images for Downtown, Dubai Marina, and Marasi Drive; update existing images for better quality. Remove TestPage route from main.tsx and introduce UnitSlider component for enhanced unit display functionality.

This commit is contained in:
2026-03-02 19:02:43 +05:00
parent af11ef08e4
commit 2e40b3a6cd
29 changed files with 375 additions and 255 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+6 -6
View File
@@ -39,9 +39,9 @@ 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`}
{`HQ by Rove instead
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="opacity-70 text-s 2xl:w-1/4 md:max-2xl:w-1/3">
<p className="text-s 2xl:w-1/4 md:max-2xl:w-1/3 opacity-70">
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
@@ -82,7 +82,7 @@ export default function AboutHQ() {
<span className="opacity-40">More than an office,</span>{" "}
<br className="2xl:hidden" /> a lifestyle.
</h2>
<p className="text-center whitespace-pre-line opacity-70 text-s max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16">
<p className="text-s max-md:whitespace-normal max-md:mb-12 md:max-2xl:mb-16 text-center whitespace-pre-line opacity-70">
{`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.`}
@@ -274,7 +274,7 @@ export default function AboutHQ() {
</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="font-medium whitespace-pre-line text-h2 max-2xl:mb-4">
<h2 className="text-h2 max-2xl:mb-4 font-medium whitespace-pre-line">
{`Work looks different here`}
</h2>
<p className="opacity-40 text-s max-md:mb-[32px] md:max-2xl:max-w-[57.943vw]">
@@ -6,12 +6,14 @@ interface FloorPlanMarasiDriveEastProps {
selectedFloor: string | null;
unitsOnFloor: Unit[];
chosenUnit?: Unit;
isUnitPage?: boolean;
}
function FloorPlanMarasiDriveEast({
selectedFloor,
unitsOnFloor,
chosenUnit,
isUnitPage,
}: FloorPlanMarasiDriveEastProps) {
return (
<GenericFloorPlan
@@ -22,6 +24,7 @@ function FloorPlanMarasiDriveEast({
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={chosenUnit}
isUnitPage={isUnitPage}
wing="East"
>
<g>
@@ -6,12 +6,14 @@ interface FloorPlanMarasiDriveWestLowerProps {
selectedFloor: string | null;
unitsOnFloor: Unit[];
chosenUnit?: Unit;
isUnitPage?: boolean;
}
function FloorPlanMarasiDriveWestLower({
selectedFloor,
unitsOnFloor,
chosenUnit,
isUnitPage,
}: FloorPlanMarasiDriveWestLowerProps) {
return (
<GenericFloorPlan
@@ -22,6 +24,7 @@ function FloorPlanMarasiDriveWestLower({
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={chosenUnit}
isUnitPage={isUnitPage}
wing="West"
>
<g>
@@ -6,12 +6,14 @@ interface FloorPlanMarasiDriveWestUpperProps {
selectedFloor: string | null;
unitsOnFloor: Unit[];
chosenUnit?: Unit;
isUnitPage?: boolean;
}
function FloorPlanMarasiDriveWestUpper({
unitsOnFloor,
selectedFloor,
chosenUnit,
isUnitPage,
}: FloorPlanMarasiDriveWestUpperProps) {
return (
<GenericFloorPlan
@@ -22,6 +24,7 @@ function FloorPlanMarasiDriveWestUpper({
selectedFloor={selectedFloor}
unitsOnFloor={unitsOnFloor}
chosenUnit={chosenUnit}
isUnitPage={isUnitPage}
wing="West"
>
<g>
+1 -1
View File
@@ -64,7 +64,7 @@ function Marker({
/>
<div
className={clsx(
"absolute bottom-[10%]",
"absolute bottom-[10%]ф top-1/2 -translate-y-1/2",
marker.popupPosition === "left" ? "right-full" : "left-full"
)}
>
+41 -5
View File
@@ -1,16 +1,50 @@
import { useQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
import { Unit } from "../types/IUnit";
import GenericFloorPlan from "./floor-plans/GenericFloorPlan";
import { getFloorPlanConfigForUnit } from "../data/floor-plan-config";
import { getSlugFromProjectName } from "../data/complex-config";
import { ComplexName } from "../types/ComplexName";
// ─── Helpers ─────────────────────────────────────────────
function getFloorParam(complexName: ComplexName, unit: Unit): string {
if (complexName === "dubai-marina" && (unit.floor === 39 || unit.floor === 40))
return "39";
if (complexName === "dubai-marina" && (unit.floor === 41 || unit.floor === 42))
return "41";
if (complexName === "hq" && (unit.floor === 29 || unit.floor === 30))
return "29";
return unit.floor.toString();
}
// ─── Component ───────────────────────────────────────────
function OnFloorMask({ unit }: { unit: Unit }) {
const complexName = getSlugFromProjectName(unit.project);
if (!complexName) return null;
const config = complexName
? getFloorPlanConfigForUnit(complexName, unit)
: null;
const floorParam = complexName ? getFloorParam(complexName, unit) : "";
const config = getFloorPlanConfigForUnit(complexName, unit);
if (!config) return null;
const { data: unitsOnFloor } = useQuery({
queryKey: ["units-on-floor", complexName, floorParam, unit.wing],
queryFn: () =>
api
.get(
`units/on-floor?project=${complexName}&floor=${floorParam}${
complexName === "marasi-drive" && unit.wing
? `&wing=${unit.wing}`
: ""
}`
)
.json<Unit[]>(),
enabled: !!complexName && !!config,
});
if (!complexName || !config) return null;
const units = unitsOnFloor ?? [unit];
// Marasi Drive uses component-based rendering (inline SVG)
if (config.component) {
@@ -18,8 +52,9 @@ function OnFloorMask({ unit }: { unit: Unit }) {
return (
<Component
selectedFloor={unit.floor.toString()}
unitsOnFloor={[unit]}
unitsOnFloor={units}
chosenUnit={unit}
isUnitPage
/>
);
}
@@ -34,8 +69,9 @@ function OnFloorMask({ unit }: { unit: Unit }) {
getMaskKey={config.getMaskKey}
filterUnits={config.filterUnits}
selectedFloor={unit.floor.toString()}
unitsOnFloor={[unit]}
unitsOnFloor={units}
chosenUnit={unit}
isUnitPage
wing={config.wing}
/>
);
+5 -4
View File
@@ -61,13 +61,14 @@ function PopupContainer() {
<div
className={clsx(
"max-md:hidden absolute 2xl:border-[0.556vw_0px_0.486vw_0.556vw] [border-width:8px_0px_7px_8px] [border-color:_transparent_transparent_transparent_#fff]",
side === "left" && "top-1/2 [y:-50%] left-full [x:1px]",
side === "left" &&
"top-1/2 -translate-y-1/2 left-full -translate-x-[1px]",
side === "right" &&
"top-1/2 [y:-50%] right-full [x:1px] [rotate:180deg]",
"top-1/2 translate-y-1/2 right-full -translate-x-[1px] [rotate:180deg]",
side === "top" &&
"left-1/2 [x:100%] absolute top-full [y:1px] [rotate:90deg] origin-top-left",
"left-1/2 -translate-y-full -translate-x-[1px] absolute top-full [rotate:90deg] origin-top-left",
side === "bottom" &&
"left-1/2 [x:100%] absolute bottom-full [y:1px] [rotate:-90deg] origin-bottom-left"
"left-1/2 translate-y-full -translate-x-[1px] absolute bottom-full [rotate:-90deg] origin-bottom-left"
)}
/>
</motion.div>
+2 -2
View File
@@ -173,7 +173,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="font-medium text-h2">
<p className="text-h2 font-medium">
{inModal ? "Filters" : "Search"}
</p>
<div className={clsx(!inModal && "max-md:hidden")}>
@@ -372,7 +372,7 @@ function SearchFilters({
<div className="w-5 h-5">
<FiltersIcon />
</div>
<p className="text-sm leading-0">Filters</p>
<p className="leading-0 text-sm">Filters</p>
</Button>
<Button
variant="secondary"
+6 -7
View File
@@ -203,30 +203,29 @@ function SequenceSlider({ complexName }: SequenceSliderProps) {
<span className="max-md:hidden">About</span>
</Button>
<p className="absolute md:text-h4 text-h5 font-medium text-white -translate-x-1/2 select-none left-1/2 2xl:top-[2.5vw] md:max-2xl:top-[3.646vw] top-[7.5vw] drop-shadow pointer-events-none">
ROVE Home{" "}
{markers.find((marker) => marker.name === complexName)?.title}
</p>
<Button
onlyIcon
variant="secondary"
className="absolute top-1/2 -translate-y-1/2 2xl:left-[31.111vw] md:max-2xl:left-[8.854vw] left-4 !bg-[#0D1922] !bg-opacity-40 backdrop-blur-md"
variant="fab"
className="absolute top-1/2 -translate-y-1/2 2xl:left-[31.111vw] md:max-2xl:left-[8.854vw] left-4"
roundedFull
disabled={isAnimating || !isShowVideo}
onClick={() => handleSwipe("prev")}
>
<span className="2xl:size-[1.111vw] size-4 text-white">
<span className="2xl:size-[1.111vw] size-4">
<ArrowLeftIcon />
</span>
</Button>
<Button
onlyIcon
variant="secondary"
className="absolute top-1/2 -translate-y-1/2 2xl:right-[31.111vw] md:max-2xl:right-[8.854vw] right-4 !bg-[#0D1922] !bg-opacity-40 backdrop-blur-md"
variant="fab"
className="absolute top-1/2 -translate-y-1/2 2xl:right-[31.111vw] md:max-2xl:right-[8.854vw] right-4"
roundedFull
disabled={isAnimating || !isShowVideo}
onClick={() => handleSwipe("next")}
>
<span className="2xl:size-[1.111vw] size-4 text-white">
<span className="2xl:size-[1.111vw] size-4">
<ArrowRightIcon />
</span>
</Button>
+5 -1
View File
@@ -25,7 +25,11 @@ function UnitTypeCard({ project, type }: { project: Project; type: UnitType }) {
<img
src={`/images/unit-types/${project.slug}/${type.slug}${
type.slug.includes("loft") ? "-lower" : ""
}${project.hasSides !== false && type.slug !== "2-bedroom-b" ? "-left" : ""}.jpg`}
}${
project.hasSides !== false && type.slug !== "2-bedroom-b"
? "-left"
: ""
}.jpg`}
alt=""
/>
</div>
+10 -6
View File
@@ -60,24 +60,28 @@ function UnitTypeImageWithMarkers({
}}
/>
{filteredLegend.map((item, index) => {
const coords = unitTypeVariant?.endsWith("left") ? item.left : item.right;
const coords = unitTypeVariant?.endsWith("left")
? item.left
: item.right;
return (
<rect
key={`marker-${index}`}
ref={refRect}
x={coords.x}
y={coords.y}
width={16}
height={16}
rx={8}
className="stroke-white fill-[#00BED7] hover:fill-white transition-colors cursor-pointer max-md:hidden"
width={12}
height={12}
rx={6}
className="stroke-white hover:fill-[#00BED7] fill-white transition-colors max-md:hidden"
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
/>
);
})}
{filteredLegend.map((item, index) => {
const coords = unitTypeVariant?.endsWith("left") ? item.left : item.right;
const coords = unitTypeVariant?.endsWith("left")
? item.left
: item.right;
return (
<g
key={`tooltip-${index}`}
@@ -43,7 +43,7 @@ function AmenitiesFloorView({ floor }: AmenitiesFloorViewProps) {
<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="font-medium text-h4">{floor.displayName}</p>
<p className="text-h4 font-medium">{floor.displayName}</p>
<Badge
variant="secondary"
text={`${floor.amenitiesCount.total} Amenities`}
@@ -80,13 +80,13 @@ function AmenitiesFloorView({ floor }: AmenitiesFloorViewProps) {
<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>
Play Video
</Button>
)}
</div>
@@ -94,7 +94,7 @@ function AmenitiesFloorView({ floor }: AmenitiesFloorViewProps) {
{hasIndoorOutdoorSplit && indoorAmenities.length > 0 && (
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">Indoor Amenities</p>
<p className="text-h4 font-medium">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} />
@@ -108,7 +108,7 @@ function AmenitiesFloorView({ floor }: AmenitiesFloorViewProps) {
)}
<div className="2xl:space-y-[1.667vw] space-y-6">
<p className="font-medium text-h4">
<p className="text-h4 font-medium">
{hasIndoorOutdoorSplit ? "Outdoor Amenities" : "Amenities"}
</p>
<div
+59 -18
View File
@@ -1,4 +1,5 @@
import { Fragment, useState } from "react";
import { useNavigate } from "react-router";
import { Unit } from "../../types/IUnit";
import { ComplexName } from "../../types/ComplexName";
import { FloorPlanMasks } from "../../types/FloorPlanMasks";
@@ -18,6 +19,8 @@ interface GenericFloorPlanProps {
selectedFloor: string | null;
unitsOnFloor: Unit[];
chosenUnit?: Unit | null;
/** When true: dim background, gray other units, no hover/cursor. Only from OnFloorMask. */
isUnitPage?: boolean;
/** How to extract the mask key from unit.unitNo. Default: unitNo.slice(-2) */
getMaskKey?: (unitNo: string) => string;
/** Optional unit filter (e.g. filterDuplicateUnits for combinable layouts) */
@@ -40,6 +43,7 @@ function GenericFloorPlan({
selectedFloor,
unitsOnFloor,
chosenUnit,
isUnitPage = false,
getMaskKey = defaultGetMaskKey,
filterUnits,
wing,
@@ -61,8 +65,16 @@ function GenericFloorPlan({
{...svgProps}
>
{/* Background: either an image or custom inline SVG */}
{imagePath && <image transform="scale(.5)" xlinkHref={imagePath} />}
{children}
{imagePath && (
<image
transform="scale(.5)"
xlinkHref={imagePath}
opacity={isUnitPage ? 0.3 : 1}
/>
)}
{children ? (
<g opacity={isUnitPage ? 0.3 : 1}>{children}</g>
) : null}
{/* Unit overlays */}
{units.map((unit) => {
@@ -76,6 +88,8 @@ function GenericFloorPlan({
key={unit.unitNo}
complexName={complexName}
wing={wing}
chosenUnit={chosenUnit}
isUnitPage={isUnitPage}
selectedUnit={selectedUnit}
onSelect={setSelectedUnit}
formattedUnitType={maskData.formattedUnitType}
@@ -104,6 +118,8 @@ function GenericFloorPlanUnit({
formattedUnitType,
onSelect,
selectedUnit,
chosenUnit,
isUnitPage = false,
}: {
complexName: ComplexName;
wing?: "East" | "West";
@@ -114,18 +130,23 @@ function GenericFloorPlanUnit({
formattedUnitType: string;
selectedUnit: Unit | null;
onSelect: (unit: Unit | null) => void;
chosenUnit?: Unit | null;
isUnitPage?: boolean;
}) {
const navigate = useNavigate();
const { setPopup, setSide, setPosition } = usePopupStore();
function handleClick(unit: Unit) {
window.open(`/complex/${complexName}/${unit.unitNo}`, "_blank");
const isOtherUnit =
isUnitPage && chosenUnit && unit.unitNo !== chosenUnit.unitNo;
function handleClick(u: Unit) {
setPopup(null);
navigate(`/complex/${complexName}/${u.unitNo}`);
}
function handleMouseEnter() {
if (floor === null) return;
setSide("top");
if (!selectedUnit)
setPopup(
<UnitPopup
@@ -146,7 +167,10 @@ function GenericFloorPlanUnit({
<g>
<text
transform={textTransform}
className="fill-white text-[8px] select-none"
className={clsx(
"text-[8px] select-none",
isUnitPage && !isOtherUnit ? "fill-[#374151]" : "fill-white"
)}
textAnchor="middle"
>
<tspan x={0} y={0}>
@@ -157,18 +181,35 @@ function GenericFloorPlanUnit({
</tspan>
</text>
<path
onClick={() => !isMobile && !selectedUnit && handleClick(unit)}
onMouseEnter={!isMobile ? handleMouseEnter : undefined}
onMouseLeave={() => !isMobile && setPopup(null)}
onTouchStart={(e) => {
onSelect(unit);
setPosition({ x: e.touches[0].clientX, y: e.touches[0].clientY });
handleMouseEnter();
}}
onClick={
!isUnitPage && !isMobile && !selectedUnit
? () => handleClick(unit)
: undefined
}
onMouseEnter={
!isUnitPage && !isMobile ? handleMouseEnter : undefined
}
onMouseLeave={!isUnitPage && !isMobile ? () => setPopup(null) : undefined}
onTouchStart={
!isUnitPage
? (e) => {
onSelect(unit);
setPosition({ x: e.touches[0].clientX, y: e.touches[0].clientY });
handleMouseEnter();
}
: undefined
}
className={clsx(
"fill-transparent hover:fill-[#00BED7] opacity-40 isolate cursor-pointer transition-colors",
selectedUnit?.unitNo === unit.unitNo &&
"!fill-[#00BED7] opacity-40 cursor-default"
"isolate transition-colors",
isUnitPage
? isOtherUnit
? "fill-[#9CA3AF] opacity-50 pointer-events-none"
: "fill-[#00BED7] opacity-40"
: clsx(
"fill-transparent hover:fill-[#00BED7] opacity-40 cursor-pointer",
selectedUnit?.unitNo === unit.unitNo &&
"!fill-[#00BED7] opacity-40 cursor-default"
)
)}
d={d}
/>
+6 -1
View File
@@ -3,7 +3,7 @@ import { clsx } from "clsx";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: "link" | "primary" | "secondary" | "tertiary" | "cta";
variant?: "link" | "primary" | "secondary" | "tertiary" | "cta" | "fab";
className?: string;
size?: "small" | "medium" | "large";
onlyIcon?: boolean;
@@ -50,6 +50,11 @@ function Button({
"bg-white hover:!bg-white hover:bg-opacity-80 text-[#0D1922]/70 disabled:!bg-[#0D1922] disabled:!bg-opacity-[8%] disabled:text-[#0D1922]/40",
variant === "tertiary" &&
"text-xs leading-[135%] text-[#0D1922]/70 !px-0 hover:!text-[#0D1922] disabled:!bg-transparent",
variant === "fab" &&
"bg-[#0D192266] hover:bg-[#0D192299] active:bg-[#00BED7] disabled:bg-[#0D192214] disabled:text-[#0D192214] text-white backdrop-blur-md rounded-full",
variant === "fab" && onlyIcon
? "active:!bg-[#0D192299] active:!text-[#00BED7]"
: "",
roundedFull ? "rounded-full" : "2xl:rounded-[0.833vw] rounded-xl",
className
)}
+1 -1
View File
@@ -101,7 +101,7 @@ export const projectBrochures: ProjectBrochures[] = [
],
},
{
projectTitle: "Rove Home HQ",
projectTitle: "HQ by Rove",
brochures: [
{
title: "Main Brochure",
+1 -1
View File
@@ -38,7 +38,7 @@ const complexConfigs: Record<ComplexName, ComplexConfig> = {
},
hq: {
slug: "hq",
projectName: "Rove Home HQ",
projectName: "HQ by Rove",
floors: hqFloors,
hasWings: false,
hasCombinable: false,
+1
View File
@@ -48,6 +48,7 @@ export interface FloorPlanLayoutConfig {
selectedFloor: string | null;
unitsOnFloor: Unit[];
chosenUnit?: Unit;
isUnitPage?: boolean;
}>;
/** Wing designation for Marasi Drive popups */
wing?: "East" | "West";
+6 -6
View File
@@ -7,7 +7,7 @@ export const markers: IMarker[] = [
x: "54.2%",
y: "50.6%",
popupPosition: "left",
title: "Downtown",
title: "Rove Home Downtown",
disabled: true,
},
{
@@ -16,7 +16,7 @@ export const markers: IMarker[] = [
x: "55.6%",
y: "52.5%",
popupPosition: "right",
title: "Marasi Drive",
title: "Rove Home Marasi Drive",
numberOfUnits: 809,
},
{
@@ -25,16 +25,16 @@ export const markers: IMarker[] = [
x: "35.35%",
y: "71.6%",
popupPosition: "right",
title: "Dubai Marina",
title: "Rove Home Dubai Marina",
numberOfUnits: 958,
},
{
id:4 ,
id: 4,
name: "hq",
x: "57%",
y: "48%",
popupPosition: "right",
title: "HQ",
title: "HQ by Rove",
numberOfUnits: 1000,
}
},
];
+7 -8
View File
@@ -1989,14 +1989,14 @@ export const projects: Project[] = [
],
},
{
title: "Rove Home HQ",
title: "HQ by Rove",
slug: "hq",
img: "/images/search/rove_home_hq.png",
buildingType: "commercial",
hasSides: false,
types: [
{
name: "Studio",
name: "Studio office",
slug: "studio",
floors: "Ground Floor",
area: "TBD Sqft",
@@ -2054,7 +2054,7 @@ export const projects: Project[] = [
tourAvailable: false,
},
{
name: "Simplex Edge",
name: "Simplex Edge office",
slug: "simplex-edge",
floors: "Ground Floor",
area: "TBD Sqft",
@@ -2108,12 +2108,11 @@ export const projects: Project[] = [
left: { x: 115, y: 185 },
right: { x: 115, y: 185 },
},
],
tourAvailable: false,
},
{
name: "Loft Edge",
name: "Loft Edge office",
slug: "loft-edge",
floors: "Ground Floor & Mezzanine",
area: "TBD Sqft",
@@ -2263,7 +2262,7 @@ export const projects: Project[] = [
tourAvailable: false,
},
{
name: "Penthouse Loft",
name: "Penthouse Loft office",
slug: "penthouse-loft",
floors: "Upper Floors",
area: "TBD Sqft",
@@ -2327,7 +2326,7 @@ export const projects: Project[] = [
},
{
name: "Powder room",
left: { x: 430, y: 536},
left: { x: 430, y: 536 },
right: { x: 430, y: 536 },
floor: "lower",
},
@@ -2407,7 +2406,7 @@ export const projects: Project[] = [
tourAvailable: false,
},
{
name: "Presidential Loft",
name: "Presidential Loft office",
slug: "presidential-loft",
floors: "Upper Floors",
area: "TBD Sqft",
-5
View File
@@ -20,7 +20,6 @@ import LayoutWithoutFooter from "./layout/LayoutWithoutFooter.tsx";
import { queryClient } from "./lib/queryClient.ts";
import AboutComplexPage from "./pages/AboutComplexPage.tsx";
import UnitTypeItemPage from "./pages/UnitTypeItemPage.tsx";
import TestPage from "./pages/TestPage.tsx";
import UnitPage from "./pages/UnitPage.tsx";
import PopupContainer from "./components/PopupContainer.tsx";
import VirtualTourPage from "./pages/VirtualTourPage.tsx";
@@ -80,10 +79,6 @@ const route = createBrowserRouter([
},
],
},
{
path: "/test",
element: <TestPage />,
},
]);
createRoot(document.getElementById("root")!).render(
+205 -179
View File
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router";
import { api } from "../api/ky";
@@ -42,6 +43,17 @@ function UnitPage() {
const { setModal } = useModalStore();
const displayUnitType = unit
? formattedUnitTypes.get(unit.unitType) || unit.unitType
: "Rove Home";
useEffect(() => {
document.title = displayUnitType;
return () => {
document.title = "Rove Home";
};
}, [displayUnitType]);
if (!unit) return null;
function handleShare() {
@@ -51,151 +63,167 @@ function UnitPage() {
});
}
if (!unit) return <div>Loading...</div>;
const unitType = projects
.find((project) => project.slug === unit.projectSlug)
?.types.find((type) => type.slug === unit.unitTypeVariantSlug);
return (
<html>
<head>
<title>{unit.unitTypeVariantSlug}</title>
</head>
<body>
<div className="2xl:p-[2.222vw] md:max-2xl:p-6 p-4 max-2xl:pb-0 bg-white flex 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 max-2xl:flex-col">
<NewUnitSlider>
{unit.isLoft ? (
<>
<UnitSliderItem text="Lower">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-lower" +
(unit.side ? `-${unit.side}` : "")
}
floor="lower"
legend={unitType?.legend || []}
/>
</UnitSliderItem>
<UnitSliderItem text="Upper">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-upper" +
(unit.side ? `-${unit.side}` : "")
}
floor="upper"
legend={unitType?.legend || []}
/>
</UnitSliderItem>
</>
) : (
<UnitSliderItem text="Layout">
<div className="2xl:p-[2.222vw] md:max-2xl:p-6 p-4 max-2xl:pb-0 bg-white flex 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 max-2xl:flex-col">
<div className="relative">
<NewUnitSlider>
{unit.isLoft ? (
<>
<UnitSliderItem text="Lower">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-lower" +
(unit.side ? `-${unit.side}` : "")
}
floor="lower"
legend={unitType?.legend || []}
/>
</UnitSliderItem>
)}
<UnitSliderItem text="On the floor">
<svg className="2xl:py-[2.222vw] py-8 size-full">
<OnFloorMask unit={unit} />
</svg>
</UnitSliderItem>
{innerWidth >= 768 ? (
<UnitSliderItem text="Interior">
<InteriorSlider
unitTypeSlug={unit.unitTypeVariantSlug}
projectSlug={unit.projectSlug}
<UnitSliderItem text="Upper">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug +
"-upper" +
(unit.side ? `-${unit.side}` : "")
}
floor="upper"
legend={unitType?.legend || []}
/>
</UnitSliderItem>
) : (
unitType?.interiors.map((interior, index) => (
<UnitSliderItem text={`interior ${index + 1}`} key={index}>
<img
src={interior}
alt=""
className="size-full object-cover pointer-events-none"
</>
) : (
<UnitSliderItem text="Layout">
<UnitTypeImageWithMarkers
complexName={unit.projectSlug}
unitTypeVariant={
unit.unitTypeVariantSlug + (unit.side ? `-${unit.side}` : "")
}
legend={unitType?.legend || []}
/>
</UnitSliderItem>
)}
<UnitSliderItem text="On the floor">
<svg className="2xl:py-[2.222vw] py-8 size-full">
<OnFloorMask unit={unit} />
</svg>
</UnitSliderItem>
{innerWidth >= 768 ? (
<UnitSliderItem text="Interior">
<InteriorSlider
unitTypeSlug={unit.unitTypeVariantSlug}
projectSlug={unit.projectSlug}
/>
</UnitSliderItem>
) : (
unitType?.interiors.map((interior, index) => (
<UnitSliderItem text={`interior ${index + 1}`} key={index}>
<img
src={interior}
alt=""
className="size-full object-cover pointer-events-none"
/>
</UnitSliderItem>
))
)}
</NewUnitSlider>
{unit.projectSlug === "hq" &&
[
"loft-edge",
"penthouse-loft",
"presidential-loft",
"studio",
].includes(unit.unitTypeVariantSlug) && (
<Button
variant="cta"
onlyIcon={innerWidth < 768}
size="large"
className="absolute 2xl:right-[1.667vw] 2xl:top-[1.667vw] right-6 top-6"
onClick={() =>
setModal(
<VideoModal
src={`/videos/unit-types/hq/${unit.unitTypeVariantSlug}.mp4`}
/>
</UnitSliderItem>
))
)}
</NewUnitSlider>
<div className="flex flex-col justify-between md:max-2xl:gap-6 gap-4 2xl:w-[21.944vw] flex-shrink-0">
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="flex justify-between items-start">
<div className="2xl:space-y-[1.111vw] space-y-4">
<h3 className="text-h3 font-medium">
{formattedUnitTypes.get(unit.unitType) || unit.unitType}
</h3>
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="text-s text-[#00BED7]">{unit.project}</p>
<div className="flex items-center 2xl:gap-[0.556vw]">
{unit.wing && (
<>
<p className="text-s opacity-70">{unit.wing}</p>
<div className="2xl:w-[0.278vw] 2xl:h-[0.278vw] w-1 h-1 rounded-full bg-[#E2E2DC]" />
</>
)}
<p className="text-s opacity-70">Floor {unit.floor}</p>
)
}
>
<div className="2xl:size-[1.389vw] size-5">
<PlayIcon />
</div>
<span className="max-md:hidden">Video tour</span>
</Button>
)}
</div>
<div className="flex flex-col justify-between md:max-2xl:gap-6 gap-4 2xl:w-[21.944vw] flex-shrink-0">
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="flex justify-between items-start">
<div className="2xl:space-y-[1.111vw] space-y-4">
<h3 className="text-h3 font-medium">
{formattedUnitTypes.get(unit.unitType) || unit.unitType}
</h3>
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="text-s text-[#00BED7]">{unit.project}</p>
<div className="flex items-center 2xl:gap-[0.556vw]">
{unit.wing && (
<>
<p className="text-s opacity-70">{unit.wing}</p>
<div className="2xl:w-[0.278vw] 2xl:h-[0.278vw] w-1 h-1 rounded-full bg-[#E2E2DC]" />
<p className="text-s opacity-70">{unit.unitNo}</p>
</div>
</div>
</div>
<div>
<Button variant="tertiary" onlyIcon onClick={handleFavorite}>
<span className="2xl:size-[1.389vw] size-5">
{favoriteUnits.some(
(favoriteUnit) => favoriteUnit.id === unit.id
) ? (
<FilledHeartIcon />
) : (
<HeartIcon />
)}
</span>
</Button>
<Button variant="tertiary" onlyIcon onClick={handleShare}>
<span className="2xl:size-[1.389vw] size-5">
<ShareIcon />
</span>
</Button>
</>
)}
<p className="text-s opacity-70">Floor {unit.floor}</p>
<div className="2xl:w-[0.278vw] 2xl:h-[0.278vw] w-1 h-1 rounded-full bg-[#E2E2DC]" />
<p className="text-s opacity-70">{unit.unitNo}</p>
</div>
</div>
<hr className="w-full border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
{unitType?.video && (
<button
onClick={() =>
setModal(<VideoModal src={unitType.video?.src || ""} />)
}
className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] text-left rounded-2xl flex items-center gap-[0.556vw] ring-[0.069vw] ring-[#E2E2DC] cursor-pointer w-full"
>
<div className="lg:space-y-[0.278vw] space-y-1 flex-1">
<p className="text-h5 font-medium">
{unitType.video.title}
</p>
<p className="text-s text-[#00BED7]">
{unitType.video.desc}
</p>
</div>
<div className="2xl:size-[1.389vw] size-5">
<PlayIcon />
</div>
</button>
)}
<div className="flex flex-col 2xl:gap-y-[0.556vw]">
<div className="flex justify-between">
<p className="text-s opacity-70">Total Area</p>
<p className="text-s">{unit.squareFt.toFixed(2)} Sqft</p>
</div>
{/* <div className="flex justify-between">
</div>
<div>
<Button variant="tertiary" onlyIcon onClick={handleFavorite}>
<span className="2xl:size-[1.389vw] size-5">
{favoriteUnits.some(
(favoriteUnit) => favoriteUnit.id === unit.id
) ? (
<FilledHeartIcon />
) : (
<HeartIcon />
)}
</span>
</Button>
<Button variant="tertiary" onlyIcon onClick={handleShare}>
<span className="2xl:size-[1.389vw] size-5">
<ShareIcon />
</span>
</Button>
</div>
</div>
<hr className="w-full border-[#E2E2DC] 2xl:h-[0.069vw] h-px" />
{unitType?.video && (
<button
onClick={() =>
setModal(<VideoModal src={unitType.video?.src || ""} />)
}
className="2xl:p-[1.111vw] p-4 2xl:rounded-[1.111vw] text-left rounded-2xl flex items-center gap-[0.556vw] ring-[0.069vw] ring-[#E2E2DC] cursor-pointer w-full"
>
<div className="lg:space-y-[0.278vw] space-y-1 flex-1">
<p className="text-h5 font-medium">{unitType.video.title}</p>
<p className="text-s text-[#00BED7]">{unitType.video.desc}</p>
</div>
<div className="2xl:size-[1.389vw] size-5">
<PlayIcon />
</div>
</button>
)}
<div className="flex flex-col 2xl:gap-y-[0.556vw]">
<div className="flex justify-between">
<p className="text-s opacity-70">Total Area</p>
<p className="text-s">{unit.squareFt.toFixed(2)} Sqft</p>
</div>
{/* <div className="flex justify-between">
<p className="text-s opacity-70">Suite Area</p>
<p className="text-s">{unit.suitsArea.toFixed(2)} Sqft</p>
</div>
@@ -205,7 +233,7 @@ function UnitPage() {
{unit.balconyArea.toFixed(2)} Sqft
</p>
</div> */}
{/* <div className="flex justify-between">
{/* <div className="flex justify-between">
<p className="text-s opacity-70">Status</p>
<p className="text-s">
{unit.projectSlug === "dubai-marina" &&
@@ -215,20 +243,20 @@ function UnitPage() {
unit.state.slice(1).replace(/_/g, " ")}
</p>
</div> */}
<div className="flex justify-between">
<p className="text-s opacity-70">Parking Space</p>
<p className="text-s">{unit.noOfParkingSpace}</p>
</div>
</div>
{/* <p className="text-h4 font-medium text-[#00BED7]">{`AED ${Intl.NumberFormat(
<div className="flex justify-between">
<p className="text-s opacity-70">Parking Space</p>
<p className="text-s">{unit.noOfParkingSpace}</p>
</div>
</div>
{/* <p className="text-h4 font-medium text-[#00BED7]">{`AED ${Intl.NumberFormat(
"ar-AE",
{
currency: "AED",
minimumFractionDigits: 0,
}
).format(unit.salesPrice)}`}</p> */}
</div>
{/* <div className="flex 2xl:flex-col 2xl:gap-[0.556vw] gap-2 2xl:bottom-[2.222vw] bottom-0 bg-white md:max-2xl:-mx-[3.125vw] md:max-2xl:p-[3.125vw] max-md:-mx-4 max-md:p-4 max-2xl:rounded-t-2xl max-2xl:shadow-[0_-4px_20px_0_rgba(0,0,0,0.05)]">
</div>
{/* <div className="flex 2xl:flex-col 2xl:gap-[0.556vw] gap-2 2xl:bottom-[2.222vw] bottom-0 bg-white md:max-2xl:-mx-[3.125vw] md:max-2xl:p-[3.125vw] max-md:-mx-4 max-md:p-4 max-2xl:rounded-t-2xl max-2xl:shadow-[0_-4px_20px_0_rgba(0,0,0,0.05)]">
<Button
disabled={!unitTypeVariantMarasiDrive}
variant="cta"
@@ -247,55 +275,53 @@ function UnitPage() {
Book
</Button>
</div> */}
<div className="flex 2xl:flex-col 2xl:gap-[0.556vw] gap-2 2xl:bottom-[2.222vw] bottom-0 bg-white md:max-2xl:-mx-[3.125vw] md:max-2xl:p-[3.125vw] max-md:-mx-4 max-md:p-4 max-2xl:rounded-t-2xl max-2xl:shadow-[0_-4px_20px_0_rgba(0,0,0,0.05)]">
{projects
.find((project) => project.slug === params.complexName)
?.types.find((type) => type.slug === unit.unitTypeVariantSlug)
?.tourAvailable && (
<Button
variant="cta"
size="large"
onClick={() =>
window.open(
`/virtual-tour/${params.complexName}/${unit.unitTypeVariantSlug}`,
"_blank"
)
}
>
Virtual tour
</Button>
)}
{/* <Button disabled variant="cta" size="large">
<div className="flex 2xl:flex-col 2xl:gap-[0.556vw] gap-2 2xl:bottom-[2.222vw] bottom-0 bg-white md:max-2xl:-mx-[3.125vw] md:max-2xl:p-[3.125vw] max-md:-mx-4 max-md:p-4 max-2xl:rounded-t-2xl max-2xl:shadow-[0_-4px_20px_0_rgba(0,0,0,0.05)]">
{projects
.find((project) => project.slug === params.complexName)
?.types.find((type) => type.slug === unit.unitTypeVariantSlug)
?.tourAvailable && (
<Button
variant="cta"
size="large"
onClick={() =>
window.open(
`/virtual-tour/${params.complexName}/${unit.unitTypeVariantSlug}`,
"_blank"
)
}
>
Virtual tour
</Button>
)}
{/* <Button disabled variant="cta" size="large">
Book
</Button> */}
{/* videos for hq units */}
{unit.projectSlug === "hq" &&
[
"loft-edge",
"penthouse-loft",
"presidential-loft",
"studio",
].includes(unit.unitTypeVariantSlug) && (
<Button
variant="cta"
size="large"
onClick={() =>
setModal(
<VideoModal
src={`/videos/unit-types/hq/${unit.unitTypeVariantSlug}.mp4`}
/>
)
}
>
Video tour
</Button>
)}
</div>
</div>
{/* videos for hq units */}
{/* {unit.projectSlug === "hq" &&
[
"loft-edge",
"penthouse-loft",
"presidential-loft",
"studio",
].includes(unit.unitTypeVariantSlug) && (
<Button
variant="cta"
size="large"
onClick={() =>
setModal(
<VideoModal
src={`/videos/unit-types/hq/${unit.unitTypeVariantSlug}.mp4`}
/>
)
}
>
Video tour
</Button>
)} */}
</div>
</body>
</html>
</div>
</div>
);
}