Rewritten most of the frontend to use Redux.
Also, lots of code cleanup, especially in frontend.
This commit is contained in:
parent
bfdcb87cef
commit
0451cdbbed
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -11,4 +11,4 @@ build/
|
|||
dist/
|
||||
q3stats.egg-info/
|
||||
|
||||
tests_web_app/tmp/
|
||||
tests_web_app/tmp/tmp*
|
||||
|
|
|
@ -24,16 +24,22 @@ import React from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
|
||||
import DashboardView from "./views/DashboardView";
|
||||
import DataSource from "./lib/DataSource";
|
||||
import NavigationBarView from "./views/NavigationBarView";
|
||||
|
||||
class AppWindow extends React.Component {
|
||||
componentWillMount () {
|
||||
DataSource.setRouter(this.props.router);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-app-window">
|
||||
<NavigationBarView router={this.props.router} />
|
||||
<NavigationBarView
|
||||
location={this.props.location}
|
||||
router={this.props.router} />
|
||||
|
||||
<div className="q3stats-content-view">
|
||||
{this.props.children || <DashboardView router={this.props.router} />}
|
||||
{this.props.children || <DashboardView />}
|
||||
</div>
|
||||
|
||||
<div className="q3stats-footer">
|
||||
|
@ -49,8 +55,4 @@ class AppWindow extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
AppWindow.propTypes = {
|
||||
errorCode: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default AppWindow;
|
||||
|
|
70
frontend/src/actions/dashboard.js
Normal file
70
frontend/src/actions/dashboard.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* 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 {DASHBOARD_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: DASHBOARD_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const loadData = (dispatch) => {
|
||||
DataSource.loadDashboardData().then((data) => {
|
||||
dispatch(parseData(dispatch, data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: DASHBOARD_ACTIONS.LOAD_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseData = (dispatch, data) => {
|
||||
if (data.day) {
|
||||
dispatch(loadChartData(dispatch, data.day));
|
||||
}
|
||||
|
||||
return {
|
||||
type: DASHBOARD_ACTIONS.PARSE_DATA,
|
||||
data: data
|
||||
};
|
||||
};
|
||||
|
||||
export const loadChartData = (dispatch, day) => {
|
||||
DataSource.loadDayChartData(day).then((data) => {
|
||||
dispatch(parseChartData(data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: DASHBOARD_ACTIONS.LOAD_CHART_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseChartData = (data) => {
|
||||
return {
|
||||
type: DASHBOARD_ACTIONS.PARSE_CHART_DATA,
|
||||
categories: data.maps,
|
||||
series: data.scores
|
||||
};
|
||||
};
|
67
frontend/src/actions/navigationBar.js
Normal file
67
frontend/src/actions/navigationBar.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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 {NAVIGATION_BAR_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const expand = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.EXPAND
|
||||
};
|
||||
};
|
||||
|
||||
export const collapse = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.COLLAPSE
|
||||
};
|
||||
};
|
||||
|
||||
export const showAboutModal = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.SHOW_ABOUT_MODAL
|
||||
};
|
||||
};
|
||||
|
||||
export const hideAboutModal = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.HIDE_ABOUT_MODAL
|
||||
};
|
||||
};
|
||||
|
||||
export const showSettingsModal = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.SHOW_SETTINGS_MODAL
|
||||
};
|
||||
};
|
||||
|
||||
export const hideSettingsModal = () => {
|
||||
return {
|
||||
type: NAVIGATION_BAR_ACTIONS.HIDE_SETTINGS_MODAL
|
||||
};
|
||||
};
|
67
frontend/src/actions/playerGame.js
Normal file
67
frontend/src/actions/playerGame.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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 {PLAYER_GAME_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: PLAYER_GAME_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const loadData = (dispatch, player, game) => {
|
||||
DataSource.loadPlayerGameData(player, game).then((data) => {
|
||||
dispatch(parseData(dispatch, data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: PLAYER_GAME_ACTIONS.LOAD_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseData = (dispatch, data) => {
|
||||
dispatch(loadChartsData(dispatch, data.player, data.game));
|
||||
|
||||
return {
|
||||
type: PLAYER_GAME_ACTIONS.PARSE_DATA,
|
||||
data: data
|
||||
};
|
||||
};
|
||||
|
||||
export const loadChartsData = (dispatch, player, game) => {
|
||||
DataSource.loadPlayerGameChartsData(player, game).then((data) => {
|
||||
dispatch(parseChartsData(data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: PLAYER_GAME_ACTIONS.LOAD_CHARTS_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseChartsData = (data) => {
|
||||
return {
|
||||
type: PLAYER_GAME_ACTIONS.PARSE_CHARTS_DATA,
|
||||
data: data
|
||||
};
|
||||
};
|
57
frontend/src/actions/playerStats.js
Normal file
57
frontend/src/actions/playerStats.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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 {PLAYER_STATS_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: PLAYER_STATS_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const setMode = (mode) => {
|
||||
return {
|
||||
type: PLAYER_STATS_ACTIONS.SET_MODE,
|
||||
mode: mode
|
||||
};
|
||||
};
|
||||
|
||||
export const loadChartData = (dispatch, player, kind, mode) => {
|
||||
DataSource.loadPlayerStatsChartData(player, kind, mode).then((data) => {
|
||||
dispatch(parseChartData(data, player, kind, mode));
|
||||
});
|
||||
|
||||
return {
|
||||
type: PLAYER_STATS_ACTIONS.LOAD_CHART_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseChartData = (data, player, kind, mode) => {
|
||||
return {
|
||||
type: PLAYER_STATS_ACTIONS.PARSE_CHART_DATA,
|
||||
kind: kind,
|
||||
mode: mode,
|
||||
data: data
|
||||
};
|
||||
};
|
48
frontend/src/actions/players.js
Normal file
48
frontend/src/actions/players.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 {PLAYERS_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: PLAYERS_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const loadData = (dispatch) => {
|
||||
DataSource.loadPlayersData().then((data) => {
|
||||
dispatch(parseData(dispatch, data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: PLAYERS_ACTIONS.LOAD_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseData = (dispatch, data) => {
|
||||
return {
|
||||
type: PLAYERS_ACTIONS.PARSE_DATA,
|
||||
data: data
|
||||
};
|
||||
};
|
81
frontend/src/actions/sessions.js
Normal file
81
frontend/src/actions/sessions.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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 {SESSIONS_ACTIONS} from "../lib/defs";
|
||||
|
||||
import DataSource from "../lib/DataSource";
|
||||
|
||||
export const cleanup = () => {
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.CLEANUP
|
||||
};
|
||||
};
|
||||
|
||||
export const setDay = (dispatch, day) => {
|
||||
let actualDay = day || "";
|
||||
|
||||
dispatch(loadData(dispatch, actualDay));
|
||||
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.SET_DAY,
|
||||
day: actualDay
|
||||
};
|
||||
};
|
||||
|
||||
export const loadData = (dispatch, day) => {
|
||||
DataSource.loadSessionData(day).then((data) => {
|
||||
dispatch(parseData(dispatch, data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.LOAD_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseData = (dispatch, data) => {
|
||||
if (data.day) {
|
||||
dispatch(loadChartData(dispatch, data.day));
|
||||
}
|
||||
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.PARSE_DATA,
|
||||
data: data
|
||||
};
|
||||
};
|
||||
|
||||
export const loadChartData = (dispatch, day) => {
|
||||
DataSource.loadDayChartData(day).then((data) => {
|
||||
dispatch(parseChartData(data));
|
||||
});
|
||||
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.LOAD_CHART_DATA
|
||||
};
|
||||
};
|
||||
|
||||
export const parseChartData = (data) => {
|
||||
return {
|
||||
type: SESSIONS_ACTIONS.PARSE_CHART_DATA,
|
||||
categories: data.maps,
|
||||
series: data.scores
|
||||
};
|
||||
};
|
|
@ -20,11 +20,17 @@
|
|||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import {ACTION_SETTINGS_SET_LAYOUT} from "../lib/defs";
|
||||
import {SETTINGS_ACTIONS, SETTINGS_KEYS} from "../lib/defs";
|
||||
|
||||
export const setLayout = (newLayout) => {
|
||||
try {
|
||||
window.localStorage.setItem(SETTINGS_KEYS.LAYOUT, newLayout);
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
||||
return {
|
||||
type: ACTION_SETTINGS_SET_LAYOUT,
|
||||
type: SETTINGS_ACTIONS.SET_LAYOUT,
|
||||
layout: newLayout
|
||||
};
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ import ReactDOM from "react-dom";
|
|||
import {connect} from "react-redux";
|
||||
import underscore from "underscore";
|
||||
|
||||
class ChartComponent extends React.Component {
|
||||
class ChartComponent extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ const LAYOUT_TO_CLASSNAME = {};
|
|||
LAYOUT_TO_CLASSNAME[LAYOUT_WIDE] = "container-fluid";
|
||||
LAYOUT_TO_CLASSNAME[LAYOUT_NARROW] = "container";
|
||||
|
||||
class ContainerComponent extends React.Component {
|
||||
class ContainerComponent extends React.PureComponent {
|
||||
render () {
|
||||
let className = LAYOUT_TO_CLASSNAME[this.props.layout];
|
||||
if (!className) {
|
||||
|
|
|
@ -24,7 +24,7 @@ import React from "react";
|
|||
|
||||
import {ITEM_NAMES} from "../lib/defs";
|
||||
|
||||
class ItemIconComponent extends React.Component {
|
||||
class ItemIconComponent extends React.PureComponent {
|
||||
render () {
|
||||
let itemName = ITEM_NAMES[this.props.item] || "?";
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
import ClassName from "classnames";
|
||||
import React from "react";
|
||||
|
||||
class LoaderComponent extends React.Component {
|
||||
class LoaderComponent extends React.PureComponent {
|
||||
render () {
|
||||
let className = ClassName("q3stats-loader", {
|
||||
"visible": this.props.visible
|
||||
|
|
|
@ -24,7 +24,7 @@ import React from "react";
|
|||
|
||||
import {POWERUP_NAMES} from "../lib/defs";
|
||||
|
||||
class PowerupIconComponent extends React.Component {
|
||||
class PowerupIconComponent extends React.PureComponent {
|
||||
render () {
|
||||
let powerupName = POWERUP_NAMES[this.props.powerup] || "?";
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import React from "react";
|
|||
|
||||
import {WEAPON_NAMES} from "../lib/defs";
|
||||
|
||||
class WeaponIconComponent extends React.Component {
|
||||
class WeaponIconComponent extends React.PureComponent {
|
||||
render () {
|
||||
let weaponName = WEAPON_NAMES[this.props.weapon] || "?";
|
||||
|
||||
|
|
64
frontend/src/lib/DataSource.js
Normal file
64
frontend/src/lib/DataSource.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import underscore from "underscore";
|
||||
import "whatwg-fetch";
|
||||
|
||||
const DEFAULT_INIT = {
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
class DataSource {
|
||||
constructor () {
|
||||
this._router = null;
|
||||
}
|
||||
setRouter (router) {
|
||||
this._router = router;
|
||||
}
|
||||
_fetch (requestOrURL, init) {
|
||||
init = init || {};
|
||||
|
||||
let actualInit = underscore.extendOwn(
|
||||
underscore.clone(DEFAULT_INIT), init
|
||||
);
|
||||
|
||||
return window.fetch(requestOrURL, actualInit).catch((error) => {
|
||||
this._router.push("/error/");
|
||||
throw new Error("FetchError: Unable to fetch");
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
let statusCode = response.status || "";
|
||||
|
||||
this._router.push("/error/" + statusCode);
|
||||
throw new Error(
|
||||
"FetchError: '" + statusCode + "'" + " '" + response.statusText + "'"
|
||||
);
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
});
|
||||
}
|
||||
loadDashboardData () {
|
||||
return this._fetch("/api/v1/dashboard");
|
||||
}
|
||||
loadDayChartData (day) {
|
||||
return this._fetch("/api/v1/charts/day/" + day);
|
||||
}
|
||||
loadSessionData (day) {
|
||||
return this._fetch("/api/v1/sessions/" + day);
|
||||
}
|
||||
loadPlayerGameData (player, game) {
|
||||
return this._fetch("/api/v1/players/" + player + "/game/" + game);
|
||||
}
|
||||
loadPlayerGameChartsData (player, game) {
|
||||
return this._fetch("/api/v1/charts/player/" + player + "/game/" + game);
|
||||
}
|
||||
loadPlayersData () {
|
||||
return this._fetch("/api/v1/players");
|
||||
}
|
||||
loadPlayerStatsChartData (player, kind, mode) {
|
||||
return this._fetch(
|
||||
"/api/v1/charts/player/" + player + "/" + kind + "/" + mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let sharedDataSource = new DataSource();
|
||||
export default sharedDataSource;
|
|
@ -30,9 +30,61 @@ export const LAYOUT_CHOICES = [
|
|||
[LAYOUT_NARROW, "Narrow"]
|
||||
];
|
||||
|
||||
export const SETTINGS_KEY_LAYOUT = "layout";
|
||||
export const SETTINGS_KEYS = {
|
||||
LAYOUT: "layout"
|
||||
};
|
||||
|
||||
export const ACTION_SETTINGS_SET_LAYOUT = "ACTION_SETTINGS_SET_LAYOUT";
|
||||
export const DASHBOARD_ACTIONS = {
|
||||
CLEANUP: "ACTION_DASHBOARD_CLEANUP",
|
||||
LOAD_CHART_DATA: "ACTION_DASHBOARD_LOAD_CHART_DATA",
|
||||
LOAD_DATA: "ACTION_DASHBOARD_LOAD_DATA",
|
||||
PARSE_CHART_DATA: "ACTION_DASHBOARD_PARSE_CHART_DATA",
|
||||
PARSE_DATA: "ACTION_DASHBOARD_PARSE_DATA"
|
||||
};
|
||||
|
||||
export const NAVIGATION_BAR_ACTIONS = {
|
||||
CLEANUP: "ACTION_NAVIGATION_BAR_CLEANUP",
|
||||
EXPAND: "ACTION_NAVIGATION_BAR_EXPAND",
|
||||
COLLAPSE: "ACTION_NAVIGATION_BAR_COLLAPSE",
|
||||
SHOW_ABOUT_MODAL: "ACTION_NAVIGATION_BAR_SHOW_ABOUT_MODAL",
|
||||
HIDE_ABOUT_MODAL: "ACTION_NAVIGATION_BAR_HIDE_ABOUT_MODAL",
|
||||
SHOW_SETTINGS_MODAL: "ACTION_NAVIGATION_BAR_SHOW_SETTINGS_MODAL",
|
||||
HIDE_SETTINGS_MODAL: "ACTION_NAVIGATION_BAR_HIDE_SETTINGS_MODAL"
|
||||
};
|
||||
|
||||
export const PLAYER_GAME_ACTIONS = {
|
||||
CLEANUP: "ACTION_PLAYER_GAME_CLEANUP",
|
||||
LOAD_CHARTS_DATA: "ACTION_PLAYER_GAME_LOAD_CHARTS_DATA",
|
||||
LOAD_DATA: "ACTION_PLAYER_GAME_LOAD_DATA",
|
||||
PARSE_CHARTS_DATA: "ACTION_PLAYER_GAME_PARSE_CHARTS",
|
||||
PARSE_DATA: "ACTION_PLAYER_GAME_PARSE_DATA"
|
||||
};
|
||||
|
||||
export const PLAYER_STATS_ACTIONS = {
|
||||
CLEANUP: "ACTION_PLAYER_STATS_CLEANUP",
|
||||
SET_MODE: "ACTION_PLAYER_STATS_SET_MODE",
|
||||
LOAD_CHART_DATA: "ACTION_PLAYER_STATS_LOAD_CHART_DATA",
|
||||
PARSE_CHART_DATA: "ACTION_PLAYER_STATS_PARSE_CHART_DATA"
|
||||
};
|
||||
|
||||
export const PLAYERS_ACTIONS = {
|
||||
CLEANUP: "ACTION_PLAYERS_CLEANUP",
|
||||
LOAD_DATA: "ACTION_PLAYERS_LOAD_DATA",
|
||||
PARSE_DATA: "ACTION_PLAYERS_PARSE_DATA"
|
||||
};
|
||||
|
||||
export const SESSIONS_ACTIONS = {
|
||||
CLEANUP: "ACTION_SESSIONS_CLEANUP",
|
||||
LOAD_CHART_DATA: "ACTION_SESSIONS_LOAD_CHART_DATA",
|
||||
LOAD_DATA: "ACTION_SESSIONS_LOAD_DATA",
|
||||
PARSE_CHART_DATA: "ACTION_SESSIONS_PARSE_CHART_DATA",
|
||||
PARSE_DATA: "ACTION_SESSIONS_PARSE_DATA",
|
||||
SET_DAY: "ACTION_SESSIONS_SET_DAY"
|
||||
};
|
||||
|
||||
export const SETTINGS_ACTIONS = {
|
||||
SET_LAYOUT: "ACTION_SETTINGS_SET_LAYOUT"
|
||||
};
|
||||
|
||||
export const WEAPON_NAMES = {
|
||||
"BFG": "BFG10k",
|
||||
|
|
|
@ -7,7 +7,6 @@ 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";
|
||||
|
@ -15,11 +14,11 @@ import PlayersView from "./views/PlayersView";
|
|||
import PlayerGameView from "./views/PlayerGameView";
|
||||
import PlayerStatsView from "./views/PlayerStatsView";
|
||||
import ErrorView from "./views/ErrorView";
|
||||
import reactStore from "./store";
|
||||
import reduxStore from "./store";
|
||||
|
||||
window.addEventListener("load", function () {
|
||||
ReactDOM.render(
|
||||
<Provider store={reactStore}>
|
||||
<Provider store={reduxStore}>
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={AppWindow}>
|
||||
<Route path="/sessions/" component={SessionsView}>
|
||||
|
|
71
frontend/src/reducers/dashboard.js
Normal file
71
frontend/src/reducers/dashboard.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* 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 {DASHBOARD_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
loading: true,
|
||||
day: "",
|
||||
emosOfTheMonth: [],
|
||||
fraggersOfTheMonth: [],
|
||||
categories: [],
|
||||
series: []
|
||||
};
|
||||
|
||||
const dashboardReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case DASHBOARD_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
case DASHBOARD_ACTIONS.LOAD_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case DASHBOARD_ACTIONS.PARSE_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: false,
|
||||
day: action.data.day,
|
||||
emosOfTheMonth: action.data.eotm,
|
||||
fraggersOfTheMonth: action.data.fotm
|
||||
});
|
||||
|
||||
case DASHBOARD_ACTIONS.LOAD_CHART_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
categories: [],
|
||||
series: []
|
||||
});
|
||||
|
||||
case DASHBOARD_ACTIONS.PARSE_CHART_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
categories: action.categories,
|
||||
series: action.series
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default dashboardReducer;
|
|
@ -22,9 +22,21 @@
|
|||
|
||||
import {combineReducers} from "redux";
|
||||
|
||||
import dashboardReducer from "./dashboard";
|
||||
import navigationBarReducer from "./navigationBar";
|
||||
import playerGameReducer from "./playerGame";
|
||||
import playerStatsReducer from "./playerStats";
|
||||
import playersReducer from "./players";
|
||||
import sessionsReducer from "./sessions";
|
||||
import settingsReducer from "./settings";
|
||||
|
||||
const Q3StatsApp = combineReducers({
|
||||
dashboard: dashboardReducer,
|
||||
navigationBar: navigationBarReducer,
|
||||
playerGame: playerGameReducer,
|
||||
playerStats: playerStatsReducer,
|
||||
players: playersReducer,
|
||||
sessions: sessionsReducer,
|
||||
settings: settingsReducer
|
||||
});
|
||||
|
||||
|
|
75
frontend/src/reducers/navigationBar.js
Normal file
75
frontend/src/reducers/navigationBar.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* 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 {NAVIGATION_BAR_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
expanded: false,
|
||||
showAboutModal: false,
|
||||
showSettingsModal: false
|
||||
};
|
||||
|
||||
const navigationBarReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case NAVIGATION_BAR_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.EXPAND:
|
||||
return underscore.extendOwn({}, state, {
|
||||
expanded: true
|
||||
});
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.COLLAPSE:
|
||||
return underscore.extendOwn({}, state, {
|
||||
expanded: false
|
||||
});
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.SHOW_ABOUT_MODAL:
|
||||
return underscore.extendOwn({}, state, {
|
||||
expanded: false,
|
||||
showAboutModal: true
|
||||
});
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.HIDE_ABOUT_MODAL:
|
||||
return underscore.extendOwn({}, state, {
|
||||
showAboutModal: false
|
||||
});
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.SHOW_SETTINGS_MODAL:
|
||||
return underscore.extendOwn({}, state, {
|
||||
expanded: false,
|
||||
showSettingsModal: true
|
||||
});
|
||||
|
||||
case NAVIGATION_BAR_ACTIONS.HIDE_SETTINGS_MODAL:
|
||||
return underscore.extendOwn({}, state, {
|
||||
showSettingsModal: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default navigationBarReducer;
|
90
frontend/src/reducers/playerGame.js
Normal file
90
frontend/src/reducers/playerGame.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* 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 {PLAYER_GAME_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
loading: true,
|
||||
map: "",
|
||||
weaponStats: {},
|
||||
itemStats: {},
|
||||
powerupStats: {},
|
||||
scoreChartSeries: [],
|
||||
damageChartSeries: [],
|
||||
totalsChartSeries: []
|
||||
};
|
||||
|
||||
const processSerieData = (serie, name) => {
|
||||
if (!serie) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
"type": "pie",
|
||||
"name": name,
|
||||
"data": serie
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const playerGameReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case PLAYER_GAME_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
case PLAYER_GAME_ACTIONS.LOAD_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case PLAYER_GAME_ACTIONS.PARSE_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: false,
|
||||
map: action.data.map,
|
||||
weaponStats: action.data.weapons || {},
|
||||
itemStats: action.data.items || {},
|
||||
powerupStats: action.data.powerups || {}
|
||||
});
|
||||
|
||||
case PLAYER_GAME_ACTIONS.LOAD_CHARTS_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
scoreChartSeries: [],
|
||||
damageChartSeries: [],
|
||||
totalsChartSeries: []
|
||||
});
|
||||
|
||||
case PLAYER_GAME_ACTIONS.PARSE_CHARTS_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
scoreChartSeries: processSerieData(action.data.score),
|
||||
damageChartSeries: processSerieData(action.data.damage),
|
||||
totalsChartSeries: processSerieData(action.data.totals)
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default playerGameReducer;
|
101
frontend/src/reducers/playerStats.js
Normal file
101
frontend/src/reducers/playerStats.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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 {PLAYER_STATS_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
mode: "session",
|
||||
sessionWinsChartData: {
|
||||
categories: [],
|
||||
series: []
|
||||
},
|
||||
mapWinsChartData: {
|
||||
categories: [],
|
||||
series: []
|
||||
},
|
||||
sessionAccuracyChartData: {
|
||||
categories: [],
|
||||
series: []
|
||||
},
|
||||
mapAccuracyChartData: {
|
||||
categories: [],
|
||||
series: []
|
||||
}
|
||||
};
|
||||
|
||||
const processChartData = (data, mode, kind) => {
|
||||
let parsedData = {
|
||||
categories: [],
|
||||
series: []
|
||||
};
|
||||
|
||||
if (mode == "session") {
|
||||
parsedData.categories = data.dates;
|
||||
} else {
|
||||
parsedData.categories = data.maps;
|
||||
}
|
||||
|
||||
let key = mode;
|
||||
if (kind == "wins") {
|
||||
key = key + "WinsChartData";
|
||||
|
||||
parsedData.series = [
|
||||
{"name": "Wins", "data": data.wins},
|
||||
{"name": "Losses", "data": data.losses}
|
||||
];
|
||||
} else if (kind == "accuracy") {
|
||||
key = key + "AccuracyChartData";
|
||||
parsedData.series = data.series;
|
||||
}
|
||||
|
||||
let newState = {};
|
||||
newState[key] = parsedData;
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
const playerStatsReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case PLAYER_STATS_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
case PLAYER_STATS_ACTIONS.SET_MODE:
|
||||
return underscore.extendOwn({}, state, {
|
||||
mode: action.mode
|
||||
});
|
||||
|
||||
case PLAYER_STATS_ACTIONS.LOAD_CHART_DATA:
|
||||
return state;
|
||||
|
||||
case PLAYER_STATS_ACTIONS.PARSE_CHART_DATA:
|
||||
return underscore.extendOwn(
|
||||
{}, state, processChartData(action.data, action.mode, action.kind)
|
||||
);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default playerStatsReducer;
|
|
@ -20,37 +20,34 @@
|
|||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import underscore from "underscore";
|
||||
|
||||
const DEFAULT_INIT = {
|
||||
credentials: "include"
|
||||
import {PLAYERS_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
loading: true,
|
||||
players: []
|
||||
};
|
||||
|
||||
class BaseView extends React.Component {
|
||||
fetch (requestOrURL, init) {
|
||||
init = init || {};
|
||||
const playersReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case PLAYERS_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
let actualInit = underscore.extendOwn(
|
||||
underscore.clone(DEFAULT_INIT), init
|
||||
);
|
||||
case PLAYERS_ACTIONS.LOAD_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
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 || "";
|
||||
case PLAYERS_ACTIONS.PARSE_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: false,
|
||||
players: action.data.players
|
||||
});
|
||||
|
||||
this.props.router.push("/error/" + statusCode);
|
||||
throw new Error(
|
||||
"FetchError: '" + statusCode + "'" + " '" + response.statusText + "'"
|
||||
);
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default BaseView;
|
||||
export default playersReducer;
|
79
frontend/src/reducers/sessions.js
Normal file
79
frontend/src/reducers/sessions.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* 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 {SESSIONS_ACTIONS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
loading: true,
|
||||
day: "",
|
||||
previousDay: "",
|
||||
nextDay: "",
|
||||
games: [],
|
||||
categories: [],
|
||||
series: []
|
||||
};
|
||||
|
||||
const sessionsReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SESSIONS_ACTIONS.CLEANUP:
|
||||
return underscore.clone(DEFAULT_STATE);
|
||||
|
||||
case SESSIONS_ACTIONS.SET_DAY:
|
||||
return underscore.extendOwn({}, state, {
|
||||
day: action.day,
|
||||
loading: true
|
||||
});
|
||||
|
||||
case SESSIONS_ACTIONS.LOAD_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
loading: true
|
||||
});
|
||||
|
||||
case SESSIONS_ACTIONS.PARSE_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
day: action.data.day,
|
||||
games: action.data.games,
|
||||
loading: false,
|
||||
nextDay: action.data.next_day || "",
|
||||
previousDay: action.data.previous_day || ""
|
||||
});
|
||||
|
||||
case SESSIONS_ACTIONS.LOAD_CHART_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
categories: [],
|
||||
series: []
|
||||
});
|
||||
|
||||
case SESSIONS_ACTIONS.PARSE_CHART_DATA:
|
||||
return underscore.extendOwn({}, state, {
|
||||
categories: action.categories,
|
||||
series: action.series
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default sessionsReducer;
|
|
@ -22,15 +22,13 @@
|
|||
|
||||
import underscore from "underscore";
|
||||
|
||||
import {
|
||||
DEFAULT_LAYOUT, SETTINGS_KEY_LAYOUT, ACTION_SETTINGS_SET_LAYOUT
|
||||
} from "../lib/defs";
|
||||
import {DEFAULT_LAYOUT, SETTINGS_ACTIONS, SETTINGS_KEYS} from "../lib/defs";
|
||||
|
||||
const DEFAULT_STATE = {};
|
||||
DEFAULT_STATE[SETTINGS_KEY_LAYOUT] = (function () {
|
||||
DEFAULT_STATE[SETTINGS_KEYS.LAYOUT] = (function () {
|
||||
let result = null;
|
||||
try {
|
||||
result = window.localStorage.getItem(SETTINGS_KEY_LAYOUT);
|
||||
result = window.localStorage.getItem(SETTINGS_KEYS.LAYOUT);
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
|
@ -38,9 +36,9 @@ DEFAULT_STATE[SETTINGS_KEY_LAYOUT] = (function () {
|
|||
return result || DEFAULT_LAYOUT;
|
||||
})();
|
||||
|
||||
const settings = (state = DEFAULT_STATE, action) => {
|
||||
const settingsReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_SETTINGS_SET_LAYOUT:
|
||||
case SETTINGS_ACTIONS.SET_LAYOUT:
|
||||
return underscore.extendOwn({}, state, {
|
||||
layout: action.layout
|
||||
});
|
||||
|
@ -50,4 +48,4 @@ const settings = (state = DEFAULT_STATE, action) => {
|
|||
}
|
||||
};
|
||||
|
||||
export default settings;
|
||||
export default settingsReducer;
|
||||
|
|
|
@ -24,7 +24,7 @@ import Button from "react-bootstrap/lib/Button";
|
|||
import Modal from "react-bootstrap/lib/Modal";
|
||||
import React from "react";
|
||||
|
||||
class AboutModalView extends React.Component {
|
||||
class AboutModalView extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
|
@ -60,7 +60,7 @@ class AboutModalView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
AboutModalView.PropTypes = {
|
||||
AboutModalView.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -21,13 +21,14 @@
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ChartComponent from "../components/ChartComponent";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
import {cleanup, loadData} from "../actions/dashboard";
|
||||
|
||||
class TopPlayersTableView extends React.Component {
|
||||
class TopPlayersTableView extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
|
@ -64,7 +65,7 @@ TopPlayersTableView.propTypes = {
|
|||
scores: React.PropTypes.array
|
||||
};
|
||||
|
||||
class DashboardView extends BaseView {
|
||||
class DashboardView extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
|
@ -91,35 +92,14 @@ class DashboardView extends BaseView {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
if (!this.props.day) {
|
||||
this.props.dispatch(loadData(this.props.dispatch));
|
||||
}
|
||||
}
|
||||
loadChartData () {
|
||||
this.fetch("/api/v1/charts/day/" + this.state.day).then((data) => {
|
||||
this.setState({
|
||||
categories: data.maps,
|
||||
series: data.scores
|
||||
});
|
||||
});
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
|
@ -128,9 +108,9 @@ class DashboardView extends BaseView {
|
|||
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
categories={this.state.categories}
|
||||
series={this.state.series}
|
||||
subtitle={this.state.day} />
|
||||
categories={this.props.categories}
|
||||
series={this.props.series}
|
||||
subtitle={this.props.day} />
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
|
@ -138,7 +118,7 @@ class DashboardView extends BaseView {
|
|||
|
||||
<TopPlayersTableView
|
||||
scoreColumnTitle="Frags"
|
||||
scores={this.state.fraggersOfTheMonth} />
|
||||
scores={this.props.fraggersOfTheMonth} />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6">
|
||||
|
@ -146,14 +126,29 @@ class DashboardView extends BaseView {
|
|||
|
||||
<TopPlayersTableView
|
||||
scoreColumnTitle="Suicides"
|
||||
scores={this.state.emosOfTheMonth} />
|
||||
scores={this.props.emosOfTheMonth} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
<LoaderComponent visible={this.props.loading} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardView;
|
||||
DashboardView.propTypes = {
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
day: React.PropTypes.string.isRequired,
|
||||
emosOfTheMonth: React.PropTypes.array.isRequired,
|
||||
fraggersOfTheMonth: React.PropTypes.array.isRequired,
|
||||
categories: React.PropTypes.array.isRequired,
|
||||
series: React.PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
return state.dashboard;
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(DashboardView);
|
||||
|
||||
export default reduxContainer;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
import React from "react";
|
||||
|
||||
class ErrorView extends React.Component {
|
||||
class ErrorView extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-error-view">
|
||||
|
@ -44,4 +44,8 @@ class ErrorView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ErrorView.propTypes = {
|
||||
params: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default ErrorView;
|
||||
|
|
|
@ -25,15 +25,19 @@ import ContainerComponent from "../components/ContainerComponent";
|
|||
import Glyphicon from "react-bootstrap/lib/Glyphicon";
|
||||
import React from "react";
|
||||
import Navbar from "react-bootstrap/lib/Navbar";
|
||||
import {connect} from "react-redux";
|
||||
import {Router, Link} from "react-router";
|
||||
import reduxStore from "../store";
|
||||
|
||||
import {
|
||||
cleanup, expand, collapse, showAboutModal, hideAboutModal, showSettingsModal,
|
||||
hideSettingsModal
|
||||
} from "../actions/navigationBar";
|
||||
import {LAYOUT_WIDE} from "../lib/defs";
|
||||
|
||||
import AboutModalView from "./AboutModalView";
|
||||
import SettingsModalView from "./SettingsModalView";
|
||||
|
||||
class NavigationBarItemView extends React.Component {
|
||||
class NavigationBarItemView extends React.PureComponent {
|
||||
render () {
|
||||
let className = ClassNames({
|
||||
active: this.props.active
|
||||
|
@ -56,66 +60,46 @@ NavigationBarItemView.propTypes = {
|
|||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class NavigationBarView extends React.Component {
|
||||
class NavigationBarView extends React.PureComponent {
|
||||
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});
|
||||
this.props.dispatch(collapse());
|
||||
}
|
||||
onNavigationBarToggleExpanded (expanded) {
|
||||
this.setState({navBarExpanded: expanded});
|
||||
}
|
||||
onHideSettingsModal () {
|
||||
this.setState({showSettingsModal: false});
|
||||
if (expanded) {
|
||||
this.props.dispatch(expand());
|
||||
} else {
|
||||
this.props.dispatch(collapse());
|
||||
}
|
||||
}
|
||||
onShowSettingsModalButtonClick () {
|
||||
this.setState({
|
||||
navBarExpanded: false,
|
||||
showSettingsModal: true
|
||||
});
|
||||
this.props.dispatch(showSettingsModal());
|
||||
}
|
||||
onStoreChange () {
|
||||
this.setState({layout: reduxStore.getState().settings.layout});
|
||||
}
|
||||
onHideAboutModal () {
|
||||
this.setState({showAboutModal: false});
|
||||
onHideSettingsModal () {
|
||||
this.props.dispatch(hideSettingsModal());
|
||||
}
|
||||
onShowAboutModalButtonClick () {
|
||||
this.setState({
|
||||
navBarExpanded: false,
|
||||
showAboutModal: true
|
||||
});
|
||||
this.props.dispatch(showAboutModal());
|
||||
}
|
||||
onHideAboutModal () {
|
||||
this.props.dispatch(hideAboutModal());
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="q3stats-navigation-bar">
|
||||
<Navbar
|
||||
ref="navbar"
|
||||
collapseOnSelect
|
||||
expanded={this.state.navBarExpanded}
|
||||
expanded={this.props.expanded}
|
||||
fixedTop
|
||||
fluid={this.state.layout == LAYOUT_WIDE}
|
||||
fluid={this.props.layout == LAYOUT_WIDE}
|
||||
onToggle={this.onNavigationBarToggleExpanded}>
|
||||
<Navbar.Header>
|
||||
<Navbar.Brand>
|
||||
|
@ -160,20 +144,38 @@ class NavigationBarView extends React.Component {
|
|||
</Navbar.Collapse>
|
||||
</Navbar>
|
||||
|
||||
<SettingsModalView
|
||||
show={this.state.showSettingsModal}
|
||||
onHide={this.onHideSettingsModal} />
|
||||
|
||||
<AboutModalView
|
||||
show={this.state.showAboutModal}
|
||||
show={this.props.showAboutModal}
|
||||
onHide={this.onHideAboutModal} />
|
||||
|
||||
<SettingsModalView
|
||||
show={this.props.showSettingsModal}
|
||||
onHide={this.onHideSettingsModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBarView.PropTypes = {
|
||||
router: React.PropTypes.instanceOf(Router).isRequired
|
||||
NavigationBarView.propTypes = {
|
||||
expanded: React.PropTypes.bool.isRequired,
|
||||
layout: React.PropTypes.string.isRequired,
|
||||
location: React.PropTypes.object.isRequired,
|
||||
router: React.PropTypes.object.isRequired,
|
||||
showAboutModal: React.PropTypes.bool.isRequired,
|
||||
showSettingsModal: React.PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default NavigationBarView;
|
||||
const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
expanded: state.navigationBar.expanded,
|
||||
layout: state.settings.layout,
|
||||
location: props.location,
|
||||
router: props.router,
|
||||
showAboutModal: state.navigationBar.showAboutModal,
|
||||
showSettingsModal: state.navigationBar.showSettingsModal
|
||||
};
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(NavigationBarView);
|
||||
|
||||
export default reduxContainer;
|
||||
|
|
|
@ -21,17 +21,18 @@
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
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";
|
||||
import {cleanup, loadData} from "../actions/playerGame";
|
||||
|
||||
class PlayerGameView extends BaseView {
|
||||
class PlayerGameView extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
|
@ -40,87 +41,39 @@ class PlayerGameView extends BaseView {
|
|||
"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();
|
||||
});
|
||||
this.props.dispatch(loadData(
|
||||
this.props.dispatch, this.props.params.player, this.props.params.game
|
||||
));
|
||||
}
|
||||
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)
|
||||
});
|
||||
});
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<ContainerComponent>
|
||||
<h2>{this.props.params.player} stats on {this.state.map}</h2>
|
||||
<h2>{this.props.params.player} stats on {this.props.map}</h2>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.scoreChartSeries}
|
||||
series={this.props.scoreChartSeries}
|
||||
title="Score breakdown" />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.damageChartSeries}
|
||||
series={this.props.damageChartSeries}
|
||||
title="Damage breakdown" />
|
||||
</div>
|
||||
|
||||
<div className="col-sm-4">
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
series={this.state.totalsChartSeries}
|
||||
series={this.props.totalsChartSeries}
|
||||
title="Health & armor" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,15 +92,15 @@ class PlayerGameView extends BaseView {
|
|||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.weaponStats).length == 0 &&
|
||||
{underscore.keys(this.props.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];
|
||||
underscore.keys(this.props.weaponStats), (key, index) => {
|
||||
let stats = this.props.weaponStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
|
@ -176,15 +129,15 @@ class PlayerGameView extends BaseView {
|
|||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.itemStats).length == 0 &&
|
||||
{underscore.keys(this.props.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];
|
||||
underscore.keys(this.props.itemStats), (key, index) => {
|
||||
let value = this.props.itemStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
|
@ -211,15 +164,15 @@ class PlayerGameView extends BaseView {
|
|||
</thead>
|
||||
|
||||
<tbody>
|
||||
{underscore.keys(this.state.powerupStats).length == 0 &&
|
||||
{underscore.keys(this.props.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];
|
||||
underscore.keys(this.props.powerupStats), (key, index) => {
|
||||
let stats = this.props.powerupStats[key];
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
|
@ -235,10 +188,38 @@ class PlayerGameView extends BaseView {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
<LoaderComponent visible={this.props.loading} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerGameView;
|
||||
PlayerGameView.propTypes = {
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
map: React.PropTypes.string.isRequired,
|
||||
weaponStats: React.PropTypes.object.isRequired,
|
||||
itemStats: React.PropTypes.object.isRequired,
|
||||
powerupStats: React.PropTypes.object.isRequired,
|
||||
scoreChartSeries: React.PropTypes.array.isRequired,
|
||||
damageChartSeries: React.PropTypes.array.isRequired,
|
||||
totalsChartSeries: React.PropTypes.array.isRequired,
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
loading: state.playerGame.loading,
|
||||
map: state.playerGame.map,
|
||||
weaponStats: state.playerGame.weaponStats,
|
||||
itemStats: state.playerGame.itemStats,
|
||||
powerupStats: state.playerGame.powerupStats,
|
||||
scoreChartSeries: state.playerGame.scoreChartSeries,
|
||||
damageChartSeries: state.playerGame.damageChartSeries,
|
||||
totalsChartSeries: state.playerGame.totalsChartSeries,
|
||||
params: props.params
|
||||
};
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(PlayerGameView);
|
||||
|
||||
export default reduxContainer;
|
||||
|
|
|
@ -22,16 +22,18 @@
|
|||
|
||||
import ClassName from "classnames";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import underscore from "underscore";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
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.Component {
|
||||
class ModeLinkComponent extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
|
@ -62,80 +64,55 @@ ModeLinkComponent.propTypes = {
|
|||
onSwitchMode: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
class BaseStatsChartComponent extends BaseView {
|
||||
class BaseStatsChartComponent extends React.PureComponent {
|
||||
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) {
|
||||
if (prevProps.player != this.props.player) {
|
||||
this.load(true);
|
||||
} else if (prevProps.mode != this.props.mode) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
url () {
|
||||
kind () {
|
||||
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;
|
||||
load (force) {
|
||||
if (underscore.isUndefined(force)) {
|
||||
force = false;
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
this.fetch(this.url()).then((data) => {
|
||||
let newState = {};
|
||||
let parsedData = this.parse(data);
|
||||
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 (this.props.mode == "session") {
|
||||
newState.sessionCategories = parsedData.categories;
|
||||
newState.sessionSeries = parsedData.series;
|
||||
} else {
|
||||
newState.mapCategories = parsedData.categories;
|
||||
newState.mapSeries = parsedData.series;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
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.state.sessionCategories;
|
||||
let series = this.state.sessionSeries;
|
||||
let categories = this.props.sessionData.categories;
|
||||
let series = this.props.sessionData.series;
|
||||
|
||||
if (this.props.mode == "map") {
|
||||
categories = this.state.mapCategories;
|
||||
series = this.state.mapSeries;
|
||||
categories = this.props.mapData.categories;
|
||||
series = this.props.mapData.series;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -188,27 +165,8 @@ class WinsChartComponent extends BaseStatsChartComponent {
|
|||
}
|
||||
};
|
||||
}
|
||||
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;
|
||||
kind () {
|
||||
return "wins";
|
||||
}
|
||||
title () {
|
||||
if (this.props.mode == "map") {
|
||||
|
@ -222,6 +180,13 @@ class WinsChartComponent extends BaseStatsChartComponent {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -241,25 +206,8 @@ class AccuracyChartComponent extends BaseStatsChartComponent {
|
|||
}
|
||||
};
|
||||
}
|
||||
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;
|
||||
kind () {
|
||||
return "accuracy";
|
||||
}
|
||||
title () {
|
||||
if (this.props.mode == "map") {
|
||||
|
@ -273,25 +221,28 @@ class AccuracyChartComponent extends BaseStatsChartComponent {
|
|||
}
|
||||
}
|
||||
|
||||
WinsChartComponent.propTypes = {
|
||||
AccuracyChartComponent.propTypes = {
|
||||
player: React.PropTypes.string.isRequired,
|
||||
mode: React.PropTypes.string.isRequired
|
||||
mode: React.PropTypes.string.isRequired,
|
||||
sessionData: React.PropTypes.object.isRequired,
|
||||
mapData: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
class PlayerStatsView extends React.Component {
|
||||
class PlayerStatsView extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onSwitchMode = this.onSwitchMode.bind(this);
|
||||
|
||||
this.state = {
|
||||
mode: "session"
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.setState({mode: "session"});
|
||||
if (nextProps.params.player != this.props.params.player) {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
}
|
||||
onSwitchMode (mode) {
|
||||
this.setState({mode: mode});
|
||||
this.props.dispatch(setMode(mode));
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
|
@ -300,13 +251,13 @@ class PlayerStatsView extends React.Component {
|
|||
|
||||
<ul className="nav nav-tabs">
|
||||
<ModeLinkComponent
|
||||
mode={this.state.mode}
|
||||
mode={this.props.mode}
|
||||
name="By session"
|
||||
code="session"
|
||||
onSwitchMode={this.onSwitchMode} />
|
||||
|
||||
<ModeLinkComponent
|
||||
mode={this.state.mode}
|
||||
mode={this.props.mode}
|
||||
name="By map"
|
||||
code="map"
|
||||
onSwitchMode={this.onSwitchMode} />
|
||||
|
@ -314,16 +265,42 @@ class PlayerStatsView extends React.Component {
|
|||
|
||||
<WinsChartComponent
|
||||
player={this.props.params.player}
|
||||
mode={this.state.mode}
|
||||
router={this.props.router} />
|
||||
mode={this.props.mode}
|
||||
sessionData={this.props.sessionWinsChartData}
|
||||
mapData={this.props.mapWinsChartData}
|
||||
dispatch={this.props.dispatch} />
|
||||
|
||||
<AccuracyChartComponent
|
||||
player={this.props.params.player}
|
||||
mode={this.state.mode}
|
||||
router={this.props.router} />
|
||||
mode={this.props.mode}
|
||||
sessionData={this.props.sessionAccuracyChartData}
|
||||
mapData={this.props.mapAccuracyChartData}
|
||||
dispatch={this.props.dispatch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerStatsView;
|
||||
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;
|
||||
|
|
|
@ -23,13 +23,14 @@
|
|||
import ClassName from "classnames";
|
||||
import React from "react";
|
||||
import {Link} from "react-router";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
import BaseView from "../lib/BaseView";
|
||||
import ContainerComponent from "../components/ContainerComponent";
|
||||
import LoaderComponent from "../components/LoaderComponent";
|
||||
import PlayerStatsView from "./PlayerStatsView";
|
||||
import {cleanup, loadData} from "../actions/players";
|
||||
|
||||
class PlayerLinkComponent extends React.Component {
|
||||
class PlayerLinkComponent extends React.PureComponent {
|
||||
render () {
|
||||
let className = ClassName({
|
||||
active: this.props.active
|
||||
|
@ -48,22 +49,12 @@ PlayerLinkComponent.propTypes = {
|
|||
player: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
class PlayersView extends BaseView {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
ready: false,
|
||||
players: []
|
||||
};
|
||||
}
|
||||
class PlayersView extends React.PureComponent {
|
||||
componentDidMount () {
|
||||
this.fetch("/api/v1/players").then((data) => {
|
||||
this.setState({
|
||||
ready: true,
|
||||
players: data.players
|
||||
});
|
||||
});
|
||||
this.props.dispatch(loadData(this.props.dispatch));
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
|
@ -72,7 +63,7 @@ class PlayersView extends BaseView {
|
|||
<div className="col-sm-3">
|
||||
<h2>Players</h2>
|
||||
<ul className="nav nav-pills nav-stacked">
|
||||
{this.state.players.map((item, index) => {
|
||||
{this.props.players.map((item, index) => {
|
||||
return (
|
||||
<PlayerLinkComponent
|
||||
key={index}
|
||||
|
@ -93,10 +84,27 @@ class PlayersView extends BaseView {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
<LoaderComponent visible={this.props.loading} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayersView;
|
||||
PlayersView.propTypes = {
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
players: React.PropTypes.array.isRequired,
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
loading: state.players.loading,
|
||||
players: state.players.players,
|
||||
params: props.params
|
||||
};
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(PlayersView);
|
||||
|
||||
export default reduxContainer;
|
||||
|
||||
|
|
|
@ -22,15 +22,16 @@
|
|||
|
||||
import ClassName from "classnames";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
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";
|
||||
import {cleanup, setDay} from "../actions/sessions";
|
||||
|
||||
class NavComponent extends React.Component {
|
||||
class NavComponent extends React.PureComponent {
|
||||
render () {
|
||||
let previousClassName = ClassName("previous", {
|
||||
disabled: !this.props.previousDay
|
||||
|
@ -64,7 +65,7 @@ NavComponent.propTypes = {
|
|||
nextDay: React.PropTypes.string
|
||||
};
|
||||
|
||||
class GamesTableView extends React.Component {
|
||||
class GamesTableView extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<table className="table table-striped table-bordered table-hover">
|
||||
|
@ -136,7 +137,7 @@ GamesTableView.propTypes = {
|
|||
games: React.PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
class SessionsView extends BaseView {
|
||||
class SessionsView extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
|
@ -163,96 +164,80 @@ class SessionsView extends BaseView {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.state = {
|
||||
shouldReload: false,
|
||||
ready: false,
|
||||
day: "",
|
||||
previousDay: "",
|
||||
nextDay: "",
|
||||
games: [],
|
||||
categories: null,
|
||||
series: []
|
||||
};
|
||||
}
|
||||
setDay (day) {
|
||||
this.props.dispatch(setDay(this.props.dispatch, day));
|
||||
}
|
||||
componentDidMount () {
|
||||
this.loadData();
|
||||
this.setDay(this.props.params.day);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(cleanup());
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if ((this.state.shouldReload) && (!this.state.ready)) {
|
||||
this.setState({shouldReload: false});
|
||||
this.loadData();
|
||||
if (prevProps.params.day != this.props.params.day) {
|
||||
this.setDay(this.props.params.day);
|
||||
}
|
||||
}
|
||||
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 &&
|
||||
{!this.props.loading &&
|
||||
<div>
|
||||
<NavComponent
|
||||
previousDay={this.state.previousDay}
|
||||
nextDay={this.state.nextDay} />
|
||||
previousDay={this.props.previousDay}
|
||||
nextDay={this.props.nextDay} />
|
||||
|
||||
<h2>Results</h2>
|
||||
|
||||
<ChartComponent
|
||||
config={this.chartConfig}
|
||||
categories={this.state.categories}
|
||||
series={this.state.series}
|
||||
subtitle={this.state.day} />
|
||||
categories={this.props.categories}
|
||||
series={this.props.series}
|
||||
subtitle={this.props.day} />
|
||||
|
||||
<h2>Stats</h2>
|
||||
|
||||
<GamesTableView
|
||||
games={this.state.games} />
|
||||
games={this.props.games} />
|
||||
|
||||
<NavComponent
|
||||
onNavigateToDay={this.onNavigateToDay}
|
||||
previousDay={this.state.previousDay}
|
||||
nextDay={this.state.nextDay} />
|
||||
previousDay={this.props.previousDay}
|
||||
nextDay={this.props.nextDay} />
|
||||
</div>
|
||||
}
|
||||
|
||||
<LoaderComponent visible={!this.state.ready} />
|
||||
<LoaderComponent visible={this.props.loading} />
|
||||
</ContainerComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionsView;
|
||||
SessionsView.propTypes = {
|
||||
categories: React.PropTypes.array.isRequired,
|
||||
day: React.PropTypes.string.isRequired,
|
||||
games: React.PropTypes.array.isRequired,
|
||||
loading: React.PropTypes.bool.isRequired,
|
||||
nextDay: React.PropTypes.string.isRequired,
|
||||
params: React.PropTypes.object.isRequired,
|
||||
previousDay: React.PropTypes.string.isRequired,
|
||||
series: React.PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
loading: state.sessions.loading,
|
||||
day: state.sessions.day,
|
||||
previousDay: state.sessions.previousDay,
|
||||
nextDay: state.sessions.nextDay,
|
||||
games: state.sessions.games,
|
||||
categories: state.sessions.categories,
|
||||
series: state.sessions.series,
|
||||
params: props.params
|
||||
};
|
||||
};
|
||||
|
||||
const reduxContainer = connect(mapStateToProps)(SessionsView);
|
||||
|
||||
export default reduxContainer;
|
||||
|
|
|
@ -29,10 +29,12 @@ 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 {
|
||||
DEFAULT_LAYOUT, LAYOUT_CHOICES, SETTINGS_KEY_LAYOUT
|
||||
} from "../lib/defs";
|
||||
import {setLayout} from "../actions/settings";
|
||||
|
||||
class SettingsModalView extends React.Component {
|
||||
class SettingsModalView extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
|
||||
|
@ -42,16 +44,15 @@ class SettingsModalView extends React.Component {
|
|||
layout: this.props.settings.layout
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
this.setState({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();
|
||||
|
@ -95,7 +96,6 @@ class SettingsModalView extends React.Component {
|
|||
}
|
||||
|
||||
SettingsModalView.PropTypes = {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
settings: React.PropTypes.object.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
|
|
|
@ -92,6 +92,8 @@ def get_api_v1_player_game(player, game_uuid):
|
|||
abort(404)
|
||||
|
||||
result = {
|
||||
"game": game_uuid,
|
||||
"player": player,
|
||||
"map": game.map,
|
||||
"items": score.items,
|
||||
"weapons": _process_weapons(score.weapons),
|
||||
|
|
|
@ -86,6 +86,8 @@ class Test_GetAPIv1Players(BaseQ3StatsWebAppTestCase):
|
|||
rsp = self.client.get('/api/v1/players/Player 1/game/game1')
|
||||
assert rsp.status_code == 200
|
||||
|
||||
assert rsp.json['game'] == 'game1'
|
||||
assert rsp.json['player'] == 'Player 1'
|
||||
assert rsp.json['map'] == 'Q3DM7'
|
||||
assert rsp.json['items'] == {'YA': 1}
|
||||
|
||||
|
|
0
tests_web_app/tmp/.placeholder
Normal file
0
tests_web_app/tmp/.placeholder
Normal file
Loading…
Reference in New Issue
Block a user