From 1497d6ede3e1e7cc75d5e3562e600f66f980edca Mon Sep 17 00:00:00 2001 From: C4rnivore Date: Thu, 24 Jul 2025 17:27:14 +0500 Subject: [PATCH] Markers clusterization, mobile version of map --- src/components/AboutMarasiDrive.tsx | 7 +- src/components/GoogleMap.tsx | 120 ++++++++---------- src/components/GoogleMapFilterButtons.tsx | 64 ++++++++++ src/components/GoogleMapMarkers.tsx | 102 +++++++++++++++ src/components/MarasiDriveMapMobile.tsx | 53 +++++++- src/components/icons/map/AllIcon.tsx | 20 +++ .../icons/map/EntertainmentIcon.tsx | 19 +++ src/components/icons/map/HotelIcon.tsx | 19 +++ src/components/icons/map/MallsIcon.tsx | 20 +++ src/components/icons/map/OtherIcon.tsx | 21 +++ src/components/ui/MapMarker.tsx | 31 ++++- src/data/mapMarasiDrive.ts | 5 - src/data/marasiDriveMapData.tsx | 66 ++++++++++ src/types/IGMapPoi.ts | 5 + 14 files changed, 463 insertions(+), 89 deletions(-) create mode 100644 src/components/GoogleMapFilterButtons.tsx create mode 100644 src/components/GoogleMapMarkers.tsx create mode 100644 src/components/icons/map/AllIcon.tsx create mode 100644 src/components/icons/map/EntertainmentIcon.tsx create mode 100644 src/components/icons/map/HotelIcon.tsx create mode 100644 src/components/icons/map/MallsIcon.tsx create mode 100644 src/components/icons/map/OtherIcon.tsx delete mode 100644 src/data/mapMarasiDrive.ts create mode 100644 src/data/marasiDriveMapData.tsx diff --git a/src/components/AboutMarasiDrive.tsx b/src/components/AboutMarasiDrive.tsx index 95de612..3b1a9ac 100644 --- a/src/components/AboutMarasiDrive.tsx +++ b/src/components/AboutMarasiDrive.tsx @@ -16,7 +16,7 @@ import GoogleMap from "./GoogleMap"; import MarasiDriveNeighboursSliderTablet from "./MarasiDriveNeighboursSliderTablet"; import CustomScrollBar from "./ui/ScrollBar"; import BrochureButton from "./ui/BrochureButton"; -import { marasiDriveMapData } from "../data/mapMarasiDrive"; +import { MarasiDriveMapData } from "../data/marasiDriveMapData"; function AboutMarasiDrive() { const target = useRef(null); @@ -239,7 +239,10 @@ function AboutMarasiDrive() { ))}
- +
diff --git a/src/components/GoogleMap.tsx b/src/components/GoogleMap.tsx index 2abe9c1..496b5b9 100644 --- a/src/components/GoogleMap.tsx +++ b/src/components/GoogleMap.tsx @@ -1,88 +1,68 @@ -/* eslint-disable no-loss-of-precision */ -import { Map } from "@vis.gl/react-google-maps"; -import MapMarker from "./ui/MapMarker"; import IGMapPoi from "../types/IGMapPoi"; -import MarasiMarker from "../../public/images/map/markers/marasi-drive.png"; -import MarasiPopup from "../../public/images/map/markers/popups/marasi-drive.png"; -import MarinaMarker from "../../public/images/map/markers/dubai-marina.png"; -import MarinaPopup from "../../public/images/map/markers/popups/dubai-marina.png"; -import ShopIcon from "../../public/images/map/markers/points/shop-point.svg"; +import GoogleMapMarkers from "./GoogleMapMarkers"; +import { Map, useMap } from "@vis.gl/react-google-maps"; +import { useEffect, useState } from "react"; +import GoogleMapFilterButtons from "./GoogleMapFilterButtons"; interface IGMapProps { mapCenter: google.maps.LatLngLiteral; defaultZoom?: number; markers?: IGMapPoi[]; minZoom?: number; - disableUI?: boolean; + mobile?: boolean; + mobileActive?: boolean; } export default function GoogleMap({ mapCenter, + markers, defaultZoom = 14, minZoom = 10, - disableUI = true, + mobile = false, + mobileActive = false, }: IGMapProps) { + const [mapMarkersFilter, setMapMarkersFilter] = useState("All"); + const [isInteractale, setIsInteractale] = useState(!mobile); + const map = useMap(); + + useEffect(() => { + if (!map) return; + + setIsInteractale(!mobile || (mobile && mobileActive)); + if (!mobileActive && mobile) { + map.panTo(mapCenter); + map.setZoom(defaultZoom); + } + }, [mobile, mobileActive, map]); + + useEffect(() => { + if (!mobileActive) setMapMarkersFilter("All"); + }, [mobileActive]); + return ( - - - - ); -} - -function MapMarkers() { - const marasiDriveMarker = ( -
- - -
- ); - - const DubaiMarinaMarker = ( -
- - -
- ); - - const shopMarker = ( -
- -
- ); - - const templateMarkers: IGMapPoi[] = [ - { - location: { lat: 25.181504160790247, lng: 55.27565159760525 }, - customMarker: marasiDriveMarker, - }, - { - location: { lat: 25.069466431595334, lng: 55.128736429300375 }, - customMarker: DubaiMarinaMarker, - }, - { - location: { lat: 25.193476007744233, lng: 55.274782084720286 }, - customMarker: shopMarker, - }, - { - location: { lat: 25.193476007744233, lng: 55.244782084720286 }, - customMarker: shopMarker, - }, - ]; - - return ( - <> - {templateMarkers.map((poi: IGMapPoi, index: number) => ( - - {poi.customMarker} - - ))} - + + {markers && ( + + )} + + + ); } diff --git a/src/components/GoogleMapFilterButtons.tsx b/src/components/GoogleMapFilterButtons.tsx new file mode 100644 index 0000000..9d29227 --- /dev/null +++ b/src/components/GoogleMapFilterButtons.tsx @@ -0,0 +1,64 @@ +import { useState } from "react"; +import AllIcon from "./icons/map/AllIcon"; +import EntertainmentIcon from "./icons/map/EntertainmentIcon"; +import HotelIcon from "./icons/map/HotelIcon"; +import MallsIcon from "./icons/map/MallsIcon"; +import OtherIcon from "./icons/map/OtherIcon"; +import Button from "./ui/Button"; + +export default function GoogleMapFilterButtons({ + mobile, + mobileActive, + currentFilter, + onChange, +}: { + mobile: boolean; + mobileActive: boolean; + currentFilter: string; + onChange: (newFilter: string) => void; +}) { + const filters = ["All", "Hotels", "Malls", "Entertainment", "Other"]; + const [expanded, setExpanded] = useState(false); + const buttonsVisisble = (expanded && mobile && mobileActive) || !mobile; + + return ( +
+ {buttonsVisisble && + filters.map((key) => ( + + ))} + + {mobile && mobileActive && ( + + )} +
+ ); +} diff --git a/src/components/GoogleMapMarkers.tsx b/src/components/GoogleMapMarkers.tsx new file mode 100644 index 0000000..5d06308 --- /dev/null +++ b/src/components/GoogleMapMarkers.tsx @@ -0,0 +1,102 @@ +import { useMap } from "@vis.gl/react-google-maps"; +import { useEffect, useRef, useState } from "react"; +import IGMapPoi from "../types/IGMapPoi"; +import MapMarker from "./ui/MapMarker"; +import { + MarkerClusterer, + DefaultRenderer, + Cluster, + Marker, +} from "@googlemaps/markerclusterer"; +import { AnimatePresence, motion } from "motion/react"; + +class CustomMarkerRenderer extends DefaultRenderer { + render({ + count, + position, + }: Cluster): google.maps.marker.AdvancedMarkerElement { + const svgElement = document.createElement("div"); + svgElement.innerHTML = ` + + + +
+ ${count} +
`; + + // Create the AdvancedMarkerElement + const marker = new google.maps.marker.AdvancedMarkerElement({ + position, + content: svgElement, // Use the custom SVG element as content + zIndex: Math.max(1000, count), + }); + + return marker; + } +} + +export default function GoogleMapMarkers({ + data, + filter, +}: { + data: IGMapPoi[]; + filter: string; +}) { + const map = useMap(); + const [markers, setMarkers] = useState<{ [key: string]: Marker }>({}); + const clusterer = useRef(null); + + useEffect(() => { + if (!map) return; + if (!clusterer.current) { + clusterer.current = new MarkerClusterer({ + map: map, + renderer: new CustomMarkerRenderer(), + }); + } + }, [map]); + + useEffect(() => { + clusterer.current?.clearMarkers(); + clusterer.current?.addMarkers(Object.values(markers)); + }, [markers]); + + const setMarkerRef = (marker: Marker | null, key: string) => { + if (marker && markers[key]) return; + if (!marker && !markers[key]) return; + + setMarkers((prev) => { + if (marker) { + return { ...prev, [key]: marker }; + } else { + const newMarkers = { ...prev }; + delete newMarkers[key]; + return newMarkers; + } + }); + }; + + return ( + + {data.map( + (poi: IGMapPoi, index: number) => + (filter === poi.type || filter === "All") && ( + + + {poi.customMarker} + + + ) + )} + + ); +} diff --git a/src/components/MarasiDriveMapMobile.tsx b/src/components/MarasiDriveMapMobile.tsx index e786c7a..0363c85 100644 --- a/src/components/MarasiDriveMapMobile.tsx +++ b/src/components/MarasiDriveMapMobile.tsx @@ -1,18 +1,52 @@ -import { useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { marasiDriveMapCards } from "../data/aboutMarasiDrive"; import CustomScrollBar from "./ui/ScrollBar"; +import { MarasiDriveMapData } from "../data/marasiDriveMapData"; +import GoogleMap from "./GoogleMap"; +import Button from "./ui/Button"; +import FullScreenIcon from "./icons/FullScreenIcon"; +import CloseIcon from "./icons/CloseIcon"; function MarasiDriveMapMobile() { const containerRef = useRef(null); + const mapRef = useRef(null); + const [mapActive, setMapActive] = useState(false); + + useEffect(() => { + if (mapActive) { + mapRef.current?.scrollIntoView(); + window.scrollBy({ + top: -40, + left: 0, + behavior: "smooth", + }); + } + }, [mapActive]); return (
-
- + +
))}
- +
); } diff --git a/src/components/icons/map/AllIcon.tsx b/src/components/icons/map/AllIcon.tsx new file mode 100644 index 0000000..6b9773d --- /dev/null +++ b/src/components/icons/map/AllIcon.tsx @@ -0,0 +1,20 @@ +import { SVGProps } from "react"; + +const AllIcon = (props: SVGProps) => ( + + + +); +export default AllIcon; diff --git a/src/components/icons/map/EntertainmentIcon.tsx b/src/components/icons/map/EntertainmentIcon.tsx new file mode 100644 index 0000000..e39bca0 --- /dev/null +++ b/src/components/icons/map/EntertainmentIcon.tsx @@ -0,0 +1,19 @@ +import { SVGProps } from "react"; + +const EntertainmentIcon = (props: SVGProps) => ( + + + +); +export default EntertainmentIcon; diff --git a/src/components/icons/map/HotelIcon.tsx b/src/components/icons/map/HotelIcon.tsx new file mode 100644 index 0000000..4024a7d --- /dev/null +++ b/src/components/icons/map/HotelIcon.tsx @@ -0,0 +1,19 @@ +import { SVGProps } from "react"; + +const HotelIcon = (props: SVGProps) => ( + + + +); +export default HotelIcon; diff --git a/src/components/icons/map/MallsIcon.tsx b/src/components/icons/map/MallsIcon.tsx new file mode 100644 index 0000000..8786ccb --- /dev/null +++ b/src/components/icons/map/MallsIcon.tsx @@ -0,0 +1,20 @@ +import { SVGProps } from "react"; + +const MallsIcon = (props: SVGProps) => ( + + + +); +export default MallsIcon; diff --git a/src/components/icons/map/OtherIcon.tsx b/src/components/icons/map/OtherIcon.tsx new file mode 100644 index 0000000..8930e70 --- /dev/null +++ b/src/components/icons/map/OtherIcon.tsx @@ -0,0 +1,21 @@ +import { SVGProps } from "react"; + +const OtherIcon = (props: SVGProps) => ( + + + +); +export default OtherIcon; diff --git a/src/components/ui/MapMarker.tsx b/src/components/ui/MapMarker.tsx index 1fdc5f4..1f41548 100644 --- a/src/components/ui/MapMarker.tsx +++ b/src/components/ui/MapMarker.tsx @@ -1,18 +1,39 @@ -import { AdvancedMarker, Pin } from "@vis.gl/react-google-maps"; +import { AdvancedMarker, Pin, useMap } from "@vis.gl/react-google-maps"; +import type { Marker } from "@googlemaps/markerclusterer"; +import IGMapPoi from "../../types/IGMapPoi"; interface IGMapMarker { - key: string; - location: google.maps.LatLngLiteral; + markerKey: number; + poi: IGMapPoi; + setMarkerRef: (marker: Marker | null, key: string) => void | undefined; children: React.ReactNode; } -export default function MapMarker({ key, location, children }: IGMapMarker) { +export default function MapMarker({ + markerKey, + poi, + setMarkerRef, + children, +}: IGMapMarker) { + const map = useMap(); + return ( - + { + map?.panTo(poi.location); + map?.setZoom(15); + }} + key={markerKey} + position={poi.location} + ref={(marker) => + !poi.ignoreClusterization && setMarkerRef(marker, markerKey.toString()) + } + > {children} diff --git a/src/data/mapMarasiDrive.ts b/src/data/mapMarasiDrive.ts deleted file mode 100644 index 56fc364..0000000 --- a/src/data/mapMarasiDrive.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const marasiDriveMapData = { - mapCenter: { lat: 25.183476007744233, lng: 55.274782084720286 } -} - - diff --git a/src/data/marasiDriveMapData.tsx b/src/data/marasiDriveMapData.tsx new file mode 100644 index 0000000..92d9cf6 --- /dev/null +++ b/src/data/marasiDriveMapData.tsx @@ -0,0 +1,66 @@ +/* eslint-disable no-loss-of-precision */ +import MarasiMarker from "../../public/images/map/markers/marasi-drive.png"; +import MarinaMarker from "../../public/images/map/markers/dubai-marina.png"; +import ShopIcon from "../../public/images/map/markers/points/shop-point.svg"; +import IGMapPoi from "../types/IGMapPoi"; + +const marasiDriveMarker = ( +
+ +
+); + +const DubaiMarinaMarker = ( +
+ +
+); + +const shopMarker = ( +
+ +
+); + +export const MarasiDriveMapData: { + defaultCenter: { lat: number; lng: number }; + markers: IGMapPoi[]; +} = { + defaultCenter: { lat: 25.183476007744233, lng: 55.274782084720286 }, + markers: [ + { + location: { lat: 25.223476007744233, lng: 55.274782084720286 }, + type: "Malls", + customMarker: shopMarker, + }, + { + location: { lat: 25.123476007744233, lng: 55.258782084720286 }, + type: "Malls", + customMarker: shopMarker, + }, + { + location: { lat: 25.143476007744233, lng: 55.224782084720286 }, + type: "Malls", + customMarker: shopMarker, + }, + { + location: { lat: 25.223476007744233, lng: 55.278782084720286 }, + type: "Malls", + customMarker: shopMarker, + }, + { + location: { lat: 25.181371868546396, lng: 55.27515332907251 }, + type: "Hotels", + ignoreClusterization: true, + customMarker: marasiDriveMarker, + clickableAreaScale: 3, + }, + { + location: { lat: 25.069466431595334, lng: 55.128736429300375 }, + type: "Hotels", + ignoreClusterization: true, + customMarker: DubaiMarinaMarker, + clickableAreaScale: 3, + }, + ], +}; diff --git a/src/types/IGMapPoi.ts b/src/types/IGMapPoi.ts index 4c0f681..28f45a5 100644 --- a/src/types/IGMapPoi.ts +++ b/src/types/IGMapPoi.ts @@ -1,4 +1,9 @@ export default interface IGMapPoi { location: google.maps.LatLngLiteral; + ignoreClusterization?:boolean; + type:"All" | "Hotels" | "Malls" | "Entertainment"| "Other"; + + // Scale multiplyer for Pin (MapMarker.tsx) component. (To match visible marker size). + clickableAreaScale?:number; customMarker?: React.ReactNode; } \ No newline at end of file