Files
irth-new-client-120/src/components/FloorSelect.tsx
T

916 lines
29 KiB
TypeScript

/* eslint-disable react-hooks/exhaustive-deps */
import { Fragment, useEffect, useRef, useState } from "react";
import { markers } from "../data/markers";
import { enumerationMasks, floorsMasks } from "../data/masks";
import Compass from "./Compass";
import ArrowLeftIcon from "./icons/ArrowLeftIcon";
import InfoIcon from "./icons/InfoIcon";
import Button from "./ui/Button";
import { useLocation, useNavigate } from "react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "../api/ky";
import clsx from "clsx";
import { usePopupStore } from "../stores/usePopupStore";
import FloorPopup from "./FloorPopup";
import { isMobile } from "react-device-detect";
import { useClickAway } from "@uidotdev/usehooks";
import ButtonGroup from "./ButtonGroup";
import { SPECIAL_FLOORS } from "../constants/floors";
import { ComplexName } from "../types/ComplexName";
interface Position {
x: number;
y: number;
}
interface Size {
width: number;
height: number;
}
const constrainPosition = (
position: Position,
containerSize: Size,
imageSize: Size,
zoom: number
): Position => {
const scaledWidth = imageSize.width * zoom;
const scaledHeight = imageSize.height * zoom;
// Calculate center position for X axis
const centerX = (containerSize.width - scaledWidth) / 2;
// Ограничение по X: 10% от ширины изображения
const limitXPercent = 0.1;
const maxOffsetX = scaledWidth * limitXPercent;
const minX = centerX - maxOffsetX;
const maxX = centerX + maxOffsetX;
// Ограничение по Y: обычные границы контейнера
const minY = containerSize.height - scaledHeight;
const maxY = 0;
return {
x: Math.min(maxX, Math.max(minX, position.x)),
y: Math.min(maxY, Math.max(minY, position.y)),
};
};
const getEventPosition = (
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent
): Position => {
if ("touches" in e)
return {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
return {
x: e.clientX,
y: e.clientY,
};
};
const calculateMinZoom = (containerSize: Size, imageSize: Size): number => {
if (imageSize.width === 0 || imageSize.height === 0) return 0.1;
// Используем только высоту для подгонки - изображение должно занимать всю высоту экрана
const heightRatio = containerSize.height / imageSize.height;
return heightRatio;
};
const calculateCenterPosition = (
containerSize: Size,
imageSize: Size,
zoom: number
): Position => {
const scaledWidth = imageSize.width * zoom;
const scaledHeight = imageSize.height * zoom;
return {
x: (containerSize.width - scaledWidth) / 2,
y: (containerSize.height - scaledHeight) / 2,
};
};
export interface FloorsData {
floor: number;
West: {
types: Record<string, number>;
totalUnits: number;
};
East: {
types: Record<string, number>;
totalUnits: number;
};
others: {
types: Record<string, number>;
totalUnits: number;
};
}
function FloorSelect({
complexName,
selectedFloor,
onSelect,
}: {
complexName: ComplexName;
selectedFloor: string | null;
onSelect: (floor: string) => void;
}) {
const navigate = useNavigate();
const { popup, setPopup, setPosition, setSide } = usePopupStore();
const [hoveredFloor, setHoveredFloor] = useState<string | null>(null);
const [isImageLoaded, setIsImageLoaded] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
// Image size constants
const originalSize = { width: 4096, height: 1752 };
// Zoom and pan state
const [isDragging, setIsDragging] = useState(false);
const [startPosition, setStartPosition] = useState<Position>({ x: 0, y: 0 });
const [position, setImagePosition] = useState<Position>({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const containerRef = useRef<SVGSVGElement>(null);
const previousTouchDistance = useRef<number | null>(null);
const initialTouchDistance = useRef<number | null>(null);
const minZoomRef = useRef<number>(0.1); // Минимальный зум будет вычислен динамически
const maxZoomRef = useRef<number>(1.5); // Максимальный зум будет вычислен динамически
const maxZoomMultiplier = 1.5; // Коэффициент для максимального зума (50% больше минимального)
const containerSizeRef = useRef<Size>({ width: 0, height: 0 });
const lastPanTime = useRef<number>(0);
const panVelocity = useRef<Position>({ x: 0, y: 0 });
// Track interaction state to prevent clicks during drag/zoom
const [hasMoved, setHasMoved] = useState(false);
const [hasZoomed, setHasZoomed] = useState(false);
const dragStartPosition = useRef<Position>({ x: 0, y: 0 });
const dragThreshold = 5; // pixels
// Enhanced zoom and pan handlers
const updatePosition = (newPosition: Position, newZoom: number = zoom) => {
if (!containerRef.current || !rootRef.current) return;
const containerRect = rootRef.current.getBoundingClientRect();
const constrainedPosition = constrainPosition(
newPosition,
{ width: containerRect.width, height: containerRect.height },
originalSize,
newZoom
);
setImagePosition(constrainedPosition);
};
// Check if zoom is at limits
const isAtMaxZoom = () => zoom >= maxZoomRef.current;
const isAtMinZoom = () => zoom <= minZoomRef.current;
const handleMouseDown = (e: React.MouseEvent<SVGSVGElement>) => {
if (e.button !== 0) return; // Only handle left mouse button
setIsDragging(true);
setHasMoved(false);
const { x, y } = getEventPosition(e);
dragStartPosition.current = { x, y };
setStartPosition({
x: x - position.x,
y: y - position.y,
});
lastPanTime.current = Date.now();
panVelocity.current = { x: 0, y: 0 };
};
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isDragging) return;
const { x, y } = getEventPosition(e);
// Check if we've moved beyond the threshold
const distanceMoved = Math.hypot(
x - dragStartPosition.current.x,
y - dragStartPosition.current.y
);
if (distanceMoved > dragThreshold) {
setHasMoved(true);
}
const currentTime = Date.now();
const deltaTime = currentTime - lastPanTime.current;
const newPosition = {
x: x - startPosition.x,
y: y - startPosition.y,
};
// Calculate velocity for momentum
if (deltaTime > 0) {
panVelocity.current = {
x: (newPosition.x - position.x) / deltaTime,
y: (newPosition.y - position.y) / deltaTime,
};
}
updatePosition(newPosition);
lastPanTime.current = currentTime;
};
const handleMouseUp = () => {
setIsDragging(false);
// Apply momentum if velocity is significant
const velocityThreshold = 0.1;
if (
Math.abs(panVelocity.current.x) > velocityThreshold ||
Math.abs(panVelocity.current.y) > velocityThreshold
) {
const momentum = {
x: position.x + panVelocity.current.x * 100,
y: position.y + panVelocity.current.y * 100,
};
updatePosition(momentum);
}
// Reset interaction state after a short delay
setTimeout(() => {
setHasMoved(false);
setHasZoomed(false);
}, 200);
};
const handleTouchStart = (e: React.TouchEvent<SVGSVGElement>) => {
if (e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
touch1.clientX - touch2.clientX,
touch1.clientY - touch2.clientY
);
initialTouchDistance.current = distance;
previousTouchDistance.current = distance;
setHasZoomed(false);
return;
}
if (e.touches.length === 1) {
setIsDragging(true);
setHasMoved(false);
const { x, y } = getEventPosition(e);
dragStartPosition.current = { x, y };
setStartPosition({
x: x - position.x,
y: y - position.y,
});
lastPanTime.current = Date.now();
panVelocity.current = { x: 0, y: 0 };
}
};
const handleTouchMove = (e: React.TouchEvent<SVGSVGElement>) => {
if (!rootRef.current) return;
// setHasMoved(true);
// setHasZoomed(true);
if (e.touches.length === 2) {
e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
touch1.clientX - touch2.clientX,
touch1.clientY - touch2.clientY
);
if (initialTouchDistance.current === null) {
initialTouchDistance.current = distance;
previousTouchDistance.current = distance;
return;
}
if (previousTouchDistance.current === null) {
previousTouchDistance.current = distance;
return;
}
const distanceChange = distance - previousTouchDistance.current;
const minDistanceChange = 5; // минимальное изменение в пикселях
if (Math.abs(distanceChange) >= minDistanceChange) {
// Zoom based on distance change, not time
const zoomSensitivity = 0.003; // увеличенная чувствительность зума
const zoomFactor = 1 + distanceChange * zoomSensitivity;
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
const newZoom = Math.min(
maxZoomRef.current,
Math.max(minZoomRef.current, zoom * zoomFactor)
);
// Prevent zoom if at limits or change is too small
if (
Math.abs(newZoom - zoom) > 0.005 &&
!(
(zoomFactor > 1 && isAtMaxZoom()) ||
(zoomFactor < 1 && isAtMinZoom())
)
) {
setHasZoomed(true);
setHasMoved(true);
setZoom(newZoom);
const containerRect = rootRef.current.getBoundingClientRect();
const mouseX = centerX - containerRect.left;
const mouseY = centerY - containerRect.top;
const scale = newZoom / zoom;
const dx = mouseX - position.x;
const dy = mouseY - position.y;
const newPosition = {
x: mouseX - dx * scale,
y: mouseY - dy * scale,
};
updatePosition(newPosition, newZoom);
// Update previous distance only after successful zoom
previousTouchDistance.current = distance;
}
}
return;
}
if (isDragging && e.touches.length === 1) {
const { x, y } = getEventPosition(e);
// Check if we've moved beyond the threshold
const distanceMoved = Math.hypot(
x - dragStartPosition.current.x,
y - dragStartPosition.current.y
);
if (distanceMoved > dragThreshold) {
setHasMoved(true);
}
const currentTime = Date.now();
const deltaTime = currentTime - lastPanTime.current;
const newPosition = {
x: x - startPosition.x,
y: y - startPosition.y,
};
// Calculate velocity for momentum
if (deltaTime > 0) {
panVelocity.current = {
x: (newPosition.x - position.x) / deltaTime,
y: (newPosition.y - position.y) / deltaTime,
};
}
// Удаляем неиспользуемые переменные и console.log
updatePosition(newPosition);
lastPanTime.current = currentTime;
}
};
const handleTouchEnd = () => {
if (isDragging) {
// Apply momentum if velocity is significant
const velocityThreshold = 0.1;
if (
Math.abs(panVelocity.current.x) > velocityThreshold ||
Math.abs(panVelocity.current.y) > velocityThreshold
) {
const momentum = {
x: position.x + panVelocity.current.x * 100,
y: position.y + panVelocity.current.y * 100,
};
updatePosition(momentum);
}
}
setIsDragging(false);
previousTouchDistance.current = null;
initialTouchDistance.current = null;
// Reset interaction state after a longer delay to prevent accidental clicks after zoom
setTimeout(() => {
setHasMoved(false);
setHasZoomed(false);
}, 200);
};
const handleWheel = (e: WheelEvent) => {
e.preventDefault();
if (!rootRef.current) return;
const containerRect = rootRef.current.getBoundingClientRect();
const mouseX = e.clientX - containerRect.left;
const mouseY = e.clientY - containerRect.top;
// Enhanced zoom with better sensitivity
const zoomSensitivity = 0.001;
const zoomFactor = 1 - e.deltaY * zoomSensitivity;
const newZoom = Math.min(
maxZoomRef.current,
Math.max(minZoomRef.current, zoom * zoomFactor)
);
// Prevent zoom if at limits or change is too small
if (
Math.abs(newZoom - zoom) < 0.005 ||
(zoomFactor > 1 && isAtMaxZoom()) ||
(zoomFactor < 1 && isAtMinZoom())
) {
return;
}
const scale = newZoom / zoom;
const dx = mouseX - position.x;
const dy = mouseY - position.y;
const newPosition = {
x: mouseX - dx * scale,
y: mouseY - dy * scale,
};
setZoom(newZoom);
updatePosition(newPosition, newZoom);
};
// Reset zoom and position
const resetView = () => {
if (!rootRef.current) return;
const { width, height } = rootRef.current.getBoundingClientRect();
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
const centerPosition = calculateCenterPosition(
{ width, height },
originalSize,
newMinZoom // Сбрасываем к минимальному зуму (изображение на всю высоту)
);
setZoom(newMinZoom);
setImagePosition(centerPosition);
};
// Initialize and update container size on resize
useEffect(() => {
const updateContainerSize = () => {
if (!rootRef.current) return;
const { width, height } = rootRef.current.getBoundingClientRect();
if (
width === containerSizeRef.current.width &&
height === containerSizeRef.current.height
)
return;
containerSizeRef.current = { width, height };
// Вычисляем минимальный зум на основе высоты контейнера
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
minZoomRef.current = newMinZoom;
// Устанавливаем максимальный зум как минимальный + 50%
maxZoomRef.current = newMinZoom * maxZoomMultiplier;
// Инициализируем при первой загрузке или если текущий зум меньше нового минимального
if (!isImageLoaded || zoom < newMinZoom) {
const centerPosition = calculateCenterPosition(
{ width, height },
originalSize,
newMinZoom // Используем вычисленный минимальный зум
);
setZoom(newMinZoom);
setImagePosition(centerPosition);
setIsImageLoaded(true);
}
};
// Initial setup
setTimeout(updateContainerSize, 0);
window.addEventListener("resize", updateContainerSize);
return () => {
window.removeEventListener("resize", updateContainerSize);
};
}, [isImageLoaded, zoom]);
// Add wheel event listener (only for mobile)
useEffect(() => {
if (!isMobile) return;
const container = containerRef.current;
if (!container) return;
container.addEventListener("wheel", handleWheel, { passive: false });
return () => {
container.removeEventListener("wheel", handleWheel);
};
}, [zoom, position, isMobile]);
// Add global mouse events for better dragging experience (only for mobile)
useEffect(() => {
if (!isMobile || !isDragging) return;
const handleGlobalMouseMove = (e: MouseEvent) => {
const { x, y } = getEventPosition(e);
const currentTime = Date.now();
const deltaTime = currentTime - lastPanTime.current;
const newPosition = {
x: x - startPosition.x,
y: y - startPosition.y,
};
// Calculate velocity for momentum
if (deltaTime > 0) {
panVelocity.current = {
x: (newPosition.x - position.x) / deltaTime,
y: (newPosition.y - position.y) / deltaTime,
};
}
updatePosition(newPosition);
lastPanTime.current = currentTime;
};
const handleGlobalMouseUp = () => {
handleMouseUp();
};
document.addEventListener("mousemove", handleGlobalMouseMove);
document.addEventListener("mouseup", handleGlobalMouseUp);
return () => {
document.removeEventListener("mousemove", handleGlobalMouseMove);
document.removeEventListener("mouseup", handleGlobalMouseUp);
};
}, [isMobile, isDragging, startPosition, position]);
function handleFloorMouseMove(e: React.MouseEvent<SVGPathElement>) {
const x = e.clientX;
const y = e.clientY;
setPosition({ x, y });
}
const ref = useClickAway<SVGGElement>(() => {
if (isMobile && innerWidth >= 768 && !selectedFloor) {
setPopup(null);
setHoveredFloor(null);
}
});
useEffect(() => {
if (!selectedFloor) setPopup(null);
}, [selectedFloor, setPopup]);
const { data } = useQuery({
queryKey: ["floors-data", complexName],
queryFn: () =>
api.get(`units/get-floors-data/${complexName}`).json<FloorsData[]>(),
});
function handleFloorClick(floor: string) {
if (SPECIAL_FLOORS.includes(floor) || complexName === "marasi-drive") {
onSelect(floor);
} else {
onSelect(floor.split(" ").at(-1)!);
}
}
function openPopup(floor: string, wing?: "West" | "East") {
if (!data) return;
if (isMobile) setSide("bottom");
else setSide(!wing || wing === "East" ? "left" : "right");
if (
data.some(
(floorData) =>
floorData.floor === +floor!.split(" ").at(-1)! ||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0]
) ||
SPECIAL_FLOORS.includes(floor) ||
(["19-20", "23-24", "27-28"].includes(floor) && complexName === "hq")
)
setPopup(
<FloorPopup
title={floor}
complexName={complexName}
data={
data.find(
(floorData) =>
floorData.floor === +floor!.split(" ").at(-1)! ||
floorData.floor === +floor!.split(" ").at(-1)!.split("-")[0]
)!
}
onSelect={handleFloorClick}
/>
);
}
const { pathname } = useLocation();
useEffect(() => {
if (!popup) setHoveredFloor(null);
}, [popup]);
useEffect(() => {
setPopup(null);
}, [pathname]);
const imageStyle = {
transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`,
transformOrigin: "0 0",
width: originalSize.width,
height: originalSize.height,
};
return (
<div
className={clsx(
"overflow-hidden h-full w-full relative transition-transform duration-300",
selectedFloor && "2xl:-translate-x-1/4"
)}
ref={rootRef}
>
<div className="overflow-hidden relative w-full h-full">
<svg
ref={containerRef}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
viewBox="0 0 4096 1752"
className={clsx(
"touch-none absolute inset-0 select-none will-change-[opacity,scale,transform] transition-opacity duration-300",
isImageLoaded && originalSize.width !== 0
? "opacity-100"
: "opacity-0"
)}
style={{
cursor: isMobile ? (isDragging ? "grabbing" : "grab") : "default",
...imageStyle,
}}
onMouseDown={isMobile ? handleMouseDown : undefined}
onMouseMove={isMobile ? handleMouseMove : undefined}
onMouseUp={isMobile ? handleMouseUp : undefined}
onTouchStart={isMobile ? handleTouchStart : undefined}
onTouchEnd={isMobile ? handleTouchEnd : undefined}
onTouchCancel={isMobile ? handleTouchEnd : undefined}
onTouchMove={isMobile ? handleTouchMove : undefined}
>
<defs>
<pattern
id={`background-image-${complexName}`}
patternUnits="userSpaceOnUse"
width="4096"
height="1752"
>
<image
href={`/images/${complexName}-floors.jpg`}
x="0"
y="0"
width="4096"
height="1752"
preserveAspectRatio="none"
/>
</pattern>
</defs>
<rect
x="0"
y="0"
width="4096"
height="1752"
fill={`url(#background-image-${complexName})`}
className="pointer-events-none"
/>
<g ref={ref}>
{Object.entries(
floorsMasks[complexName as keyof typeof floorsMasks]
).map(([floorTitle, d]) => (
<path
onMouseMove={!isMobile ? handleFloorMouseMove : undefined}
onClick={() => {
if (!isMobile && !hasMoved && !hasZoomed) {
handleFloorClick(floorTitle);
}
}}
// onTouchStart={(e) => {
// // e.stopPropagation();
// setPosition({
// x: e.touches[0].clientX,
// y: e.touches[0].clientY,
// });
// }}
onTouchEnd={(e) => {
if (isMobile && !hasMoved && !hasZoomed) {
e.stopPropagation();
setHoveredFloor(
SPECIAL_FLOORS.includes(floorTitle) ||
complexName === "marasi-drive"
? floorTitle
: floorTitle.split(" ").at(-1)!
);
openPopup(
floorTitle,
!SPECIAL_FLOORS.includes(floorTitle) &&
complexName === "marasi-drive"
? (floorTitle.split(" ")[0] as "West" | "East")
: undefined
);
}
}}
onMouseEnter={() => {
setHoveredFloor(
SPECIAL_FLOORS.includes(floorTitle) ||
complexName === "marasi-drive"
? floorTitle
: floorTitle.split(" ").at(-1)!
);
if (!isMobile)
openPopup(
floorTitle,
!SPECIAL_FLOORS.includes(floorTitle) &&
complexName === "marasi-drive"
? (floorTitle.split(" ")[0] as "West" | "East")
: undefined
);
}}
onMouseLeave={(e) => {
if (!isMobile) {
const relatedTarget = (e.nativeEvent as MouseEvent)
.relatedTarget;
const movingToAnotherFloor =
relatedTarget instanceof SVGPathElement &&
ref.current?.contains(relatedTarget);
if (!movingToAnotherFloor) {
setPopup(null);
setHoveredFloor(null);
}
}
}}
key={floorTitle}
d={d}
className={clsx(
`fill-[#00BED7] cursor-pointer transition-opacity duration-300 hover:opacity-60 pointer-events-auto`,
selectedFloor ===
(SPECIAL_FLOORS.includes(floorTitle) ||
complexName === "marasi-drive"
? floorTitle
: floorTitle.split(" ").at(-1)!) ||
hoveredFloor ===
(SPECIAL_FLOORS.includes(floorTitle) ||
complexName === "marasi-drive"
? floorTitle
: floorTitle.split(" ").at(-1)!)
? "opacity-60"
: "opacity-20"
)}
/>
))}
{Object.entries(enumerationMasks[complexName]).map(
([floorTitle, mask]) =>
Array.isArray(mask) ? (
mask.map(({ x, y, width, height, d }) => (
<Fragment
key={`${floorTitle}-${x}-${y}-${width}-${height}-${d}`}
>
<rect
x={x}
y={y}
width={width}
height={height}
rx={height / 2}
fillOpacity={0.4}
className={clsx(
"transition-[fill] pointer-events-none",
((hoveredFloor &&
floorTitle ===
(complexName === "dubai-marina"
? hoveredFloor
: SPECIAL_FLOORS.includes(hoveredFloor)
? hoveredFloor
: hoveredFloor.split(" ").at(-1)!)) ||
selectedFloor?.split(" ").at(-1) === floorTitle) &&
"fill-[#00BED7]"
)}
/>
<path d={d} className="fill-white pointer-events-none" />
</Fragment>
))
) : (
// <rect
// x={x[1]}
// y={y[1]}
// width={width[1]}
// height={height[1]}
// rx={complexName === "marasi-drive" ? 14.2 : 10}
// fillOpacity={0.4}
// className={clsx(
// "transition-[fill] pointer-events-none",
// ((hoveredFloor &&
// floorTitle ===
// (complexName === "dubai-marina"
// ? hoveredFloor
// : SPECIAL_FLOORS.includes(hoveredFloor)
// ? hoveredFloor
// : hoveredFloor.split(" ").at(-1)!)) ||
// selectedFloor?.split(" ").at(-1) === floorTitle) &&
// "fill-[#00BED7]"
// )}
// />
// <path d={d[1]} className="fill-white pointer-events-none" />
<Fragment key={floorTitle}>
<rect
x={mask.x}
y={mask.y}
width={mask.width}
height={mask.height}
rx={mask.height / 2}
fillOpacity={0.4}
className={clsx(
"transition-[fill] pointer-events-none",
((hoveredFloor &&
floorTitle ===
(complexName === "dubai-marina"
? hoveredFloor
: SPECIAL_FLOORS.includes(hoveredFloor)
? hoveredFloor
: hoveredFloor.split(" ").at(-1)!)) ||
selectedFloor?.split(" ").at(-1) === floorTitle) &&
"fill-[#00BED7]"
)}
/>
<path
d={mask.d}
className="fill-white pointer-events-none"
/>
</Fragment>
)
)}
</g>
</svg>
</div>
<Compass imgStyle={{ rotate: "180deg" }} />
<Button
variant="secondary"
size="small"
onlyIcon={innerWidth < 768}
className="!bg-white absolute 2xl:left-[2.222vw] 2xl:top-[2.222vw] md:max-2xl:left-6 md:max-2xl:top-6 left-4 top-4"
onClick={() => navigate(`/complex/${complexName}`)}
>
<span className="2xl:size-[1.389vw] size-5">
<ArrowLeftIcon />
</span>
<span className="max-md:hidden">Project view</span>
</Button>
<Button
variant="secondary"
size="small"
onlyIcon={innerWidth < 768}
onClick={() => navigate(`/complex/${complexName}/about`)}
className="absolute 2xl:right-[2.222vw] 2xl:top-[2.222vw] md:max-2xl:right-6 md:max-2xl:top-6 right-4 top-4"
>
<span className="2xl:size-[1.389vw] size-5">
<InfoIcon />
</span>
<span className="max-md:hidden">About</span>
</Button>
{/* Reset zoom button (only for mobile) */}
{isMobile && (
<Button
variant="secondary"
size="small"
onlyIcon={true}
onClick={resetView}
className="absolute 2xl:right-[2.222vw] 2xl:bottom-[2.222vw] md:max-2xl:right-6 md:max-2xl:bottom-6 right-4 bottom-4"
title="Reset view"
>
<span className="2xl:size-[1.389vw] size-5">🔄</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-6 top-5 drop-shadow pointer-events-none">
{markers.find((marker) => marker.name === complexName)?.title}
</p>
<ButtonGroup />
</div>
);
}
export default FloorSelect;