This commit is contained in:
2025-07-28 16:00:24 +05:00
parent 72bfdeb5a3
commit a1b896b37f
9 changed files with 24 additions and 26 deletions
+69
View File
@@ -0,0 +1,69 @@
import IGMapPoi from "../../types/IGMapPoi";
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;
mobile?: boolean;
mobileActive?: boolean;
}
export type MapFilter = "All" | "Hotels" | "Malls" | "Entertainment" | "Other";
export default function GoogleMap({
mapCenter,
markers,
defaultZoom = 14,
minZoom = 10,
mobile = false,
mobileActive = false,
}: IGMapProps) {
const [mapMarkersFilter, setMapMarkersFilter] = useState<MapFilter>("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 (
<div
className={`size-[100%] ${
isInteractale ? "pointer-events-auto" : "pointer-events-none"
}`}
>
<Map
mapId={import.meta.env.VITE_GOOGLE_MAP_ID}
defaultCenter={mapCenter}
defaultZoom={defaultZoom}
disableDefaultUI={true}
minZoom={minZoom}
gestureHandling={!mobile || mobileActive ? "greedy" : "none"}
>
{markers && (
<GoogleMapMarkers data={markers} filter={mapMarkersFilter} />
)}
{/* <GoogleMapFilterButtons
mobile={mobile}
mobileActive={mobileActive}
currentFilter={mapMarkersFilter}
onChange={setMapMarkersFilter}
/> */}
</Map>
</div>
);
}
@@ -0,0 +1,89 @@
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";
import { AnimatePresence, motion } from "motion/react";
import { MapFilter } from "./GoogleMap";
export default function GoogleMapFilterButtons({
mobile,
mobileActive,
currentFilter,
onChange,
}: {
mobile: boolean;
mobileActive: boolean;
currentFilter: MapFilter;
onChange: (newFilter: MapFilter) => void;
}) {
const filters: MapFilter[] = [
"All",
"Hotels",
"Malls",
"Entertainment",
"Other",
];
const [expanded, setExpanded] = useState(false);
const buttonsVisisble = (expanded && mobile && mobileActive) || !mobile;
const IconsByFilter = {
All: <AllIcon />,
Hotels: <HotelIcon />,
Malls: <MallsIcon />,
Entertainment: <EntertainmentIcon />,
Other: <OtherIcon />,
};
return (
<div
className={`flex gap-[0.556vw] absolute items-center bottom-[1.111vw] left-1/2 -translate-x-1/2 ${
mobile &&
`flex-col gap-[1.111vw] w-full pt-5 translate-y-2 ${
expanded && "backdrop-blur-[1px]"
}`
}`}
>
<AnimatePresence>
{buttonsVisisble &&
filters.map((key) => (
<motion.div
initial={{ translateY: 150, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
exit={{ translateY: 150, opacity: 0 }}
// transition={{ duration: 0.1, delay: index * 0.1 }}
key={key}
>
<Button
onClick={() => onChange(key)}
variant={key === currentFilter ? "cta" : "secondary"}
>
<div className="size-5">{IconsByFilter[key]}</div>
{key}
</Button>
</motion.div>
))}
</AnimatePresence>
{mobile && mobileActive && (
<Button
onClick={() => setExpanded(!expanded)}
variant={expanded ? "cta" : "secondary"}
className="my-4 px-[3.889vw] py-[2.778vw] z-10 h-10 transition-all"
>
{!expanded && <div>{IconsByFilter[currentFilter]}</div>}
{expanded ? (
<span>Apply</span>
) : currentFilter === "All" ? (
<span>Select Category</span>
) : (
<span>{currentFilter}</span>
)}
</Button>
)}
</div>
);
}
@@ -0,0 +1,94 @@
import { useMap } from "@vis.gl/react-google-maps";
import { useEffect, useRef, useState } from "react";
import IGMapPoi from "../../types/IGMapPoi";
import MapMarker from "./MapMarker";
import {
MarkerClusterer,
DefaultRenderer,
Cluster,
Marker,
} from "@googlemaps/markerclusterer";
class CustomMarkerRenderer extends DefaultRenderer {
render({
count,
position,
}: Cluster): google.maps.marker.AdvancedMarkerElement {
const svgElement = document.createElement("div");
svgElement.innerHTML = `
<svg fill="white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="50" height="50">
<circle cx="120" cy="120" opacity="1" r="70" />
</svg>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: black; font-size: 12px; font-weight: bold;">
${count}
</div>`;
const marker = new google.maps.marker.AdvancedMarkerElement({
position,
content: svgElement,
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<MarkerClusterer | null>(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") && (
<MapMarker
key={index}
markerKey={index}
poi={poi}
setMarkerRef={setMarkerRef}
>
{poi.customMarker}
</MapMarker>
)
)}
</>
);
}
@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
// import { marasiDriveMapCards } from "../data/aboutMarasiDrive";
// import CustomScrollBar from "./ui/ScrollBar";
// import { GoogleMapData } from "../data/googleMapData";
import GoogleMap from "./GoogleMap";
import Button from "../ui/Button";
import FullScreenIcon from "../icons/FullScreenIcon";
import useModalStore from "../../stores/useModalStore";
import IGMapPoi from "../../types/IGMapPoi";
function GoogleMapMobile({
markers,
mapCenter,
}: {
markers?: IGMapPoi[];
mapCenter: google.maps.LatLngLiteral;
}) {
// const containerRef = useRef<HTMLDivElement>(null);
const [mapActive, setMapActive] = useState(false);
const { modal, setModal } = useModalStore();
useEffect(() => {
if (mapActive) setModal(<MapModal />);
else setModal(null);
}, [mapActive]);
useEffect(() => {
setMapActive(!!modal);
}, [modal]);
const MapModal = () => {
return (
<div
className={`relative max-md:aspect-[360/640] md:max-2xl:aspect[1/1] max-2xl:mb-4 overflow-clip rounded-[4.444vw] h-[calc(100dvh-100px)]`}
>
<GoogleMap
mapCenter={mapCenter}
markers={markers}
mobileActive={true}
mobile={true}
/>
</div>
);
};
return (
<div className="md:hidden relative flex flex-col">
{
<div
className={` relative max-md:aspect-[328/328] scroll-mt-10 md:max-2xl:aspect[1/1] transition-all max-2xl:mb-4 overflow-clip rounded-[4.444vw]`}
>
<GoogleMap
mapCenter={mapCenter}
markers={markers}
mobileActive={false}
mobile={true}
/>
<Button
className={`absolute size-[11.111vw] right-[1.111vw] bottom-[1.111vw] `}
onClick={() => setMapActive(true)}
>
<div className="size-[5.556vw] text-[#0D1922]">
<FullScreenIcon />
</div>
</Button>
</div>
}
{/*
<div
ref={containerRef}
className="flex flex-nowrap overflow-x-scroll scroll-pl-4 gap-x-[16px] overflow-y-hidden justify-start snap-x snap-mandatory [&::-webkit-scrollbar]:h-[1.111vw] [&::-webkit-scrollbar]:w-[none] [&::-webkit-scrollbar-thumb]:bg-[#FFFFFF] [&::-webkit-scrollbar-thumb]:w-4 [&::-webkit-scrollbar-thumb]:rounded-full -mx-4 px-4 "
>
{marasiDriveMapCards.map((card, index) => (
<div key={index} className="snap-start">
<div
className={`rounded-[6.667vw] px-[4.444vw] py-[3.333vw] w-[51.111vw] aspect-[184/122] bg-[#F3F3F2] flex-shrink-0 flex flex-col justify-between relative md:max-2xl:w-[25vw] md:max-2xl:rounded-[3.125vw] md:max-2xl:px-[2.083vw] md:max-2xl:py-[1.563vw]`}
>
<div className="space-y-[0.278vw]">
<p className="text-m">{card.title}</p>
<p className="text-s text-[#73787C]">{`${card.mins} mins`}</p>
</div>
<img
src={card.image}
className="rounded-[0.278vw] size-[13.333vw] object-cover absolute bottom-[4.444vw] right-[3.333vw] md:max-2xl:size-[6.25vw] md:max-2xl:bottom-[1.563vw] md:max-2xl:right-[2.083vw]"
alt={card.title}
/>
</div>
</div>
))}
</div>
<CustomScrollBar
containerRef={containerRef}
inlinePadding={16}
trackStyle="min-2xl:hidden max-2xl:translate-y-5"
thumbStyle="min-2xl:hidden"
/> */}
</div>
);
}
export default GoogleMapMobile;
+47
View File
@@ -0,0 +1,47 @@
import { AdvancedMarker, useMap } from "@vis.gl/react-google-maps";
import type { Marker } from "@googlemaps/markerclusterer";
import IGMapPoi from "../../types/IGMapPoi";
interface IGMapMarker {
markerKey: number;
poi: IGMapPoi;
setMarkerRef: (marker: Marker | null, key: string) => void | undefined;
children: React.ReactNode;
}
export default function MapMarker({
poi,
children,
markerKey,
setMarkerRef,
}: IGMapMarker) {
const map = useMap();
const { location, ignoreClusterization, label } = poi;
return (
<AdvancedMarker
key={markerKey}
position={location}
ref={(marker) => {
if (!ignoreClusterization) {
setMarkerRef(marker, markerKey.toString());
}
}}
onClick={() => {
map?.panTo(location);
map?.setZoom(17);
}}
>
<div
className={`relative flex items-center gap-x-2 hover:[&>.label-container]:opacity-100 hover:[&>.label-container]:left-[calc(100%-38px)] hover:[&>.label-container]:pointer-events-auto hover:[&>.label-container]:z-140 hover:[&>.gmap-img-container]:z-150`}
>
{children}
{label && (
<div className="label-container text-black absolute text-s left-0 opacity-0 w-max pointer-events-none transition-[left,opacity] bg-white pl-11 pr-3 py-2.5 rounded-2xl">
{label}
</div>
)}
</div>
</AdvancedMarker>
);
}