Implemented map to show visits from every city

This commit is contained in:
Alejandro Celaya 2019-01-07 21:00:28 +01:00
parent 78745366c2
commit 4870801f8f
6 changed files with 90 additions and 33 deletions

View file

@ -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(

View file

@ -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,
});
};

View file

@ -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 = ({
<SortableBarGraph
stats={processCitiesStats(visits)}
title="Cities"
extraHeaderContent={[ () => <OpenMapModalBtn title="Cities" /> ]}
extraHeaderContent={[
() => (
<OpenMapModalBtn
title="Cities"
locations={values(processCitiesStatsForMap(visits))}
/>
),
]}
sortingItems={{
name: 'City name',
amount: 'Visits amount',

View file

@ -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 = () => (
<TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
);
return (
<Modal toggle={toggle} isOpen={isOpen} className="map-modal__modal" contentClassName="map-modal__modal-content">
<ModalHeader toggle={toggle}>{title}</ModalHeader>
<ModalBody className="map-modal__modal-body">
<Map center={latLong} zoom="13">
<TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={latLong}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
const MapModal = ({ toggle, isOpen, title, locations }) => (
<Modal toggle={toggle} isOpen={isOpen} className="map-modal__modal" contentClassName="map-modal__modal-content">
<ModalHeader toggle={toggle}>{title}</ModalHeader>
<ModalBody className="map-modal__modal-body">
<Map center={[ 0, 0 ]} zoom="3">
<OpenStreetMapTile />
{locations.map(({ city, latLong, count }, index) => (
<Marker key={index} position={latLong}>
<Popup><b>{count}</b> visit{count > 1 ? 's' : ''} from <b>{city}</b></Popup>
</Marker>
</Map>
</ModalBody>
</Modal>
);
};
))}
</Map>
</ModalBody>
</Modal>
);
MapModal.propTypes = propTypes;
MapModal.defaultProps = defaultProps;
export default MapModal;

View file

@ -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 {
<FontAwesomeIcon icon={mapIcon} />
</button>
<UncontrolledTooltip placement="bottom" target={() => buttonRef.current}>Show in map</UncontrolledTooltip>
<MapModal toggle={toggleMap} isOpen={this.state.mapIsOpened} title={title} />
<MapModal toggle={toggleMap} isOpen={this.state.mapIsOpened} title={title} locations={locations} />
</React.Fragment>
);
}

View file

@ -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,
);