Rewritten most of the frontend to use Redux.

Also, lots of code cleanup, especially in frontend.
master
Tomek Wójcik 2017-03-13 12:25:44 +01:00
parent bfdcb87cef
commit 0451cdbbed
38 changed files with 1302 additions and 410 deletions

2
.gitignore vendored
View File

@ -11,4 +11,4 @@ build/
dist/ dist/
q3stats.egg-info/ q3stats.egg-info/
tests_web_app/tmp/ tests_web_app/tmp/tmp*

View File

@ -24,16 +24,22 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import DashboardView from "./views/DashboardView"; import DashboardView from "./views/DashboardView";
import DataSource from "./lib/DataSource";
import NavigationBarView from "./views/NavigationBarView"; import NavigationBarView from "./views/NavigationBarView";
class AppWindow extends React.Component { class AppWindow extends React.Component {
componentWillMount () {
DataSource.setRouter(this.props.router);
}
render () { render () {
return ( return (
<div className="q3stats-app-window"> <div className="q3stats-app-window">
<NavigationBarView router={this.props.router} /> <NavigationBarView
location={this.props.location}
router={this.props.router} />
<div className="q3stats-content-view"> <div className="q3stats-content-view">
{this.props.children || <DashboardView router={this.props.router} />} {this.props.children || <DashboardView />}
</div> </div>
<div className="q3stats-footer"> <div className="q3stats-footer">
@ -49,8 +55,4 @@ class AppWindow extends React.Component {
} }
} }
AppWindow.propTypes = {
errorCode: React.PropTypes.string
};
export default AppWindow; export default AppWindow;

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View 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
};
};

View File

@ -20,11 +20,17 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import {ACTION_SETTINGS_SET_LAYOUT} from "../lib/defs"; import {SETTINGS_ACTIONS, SETTINGS_KEYS} from "../lib/defs";
export const setLayout = (newLayout) => { export const setLayout = (newLayout) => {
try {
window.localStorage.setItem(SETTINGS_KEYS.LAYOUT, newLayout);
} catch (error) {
// pass
}
return { return {
type: ACTION_SETTINGS_SET_LAYOUT, type: SETTINGS_ACTIONS.SET_LAYOUT,
layout: newLayout layout: newLayout
}; };
}; };

View File

@ -27,7 +27,7 @@ import ReactDOM from "react-dom";
import {connect} from "react-redux"; import {connect} from "react-redux";
import underscore from "underscore"; import underscore from "underscore";
class ChartComponent extends React.Component { class ChartComponent extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);

View File

@ -29,7 +29,7 @@ const LAYOUT_TO_CLASSNAME = {};
LAYOUT_TO_CLASSNAME[LAYOUT_WIDE] = "container-fluid"; LAYOUT_TO_CLASSNAME[LAYOUT_WIDE] = "container-fluid";
LAYOUT_TO_CLASSNAME[LAYOUT_NARROW] = "container"; LAYOUT_TO_CLASSNAME[LAYOUT_NARROW] = "container";
class ContainerComponent extends React.Component { class ContainerComponent extends React.PureComponent {
render () { render () {
let className = LAYOUT_TO_CLASSNAME[this.props.layout]; let className = LAYOUT_TO_CLASSNAME[this.props.layout];
if (!className) { if (!className) {

View File

@ -24,7 +24,7 @@ import React from "react";
import {ITEM_NAMES} from "../lib/defs"; import {ITEM_NAMES} from "../lib/defs";
class ItemIconComponent extends React.Component { class ItemIconComponent extends React.PureComponent {
render () { render () {
let itemName = ITEM_NAMES[this.props.item] || "?"; let itemName = ITEM_NAMES[this.props.item] || "?";

View File

@ -23,7 +23,7 @@
import ClassName from "classnames"; import ClassName from "classnames";
import React from "react"; import React from "react";
class LoaderComponent extends React.Component { class LoaderComponent extends React.PureComponent {
render () { render () {
let className = ClassName("q3stats-loader", { let className = ClassName("q3stats-loader", {
"visible": this.props.visible "visible": this.props.visible

View File

@ -24,7 +24,7 @@ import React from "react";
import {POWERUP_NAMES} from "../lib/defs"; import {POWERUP_NAMES} from "../lib/defs";
class PowerupIconComponent extends React.Component { class PowerupIconComponent extends React.PureComponent {
render () { render () {
let powerupName = POWERUP_NAMES[this.props.powerup] || "?"; let powerupName = POWERUP_NAMES[this.props.powerup] || "?";

View File

@ -24,7 +24,7 @@ import React from "react";
import {WEAPON_NAMES} from "../lib/defs"; import {WEAPON_NAMES} from "../lib/defs";
class WeaponIconComponent extends React.Component { class WeaponIconComponent extends React.PureComponent {
render () { render () {
let weaponName = WEAPON_NAMES[this.props.weapon] || "?"; let weaponName = WEAPON_NAMES[this.props.weapon] || "?";

View 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;

View File

@ -30,9 +30,61 @@ export const LAYOUT_CHOICES = [
[LAYOUT_NARROW, "Narrow"] [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 = { export const WEAPON_NAMES = {
"BFG": "BFG10k", "BFG": "BFG10k",

View File

@ -7,7 +7,6 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import {Router, Route, hashHistory} from "react-router"; import {Router, Route, hashHistory} from "react-router";
import {Provider} from "react-redux"; import {Provider} from "react-redux";
import "whatwg-fetch";
import AppWindow from "./AppWindow"; import AppWindow from "./AppWindow";
import SessionsView from "./views/SessionsView"; import SessionsView from "./views/SessionsView";
@ -15,11 +14,11 @@ import PlayersView from "./views/PlayersView";
import PlayerGameView from "./views/PlayerGameView"; import PlayerGameView from "./views/PlayerGameView";
import PlayerStatsView from "./views/PlayerStatsView"; import PlayerStatsView from "./views/PlayerStatsView";
import ErrorView from "./views/ErrorView"; import ErrorView from "./views/ErrorView";
import reactStore from "./store"; import reduxStore from "./store";
window.addEventListener("load", function () { window.addEventListener("load", function () {
ReactDOM.render( ReactDOM.render(
<Provider store={reactStore}> <Provider store={reduxStore}>
<Router history={hashHistory}> <Router history={hashHistory}>
<Route path="/" component={AppWindow}> <Route path="/" component={AppWindow}>
<Route path="/sessions/" component={SessionsView}> <Route path="/sessions/" component={SessionsView}>

View 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;

View File

@ -22,9 +22,21 @@
import {combineReducers} from "redux"; 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"; import settingsReducer from "./settings";
const Q3StatsApp = combineReducers({ const Q3StatsApp = combineReducers({
dashboard: dashboardReducer,
navigationBar: navigationBarReducer,
playerGame: playerGameReducer,
playerStats: playerStatsReducer,
players: playersReducer,
sessions: sessionsReducer,
settings: settingsReducer settings: settingsReducer
}); });

View 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;

View 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;

View 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;

View File

@ -20,37 +20,34 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import React from "react";
import underscore from "underscore"; import underscore from "underscore";
const DEFAULT_INIT = { import {PLAYERS_ACTIONS} from "../lib/defs";
credentials: "include"
const DEFAULT_STATE = {
loading: true,
players: []
}; };
class BaseView extends React.Component { const playersReducer = (state = DEFAULT_STATE, action) => {
fetch (requestOrURL, init) { switch (action.type) {
init = init || {}; case PLAYERS_ACTIONS.CLEANUP:
return underscore.clone(DEFAULT_STATE);
let actualInit = underscore.extendOwn( case PLAYERS_ACTIONS.LOAD_DATA:
underscore.clone(DEFAULT_INIT), init return underscore.extendOwn({}, state, {
); loading: true
});
return window.fetch(requestOrURL, actualInit).catch((error) => { case PLAYERS_ACTIONS.PARSE_DATA:
this.props.router.push("/error/"); return underscore.extendOwn({}, state, {
throw new Error("FetchError: Unable to fetch"); loading: false,
}).then((response) => { players: action.data.players
if (!response.ok) { });
let statusCode = response.status || "";
this.props.router.push("/error/" + statusCode); default:
throw new Error( return state;
"FetchError: '" + statusCode + "'" + " '" + response.statusText + "'"
);
} else {
return response.json();
}
});
} }
} };
export default BaseView; export default playersReducer;

View 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;

View File

@ -22,15 +22,13 @@
import underscore from "underscore"; import underscore from "underscore";
import { import {DEFAULT_LAYOUT, SETTINGS_ACTIONS, SETTINGS_KEYS} from "../lib/defs";
DEFAULT_LAYOUT, SETTINGS_KEY_LAYOUT, ACTION_SETTINGS_SET_LAYOUT
} from "../lib/defs";
const DEFAULT_STATE = {}; const DEFAULT_STATE = {};
DEFAULT_STATE[SETTINGS_KEY_LAYOUT] = (function () { DEFAULT_STATE[SETTINGS_KEYS.LAYOUT] = (function () {
let result = null; let result = null;
try { try {
result = window.localStorage.getItem(SETTINGS_KEY_LAYOUT); result = window.localStorage.getItem(SETTINGS_KEYS.LAYOUT);
} catch (error) { } catch (error) {
// pass // pass
} }
@ -38,9 +36,9 @@ DEFAULT_STATE[SETTINGS_KEY_LAYOUT] = (function () {
return result || DEFAULT_LAYOUT; return result || DEFAULT_LAYOUT;
})(); })();
const settings = (state = DEFAULT_STATE, action) => { const settingsReducer = (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case ACTION_SETTINGS_SET_LAYOUT: case SETTINGS_ACTIONS.SET_LAYOUT:
return underscore.extendOwn({}, state, { return underscore.extendOwn({}, state, {
layout: action.layout layout: action.layout
}); });
@ -50,4 +48,4 @@ const settings = (state = DEFAULT_STATE, action) => {
} }
}; };
export default settings; export default settingsReducer;

View File

@ -24,7 +24,7 @@ import Button from "react-bootstrap/lib/Button";
import Modal from "react-bootstrap/lib/Modal"; import Modal from "react-bootstrap/lib/Modal";
import React from "react"; import React from "react";
class AboutModalView extends React.Component { class AboutModalView extends React.PureComponent {
render () { render () {
return ( return (
<Modal show={this.props.show} onHide={this.props.onHide}> <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, show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired onHide: React.PropTypes.func.isRequired
}; };

View File

@ -21,13 +21,14 @@
*/ */
import React from "react"; import React from "react";
import {connect} from "react-redux";
import BaseView from "../lib/BaseView";
import ChartComponent from "../components/ChartComponent"; import ChartComponent from "../components/ChartComponent";
import ContainerComponent from "../components/ContainerComponent"; import ContainerComponent from "../components/ContainerComponent";
import LoaderComponent from "../components/LoaderComponent"; import LoaderComponent from "../components/LoaderComponent";
import {cleanup, loadData} from "../actions/dashboard";
class TopPlayersTableView extends React.Component { class TopPlayersTableView extends React.PureComponent {
render () { render () {
return ( return (
<table className="table table-striped table-bordered table-hover"> <table className="table table-striped table-bordered table-hover">
@ -64,7 +65,7 @@ TopPlayersTableView.propTypes = {
scores: React.PropTypes.array scores: React.PropTypes.array
}; };
class DashboardView extends BaseView { class DashboardView extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
@ -91,35 +92,14 @@ class DashboardView extends BaseView {
} }
} }
}; };
this.state = {
ready: false,
day: "",
emosOfTheMonth: [],
fraggersOfTheMonth: [],
categories: null,
series: []
};
} }
componentDidMount () { componentDidMount () {
this.fetch("/api/v1/dashboard").then((data) => { if (!this.props.day) {
this.setState({ this.props.dispatch(loadData(this.props.dispatch));
ready: true, }
day: data.day,
emosOfTheMonth: data.eotm,
fraggersOfTheMonth: data.fotm
});
this.loadChartData();
});
} }
loadChartData () { componentWillUnmount () {
this.fetch("/api/v1/charts/day/" + this.state.day).then((data) => { this.props.dispatch(cleanup());
this.setState({
categories: data.maps,
series: data.scores
});
});
} }
render () { render () {
return ( return (
@ -128,9 +108,9 @@ class DashboardView extends BaseView {
<ChartComponent <ChartComponent
config={this.chartConfig} config={this.chartConfig}
categories={this.state.categories} categories={this.props.categories}
series={this.state.series} series={this.props.series}
subtitle={this.state.day} /> subtitle={this.props.day} />
<div className="row"> <div className="row">
<div className="col-sm-6"> <div className="col-sm-6">
@ -138,7 +118,7 @@ class DashboardView extends BaseView {
<TopPlayersTableView <TopPlayersTableView
scoreColumnTitle="Frags" scoreColumnTitle="Frags"
scores={this.state.fraggersOfTheMonth} /> scores={this.props.fraggersOfTheMonth} />
</div> </div>
<div className="col-sm-6"> <div className="col-sm-6">
@ -146,14 +126,29 @@ class DashboardView extends BaseView {
<TopPlayersTableView <TopPlayersTableView
scoreColumnTitle="Suicides" scoreColumnTitle="Suicides"
scores={this.state.emosOfTheMonth} /> scores={this.props.emosOfTheMonth} />
</div> </div>
</div> </div>
<LoaderComponent visible={!this.state.ready} /> <LoaderComponent visible={this.props.loading} />
</ContainerComponent> </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;

View File

@ -22,7 +22,7 @@
import React from "react"; import React from "react";
class ErrorView extends React.Component { class ErrorView extends React.PureComponent {
render () { render () {
return ( return (
<div className="q3stats-error-view"> <div className="q3stats-error-view">
@ -44,4 +44,8 @@ class ErrorView extends React.Component {
} }
} }
ErrorView.propTypes = {
params: React.PropTypes.object
};
export default ErrorView; export default ErrorView;

View File

@ -25,15 +25,19 @@ import ContainerComponent from "../components/ContainerComponent";
import Glyphicon from "react-bootstrap/lib/Glyphicon"; import Glyphicon from "react-bootstrap/lib/Glyphicon";
import React from "react"; import React from "react";
import Navbar from "react-bootstrap/lib/Navbar"; import Navbar from "react-bootstrap/lib/Navbar";
import {connect} from "react-redux";
import {Router, Link} from "react-router"; 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 {LAYOUT_WIDE} from "../lib/defs";
import AboutModalView from "./AboutModalView"; import AboutModalView from "./AboutModalView";
import SettingsModalView from "./SettingsModalView"; import SettingsModalView from "./SettingsModalView";
class NavigationBarItemView extends React.Component { class NavigationBarItemView extends React.PureComponent {
render () { render () {
let className = ClassNames({ let className = ClassNames({
active: this.props.active active: this.props.active
@ -56,66 +60,46 @@ NavigationBarItemView.propTypes = {
onClick: React.PropTypes.func.isRequired onClick: React.PropTypes.func.isRequired
}; };
class NavigationBarView extends React.Component { class NavigationBarView extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
this.onNavigationBarItemClick = this.onNavigationBarItemClick.bind(this); this.onNavigationBarItemClick = this.onNavigationBarItemClick.bind(this);
this.onNavigationBarToggleExpanded = this.onNavigationBarToggleExpanded.bind(this); this.onNavigationBarToggleExpanded = this.onNavigationBarToggleExpanded.bind(this);
this.onHideSettingsModal = this.onHideSettingsModal.bind(this); this.onHideSettingsModal = this.onHideSettingsModal.bind(this);
this.onShowSettingsModalButtonClick = this.onShowSettingsModalButtonClick.bind(this); this.onShowSettingsModalButtonClick = this.onShowSettingsModalButtonClick.bind(this);
this.onStoreChange = this.onStoreChange.bind(this);
this.onHideAboutModal = this.onHideAboutModal.bind(this); this.onHideAboutModal = this.onHideAboutModal.bind(this);
this.onShowAboutModalButtonClick = this.onShowAboutModalButtonClick.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) { onNavigationBarItemClick (e) {
this.setState({navBarExpanded: false}); this.props.dispatch(collapse());
} }
onNavigationBarToggleExpanded (expanded) { onNavigationBarToggleExpanded (expanded) {
this.setState({navBarExpanded: expanded}); if (expanded) {
} this.props.dispatch(expand());
onHideSettingsModal () { } else {
this.setState({showSettingsModal: false}); this.props.dispatch(collapse());
}
} }
onShowSettingsModalButtonClick () { onShowSettingsModalButtonClick () {
this.setState({ this.props.dispatch(showSettingsModal());
navBarExpanded: false,
showSettingsModal: true
});
} }
onStoreChange () { onHideSettingsModal () {
this.setState({layout: reduxStore.getState().settings.layout}); this.props.dispatch(hideSettingsModal());
}
onHideAboutModal () {
this.setState({showAboutModal: false});
} }
onShowAboutModalButtonClick () { onShowAboutModalButtonClick () {
this.setState({ this.props.dispatch(showAboutModal());
navBarExpanded: false, }
showAboutModal: true onHideAboutModal () {
}); this.props.dispatch(hideAboutModal());
} }
render () { render () {
return ( return (
<div className="q3stats-navigation-bar"> <div className="q3stats-navigation-bar">
<Navbar <Navbar
ref="navbar" ref="navbar"
collapseOnSelect expanded={this.props.expanded}
expanded={this.state.navBarExpanded}
fixedTop fixedTop
fluid={this.state.layout == LAYOUT_WIDE} fluid={this.props.layout == LAYOUT_WIDE}
onToggle={this.onNavigationBarToggleExpanded}> onToggle={this.onNavigationBarToggleExpanded}>
<Navbar.Header> <Navbar.Header>
<Navbar.Brand> <Navbar.Brand>
@ -160,20 +144,38 @@ class NavigationBarView extends React.Component {
</Navbar.Collapse> </Navbar.Collapse>
</Navbar> </Navbar>
<SettingsModalView
show={this.state.showSettingsModal}
onHide={this.onHideSettingsModal} />
<AboutModalView <AboutModalView
show={this.state.showAboutModal} show={this.props.showAboutModal}
onHide={this.onHideAboutModal} /> onHide={this.onHideAboutModal} />
<SettingsModalView
show={this.props.showSettingsModal}
onHide={this.onHideSettingsModal} />
</div> </div>
); );
} }
} }
NavigationBarView.PropTypes = { NavigationBarView.propTypes = {
router: React.PropTypes.instanceOf(Router).isRequired 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;

View File

@ -21,17 +21,18 @@
*/ */
import React from "react"; import React from "react";
import {connect} from "react-redux";
import underscore from "underscore"; import underscore from "underscore";
import BaseView from "../lib/BaseView";
import ChartComponent from "../components/ChartComponent"; import ChartComponent from "../components/ChartComponent";
import ContainerComponent from "../components/ContainerComponent"; import ContainerComponent from "../components/ContainerComponent";
import ItemIconComponent from "../components/ItemIconComponent"; import ItemIconComponent from "../components/ItemIconComponent";
import LoaderComponent from "../components/LoaderComponent"; import LoaderComponent from "../components/LoaderComponent";
import PowerupIconComponent from "../components/PowerupIconComponent"; import PowerupIconComponent from "../components/PowerupIconComponent";
import WeaponIconComponent from "../components/WeaponIconComponent"; import WeaponIconComponent from "../components/WeaponIconComponent";
import {cleanup, loadData} from "../actions/playerGame";
class PlayerGameView extends BaseView { class PlayerGameView extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
@ -40,87 +41,39 @@ class PlayerGameView extends BaseView {
"enabled": false "enabled": false
} }
}; };
this.state = {
ready: false,
map: "",
weaponStats: {},
itemStats: {},
powerupStats: {},
scoreChartSeries: [],
damageChartSeries: [],
totalsChartSeries: []
};
} }
componentDidMount () { componentDidMount () {
let url = ( this.props.dispatch(loadData(
"/api/v1/players/" + this.props.params.player + "/game/" + this.props.dispatch, this.props.params.player, this.props.params.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) { componentWillUnmount () {
if (!serie) { this.props.dispatch(cleanup());
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 () { render () {
return ( return (
<ContainerComponent> <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="row">
<div className="col-sm-4"> <div className="col-sm-4">
<ChartComponent <ChartComponent
config={this.chartConfig} config={this.chartConfig}
series={this.state.scoreChartSeries} series={this.props.scoreChartSeries}
title="Score breakdown" /> title="Score breakdown" />
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<ChartComponent <ChartComponent
config={this.chartConfig} config={this.chartConfig}
series={this.state.damageChartSeries} series={this.props.damageChartSeries}
title="Damage breakdown" /> title="Damage breakdown" />
</div> </div>
<div className="col-sm-4"> <div className="col-sm-4">
<ChartComponent <ChartComponent
config={this.chartConfig} config={this.chartConfig}
series={this.state.totalsChartSeries} series={this.props.totalsChartSeries}
title="Health & armor" /> title="Health & armor" />
</div> </div>
</div> </div>
@ -139,15 +92,15 @@ class PlayerGameView extends BaseView {
</thead> </thead>
<tbody> <tbody>
{underscore.keys(this.state.weaponStats).length == 0 && {underscore.keys(this.props.weaponStats).length == 0 &&
<tr> <tr>
<td colSpan="5"><strong>No stats :(</strong></td> <td colSpan="5"><strong>No stats :(</strong></td>
</tr> </tr>
} }
{underscore.map( {underscore.map(
underscore.keys(this.state.weaponStats), (key, index) => { underscore.keys(this.props.weaponStats), (key, index) => {
let stats = this.state.weaponStats[key]; let stats = this.props.weaponStats[key];
return ( return (
<tr key={index}> <tr key={index}>
@ -176,15 +129,15 @@ class PlayerGameView extends BaseView {
</thead> </thead>
<tbody> <tbody>
{underscore.keys(this.state.itemStats).length == 0 && {underscore.keys(this.props.itemStats).length == 0 &&
<tr> <tr>
<td colSpan="2"><strong>No stats :(</strong></td> <td colSpan="2"><strong>No stats :(</strong></td>
</tr> </tr>
} }
{underscore.map( {underscore.map(
underscore.keys(this.state.itemStats), (key, index) => { underscore.keys(this.props.itemStats), (key, index) => {
let value = this.state.itemStats[key]; let value = this.props.itemStats[key];
return ( return (
<tr key={index}> <tr key={index}>
@ -211,15 +164,15 @@ class PlayerGameView extends BaseView {
</thead> </thead>
<tbody> <tbody>
{underscore.keys(this.state.powerupStats).length == 0 && {underscore.keys(this.props.powerupStats).length == 0 &&
<tr> <tr>
<td colSpan="3"><strong>No stats :(</strong></td> <td colSpan="3"><strong>No stats :(</strong></td>
</tr> </tr>
} }
{underscore.map( {underscore.map(
underscore.keys(this.state.powerupStats), (key, index) => { underscore.keys(this.props.powerupStats), (key, index) => {
let stats = this.state.powerupStats[key]; let stats = this.props.powerupStats[key];
return ( return (
<tr key={index}> <tr key={index}>
@ -235,10 +188,38 @@ class PlayerGameView extends BaseView {
</div> </div>
</div> </div>
<LoaderComponent visible={!this.state.ready} /> <LoaderComponent visible={this.props.loading} />
</ContainerComponent> </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;

View File

@ -22,16 +22,18 @@
import ClassName from "classnames"; import ClassName from "classnames";
import React from "react"; import React from "react";
import {connect} from "react-redux";
import underscore from "underscore";
import BaseView from "../lib/BaseView";
import ChartComponent from "../components/ChartComponent"; 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_SESSION = "Wins and losses by session";
const WINS_CHART_TITLE_MAP = "Wins and losses by map"; const WINS_CHART_TITLE_MAP = "Wins and losses by map";
const ACCURACY_CHART_TITLE_SESSION = "Average weapon accuracy by session"; const ACCURACY_CHART_TITLE_SESSION = "Average weapon accuracy by session";
const ACCURACY_CHART_TITLE_MAP = "Average weapon accuracy by map"; const ACCURACY_CHART_TITLE_MAP = "Average weapon accuracy by map";
class ModeLinkComponent extends React.Component { class ModeLinkComponent extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
this.onClick = this.onClick.bind(this); this.onClick = this.onClick.bind(this);
@ -62,80 +64,55 @@ ModeLinkComponent.propTypes = {
onSwitchMode: React.PropTypes.func.isRequired onSwitchMode: React.PropTypes.func.isRequired
}; };
class BaseStatsChartComponent extends BaseView { class BaseStatsChartComponent extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
this.chartConfig = {}; this.chartConfig = {};
this.state = {
sessionCategories: [],
sessionSeries: [],
mapCategories: [],
mapSeries: []
};
} }
componentDidMount () { componentDidMount () {
this.load(); this.load();
} }
componentWillReceiveProps (nextProps) {
if (nextProps.player != this.props.player) {
this.setState({
sessionCategories: [],
sessionSeries: [],
mapCategories: [],
mapSeries: []
});
}
}
componentDidUpdate (prevProps, prevState) { 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(); this.load();
} }
} }
url () { kind () {
throw new Error("Not Implemented"); throw new Error("Not Implemented");
} }
parse () { load (force) {
throw new Error("Not Implemented"); if (underscore.isUndefined(force)) {
} force = false;
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) { let hasData = true;
this.fetch(this.url()).then((data) => { if (this.props.mode == "session") {
let newState = {}; hasData = hasData && this.props.sessionData.categories.length > 0;
let parsedData = this.parse(data); 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") { if (force || !hasData) {
newState.sessionCategories = parsedData.categories; this.props.dispatch(loadChartData(
newState.sessionSeries = parsedData.series; this.props.dispatch, this.props.player, this.kind(), this.props.mode
} else { ));
newState.mapCategories = parsedData.categories;
newState.mapSeries = parsedData.series;
}
this.setState(newState);
});
} }
} }
title () { title () {
throw new Error("Not Implemented"); throw new Error("Not Implemented");
} }
render () { render () {
let categories = this.state.sessionCategories; let categories = this.props.sessionData.categories;
let series = this.state.sessionSeries; let series = this.props.sessionData.series;
if (this.props.mode == "map") { if (this.props.mode == "map") {
categories = this.state.mapCategories; categories = this.props.mapData.categories;
series = this.state.mapSeries; series = this.props.mapData.series;
} }
return ( return (
@ -188,27 +165,8 @@ class WinsChartComponent extends BaseStatsChartComponent {
} }
}; };
} }
url () { kind () {
return ( return "wins";
"/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 () { title () {
if (this.props.mode == "map") { 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 { class AccuracyChartComponent extends BaseStatsChartComponent {
constructor (props) { constructor (props) {
super(props); super(props);
@ -241,25 +206,8 @@ class AccuracyChartComponent extends BaseStatsChartComponent {
} }
}; };
} }
url () { kind () {
return ( return "accuracy";
"/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 () { title () {
if (this.props.mode == "map") { if (this.props.mode == "map") {
@ -273,25 +221,28 @@ class AccuracyChartComponent extends BaseStatsChartComponent {
} }
} }
WinsChartComponent.propTypes = { AccuracyChartComponent.propTypes = {
player: React.PropTypes.string.isRequired, 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) { constructor (props) {
super(props); super(props);
this.onSwitchMode = this.onSwitchMode.bind(this); this.onSwitchMode = this.onSwitchMode.bind(this);
this.state = {
mode: "session"
};
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
this.setState({mode: "session"}); if (nextProps.params.player != this.props.params.player) {
this.props.dispatch(cleanup());
}
} }
onSwitchMode (mode) { onSwitchMode (mode) {
this.setState({mode: mode}); this.props.dispatch(setMode(mode));
}
componentWillUnmount () {
this.props.dispatch(cleanup());
} }
render () { render () {
return ( return (
@ -300,13 +251,13 @@ class PlayerStatsView extends React.Component {
<ul className="nav nav-tabs"> <ul className="nav nav-tabs">
<ModeLinkComponent <ModeLinkComponent
mode={this.state.mode} mode={this.props.mode}
name="By session" name="By session"
code="session" code="session"
onSwitchMode={this.onSwitchMode} /> onSwitchMode={this.onSwitchMode} />
<ModeLinkComponent <ModeLinkComponent
mode={this.state.mode} mode={this.props.mode}
name="By map" name="By map"
code="map" code="map"
onSwitchMode={this.onSwitchMode} /> onSwitchMode={this.onSwitchMode} />
@ -314,16 +265,42 @@ class PlayerStatsView extends React.Component {
<WinsChartComponent <WinsChartComponent
player={this.props.params.player} player={this.props.params.player}
mode={this.state.mode} mode={this.props.mode}
router={this.props.router} /> sessionData={this.props.sessionWinsChartData}
mapData={this.props.mapWinsChartData}
dispatch={this.props.dispatch} />
<AccuracyChartComponent <AccuracyChartComponent
player={this.props.params.player} player={this.props.params.player}
mode={this.state.mode} mode={this.props.mode}
router={this.props.router} /> sessionData={this.props.sessionAccuracyChartData}
mapData={this.props.mapAccuracyChartData}
dispatch={this.props.dispatch} />
</div> </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;

View File

@ -23,13 +23,14 @@
import ClassName from "classnames"; import ClassName from "classnames";
import React from "react"; import React from "react";
import {Link} from "react-router"; import {Link} from "react-router";
import {connect} from "react-redux";
import BaseView from "../lib/BaseView";
import ContainerComponent from "../components/ContainerComponent"; import ContainerComponent from "../components/ContainerComponent";
import LoaderComponent from "../components/LoaderComponent"; import LoaderComponent from "../components/LoaderComponent";
import PlayerStatsView from "./PlayerStatsView"; import PlayerStatsView from "./PlayerStatsView";
import {cleanup, loadData} from "../actions/players";
class PlayerLinkComponent extends React.Component { class PlayerLinkComponent extends React.PureComponent {
render () { render () {
let className = ClassName({ let className = ClassName({
active: this.props.active active: this.props.active
@ -48,22 +49,12 @@ PlayerLinkComponent.propTypes = {
player: React.PropTypes.string.isRequired player: React.PropTypes.string.isRequired
}; };
class PlayersView extends BaseView { class PlayersView extends React.PureComponent {
constructor (props) {
super(props);
this.state = {
ready: false,
players: []
};
}
componentDidMount () { componentDidMount () {
this.fetch("/api/v1/players").then((data) => { this.props.dispatch(loadData(this.props.dispatch));
this.setState({ }
ready: true, componentWillUnmount () {
players: data.players this.props.dispatch(cleanup());
});
});
} }
render () { render () {
return ( return (
@ -72,7 +63,7 @@ class PlayersView extends BaseView {
<div className="col-sm-3"> <div className="col-sm-3">
<h2>Players</h2> <h2>Players</h2>
<ul className="nav nav-pills nav-stacked"> <ul className="nav nav-pills nav-stacked">
{this.state.players.map((item, index) => { {this.props.players.map((item, index) => {
return ( return (
<PlayerLinkComponent <PlayerLinkComponent
key={index} key={index}
@ -93,10 +84,27 @@ class PlayersView extends BaseView {
</div> </div>
</div> </div>
<LoaderComponent visible={!this.state.ready} /> <LoaderComponent visible={this.props.loading} />
</ContainerComponent> </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;

View File

@ -22,15 +22,16 @@
import ClassName from "classnames"; import ClassName from "classnames";
import React from "react"; import React from "react";
import {connect} from "react-redux";
import {Link} from "react-router"; import {Link} from "react-router";
import BaseView from "../lib/BaseView";
import ChartComponent from "../components/ChartComponent"; import ChartComponent from "../components/ChartComponent";
import ContainerComponent from "../components/ContainerComponent"; import ContainerComponent from "../components/ContainerComponent";
import LoaderComponent from "../components/LoaderComponent"; import LoaderComponent from "../components/LoaderComponent";
import WeaponIconComponent from "../components/WeaponIconComponent"; import WeaponIconComponent from "../components/WeaponIconComponent";
import {cleanup, setDay} from "../actions/sessions";
class NavComponent extends React.Component { class NavComponent extends React.PureComponent {
render () { render () {
let previousClassName = ClassName("previous", { let previousClassName = ClassName("previous", {
disabled: !this.props.previousDay disabled: !this.props.previousDay
@ -64,7 +65,7 @@ NavComponent.propTypes = {
nextDay: React.PropTypes.string nextDay: React.PropTypes.string
}; };
class GamesTableView extends React.Component { class GamesTableView extends React.PureComponent {
render () { render () {
return ( return (
<table className="table table-striped table-bordered table-hover"> <table className="table table-striped table-bordered table-hover">
@ -136,7 +137,7 @@ GamesTableView.propTypes = {
games: React.PropTypes.array.isRequired games: React.PropTypes.array.isRequired
}; };
class SessionsView extends BaseView { class SessionsView extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
@ -163,96 +164,80 @@ class SessionsView extends BaseView {
} }
} }
}; };
}
this.state = { setDay (day) {
shouldReload: false, this.props.dispatch(setDay(this.props.dispatch, day));
ready: false,
day: "",
previousDay: "",
nextDay: "",
games: [],
categories: null,
series: []
};
} }
componentDidMount () { componentDidMount () {
this.loadData(); this.setDay(this.props.params.day);
}
componentWillUnmount () {
this.props.dispatch(cleanup());
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if ((this.state.shouldReload) && (!this.state.ready)) { if (prevProps.params.day != this.props.params.day) {
this.setState({shouldReload: false}); this.setDay(this.props.params.day);
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 () { render () {
return ( return (
<ContainerComponent> <ContainerComponent>
{this.state.ready && {!this.props.loading &&
<div> <div>
<NavComponent <NavComponent
previousDay={this.state.previousDay} previousDay={this.props.previousDay}
nextDay={this.state.nextDay} /> nextDay={this.props.nextDay} />
<h2>Results</h2> <h2>Results</h2>
<ChartComponent <ChartComponent
config={this.chartConfig} config={this.chartConfig}
categories={this.state.categories} categories={this.props.categories}
series={this.state.series} series={this.props.series}
subtitle={this.state.day} /> subtitle={this.props.day} />
<h2>Stats</h2> <h2>Stats</h2>
<GamesTableView <GamesTableView
games={this.state.games} /> games={this.props.games} />
<NavComponent <NavComponent
onNavigateToDay={this.onNavigateToDay} onNavigateToDay={this.onNavigateToDay}
previousDay={this.state.previousDay} previousDay={this.props.previousDay}
nextDay={this.state.nextDay} /> nextDay={this.props.nextDay} />
</div> </div>
} }
<LoaderComponent visible={!this.state.ready} /> <LoaderComponent visible={this.props.loading} />
</ContainerComponent> </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;

View File

@ -29,10 +29,12 @@ import Modal from "react-bootstrap/lib/Modal";
import React from "react"; import React from "react";
import {connect} from "react-redux"; 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"; import {setLayout} from "../actions/settings";
class SettingsModalView extends React.Component { class SettingsModalView extends React.PureComponent {
constructor (props) { constructor (props) {
super(props); super(props);
this.onSaveButtonClick = this.onSaveButtonClick.bind(this); this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
@ -42,16 +44,15 @@ class SettingsModalView extends React.Component {
layout: this.props.settings.layout layout: this.props.settings.layout
}; };
} }
componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({layout: this.props.settings.layout});
}
}
onSaveButtonClick (e) { onSaveButtonClick (e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
try {
window.localStorage.setItem(SETTINGS_KEY_LAYOUT, this.state.layout);
} catch (error) {
// pass
}
this.props.dispatch(setLayout(this.state.layout)); this.props.dispatch(setLayout(this.state.layout));
this.props.onHide(); this.props.onHide();
@ -95,7 +96,6 @@ class SettingsModalView extends React.Component {
} }
SettingsModalView.PropTypes = { SettingsModalView.PropTypes = {
dispatch: React.PropTypes.func.isRequired,
settings: React.PropTypes.object.isRequired, settings: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired, show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired onHide: React.PropTypes.func.isRequired

View File

@ -92,6 +92,8 @@ def get_api_v1_player_game(player, game_uuid):
abort(404) abort(404)
result = { result = {
"game": game_uuid,
"player": player,
"map": game.map, "map": game.map,
"items": score.items, "items": score.items,
"weapons": _process_weapons(score.weapons), "weapons": _process_weapons(score.weapons),

View File

@ -86,6 +86,8 @@ class Test_GetAPIv1Players(BaseQ3StatsWebAppTestCase):
rsp = self.client.get('/api/v1/players/Player 1/game/game1') rsp = self.client.get('/api/v1/players/Player 1/game/game1')
assert rsp.status_code == 200 assert rsp.status_code == 200
assert rsp.json['game'] == 'game1'
assert rsp.json['player'] == 'Player 1'
assert rsp.json['map'] == 'Q3DM7' assert rsp.json['map'] == 'Q3DM7'
assert rsp.json['items'] == {'YA': 1} assert rsp.json['items'] == {'YA': 1}

View File