Skip to content

Commit

Permalink
Merge pull request #709 from cboard-org/analytics
Browse files Browse the repository at this point in the history
Add client based Analytics using Google analytics reports
  • Loading branch information
martinbedouret authored Jun 30, 2020
2 parents 6be1983 + a07937f commit f2004c7
Show file tree
Hide file tree
Showing 43 changed files with 15,913 additions and 14,367 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
"axios": "^0.18.0",
"browser-image-resizer": "^1.2.0",
"copy-to-clipboard": "^3.0.8",
"date-fns": "2.11.1",
"dom-to-image": "^2.6.0",
"echarts": "4.7.0",
"echarts-for-react": "^2.0.15-beta.1",
"file-saver": "^2.0.0",
"formik": "^1.3.2",
"i18n-iso-countries": "4.3.1",
Expand All @@ -41,6 +44,7 @@
"jszip-utils": "^0.1.0",
"keycode": "^2.2.0",
"lodash": "^4.17.15",
"matx": "1.0.0",
"moment": "2.24.0",
"opencollective": "^1.0.3",
"pdfmake": "^0.1.65",
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->

<!-- Add to homescreen for Chrome on Android -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Cboard">
Expand Down
19 changes: 19 additions & 0 deletions src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,25 @@ class API {

return data;
}

async analyticsReport(report) {
const authToken = getAuthToken();
if (!(authToken && authToken.length)) {
throw new Error('Need to be authenticated to perform this request');
}
const headers = {
Authorization: `Bearer ${authToken}`
};

const { data } = await this.axiosInstance.post(
`/analytics/batchGet`,
report,
{
headers
}
);
return data;
}
}

const API_INSTANCE = new API({});
Expand Down
250 changes: 250 additions & 0 deletions src/components/Analytics/Analytics.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import moment from 'moment';
import { Grid } from '@material-ui/core';
import { withStyles } from '@material-ui/core/styles';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import CircularProgress from '@material-ui/core/CircularProgress';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContentText from '@material-ui/core/DialogContentText';
import Button from '@material-ui/core/Button';
import Slide from '@material-ui/core/Slide';

import messages from './Analytics.messages';
import FullScreenDialog from '../UI/FullScreenDialog';
import ModifiedAreaChart from '../UI/ModifiedAreaChart';
import StatCards from '../UI/StatCards';
import StatCards2 from '../UI/StatCards2';
import TableCard from '../UI/TableCard';
import DoughnutChart from '../UI/Doughnut';
import './Analytics.css';
import Barchart from '../UI/Barchart';
import StyledTable from '../UI/StyledTable';

const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});

const propTypes = {
intl: intlShape.isRequired,
classes: PropTypes.object.isRequired,
onDaysChange: PropTypes.func.isRequired,
days: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isLogged: PropTypes.bool.isRequired,
logout: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
symbolSources: PropTypes.array.isRequired,
totals: PropTypes.object.isRequired,
categoryTotals: PropTypes.object.isRequired,
usage: PropTypes.object.isRequired,
topUsed: PropTypes.object.isRequired
};

const styles = theme => ({
root: {
color: 'white'
},
});

