You've already forked q3stats
Q3Stats is now open source! :)
This commit is contained in:
56
frontend/src/AppWindow.js
Normal file
56
frontend/src/AppWindow.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import DashboardView from "./views/DashboardView";
|
||||
import NavigationBarView from "./views/NavigationBarView";
|
||||
|
||||
class AppWindow extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-app-window">
|
||||
<NavigationBarView router={this.props.router} />
|
||||
|
||||
<div className="q3stats-content-view">
|
||||
{this.props.children || <DashboardView router={this.props.router} />}
|
||||
</div>
|
||||
|
||||
<div className="q3stats-footer">
|
||||
<p className="text-center text-muted">
|
||||
<small>
|
||||
<span>Q3Stats v{window.Q3STATS.version} by </span>
|
||||
<a href="https://www.bthlabs.pl/">Tomek Wójcik</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppWindow.propTypes = {
|
||||
errorCode: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default AppWindow;
|
||||
30
frontend/src/actions/settings.js
Normal file
30
frontend/src/actions/settings.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 {ACTION_SETTINGS_SET_LAYOUT} from "../lib/defs";
|
||||
|
||||
export const setLayout = (newLayout) => {
|
||||
return {
|
||||
type: ACTION_SETTINGS_SET_LAYOUT,
|
||||
layout: newLayout
|
||||
};
|
||||
};
|
||||
143
frontend/src/components/ChartComponent.js
Normal file
143
frontend/src/components/ChartComponent.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 Highcharts from "highcharts";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {connect} from "react-redux";
|
||||
import underscore from "underscore";
|
||||
|
||||
class ChartComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this._chart = null;
|
||||
}
|
||||
componentDidMount () {
|
||||
this.renderChart();
|
||||
}
|
||||
componentDidUpdate () {
|
||||
this.renderChart();
|
||||
}
|
||||
componentWillUnmount () {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
}
|
||||
}
|
||||
hasSeries () {
|
||||
return (
|
||||
underscore.isArray(this.props.series) && this.props.series.length > 0
|
||||
);
|
||||
}
|
||||
renderChart () {
|
||||
let config = underscore.extendOwn({}, this.props.config);
|
||||
|
||||
if (!config.title) {
|
||||
config.title = {};
|
||||
}
|
||||
|
||||
if (this.props.title) {
|
||||
config.title.text = this.props.title;
|
||||
}
|
||||
|
||||
if (!config.subtitle) {
|
||||
config.subtitle = {};
|
||||
}
|
||||
|
||||
if (this.props.subtitle) {
|
||||
config.subtitle.text = this.props.subtitle;
|
||||
}
|
||||
|
||||
config.series = [];
|
||||
|
||||
if (!config.xAxis) {
|
||||
config.xAxis = {};
|
||||
}
|
||||
|
||||
config.xAxis.categories = [];
|
||||
|
||||
if (this.props.categories) {
|
||||
config.xAxis.categories = this.props.categories;
|
||||
}
|
||||
|
||||
if (this.props.series) {
|
||||
config.series = this.props.series;
|
||||
}
|
||||
|
||||
if (!this._chart) {
|
||||
this._chart = Highcharts.chart(
|
||||
ReactDOM.findDOMNode(this), config
|
||||
);
|
||||
} else {
|
||||
this._chart.setTitle(config.title, config.subtitle, false);
|
||||
|
||||
let currentSeriesCount = this._chart.series.length;
|
||||
for (let i = currentSeriesCount - 1; i >= 0; i--) {
|
||||
this._chart.series[i].remove(false);
|
||||
}
|
||||
|
||||
for (let i = 0; i < config.series.length; i++) {
|
||||
this._chart.addSeries(config.series[i], false);
|
||||
}
|
||||
|
||||
this._chart.xAxis[0].update(config.xAxis, false);
|
||||
|
||||
this._chart.reflow();
|
||||
this._chart.redraw();
|
||||
}
|
||||
|
||||
if (this._chart.series.length == 0) {
|
||||
this._chart.showLoading();
|
||||
} else {
|
||||
this._chart.hideLoading();
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-chart"></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChartComponent.propTypes = {
|
||||
categories: React.PropTypes.array,
|
||||
config: React.PropTypes.object.isRequired,
|
||||
series: React.PropTypes.array.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
subtitle: React.PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
categories: ownProps.categories,
|
||||
config: ownProps.config,
|
||||
layout: state.settings.layout,
|
||||
series: ownProps.series,
|
||||
title: ownProps.title,
|
||||
subtitle: ownProps.subtitle
|
||||
};
|
||||
};
|
||||
|
||||
let reduxContainer = connect(mapStateToProps)(ChartComponent);
|
||||
|
||||
export default reduxContainer;
|
||||
59
frontend/src/components/ContainerComponent.js
Normal file
59
frontend/src/components/ContainerComponent.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
import {DEFAULT_LAYOUT, LAYOUT_WIDE, LAYOUT_NARROW} from "../lib/defs";
|
||||
|
||||
const LAYOUT_TO_CLASSNAME = {};
|
||||
LAYOUT_TO_CLASSNAME[LAYOUT_WIDE] = "container-fluid";
|
||||
LAYOUT_TO_CLASSNAME[LAYOUT_NARROW] = "container";
|
||||
|
||||
class ContainerComponent extends React.Component {
|
||||
render () {
|
||||
let className = LAYOUT_TO_CLASSNAME[this.props.layout];
|
||||
if (!className) {
|
||||
className = LAYOUT_TO_CLASSNAME[DEFAULT_LAYOUT];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ContainerComponent.PropTypes = {
|
||||
layout: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
layout: state.settings.layout
|
||||
};
|
||||
};
|
||||
|
||||
let reduxContainer = connect(mapStateToProps)(ContainerComponent);
|
||||
|
||||
export default reduxContainer;
|
||||
44
frontend/src/components/ItemIconComponent.js
Normal file
44
frontend/src/components/ItemIconComponent.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
import {ITEM_NAMES} from "../lib/defs";
|
||||
|
||||
class ItemIconComponent extends React.Component {
|
||||
render () {
|
||||
let itemName = ITEM_NAMES[this.props.item] || "?";
|
||||
|
||||
return (
|
||||
<img
|
||||
src={"/static/img/items/" + this.props.item + ".png"}
|
||||
alt={itemName}
|
||||
title={itemName} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ItemIconComponent.propTypes = {
|
||||
item: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ItemIconComponent;
|
||||
42
frontend/src/components/LoaderComponent.js
Normal file
42
frontend/src/components/LoaderComponent.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
class LoaderComponent extends React.Component {
|
||||
render () {
|
||||
let className = ClassName("q3stats-loader", {
|
||||
"visible": this.props.visible
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}><div className="inner"></div></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoaderComponent.propTypes = {
|
||||
visible: React.PropTypes.bool
|
||||
};
|
||||
|
||||
export default LoaderComponent;
|
||||
44
frontend/src/components/PowerupIconComponent.js
Normal file
44
frontend/src/components/PowerupIconComponent.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
import {POWERUP_NAMES} from "../lib/defs";
|
||||
|
||||
class PowerupIconComponent extends React.Component {
|
||||
render () {
|
||||
let powerupName = POWERUP_NAMES[this.props.powerup] || "?";
|
||||
|
||||
return (
|
||||
<img
|
||||
src={"/static/img/powerups/" + this.props.powerup + ".png"}
|
||||
alt={powerupName}
|
||||
title={powerupName} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PowerupIconComponent.propTypes = {
|
||||
powerup: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default PowerupIconComponent;
|
||||
44
frontend/src/components/WeaponIconComponent.js
Normal file
44
frontend/src/components/WeaponIconComponent.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
import {WEAPON_NAMES} from "../lib/defs";
|
||||
|
||||
class WeaponIconComponent extends React.Component {
|
||||
render () {
|
||||
let weaponName = WEAPON_NAMES[this.props.weapon] || "?";
|
||||
|
||||
return (
|
||||
<img
|
||||
src={"/static/img/weapons/" + this.props.weapon + ".png"}
|
||||
alt={weaponName}
|
||||
title={weaponName} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WeaponIconComponent.propTypes = {
|
||||
weapon: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default WeaponIconComponent;
|
||||
56
frontend/src/lib/BaseView.js
Normal file
56
frontend/src/lib/BaseView.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import underscore from "underscore";
|
||||
|
||||
const DEFAULT_INIT = {
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
class BaseView extends React.Component {
|
||||
fetch (requestOrURL, init) {
|
||||
init = init || {};
|
||||
|
||||
let actualInit = underscore.extendOwn(
|
||||
underscore.clone(DEFAULT_INIT), init
|
||||
);
|
||||
|
||||
return window.fetch(requestOrURL, actualInit).catch((error) => {
|
||||
this.props.router.push("/error/");
|
||||
throw new Error("FetchError: Unable to fetch");
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
let statusCode = response.status || "";
|
||||
|
||||
this.props.router.push("/error/" + statusCode);
|
||||
throw new Error(
|
||||
"FetchError: '" + statusCode + "'" + " '" + response.statusText + "'"
|
||||
);
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseView;
|
||||
59
frontend/src/lib/defs.js
Normal file
59
frontend/src/lib/defs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const LAYOUT_WIDE = "wide";
|
||||
export const LAYOUT_NARROW = "narrow";
|
||||
|
||||
export const DEFAULT_LAYOUT = LAYOUT_WIDE;
|
||||
|
||||
export const LAYOUT_CHOICES = [
|
||||
[LAYOUT_WIDE, "Wide"],
|
||||
[LAYOUT_NARROW, "Narrow"]
|
||||
];
|
||||
|
||||
export const SETTINGS_KEY_LAYOUT = "layout";
|
||||
|
||||
export const ACTION_SETTINGS_SET_LAYOUT = "ACTION_SETTINGS_SET_LAYOUT";
|
||||
|
||||
export const WEAPON_NAMES = {
|
||||
"BFG": "BFG10k",
|
||||
"G": "Gauntlet",
|
||||
"GL": "Grenade Launcher",
|
||||
"LG": "Lightning Gun",
|
||||
"MG": "Machine Gun",
|
||||
"PG": "Plasma Gun",
|
||||
"RG": "Railgun",
|
||||
"RL": "Rocket Launcher",
|
||||
"SG": "Shotgun"
|
||||
};
|
||||
|
||||
export const ITEM_NAMES = {
|
||||
"GA": "Green Armor",
|
||||
"MH": "Mega Health",
|
||||
"RA": "Red Armor",
|
||||
"Regen": "Regeneration",
|
||||
"YA": "Yellow Armor"
|
||||
};
|
||||
|
||||
export const POWERUP_NAMES = {
|
||||
"Quad": "Quad Damage"
|
||||
};
|
||||
40
frontend/src/main.js
Normal file
40
frontend/src/main.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/*!
|
||||
* Q3Stats JS app | Copyright 2017 by Tomek Wójcik | MIT License
|
||||
* https://git.bthlabs.pl/q3stats
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {Router, Route, hashHistory} from "react-router";
|
||||
import {Provider} from "react-redux";
|
||||
import "whatwg-fetch";
|
||||
|
||||
import AppWindow from "./AppWindow";
|
||||
import SessionsView from "./views/SessionsView";
|
||||
import PlayersView from "./views/PlayersView";
|
||||
import PlayerGameView from "./views/PlayerGameView";
|
||||
import PlayerStatsView from "./views/PlayerStatsView";
|
||||
import ErrorView from "./views/ErrorView";
|
||||
import reactStore from "./store";
|
||||
|
||||
window.addEventListener("load", function () {
|
||||
ReactDOM.render(
|
||||
<Provider store={reactStore}>
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={AppWindow}>
|
||||
<Route path="/sessions/" component={SessionsView}>
|
||||
<Route path="/sessions/:day" component={SessionsView} />
|
||||
</Route>
|
||||
<Route path="/players/:player/game/:game" component={PlayerGameView} />
|
||||
<Route path="/players/" component={PlayersView}>
|
||||
<Route path="/players/:player" component={PlayerStatsView} />
|
||||
</Route>
|
||||
<Route path="/error/" component={ErrorView}>
|
||||
<Route path="/error/:errorCode" component={ErrorView} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById("main")
|
||||
);
|
||||
});
|
||||
31
frontend/src/reducers/index.js
Normal file
31
frontend/src/reducers/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 {combineReducers} from "redux";
|
||||
|
||||
import settingsReducer from "./settings";
|
||||
|
||||
const Q3StatsApp = combineReducers({
|
||||
settings: settingsReducer
|
||||
});
|
||||
|
||||
export default Q3StatsApp;
|
||||
53
frontend/src/reducers/settings.js
Normal file
53
frontend/src/reducers/settings.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 underscore from "underscore";
|
||||
|
||||
import {
|
||||
DEFAULT_LAYOUT, SETTINGS_KEY_LAYOUT, ACTION_SETTINGS_SET_LAYOUT
|
||||
} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {};
|
||||
DEFAULT_STATE[SETTINGS_KEY_LAYOUT] = (function () {
|
||||
let result = null;
|
||||
try {
|
||||
result = window.localStorage.getItem(SETTINGS_KEY_LAYOUT);
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
||||
return result || DEFAULT_LAYOUT;
|
||||
})();
|
||||
|
||||
const settings = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_SETTINGS_SET_LAYOUT:
|
||||
return underscore.extendOwn({}, state, {
|
||||
layout: action.layout
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default settings;
|
||||
29
frontend/src/store.js
Normal file
29
frontend/src/store.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 {createStore} from "redux";
|
||||
|
||||
import Q3StatsApp from "./reducers";
|
||||
|
||||
let store = createStore(Q3StatsApp);
|
||||
|
||||
export default store;
|
||||
68
frontend/src/views/AboutModalView.js
Normal file
68
frontend/src/views/AboutModalView.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 Button from "react-bootstrap/lib/Button";
|
||||
import Modal from "react-bootstrap/lib/Modal";
|
||||
import React from "react";
|
||||
|
||||
class AboutModalView extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>About</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body className="q3stats-about-modal-view">
|
||||
<p className="lead text-center">Q3Stats v{window.Q3STATS.version}</p>
|
||||
<p className="text-center small">
|
||||
Copyright © 2017 by <a href="https://www.bthlabs.pl/">Tomek Wójcik</a>
|
||||
</p>
|
||||
<p className="text-center small">Licensed under terms of MIT License</p>
|
||||
<p>
|
||||
This application uses Highcharts. Please make sure you understand
|
||||
the licesing terms of this library and purchase license
|
||||
if needed. <a href="https://shop.highsoft.com/highcharts/#non-com">Learn more »</a>
|
||||
</p>
|
||||
<p>Q3Stats is powered by: <em>classnames</em>, <em>highcharts</em>, <em>
|
||||
react</em>, <em>react-dom</em>, <em>react-redux</em>, <em>
|
||||
react-router</em>, <em>redux</em>, <em>underscore</em> and <em>
|
||||
whatwg-fetch</em>.</p>
|
||||
<p>Greetz: <em>Eliminator</em>, <em>kapitan</em>, <em>kraju</em>, <em>mcvsama
|
||||
</em>, <em>Szeryf Bydgoszczy</em>, <em>unnamedDzwiedziu</em>.</p>
|
||||
<p className="text-center"><small><strong>The 5th floor lives on!</strong></small></p>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.props.onHide}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AboutModalView.PropTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AboutModalView;
|
||||
159
frontend/src/views/DashboardView.js
Normal file
159
frontend/src/views/DashboardView.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ChartComponent from "../components/ChartComponent";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
|
||||
class TopPlayersTableView extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>{this.props.scoreColumnTitle}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{(this.props.scores.length == 0) &&
|
||||
<tr>
|
||||
<td colSpan="2"><strong>No stats :(</strong></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{this.props.scores.map(function (item, index) {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>{item[0]}</td>
|
||||
<td>{item[1]}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TopPlayersTableView.propTypes = {
|
||||
scoreColumnTitle: React.PropTypes.string.isRequired,
|
||||
scores: React.PropTypes.array
|
||||
};
|
||||
|
||||
class DashboardView extends BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.chartConfig = {
|
||||
"chart": {
|
||||
"type": "column"
|
||||
},
|
||||
"title": {
|
||||
"text": "Scores by map and player"
|
||||
},
|
||||
"credits": {
|
||||
"enabled": false
|
||||
},
|
||||
"yAxis": {
|
||||
"min": -10,
|
||||
"title": {
|
||||
"text": "Score"
|
||||
}
|
||||
},
|
||||
"plotOptions": {
|
||||
"column": {
|
||||
"pointPadding": 0.2,
|
||||
"borderWidth": 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.state = {
|
||||
ready: false,
|
||||
day: "",
|
||||
emosOfTheMonth: [],
|
||||
fraggersOfTheMonth: [],
|
||||
categories: null,
|
||||
series: []
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.fetch("/api/v1/dashboard").then((data) => {
|
||||
this.setState({
|
||||
ready: true,
|
||||
day: data.day,
|
||||
emosOfTheMonth: data.eotm,
|
||||
fraggersOfTheMonth: data.fotm
|
||||
});
|
||||
|
||||
this.loadChartData();
|
||||
});
|
||||
}
|
||||
loadChartData () {
|
||||
this.fetch("/api/v1/charts/day/" + this.state.day).then((data) => {
|
||||
this.setState({
|
||||
categories: data.maps,
|
||||
series: data.scores
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ContainerComponent>
|
||||
<h2>Dashboard</h2>
|
||||
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
categories={this.state.categories}
|
||||
series={this.state.series}
|
||||
subtitle={this.state.day} />
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<h2>Fraggers of the month</h2>
|
||||
|
||||
<TopPlayersTableView
|
||||
scoreColumnTitle="Frags"
|
||||
scores={this.state.fraggersOfTheMonth} />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6">
|
||||
<h2>Emos of the month</h2>
|
||||
|
||||
<TopPlayersTableView
|
||||
scoreColumnTitle="Suicides"
|
||||
scores={this.state.emosOfTheMonth} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardView;
|
||||
47
frontend/src/views/ErrorView.js
Normal file
47
frontend/src/views/ErrorView.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
|
||||
class ErrorView extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-error-view">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-sm-offset-3 text-center">
|
||||
<h2>Whoops!</h2>
|
||||
<p className="lead">Apparently something went wrong. Sorry for that.</p>
|
||||
{(this.props.params.errorCode) &&
|
||||
<p className="text-muted">
|
||||
<small>Error code: {this.props.params.errorCode}</small>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorView;
|
||||
179
frontend/src/views/NavigationBarView.js
Normal file
179
frontend/src/views/NavigationBarView.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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 ClassNames from "classnames";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import Glyphicon from "react-bootstrap/lib/Glyphicon";
|
||||
import React from "react";
|
||||
import Navbar from "react-bootstrap/lib/Navbar";
|
||||
import {Router, Link} from "react-router";
|
||||
import reduxStore from "../store";
|
||||
|
||||
import {LAYOUT_WIDE} from "../lib/defs";
|
||||
|
||||
import AboutModalView from "./AboutModalView";
|
||||
import SettingsModalView from "./SettingsModalView";
|
||||
|
||||
class NavigationBarItemView extends React.Component {
|
||||
render () {
|
||||
let className = ClassNames({
|
||||
active: this.props.active
|
||||
});
|
||||
|
||||
return (
|
||||
<li className={className}>
|
||||
<Link to={this.props.route} onClick={this.props.onClick}>
|
||||
{this.props.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBarItemView.propTypes = {
|
||||
active: React.PropTypes.bool,
|
||||
route: React.PropTypes.string,
|
||||
title: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class NavigationBarView extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onNavigationBarItemClick = this.onNavigationBarItemClick.bind(this);
|
||||
this.onNavigationBarToggleExpanded = this.onNavigationBarToggleExpanded.bind(this);
|
||||
this.onHideSettingsModal = this.onHideSettingsModal.bind(this);
|
||||
this.onShowSettingsModalButtonClick = this.onShowSettingsModalButtonClick.bind(this);
|
||||
this.onStoreChange = this.onStoreChange.bind(this);
|
||||
this.onHideAboutModal = this.onHideAboutModal.bind(this);
|
||||
this.onShowAboutModalButtonClick = this.onShowAboutModalButtonClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
layout: reduxStore.getState().settings.layout,
|
||||
navBarExpanded: false,
|
||||
showAboutModal: false,
|
||||
showSettingsModal: false
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this._storeUnsubscribe = reduxStore.subscribe(this.onStoreChange);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this._storeUnsubscribe();
|
||||
}
|
||||
onNavigationBarItemClick (e) {
|
||||
this.setState({navBarExpanded: false});
|
||||
}
|
||||
onNavigationBarToggleExpanded (expanded) {
|
||||
this.setState({navBarExpanded: expanded});
|
||||
}
|
||||
onHideSettingsModal () {
|
||||
this.setState({showSettingsModal: false});
|
||||
}
|
||||
onShowSettingsModalButtonClick () {
|
||||
this.setState({
|
||||
navBarExpanded: false,
|
||||
showSettingsModal: true
|
||||
});
|
||||
}
|
||||
onStoreChange () {
|
||||
this.setState({layout: reduxStore.getState().settings.layout});
|
||||
}
|
||||
onHideAboutModal () {
|
||||
this.setState({showAboutModal: false});
|
||||
}
|
||||
onShowAboutModalButtonClick () {
|
||||
this.setState({
|
||||
navBarExpanded: false,
|
||||
showAboutModal: true
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-navigation-bar">
|
||||
<Navbar
|
||||
ref="navbar"
|
||||
collapseOnSelect
|
||||
expanded={this.state.navBarExpanded}
|
||||
fixedTop
|
||||
fluid={this.state.layout == LAYOUT_WIDE}
|
||||
onToggle={this.onNavigationBarToggleExpanded}>
|
||||
<Navbar.Header>
|
||||
<Navbar.Brand>
|
||||
<a>Q3Stats</a>
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle />
|
||||
</Navbar.Header>
|
||||
|
||||
<Navbar.Collapse>
|
||||
<ul className="nav navbar-nav">
|
||||
<NavigationBarItemView
|
||||
active={this.props.router.isActive("/", true)}
|
||||
route="/"
|
||||
title="Dashboard"
|
||||
onClick={this.onNavigationBarItemClick} />
|
||||
<NavigationBarItemView
|
||||
active={this.props.router.isActive("/sessions/", false)}
|
||||
route="/sessions/"
|
||||
title="Sessions"
|
||||
onClick={this.onNavigationBarItemClick} />
|
||||
<NavigationBarItemView
|
||||
active={this.props.router.isActive("/players/", false)}
|
||||
route="/players/"
|
||||
title="Players"
|
||||
onClick={this.onNavigationBarItemClick} />
|
||||
</ul>
|
||||
|
||||
<ul className="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a onClick={this.onShowSettingsModalButtonClick}>
|
||||
<Glyphicon className="hidden-xs hidden-sm" glyph="cog" />
|
||||
<span className="hidden-md hidden-lg">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={this.onShowAboutModalButtonClick}>
|
||||
<Glyphicon className="hidden-xs hidden-sm" glyph="info-sign" />
|
||||
<span className="hidden-md hidden-lg">About</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
|
||||
<SettingsModalView
|
||||
show={this.state.showSettingsModal}
|
||||
onHide={this.onHideSettingsModal} />
|
||||
|
||||
<AboutModalView
|
||||
show={this.state.showAboutModal}
|
||||
onHide={this.onHideAboutModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBarView.PropTypes = {
|
||||
router: React.PropTypes.instanceOf(Router).isRequired
|
||||
};
|
||||
|
||||
export default NavigationBarView;
|
||||
244
frontend/src/views/PlayerGameView.js
Normal file
244
frontend/src/views/PlayerGameView.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import underscore from "underscore";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ChartComponent from "../components/ChartComponent";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import ItemIconComponent from "../components/ItemIconComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
import PowerupIconComponent from "../components/PowerupIconComponent";
|
||||
import WeaponIconComponent from "../components/WeaponIconComponent";
|
||||
|
||||
class PlayerGameView extends BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.chartConfig = {
|
||||
"credits": {
|
||||
"enabled": false
|
||||
}
|
||||
};
|
||||
|
||||
this.state = {
|
||||
ready: false,
|
||||
map: "",
|
||||
weaponStats: {},
|
||||
itemStats: {},
|
||||
powerupStats: {},
|
||||
scoreChartSeries: [],
|
||||
damageChartSeries: [],
|
||||
totalsChartSeries: []
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
let url = (
|
||||
"/api/v1/players/" + this.props.params.player + "/game/" +
|
||||
this.props.params.game
|
||||
);
|
||||
|
||||
this.fetch(url).then((data) => {
|
||||
this.setState({
|
||||
ready: true,
|
||||
map: data.map,
|
||||
weaponStats: data.weapons || {},
|
||||
itemStats: data.items || {},
|
||||
powerupStats: data.powerups || {}
|
||||
});
|
||||
|
||||
this.loadChartsData();
|
||||
});
|
||||
}
|
||||
processSerieData (serie, name) {
|
||||
if (!serie) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
"type": "pie",
|
||||
"name": name,
|
||||
"data": serie
|
||||
}
|
||||
];
|
||||
}
|
||||
loadChartsData () {
|
||||
let url = (
|
||||
"/api/v1/charts/player/" + this.props.params.player + "/game/" +
|
||||
this.props.params.game
|
||||
);
|
||||
|
||||
this.fetch(url).then((data) => {
|
||||
this.setState({
|
||||
scoreChartSeries: this.processSerieData(data.score),
|
||||
damageChartSeries: this.processSerieData(data.damage),
|
||||
totalsChartSeries: this.processSerieData(data.totals)
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ContainerComponent>
|
||||
<h2>{this.props.params.player} stats on {this.state.map}</h2>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.scoreChartSeries}
|
||||
title="Score breakdown" />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.damageChartSeries}
|
||||
title="Damage breakdown" />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.totalsChartSeries}
|
||||
title="Health & armor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Weapons</h2>
|
||||
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Weapon</th>
|
||||
<th>Shots</th>
|
||||
<th>Hits</th>
|
||||
<th>Kills</th>
|
||||
<th>Accuracy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.weaponStats).length == 0 &&
|
||||
<tr>
|
||||
<td colSpan="5"><strong>No stats :(</strong></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{underscore.map(
|
||||
underscore.keys(this.state.weaponStats), (key, index) => {
|
||||
let stats = this.state.weaponStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td><WeaponIconComponent weapon={key} /></td>
|
||||
<td className="text-right">{stats.shots}</td>
|
||||
<td className="text-right">{stats.hits}</td>
|
||||
<td className="text-right">{stats.kills}</td>
|
||||
<td className="text-right">{stats.accuracy}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<h2>Items</h2>
|
||||
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Pickups</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.itemStats).length == 0 &&
|
||||
<tr>
|
||||
<td colSpan="2"><strong>No stats :(</strong></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{underscore.map(
|
||||
underscore.keys(this.state.itemStats), (key, index) => {
|
||||
let value = this.state.itemStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td><ItemIconComponent item={key} /></td>
|
||||
<td className="text-right">{value}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6">
|
||||
<h2>Powerups</h2>
|
||||
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Pickups</th>
|
||||
<th>Total time [s]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.powerupStats).length == 0 &&
|
||||
<tr>
|
||||
<td colSpan="3"><strong>No stats :(</strong></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{underscore.map(
|
||||
underscore.keys(this.state.powerupStats), (key, index) => {
|
||||
let stats = this.state.powerupStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td><PowerupIconComponent powerup={key} /></td>
|
||||
<td className="text-right">{stats[0]}</td>
|
||||
<td className="text-right">{stats[1]}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerGameView;
|
||||
329
frontend/src/views/PlayerStatsView.js
Normal file
329
frontend/src/views/PlayerStatsView.js
Normal file
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* 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 BaseView from "../lib/BaseView";
|
||||
import ChartComponent from "../components/ChartComponent";
|
||||
|
||||
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.Component {
|
||||
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 BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.chartConfig = {};
|
||||
|
||||
this.state = {
|
||||
sessionCategories: [],
|
||||
sessionSeries: [],
|
||||
mapCategories: [],
|
||||
mapSeries: []
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.load();
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.player != this.props.player) {
|
||||
this.setState({
|
||||
sessionCategories: [],
|
||||
sessionSeries: [],
|
||||
mapCategories: [],
|
||||
mapSeries: []
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevProps.player != this.props.player || prevProps.mode != this.props.mode) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
url () {
|
||||
throw new Error("Not Implemented");
|
||||
}
|
||||
parse () {
|
||||
throw new Error("Not Implemented");
|
||||
}
|
||||
load () {
|
||||
let hasData = true;
|
||||
if (this.props.mode == "session") {
|
||||
hasData = hasData && this.state.sessionCategories.length > 0;
|
||||
hasData = hasData && this.state.sessionSeries.length > 0;
|
||||
} else {
|
||||
hasData = hasData && this.state.mapCategories.length > 0;
|
||||
hasData = hasData && this.state.mapSeries.length > 0;
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
this.fetch(this.url()).then((data) => {
|
||||
let newState = {};
|
||||
let parsedData = this.parse(data);
|
||||
|
||||
if (this.props.mode == "session") {
|
||||
newState.sessionCategories = parsedData.categories;
|
||||
newState.sessionSeries = parsedData.series;
|
||||
} else {
|
||||
newState.mapCategories = parsedData.categories;
|
||||
newState.mapSeries = parsedData.series;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
}
|
||||
title () {
|
||||
throw new Error("Not Implemented");
|
||||
}
|
||||
render () {
|
||||
let categories = this.state.sessionCategories;
|
||||
let series = this.state.sessionSeries;
|
||||
|
||||
if (this.props.mode == "map") {
|
||||
categories = this.state.mapCategories;
|
||||
series = this.state.mapSeries;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
url () {
|
||||
return (
|
||||
"/api/v1/charts/player/" + this.props.player + "/wins/" + this.props.mode
|
||||
);
|
||||
}
|
||||
parse (data) {
|
||||
let result = {
|
||||
categories: [],
|
||||
series: [
|
||||
{"name": "Wins", "data": data.wins},
|
||||
{"name": "Losses", "data": data.losses}
|
||||
]
|
||||
};
|
||||
|
||||
if (this.props.mode == "session") {
|
||||
result.categories = data.dates;
|
||||
} else {
|
||||
result.categories = data.maps;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
title () {
|
||||
if (this.props.mode == "map") {
|
||||
return WINS_CHART_TITLE_MAP;
|
||||
}
|
||||
|
||||
return WINS_CHART_TITLE_SESSION;
|
||||
}
|
||||
render () {
|
||||
return super.render();
|
||||
}
|
||||
}
|
||||
|
||||
class AccuracyChartComponent extends BaseStatsChartComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.chartConfig = {
|
||||
"credits": {
|
||||
"enabled": false
|
||||
},
|
||||
"tooltip": {
|
||||
"valueDecimals": 2,
|
||||
"valueSuffix": "%"
|
||||
},
|
||||
"yAxis": {
|
||||
"title": {
|
||||
"text": "Avg accuracy [%]"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
url () {
|
||||
return (
|
||||
"/api/v1/charts/player/" + this.props.player + "/accuracy/" +
|
||||
this.props.mode
|
||||
);
|
||||
}
|
||||
parse (data) {
|
||||
let result = {
|
||||
categories: [],
|
||||
series: data.series
|
||||
};
|
||||
|
||||
if (this.props.mode == "session") {
|
||||
result.categories = data.dates;
|
||||
} else {
|
||||
result.categories = data.maps;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
title () {
|
||||
if (this.props.mode == "map") {
|
||||
return ACCURACY_CHART_TITLE_MAP;
|
||||
}
|
||||
|
||||
return ACCURACY_CHART_TITLE_SESSION;
|
||||
}
|
||||
render () {
|
||||
return super.render();
|
||||
}
|
||||
}
|
||||
|
||||
WinsChartComponent.propTypes = {
|
||||
player: React.PropTypes.string.isRequired,
|
||||
mode: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class PlayerStatsView extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onSwitchMode = this.onSwitchMode.bind(this);
|
||||
|
||||
this.state = {
|
||||
mode: "session"
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.setState({mode: "session"});
|
||||
}
|
||||
onSwitchMode (mode) {
|
||||
this.setState({mode: mode});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<h2>{this.props.params.player} stats</h2>
|
||||
|
||||
<ul className="nav nav-tabs">
|
||||
<ModeLinkComponent
|
||||
mode={this.state.mode}
|
||||
name="By session"
|
||||
code="session"
|
||||
onSwitchMode={this.onSwitchMode} />
|
||||
|
||||
<ModeLinkComponent
|
||||
mode={this.state.mode}
|
||||
name="By map"
|
||||
code="map"
|
||||
onSwitchMode={this.onSwitchMode} />
|
||||
</ul>
|
||||
|
||||
<WinsChartComponent
|
||||
player={this.props.params.player}
|
||||
mode={this.state.mode}
|
||||
router={this.props.router} />
|
||||
|
||||
<AccuracyChartComponent
|
||||
player={this.props.params.player}
|
||||
mode={this.state.mode}
|
||||
router={this.props.router} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerStatsView;
|
||||
102
frontend/src/views/PlayersView.js
Normal file
102
frontend/src/views/PlayersView.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 {Link} from "react-router";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
import PlayerStatsView from "./PlayerStatsView";
|
||||
|
||||
class PlayerLinkComponent extends React.Component {
|
||||
render () {
|
||||
let className = ClassName({
|
||||
active: this.props.active
|
||||
});
|
||||
|
||||
return (
|
||||
<li className={className}>
|
||||
<Link to={"/players/" + this.props.player}>{this.props.player}</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerLinkComponent.propTypes = {
|
||||
active: React.PropTypes.bool.isRequired,
|
||||
player: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class PlayersView extends BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
ready: false,
|
||||
players: []
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.fetch("/api/v1/players").then((data) => {
|
||||
this.setState({
|
||||
ready: true,
|
||||
players: data.players
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ContainerComponent>
|
||||
<div className="row">
|
||||
<div className="col-sm-3">
|
||||
<h2>Players</h2>
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
{this.state.players.map((item, index) => {
|
||||
return (
|
||||
<PlayerLinkComponent
|
||||
key={index}
|
||||
active={(this.props.params.player == item)}
|
||||
player={item} />
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-9">
|
||||
{this.props.children ||
|
||||
<div>
|
||||
<h2>Players</h2>
|
||||
<p className="lead">Choose a player from the list to see their stats.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayersView;
|
||||
258
frontend/src/views/SessionsView.js
Normal file
258
frontend/src/views/SessionsView.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 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 {Link} from "react-router";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ChartComponent from "../components/ChartComponent";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
import WeaponIconComponent from "../components/WeaponIconComponent";
|
||||
|
||||
class NavComponent extends React.Component {
|
||||
render () {
|
||||
let previousClassName = ClassName("previous", {
|
||||
disabled: !this.props.previousDay
|
||||
});
|
||||
|
||||
let nextClassName = ClassName("next", {
|
||||
disabled: !this.props.nextDay
|
||||
});
|
||||
|
||||
let previousDayLinkTo = this.props.previousDay ? "/sessions/" + this.props.previousDay : "";
|
||||
let nextDayLinkTo = this.props.nextDay ? "/sessions/" + this.props.nextDay : "";
|
||||
|
||||
return (
|
||||
<nav>
|
||||
<ul className="pager">
|
||||
<li className={previousClassName}>
|
||||
<Link to={previousDayLinkTo}>← Previous</Link>
|
||||
</li>
|
||||
|
||||
<li className={nextClassName}>
|
||||
<Link to={nextDayLinkTo}>Next →</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavComponent.propTypes = {
|
||||
previousDay: React.PropTypes.string,
|
||||
nextDay: React.PropTypes.string
|
||||
};
|
||||
|
||||
class GamesTableView extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowSpan="2">Map</th>
|
||||
<th rowSpan="2">Player</th>
|
||||
<th colSpan="4" className="text-center">Scores</th>
|
||||
<th rowSpan="2">Favourite weapon</th>
|
||||
<th rowSpan="2">Quad freak?</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<th>Frags</th>
|
||||
<th>Deaths</th>
|
||||
<th>Suicides</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{(this.props.games.length == 0) &&
|
||||
<tr>
|
||||
<td colSpan="8"><strong>No stats :(</strong></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{this.props.games.map((game) => {
|
||||
return game.scores.map((score, index) => {
|
||||
let playerStatsLinkTo = "/players/" + score.player + "/game/" + game.uuid;
|
||||
|
||||
return (
|
||||
<tr key={game.uuid + index}>
|
||||
{(index == 0) &&
|
||||
<td rowSpan={game.scores.length}>{game.map}</td>
|
||||
}
|
||||
<td>
|
||||
<Link to={playerStatsLinkTo}>{score.player}</Link>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<strong>{score.score}</strong>
|
||||
</td>
|
||||
<td className="text-right">{score.kills}</td>
|
||||
<td className="text-right">{score.deaths}</td>
|
||||
<td className="text-right">{score.suicides}</td>
|
||||
<td className="text-right">
|
||||
{(!score.favourite_weapon) && "?"}
|
||||
{(score.favourite_weapon) &&
|
||||
<WeaponIconComponent weapon={score.favourite_weapon} />
|
||||
}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{(!score.quad_freak) && "No"}
|
||||
{(score.quad_freak) &&
|
||||
"Yes (" + score.quad_pickups + " pickups)"
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GamesTableView.propTypes = {
|
||||
games: React.PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
class SessionsView extends BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.chartConfig = {
|
||||
"chart": {
|
||||
"type": "column"
|
||||
},
|
||||
"title": {
|
||||
"text": "Scores by map and player"
|
||||
},
|
||||
"credits": {
|
||||
"enabled": false
|
||||
},
|
||||
"yAxis": {
|
||||
"min": -10,
|
||||
"title": {
|
||||
"text": "Score"
|
||||
}
|
||||
},
|
||||
"plotOptions": {
|
||||
"column": {
|
||||
"pointPadding": 0.2,
|
||||
"borderWidth": 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.state = {
|
||||
shouldReload: false,
|
||||
ready: false,
|
||||
day: "",
|
||||
previousDay: "",
|
||||
nextDay: "",
|
||||
games: [],
|
||||
categories: null,
|
||||
series: []
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.loadData();
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if ((this.state.shouldReload) && (!this.state.ready)) {
|
||||
this.setState({shouldReload: false});
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.day != this.props.params.day) {
|
||||
this.setState({
|
||||
shouldReload: true,
|
||||
ready: false,
|
||||
day: "",
|
||||
previousDay: "",
|
||||
nextDay: "",
|
||||
games: [],
|
||||
categories: null,
|
||||
series: []
|
||||
});
|
||||
}
|
||||
}
|
||||
loadData () {
|
||||
this.fetch("/api/v1/sessions/" + (this.props.params.day || "")).then((data) => {
|
||||
this.setState({
|
||||
shouldReload: false,
|
||||
ready: true,
|
||||
day: data.day,
|
||||
previousDay: data.previous_day,
|
||||
nextDay: data.next_day,
|
||||
games: data.games
|
||||
});
|
||||
|
||||
this.loadChartData();
|
||||
});
|
||||
}
|
||||
loadChartData () {
|
||||
this.fetch("/api/v1/charts/day/" + this.state.day).then((data) => {
|
||||
this.setState({
|
||||
categories: data.maps,
|
||||
series: data.scores
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ContainerComponent>
|
||||
{this.state.ready &&
|
||||
<div>
|
||||
<NavComponent
|
||||
previousDay={this.state.previousDay}
|
||||
nextDay={this.state.nextDay} />
|
||||
|
||||
<h2>Results</h2>
|
||||
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
categories={this.state.categories}
|
||||
series={this.state.series}
|
||||
subtitle={this.state.day} />
|
||||
|
||||
<h2>Stats</h2>
|
||||
|
||||
<GamesTableView
|
||||
games={this.state.games} />
|
||||
|
||||
<NavComponent
|
||||
onNavigateToDay={this.onNavigateToDay}
|
||||
previousDay={this.state.previousDay}
|
||||
nextDay={this.state.nextDay} />
|
||||
</div>
|
||||
}
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionsView;
|
||||
114
frontend/src/views/SettingsModalView.js
Normal file
114
frontend/src/views/SettingsModalView.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 Button from "react-bootstrap/lib/Button";
|
||||
import ControlLabel from "react-bootstrap/lib/ControlLabel";
|
||||
import Form from "react-bootstrap/lib/Form";
|
||||
import FormControl from "react-bootstrap/lib/FormControl";
|
||||
import FormGroup from "react-bootstrap/lib/FormGroup";
|
||||
import Modal from "react-bootstrap/lib/Modal";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
import {DEFAULT_LAYOUT, LAYOUT_CHOICES, SETTINGS_KEY_LAYOUT} from "../lib/defs";
|
||||
import {setLayout} from "../actions/settings";
|
||||
|
||||
class SettingsModalView extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
|
||||
this.onSelectLayoutChange = this.onSelectLayoutChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
layout: this.props.settings.layout
|
||||
};
|
||||
}
|
||||
onSaveButtonClick (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(SETTINGS_KEY_LAYOUT, this.state.layout);
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
||||
this.props.dispatch(setLayout(this.state.layout));
|
||||
|
||||
this.props.onHide();
|
||||
}
|
||||
onSelectLayoutChange (e) {
|
||||
this.setState({layout: e.target.value});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Settings</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<FormGroup controlId="settingsLayout">
|
||||
<ControlLabel>Layout</ControlLabel>
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
placeholder="-- select --"
|
||||
value={this.state.layout}
|
||||
onChange={this.onSelectLayoutChange}>
|
||||
{LAYOUT_CHOICES.map(function (item, index) {
|
||||
return (
|
||||
<option key={index} value={item[0]}>{item[1]}</option>
|
||||
);
|
||||
})}
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.props.onHide}>Cancel</Button>
|
||||
<Button bsStyle="primary" onClick={this.onSaveButtonClick}>Save</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsModalView.PropTypes = {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
settings: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
show: props.show,
|
||||
onHide: props.onHide
|
||||
};
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(SettingsModalView);
|
||||
|
||||
export default reduxContainer;
|
||||
Reference in New Issue
Block a user