From 4870801f8f88214a91a96d701c8f0902870efdd7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Jan 2019 21:00:28 +0100 Subject: [PATCH] Implemented map to show visits from every city --- src/index.js | 8 +++-- src/utils/utils.js | 15 ++++++++ src/visits/ShortUrlVisits.js | 12 +++++-- src/visits/helpers/MapModal.js | 51 +++++++++++++++------------ src/visits/helpers/OpenMapModalBtn.js | 5 +-- src/visits/services/VisitsParser.js | 32 ++++++++++++++--- 6 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/index.js b/src/index.js index a0604e47..c875ae06 100644 --- a/src/index.js +++ b/src/index.js @@ -7,11 +7,15 @@ import { homepage } from '../package.json'; import registerServiceWorker from './registerServiceWorker'; import container from './container'; import store from './container/store'; -import '../node_modules/react-datepicker/dist/react-datepicker.css'; -import '../node_modules/leaflet/dist/leaflet.css'; +import { fixLeafletIcons } from './utils/utils'; +import 'react-datepicker/dist/react-datepicker.css'; +import 'leaflet/dist/leaflet.css'; import './common/react-tagsinput.scss'; import './index.scss'; +// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS +fixLeafletIcons(); + const { App, ScrollToTop } = container; render( diff --git a/src/utils/utils.js b/src/utils/utils.js index 42e65983..080005bf 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,3 +1,8 @@ +import L from 'leaflet'; +import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; +import marker from 'leaflet/dist/images/marker-icon.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; + const DEFAULT_TIMEOUT_DELAY = 2000; export const stateFlagTimeout = (setState, flagName, initialValue = true, delay = DEFAULT_TIMEOUT_DELAY) => { @@ -17,3 +22,13 @@ export const determineOrderDir = (clickedField, currentOrderField, currentOrderD return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC'; }; + +export const fixLeafletIcons = () => { + delete L.Icon.Default.prototype._getIconUrl; + + L.Icon.Default.mergeOptions({ + iconRetinaUrl: marker2x, + iconUrl: marker, + shadowUrl: markerShadow, + }); +}; diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index 1604b5bb..ec1d8a42 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -1,6 +1,6 @@ import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isEmpty, mapObjIndexed } from 'ramda'; +import { isEmpty, mapObjIndexed, values } from 'ramda'; import React from 'react'; import { Card } from 'reactstrap'; import PropTypes from 'prop-types'; @@ -20,6 +20,7 @@ const ShortUrlVisits = ({ processCountriesStats, processCitiesStats, processReferrersStats, + processCitiesStatsForMap, }) => class ShortUrlVisits extends React.Component { static propTypes = { match: PropTypes.shape({ @@ -102,7 +103,14 @@ const ShortUrlVisits = ({ ]} + extraHeaderContent={[ + () => ( + + ), + ]} sortingItems={{ name: 'City name', amount: 'Visits amount', diff --git a/src/visits/helpers/MapModal.js b/src/visits/helpers/MapModal.js index 5126f56a..773bdadc 100644 --- a/src/visits/helpers/MapModal.js +++ b/src/visits/helpers/MapModal.js @@ -8,33 +8,40 @@ const propTypes = { toggle: PropTypes.func, isOpen: PropTypes.bool, title: PropTypes.string, + locations: PropTypes.arrayOf(PropTypes.shape({ + city: PropTypes.string.isRequired, + latLong: PropTypes.arrayOf(PropTypes.number).isRequired, + count: PropTypes.number.isRequired, + })), +}; +const defaultProps = { + locations: [], }; -const MapModal = ({ toggle, isOpen, title }) => { - const madridLat = 40.416775; - const madridLong = -3.703790; - const latLong = [ madridLat, madridLong ]; +const OpenStreetMapTile = () => ( + +); - return ( - - {title} - - - - - - A pretty CSS3 popup.
Easily customizable. -
+const MapModal = ({ toggle, isOpen, title, locations }) => ( + + {title} + + + + {locations.map(({ city, latLong, count }, index) => ( + + {count} visit{count > 1 ? 's' : ''} from {city} - - - - ); -}; + ))} +
+
+
+); MapModal.propTypes = propTypes; +MapModal.defaultProps = defaultProps; export default MapModal; diff --git a/src/visits/helpers/OpenMapModalBtn.js b/src/visits/helpers/OpenMapModalBtn.js index c90fdc9d..f775be40 100644 --- a/src/visits/helpers/OpenMapModalBtn.js +++ b/src/visits/helpers/OpenMapModalBtn.js @@ -9,12 +9,13 @@ import './OpenMapModalBtn.scss'; export default class OpenMapModalBtn extends React.Component { static propTypes = { title: PropTypes.string.isRequired, + locations: PropTypes.arrayOf(PropTypes.object), }; state = { mapIsOpened: false }; render() { - const { title } = this.props; + const { title, locations = [] } = this.props; const toggleMap = () => this.setState(({ mapIsOpened }) => ({ mapIsOpened: !mapIsOpened })); const buttonRef = React.createRef(); @@ -24,7 +25,7 @@ export default class OpenMapModalBtn extends React.Component { buttonRef.current}>Show in map - + ); } diff --git a/src/visits/services/VisitsParser.js b/src/visits/services/VisitsParser.js index 39b550f3..6ff5662b 100644 --- a/src/visits/services/VisitsParser.js +++ b/src/visits/services/VisitsParser.js @@ -76,15 +76,18 @@ export const processReferrersStats = (visits) => visits, ); +const visitLocationHasProperty = (visitLocation, propertyName) => + !isNil(visitLocation) + && !isNil(visitLocation[propertyName]) + && !isEmpty(visitLocation[propertyName]); + const buildLocationStatsProcessorByProperty = (propertyName) => (visits) => reduce( (stats, { visitLocation }) => { - const notHasCountry = isNil(visitLocation) - || isNil(visitLocation[propertyName]) - || isEmpty(visitLocation[propertyName]); - const country = notHasCountry ? 'Unknown' : visitLocation[propertyName]; + const hasLocationProperty = visitLocationHasProperty(visitLocation, propertyName); + const value = hasLocationProperty ? visitLocation[propertyName] : 'Unknown'; - return assoc(country, (stats[country] || 0) + 1, stats); + return assoc(value, (stats[value] || 0) + 1, stats); }, {}, visits, @@ -93,3 +96,22 @@ const buildLocationStatsProcessorByProperty = (propertyName) => (visits) => export const processCountriesStats = buildLocationStatsProcessorByProperty('countryName'); export const processCitiesStats = buildLocationStatsProcessorByProperty('cityName'); + +export const processCitiesStatsForMap = (visits) => + reduce( + (stats, { visitLocation }) => { + const hasCity = visitLocationHasProperty(visitLocation, 'cityName'); + const city = hasCity ? visitLocation.cityName : 'unknown'; + const currentCity = stats[city] || { + city, + count: 0, + latLong: hasCity ? [ parseFloat(visitLocation.latitude), parseFloat(visitLocation.longitude) ] : [ 0, 0 ], + }; + + currentCity.count++; + + return assoc(city, currentCity, stats); + }, + {}, + visits, + );