export class Analytics extends PureComponent {

constructor(props) {
super(props);

this.state = {
openDetailsDialog: false,
detailsData: []
};
}

handleUserHelpClick = () => {
window.open('https://www.cboard.io/help', '_blank');
};

handleGoBack = () => {
const { history } = this.props;
history.replace('/');
};

handleDaysChange = event => {
this.props.onDaysChange(event.target.value);
};

handleDetailsDialogOpen = name => event => {
switch (name) {
case 'boards':
this.setState({ detailsData: this.props.totals.boards['rows'] });
break;
case 'words':
this.setState({ detailsData: this.props.totals.words['rows'] });
break;
case 'phrases':
this.setState({ detailsData: this.props.totals.phrases['rows'] });
break;
case 'editions':
this.setState({ detailsData: this.props.totals.editions['rows'] });
break;
default:
this.setState({ detailsData: [] });
break;
}
this.setState({
openDetailsDialog: true
})
};

getDates = range => {
const days = [];
const dateEnd = moment();
const dateStart = moment().subtract(range, 'days');
while (dateEnd.diff(dateStart, 'days') >= 0) {
days.push(dateStart.format('DD/MM'));
dateStart.add(1, 'days');
}
return days;
};

handleDialogClose() {
this.setState({
openDetailsDialog: false
});
}

render() {
const {
intl,
classes,
theme,
usage,
symbolSources,
topUsed,
days,
totals,
categoryTotals,
isFetching
} = this.props;
const tablesHead = [
intl.formatMessage(messages.name),
intl.formatMessage(messages.timesClicked),
intl.formatMessage(messages.action)
];
return (
<FullScreenDialog
className="Analytics"
open
title={<FormattedMessage {...messages.analytics} />}
onClose={this.handleGoBack}
fullWidth={true}
>
<Fragment>
<div className="Analytics__Graph">
<Grid
container
direction="row"
className="Analytics__Graph__Select">
<Grid item className="Analytics__Graph__Select__Item">
<FormControl variant="outlined">
<Select
className={classes.root}
labelId="range-select-label"
id="range-select"
autoWidth={false}
onChange={this.handleDaysChange}
value={days}
>
<MenuItem value={10}>{intl.formatMessage(messages.tenDaysUsage)}</MenuItem>
<MenuItem value={20}>{intl.formatMessage(messages.twentyDaysUsage)}</MenuItem>
<MenuItem value={30}>{intl.formatMessage(messages.thirtyDaysUsage)}</MenuItem>
<MenuItem value={60}>{intl.formatMessage(messages.sixtyDaysUsage)}</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item className="Analytics__Graph__Select__Item">
{isFetching && (<CircularProgress size={30} thickness={4} className={classes.root} />)}
</Grid>
</Grid>
<ModifiedAreaChart
height="200px"
option={{
series: [
{
data: usage.data,
type: 'line'
}
],
xAxis: {
data: this.getDates(days)
},
yAxis: {
max: usage.max,
min: usage.min,
offset: -13
}
}}
/>
</div>
<div className="Analytics__Metrics">
<Grid container spacing={3}>
<Grid item lg={8} md={8} sm={12} xs={12}>
<StatCards onDetailsClick={this.handleDetailsDialogOpen.bind(this)} data={totals} />
<TableCard
data={topUsed.symbols}
tableHead={tablesHead}
title={intl.formatMessage(messages.topUsedButtons)}
/>
</Grid>
<Grid item lg={4} md={4} sm={12} xs={12}>
<DoughnutChart
data={symbolSources}
title={intl.formatMessage(messages.symbolSources)}
height="300px"
color={[
theme.palette.primary.dark,
theme.palette.primary.main,
theme.palette.primary.light
]}
/>
<StatCards2 categoryTotals={categoryTotals} />
<Barchart
data={topUsed.boards}
title={intl.formatMessage(messages.mostUsedBoards)}
/>
</Grid>
</Grid>
</div>
<Dialog
onClose={this.handleDialogClose.bind(this)}
aria-labelledby="details-dialog"
open={this.state.openDetailsDialog}
TransitionComponent={Transition}
aria-describedby="details-desc"
>
<DialogContent>
<DialogContentText id="details-dialog-desc">
</DialogContentText>
<StyledTable data={this.state.detailsData} tableHead={tablesHead} isDense={true} />
</DialogContent>
<DialogActions>
<Button
onClick={this.handleDialogClose.bind(this)}
color="primary"
>
{intl.formatMessage(messages.close)}
</Button>
</DialogActions>
</Dialog>
</Fragment>
</FullScreenDialog>
);
}
}

Analytics.propTypes = propTypes;

export default withStyles(styles, { withTheme: true })(injectIntl(Analytics));
Loading

0 comments on commit f2004c7

Please sign in to comment.