q3stats/frontend/src/views/PlayerStatsView.js

307 lines
8.2 KiB
JavaScript

/**
* Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import ClassName from "classnames";
import React from "react";
import {connect} from "react-redux";
import underscore from "underscore";
import ChartComponent from "../components/ChartComponent";
import {cleanup, loadChartData, setMode} from "../actions/playerStats";
const WINS_CHART_TITLE_SESSION = "Wins and losses by session";
const WINS_CHART_TITLE_MAP = "Wins and losses by map";
const ACCURACY_CHART_TITLE_SESSION = "Average weapon accuracy by session";
const ACCURACY_CHART_TITLE_MAP = "Average weapon accuracy by map";
class ModeLinkComponent extends React.PureComponent {
constructor (props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick (event) {
event.stopPropagation();
event.preventDefault();
this.props.onSwitchMode(this.props.code);
}
render () {
let className = ClassName({
active: (this.props.mode == this.props.code)
});
return (
<li className={className}>
<a onClick={this.onClick} href="#">{this.props.name}</a>
</li>
);
}
}
ModeLinkComponent.propTypes = {
mode: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
code: React.PropTypes.string.isRequired,
onSwitchMode: React.PropTypes.func.isRequired
};
class BaseStatsChartComponent extends React.PureComponent {
constructor (props) {
super(props);
this.chartConfig = {};
}
componentDidMount () {
this.load();
}
componentDidUpdate (prevProps, prevState) {
if (prevProps.player != this.props.player) {
this.load(true);
} else if (prevProps.mode != this.props.mode) {
this.load();
}
}
kind () {
throw new Error("Not Implemented");
}
load (force) {
if (underscore.isUndefined(force)) {
force = false;
}
let hasData = true;
if (this.props.mode == "session") {
hasData = hasData && this.props.sessionData.categories.length > 0;
hasData = hasData && this.props.sessionData.series.length > 0;
} else {
hasData = hasData && this.props.mapData.categories.length > 0;
hasData = hasData && this.props.mapData.series.length > 0;
}
if (force || !hasData) {
this.props.dispatch(loadChartData(
this.props.dispatch, this.props.player, this.kind(), this.props.mode
));
}
}
title () {
throw new Error("Not Implemented");
}
render () {
let categories = this.props.sessionData.categories;
let series = this.props.sessionData.series;
if (this.props.mode == "map") {
categories = this.props.mapData.categories;
series = this.props.mapData.series;
}
return (
<ChartComponent
config={this.chartConfig}
categories={categories}
series={series}
title={this.title()} />
);
}
}
class WinsChartComponent extends BaseStatsChartComponent {
constructor (props) {
super(props);
this.chartConfig = {
"chart": {
"type": "column"
},
"credits": {
"enabled": false
},
"yAxis": {
"title": {
"text": "Score"
}
},
"plotOptions": {
"column": {
"pointPadding": 0.2,
"borderWidth": 0,
"stacking": "normal",
"dataLabels": {
"enabled": true,
"color": "white",
"style": {
"textShadow": "0 0 3px black"
}
}
}
},
"tooltip": {
"formatter": function () {
return (
"<b>" + this.x + "</b><br/>" + this.series.name + ": " +
this.y + "<br/>" + "Total: " + this.point.stackTotal
);
}
}
};
}
kind () {
return "wins";
}
title () {
if (this.props.mode == "map") {
return WINS_CHART_TITLE_MAP;
}
return WINS_CHART_TITLE_SESSION;
}
render () {
return super.render();
}
}
WinsChartComponent.propTypes = {
player: React.PropTypes.string.isRequired,
mode: React.PropTypes.string.isRequired,
sessionData: React.PropTypes.object.isRequired,
mapData: React.PropTypes.object.isRequired
};
class AccuracyChartComponent extends BaseStatsChartComponent {
constructor (props) {
super(props);
this.chartConfig = {
"credits": {
"enabled": false
},
"tooltip": {
"valueDecimals": 2,
"valueSuffix": "%"
},
"yAxis": {
"title": {
"text": "Avg accuracy [%]"
}
}
};
}
kind () {
return "accuracy";
}
title () {
if (this.props.mode == "map") {
return ACCURACY_CHART_TITLE_MAP;
}
return ACCURACY_CHART_TITLE_SESSION;
}
render () {
return super.render();
}
}
AccuracyChartComponent.propTypes = {
player: React.PropTypes.string.isRequired,
mode: React.PropTypes.string.isRequired,
sessionData: React.PropTypes.object.isRequired,
mapData: React.PropTypes.object.isRequired
};
class PlayerStatsView extends React.PureComponent {
constructor (props) {
super(props);
this.onSwitchMode = this.onSwitchMode.bind(this);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.player != this.props.params.player) {
this.props.dispatch(cleanup());
}
}
onSwitchMode (mode) {
this.props.dispatch(setMode(mode));
}
componentWillUnmount () {
this.props.dispatch(cleanup());
}
render () {
return (
<div>
<h2>{this.props.params.player} stats</h2>
<ul className="nav nav-tabs">
<ModeLinkComponent
mode={this.props.mode}
name="By session"
code="session"
onSwitchMode={this.onSwitchMode} />
<ModeLinkComponent
mode={this.props.mode}
name="By map"
code="map"
onSwitchMode={this.onSwitchMode} />
</ul>
<WinsChartComponent
player={this.props.params.player}
mode={this.props.mode}
sessionData={this.props.sessionWinsChartData}
mapData={this.props.mapWinsChartData}
dispatch={this.props.dispatch} />
<AccuracyChartComponent
player={this.props.params.player}
mode={this.props.mode}
sessionData={this.props.sessionAccuracyChartData}
mapData={this.props.mapAccuracyChartData}
dispatch={this.props.dispatch} />
</div>
);
}
}
PlayerStatsView.propTypes = {
mode: React.PropTypes.string.isRequired,
sessionWinsChartData: React.PropTypes.object.isRequired,
mapWinsChartData: React.PropTypes.object.isRequired,
sessionAccuracyChartData: React.PropTypes.object.isRequired,
mapAccuracyChartData: React.PropTypes.object.isRequired,
params: React.PropTypes.object.isRequired
};
const mapStateToProps = (state, props) => {
return {
mode: state.playerStats.mode,
sessionWinsChartData: state.playerStats.sessionWinsChartData,
mapWinsChartData: state.playerStats.mapWinsChartData,
sessionAccuracyChartData: state.playerStats.sessionAccuracyChartData,
mapAccuracyChartData: state.playerStats.mapAccuracyChartData,
params: props.params
};
};
const reduxContainer = connect(mapStateToProps)(PlayerStatsView);
export default reduxContainer;