diff --git a/public/img/surroundings/location.png b/public/img/surroundings/location.png new file mode 100644 index 0000000..49e928f Binary files /dev/null and b/public/img/surroundings/location.png differ diff --git a/public/img/surroundings/map.png b/public/img/surroundings/map.png new file mode 100644 index 0000000..525b74d Binary files /dev/null and b/public/img/surroundings/map.png differ diff --git a/src/components/layouts/LayoutWithFooter.tsx b/src/components/layouts/LayoutWithFooter.tsx new file mode 100644 index 0000000..0ea8534 --- /dev/null +++ b/src/components/layouts/LayoutWithFooter.tsx @@ -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 ( +
+ +
+ +
+
+ ); +} + +export default LayoutWithFooter; diff --git a/src/components/pages/SurroundingsPage.tsx b/src/components/pages/SurroundingsPage.tsx new file mode 100644 index 0000000..ee6209f --- /dev/null +++ b/src/components/pages/SurroundingsPage.tsx @@ -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({ + width: 0, + height: 0, + }); + + const mapRef = useRef(null); + const containerRef = useRef(null); + + const containerSizeRef = useRef({ width: 0, height: 0 }); + + const [isDragging, setIsDragging] = useState(false); + const [startPosition, setStartPosition] = useState({ x: 0, y: 0 }); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(0); + const previousTouchDistance = useRef(null); + const initialTouchDistance = useRef(null); + const minZoomRef = useRef(1); + const markersContainerRef = useRef(null); + const [hoveredPoint, setHoveredPoint] = useState( + null + ); + const [selectedPoint, setSelectedPoint] = useState( + null + ); + const [isImageLoaded, setIsImageLoaded] = useState(false); + const [lastClickTime, setLastClickTime] = useState(0); + const [selectedCategories, setSelectedCategories] = useState>( + 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) => { + 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) => { + 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 | React.TouchEvent + ) => { + 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 | React.TouchEvent + ) => { + 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 | React.TouchEvent + ) => { + 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, + 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( + + ); + + // Определяем сторону для попапа + 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, + 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 ( + //
+ // {/* Переключатель категорий */} + //
+ //
+ //
+ //

+ // Baraha Town + //

+ //

+ // Select location on the map + //

+ //
+ //
+ // + //
+ //
+ +
{ + handleEnd(); + handlePointMouseLeave(); + }} + onMouseMove={handleMouseMove} + onClick={handleClick} + > + {containerRef.current?.clientWidth && ( + map + )} + + {/* SVG для путей из Figma */} + + {selectedPoint && ( + <> + {Array.isArray(selectedPoint.path) ? ( + // Если path - массив, отрисовываем два пути + selectedPoint.path.map((pathData, index) => { + const color = + categories.get(selectedPoint.category) || "#F47F52"; + return ( + + ); + }) + ) : ( + // Если path - строка, отрисовываем один путь + <> + + + + )} + + )} + + + {/* Контейнер для точек */} +
+
+ {/* Центральная точка (Baraha Town) */} +
+
+ { + if (!el) return; + el.style.minWidth = `${el.naturalWidth}px`; + }} + src="/img/surroundings/location.png" + className="select-none" + draggable={false} + /> +
+
+ {/* Точки на карте */} + {filteredPoints.map((point) => { + const color = categories.get(point.category) || "#F47F52"; + + return ( +
handlePointMouseEnter(e, point)} + onMouseLeave={handlePointMouseLeave} + onClick={(e) => handlePointClick(e, point)} + > +
+
+ ); + })} +
+
+
+ //
+ ); +} + +export default SurroundingsPage; diff --git a/src/components/popups/LocationPopup.tsx b/src/components/popups/LocationPopup.tsx new file mode 100644 index 0000000..7edc90b --- /dev/null +++ b/src/components/popups/LocationPopup.tsx @@ -0,0 +1,24 @@ +import CarIcon from "../icons/CarIcon"; + +interface LocationPopupProps { + title: string; + travelTime: number; +} + +function LocationPopup({ title, travelTime }: LocationPopupProps) { + return ( +
+

+ {title} +

+
+
+ +
+

{travelTime} min

+
+
+ ); +} + +export default LocationPopup; diff --git a/src/components/ui/CategoryFilter.tsx b/src/components/ui/CategoryFilter.tsx new file mode 100644 index 0000000..ec64c67 --- /dev/null +++ b/src/components/ui/CategoryFilter.tsx @@ -0,0 +1,50 @@ +import clsx from "clsx"; +import { categories } from "../../consts/surroundingPoints"; + +interface CategoryFilterProps { + selectedCategories: Set; + onToggleCategory: (category: string) => void; +} + +function CategoryFilter({ + selectedCategories, + onToggleCategory, +}: CategoryFilterProps) { + return ( +
+ {Array.from(categories.entries()).map(([category, color]) => { + const isSelected = selectedCategories.has(category); + return ( + + ); + })} +
+ ); +} + +export default CategoryFilter; diff --git a/src/consts/surroundingPoints.ts b/src/consts/surroundingPoints.ts new file mode 100644 index 0000000..cff5d74 --- /dev/null +++ b/src/consts/surroundingPoints.ts @@ -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", + }, +]; diff --git a/src/main.tsx b/src/main.tsx index 7feb222..b8b50b8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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: , children: [ - { - path: "/", - element: <>, - }, - { - path: "/surroundings", - element: <>, - }, { path: "/about", element: , @@ -28,6 +22,19 @@ const router = createBrowserRouter([ }, ], }, + { + element: , + children: [ + { + path: "/", + element: <>, + }, + { + path: "/surroundings", + element: , + }, + ], + }, ]); createRoot(document.getElementById("root")!).render(