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

253 lines
8.6 KiB
JavaScript
Raw Normal View History

import { isEmpty, propEq, values } from 'ramda';
import React, { useState, useEffect, useMemo } 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-10 14:27:01 +03:00
import classNames from 'classnames';
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 selectedBar;
const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, 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);
2020-04-10 14:27:01 +03:00
const setSelectedVisits = (selectedVisits) => {
selectedBar = undefined;
setHighlightedVisits(selectedVisits);
};
const highlightVisitsForProp = (prop) => (value) => {
const newSelectedBar = `${prop}_${value}`;
if (selectedBar === newSelectedBar) {
setHighlightedVisits([]);
selectedBar = undefined;
} else {
setHighlightedVisits(normalizedVisits.filter(propEq(prop, value)));
selectedBar = newSelectedBar;
}
};
const { params } = match;
const { shortCode } = params;
const { search } = location;
const { domain } = qs.parse(search, { ignoreQueryPrefix: true });
const { visits, loading, loadingLarge, error } = shortUrlVisits;
const showTableControls = !loading && visits.length > 0;
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
() => processStatsFromVisits(visits),
[ visits ]
);
const mapLocations = values(citiesForMap);
const loadVisits = () =>
getShortUrlVisits(shortCode, { startDate: format(startDate), endDate: format(endDate), domain });
2018-07-29 20:25:22 +03:00
useEffect(() => {
getShortUrlDetail(shortCode, domain);
determineIsMobileDevice();
window.addEventListener('resize', determineIsMobileDevice);
return () => {
cancelGetShortUrlVisits();
window.removeEventListener('resize', determineIsMobileDevice);
};
}, []);
useEffect(() => {
loadVisits();
}, [ startDate, endDate ]);
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>;
}
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',
}}
onClick={highlightVisitsForProp('referer')}
/>
</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',
}}
onClick={highlightVisitsForProp('country')}
2018-10-29 01:04:52 +03:00
/>
</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',
}}
onClick={highlightVisitsForProp('city')}
2018-10-29 01:04:52 +03:00
/>
</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">
2020-04-10 14:27:01 +03:00
<div className="col-lg-7 col-xl-6">
2020-04-04 00:00:57 +03:00
<DateRangeRow
2020-04-10 13:25:06 +03:00
disabled={loading}
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
2020-04-04 00:00:57 +03:00
/>
</div>
2020-04-10 14:27:01 +03:00
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
{showTableControls && (
2020-04-10 14:27:01 +03:00
<span className={classNames({ 'row flex-row-reverse': isMobileDevice })}>
<span className={classNames({ 'col-6': isMobileDevice })}>
<Button outline color="primary" block={isMobileDevice} onClick={toggleTable}>
{showTable ? 'Hide' : 'Show'} table
<FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} className="ml-2" />
</Button>
</span>
<span className={classNames({ 'col-6': isMobileDevice, 'ml-2': !isMobileDevice })}>
<Button
outline
disabled={highlightedVisits.length === 0}
block={isMobileDevice}
onClick={() => setSelectedVisits([])}
>
Reset selection
</Button>
</span>
</span>
2020-04-04 00:00:57 +03:00
)}
</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={normalizedVisits}
selectedVisits={highlightedVisits}
2020-04-10 14:27:01 +03:00
setSelectedVisits={setSelectedVisits}
isSticky={tableIsSticky}
/>
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;