upd
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user