mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-13 02:53:47 +00:00
Moved visits-related elements to visits folder
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
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 {
|
||||
processOsStats,
|
||||
processBrowserStats,
|
||||
processCountriesStats,
|
||||
processReferrersStats,
|
||||
} from '../visits/services/VisitsParser';
|
||||
import MutedMessage from '../utils/MuttedMessage';
|
||||
import ExternalLink from '../utils/ExternalLink';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||
import './ShortUrlVisits.scss';
|
||||
|
||||
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
|
||||
));
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadVisits();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
match: { params },
|
||||
selectedServer,
|
||||
processOsStats,
|
||||
processBrowserStats,
|
||||
processCountriesStats,
|
||||
processReferrersStats,
|
||||
shortUrlVisits: { visits, loading, error, shortUrl },
|
||||
} = this.props;
|
||||
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',
|
||||
],
|
||||
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>
|
||||
);
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <MutedMessage><FontAwesomeIcon icon={preloader} spin /> Loading...</MutedMessage>;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="shlink-container">
|
||||
<header>
|
||||
<Card className="bg-light">
|
||||
<CardBody>
|
||||
<h2>
|
||||
{
|
||||
shortUrl.visitsCount &&
|
||||
<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:
|
||||
|
||||
{loading && <small>Loading...</small>}
|
||||
{!loading && renderCreated()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
Long URL:
|
||||
|
||||
{loading && <small>Loading...</small>}
|
||||
{!loading && <ExternalLink href={shortUrl.longUrl}>{shortUrl.longUrl}</ExternalLink>}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</header>
|
||||
|
||||
<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"
|
||||
isClearable
|
||||
onChange={(date) => this.setState({ startDate: date }, () => this.loadVisits())}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-xl-3 col-lg-4 col-md-6">
|
||||
<DateInput
|
||||
selected={this.state.endDate}
|
||||
placeholderText="Until"
|
||||
isClearable
|
||||
className="short-url-visits__date-input"
|
||||
onChange={(date) => this.setState({ endDate: date }, () => this.loadVisits())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
{renderContent()}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ShortUrlsVisits = connect(
|
||||
pick([ 'selectedServer', 'shortUrlVisits' ]),
|
||||
{ getShortUrlVisits }
|
||||
)(ShortUrlsVisitsComponent);
|
||||
|
||||
export default ShortUrlsVisits;
|
||||
@@ -1,7 +0,0 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.short-url-visits__date-input {
|
||||
@media (max-width: $smMax) {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { curry } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||
import { shortUrlType } from './shortUrlsList';
|
||||
|
||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||
const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
||||
const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
|
||||
const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
|
||||
/* eslint-enable padding-line-between-statements, newline-after-var */
|
||||
|
||||
export const shortUrlVisitsType = PropTypes.shape({
|
||||
shortUrl: shortUrlType,
|
||||
visits: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
shortUrl: {},
|
||||
visits: [],
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default function dispatch(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case GET_SHORT_URL_VISITS_START:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
};
|
||||
case GET_SHORT_URL_VISITS_ERROR:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
};
|
||||
case GET_SHORT_URL_VISITS:
|
||||
return {
|
||||
shortUrl: action.shortUrl,
|
||||
visits: action.visits,
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => (dispatch) => {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||
|
||||
Promise.all([
|
||||
shlinkApiClient.getShortUrlVisits(shortCode, dates),
|
||||
shlinkApiClient.getShortUrl(shortCode),
|
||||
])
|
||||
.then(([ visits, shortUrl ]) => dispatch({ visits, shortUrl, type: GET_SHORT_URL_VISITS }))
|
||||
.catch(() => dispatch({ type: GET_SHORT_URL_VISITS_ERROR }));
|
||||
};
|
||||
|
||||
export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient);
|
||||
Reference in New Issue
Block a user