Created
May 10, 2023 14:20
-
-
Save iam-rohid/aebde17198bda9d6509bfaa881f8a88f to your computer and use it in GitHub Desktop.
Leaflet Seller Map Component with NextJS 13.4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.leaflet-container { | |
outline: none; | |
} | |
.seller-marker-popup .seller-marker-popup-name { | |
font-weight: 600; | |
margin: 4px 0; | |
} | |
.seller-marker-popup .seller-marker-popup-sales { | |
margin: 0; | |
} | |
.seller-marker-icon { | |
background-color: rgba(251, 191, 36, 0.3); | |
border: 1px solid rgb(251, 191, 36); | |
border-radius: 9999px; | |
} | |
.cluster-marker-icon { | |
background-color: #4f7bff; | |
border-radius: 9999px; | |
display: flex !important; | |
align-items: center; | |
justify-content: center; | |
color: #fff; | |
text-shadow: 0 1px 0px rgba(0, 0, 0, 0.4); | |
font-weight: 500; | |
text-align: center; | |
font-size: 16px; | |
} | |
.spider-leg-polyline { | |
stroke-width: 1px; | |
stroke: rgba(0, 0, 0, 0.2); | |
transition: all ease-in; | |
} | |
.leaflet-left .leaflet-control { | |
@apply ml-4 mt-4 overflow-hidden rounded-lg border border-slate-200 shadow-xl; | |
} | |
.leaflet-touch .leaflet-bar a { | |
@apply flex h-10 w-10 items-center justify-center bg-white text-2xl hover:bg-slate-100; | |
} | |
.leaflet-touch .leaflet-bar a.leaflet-disabled { | |
@apply bg-white; | |
} | |
.leaflet-touch .leaflet-bar a span { | |
@apply leading-4; | |
} | |
.leaflet-popup-content-wrapper { | |
@apply rounded-lg border border-slate-200 bg-white shadow-xl; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import L from "leaflet"; | |
import "leaflet.markercluster"; | |
import { useEffect, useRef } from "react"; | |
import { range } from "@/utils/range"; | |
import "leaflet.markercluster/dist/MarkerCluster.Default.css"; | |
import "leaflet.markercluster/dist/MarkerCluster.css"; | |
import "leaflet/dist/leaflet.css"; | |
import "./seller-map.css"; | |
export type Seller = { | |
id: number; | |
name: string; | |
estimate_sales: number; | |
latitude: number; | |
longitude: number; | |
}; | |
export type SellerMapProps = { | |
sellers: Seller[]; | |
onItemClick?: (seller: Seller) => void; | |
}; | |
export default function SellerMap({ sellers, onItemClick }: SellerMapProps) { | |
const mapDivRef = useRef<HTMLDivElement | null>(null); | |
const mapRef = useRef<L.Map | null>(null); | |
useEffect(() => { | |
if (typeof window === "undefined") return; | |
if (!mapDivRef.current) return; | |
mapRef.current = L.map(mapDivRef.current).setView([51.505, -0.09], 4); | |
L.tileLayer( | |
"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", | |
{ | |
attribution: | |
'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' + | |
'<a href="https://carto.com/">CARTO</a>', | |
minZoom: 3, | |
maxZoom: 19, | |
} | |
).addTo(mapRef.current); | |
return () => { | |
mapRef.current?.remove(); | |
}; | |
}, [mapDivRef, mapRef]); | |
useEffect(() => { | |
if (!mapRef.current) return; | |
if (!sellers?.length) return; | |
const sortedData = sellers.sort((a, b) => | |
(a.estimate_sales || 0) > (b.estimate_sales || 0) ? 1 : -1 | |
); | |
let sum = 0; | |
sortedData.forEach((seller) => { | |
sum += seller.estimate_sales || 0; | |
}); | |
const avg = sum / sellers.length; | |
const markers = L.markerClusterGroup({ | |
animate: true, | |
animateAddingMarkers: true, | |
maxClusterRadius: 100, | |
disableClusteringAtZoom: 8, | |
spiderfyOnMaxZoom: true, | |
spiderLegPolylineOptions: { | |
className: "spider-leg-polyline", | |
}, | |
iconCreateFunction: (cluster) => { | |
const count = cluster.getChildCount(); | |
const iconSize = range(0, sortedData.length, 30, 100, count); | |
return L.divIcon({ | |
html: count.toLocaleString(), | |
className: "cluster-marker-icon", | |
iconSize: [iconSize, iconSize], | |
}); | |
}, | |
}); | |
sortedData.forEach((seller) => { | |
const point = L.latLng(seller.latitude || 0, seller.longitude || 0); | |
const iconSize = Math.max( | |
10, | |
Math.min(60, Math.round(((seller.estimate_sales || 0) * 40) / avg)) | |
); | |
const marker = L.marker(point, { | |
icon: L.divIcon({ | |
className: "seller-marker-icon", | |
iconSize: [iconSize, iconSize], | |
}), | |
}); | |
const popupContent = `<div class='seller-marker-popup'> | |
<p class='seller-marker-popup-name'> | |
${seller.name} | |
</p> | |
<p class='seller-marker-popup-sales'> | |
$${(seller.estimate_sales || 0).toLocaleString()} | |
</p> | |
</div>`; | |
marker.bindPopup(popupContent, { | |
closeButton: false, | |
}); | |
marker.on("mouseover", () => marker.openPopup()); | |
marker.on("mouseout", () => marker.closePopup()); | |
marker.on("click", () => onItemClick && onItemClick(seller)); | |
markers.addLayer(marker); | |
}); | |
mapRef.current.addLayer(markers); | |
return () => { | |
mapRef.current?.removeLayer(markers); | |
}; | |
}, [mapRef, sellers, onItemClick]); | |
return <div ref={mapDivRef} className="z-0 h-full w-full" />; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment