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:
2026-04-20 17:45:41 +05:00
parent 934023cddf
commit 9b69767b5b
15 changed files with 339 additions and 176 deletions
+13 -2
View File
@@ -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}
/>
);
}
+6 -4
View File
@@ -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}
+48 -14
View File
@@ -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>
</>
)}
+74 -46
View File
@@ -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
+12 -6
View File
@@ -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 (
+6 -4
View File
@@ -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" />
+20 -2
View File
@@ -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" />
+9 -8
View File
@@ -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
)}
>
+28 -2
View File
@@ -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 />
+10
View File
@@ -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
View File
@@ -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,
// },
],
];
+7 -1
View File
@@ -2,6 +2,7 @@ export type UnitType =
| "Studio"
| "Onebedroom"
| "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
View File
@@ -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 youve 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",
+42 -40
View File
@@ -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,
+17
View File
@@ -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[]);