Refactor FloorsPage component to streamline floor selection and rendering; replace individual floor components with a centralized FloorPlanViewer for improved maintainability. Introduce new floor data structure in Project type for better data management.

This commit is contained in:
2026-01-21 19:11:32 +05:00
parent 36046214e1
commit 454882392b
9 changed files with 950 additions and 305 deletions
@@ -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;
+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",
},
];
+29 -303
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}
{currentFloor && (
<FloorPlanViewer
floor={currentFloor}
complexName={complexName!}
unitsOnFloor={unitsOnFloor}
floorsData={floorsData}
selectedFloor={selectedFloor!}
onFloorSelect={setSelectedFloor}
/>
)}
</>
{!currentFloor && complexName === "hq" && <>HQ</>}
{!currentFloor && selectedFloor && (
<div>Floor not found: {selectedFloor}</div>
)}
{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>
)}
</>
)}
{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>
)}
</>
)}
{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;
+2
View File
@@ -1,4 +1,5 @@
import UnitType from "./UnitType";
import { FloorData } from "./Floor";
export default interface Project {
title: string;
@@ -7,6 +8,7 @@ export default interface Project {
buildingType: "residential" | "commercial";
types: UnitType[];
amenitiesFloors: AmenitiesFloor[];
floors?: FloorData[]; // New centralized floor data structure
}
export interface AmenitiesFloor {