Add SurroundingsPage with interactive map and location popups; include new layout and category filter components
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 MiB |
@@ -0,0 +1,22 @@
|
||||
import { Outlet, useLocation } from "react-router";
|
||||
import NavMenu from "./NavMenu";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function LayoutWithFooter() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#F7F6F3]">
|
||||
<NavMenu />
|
||||
<main className="h-dvh">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutWithFooter;
|
||||
@@ -0,0 +1,679 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
surroundingPoints,
|
||||
categories,
|
||||
CENTER_POINT,
|
||||
type ISurroundingPoint,
|
||||
} from "../../consts/surroundingPoints";
|
||||
import { usePopupStore } from "../../stores/usePopupStore";
|
||||
import LocationPopup from "../popups/LocationPopup";
|
||||
import CategoryFilter from "../ui/CategoryFilter";
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface MapProps {
|
||||
maxZoom?: number;
|
||||
}
|
||||
|
||||
const getEventPosition = (
|
||||
e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent
|
||||
): Position => {
|
||||
if ("touches" in e)
|
||||
return {
|
||||
x: e.touches[0].clientX,
|
||||
y: e.touches[0].clientY,
|
||||
};
|
||||
return {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
};
|
||||
|
||||
const constrainPosition = (
|
||||
position: Position,
|
||||
containerSize: Size,
|
||||
imageSize: Size,
|
||||
zoom: number
|
||||
): Position => {
|
||||
const scaledWidth = imageSize.width * zoom;
|
||||
const scaledHeight = imageSize.height * zoom;
|
||||
const minX = containerSize.width - scaledWidth;
|
||||
const minY = containerSize.height - scaledHeight;
|
||||
|
||||
return {
|
||||
x: Math.min(0, Math.max(minX, position.x)),
|
||||
y: Math.min(0, Math.max(minY, position.y)),
|
||||
};
|
||||
};
|
||||
|
||||
const calculateMinZoom = (containerSize: Size, imageSize: Size): number => {
|
||||
if (imageSize.width === 0 || imageSize.height === 0) return 0.1;
|
||||
|
||||
const widthRatio = containerSize.width / imageSize.width;
|
||||
const heightRatio = containerSize.height / imageSize.height;
|
||||
|
||||
return Math.max(widthRatio, heightRatio);
|
||||
};
|
||||
|
||||
function SurroundingsPage({ maxZoom = 1 }: MapProps) {
|
||||
const [originalSize, setOriginalSize] = useState<Size>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
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 [selectedPoint, setSelectedPoint] = useState<ISurroundingPoint | null>(
|
||||
null
|
||||
);
|
||||
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
||||
const [lastClickTime, setLastClickTime] = useState(0);
|
||||
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(
|
||||
new Set(Array.from(categories.keys()))
|
||||
);
|
||||
const { setPopup, setPosition: setPopupPosition, setSide } = usePopupStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !isImageLoaded || originalSize.width === 0)
|
||||
return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const scaledWidth = originalSize.width * zoom;
|
||||
const scaledHeight = originalSize.height * zoom;
|
||||
|
||||
const maxOffsetX = Math.max(0, scaledWidth - containerRect.width);
|
||||
const maxOffsetY = Math.max(0, scaledHeight - containerRect.height);
|
||||
|
||||
const desiredOffsetX = containerRect.width * 0.35;
|
||||
const desiredOffsetY = containerRect.height * 0.58;
|
||||
|
||||
const boundedOffsetX = Math.min(desiredOffsetX, maxOffsetX);
|
||||
const boundedOffsetY = Math.min(desiredOffsetY, maxOffsetY);
|
||||
|
||||
setPosition({
|
||||
x: -boundedOffsetX,
|
||||
y: -boundedOffsetY,
|
||||
});
|
||||
}, [isImageLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateContainerSize = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const { width, height } = containerRef.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
width === containerSizeRef.current.width &&
|
||||
height === containerSizeRef.current.height
|
||||
)
|
||||
return;
|
||||
|
||||
containerSizeRef.current = { width, height };
|
||||
|
||||
const newMinZoom = calculateMinZoom({ width, height }, originalSize);
|
||||
minZoomRef.current = newMinZoom;
|
||||
|
||||
setZoom(newMinZoom);
|
||||
|
||||
const scaledWidth = originalSize.width * newMinZoom;
|
||||
const scaledHeight = originalSize.height * newMinZoom;
|
||||
|
||||
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);
|
||||
|
||||
setPosition({
|
||||
x: -boundedOffsetX,
|
||||
y: -boundedOffsetY,
|
||||
});
|
||||
};
|
||||
|
||||
updateContainerSize();
|
||||
|
||||
addEventListener("resize", updateContainerSize);
|
||||
|
||||
return () => {
|
||||
removeEventListener("resize", updateContainerSize);
|
||||
};
|
||||
}, [originalSize, zoom]);
|
||||
|
||||
function handleLoad() {
|
||||
if (!mapRef.current || !containerRef.current) return;
|
||||
|
||||
const img = new Image();
|
||||
img.src = mapRef.current.src;
|
||||
|
||||
img.onload = () => {
|
||||
const newOriginalSize = {
|
||||
width: img.naturalWidth || img.width,
|
||||
height: img.naturalHeight || img.height,
|
||||
};
|
||||
|
||||
setOriginalSize(newOriginalSize);
|
||||
|
||||
const containerRect = containerRef.current!.getBoundingClientRect();
|
||||
const minZoom = calculateMinZoom(
|
||||
{
|
||||
width: containerRect.width,
|
||||
height: containerRect.height,
|
||||
},
|
||||
newOriginalSize
|
||||
);
|
||||
|
||||
minZoomRef.current = minZoom;
|
||||
setZoom(minZoom);
|
||||
setIsImageLoaded(true);
|
||||
};
|
||||
}
|
||||
|
||||
const getContainerSize = (): Size => containerSizeRef.current;
|
||||
|
||||
const updatePosition = (newPosition: Position, newZoom: number = zoom) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerSize = getContainerSize();
|
||||
const constrainedPosition = constrainPosition(
|
||||
newPosition,
|
||||
containerSize,
|
||||
originalSize,
|
||||
newZoom
|
||||
);
|
||||
|
||||
setPosition(constrainedPosition);
|
||||
};
|
||||
|
||||
const zoomToPoint = (point: Position, targetZoom: number) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const boundedZoom = Math.min(
|
||||
maxZoom,
|
||||
Math.max(minZoomRef.current, targetZoom)
|
||||
);
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = point.x - containerRect.left;
|
||||
const mouseY = point.y - containerRect.top;
|
||||
|
||||
const scale = boundedZoom / zoom;
|
||||
const dx = mouseX - position.x;
|
||||
const dy = mouseY - position.y;
|
||||
const newPosition = {
|
||||
x: mouseX - dx * scale,
|
||||
y: mouseY - dy * scale,
|
||||
};
|
||||
|
||||
setZoom(boundedZoom);
|
||||
updatePosition(newPosition, boundedZoom);
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (e.touches.length === 2) {
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
const distance = Math.hypot(
|
||||
touch1.clientX - touch2.clientX,
|
||||
touch1.clientY - touch2.clientY
|
||||
);
|
||||
initialTouchDistance.current = distance;
|
||||
previousTouchDistance.current = distance;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.touches.length === 1) handleStart(e);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (e.touches.length === 2) {
|
||||
e.preventDefault();
|
||||
const touch1 = e.touches[0];
|
||||
const touch2 = e.touches[1];
|
||||
const distance = Math.hypot(
|
||||
touch1.clientX - touch2.clientX,
|
||||
touch1.clientY - touch2.clientY
|
||||
);
|
||||
|
||||
if (initialTouchDistance.current === null) {
|
||||
initialTouchDistance.current = distance;
|
||||
previousTouchDistance.current = distance;
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousTouchDistance.current === null) {
|
||||
previousTouchDistance.current = distance;
|
||||
return;
|
||||
}
|
||||
|
||||
const distanceChange = Math.abs(distance - previousTouchDistance.current);
|
||||
const changePercentage =
|
||||
(distanceChange / previousTouchDistance.current) * 100;
|
||||
|
||||
if (changePercentage >= 5) {
|
||||
const zoomFactor = distance > previousTouchDistance.current ? 1.1 : 0.9;
|
||||
const centerX = (touch1.clientX + touch2.clientX) / 2;
|
||||
const centerY = (touch1.clientY + touch2.clientY) / 2;
|
||||
|
||||
const newZoom = Math.min(
|
||||
maxZoom,
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor)
|
||||
);
|
||||
|
||||
if (Math.abs(newZoom - zoom) > 0.001) {
|
||||
setZoom(newZoom);
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = centerX - containerRect.left;
|
||||
const mouseY = centerY - containerRect.top;
|
||||
|
||||
const scale = newZoom / zoom;
|
||||
const dx = mouseX - position.x;
|
||||
const dy = mouseY - position.y;
|
||||
const newPosition = {
|
||||
x: mouseX - dx * scale,
|
||||
y: mouseY - dy * scale,
|
||||
};
|
||||
|
||||
updatePosition(newPosition, newZoom);
|
||||
}
|
||||
|
||||
previousTouchDistance.current = distance;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDragging && e.touches.length === 1) {
|
||||
const { x, y } = getEventPosition(e);
|
||||
const newPosition = {
|
||||
x: x - startPosition.x,
|
||||
y: y - startPosition.y,
|
||||
};
|
||||
|
||||
updatePosition(newPosition);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnd = () => setIsDragging(false);
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setIsDragging(false);
|
||||
previousTouchDistance.current = null;
|
||||
initialTouchDistance.current = null;
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!containerRef.current || !mapRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
const mouseY = e.clientY - containerRect.top;
|
||||
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newZoom = Math.min(
|
||||
maxZoom,
|
||||
Math.max(minZoomRef.current, zoom * zoomFactor)
|
||||
);
|
||||
|
||||
if (Math.abs(newZoom - zoom) < 0.01) return;
|
||||
|
||||
const scale = newZoom / zoom;
|
||||
const dx = mouseX - position.x;
|
||||
const dy = mouseY - position.y;
|
||||
const newPosition = {
|
||||
x: mouseX - dx * scale,
|
||||
y: mouseY - dy * scale,
|
||||
};
|
||||
|
||||
if (isDragging) {
|
||||
const eventPosition = getEventPosition(e);
|
||||
setStartPosition({
|
||||
x: eventPosition.x - newPosition.x,
|
||||
y: eventPosition.y - newPosition.y,
|
||||
});
|
||||
}
|
||||
|
||||
setZoom(newZoom);
|
||||
updatePosition(newPosition, newZoom);
|
||||
};
|
||||
|
||||
const handleMouseMove = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!isDragging || !containerRef.current) return;
|
||||
|
||||
const { x, y } = getEventPosition(e);
|
||||
const newPosition = {
|
||||
x: x - startPosition.x,
|
||||
y: y - startPosition.y,
|
||||
};
|
||||
|
||||
updatePosition(newPosition);
|
||||
};
|
||||
|
||||
const handleStart = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!mapRef.current) return;
|
||||
|
||||
setIsDragging(true);
|
||||
const { x, y } = getEventPosition(e);
|
||||
setStartPosition({
|
||||
x: x - position.x,
|
||||
y: y - position.y,
|
||||
});
|
||||
};
|
||||
|
||||
function smoothZoomTo(targetZoom: number, point: Position) {
|
||||
const duration = 400;
|
||||
const startZoom = zoom;
|
||||
const startTime = performance.now();
|
||||
|
||||
function animateZoom(now: number) {
|
||||
const elapsed = now - startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
const currentZoom = startZoom + (targetZoom - startZoom) * ease;
|
||||
|
||||
zoomToPoint(point, currentZoom);
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(animateZoom);
|
||||
} else {
|
||||
setZoom(targetZoom);
|
||||
zoomToPoint(point, targetZoom);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateZoom);
|
||||
}
|
||||
|
||||
const handleClick = (
|
||||
e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>
|
||||
) => {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (
|
||||
(e.target as HTMLElement).closest("button") ||
|
||||
(e.target as HTMLElement).closest("#selected-complex-card")
|
||||
)
|
||||
return;
|
||||
|
||||
if (currentTime - lastClickTime < 200) {
|
||||
const targetZoom =
|
||||
Math.abs(zoom - maxZoom) < 0.01 ? minZoomRef.current : maxZoom;
|
||||
const point = getEventPosition(e);
|
||||
|
||||
smoothZoomTo(targetZoom, point);
|
||||
|
||||
setLastClickTime(0);
|
||||
} else {
|
||||
setLastClickTime(currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
containerRef.current?.addEventListener("wheel", handleWheel, {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
return () =>
|
||||
containerRef.current?.removeEventListener("wheel", handleWheel);
|
||||
}, [isDragging, position]);
|
||||
|
||||
const handlePointMouseEnter = (
|
||||
_e: React.MouseEvent<HTMLDivElement>,
|
||||
point: ISurroundingPoint
|
||||
) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const pointX = position.x + point.coordinates.x * zoom;
|
||||
const pointY = position.y + point.coordinates.y * zoom;
|
||||
|
||||
const screenX = containerRect.left + pointX;
|
||||
const screenY = containerRect.top + pointY;
|
||||
|
||||
setHoveredPoint(point);
|
||||
setPopupPosition({ x: screenX, y: screenY });
|
||||
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);
|
||||
};
|
||||
|
||||
const filteredPoints = surroundingPoints.filter((point) =>
|
||||
selectedCategories.has(point.category)
|
||||
);
|
||||
|
||||
const handlePointClick = (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
point: ISurroundingPoint
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
// Если кликнули по уже выбранной точке - снимаем выделение
|
||||
if (selectedPoint?.title === point.title) {
|
||||
setSelectedPoint(null);
|
||||
} else {
|
||||
setSelectedPoint(point);
|
||||
}
|
||||
};
|
||||
|
||||
const imageStyle = {
|
||||
transform: `translateX(${position.x}px) translateY(${position.y}px) translateZ(0px) scale(${zoom})`,
|
||||
transformOrigin: "0 0",
|
||||
...originalSize,
|
||||
};
|
||||
|
||||
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(
|
||||
"touch-none overflow-hidden relative h-full select-none",
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab"
|
||||
)}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
onTouchMove={handleTouchMove}
|
||||
onMouseDown={handleStart}
|
||||
onMouseUp={handleEnd}
|
||||
onMouseLeave={() => {
|
||||
handleEnd();
|
||||
handlePointMouseLeave();
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{containerRef.current?.clientWidth && (
|
||||
<img
|
||||
ref={mapRef}
|
||||
src="/img/surroundings/map.png"
|
||||
style={imageStyle}
|
||||
className="absolute max-w-none pointer-events-none"
|
||||
alt="map"
|
||||
onLoad={handleLoad}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* SVG для путей из Figma */}
|
||||
<svg
|
||||
className="absolute pointer-events-none"
|
||||
style={imageStyle}
|
||||
width={originalSize.width}
|
||||
height={originalSize.height}
|
||||
>
|
||||
{selectedPoint && (
|
||||
<>
|
||||
{Array.isArray(selectedPoint.path) ? (
|
||||
// Если path - массив, отрисовываем два пути
|
||||
selectedPoint.path.map((pathData, index) => {
|
||||
const color =
|
||||
categories.get(selectedPoint.category) || "#F47F52";
|
||||
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"
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
// Если path - строка, отрисовываем один путь
|
||||
<>
|
||||
<path
|
||||
d={selectedPoint.path}
|
||||
fill="none"
|
||||
stroke="#FFFFFF"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d={selectedPoint.path}
|
||||
fill="none"
|
||||
stroke={categories.get(selectedPoint.category) || "#F47F52"}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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"
|
||||
style={{
|
||||
left: CENTER_POINT.x,
|
||||
top: CENTER_POINT.y,
|
||||
transform: `scale(${maxZoom / zoom})`,
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<img
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
el.style.minWidth = `${el.naturalWidth}px`;
|
||||
}}
|
||||
src="/img/surroundings/location.png"
|
||||
className="select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Точки на карте */}
|
||||
{filteredPoints.map((point) => {
|
||||
const color = categories.get(point.category) || "#F47F52";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={point.title}
|
||||
className="absolute cursor-pointer transition-all"
|
||||
style={{
|
||||
left: point.coordinates.x,
|
||||
top: point.coordinates.y,
|
||||
transform: `scale(${maxZoom / zoom})`,
|
||||
}}
|
||||
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"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SurroundingsPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
import CarIcon from "../icons/CarIcon";
|
||||
|
||||
interface LocationPopupProps {
|
||||
title: string;
|
||||
travelTime: number;
|
||||
}
|
||||
|
||||
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]">
|
||||
<CarIcon />
|
||||
</div>
|
||||
<p className="caption font-medium text-[#A7A08E]">{travelTime} min</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LocationPopup;
|
||||
@@ -0,0 +1,50 @@
|
||||
import clsx from "clsx";
|
||||
import { categories } from "../../consts/surroundingPoints";
|
||||
|
||||
interface CategoryFilterProps {
|
||||
selectedCategories: Set<string>;
|
||||
onToggleCategory: (category: string) => void;
|
||||
}
|
||||
|
||||
function CategoryFilter({
|
||||
selectedCategories,
|
||||
onToggleCategory,
|
||||
}: CategoryFilterProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap 2xl:gap-[0.556vw] gap-2">
|
||||
{Array.from(categories.entries()).map(([category, color]) => {
|
||||
const isSelected = selectedCategories.has(category);
|
||||
return (
|
||||
<button
|
||||
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",
|
||||
"hover:opacity-80"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-4 size-4 rounded-full border-2 transition-colors",
|
||||
isSelected ? "border-transparent" : "border-[#E2DCCF]"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isSelected ? color : "transparent",
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-s [font-family:Poppins] font-normal transition-colors",
|
||||
isSelected ? "text-[#324D43]" : "text-[#324D43]"
|
||||
)}
|
||||
>
|
||||
{category}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CategoryFilter;
|
||||
@@ -0,0 +1,178 @@
|
||||
export const categories = new Map([
|
||||
["Airports", "#93D7EC"],
|
||||
["Health care", "#7794CC"],
|
||||
["Souqs", "#F0D925"],
|
||||
["Leisure", "#E86748"],
|
||||
["Education", "#8F3B44"],
|
||||
["Parks", "#799E97"],
|
||||
["Rest", "#EF88B7"],
|
||||
["Other", "#B352F4"],
|
||||
]);
|
||||
|
||||
export interface ISurroundingPoint {
|
||||
category: string;
|
||||
title: string;
|
||||
coordinates: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
travelTime: number;
|
||||
path: string | [string, string]; // svg path маршрута или маршрут с пунктиром
|
||||
}
|
||||
|
||||
// Центральная точка (Baraha Town) - координаты центра эллипса из Figma
|
||||
// Ellipse 3010: x: 1864, y: 1145, размер: 10x10, центр: x: 1869, y: 1150
|
||||
export const CENTER_POINT = { x: 1853, y: 1135 };
|
||||
|
||||
// Координаты точек - центры эллипсов (Ellipse 3008, размер 12x12) из Figma
|
||||
export const surroundingPoints: ISurroundingPoint[] = [
|
||||
{
|
||||
title: "Palace Gardens 1",
|
||||
category: "Other",
|
||||
coordinates: { x: 1899.5, y: 1112 },
|
||||
travelTime: 3,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.8 15.69 24.3 46.21 35.77 58.22.14.15.25.32.37.48.45.63 1.44 1.29 2.84.24 2-1.5 1.5-3.5 0-5s-28-37-28.5-45 2.5-7 7.5-11c3.56-2.85 11.34-10.13 15.95-14.52.84-.8.81-2.14-.05-2.92l-8.4-7.56",
|
||||
},
|
||||
{
|
||||
title: "The English Kindergarten",
|
||||
category: "Education",
|
||||
coordinates: { x: 1929, y: 1121 },
|
||||
travelTime: 4,
|
||||
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.5-2 2.5-1.5-1-5.5-3.42-3.91-20.2-27.86-28.43-46.22-.35-.77-.16-1.67.46-2.24l35.61-33.27c.77-.72 1.96-.72 2.72-.01l4.74 4.37c.26.24.45.55.56.89l1.34 4.48",
|
||||
},
|
||||
{
|
||||
title: "Aspire Park",
|
||||
category: "Parks",
|
||||
coordinates: { x: 1284, y: 985 },
|
||||
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.28.1.1.19.21.26.33 1.39 2.2 4.22 5.87 5.39 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.53-21.487-27.52-52.342-32.92-65.297-.05-.14-.12-.266-.22-.379-1.03-1.141-4.4-2.486-10.86.176-8.5 3.5-202.5 91.001-213.5 95.501s-134 57.5-145 61-62 26-64.5 26.5-8 1.5-8-6v-95.519c0-1.104.9-1.981 2-1.981h13",
|
||||
"M1278 939c5 9 12 23.502 12 52.501",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Khalifa International Stadium",
|
||||
category: "Leisure",
|
||||
coordinates: { x: 1424, y: 919 },
|
||||
travelTime: 10,
|
||||
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.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-4.5-7.33-14.3-23.1-17.5-27.5-4-5.5-21-24.5-30-36.5-7.2-9.6-31-53-42-73.5l-7.64-14.833c-.48-.937-1.58-1.354-2.54-.932C1648.4 880.332 1543.08 926.668 1532 933c-14 8-116.5 49-119 50.5-2 1.2-6.83-.167-9-1",
|
||||
"M1404 983c-1.83-12 .6-40.2 25-57",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Al Khebra Driving Academy",
|
||||
category: "Education",
|
||||
coordinates: { x: 2002, y: 1199 },
|
||||
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",
|
||||
},
|
||||
{
|
||||
title: "Al Thumama Stadium",
|
||||
category: "Leisure",
|
||||
coordinates: { x: 2248, y: 1229 },
|
||||
travelTime: 8,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 30.66 37.37 30.57 40.31 40.18 50.17.74.75 1.93.8 2.72.11 14.28-12.43 41.12-36.48 44.6-42.28 4.5-7.5 15.5-24 23-30.5s34.5-31 37-33 16-15.5 24-14 20 12 28 11.5 8.5.5 12-2 5-6 11.5-9.5c4.78-2.57 20.07-5.63 28.07-6.75 1.08-.15 2.05.61 2.21 1.68L2253 1235",
|
||||
},
|
||||
{
|
||||
title: "Wakrah Park Beach",
|
||||
category: "Rest",
|
||||
coordinates: { x: 3087, y: 1786 },
|
||||
travelTime: 17,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5c12.59-1.94 69.29-11.38 89.64-20.15 1.03-.45 2.23-.06 2.7.96 33.14 71.82 100.24 216.75 110.16 236.19 9.9 19.41 66.05 133.24 93.69 189.36.47.94 1.57 1.33 2.54.92l31.54-13.06c.98-.41 2.09 0 2.55.94 11.93 24.02 34.62 69.71 36.18 72.84 1.45 2.9 3.55 12.29 4.68 17.88.19.94 1.01 1.62 1.97 1.62H2996c6 0 75 .5 76.5 1.5s1.5 3.5 4 4 8 1.5 9 0 4-4 6.5-4",
|
||||
},
|
||||
{
|
||||
title: "Hamad Intranational Airport",
|
||||
category: "Airports",
|
||||
coordinates: { x: 2892, y: 1150 },
|
||||
travelTime: 13,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5c23.5-3.83 74.8-13.7 92-22.5s140.17-86.33 199.5-124",
|
||||
},
|
||||
{
|
||||
title: "Doha Intranational Airport",
|
||||
category: "Airports",
|
||||
coordinates: { x: 2503, y: 917 },
|
||||
travelTime: 13,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5c22.75-3.71 71.55-13.08 90.25-21.65.98-.45 1.4-1.59.93-2.57-33.47-69.78-101.06-210.53-110.18-228.78-11.5-23-36-74-40.5-80.5-3.49-5.038-26.97-28.319-39.34-40.374a1.994 1.994 0 0 1-.3-2.495l1.64-2.631",
|
||||
},
|
||||
{
|
||||
title: "974 Stadium",
|
||||
category: "Leisure",
|
||||
coordinates: { x: 2592, y: 652 },
|
||||
travelTime: 15,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5 73-12 91.5-21 71-42.5 81-49.5 63-40.5 67.5-42.5 11-9 16-8 6.5 4 6.5 6 0 6.5-4.5 8.5-9-2-10-4-7.5-17-9-20.5-52-148-63-173.5-55.5-138-67-160.5-33.5-59.5-57-85c-23.36-25.346-54.13-46.741-61.37-49.944a2 2 0 0 1-.26-.139c-1.34-.894-3.42-3.093-1.87-5.417 1.28-1.914 4.12-5.652 6.15-8.27a2.01 2.01 0 0 0-.49-2.92l-11.89-7.67a2 2 0 0 1-.54-2.848L2598 661",
|
||||
},
|
||||
{
|
||||
title: "Mina District",
|
||||
category: "Other",
|
||||
coordinates: { x: 2453, y: 497 },
|
||||
travelTime: 20,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5 73-12 91.5-21 71-42.5 81-49.5 63-40.5 67.5-42.5 11-9 16-8 6.5 4 6.5 6 0 6.5-4.5 8.5-9-2-10-4-7.5-17-9-20.5-52-148-63-173.5-55.5-138-67-160.5-33.5-59.5-57-85-54.5-47-61.5-50-14.5-9.5-31.5-8-30 9.001-68 8-37.5 1-40-12-3-23-9.5-34.5-19.5-31-37.5-38.5c-3.64-1.656-13.86-4.56-25.75-3.033-.17.021-.33.067-.49.113-1.72.502-4.76-.559-4.76-8.58 0-7.553.27-15.134.45-18.888a1.97 1.97 0 0 1 1.03-1.647l88.99-48.63c.88-.479 1.27-1.53.91-2.463L2458.5 503",
|
||||
},
|
||||
{
|
||||
title: "Souq Waqif",
|
||||
category: "Souqs",
|
||||
coordinates: { x: 2252, y: 648 },
|
||||
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.28.1.1.19.21.26.33 1.39 2.2 4.22 5.88 5.39 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.5-6.5-8.5 0-14S2028 845 2035.5 840s38-29 44-35.5 35-34.5 47.5-43c11.47-7.802 51.17-33.295 58.77-38.204.75-.481 1.05-1.376.8-2.225-2.3-7.717-6.44-23.696-7.57-36.071-1.08-11.908-.62-25.255-.15-32.036.07-1.091 1.03-1.901 2.12-1.822l60.53 4.358h16",
|
||||
},
|
||||
{
|
||||
title: "Al Bidda Park",
|
||||
category: "Parks",
|
||||
coordinates: { x: 2117, y: 515 },
|
||||
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",
|
||||
},
|
||||
{
|
||||
title: "Rixos Gulf Hotel Doha",
|
||||
category: "Rest",
|
||||
coordinates: { x: 2546, y: 664 },
|
||||
travelTime: 15,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5 73-12 91.5-21 71-42.5 81-49.5 63-40.5 67.5-42.5 11-9 16-8 6.5 4 6.5 6 0 6.5-4.5 8.5-9-2-10-4-7.5-17-9-20.5-52-148-63-173.5-55.5-138-67-160.5-33.5-59.5-57-85-54.5-47-61.5-50c-4.97-2.886-17.31-8.456-28.22-8.519-.78-.005-1.51-.429-1.82-1.147-1.08-2.458-2.62-6.788-2.96-10.834v-1.089c0-.579-.25-1.129-.69-1.509L2552 670",
|
||||
},
|
||||
{
|
||||
title: "Museum of Islamic Art",
|
||||
category: "Leisure",
|
||||
coordinates: { x: 2325, y: 569 },
|
||||
travelTime: 17,
|
||||
path: [
|
||||
"m1869.5 1149.5 7.59-5.19a2 2 0 0 1 2.93.76c7.84 15.78 24.5 46.57 35.98 58.43 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5 73-12 91.5-21 71-42.5 81-49.5 63-40.5 67.5-42.5 11-9 16-8 6.5 4 6.5 6 0 6.5-4.5 8.5-9-2-10-4-7.5-17-9-20.5-52-148-63-173.498c-11-25.5-55.5-138-67-160.5s-33.5-59.5-57-85-54.5-47-61.5-50-14.5-9.5-31.5-8-30 9.001-68 8-37.5 1-40-12-3-23-9.5-34.5-19.5-31-37.5-38.5c-3.67-1.667-14-4.6-26-3-1 0-5 1.498-11 1.498s-11-.497-15.5 1.502c-3.95 1.752-17.13 3.506-23 3.911-1.11.076-2-.808-2-1.913v-13",
|
||||
"M2332.5 606.5v-22.825q0-.175-.03-.347L2331 575",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Msherieb Downtown",
|
||||
category: "Other",
|
||||
coordinates: { x: 2208, y: 672 },
|
||||
travelTime: 15,
|
||||
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.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.5-6.5-8.5 0-14S2028 845 2035.5 840s38-29 44-35.5 35-34.5 47.5-43 58.5-38 60-39-8-30-7.5-34 2.5-5 5.5-5c2.16 0 18.48.539 28.78.89 1.48.05 2.5-1.476 1.88-2.828L2214.5 679",
|
||||
},
|
||||
{
|
||||
title: "Al Wakrah Souq",
|
||||
category: "Souqs",
|
||||
coordinates: { x: 3042, y: 1920 },
|
||||
travelTime: 18,
|
||||
path: "m1869.5 1149.5 7.59-5.19c1.01-.69 2.38-.34 2.93.75 7.84 15.79 24.5 46.58 35.98 58.44 16.33 16 55.4 55.8 81 87 32 39 39 49 50 60s64.5 53 110 57 82 1.5 138-16 203.5-60 218.5-63.5 79.5-23.5 92.5-25.5c12.59-1.94 69.29-11.38 89.64-20.15 1.03-.45 2.23-.06 2.7.96 33.14 71.82 100.24 216.75 110.16 236.19 10 19.6 67.17 135.5 94.5 191l74 158c.67 1.5 3.7 3.6 10.5 0 8.5-4.5 14-6.5 19.5-6 4.17.38 30.19 3.18 44.68 4.75 1.23.13 2.03 1.32 1.74 2.51-1.16 4.72-2.6 11.7-1.92 13.74.63 1.88 5.59 4.7 9.56 6.64 1.33.65 1.6 2.49.51 3.51-1.78 1.66-3.57 3.55-3.57 4.35 0 1.5 0 9-2.5 10.5s-6 .5-6.5 2c-.4 1.2-1.83 10.5-2.5 15",
|
||||
},
|
||||
{
|
||||
title: "Venus Medical Center",
|
||||
category: "Health care",
|
||||
coordinates: { x: 1822, y: 1146 },
|
||||
travelTime: 4,
|
||||
path: "m1869.5 1149.5 7.67-5.25c.98-.67 2.32-.35 2.89.69l4.11 7.55c.48.87.24 1.95-.55 2.55l-62.52 47.25c-.89.67-2.14.5-2.81-.39l-13.1-17.33c-.66-.87-.5-2.11.37-2.78l63.44-49.29c.5-.5 1.77-1.3.5-3-1.5-2-3.67.5-5 1.5l-31.72 24.62c-.95.73-2.32.47-2.94-.55L1828 1152",
|
||||
},
|
||||
{
|
||||
title: "International Medical Company",
|
||||
category: "Health care",
|
||||
coordinates: { x: 1737, y: 1092 },
|
||||
travelTime: 9,
|
||||
path: "m1869.5 1149.5 7.67-5.25c.98-.67 2.32-.35 2.89.69l4.12 7.55c.47.87.24 1.95-.55 2.55l-63.06 47.77c-.87.66-2.11.5-2.78-.36l-13.6-17.43a1.99 1.99 0 0 0-2.75-.38l-7.77 5.65c-.92.66-2.2.44-2.83-.49l-65.2-95.62c-.62-.92-.38-2.18.56-2.8l17.18-11.31c.9-.6 2.11-.36 2.73.52l8.29 11.84c.61.88.43 2.08-.43 2.73l-7.33 5.59c-.9.68-2.18.49-2.84-.43l-1.3-1.82",
|
||||
},
|
||||
{
|
||||
title: "Abu Hamour Petrol Station",
|
||||
category: "Other",
|
||||
coordinates: { x: 1931, y: 1196 },
|
||||
travelTime: 4,
|
||||
path: "m1869.5 1149.5 7.68-5.26c.98-.66 2.31-.36 2.88.68 8.45 15.17 25.51 44.27 33.94 53.08 2 2.5 7.5 5.8 13.5-1 7.5-8.5 8.5-10 10.5-8.5 1.49 1.11 1.07 2.6.61 3.34-.07.11-.16.2-.25.29-1.88 1.71-5.58 5.2-6.36 6.37-.8 1.2 3 2.83 5 3.5",
|
||||
},
|
||||
];
|
||||
+15
-8
@@ -5,19 +5,13 @@ import DefaultLayout from "./components/layouts/DefaultLayout.tsx";
|
||||
import AboutPage from "./components/pages/AboutPage.tsx";
|
||||
import ContactsPage from "./components/pages/ContactsPage.tsx";
|
||||
import PopupContainer from "./components/ui/PopupContainer.tsx";
|
||||
import SurroundingsPage from "./components/pages/SurroundingsPage.tsx";
|
||||
import LayoutWithFooter from "./components/layouts/LayoutWithFooter.tsx";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <DefaultLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <></>,
|
||||
},
|
||||
{
|
||||
path: "/surroundings",
|
||||
element: <></>,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
element: <AboutPage />,
|
||||
@@ -28,6 +22,19 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <LayoutWithFooter />,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <></>,
|
||||
},
|
||||
{
|
||||
path: "/surroundings",
|
||||
element: <SurroundingsPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
|
||||
Reference in New Issue
Block a user