Add SurroundingsPage with interactive map and location popups; include new layout and category filter components

This commit is contained in:
2025-11-25 16:08:23 +05:00
parent e9ddbe07d7
commit 90cacefc8f
8 changed files with 968 additions and 8 deletions
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;
+679
View File
@@ -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;
+24
View File
@@ -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;
+50
View File
@@ -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;
+178
View File
@@ -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
View File
@@ -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(