Enhance UnitFilters and BlockPage functionality; add Hypermarket filter support and improve mobile interactions. Update NavMenu links and adjust FullscreenButton state management.
This commit is contained in:
@@ -9,7 +9,14 @@ import FiltersPopup from "./popups/FiltersPopup";
|
||||
import { useSearchParams } from "react-router";
|
||||
import clsx from "clsx";
|
||||
|
||||
function UnitFilters({ className }: { className?: string }) {
|
||||
function UnitFilters({
|
||||
className,
|
||||
showHypermarketFilter,
|
||||
}: {
|
||||
className?: string;
|
||||
/** Только для MasterplanPage */
|
||||
showHypermarketFilter?: boolean;
|
||||
}) {
|
||||
const { selectedUnitTypes, hasActiveFilters, setFiltersFromURLParams } =
|
||||
useUnitFiltersStore();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -67,7 +74,11 @@ function UnitFilters({ className }: { className?: string }) {
|
||||
setSide("right");
|
||||
setHasBackdrop(true);
|
||||
setPopup(
|
||||
<FiltersPopup onUpdateURL={updateURLParams} onResetURL={resetURLParams} />
|
||||
<FiltersPopup
|
||||
onUpdateURL={updateURLParams}
|
||||
onResetURL={resetURLParams}
|
||||
showHypermarketFilter={showHypermarketFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,13 +48,14 @@ function NavMenu() {
|
||||
Surroundings
|
||||
</MenuLink>
|
||||
<MenuLink
|
||||
to="/about"
|
||||
target="_blank"
|
||||
to="https://barahatown.com"
|
||||
className={({ isActive }) => clsx(!isActive && "max-md:hidden")}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<InfoIcon />
|
||||
</div>
|
||||
About
|
||||
Home
|
||||
</MenuLink>
|
||||
<MenuLink
|
||||
to="/contacts"
|
||||
@@ -108,14 +109,15 @@ function NavMenu() {
|
||||
</MenuLink>
|
||||
<MenuLink
|
||||
highlighted={false}
|
||||
to="/about"
|
||||
to="https://barahatown.com"
|
||||
target="_blank"
|
||||
className="[&>button]:w-full [&>button]:justify-start"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<InfoIcon />
|
||||
</div>
|
||||
About
|
||||
Home
|
||||
</MenuLink>
|
||||
<MenuLink
|
||||
highlighted={false}
|
||||
|
||||
@@ -8,6 +8,7 @@ import FullscreenButton from "../ui/FullscreenButton";
|
||||
import {
|
||||
blockMasks,
|
||||
unitLabelWidths,
|
||||
unitLegendLabel,
|
||||
type Unit,
|
||||
} from "../../consts/blocksMasks";
|
||||
import { usePopupStore } from "../../stores/usePopupStore";
|
||||
@@ -16,11 +17,12 @@ import { useUnitFiltersStore } from "../../stores/useUnitFiltersStore";
|
||||
import { UNIT_TYPE_MAPPING, type UnitTypeKey } from "../../consts/blocksStats";
|
||||
import { unitTypeByLabel } from "../../utils/unitTypeToLabel";
|
||||
import UnitFilters from "../UnitFilters";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import VirtualDTourIcon from "../icons/VirtualDTourIcon";
|
||||
|
||||
function BlockPage() {
|
||||
const { blockNumber } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [floor, setFloor] = useState<"ground" | "first">("ground");
|
||||
|
||||
@@ -63,14 +65,33 @@ function BlockPage() {
|
||||
setAllowPointerEventsThrough(false);
|
||||
}, [floor, setPopup, setAllowPointerEventsThrough]);
|
||||
|
||||
const { selectedUnitTypes, hasActiveFilters, setFiltersFromURLParams } =
|
||||
useUnitFiltersStore();
|
||||
const {
|
||||
selectedUnitTypes,
|
||||
hasActiveFilters,
|
||||
setFiltersFromURLParams,
|
||||
removeFilterKeys,
|
||||
} = useUnitFiltersStore();
|
||||
|
||||
// Инициализация фильтров из URL при монтировании
|
||||
// Hypermarket только на мастерплане — при заходе на блок сбрасываем и из стора, и из ?units=
|
||||
useEffect(() => {
|
||||
const units = searchParams.get("units");
|
||||
if (units) setFiltersFromURLParams(units.split(","));
|
||||
}, [searchParams, setFiltersFromURLParams]);
|
||||
removeFilterKeys(["Hypermarket"]);
|
||||
|
||||
const unitsParam = searchParams.get("units");
|
||||
const keysFromUrl = unitsParam ? unitsParam.split(",").filter(Boolean) : [];
|
||||
const cleaned = keysFromUrl.filter((k) => k !== "Hypermarket");
|
||||
|
||||
if (cleaned.length !== keysFromUrl.length) {
|
||||
if (cleaned.length === 0) setSearchParams({});
|
||||
else setSearchParams({ units: cleaned.join(",") });
|
||||
}
|
||||
|
||||
if (unitsParam) setFiltersFromURLParams(cleaned);
|
||||
}, [
|
||||
searchParams,
|
||||
setFiltersFromURLParams,
|
||||
setSearchParams,
|
||||
removeFilterKeys,
|
||||
]);
|
||||
|
||||
function isUnitActive(unitTypeKey: UnitTypeKey) {
|
||||
return UNIT_TYPE_MAPPING[unitTypeKey].some((type) =>
|
||||
@@ -178,12 +199,25 @@ function BlockPage() {
|
||||
d={unit.path}
|
||||
fill="#fff"
|
||||
fillOpacity={currentUnit === index ? 1 : 0.7}
|
||||
onMouseEnter={(e) => handleUnitMouseEnter(e, unit, index)}
|
||||
onMouseLeave={() => {
|
||||
setCurrentUnit(null);
|
||||
setPopup(null);
|
||||
setAllowPointerEventsThrough(false);
|
||||
}}
|
||||
onMouseEnter={
|
||||
isMobile
|
||||
? undefined
|
||||
: (e) => handleUnitMouseEnter(e, unit, index)
|
||||
}
|
||||
onClick={
|
||||
isMobile
|
||||
? (e) => handleUnitMouseEnter(e, unit, index)
|
||||
: undefined
|
||||
}
|
||||
onMouseLeave={
|
||||
isMobile
|
||||
? undefined
|
||||
: () => {
|
||||
setCurrentUnit(null);
|
||||
setPopup(null);
|
||||
setAllowPointerEventsThrough(false);
|
||||
}
|
||||
}
|
||||
className={clsx(
|
||||
"transition-[fill-opacity,opacity]",
|
||||
isUnitActive(unitTypeByLabel(unit.type)) ||
|
||||
@@ -250,7 +284,7 @@ function BlockPage() {
|
||||
y={unit.labelPosition.y + 2}
|
||||
className="translate-y-[10px] px-1.5 py-0.5 rounded-[20px] fill-[#A7A08E] leading-[120%] text-[10px] pointer-events-none"
|
||||
>
|
||||
{unit.type}
|
||||
{unitLegendLabel(unit.type)}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import Button from "../ui/Button";
|
||||
import gsap from "gsap";
|
||||
import ChevronLeftIcon from "../icons/ChevronLeftIcon";
|
||||
@@ -12,7 +12,10 @@ import { usePopupStore } from "../../stores/usePopupStore";
|
||||
import { sequencePoints } from "../../consts/blocksPoints";
|
||||
import { clsx } from "clsx";
|
||||
import BlockPopup from "../popups/BlockPopup";
|
||||
import { blocksStats } from "../../consts/blocksStats";
|
||||
import {
|
||||
blocksStats,
|
||||
FACILITY_ONLY_UNIT_TYPES,
|
||||
} from "../../consts/blocksStats";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import FacilityPopup from "../popups/FacilitiesPopup";
|
||||
import { facilities } from "../../consts/facilities";
|
||||
@@ -125,6 +128,9 @@ function calculateCenterPosition(
|
||||
const FRAME_COUNT = 120;
|
||||
const FRAME_STEP = 30;
|
||||
|
||||
/** После закрытия попапа на тач-устройствах приходит синтетический click по маске под попапом — без подавления снова включается подсветка блока. */
|
||||
const MOBILE_BLOCK_CLICK_SUPPRESS_MS = 450;
|
||||
|
||||
function MasterplanPage() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
@@ -135,6 +141,8 @@ function MasterplanPage() {
|
||||
|
||||
const directionRef = useRef<"next" | "prev">("next");
|
||||
const loadedImagesRef = useRef<Set<number>>(new Set());
|
||||
const blockClickSuppressUntilRef = useRef(0);
|
||||
const prevPopupRef = useRef<React.ReactNode | null>(null);
|
||||
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
@@ -146,6 +154,12 @@ function MasterplanPage() {
|
||||
function blockHasSelectedUnits(blockIndex: number) {
|
||||
if (!hasActiveFilters()) return true; // Если фильтры не активны, показываем все блоки
|
||||
|
||||
const hasBlockUnitFilter = [...selectedUnitTypes].some(
|
||||
(t) => !FACILITY_ONLY_UNIT_TYPES.has(t)
|
||||
);
|
||||
// Только инфраструктурные фильтры (напр. Hypermarket) — маски блоков не скрываем
|
||||
if (!hasBlockUnitFilter) return true;
|
||||
|
||||
const blockStats = blocksStats[blockIndex];
|
||||
for (const [unitType] of blockStats.entries())
|
||||
if (selectedUnitTypes.has(unitType)) return true;
|
||||
@@ -153,6 +167,11 @@ function MasterplanPage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Индекс 0 в facilitiesPoints соответствует Hypermarket в facilities */
|
||||
function isFacilityFilterHighlight(index: number) {
|
||||
return index === 0 && selectedUnitTypes.has("Hypermarket");
|
||||
}
|
||||
|
||||
function handleImageLoad(index: number) {
|
||||
if (loadedImagesRef.current.has(index)) return;
|
||||
loadedImagesRef.current.add(index);
|
||||
@@ -671,8 +690,15 @@ function MasterplanPage() {
|
||||
};
|
||||
}, [isDragging, startPosition, position]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!popup && isMobile) setHoveredMaskIndex(null);
|
||||
useLayoutEffect(() => {
|
||||
const hadPopup = prevPopupRef.current != null;
|
||||
prevPopupRef.current = popup;
|
||||
|
||||
if (hadPopup && !popup && isMobile) {
|
||||
setHoveredMaskIndex(null);
|
||||
blockClickSuppressUntilRef.current =
|
||||
Date.now() + MOBILE_BLOCK_CLICK_SUPPRESS_MS;
|
||||
}
|
||||
}, [popup, isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -692,7 +718,7 @@ function MasterplanPage() {
|
||||
setHoveredMaskIndex(index);
|
||||
setSide("right");
|
||||
setAlign("center");
|
||||
setAllowPointerEventsThrough(true);
|
||||
setAllowPointerEventsThrough(!isMobile);
|
||||
setParentBoundingClientRect(e.currentTarget.getBoundingClientRect());
|
||||
setPopup(
|
||||
<BlockPopup
|
||||
@@ -723,12 +749,18 @@ function MasterplanPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMobile && Date.now() < blockClickSuppressUntilRef.current) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// На мобильных устройствах показываем попап рядом с блоком
|
||||
if (isMobile) {
|
||||
setHoveredMaskIndex(index);
|
||||
setSide("right");
|
||||
setAlign("center");
|
||||
setAllowPointerEventsThrough(true);
|
||||
setAllowPointerEventsThrough(false);
|
||||
setParentBoundingClientRect(e.currentTarget.getBoundingClientRect());
|
||||
setPopup(
|
||||
<BlockPopup
|
||||
@@ -760,6 +792,13 @@ function MasterplanPage() {
|
||||
setParentBoundingClientRect(null);
|
||||
}
|
||||
|
||||
function openFacilityVideoModal() {
|
||||
setPopup(null);
|
||||
setParentBoundingClientRect(null);
|
||||
setHasBackdrop(false);
|
||||
setModal(<VideoModal />);
|
||||
}
|
||||
|
||||
function showFacilityPopup(
|
||||
index: number,
|
||||
e: React.MouseEvent | React.TouchEvent
|
||||
@@ -780,14 +819,7 @@ function MasterplanPage() {
|
||||
caption={facilities[index].caption}
|
||||
description={facilities[index].description}
|
||||
showVideoButton={isLastPoint}
|
||||
onPlayVideo={
|
||||
isLastPoint
|
||||
? () => {
|
||||
setPopup(null);
|
||||
setModal(<VideoModal />);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onPlayVideo={isLastPoint ? openFacilityVideoModal : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -838,22 +870,6 @@ function MasterplanPage() {
|
||||
draggable={false}
|
||||
/>
|
||||
))}
|
||||
{/* <AnimatePresence>
|
||||
{isShowVideo && (
|
||||
<motion.img
|
||||
key={currentIndex}
|
||||
src={`/img/masterplan/posters-${
|
||||
innerWidth >= 768 ? "desktop" : "mobile"
|
||||
}/${currentFrame}.jpg`}
|
||||
alt=""
|
||||
className="object-cover absolute inset-0 w-full h-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence> */}
|
||||
</div>
|
||||
|
||||
{/* SVG layer for masks and interactive elements */}
|
||||
@@ -915,7 +931,6 @@ function MasterplanPage() {
|
||||
exit={{ opacity: 0 }}
|
||||
key={`${currentFrame}-${index}`}
|
||||
style={{
|
||||
// display: !blockHasSelectedUnits(index) ? "none" : "block",
|
||||
scale: 1 / zoom,
|
||||
transformOrigin: `${
|
||||
x - Math.max(0.00694 * innerWidth, 10) / 2
|
||||
@@ -934,10 +949,7 @@ function MasterplanPage() {
|
||||
: "rgba(50, 77, 67, 0.15)"
|
||||
}
|
||||
strokeWidth={2}
|
||||
className={clsx(
|
||||
"cursor-pointer transition-[fill,stroke] duration-300"
|
||||
// !blockHasSelectedUnits(index) && "hidden"
|
||||
)}
|
||||
className="cursor-pointer transition-[fill,stroke] duration-300"
|
||||
onMouseEnter={(e) => {
|
||||
if (index > 15) handleBlockMouseEnter(e, index);
|
||||
}}
|
||||
@@ -976,8 +988,9 @@ function MasterplanPage() {
|
||||
<AnimatePresence>
|
||||
{isShowVideo &&
|
||||
hoveredMaskIndex === null &&
|
||||
facilitiesPoints[currentFrame].map(
|
||||
({ x, y, icon }, index, { length }) => (
|
||||
facilitiesPoints[currentFrame].map(({ x, y, icon }, index) => {
|
||||
const facilityHighlight = isFacilityFilterHighlight(index);
|
||||
return (
|
||||
<motion.g
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -985,7 +998,6 @@ function MasterplanPage() {
|
||||
key={`facility-${currentFrame}-${index}`}
|
||||
style={{
|
||||
scale: 1 / zoom,
|
||||
// transform: `scale(${1 / zoom})`,
|
||||
transformOrigin: `${
|
||||
x - Math.max(0.02222 * innerWidth, 32) / 2
|
||||
}px ${y - Math.max(0.02222 * innerWidth, 32) / 2}px`,
|
||||
@@ -997,21 +1009,29 @@ function MasterplanPage() {
|
||||
width={Math.max(0.02222 * innerWidth, 32)}
|
||||
height={Math.max(0.02222 * innerWidth, 32)}
|
||||
rx={8}
|
||||
fill="white"
|
||||
stroke="rgba(50, 77, 67, 0.15)"
|
||||
fill={facilityHighlight ? "#F47F52" : "white"}
|
||||
stroke={
|
||||
facilityHighlight ? "#ffffff" : "rgba(50, 77, 67, 0.15)"
|
||||
}
|
||||
strokeWidth={1}
|
||||
className="cursor-pointer"
|
||||
onMouseEnter={(e) => {
|
||||
if (!isMobile)
|
||||
if (!isMobile && index !== facilities.length - 1)
|
||||
showFacilityPopup(
|
||||
index,
|
||||
e as unknown as React.MouseEvent
|
||||
);
|
||||
}}
|
||||
onMouseLeave={
|
||||
index === length - 1 ? undefined : handleFacilityLeave
|
||||
index === facilities.length - 1
|
||||
? undefined
|
||||
: handleFacilityLeave
|
||||
}
|
||||
onClick={(e) => {
|
||||
if (index === facilities.length - 1) {
|
||||
openFacilityVideoModal();
|
||||
return;
|
||||
}
|
||||
if (isMobile)
|
||||
showFacilityPopup(
|
||||
index,
|
||||
@@ -1026,11 +1046,19 @@ function MasterplanPage() {
|
||||
height={Math.max(0.01111 * innerWidth, 16)}
|
||||
style={{ pointerEvents: "none" }}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">{icon}</div>
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-[1.111vw] size-4 flex items-center justify-center",
|
||||
facilityHighlight &&
|
||||
"text-white [&_svg]:text-white [&_*]:stroke-white"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</motion.g>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -1062,7 +1090,7 @@ function MasterplanPage() {
|
||||
className="absolute 2xl:bottom-[1.111vw] bottom-4 2xl:left-[1.111vw] left-4"
|
||||
/>
|
||||
<FullscreenButton className="absolute 2xl:bottom-[1.111vw] bottom-4 2xl:right-[1.111vw] right-4 z-10" />
|
||||
<UnitFilters />
|
||||
<UnitFilters showHypermarketFilter />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { UnitType } from "../../consts/blocksStats";
|
||||
import {
|
||||
FACILITY_ONLY_UNIT_TYPES,
|
||||
type UnitType,
|
||||
} from "../../consts/blocksStats";
|
||||
import Button from "../ui/Button";
|
||||
import { useUnitFiltersStore } from "../../stores/useUnitFiltersStore";
|
||||
import clsx from "clsx";
|
||||
@@ -17,13 +20,16 @@ function BlockPopup({
|
||||
}) {
|
||||
const { setPopup, setHasBackdrop } = usePopupStore();
|
||||
|
||||
const { selectedUnitTypes: selectedUnits, hasActiveFilters } =
|
||||
useUnitFiltersStore();
|
||||
const { selectedUnitTypes: selectedUnits } = useUnitFiltersStore();
|
||||
|
||||
// Функция для определения, должен ли юнит быть подсвечен оранжевым
|
||||
/** Есть ли выбранные фильтры, влияющие на состав юнитов блока (не только Hypermarket и др. facility-only). */
|
||||
const hasActiveBlockUnitFilters = (): boolean =>
|
||||
[...selectedUnits].some((t) => !FACILITY_ONLY_UNIT_TYPES.has(t));
|
||||
|
||||
// Если для блока фильтров нет — все лейблы и счётчики оранжевые (в т.ч. на мобилке при только Hypermarket на мастерплане).
|
||||
const isUnitHighlighted = (unitType: UnitType): boolean => {
|
||||
if (!hasActiveFilters()) return true; // Если фильтры не активны, все юниты оранжевые
|
||||
return selectedUnits.has(unitType); // Иначе только выбранные
|
||||
if (!hasActiveBlockUnitFilters()) return true;
|
||||
return selectedUnits.has(unitType);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isMobile } from "react-device-detect";
|
||||
|
||||
export interface FacilitiesPopupProps {
|
||||
title: string;
|
||||
caption: string;
|
||||
caption?: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
showVideoButton?: boolean;
|
||||
@@ -37,17 +37,19 @@ function FacilityPopup({
|
||||
setPopup(null);
|
||||
setHasBackdrop(false);
|
||||
}}
|
||||
className="absolute top-1 right-1 max-md:hidden"
|
||||
className="max-md:hidden absolute top-1 right-1"
|
||||
>
|
||||
<div className="size-4 text-current">
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex gap-2 justify-between items-start">
|
||||
<div className="2xl:space-y-[0.556vw] space-y-2 flex-1">
|
||||
<p className="subheadline-m [font-family:New_York_Large]">{title}</p>
|
||||
<p className="caption font-medium text-[#A7A08E]">{caption}</p>
|
||||
{caption && (
|
||||
<p className="caption font-medium text-[#A7A08E]">{caption}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<hr className="border-[#ECE8DF] 2xl:h-[0.069vw] h-px" />
|
||||
|
||||
@@ -10,9 +10,15 @@ import { useClickAway } from "@uidotdev/usehooks";
|
||||
interface FiltersPopupProps {
|
||||
onUpdateURL: () => void;
|
||||
onResetURL: () => void;
|
||||
/** Только на MasterplanPage — фильтр точки Hypermarket на карте */
|
||||
showHypermarketFilter?: boolean;
|
||||
}
|
||||
|
||||
function FiltersPopup({ onUpdateURL, onResetURL }: FiltersPopupProps) {
|
||||
function FiltersPopup({
|
||||
onUpdateURL,
|
||||
onResetURL,
|
||||
showHypermarketFilter = false,
|
||||
}: FiltersPopupProps) {
|
||||
const { parentBoundingClientRect, setPopup, setHasBackdrop } =
|
||||
usePopupStore();
|
||||
|
||||
@@ -134,8 +140,20 @@ function FiltersPopup({ onUpdateURL, onResetURL }: FiltersPopupProps) {
|
||||
isFilterActive("Healthcare") && "!bg-[#F47F52] !text-white"
|
||||
)}
|
||||
>
|
||||
Healthcare
|
||||
Health & Wellness
|
||||
</Button>
|
||||
{showHypermarketFilter && (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => handleFilterClick("Hypermarket")}
|
||||
className={clsx(
|
||||
isFilterActive("Hypermarket") && "!bg-[#F47F52] !text-white"
|
||||
)}
|
||||
>
|
||||
Hypermarket
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<hr className="bg-[#E2DCCF] 2xl:h-[0.069vw] h-px" />
|
||||
|
||||
@@ -6,7 +6,9 @@ import FullscreenIcon from "../icons/FullscreenIcon";
|
||||
import clsx from "clsx";
|
||||
|
||||
function FullscreenButton({ className }: { className?: string }) {
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const [isFullScreen, setIsFullScreen] = useState(() =>
|
||||
typeof document !== "undefined" ? !!document.fullscreenElement : false
|
||||
);
|
||||
|
||||
async function handleFullScreenClick() {
|
||||
try {
|
||||
@@ -26,23 +28,22 @@ function FullscreenButton({ className }: { className?: string }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullScreenChange = () => {
|
||||
setIsFullScreen(!!document.fullscreenElement);
|
||||
};
|
||||
const sync = () => setIsFullScreen(!!document.fullscreenElement);
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullScreenChange);
|
||||
sync();
|
||||
document.addEventListener("fullscreenchange", sync);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullScreenChange);
|
||||
document.removeEventListener("fullscreenchange", sync);
|
||||
};
|
||||
}, [handleFullScreenClick]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleFullScreenClick}
|
||||
className={clsx(
|
||||
"fixeda 2xl:bottom-[1.111vw] bottom-4 2xl:right-[1.111vw] right-4",
|
||||
"fixed 2xl:bottom-[1.111vw] bottom-4 2xl:right-[1.111vw] right-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -30,6 +30,21 @@ function PopupContainer() {
|
||||
});
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const mobileCloseBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
/** Без этого на тач-устройствах после закрытия приходит синтетический click по элементу под попапом (маска блока и т.д.). */
|
||||
useEffect(() => {
|
||||
const btn = mobileCloseBtnRef.current;
|
||||
if (!btn || !popup || !isMobile) return;
|
||||
|
||||
const onTouchEnd = (ev: TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
setPopup(null);
|
||||
};
|
||||
|
||||
btn.addEventListener("touchend", onTouchEnd, { passive: false });
|
||||
return () => btn.removeEventListener("touchend", onTouchEnd);
|
||||
}, [popup, setPopup]);
|
||||
|
||||
function getPopupPosition() {
|
||||
if (parentBoundingClientRect && innerWidth >= 768) {
|
||||
@@ -134,6 +149,11 @@ function PopupContainer() {
|
||||
hasBackdrop ? "md:hidden" : "hidden"
|
||||
)}
|
||||
onClick={() => setPopup(null)}
|
||||
onTouchEnd={(e) => {
|
||||
if (!isMobile) return;
|
||||
e.preventDefault();
|
||||
setPopup(null);
|
||||
}}
|
||||
/>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{popup && (
|
||||
@@ -169,9 +189,15 @@ function PopupContainer() {
|
||||
{popup}
|
||||
{isMobile && (
|
||||
<Button
|
||||
ref={mobileCloseBtnRef}
|
||||
variant="secondary"
|
||||
className="md:hidden absolute top-1 right-1 z-10 pointer-events-auto"
|
||||
onClick={() => setPopup(null)}
|
||||
type="button"
|
||||
className="md:hidden absolute top-1 right-1 z-10 pointer-events-auto touch-manipulation"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPopup(null);
|
||||
}}
|
||||
>
|
||||
<span className="size-4">
|
||||
<CloseIcon />
|
||||
|
||||
@@ -7,6 +7,16 @@ export type UnitTypeLabel =
|
||||
| "Healthcare"
|
||||
| "Nursery";
|
||||
|
||||
/** Подписи легенды на планировке блока (ключ данных остаётся прежним) */
|
||||
export const UNIT_LABEL_DISPLAY = {
|
||||
Healthcare: "Health & Wellness",
|
||||
} as const satisfies Partial<Record<UnitTypeLabel, string>>;
|
||||
|
||||
export function unitLegendLabel(type: UnitTypeLabel): string {
|
||||
const map = UNIT_LABEL_DISPLAY as Partial<Record<UnitTypeLabel, string>>;
|
||||
return map[type] ?? type;
|
||||
}
|
||||
|
||||
export interface Unit {
|
||||
path: string;
|
||||
type: UnitTypeLabel;
|
||||
|
||||
+32
-32
@@ -65,14 +65,14 @@ export const sequencePoints = [
|
||||
y: 1640 + 13.33,
|
||||
},
|
||||
// 17-18
|
||||
{
|
||||
x: 2901.33 + 13.33,
|
||||
y: 597.33 + 13.33,
|
||||
},
|
||||
{
|
||||
x: 3157.33 + 13.33,
|
||||
y: 442.67 + 13.33,
|
||||
},
|
||||
// {
|
||||
// x: 2901.33 + 13.33,
|
||||
// y: 597.33 + 13.33,
|
||||
// },
|
||||
// {
|
||||
// x: 3157.33 + 13.33,
|
||||
// y: 442.67 + 13.33,
|
||||
// },
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -140,14 +140,14 @@ export const sequencePoints = [
|
||||
y: 896 + 13.33,
|
||||
},
|
||||
// 17-18
|
||||
{
|
||||
x: 2381.33 + 13.33,
|
||||
y: 1376 + 13.33,
|
||||
},
|
||||
{
|
||||
x: 2832 + 13.33,
|
||||
y: 1581.33 + 13.33,
|
||||
},
|
||||
// {
|
||||
// x: 2381.33 + 13.33,
|
||||
// y: 1376 + 13.33,
|
||||
// },
|
||||
// {
|
||||
// x: 2832 + 13.33,
|
||||
// y: 1581.33 + 13.33,
|
||||
// },
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -215,14 +215,14 @@ export const sequencePoints = [
|
||||
y: 445.33 + 13.33,
|
||||
},
|
||||
// 17-18
|
||||
{
|
||||
x: 930.67 + 13.33,
|
||||
y: 992 + 13.33,
|
||||
},
|
||||
{
|
||||
x: 488 + 13.33,
|
||||
y: 1104 + 13.33,
|
||||
},
|
||||
// {
|
||||
// x: 930.67 + 13.33,
|
||||
// y: 992 + 13.33,
|
||||
// },
|
||||
// {
|
||||
// x: 488 + 13.33,
|
||||
// y: 1104 + 13.33,
|
||||
// },
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -290,13 +290,13 @@ export const sequencePoints = [
|
||||
y: 984 + 13.33,
|
||||
},
|
||||
// 17-18
|
||||
{
|
||||
x: 1490.67 + 13.33,
|
||||
y: 453.33 + 13.33,
|
||||
},
|
||||
{
|
||||
x: 1293.33 + 13.33,
|
||||
y: 333.33 + 13.33,
|
||||
},
|
||||
// {
|
||||
// x: 1490.67 + 13.33,
|
||||
// y: 453.33 + 13.33,
|
||||
// },
|
||||
// {
|
||||
// x: 1293.33 + 13.33,
|
||||
// y: 333.33 + 13.33,
|
||||
// },
|
||||
],
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ export type UnitType =
|
||||
| "Studio"
|
||||
| "One‑bedroom"
|
||||
| "Office spaces"
|
||||
| "Hypermarket"
|
||||
| "Healthcare service"
|
||||
| "F&B spots"
|
||||
| "F&B units"
|
||||
@@ -19,7 +20,11 @@ export type UnitTypeKey =
|
||||
| "F&B"
|
||||
| "Office spaces"
|
||||
| "Retail"
|
||||
| "Healthcare";
|
||||
| "Healthcare"
|
||||
| "Hypermarket";
|
||||
|
||||
/** Юнит-типы фильтров, которые подсвечивают только маркеры на мастерплане, не квартиры в блоках */
|
||||
export const FACILITY_ONLY_UNIT_TYPES = new Set<UnitType>(["Hypermarket"]);
|
||||
|
||||
export const UNIT_TYPE_MAPPING: Record<UnitTypeKey, UnitType[]> = {
|
||||
Studio: ["Studio"],
|
||||
@@ -36,6 +41,7 @@ export const UNIT_TYPE_MAPPING: Record<UnitTypeKey, UnitType[]> = {
|
||||
"Office spaces": ["Office spaces"],
|
||||
Retail: ["Retail shops", "401 m² retail anchor"],
|
||||
Healthcare: ["Healthcare service"],
|
||||
Hypermarket: ["Hypermarket"],
|
||||
};
|
||||
|
||||
export const blocksStats = [
|
||||
|
||||
+15
-15
@@ -1,21 +1,21 @@
|
||||
export const facilities = [
|
||||
// {
|
||||
// title: "Hypermarket",
|
||||
// caption: "All you need right at your doorsteps.",
|
||||
// description:
|
||||
// "Lulu Hypermarket is the largest in the area, offering more extraordinary product assortments from food, beverages, and household products. ",
|
||||
// },
|
||||
// {
|
||||
// title: "Fitness Club",
|
||||
// caption: "Make space for the things you love at Baraha Town.",
|
||||
// description:
|
||||
// "With everything on your doorstep, living in Baraha Town gives you more time to pursue your passions and curate the life that you’ve dreamed of. Enjoy Life. Stay Fit.",
|
||||
// },
|
||||
{
|
||||
title: "Central Plaza",
|
||||
caption: "The on-site play area provides quality childcare.",
|
||||
title: "Hypermarket",
|
||||
// caption: "All you need right at your doorsteps.",
|
||||
description:
|
||||
"Whether you live or work here, the on-site play area ensures access to quality childcare from our passionate team of early years educators.",
|
||||
"Your favorite shopping destination for great products – perfect for your everyday errands and top groceries selection at Baraha Town.",
|
||||
},
|
||||
{
|
||||
title: "Fitness Club",
|
||||
// caption: "Make space for the things you love at Baraha Town.",
|
||||
description:
|
||||
"To support an active lifestyle, offering residents convenient access to high-quality equipment, training spaces, and wellness-focused amenities within the community.",
|
||||
},
|
||||
{
|
||||
title: "Sahat Al Baraha",
|
||||
caption: "The beating heart of Baraha Town.",
|
||||
description:
|
||||
"The perfect social scene for you to shop, work and dine in the most controlled and cooled environment.",
|
||||
},
|
||||
// {
|
||||
// title: "F&B",
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import BusStopIcon from "../components/icons/BusStopIcon";
|
||||
import GymIcon from "../components/icons/GymIcon";
|
||||
// import FoodIcon from "../components/icons/FoodIcon";
|
||||
import PlayIcon from "../components/icons/PlayIcon";
|
||||
import ShopIcon from "../components/icons/ShopIcon";
|
||||
// import GymIcon from "../components/icons/GymIcon";
|
||||
// import ShopIcon from "../components/icons/ShopIcon";
|
||||
|
||||
export const facilitiesPoints = [
|
||||
[
|
||||
// {
|
||||
// x: 2901.33 + 42.67,
|
||||
// y: 597.33 + 42.67,
|
||||
// icon: <ShopIcon />,
|
||||
// },
|
||||
// {
|
||||
// x: 3157.33 + 42.67,
|
||||
// y: 442.67 + 42.67,
|
||||
// icon: <GymIcon />,
|
||||
// },
|
||||
{
|
||||
x: 2901.33 + 42.67,
|
||||
y: 597.33 + 42.67,
|
||||
icon: <ShopIcon />,
|
||||
},
|
||||
{
|
||||
x: 3157.33 + 42.67,
|
||||
y: 442.67 + 42.67,
|
||||
icon: <GymIcon />,
|
||||
},
|
||||
{
|
||||
x: 1848 + 42.67,
|
||||
y: 837.33 + 42.67,
|
||||
@@ -33,16 +35,16 @@ export const facilitiesPoints = [
|
||||
},
|
||||
],
|
||||
[
|
||||
// {
|
||||
// x: 2381.33 + 42.67,
|
||||
// y: 1376 + 42.67,
|
||||
// icon: <ShopIcon />,
|
||||
// },
|
||||
// {
|
||||
// x: 2832 + 42.67,
|
||||
// y: 1581.33 + 42.67,
|
||||
// icon: <GymIcon />,
|
||||
// },
|
||||
{
|
||||
x: 2381.33 + 42.67,
|
||||
y: 1376 + 42.67,
|
||||
icon: <ShopIcon />,
|
||||
},
|
||||
{
|
||||
x: 2832 + 42.67,
|
||||
y: 1581.33 + 42.67,
|
||||
icon: <GymIcon />,
|
||||
},
|
||||
{
|
||||
x: 1653 + 42.67,
|
||||
y: 770.67 + 42.67,
|
||||
@@ -60,16 +62,16 @@ export const facilitiesPoints = [
|
||||
},
|
||||
],
|
||||
[
|
||||
// {
|
||||
// x: 930.67 + 42.67,
|
||||
// y: 992 + 42.67,
|
||||
// icon: <ShopIcon />,
|
||||
// },
|
||||
// {
|
||||
// x: 488 + 42.67,
|
||||
// y: 1104 + 42.67,
|
||||
// icon: <GymIcon />,
|
||||
// },
|
||||
{
|
||||
x: 930.67 + 42.67,
|
||||
y: 992 + 42.67,
|
||||
icon: <ShopIcon />,
|
||||
},
|
||||
{
|
||||
x: 488 + 42.67,
|
||||
y: 1104 + 42.67,
|
||||
icon: <GymIcon />,
|
||||
},
|
||||
{
|
||||
x: 2314.67 + 42.67,
|
||||
y: 1008 + 42.67,
|
||||
@@ -87,16 +89,16 @@ export const facilitiesPoints = [
|
||||
},
|
||||
],
|
||||
[
|
||||
// {
|
||||
// x: 1490.67 + 42.67,
|
||||
// y: 453.33 + 42.67,
|
||||
// icon: <ShopIcon />,
|
||||
// },
|
||||
// {
|
||||
// x: 1293.33 + 42.67,
|
||||
// y: 333.33 + 42.67,
|
||||
// icon: <GymIcon />,
|
||||
// },
|
||||
{
|
||||
x: 1490.67 + 42.67,
|
||||
y: 453.33 + 42.67,
|
||||
icon: <ShopIcon />,
|
||||
},
|
||||
{
|
||||
x: 1293.33 + 42.67,
|
||||
y: 333.33 + 42.67,
|
||||
icon: <GymIcon />,
|
||||
},
|
||||
{
|
||||
x: 1720 + 42.67,
|
||||
y: 1056 + 42.67,
|
||||
|
||||
@@ -11,6 +11,8 @@ interface UnitFiltersStore {
|
||||
toggleTempUnit: (unit: UnitTypeKey) => void;
|
||||
applyFilters: () => void;
|
||||
resetFilters: () => void;
|
||||
/** Удалить ключи из temp и соответствующие UnitType из применённого набора */
|
||||
removeFilterKeys: (keys: UnitTypeKey[]) => void;
|
||||
setFiltersFromURLParams: (params: string[]) => void;
|
||||
hasActiveFilters: () => boolean;
|
||||
hasTempChanges: () => boolean;
|
||||
@@ -46,6 +48,21 @@ export const useUnitFiltersStore = create<UnitFiltersStore>((set, get) => ({
|
||||
tempSelectedUnits: new Set<UnitTypeKey>(),
|
||||
}),
|
||||
|
||||
removeFilterKeys: (keys: UnitTypeKey[]) =>
|
||||
set((state) => {
|
||||
const newTemp = new Set(state.tempSelectedUnits);
|
||||
const newSelected = new Set(state.selectedUnitTypes);
|
||||
keys.forEach((key) => {
|
||||
newTemp.delete(key);
|
||||
const types = UNIT_TYPE_MAPPING[key];
|
||||
if (types) types.forEach((t) => newSelected.delete(t));
|
||||
});
|
||||
return {
|
||||
tempSelectedUnits: newTemp,
|
||||
selectedUnitTypes: newSelected,
|
||||
};
|
||||
}),
|
||||
|
||||
setFiltersFromURLParams: (params: string[]) => {
|
||||
const unitTypeKeys = new Set<UnitTypeKey>(params as UnitTypeKey[]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user