Converted ShortUrlVisits in functional component

This commit is contained in:
Alejandro Celaya
2020-04-07 22:33:41 +02:00
parent 8a486d991b
commit 310831a26a
2 changed files with 86 additions and 86 deletions

View File

@@ -1,5 +1,5 @@
import { isEmpty, mapObjIndexed, values } from 'ramda'; import { isEmpty, values } from 'ramda';
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Button, Card, Collapse } from 'reactstrap'; import { Button, Card, Collapse } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import qs from 'qs'; import qs from 'qs';
@@ -8,6 +8,7 @@ import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons'
import DateRangeRow from '../utils/DateRangeRow'; import DateRangeRow from '../utils/DateRangeRow';
import Message from '../utils/Message'; import Message from '../utils/Message';
import { formatDate } from '../utils/helpers/date'; import { formatDate } from '../utils/helpers/date';
import { useToggle } from '../utils/helpers/hooks';
import SortableBarGraph from './SortableBarGraph'; import SortableBarGraph from './SortableBarGraph';
import { shortUrlVisitsType } from './reducers/shortUrlVisits'; import { shortUrlVisitsType } from './reducers/shortUrlVisits';
import VisitsHeader from './VisitsHeader'; import VisitsHeader from './VisitsHeader';
@@ -15,13 +16,7 @@ import GraphCard from './GraphCard';
import { shortUrlDetailType } from './reducers/shortUrlDetail'; import { shortUrlDetailType } from './reducers/shortUrlDetail';
import VisitsTable from './VisitsTable'; import VisitsTable from './VisitsTable';
const highlightedVisitToStats = (highlightedVisit, prop) => highlightedVisit && { [highlightedVisit[prop]]: 1 }; const propTypes = {
const ShortUrlVisits = (
{ processStatsFromVisits },
OpenMapModalBtn
) => class ShortUrlVisits extends React.PureComponent {
static propTypes = {
match: PropTypes.shape({ match: PropTypes.shape({
params: PropTypes.object, params: PropTypes.object,
}), }),
@@ -34,52 +29,61 @@ const ShortUrlVisits = (
shortUrlDetail: shortUrlDetailType, shortUrlDetail: shortUrlDetailType,
cancelGetShortUrlVisits: PropTypes.func, cancelGetShortUrlVisits: PropTypes.func,
matchMedia: PropTypes.func, matchMedia: PropTypes.func,
}; };
state = { const highlightedVisitToStats = (highlightedVisit, prop) => highlightedVisit && { [highlightedVisit[prop]]: 1 };
startDate: undefined, const format = formatDate();
endDate: undefined, let memoizationId;
showTable: false, let timeWhenMounted;
tableIsSticky: false,
isMobileDevice: false,
highlightedVisit: undefined,
};
loadVisits = (loadDetail = false) => { const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
const { match: { params }, location: { search }, getShortUrlVisits, getShortUrlDetail } = this.props; const ShortUrlVisitsComp = ({
match,
location,
shortUrlVisits,
shortUrlDetail,
getShortUrlVisits,
getShortUrlDetail,
cancelGetShortUrlVisits,
matchMedia = window.matchMedia,
}) => {
const [ startDate, setStartDate ] = useState(undefined);
const [ endDate, setEndDate ] = useState(undefined);
const [ showTable, toggleTable ] = useToggle();
const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
const [ highlightedVisit, setHighlightedVisit ] = useState(undefined);
const [ isMobileDevice, setIsMobileDevice ] = useState(false);
const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
const { params } = match;
const { shortCode } = params; const { shortCode } = params;
const { startDate, endDate } = mapObjIndexed(formatDate(), this.state); const { search } = location;
const { domain } = qs.parse(search, { ignoreQueryPrefix: true }); const { domain } = qs.parse(search, { ignoreQueryPrefix: true });
const loadVisits = () => {
const start = format(startDate);
const end = format(endDate);
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations // While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations
this.memoizationId = `${this.timeWhenMounted}_${shortCode}_${startDate}_${endDate}`; memoizationId = `${timeWhenMounted}_${shortCode}_${start}_${end}`;
getShortUrlVisits(shortCode, { startDate, endDate, domain }); getShortUrlVisits(shortCode, { startDate: start, endDate: end, domain });
};
if (loadDetail) { useEffect(() => {
timeWhenMounted = new Date().getTime();
getShortUrlDetail(shortCode, domain); getShortUrlDetail(shortCode, domain);
} determineIsMobileDevice();
window.addEventListener('resize', determineIsMobileDevice);
return () => {
cancelGetShortUrlVisits();
window.removeEventListener('resize', determineIsMobileDevice);
}; };
}, []);
useEffect(() => {
loadVisits();
}, [ startDate, endDate ]);
setIsMobileDevice = () => {
const { matchMedia = window.matchMedia } = this.props;
this.setState({ isMobileDevice: matchMedia('(max-width: 991px)').matches });
};
componentDidMount() {
this.timeWhenMounted = new Date().getTime();
this.loadVisits(true);
this.setIsMobileDevice();
window.addEventListener('resize', this.setIsMobileDevice);
}
componentWillUnmount() {
this.props.cancelGetShortUrlVisits();
window.removeEventListener('resize', this.setIsMobileDevice);
}
render() {
const { shortUrlVisits, shortUrlDetail } = this.props;
const { visits, loading, loadingLarge, error } = shortUrlVisits; const { visits, loading, loadingLarge, error } = shortUrlVisits;
const showTableControls = !loading && visits.length > 0; const showTableControls = !loading && visits.length > 0;
@@ -103,7 +107,7 @@ const ShortUrlVisits = (
} }
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits( const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
{ id: this.memoizationId, visits } { id: memoizationId, visits }
); );
const mapLocations = values(citiesForMap); const mapLocations = values(citiesForMap);
@@ -120,7 +124,7 @@ const ShortUrlVisits = (
title="Referrers" title="Referrers"
stats={referrers} stats={referrers}
withPagination={false} withPagination={false}
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'referer')} highlightedStats={highlightedVisitToStats(highlightedVisit, 'referer')}
sortingItems={{ sortingItems={{
name: 'Referrer name', name: 'Referrer name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -131,7 +135,7 @@ const ShortUrlVisits = (
<SortableBarGraph <SortableBarGraph
title="Countries" title="Countries"
stats={countries} stats={countries}
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'country')} highlightedStats={highlightedVisitToStats(highlightedVisit, 'country')}
sortingItems={{ sortingItems={{
name: 'Country name', name: 'Country name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -142,7 +146,7 @@ const ShortUrlVisits = (
<SortableBarGraph <SortableBarGraph
title="Cities" title="Cities"
stats={cities} stats={cities}
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'city')} highlightedStats={highlightedVisitToStats(highlightedVisit, 'city')}
extraHeaderContent={(activeCities) => extraHeaderContent={(activeCities) =>
mapLocations.length > 0 && mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} /> <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
@@ -156,7 +160,6 @@ const ShortUrlVisits = (
</div> </div>
); );
}; };
const setDate = (dateField) => (date) => this.setState({ [dateField]: date }, this.loadVisits);
return ( return (
<React.Fragment> <React.Fragment>
@@ -166,20 +169,21 @@ const ShortUrlVisits = (
<div className="row flex-md-row-reverse"> <div className="row flex-md-row-reverse">
<div className="col-lg-8 col-xl-6"> <div className="col-lg-8 col-xl-6">
<DateRangeRow <DateRangeRow
startDate={this.state.startDate} startDate={startDate}
endDate={this.state.endDate} endDate={endDate}
onStartDateChange={setDate('startDate')} onStartDateChange={setStartDate}
onEndDateChange={setDate('endDate')} onEndDateChange={setEndDate}
/> />
</div> </div>
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0"> <div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
{showTableControls && ( {showTableControls && (
<Button <Button
outline outline
block={this.state.isMobileDevice} block={isMobileDevice}
onClick={() => this.setState(({ showTable }) => ({ showTable: !showTable }))} onClick={toggleTable}
> >
Show table <FontAwesomeIcon icon={chevronDown} rotation={this.state.showTable ? 180 : undefined} /> {showTable ? 'Hide' : 'Show'} table{' '}
<FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} />
</Button> </Button>
)} )}
</div> </div>
@@ -188,17 +192,13 @@ const ShortUrlVisits = (
{showTableControls && ( {showTableControls && (
<Collapse <Collapse
isOpen={this.state.showTable} isOpen={showTable}
// Enable stickiness only when there's no CSS animation, to avoid weird rendering effects // Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
onEntered={() => this.setState({ tableIsSticky: true })} onEntered={setSticky}
onExiting={() => this.setState({ tableIsSticky: false })} onExiting={unsetSticky}
> >
<VisitsTable <VisitsTable visits={visits} isSticky={tableIsSticky} onVisitSelected={setHighlightedVisit} />
visits={visits}
isSticky={this.state.tableIsSticky}
onVisitSelected={(highlightedVisit) => this.setState({ highlightedVisit })}
/>
</Collapse> </Collapse>
)} )}
@@ -207,7 +207,11 @@ const ShortUrlVisits = (
</section> </section>
</React.Fragment> </React.Fragment>
); );
} };
ShortUrlVisitsComp.propTypes = propTypes;
return ShortUrlVisitsComp;
}; };
export default ShortUrlVisits; export default ShortUrlVisits;

View File

@@ -38,10 +38,7 @@ describe('<ShortUrlVisits />', () => {
return wrapper; return wrapper;
}; };
afterEach(() => { afterEach(() => wrapper && wrapper.unmount());
getShortUrlVisitsMock.mockReset();
wrapper && wrapper.unmount();
});
it('renders a preloader when visits are loading', () => { it('renders a preloader when visits are loading', () => {
const wrapper = createComponent({ loading: true, visits: [] }); const wrapper = createComponent({ loading: true, visits: [] });
@@ -91,9 +88,8 @@ describe('<ShortUrlVisits />', () => {
dateRange.simulate('endDateChange', '2016-01-02T00:00:00+01:00'); dateRange.simulate('endDateChange', '2016-01-02T00:00:00+01:00');
dateRange.simulate('endDateChange', '2016-01-03T00:00:00+01:00'); dateRange.simulate('endDateChange', '2016-01-03T00:00:00+01:00');
expect(getShortUrlVisitsMock).toHaveBeenCalledTimes(4); expect(wrapper.find(DateRangeRow).prop('startDate')).toEqual('2016-01-01T00:00:00+01:00');
expect(wrapper.state('startDate')).toEqual('2016-01-01T00:00:00+01:00'); expect(wrapper.find(DateRangeRow).prop('endDate')).toEqual('2016-01-03T00:00:00+01:00');
expect(wrapper.state('endDate')).toEqual('2016-01-03T00:00:00+01:00');
}); });
it('holds the map button content generator on cities graph extraHeaderContent', () => { it('holds the map button content generator on cities graph extraHeaderContent', () => {