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

219 lines
6.8 KiB
JavaScript
Raw Normal View History

import preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { isEmpty, mapObjIndexed, pick } from 'ramda';
import React from 'react';
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import Moment from 'react-moment';
import { connect } from 'react-redux';
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap';
import PropTypes from 'prop-types';
import DateInput from '../common/DateInput';
import MutedMessage from '../utils/MuttedMessage';
import ExternalLink from '../utils/ExternalLink';
import { serverType } from '../servers/prop-types/index';
import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits';
import {
processOsStats,
processBrowserStats,
processCountriesStats,
processReferrersStats,
} from './services/VisitsParser';
import './ShortUrlVisits.scss';
2018-07-29 19:39:00 +03:00
export class ShortUrlsVisitsComponent extends React.Component {
static propTypes = {
processOsStats: PropTypes.func,
processBrowserStats: PropTypes.func,
processCountriesStats: PropTypes.func,
processReferrersStats: PropTypes.func,
match: PropTypes.object,
getShortUrlVisits: PropTypes.func,
selectedServer: serverType,
shortUrlVisits: shortUrlVisitsType,
};
static defaultProps = {
processOsStats,
processBrowserStats,
processCountriesStats,
processReferrersStats,
};
state = { startDate: undefined, endDate: undefined };
loadVisits = () => {
const { match: { params }, getShortUrlVisits } = this.props;
getShortUrlVisits(params.shortCode, mapObjIndexed(
(value) => value && value.format ? value.format('YYYY-MM-DD') : value,
this.state
));
};
2018-07-29 20:25:22 +03:00
componentDidMount() {
this.loadVisits();
2018-07-29 20:25:22 +03:00
}
render() {
const {
match: { params },
selectedServer,
processOsStats,
processBrowserStats,
processCountriesStats,
processReferrersStats,
shortUrlVisits: { visits, loading, error, shortUrl },
} = this.props;
2018-07-29 20:25:22 +03:00
const serverUrl = selectedServer ? selectedServer.url : '';
const shortLink = `${serverUrl}/${params.shortCode}`;
const generateGraphData = (stats, label, isBarChart) => ({
labels: Object.keys(stats),
datasets: [
{
label,
data: Object.values(stats),
backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [
'#97BBCD',
'#DCDCDC',
'#F7464A',
'#46BFBD',
'#FDB45C',
'#949FB1',
'#4D5360',
],
2018-07-31 21:36:27 +03:00
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
borderWidth: 2,
},
],
});
const renderGraphCard = (title, stats, isBarChart, label) => (
<div className="col-md-6">
<Card className="mt-4">
<CardHeader>{title}</CardHeader>
<CardBody>
{!isBarChart && (
<Doughnut
data={generateGraphData(stats, label || title, isBarChart)}
options={{
legend: {
position: 'right',
},
}}
/>
)}
{isBarChart && (
<HorizontalBar
data={generateGraphData(stats, label || title, isBarChart)}
options={{
legend: {
display: false,
},
}}
/>
)}
</CardBody>
</Card>
</div>
);
2018-07-30 21:52:03 +03:00
const renderContent = () => {
if (loading) {
return <MutedMessage><FontAwesomeIcon icon={preloader} spin /> Loading...</MutedMessage>;
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 <MutedMessage>There have been no visits matching current filter :(</MutedMessage>;
}
2018-07-30 21:52:03 +03:00
return (
<div className="row">
{renderGraphCard('Operating systems', processOsStats(visits), false)}
{renderGraphCard('Browsers', processBrowserStats(visits), false)}
{renderGraphCard('Countries', processCountriesStats(visits), true, 'Visits')}
{renderGraphCard('Referrers', processReferrersStats(visits), true, 'Visits')}
2018-07-29 20:25:22 +03:00
</div>
2018-07-30 21:52:03 +03:00
);
};
const renderCreated = () => (
<span>
<b id="created"><Moment fromNow>{shortUrl.dateCreated}</Moment></b>
<UncontrolledTooltip placement="bottom" target="created">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</UncontrolledTooltip>
</span>
);
2018-08-16 19:59:00 +03:00
2018-07-30 21:52:03 +03:00
return (
2018-08-16 19:59:00 +03:00
<div className="shlink-container">
<header>
<Card className="bg-light">
<CardBody>
<h2>
{
shortUrl.visitsCount &&
2018-08-11 19:27:51 +03:00
<span className="badge badge-main float-right">Visits: {shortUrl.visitsCount}</span>
}
Visit stats for <ExternalLink href={shortLink}>{shortLink}</ExternalLink>
</h2>
<hr />
{shortUrl.dateCreated && (
<div>
Created:
&nbsp;
{loading && <small>Loading...</small>}
{!loading && renderCreated()}
</div>
)}
<div>
2018-08-05 09:49:07 +03:00
Long URL:
&nbsp;
{loading && <small>Loading...</small>}
{!loading && <ExternalLink href={shortUrl.longUrl}>{shortUrl.longUrl}</ExternalLink>}
</div>
</CardBody>
</Card>
</header>
2018-07-30 21:52:03 +03:00
2018-08-09 20:50:22 +03:00
<section className="mt-4">
<div className="row">
<div className="col-xl-3 col-lg-4 col-md-6 offset-xl-6 offset-lg-4">
<DateInput
selected={this.state.startDate}
placeholderText="Since"
2018-08-09 21:13:46 +03:00
isClearable
onChange={(date) => this.setState({ startDate: date }, () => this.loadVisits())}
2018-08-09 20:50:22 +03:00
/>
</div>
<div className="col-xl-3 col-lg-4 col-md-6">
<DateInput
selected={this.state.endDate}
placeholderText="Until"
2018-08-09 21:13:46 +03:00
isClearable
2018-08-09 20:50:22 +03:00
className="short-url-visits__date-input"
onChange={(date) => this.setState({ endDate: date }, () => this.loadVisits())}
2018-08-09 20:50:22 +03:00
/>
</div>
</div>
</section>
<section>
{renderContent()}
</section>
2018-07-29 20:25:22 +03:00
</div>
);
2018-07-29 19:39:00 +03:00
}
}
const ShortUrlsVisits = connect(
pick([ 'selectedServer', 'shortUrlVisits' ]),
{ getShortUrlVisits }
)(ShortUrlsVisitsComponent);
export default ShortUrlsVisits;