2018-07-31 23:04:20 +03:00
|
|
|
import preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch';
|
|
|
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
|
|
|
import { isEmpty, mapObjIndexed, pick } from 'ramda';
|
2018-07-29 19:39:00 +03:00
|
|
|
import React from 'react';
|
2018-07-30 21:31:48 +03:00
|
|
|
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
2018-07-30 22:34:06 +03:00
|
|
|
import Moment from 'react-moment';
|
2018-07-29 19:39:00 +03:00
|
|
|
import { connect } from 'react-redux';
|
2018-07-30 22:34:06 +03:00
|
|
|
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap';
|
2018-07-31 23:04:20 +03:00
|
|
|
import DateInput from '../common/DateInput';
|
2018-07-30 21:31:48 +03:00
|
|
|
import VisitsParser from '../visits/services/VisitsParser';
|
2018-07-31 23:04:20 +03:00
|
|
|
import { getShortUrlVisits } from './reducers/shortUrlVisits';
|
|
|
|
import './ShortUrlVisits.scss';
|
|
|
|
|
|
|
|
const MutedMessage = ({ children }) =>
|
|
|
|
<div className="col-md-10 offset-md-1">
|
|
|
|
<Card className="bg-light mt-4" body>
|
|
|
|
<h3 className="text-center text-muted mb-0">
|
|
|
|
{children}
|
|
|
|
</h3>
|
|
|
|
</Card>
|
|
|
|
</div>;
|
2018-07-29 19:39:00 +03:00
|
|
|
|
|
|
|
export class ShortUrlsVisits extends React.Component {
|
2018-07-31 23:04:20 +03:00
|
|
|
state = { startDate: undefined, endDate: undefined };
|
2018-08-01 19:32:21 +03:00
|
|
|
loadVisits = () => {
|
2018-07-31 23:04:20 +03:00
|
|
|
const { match: { params } } = this.props;
|
|
|
|
this.props.getShortUrlVisits(params.shortCode, mapObjIndexed(
|
|
|
|
value => value && value.format ? value.format('YYYY-MM-DD') : value,
|
2018-08-01 19:32:21 +03:00
|
|
|
this.state
|
2018-07-31 23:04:20 +03:00
|
|
|
))
|
|
|
|
};
|
2018-07-30 21:31:48 +03:00
|
|
|
|
2018-07-29 20:25:22 +03:00
|
|
|
componentDidMount() {
|
2018-07-31 23:04:20 +03:00
|
|
|
this.loadVisits();
|
2018-07-29 20:25:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2018-07-30 22:34:06 +03:00
|
|
|
const {
|
|
|
|
match: { params },
|
|
|
|
selectedServer,
|
|
|
|
visitsParser,
|
|
|
|
shortUrlVisits: { visits, loading, error, shortUrl }
|
|
|
|
} = this.props;
|
2018-07-29 20:25:22 +03:00
|
|
|
const serverUrl = selectedServer ? selectedServer.url : '';
|
2018-07-30 22:34:06 +03:00
|
|
|
const shortLink = `${serverUrl}/${params.shortCode}`;
|
2018-07-30 22:12:06 +03:00
|
|
|
const generateGraphData = (stats, label, isBarChart) => ({
|
2018-07-30 21:31:48 +03:00
|
|
|
labels: Object.keys(stats),
|
2018-07-30 22:12:06 +03:00
|
|
|
datasets: [
|
|
|
|
{
|
|
|
|
label,
|
|
|
|
data: Object.values(stats),
|
2018-07-31 23:04:20 +03:00
|
|
|
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
|
2018-07-30 22:12:06 +03:00
|
|
|
}
|
|
|
|
]
|
2018-07-30 21:31:48 +03:00
|
|
|
});
|
2018-07-31 21:36:27 +03:00
|
|
|
const renderGraphCard = (title, stats, isBarChart, label) =>
|
2018-07-30 22:12:06 +03:00
|
|
|
<div className="col-md-6">
|
|
|
|
<Card className="mt-4">
|
|
|
|
<CardHeader>{title}</CardHeader>
|
|
|
|
<CardBody>
|
2018-07-31 21:36:27 +03:00
|
|
|
{!isBarChart && <Doughnut data={generateGraphData(stats, label || title, isBarChart)} options={{
|
|
|
|
legend: {
|
|
|
|
position: 'right'
|
|
|
|
}
|
|
|
|
}} />}
|
|
|
|
{isBarChart && <HorizontalBar data={generateGraphData(stats, label || title, isBarChart)} options={{
|
|
|
|
legend: {
|
|
|
|
display: false
|
|
|
|
}
|
|
|
|
}} />}
|
2018-07-30 22:12:06 +03:00
|
|
|
</CardBody>
|
|
|
|
</Card>
|
|
|
|
</div>;
|
2018-07-30 21:52:03 +03:00
|
|
|
const renderContent = () => {
|
|
|
|
if (loading) {
|
2018-07-31 23:04:20 +03:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
2018-07-30 21:31:48 +03:00
|
|
|
|
2018-07-31 23:04:20 +03:00
|
|
|
if (isEmpty(visits)) {
|
|
|
|
return <MutedMessage>There have been no visits matching current filter :(</MutedMessage>;
|
|
|
|
}
|
|
|
|
|
2018-07-30 21:52:03 +03:00
|
|
|
return (
|
2018-07-30 21:31:48 +03:00
|
|
|
<div className="row">
|
2018-07-30 22:12:06 +03:00
|
|
|
{renderGraphCard('Operating systems', visitsParser.processOsStats(visits), false)}
|
|
|
|
{renderGraphCard('Browsers', visitsParser.processBrowserStats(visits), false)}
|
2018-07-31 21:36:27 +03:00
|
|
|
{renderGraphCard('Countries', visitsParser.processCountriesStats(visits), true, 'Visits')}
|
|
|
|
{renderGraphCard('Referrers', visitsParser.processReferrersStats(visits), true, 'Visits')}
|
2018-07-29 20:25:22 +03:00
|
|
|
</div>
|
2018-07-30 21:52:03 +03:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2018-07-30 22:34:06 +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-07-30 21:52:03 +03:00
|
|
|
return (
|
|
|
|
<div className="short-urls-container">
|
2018-07-30 22:34:06 +03:00
|
|
|
<header>
|
|
|
|
<Card className="bg-light">
|
|
|
|
<CardBody>
|
|
|
|
<h2>
|
|
|
|
{
|
|
|
|
shortUrl.visitsCount &&
|
|
|
|
<span className="badge badge-primary float-right">Visits: {shortUrl.visitsCount}</span>
|
|
|
|
}
|
|
|
|
Visit stats for <a target="_blank" href={shortLink}>{shortLink}</a>
|
|
|
|
</h2>
|
|
|
|
<hr />
|
|
|
|
{shortUrl.dateCreated && <div>
|
|
|
|
Created:
|
|
|
|
|
|
|
|
{loading && <small>Loading...</small>}
|
|
|
|
{!loading && renderCreated()}
|
|
|
|
</div>}
|
|
|
|
<div>
|
2018-08-05 09:49:07 +03:00
|
|
|
Long URL:
|
2018-07-30 22:34:06 +03:00
|
|
|
|
|
|
|
{loading && <small>Loading...</small>}
|
|
|
|
{!loading && <a target="_blank" href={shortUrl.longUrl}>{shortUrl.longUrl}</a>}
|
|
|
|
</div>
|
|
|
|
</CardBody>
|
|
|
|
</Card>
|
|
|
|
</header>
|
2018-07-30 21:52:03 +03:00
|
|
|
|
2018-07-31 23:04:20 +03:00
|
|
|
<section>
|
|
|
|
<form onSubmit={e => e.preventDefault()} className="form-inline mt-4 float-md-right">
|
|
|
|
<label>Period</label>
|
|
|
|
<DateInput
|
|
|
|
selected={this.state.startDate}
|
|
|
|
placeholderText="Since"
|
2018-08-01 19:32:21 +03:00
|
|
|
onChange={date => this.setState({ startDate: date }, () => this.loadVisits())}
|
2018-07-31 23:04:20 +03:00
|
|
|
className="short-url-visits__date-input"
|
|
|
|
/>
|
|
|
|
<DateInput
|
|
|
|
selected={this.state.endDate}
|
|
|
|
placeholderText="Until"
|
2018-08-01 19:32:21 +03:00
|
|
|
onChange={date => this.setState({ endDate: date }, () => this.loadVisits())}
|
2018-07-31 23:04:20 +03:00
|
|
|
className="short-url-visits__date-input"
|
|
|
|
/>
|
|
|
|
</form>
|
|
|
|
<div className="clearfix" />
|
|
|
|
</section>
|
|
|
|
|
2018-07-30 22:34:06 +03:00
|
|
|
<section>
|
|
|
|
{renderContent()}
|
|
|
|
</section>
|
2018-07-29 20:25:22 +03:00
|
|
|
</div>
|
|
|
|
);
|
2018-07-29 19:39:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-30 21:31:48 +03:00
|
|
|
ShortUrlsVisits.defaultProps = {
|
|
|
|
visitsParser: VisitsParser
|
|
|
|
};
|
|
|
|
|
2018-07-31 23:04:20 +03:00
|
|
|
export default connect(pick(['selectedServer', 'shortUrlVisits']), { getShortUrlVisits })(ShortUrlsVisits);
|