├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── index.js ├── package.json ├── src ├── ClusterMap.js ├── GeoJsonMap.js ├── Map.js ├── Pin.js ├── Pins.js └── map-widget.css └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "autobind-class-methods", 9 | [ 10 | "babel-plugin-styled-components", 11 | { 12 | "preprocess": true 13 | } 14 | ] 15 | ], 16 | "retainLines": true 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | .vscode 4 | 5 | # Types 6 | typings 7 | typings.json 8 | 9 | # Environment 10 | node_modules 11 | gift_registry_hbc.env 12 | gr_api.env 13 | gr_client.env 14 | **/package-lock.json 15 | **/yarn.lock 16 | 17 | # Translations 18 | frontend/src/system/translations/*/*_old.json 19 | backend/api/translations/*/*_old.json 20 | updateTranslations/temp 21 | 22 | # Logs 23 | error.log 24 | logs/* 25 | 26 | # Docs 27 | /docs/dist/* 28 | orders.js 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Use this command to start: 2 | ``` 3 | npm run start-dev 4 | ``` 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React map with Leaflet 7 | 8 | 9 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Map from './src/Map'; 4 | import ClusterMap from './src/ClusterMap'; 5 | import GeoJsonMap from './src/GeoJsonMap'; 6 | 7 | ReactDOM.render(, document.getElementById('map')); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-map-with-leaflet", 3 | "version": "1.0.0", 4 | "description": "React map with Leaflet", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon server.js", 8 | "start-dev": "webpack-dev-server --open", 9 | "build": "webpack -p", 10 | "watch": "webpack --watch", 11 | "storybook": "start-storybook -p 6006", 12 | "build-storybook": "build-storybook" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "map", 17 | "leaflet" 18 | ], 19 | "author": "Eugene Belkovich", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "babel-core": "^6.26.3", 23 | "babel-loader": "^7.1.5", 24 | "babel-plugin-autobind-class-methods": "^5.0.1", 25 | "babel-plugin-styled-components": "^1.5.1", 26 | "babel-preset-es2015": "^6.24.1", 27 | "babel-preset-react": "^6.24.1", 28 | "babel-preset-stage-0": "^6.24.1", 29 | "babel-runtime": "^6.26.0", 30 | "css-loader": "^0.28.7", 31 | "html-loader": "^0.5.1", 32 | "html-webpack-plugin": "^2.30.1", 33 | "style-loader": "^0.19.1", 34 | "webpack": "^3.12.0", 35 | "webpack-dev-server": "^2.9.7" 36 | }, 37 | "dependencies": { 38 | "geojson-world-map": "^0.0.2", 39 | "leaflet": "^1.3.1", 40 | "leaflet.markercluster": "^1.3.0", 41 | "react": "^16.5.2", 42 | "react-dom": "^16.5.2", 43 | "react-leaflet": "^1.9.1", 44 | "react-leaflet-markercluster": "^1.1.8" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ClusterMap.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Map as LeafletMap, GeoJSON } from 'react-leaflet' 3 | import Pins from './Pins.js'; 4 | import worldGeoJSON from 'geojson-world-map'; 5 | 6 | class MapWidget extends Component { 7 | constructor(props) { 8 | super(props) 9 | let pins = [] 10 | for (let i = 0; i < 5000; i++) { 11 | pins.push([Math.floor(Math.random() * 50), Math.floor(Math.random() * 50)]); 12 | } 13 | this.state = { 14 | pins 15 | } 16 | } 17 | 18 | 19 | render() { 20 | 21 | const pins = this.state.pins 22 | return ( 23 | 35 | ({ 38 | color: '#4a83ec', 39 | weight: 0.5, 40 | fillColor: "#1a1d62", 41 | fillOpacity: 1, 42 | })} 43 | /> 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default MapWidget; 51 | -------------------------------------------------------------------------------- /src/GeoJsonMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Map as LeafletMap, GeoJSON, Marker, Popup } from 'react-leaflet'; 3 | import worldGeoJSON from 'geojson-world-map'; 4 | 5 | class GeoJsonMap extends React.Component { 6 | render() { 7 | return ( 8 | 20 | ({ 23 | color: '#4a83ec', 24 | weight: 0.5, 25 | fillColor: "#1a1d62", 26 | fillOpacity: 1, 27 | })} 28 | /> 29 | 30 | 31 | Popup for any custom information. 32 | 33 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | export default GeoJsonMap 40 | -------------------------------------------------------------------------------- /src/Map.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Map as LeafletMap, TileLayer, Marker, Popup } from 'react-leaflet'; 3 | 4 | class Map extends React.Component { 5 | render() { 6 | return ( 7 | 19 | 22 | 23 | 24 | Popup for any custom information. 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default Map 33 | -------------------------------------------------------------------------------- /src/Pin.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CircleMarker } from 'react-leaflet'; 3 | import './map-widget.css'; 4 | 5 | class Pin extends React.Component { 6 | render() { 7 | return ( 8 | 15 | ); 16 | } 17 | } 18 | 19 | export default Pin; 20 | -------------------------------------------------------------------------------- /src/Pins.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import L from 'leaflet'; 3 | import MarkerClusterGroup from 'react-leaflet-markercluster'; 4 | import 'react-leaflet-markercluster/dist/styles.min.css'; 5 | import Pin from './Pin.js'; 6 | import './map-widget.css'; 7 | 8 | const hexToRgb = (hex, opacity) => { 9 | if (!hex.startsWith('#')) return hex; 10 | const hashless = hex.slice(1); 11 | const num = parseInt( 12 | hashless.length === 3 13 | ? hashless.split('').map(c => c.repeat(2)).join('') 14 | : hashless, 15 | 16, 16 | ); 17 | const red = num >> 16; 18 | const green = (num >> 8) & 255; 19 | const blue = num & 255; 20 | 21 | if (opacity) { 22 | return `rgba(${red}, ${green}, ${blue}, ${opacity})`; 23 | } 24 | return `rgb(${red}, ${green}, ${blue})`; 25 | }; 26 | 27 | const Pins = (props) => { 28 | const { pins } = props; 29 | 30 | const markerStyle = { 31 | color: '#fc4b51', 32 | fillColor: "#fc4b51", 33 | opacity: 1, 34 | radius: 5, 35 | }; 36 | 37 | const createClusterCustomIcon = (cluster) => { 38 | const count = cluster.getChildCount(); 39 | let size = 'LargeXL'; 40 | 41 | if (count < 10) { 42 | size = 'Small'; 43 | } 44 | else if (count >= 10 && count < 100) { 45 | size = 'Medium'; 46 | } 47 | else if (count >= 100 && count < 500) { 48 | size = 'Large'; 49 | } 50 | const options = { 51 | cluster: `markerCluster${size}`, 52 | circle1: `markerCluster${size}DivOne`, 53 | circle2: `markerCluster${size}DivTwo`, 54 | circle3: `markerCluster${size}DivThree`, 55 | circle4: `markerCluster${size}DivFour`, 56 | label: `markerCluster${size}Label`, 57 | }; 58 | 59 | const clusterColor = hexToRgb('#fc4b51'); 60 | const circleStyle1 = `background-color: ${clusterColor.slice(0, -1)}, 0.05)`; 61 | const circleStyle2 = `background-color: ${clusterColor.slice(0, -1)}, 0.15)`; 62 | const circleStyle3 = `background-color: ${clusterColor.slice(0, -1)}, 0.25)`; 63 | const circleStyle4 = `background-color: ${clusterColor.slice(0, -1)}, 0.65)`; 64 | 65 | 66 | return L.divIcon({ 67 | html: 68 | `
69 |
70 |
71 |
72 | ${count} 73 |
74 |
75 |
76 |
`, 77 | className: `${options.cluster}`, 78 | }); 79 | }; 80 | 81 | const Markers = pins 82 | .map((p, i) => ( 83 | 88 | )); 89 | 90 | return ( 91 | 101 | {Markers} 102 | 103 | ); 104 | }; 105 | 106 | export default Pins; 107 | -------------------------------------------------------------------------------- /src/map-widget.css: -------------------------------------------------------------------------------- 1 | .mapContainer { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .mapContainer .item { 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | box-shadow: 31px -32px 70px 10px rgba(0, 0, 0, 0.55); 12 | position: relative; 13 | overflow: hidden; 14 | height: calc(100% - 16px) !important; 15 | } 16 | 17 | .mapContainer .textItemContainer { 18 | position: relative !important; 19 | } 20 | 21 | .mapContainer .textItemWrapper { 22 | overflow: hidden; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .flyoverContainer::before { 29 | content: ' '; 30 | position: relative; 31 | display: inline-block; 32 | width: 0; 33 | height: 0; 34 | border-style: solid; 35 | border-width: 100px 50px 0 0; 36 | border-color: transparent #fff transparent transparent; 37 | transform: rotate(210deg); 38 | opacity: 0.7; 39 | top: 385px; 40 | left: -20px; 41 | } 42 | 43 | .markerClusterSmall { 44 | border-radius: 20px; 45 | font-size: 0.6vw; 46 | color: #fff; 47 | font-weight: 900; 48 | font-family: Open Sans; 49 | text-transform: uppercase; 50 | } 51 | 52 | .markerClusterSmallDivOne { 53 | position: absolute; 54 | margin-left: -5px; 55 | margin-top: -5px; 56 | border-radius: 7.5em; 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | animation: fadein 3s; 61 | } 62 | 63 | .markerClusterSmallDivTwo { 64 | position: absolute; 65 | width: 5em; 66 | height: 5em; 67 | border-radius: 6em; 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | animation: fadein 2.5s; 72 | } 73 | 74 | .markerClusterSmallDivThree { 75 | width: 3.5em; 76 | height: 3.5em; 77 | border-radius: 4.5em; 78 | display: flex; 79 | justify-content: center; 80 | align-items: center; 81 | animation: fadein 2s; 82 | } 83 | 84 | .markerClusterSmallDivFour { 85 | width: 2em; 86 | height: 2em; 87 | border-radius: 3em; 88 | display: flex; 89 | justify-content: center; 90 | align-items: center; 91 | animation: fadein 1.5s; 92 | } 93 | 94 | .markerClusterSmallLabel { 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | } 99 | 100 | .markerClusterMedium { 101 | border-radius: 40px; 102 | font-size: 0.6vw; 103 | color: #fff; 104 | font-weight: 900; 105 | font-family: Open Sans; 106 | text-transform: uppercase; 107 | } 108 | 109 | .markerClusterMediumDivOne { 110 | position: absolute; 111 | margin-left: -5px; 112 | margin-top: -5px; 113 | border-radius: 9.5em; 114 | display: flex; 115 | justify-content: center; 116 | align-items: center; 117 | animation: fadein 3s; 118 | } 119 | 120 | .markerClusterMediumDivTwo { 121 | position: absolute; 122 | width: 6.5em; 123 | height: 6.5em; 124 | border-radius: 8em; 125 | display: flex; 126 | justify-content: center; 127 | align-items: center; 128 | animation: fadein 2.5s; 129 | } 130 | 131 | .markerClusterMediumDivThree { 132 | width: 5em; 133 | height: 5em; 134 | border-radius: 6.5em; 135 | display: flex; 136 | justify-content: center; 137 | align-items: center; 138 | animation: fadein 2s; 139 | } 140 | 141 | .markerClusterMediumDivFour { 142 | width: 3.5em; 143 | height: 3.5em; 144 | border-radius: 5em; 145 | display: flex; 146 | justify-content: center; 147 | align-items: center; 148 | animation: fadein 1.5s; 149 | } 150 | 151 | .markerClusterMediumLabel { 152 | display: flex; 153 | justify-content: center; 154 | align-items: center; 155 | } 156 | 157 | .markerClusterLarge { 158 | border-radius: 70px; 159 | font-size: 0.6vw; 160 | color: #fff; 161 | font-weight: 900; 162 | font-family: Open Sans; 163 | text-transform: uppercase; 164 | } 165 | 166 | .markerClusterLargeDivOne { 167 | position: absolute; 168 | margin-left: -5px; 169 | margin-top: -5px; 170 | border-radius: 12em; 171 | display: flex; 172 | justify-content: center; 173 | align-items: center; 174 | animation: fadein 3s; 175 | } 176 | 177 | .markerClusterLargeDivTwo { 178 | position: absolute; 179 | width: 9em; 180 | height: 9em; 181 | border-radius: 10.5em; 182 | display: flex; 183 | justify-content: center; 184 | align-items: center; 185 | animation: fadein 2.5s; 186 | } 187 | 188 | .markerClusterLargeDivThree { 189 | width: 7.5em; 190 | height: 7.5em; 191 | border-radius: 9em; 192 | display: flex; 193 | justify-content: center; 194 | align-items: center; 195 | animation: fadein 2s; 196 | } 197 | 198 | .markerClusterLargeDivFour { 199 | padding: 1px; 200 | width: 6em; 201 | height: 6em; 202 | border-radius: 7.5em; 203 | display: flex; 204 | justify-content: center; 205 | align-items: center; 206 | animation: fadein 1.5s; 207 | } 208 | 209 | .markerClusterLargeLabel { 210 | display: flex; 211 | justify-content: center; 212 | align-items: center; 213 | } 214 | 215 | .markerClusterLargeXL { 216 | border-radius: 70px; 217 | font-size: 0.6vw; 218 | color: #fff; 219 | font-weight: 900; 220 | font-family: Open Sans; 221 | text-transform: uppercase; 222 | } 223 | 224 | .markerClusterLargeXLDivOne { 225 | position: absolute; 226 | margin-left: -5px; 227 | margin-top: -5px; 228 | border-radius: 12em; 229 | display: flex; 230 | justify-content: center; 231 | align-items: center; 232 | animation: fadein 3s; 233 | } 234 | 235 | .markerClusterLargeXLDivTwo { 236 | position: absolute; 237 | width: 10.5em; 238 | height: 10.5em; 239 | border-radius: 10.5em; 240 | display: flex; 241 | justify-content: center; 242 | align-items: center; 243 | animation: fadein 2.5s; 244 | } 245 | 246 | .markerClusterLargeXLDivThree { 247 | width: 9em; 248 | height: 9em; 249 | border-radius: 9em; 250 | display: flex; 251 | justify-content: center; 252 | align-items: center; 253 | animation: fadein 2s; 254 | } 255 | 256 | .markerClusterLargeXLDivFour { 257 | padding: 1px; 258 | width: 7.5em; 259 | height: 7.5em; 260 | border-radius: 7.5em; 261 | display: flex; 262 | justify-content: center; 263 | align-items: center; 264 | animation: fadein 1.5s; 265 | } 266 | 267 | .markerClusterLargeXLLabel { 268 | display: flex; 269 | justify-content: center; 270 | align-items: center; 271 | } 272 | 273 | @keyframes fadein { 274 | from { 275 | opacity: 0; 276 | } 277 | to { 278 | opacity: 1; 279 | } 280 | } 281 | 282 | .circle { 283 | animation: fadein 1.5s; 284 | } 285 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: [ 7 | './index.js', 8 | ], 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, 'dist') 12 | }, 13 | devServer: { 14 | contentBase: "./dist" 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: [ 'style-loader', 'css-loader' ] 21 | }, 22 | { 23 | test: /\.html$/, 24 | use: [ 25 | { 26 | loader: "html-loader" 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.js$/, 32 | exclude: /node_modules/, 33 | use: [ 34 | { 35 | loader: 'babel-loader', 36 | query: { 37 | retainLines: true 38 | } 39 | } 40 | ] 41 | }, 42 | 43 | ] 44 | }, 45 | plugins: [ 46 | new HtmlWebPackPlugin({ 47 | template: "./index.html", 48 | filename: "./index.html" 49 | }) 50 | ] 51 | }; 52 | --------------------------------------------------------------------------------