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;