гзв
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import clsx from "clsx";
|
||||
import { categories } from "../../consts/surroundingPoints";
|
||||
import { categories } from "../consts/surroundingPoints";
|
||||
|
||||
interface CategoryFilterProps {
|
||||
selectedCategories: Set<string>;
|
||||
onToggleCategory: (category: string) => void;
|
||||
}
|
||||
|
||||
function CategoryFilter({
|
||||
function CategoriesFilter({
|
||||
selectedCategories,
|
||||
onToggleCategory,
|
||||
}: CategoryFilterProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap 2xl:gap-[0.556vw] gap-2">
|
||||
<div className="grid grid-cols-2 gap-2 max-md:hidden">
|
||||
{Array.from(categories.entries()).map(([category, color]) => {
|
||||
const isSelected = selectedCategories.has(category);
|
||||
return (
|
||||
@@ -19,22 +19,22 @@ function CategoryFilter({
|
||||
key={category}
|
||||
onClick={() => onToggleCategory(category)}
|
||||
className={clsx(
|
||||
"flex items-center 2xl:gap-[0.556vw] gap-2 2xl:px-0 px-0 2xl:py-[0.556vw] py-2 transition-colors",
|
||||
"flex items-center 2xl:gap-[0.556vw] gap-2 2xl:py-[0.556vw] py-2 transition-opacity outline-none",
|
||||
"hover:opacity-80"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-4 size-4 rounded-full border-2 transition-colors",
|
||||
isSelected ? "border-transparent" : "border-[#E2DCCF]"
|
||||
"2xl:size-[1.111vw] size-4 rounded-full aspect-square transition-colors"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isSelected ? color : "transparent",
|
||||
boxShadow: `0 0 0 0.069vw ${color}`,
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-s [font-family:Poppins] font-normal transition-colors",
|
||||
"text-s transition-colors text-nowrap",
|
||||
isSelected ? "text-[#324D43]" : "text-[#324D43]"
|
||||
)}
|
||||
>
|
||||
@@ -47,4 +47,4 @@ function CategoryFilter({
|
||||
);
|
||||
}
|
||||
|
||||
export default CategoryFilter;
|
||||
export default CategoriesFilter;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type ISurroundingPoint } from "../consts/surroundingPoints";
|
||||
import CategoriesFilter from "./CategoriesFilter";
|
||||
import SurroundingsIcon from "./icons/SurroundingsIcon";
|
||||
import Select from "./ui/Select";
|
||||
|
||||
interface SurroundingsFilterProps {
|
||||
selectedLocation?: string;
|
||||
onSelectLocation: (location: string | undefined) => void;
|
||||
selectedCategories: Set<string>;
|
||||
onToggleCategory: (category: string) => void;
|
||||
filteredPoints: ISurroundingPoint[];
|
||||
}
|
||||
|
||||
function SurroundingsFilter({
|
||||
selectedLocation,
|
||||
onSelectLocation,
|
||||
selectedCategories,
|
||||
onToggleCategory,
|
||||
filteredPoints,
|
||||
}: SurroundingsFilterProps) {
|
||||
return (
|
||||
<div className="2xl:space-y-[1.111vw] space-y-4 absolute top-4 left-4 z-10 bg-[#F7F6F3] 2xl:rounded-[1.111vw] rounded-2xl 2xl:p-4 p-4 2xl:shadow-[0_0.278vw_2.778vw_0_rgba(15,16,17,0.1),0_0.139vw_0.139vw_0_rgba(0,0,0,0.06)] shadow-[0px_4px_40px_0px_rgba(15,16,17,0.1),0px_2px_2px_0px_rgba(0,0,0,0.06)] 2xl:w-[20.556vw] md:w-[296px] w-[93.333vw]">
|
||||
<div className="flex 2xl:gap-[0.556vw] gap-2 items-center">
|
||||
<div className="2xl:space-y-[0.139vw] space-y-0.5 flex-1 2xl:max-w-[16.667vw] md:max-w-[240px] max-w-[84.444vw]">
|
||||
<p className="bg-[#F0EDE6] 2xl:px-[0.833vw] px-3 2xl:py-[0.764vw] py-[11px] 2xl:rounded-t-[0.833vw] 2xl:rounded-b-[0.139vw] rounded-b-[2px] rounded-t-xl text-s">
|
||||
Baraha Town
|
||||
</p>
|
||||
<Select
|
||||
className="2xl:!rounded-t-[0.139vw] !rounded-t-[2px]"
|
||||
options={filteredPoints.map((point) => point.title)}
|
||||
onChange={onSelectLocation}
|
||||
placeholder="Select location on the map"
|
||||
value={selectedLocation}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col 2xl:gap-[0.278vw] gap-1 items-center">
|
||||
<div className="2xl:size-[0.833vw] size-3 rounded-full 2xl:ring-[0.069vw] ring ring-[#A7A08E] aspect-square 2xl:m-[0.139vw] m-0.5" />
|
||||
<div className="2xl:size-[0.139vw] size-0.5 bg-[#A7A08E] rounded-full aspect-square" />
|
||||
<div className="2xl:size-[0.139vw] size-0.5 bg-[#A7A08E] rounded-full aspect-square" />
|
||||
<div className="2xl:size-[0.139vw] size-0.5 bg-[#A7A08E] rounded-full aspect-square" />
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#F47F52]">
|
||||
<SurroundingsIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="border-[#E2DCCF] 2xl:border-[0.069vw] border max-md:hidden" />
|
||||
<CategoriesFilter
|
||||
onToggleCategory={onToggleCategory}
|
||||
selectedCategories={selectedCategories}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SurroundingsFilter;
|
||||
@@ -13,10 +13,7 @@ export default function CentralPlazaAccordeonContent() {
|
||||
events and activities.
|
||||
</p>
|
||||
<hr className="2xl:my-[1.667vw] md:my-[3.125vw] my-[6.667vw] h-[1px] bg-[#ECE8DF]" />
|
||||
<div
|
||||
className="2xl:subheadline-m subheadline-s text-[#324D43] font-[font-family:New_York_Large]
|
||||
max-md:pb-[6.667vw]"
|
||||
>
|
||||
<div className="2xl:subheadline-m subheadline-s text-[#324D43] [font-family:New_York_Large] max-md:pb-[6.667vw]">
|
||||
The charm of the town <br />
|
||||
in one spot.
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function ContemporaryOfficesAccordeonContent() {
|
||||
one of our
|
||||
</div>
|
||||
<div
|
||||
className="number max-md:!text-[17.778vw] 2xl:mb-[0.556vw] md:mb-[1.042vw] mb-[2.222vw] font-[font-family:New_York_Large]
|
||||
className="number max-md:!text-[17.778vw] 2xl:mb-[0.556vw] md:mb-[1.042vw] mb-[2.222vw] [font-family:New_York_Large]
|
||||
"
|
||||
>
|
||||
68
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function CulinaryExperienceAccordeonContent() {
|
||||
The culinary district will be <br /> the home of
|
||||
</div>
|
||||
<div
|
||||
className="number max-md:!text-[17.778vw] 2xl:mb-[0.556vw] md:mb-[1.042vw] mb-[2.222vw] font-[font-family:New_York_Large]
|
||||
className="number max-md:!text-[17.778vw] 2xl:mb-[0.556vw] md:mb-[1.042vw] mb-[2.222vw] [font-family:New_York_Large]
|
||||
"
|
||||
>
|
||||
7,764
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
function CarIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 30 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20.63 0a5.5 5.5 0 0 1 4.976 3.154L28.408 9.1a2.33 2.33 0 0 1 1.259 2.069v11.665a3.5 3.5 0 0 1-3.5 3.5h-1.834a3.5 3.5 0 0 1-3.5-3.5v-.666h-12v.666a3.5 3.5 0 0 1-3.5 3.5H3.5a3.5 3.5 0 0 1-3.5-3.5V11.168c0-.9.511-1.682 1.259-2.07l2.803-5.944A5.5 5.5 0 0 1 9.036 0zM3 22.833a.5.5 0 0 0 .5.5h1.833a.5.5 0 0 0 .5-.5v-.666h-.166a1.5 1.5 0 0 1 0-3H24a1.5 1.5 0 0 1 0 3h-.167v.666a.5.5 0 0 0 .5.5h1.834a.5.5 0 0 0 .5-.5V11.835H3zm5.167-8.666a1.5 1.5 0 0 1 0 3H6.5a1.5 1.5 0 0 1 0-3zm15 0a1.5 1.5 0 0 1 0 3H21.5a1.5 1.5 0 0 1 0-3zM9.037 3a2.5 2.5 0 0 0-2.262 1.434L4.7 8.835h20.269l-2.076-4.401A2.5 2.5 0 0 0 20.63 3z"
|
||||
d="M13.278 8.26v3.888c0 .138-.056.27-.155.367a.53.53 0 0 1-.373.152h-.528a.53.53 0 0 1-.373-.152.5.5 0 0 1-.154-.367v-.518h-7.39v.518c0 .138-.055.27-.154.367a.53.53 0 0 1-.373.152H3.25a.53.53 0 0 1-.373-.152.5.5 0 0 1-.155-.367V8.26l-.656-.16a.53.53 0 0 1-.288-.184.5.5 0 0 1-.111-.32v-.373a.26.26 0 0 1 .077-.183.27.27 0 0 1 .187-.076h.99l1.128-2.956a1.04 1.04 0 0 1 .387-.489 1.06 1.06 0 0 1 .601-.184h5.926c.215 0 .425.064.601.184.177.12.312.29.387.489l1.129 2.956h.99a.26.26 0 0 1 .263.259v.374a.5.5 0 0 1-.111.319.53.53 0 0 1-.288.184zm-9.5.777v1.037c0 .138.055.27.154.367a.53.53 0 0 0 .374.152h1.712a.27.27 0 0 0 .226-.126.26.26 0 0 0 .006-.256q-.637-1.174-2.472-1.174m8.444 0q-1.834 0-2.473 1.174a.256.256 0 0 0 .103.348q.06.034.13.034h1.713c.14 0 .274-.055.373-.152a.5.5 0 0 0 .154-.367zM4.833 4.371 4.01 6.799a.51.51 0 0 0 .072.467.53.53 0 0 0 .428.216h6.98a.54.54 0 0 0 .428-.216.51.51 0 0 0 .073-.467l-.824-2.428z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19.047 6.033a.65.65 0 0 1 .905.934l-8.91 8.64a2.65 2.65 0 0 1-3.704-.015l-3.295-3.245a.65.65 0 0 1 .913-.925l3.294 3.244a1.35 1.35 0 0 0 1.887.007z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckIcon;
|
||||
@@ -0,0 +1,12 @@
|
||||
function MinusIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20 11.35a.65.65 0 0 1 0 1.3H4a.65.65 0 0 1 0-1.3z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MinusIcon;
|
||||
@@ -0,0 +1,12 @@
|
||||
function PlusIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.024 2.016a.65.65 0 0 1 .65.651L8.667 7.35h4.667a.65.65 0 0 1 0 1.3H8.666l-.007 4.685a.65.65 0 0 1-1.3-.003l.006-4.682H2.667a.65.65 0 0 1 0-1.3h4.7l.006-4.684a.65.65 0 0 1 .651-.65"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlusIcon;
|
||||
@@ -0,0 +1,12 @@
|
||||
function SurroundingsIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.28 10.785c.478-.176.99.117 1.123.609.132.485-.155.981-.625 1.16-1.835.702-2.978 1.806-2.978 2.938 0 1.737 2.957 3.672 7.2 3.672s7.2-1.935 7.2-3.672c0-1.132-1.143-2.237-2.977-2.937-.47-.18-.757-.675-.625-1.16.133-.493.645-.785 1.124-.609 2.662.98 4.278 2.733 4.278 4.706C21 18.58 17.046 21 12 21s-9-2.42-9-5.508c0-1.973 1.616-3.726 4.28-4.707M12 3c1.93 0 3.5 1.614 3.5 3.6 0 1.672-1.12 3.07-2.625 3.472v4.398c0 .151-.034.301-.1.437l-.326.668a.5.5 0 0 1-.898 0l-.325-.668a1 1 0 0 1-.101-.437v-4.398C9.619 9.67 8.5 8.272 8.5 6.6 8.5 4.614 10.07 3 12 3m0 1.8c-.965 0-1.75.807-1.75 1.8 0 .992.785 1.8 1.75 1.8s1.75-.808 1.75-1.8c0-.993-.785-1.8-1.75-1.8"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SurroundingsIcon;
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
surroundingPoints,
|
||||
categories,
|
||||
@@ -8,8 +8,12 @@ import {
|
||||
type ISurroundingPoint,
|
||||
} from "../../consts/surroundingPoints";
|
||||
import { usePopupStore } from "../../stores/usePopupStore";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import LocationPopup from "../popups/LocationPopup";
|
||||
import CategoryFilter from "../ui/CategoryFilter";
|
||||
import SurroundingsFilter from "../SurroundingsFilter";
|
||||
import Button from "../ui/Button";
|
||||
import PlusIcon from "../icons/PlusIcon";
|
||||
import MinusIcon from "../icons/MinusIcon";
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
@@ -65,28 +69,23 @@ const calculateMinZoom = (containerSize: Size, imageSize: Size): number => {
|
||||
return Math.max(widthRatio, heightRatio);
|
||||
};
|
||||
|
||||
function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
const [originalSize, setOriginalSize] = useState<Size>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
function SurroundingsPage({ maxZoom = 3 }: MapProps) {
|
||||
const mapRef = useRef<HTMLImageElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const containerSizeRef = useRef<Size>({ width: 0, height: 0 });
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startPosition, setStartPosition] = useState<Position>({ x: 0, y: 0 });
|
||||
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(0);
|
||||
const previousTouchDistance = useRef<number | null>(null);
|
||||
const initialTouchDistance = useRef<number | null>(null);
|
||||
const minZoomRef = useRef<number>(1);
|
||||
const markersContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [hoveredPoint, setHoveredPoint] = useState<ISurroundingPoint | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [originalSize, setOriginalSize] = useState<Size>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startPosition, setStartPosition] = useState<Position>({ x: 0, y: 0 });
|
||||
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(0);
|
||||
const [selectedPoint, setSelectedPoint] = useState<ISurroundingPoint | null>(
|
||||
null
|
||||
);
|
||||
@@ -95,6 +94,7 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(
|
||||
new Set(Array.from(categories.keys()))
|
||||
);
|
||||
|
||||
const { setPopup, setPosition: setPopupPosition, setSide } = usePopupStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,9 +145,6 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
const maxOffsetX = Math.max(0, scaledWidth - width);
|
||||
const maxOffsetY = Math.max(0, scaledHeight - height);
|
||||
|
||||
// const desiredOffsetX = width * 0.35;
|
||||
// const desiredOffsetY = height * 0.58;
|
||||
|
||||
const boundedOffsetX = Math.min(maxOffsetX);
|
||||
const boundedOffsetY = Math.min(maxOffsetY);
|
||||
|
||||
@@ -331,6 +328,33 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
// Проверяем, находится ли событие внутри scrollable контейнера
|
||||
const target = e.target as HTMLElement;
|
||||
let element: HTMLElement | null = target;
|
||||
|
||||
while (element && element !== containerRef.current) {
|
||||
const style = window.getComputedStyle(element);
|
||||
const overflowY = style.overflowY || style.overflow;
|
||||
|
||||
// Если элемент имеет overflow: auto или scroll и может прокручиваться
|
||||
if (
|
||||
(overflowY === "auto" || overflowY === "scroll") &&
|
||||
element.scrollHeight > element.clientHeight
|
||||
) {
|
||||
// Проверяем, может ли элемент прокрутиться в направлении события
|
||||
const canScrollUp = element.scrollTop > 0;
|
||||
const canScrollDown =
|
||||
element.scrollTop < element.scrollHeight - element.clientHeight;
|
||||
|
||||
if ((e.deltaY < 0 && canScrollUp) || (e.deltaY > 0 && canScrollDown)) {
|
||||
// Позволяем элементу прокручиваться
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (!containerRef.current || !mapRef.current) return;
|
||||
@@ -418,6 +442,46 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
requestAnimationFrame(animateZoom);
|
||||
}
|
||||
|
||||
const handleZoomIn = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const centerX = containerRect.left + containerRect.width / 2;
|
||||
const centerY = containerRect.top + containerRect.height / 2;
|
||||
|
||||
const currentZoom = zoom;
|
||||
const zoomStep = 0.5;
|
||||
const targetZoom = Math.min(
|
||||
maxZoom,
|
||||
Math.max(minZoomRef.current, currentZoom + zoomStep)
|
||||
);
|
||||
|
||||
// Если зум уже на максимуме, не делаем ничего
|
||||
if (Math.abs(targetZoom - currentZoom) < 0.01) return;
|
||||
|
||||
smoothZoomTo(targetZoom, { x: centerX, y: centerY });
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const centerX = containerRect.left + containerRect.width / 2;
|
||||
const centerY = containerRect.top + containerRect.height / 2;
|
||||
|
||||
const currentZoom = zoom;
|
||||
const zoomStep = 0.5;
|
||||
const targetZoom = Math.min(
|
||||
maxZoom,
|
||||
Math.max(minZoomRef.current, currentZoom - zoomStep)
|
||||
);
|
||||
|
||||
// Если зум уже на минимуме, не делаем ничего
|
||||
if (Math.abs(targetZoom - currentZoom) < 0.01) return;
|
||||
|
||||
smoothZoomTo(targetZoom, { x: centerX, y: centerY });
|
||||
};
|
||||
|
||||
const handleClick = (
|
||||
e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>
|
||||
) => {
|
||||
@@ -451,6 +515,23 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
containerRef.current?.removeEventListener("wheel", handleWheel);
|
||||
}, [isDragging, position]);
|
||||
|
||||
// Устанавливаем stroke-dasharray для пунктирных линий после рендера
|
||||
useEffect(() => {
|
||||
if (!selectedPoint || !Array.isArray(selectedPoint.path)) return;
|
||||
|
||||
// Находим все пунктирные пути для выбранной точки
|
||||
const svg = document.querySelector('svg[viewBox="0 0 4096 2176"]');
|
||||
if (!svg) return;
|
||||
|
||||
const dashedPaths = svg.querySelectorAll(
|
||||
`path[data-dashed="true"][data-point="${selectedPoint.title}"][data-index="1"]`
|
||||
);
|
||||
dashedPaths.forEach((path) => {
|
||||
const element = path as SVGPathElement;
|
||||
element.setAttribute("stroke-dasharray", "6 6");
|
||||
});
|
||||
}, [selectedPoint]);
|
||||
|
||||
const handlePointMouseEnter = (
|
||||
_e: React.MouseEvent<HTMLDivElement>,
|
||||
point: ISurroundingPoint
|
||||
@@ -464,41 +545,35 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
const screenX = containerRect.left + pointX;
|
||||
const screenY = containerRect.top + pointY;
|
||||
|
||||
setHoveredPoint(point);
|
||||
setPopupPosition({ x: screenX, y: screenY });
|
||||
setSide("top");
|
||||
setPopup(
|
||||
<LocationPopup title={point.title} travelTime={point.travelTime} />
|
||||
);
|
||||
|
||||
// Определяем сторону для попапа
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (screenX < windowWidth / 2) {
|
||||
setSide("right");
|
||||
} else {
|
||||
setSide("left");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointMouseLeave = () => {
|
||||
setHoveredPoint(null);
|
||||
setPopup(null);
|
||||
};
|
||||
|
||||
const handleToggleCategory = (category: string) => {
|
||||
const newSelected = new Set(selectedCategories);
|
||||
if (newSelected.has(category)) {
|
||||
newSelected.delete(category);
|
||||
} else {
|
||||
newSelected.add(category);
|
||||
}
|
||||
setSelectedCategories(newSelected);
|
||||
if (selectedCategories.has(category)) selectedCategories.delete(category);
|
||||
else selectedCategories.add(category);
|
||||
|
||||
setSelectedCategories(new Set(selectedCategories));
|
||||
};
|
||||
|
||||
const filteredPoints = surroundingPoints.filter((point) =>
|
||||
selectedCategories.has(point.category)
|
||||
);
|
||||
|
||||
// Сбрасываем выбранный поинт, если его категория отключена
|
||||
useEffect(() => {
|
||||
if (selectedPoint && !selectedCategories.has(selectedPoint.category)) {
|
||||
setSelectedPoint(null);
|
||||
}
|
||||
}, [selectedCategories, selectedPoint]);
|
||||
|
||||
const handlePointClick = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
point: ISurroundingPoint
|
||||
@@ -519,26 +594,6 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
// <div className="relative h-full w-full">
|
||||
// {/* Переключатель категорий */}
|
||||
// <div className="absolute top-4 left-4 z-10 2xl:max-w-[296px] max-w-[296px] bg-[#F7F6F3] 2xl:rounded-2xl rounded-2xl 2xl:p-4 p-4 2xl:shadow-[0px_4px_40px_0px_rgba(15,16,17,0.1),0px_2px_2px_0px_rgba(0,0,0,0.06)] shadow-[0px_4px_40px_0px_rgba(15,16,17,0.1),0px_2px_2px_0px_rgba(0,0,0,0.06)]">
|
||||
// <div className="2xl:space-y-4 space-y-4">
|
||||
// <div className="2xl:space-y-2 space-y-2">
|
||||
// <p className="text-s [font-family:Poppins] font-normal text-[#324D43]">
|
||||
// Baraha Town
|
||||
// </p>
|
||||
// <p className="text-s [font-family:Poppins] font-normal text-[rgba(50,77,67,0.6)]">
|
||||
// Select location on the map
|
||||
// </p>
|
||||
// </div>
|
||||
// <hr className="border-[#E2DCCF] border-[1px]" />
|
||||
// <CategoryFilter
|
||||
// selectedCategories={selectedCategories}
|
||||
// onToggleCategory={handleToggleCategory}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
@@ -568,74 +623,239 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)}
|
||||
<SurroundingsFilter
|
||||
selectedLocation={selectedPoint?.title}
|
||||
onSelectLocation={(location) => {
|
||||
setSelectedPoint(
|
||||
surroundingPoints.find((point) => point.title === location) || null
|
||||
);
|
||||
}}
|
||||
selectedCategories={selectedCategories}
|
||||
onToggleCategory={handleToggleCategory}
|
||||
filteredPoints={filteredPoints}
|
||||
/>
|
||||
|
||||
{/* SVG для путей из Figma */}
|
||||
<svg
|
||||
className="absolute pointer-events-none"
|
||||
style={imageStyle}
|
||||
width={originalSize.width}
|
||||
height={originalSize.height}
|
||||
viewBox="0 0 4096 2176"
|
||||
>
|
||||
{selectedPoint && (
|
||||
<>
|
||||
{Array.isArray(selectedPoint.path) ? (
|
||||
// Если path - массив, отрисовываем два пути
|
||||
<AnimatePresence mode="wait">
|
||||
{selectedPoint &&
|
||||
(Array.isArray(selectedPoint.path) ? (
|
||||
// Для составных путей: сначала пунктир (index 1), потом сплошная (index 0)
|
||||
selectedPoint.path.map((pathData, index) => {
|
||||
const color =
|
||||
categories.get(selectedPoint.category) || "#F47F52";
|
||||
const isDashed = index === 1; // Второй путь - пунктирный (короткий)
|
||||
const delay = isDashed ? 0 : 0.3; // Сплошная линия начинается после появления пунктира (0.3s фейд + 0.2s пауза)
|
||||
|
||||
// Transition для animate (с delay)
|
||||
const animateTransition = {
|
||||
pathLength: {
|
||||
duration: 0.7,
|
||||
ease: "easeInOut" as const,
|
||||
delay,
|
||||
},
|
||||
pathOffset: {
|
||||
duration: 0.7,
|
||||
ease: "easeInOut" as const,
|
||||
delay,
|
||||
},
|
||||
opacity: { duration: 0.3, delay },
|
||||
};
|
||||
|
||||
// Transition для exit (без delay) - одинаковый для всех линий
|
||||
const exitTransition = {
|
||||
pathLength: {
|
||||
duration: 0.7,
|
||||
ease: "easeInOut" as const,
|
||||
},
|
||||
pathOffset: {
|
||||
duration: 0.7,
|
||||
ease: "easeInOut" as const,
|
||||
},
|
||||
opacity: { duration: 0.3 },
|
||||
};
|
||||
|
||||
// Variants для пунктирной линии с transition
|
||||
const dashedVariants = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.3,
|
||||
ease: "easeInOut" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.3,
|
||||
delay,
|
||||
ease: "easeInOut" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
opacity: {
|
||||
duration: 0.3,
|
||||
ease: "easeInOut" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Variants для сплошной линии с transition
|
||||
const solidVariants = {
|
||||
hidden: {
|
||||
pathLength: 0,
|
||||
pathOffset: 1,
|
||||
opacity: 0,
|
||||
transition: exitTransition,
|
||||
},
|
||||
visible: {
|
||||
pathLength: 1,
|
||||
pathOffset: 0,
|
||||
opacity: 1,
|
||||
transition: animateTransition,
|
||||
},
|
||||
exit: {
|
||||
pathLength: 0,
|
||||
pathOffset: 1,
|
||||
opacity: 0,
|
||||
transition: exitTransition,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<path
|
||||
key={`route-${selectedPoint.title}-${index}`}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={index === 0 ? "#FFFFFF" : color}
|
||||
strokeWidth={index === 0 ? 5 : 3}
|
||||
strokeDasharray={index === 0 ? "none" : "6 6"}
|
||||
strokeLinecap="round"
|
||||
className="transition-opacity"
|
||||
/>
|
||||
<Fragment key={`route-${selectedPoint.title}-${index}`}>
|
||||
{isDashed ? (
|
||||
// Для пунктирной линии используем обычный path с JavaScript анимацией
|
||||
<>
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-${index}-white`}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="max(5px, 0.347vw)"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="6 6"
|
||||
variants={dashedVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
/>
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-${index}-color`}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
strokeWidth="max(3px, 0.208vw)"
|
||||
stroke="#F47F52"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="6 6"
|
||||
variants={dashedVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
// Для сплошной линии используем pathOffset для анимации от точки к центру
|
||||
<>
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-${index}-white`}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="max(5px, 0.347vw)"
|
||||
strokeLinecap="round"
|
||||
variants={solidVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
/>
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-${index}-color`}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#F47F52"
|
||||
strokeWidth="max(3px, 0.208vw)"
|
||||
strokeLinecap="round"
|
||||
variants={solidVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Если path - строка, отрисовываем один путь
|
||||
// Для простых путей: анимация от точки к центру
|
||||
<>
|
||||
<path
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-white`}
|
||||
d={selectedPoint.path}
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="5"
|
||||
strokeWidth="max(5px, 0.347vw)"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0, pathOffset: 1, opacity: 0 }}
|
||||
animate={{ pathLength: 1, pathOffset: 0, opacity: 1 }}
|
||||
exit={{ pathLength: 0, pathOffset: 1, opacity: 0 }}
|
||||
transition={{
|
||||
pathOffset: { duration: 0.7, ease: "easeInOut" },
|
||||
opacity: { duration: 0.3 },
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
<motion.path
|
||||
key={`route-${selectedPoint.title}-color`}
|
||||
d={selectedPoint.path}
|
||||
fill="none"
|
||||
stroke={categories.get(selectedPoint.category) || "#F47F52"}
|
||||
strokeWidth="3"
|
||||
stroke="#F47F52"
|
||||
strokeWidth="max(3px, 0.208vw)"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0, pathOffset: 1, opacity: 0 }}
|
||||
animate={{ pathLength: 1, pathOffset: 0, opacity: 1 }}
|
||||
exit={{ pathLength: 0, pathOffset: 1, opacity: 0 }}
|
||||
transition={{
|
||||
pathOffset: { duration: 0.7, ease: "easeInOut" },
|
||||
opacity: { duration: 0.3 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</svg>
|
||||
|
||||
{/* Контейнер для точек */}
|
||||
<div className="absolute" ref={markersContainerRef} style={imageStyle}>
|
||||
<div className="relative h-full">
|
||||
{/* Центральная точка (Baraha Town) */}
|
||||
<div
|
||||
className="absolute transition-all -translate-x-1/2 -translate-y-1/2"
|
||||
className="absolute transition-all"
|
||||
style={{
|
||||
left: CENTER_POINT.x,
|
||||
top: CENTER_POINT.y,
|
||||
transform: `scale(${maxZoom / zoom})`,
|
||||
transform: `scale(${Math.min(
|
||||
1 / zoom,
|
||||
1
|
||||
)}) translate(-50%, -50%)`,
|
||||
// translate(-${Math.min(zoom, 1) * 50}%, -${
|
||||
// Math.min(zoom, 1) * 50
|
||||
// }%)`,
|
||||
}}
|
||||
>
|
||||
<div className="2xl:p-[0.37vw] p-[5.33px] bg-[#F47F52] 2xl:rounded-[0.556vw] rounded-lg shadow-[0px_4px_40px_0px_rgba(244,127,82,0.3),0px_2px_2px_0px_rgba(244,127,82,0.25)] w-max">
|
||||
<div className="2xl:p-[0.37vw] p-[5.33px] bg-[#F47F52] 2xl:rounded-[0.556vw] rounded-lg shadow-[0px_4px_40px_0px_rgba(244,127,82,0.3),0px_2px_2px_0px_rgba(244,127,82,0.25)]">
|
||||
<img
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
el.style.minWidth = `${el.naturalWidth}px`;
|
||||
el.style.maxWidth =
|
||||
innerWidth >= 1440
|
||||
? `${(el.naturalWidth / 3 / 1440) * 100}vw`
|
||||
: `${el.naturalWidth / 3}px`;
|
||||
}}
|
||||
src="/img/surroundings/location.png"
|
||||
className="select-none"
|
||||
@@ -643,36 +863,52 @@ function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Точки на карте */}
|
||||
{filteredPoints.map((point) => {
|
||||
const color = categories.get(point.category) || "#F47F52";
|
||||
|
||||
return (
|
||||
<div
|
||||
<AnimatePresence>
|
||||
{filteredPoints.map((point) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ ease: "easeInOut" }}
|
||||
key={point.title}
|
||||
className="absolute cursor-pointer transition-all"
|
||||
className="absolute cursor-pointer origin-center"
|
||||
style={{
|
||||
left: point.coordinates.x,
|
||||
top: point.coordinates.y,
|
||||
transform: `scale(${maxZoom / zoom})`,
|
||||
transform: `scale(${Math.min(
|
||||
1 / zoom,
|
||||
1
|
||||
)}) translate(-50%, -50%)`,
|
||||
}}
|
||||
onMouseEnter={(e) => handlePointMouseEnter(e, point)}
|
||||
onMouseLeave={handlePointMouseLeave}
|
||||
onClick={(e) => handlePointClick(e, point)}
|
||||
>
|
||||
<div
|
||||
className="2xl:size-[1.111] size-4 aspect-square rounded-full 2xl:ring-[0.069vw] ring ring-white transition-all"
|
||||
className="2xl:size-[1.111vw] size-4 aspect-square rounded-full 2xl:ring-[0.069vw] ring-1 ring-white"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
backgroundColor:
|
||||
categories.get(point.category) || "#F47F52",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-2xl:hidden space-y-[0.417vw] fixed top-1/2 -translate-y-1/2 right-[1.111vw]">
|
||||
<Button variant="primary" onClick={handleZoomIn}>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<PlusIcon />
|
||||
</div>
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleZoomOut}>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<MinusIcon />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,10 @@ interface LocationPopupProps {
|
||||
|
||||
function LocationPopup({ title, travelTime }: LocationPopupProps) {
|
||||
return (
|
||||
<div className="2xl:space-y-[0.556vw] space-y-2">
|
||||
<p className="text-s [font-family:Poppins] font-normal text-[#324D43]">
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex items-center 2xl:gap-[0.556vw] gap-2 2xl:px-[0.833vw] px-3 2xl:py-[0.556vw] py-2 bg-[#F0EDE6] rounded-xl">
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#324D43]">
|
||||
<div className="2xl:gap-y-[0.556vw] gap-y-2 flex flex-col items-center 2xl:px-[0.833vw] px-3 2xl:py-[0.694vw] py-2.5 2xl:rounded-[1.111vw] rounded-2xl bg-[#F7F6F3] shadow-[0px_4px_40px_0px_rgba(15,16,17,0.1),0px_2px_2px_0px_rgba(0,0,0,0.06)]">
|
||||
<p className="text-s [font-family:Poppins] text-[#324D43]">{title}</p>
|
||||
<div className="flex items-center 2xl:gap-[0.556vw] gap-2">
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#A7A08E]">
|
||||
<CarIcon />
|
||||
</div>
|
||||
<p className="caption font-medium text-[#A7A08E]">{travelTime} min</p>
|
||||
|
||||
@@ -40,10 +40,10 @@ function PopupContainer() {
|
||||
: { bottom: 0, left: 0 }
|
||||
}
|
||||
className={clsx(
|
||||
"fixed md:absolute z-2 2xl:w-[18.472vw] md:max-2xl:w-[266px] w-dvw bg-[#F7F6F3]",
|
||||
isUnitPopup
|
||||
? "2xl:rounded-[0.833vw] md:max-2xl:rounded-xl 2xl:p-[0.833vw] md:max-2xl:p-3 p-4 max-md:rounded-t-2xl"
|
||||
: "2xl:rounded-[1.111vw] 2xl:p-[1.111vw] p-4 md:max-2xl:rounded-2xl rounded-t-2xl",
|
||||
"fixed md:absolute z-20 2xl:w-[18.472vw]md:max-2xl:w-[266px]w-dvwbg-[#F7F6F3]",
|
||||
// isUnitPopup
|
||||
// ? "2xl:rounded-[0.833vw] md:max-2xl:rounded-xl 2xl:p-[0.833vw] md:max-2xl:p-3 p-4 max-md:rounded-t-2xl"
|
||||
// : "2xl:rounded-[1.111vw] 2xl:p-[1.111vw] p-4 md:max-2xl:rounded-2xl rounded-t-2xl",
|
||||
side === "left" &&
|
||||
"md:-translate-y-1/2 2xl:-translate-x-[calc(100%+1.25vw)] md:max-2xl:-translate-x-[calc(100%+18px)]",
|
||||
side === "right" && "md:-translate-y-1/2 2xl:translate-x-[1.25vw]",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import clsx from "clsx";
|
||||
import ChevronDownIcon from "../icons/ChevronDownIcon";
|
||||
import { useState } from "react";
|
||||
import CheckIcon from "../icons/CheckIcon";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import CloseIcon from "../icons/CloseIcon";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
interface SelectProps {
|
||||
options: string[];
|
||||
value?: string;
|
||||
onChange: (value: string | undefined) => void;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Select({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}: SelectProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsOpen(false));
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"bg-[#F0EDE6] 2xl:p-[0.833vw] p-3 2xl:rounded-[1.111vw] rounded-2xl flex items-center 2xl:gap-[1.111vw] gap-4 relative cursor-pointer justify-between",
|
||||
className
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-s text-nowrap text-ellipsis line-clamp-1",
|
||||
value ? "text-[#324D43]" : "text-[#A7A08E]"
|
||||
)}
|
||||
>
|
||||
{value || placeholder}
|
||||
</p>
|
||||
{value ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(undefined);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#A7A08E]">
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-[1.111vw] size-4 text-[#A7A08E] transition-transform",
|
||||
isOpen && "rotate-180"
|
||||
)}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="absolute top-[calc(100%+0.556vw)] left-0 w-full z-10 bg-[#F7F6F3] 2xl:shadow-[0_0.556vw_1.111vw_0_#2327341A] shadow-[0_8px_16px_0_#2327341A] 2xl:p-[0.278vw] p-1 2xl:rounded-[1.111vw] rounded-2xl 2xl:max-h-[15.556vw] max-h-[224px] overflow-auto"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div
|
||||
key={option}
|
||||
className={clsx(
|
||||
"text-s w-full 2xl:p-[0.833vw] p-3 flex justify-between items-center 2xl:gap-[0.139vw] gap-0.5 hover:bg-[#F0EDE6] hover:text-[#324D43] 2xl:rounded-[0.833vw] rounded-xl",
|
||||
value === option ? "text-[#324D43]" : "text-[#A7A08E]"
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(option);
|
||||
}}
|
||||
>
|
||||
<p>{option}</p>
|
||||
{value === option && (
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#F47F52]">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
||||
@@ -63,7 +63,7 @@ export const surroundingPoints: ISurroundingPoint[] = [
|
||||
{
|
||||
title: "Al Khebra Driving Academy",
|
||||
category: "Education",
|
||||
coordinates: { x: 2002, y: 1199 },
|
||||
coordinates: { x: 1996, y: 1195 },
|
||||
travelTime: 5,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.39-.34 2.92.76 7.58 15.4 21.4 42.34 32.84 54.28.1.1.19.21.26.33 1.39 2.2 4.22 5.88 5.39 4.32 1.48-1.98 2.48-1.51-.88-5.37-.08-.08-.15-.18-.21-.28l-7.57-12.47a2 2 0 0 1 .32-2.49l29.74-28.34a1.99 1.99 0 0 1 2.41-.26l67.73 40.73a2 2 0 0 1 .22 3.27l-1.76 1.41c-.61.49-1.45.58-2.14.23l-1.86-.93",
|
||||
},
|
||||
@@ -119,7 +119,7 @@ export const surroundingPoints: ISurroundingPoint[] = [
|
||||
{
|
||||
title: "Al Bidda Park",
|
||||
category: "Parks",
|
||||
coordinates: { x: 2117, y: 515 },
|
||||
coordinates: { x: 2111, y: 511 },
|
||||
travelTime: 17,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.39-.34 2.92.76 7.58 15.4 21.4 42.34 32.84 54.28q.15.15.27.33c1.38 2.2 4.21 5.87 5.38 4.32 1.5-2 2.5-1.5-1-5.5s-22.5-30.5-30.5-49-22.5-34-32.5-51.5-51.5-83-53.5-87.5c-2.33-5.67-9.2-19.601-18-30.001-11-13-28-28.5-45-55.5-13.6-21.6-27.67-52.667-33-65.5-6.5-18.333-20-57.5-22-67.5-2.5-12.5-3-19-6-25.5s0-9 6-9.5c5.84-.487 178.49-15.19 200.54-16.429.9-.051 1.6-.663 1.81-1.537 4.82-20.228 14.75-64.056 18.65-89.034 5-32 11.5-76 29.5-95s60-43.5 94-54 51.5-19 58.5-21 28.5-.5 30.5 1.5c1.54 1.537 4.43 20.719 5.83 31.243.11.771-.25 1.525-.9 1.942L2117 517.5",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user