shlink-web-client/src/visits/ShortUrlVisits.js

226 lines
7.3 KiB
JavaScript
Raw Normal View History

import { isEmpty, values } from 'ramda';
import React, { useState, useEffect } from 'react';
2020-04-04 00:00:57 +03:00
import { Button, Card, Collapse } from 'reactstrap';
import PropTypes from 'prop-types';
import qs from 'qs';
2020-04-04 00:00:57 +03:00
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
import DateRangeRow from '../utils/DateRangeRow';
import Message from '../utils/Message';
import { formatDate } from '../utils/helpers/date';
import { useToggle } from '../utils/helpers/hooks';
2018-10-29 01:04:52 +03:00
import SortableBarGraph from './SortableBarGraph';
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
import VisitsHeader from './VisitsHeader';
import GraphCard from './GraphCard';
import { shortUrlDetailType } from './reducers/shortUrlDetail';
2020-04-04 00:00:57 +03:00
import VisitsTable from './VisitsTable';
2018-07-29 19:39:00 +03:00
const propTypes = {
match: PropTypes.shape({
params: PropTypes.object,
}),
location: PropTypes.shape({
search: PropTypes.string,
}),
getShortUrlVisits: PropTypes.func,
shortUrlVisits: shortUrlVisitsType,
getShortUrlDetail: PropTypes.func,
shortUrlDetail: shortUrlDetailType,
cancelGetShortUrlVisits: PropTypes.func,
matchMedia: PropTypes.func,
};
2020-04-04 00:00:57 +03:00
const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
if (!acc[highlightedVisit[prop]]) {
acc[highlightedVisit[prop]] = 0;
}
acc[highlightedVisit[prop]] += 1;
return acc;
}, {});
const format = formatDate();
let memoizationId;
let timeWhenMounted;
const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
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 [ highlightedVisits, setHighlightedVisits ] = useState([]);
const [ isMobileDevice, setIsMobileDevice ] = useState(false);
const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
const { params } = match;
const { shortCode } = params;
const { search } = location;
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
memoizationId = `${timeWhenMounted}_${shortCode}_${start}_${end}`;
getShortUrlVisits(shortCode, { startDate: start, endDate: end, domain });
};
2018-07-29 20:25:22 +03:00
useEffect(() => {
timeWhenMounted = new Date().getTime();
getShortUrlDetail(shortCode, domain);
determineIsMobileDevice();
window.addEventListener('resize', determineIsMobileDevice);
return () => {
cancelGetShortUrlVisits();
window.removeEventListener('resize', determineIsMobileDevice);
};
}, []);
useEffect(() => {
loadVisits();
}, [ startDate, endDate ]);
2020-04-04 00:00:57 +03:00
const { visits, loading, loadingLarge, error } = shortUrlVisits;
const showTableControls = !loading && visits.length > 0;
const renderVisitsContent = () => {
2018-07-30 21:52:03 +03:00
if (loading) {
const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...';
return <Message loading>{message}</Message>;
2018-07-30 21:52:03 +03:00
}
if (error) {
return (
<Card className="mt-4" body inverse color="danger">
An error occurred while loading visits :(
</Card>
);
}
if (isEmpty(visits)) {
return <Message>There are no visits matching current filter :(</Message>;
}
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
{ id: memoizationId, visits }
);
const mapLocations = values(citiesForMap);
2018-07-30 21:52:03 +03:00
return (
<div className="row">
<div className="col-xl-4 col-lg-6">
<GraphCard title="Operating systems" stats={os} />
</div>
<div className="col-xl-4 col-lg-6">
<GraphCard title="Browsers" stats={browsers} />
</div>
<div className="col-xl-4">
<SortableBarGraph
title="Referrers"
stats={referrers}
2019-03-10 11:54:40 +03:00
withPagination={false}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
sortingItems={{
name: 'Referrer name',
amount: 'Visits amount',
}}
/>
</div>
<div className="col-lg-6">
2018-10-29 01:04:52 +03:00
<SortableBarGraph
title="Countries"
stats={countries}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
2018-10-29 01:04:52 +03:00
sortingItems={{
name: 'Country name',
amount: 'Visits amount',
}}
/>
</div>
<div className="col-lg-6">
2018-10-29 01:04:52 +03:00
<SortableBarGraph
title="Cities"
stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
extraHeaderContent={(activeCities) =>
mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
}
2018-10-29 01:04:52 +03:00
sortingItems={{
name: 'City name',
2018-10-29 01:04:52 +03:00
amount: 'Visits amount',
}}
/>
</div>
2018-07-29 20:25:22 +03:00
</div>
2018-07-30 21:52:03 +03:00
);
};
return (
2020-03-05 13:11:26 +03:00
<React.Fragment>
<VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />
2018-07-30 21:52:03 +03:00
2018-08-09 20:50:22 +03:00
<section className="mt-4">
2020-04-04 00:00:57 +03:00
<div className="row flex-md-row-reverse">
<div className="col-lg-8 col-xl-6">
<DateRangeRow
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
2020-04-04 00:00:57 +03:00
/>
</div>
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
{showTableControls && (
<Button
outline
block={isMobileDevice}
onClick={toggleTable}
>
{showTable ? 'Hide' : 'Show'} table{' '}
<FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} />
2020-04-04 00:00:57 +03:00
</Button>
)}
</div>
</div>
</section>
{showTableControls && (
<Collapse
isOpen={showTable}
// Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
onEntered={setSticky}
onExiting={unsetSticky}
>
<VisitsTable visits={visits} isSticky={tableIsSticky} onVisitsSelected={setHighlightedVisits} />
2020-04-04 00:00:57 +03:00
</Collapse>
)}
<section>
{renderVisitsContent()}
</section>
2020-03-05 13:11:26 +03:00
</React.Fragment>
2018-07-29 20:25:22 +03:00
);
};
ShortUrlVisitsComp.propTypes = propTypes;
return ShortUrlVisitsComp;
};
export default ShortUrlVisits;