From 0451cdbbed83fe64e3ef2433f16bc85e44621783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20Wo=CC=81jcik?= Date: Mon, 13 Mar 2017 12:25:44 +0100 Subject: [PATCH] Rewritten most of the frontend to use Redux. Also, lots of code cleanup, especially in frontend. --- .gitignore | 2 +- frontend/src/AppWindow.js | 14 +- frontend/src/actions/dashboard.js | 70 +++++++ frontend/src/actions/navigationBar.js | 67 ++++++ frontend/src/actions/playerGame.js | 67 ++++++ frontend/src/actions/playerStats.js | 57 ++++++ frontend/src/actions/players.js | 48 +++++ frontend/src/actions/sessions.js | 81 ++++++++ frontend/src/actions/settings.js | 10 +- frontend/src/components/ChartComponent.js | 2 +- frontend/src/components/ContainerComponent.js | 2 +- frontend/src/components/ItemIconComponent.js | 2 +- frontend/src/components/LoaderComponent.js | 2 +- .../src/components/PowerupIconComponent.js | 2 +- .../src/components/WeaponIconComponent.js | 2 +- frontend/src/lib/DataSource.js | 64 ++++++ frontend/src/lib/defs.js | 56 ++++- frontend/src/main.js | 5 +- frontend/src/reducers/dashboard.js | 71 +++++++ frontend/src/reducers/index.js | 12 ++ frontend/src/reducers/navigationBar.js | 75 +++++++ frontend/src/reducers/playerGame.js | 90 +++++++++ frontend/src/reducers/playerStats.js | 101 +++++++++ .../{lib/BaseView.js => reducers/players.js} | 47 ++--- frontend/src/reducers/sessions.js | 79 ++++++++ frontend/src/reducers/settings.js | 14 +- frontend/src/views/AboutModalView.js | 4 +- frontend/src/views/DashboardView.js | 67 +++--- frontend/src/views/ErrorView.js | 6 +- frontend/src/views/NavigationBarView.js | 94 ++++----- frontend/src/views/PlayerGameView.js | 121 +++++------ frontend/src/views/PlayerStatsView.js | 191 ++++++++---------- frontend/src/views/PlayersView.js | 48 +++-- frontend/src/views/SessionsView.js | 117 +++++------ frontend/src/views/SettingsModalView.js | 18 +- .../blueprints/api_v1/views/players.py | 2 + tests_web_app/test_get_api_v1_player_game.py | 2 + tests_web_app/tmp/.placeholder | 0 38 files changed, 1302 insertions(+), 410 deletions(-) create mode 100644 frontend/src/actions/dashboard.js create mode 100644 frontend/src/actions/navigationBar.js create mode 100644 frontend/src/actions/playerGame.js create mode 100644 frontend/src/actions/playerStats.js create mode 100644 frontend/src/actions/players.js create mode 100644 frontend/src/actions/sessions.js create mode 100644 frontend/src/lib/DataSource.js create mode 100644 frontend/src/reducers/dashboard.js create mode 100644 frontend/src/reducers/navigationBar.js create mode 100644 frontend/src/reducers/playerGame.js create mode 100644 frontend/src/reducers/playerStats.js rename frontend/src/{lib/BaseView.js => reducers/players.js} (60%) create mode 100644 frontend/src/reducers/sessions.js create mode 100644 tests_web_app/tmp/.placeholder diff --git a/.gitignore b/.gitignore index 246dbb9..3e7790d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ build/ dist/ q3stats.egg-info/ -tests_web_app/tmp/ +tests_web_app/tmp/tmp* diff --git a/frontend/src/AppWindow.js b/frontend/src/AppWindow.js index 3086c7b..cffa240 100644 --- a/frontend/src/AppWindow.js +++ b/frontend/src/AppWindow.js @@ -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 (
- +
- {this.props.children || } + {this.props.children || }
@@ -49,8 +55,4 @@ class AppWindow extends React.Component { } } -AppWindow.propTypes = { - errorCode: React.PropTypes.string -}; - export default AppWindow; diff --git a/frontend/src/actions/dashboard.js b/frontend/src/actions/dashboard.js new file mode 100644 index 0000000..c95d244 --- /dev/null +++ b/frontend/src/actions/dashboard.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/navigationBar.js b/frontend/src/actions/navigationBar.js new file mode 100644 index 0000000..2b4466d --- /dev/null +++ b/frontend/src/actions/navigationBar.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/playerGame.js b/frontend/src/actions/playerGame.js new file mode 100644 index 0000000..678f74c --- /dev/null +++ b/frontend/src/actions/playerGame.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/playerStats.js b/frontend/src/actions/playerStats.js new file mode 100644 index 0000000..1f6db87 --- /dev/null +++ b/frontend/src/actions/playerStats.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/players.js b/frontend/src/actions/players.js new file mode 100644 index 0000000..7325a4e --- /dev/null +++ b/frontend/src/actions/players.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/sessions.js b/frontend/src/actions/sessions.js new file mode 100644 index 0000000..0121e3b --- /dev/null +++ b/frontend/src/actions/sessions.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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 + }; +}; diff --git a/frontend/src/actions/settings.js b/frontend/src/actions/settings.js index a6131c2..ea1a915 100644 --- a/frontend/src/actions/settings.js +++ b/frontend/src/actions/settings.js @@ -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 }; }; diff --git a/frontend/src/components/ChartComponent.js b/frontend/src/components/ChartComponent.js index 3a96efa..e80e728 100644 --- a/frontend/src/components/ChartComponent.js +++ b/frontend/src/components/ChartComponent.js @@ -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); diff --git a/frontend/src/components/ContainerComponent.js b/frontend/src/components/ContainerComponent.js index 234d773..4e697a6 100644 --- a/frontend/src/components/ContainerComponent.js +++ b/frontend/src/components/ContainerComponent.js @@ -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) { diff --git a/frontend/src/components/ItemIconComponent.js b/frontend/src/components/ItemIconComponent.js index 382ca07..ffb7aa0 100644 --- a/frontend/src/components/ItemIconComponent.js +++ b/frontend/src/components/ItemIconComponent.js @@ -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] || "?"; diff --git a/frontend/src/components/LoaderComponent.js b/frontend/src/components/LoaderComponent.js index c2ea75e..9dfe63a 100644 --- a/frontend/src/components/LoaderComponent.js +++ b/frontend/src/components/LoaderComponent.js @@ -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 diff --git a/frontend/src/components/PowerupIconComponent.js b/frontend/src/components/PowerupIconComponent.js index b418952..9c4783b 100644 --- a/frontend/src/components/PowerupIconComponent.js +++ b/frontend/src/components/PowerupIconComponent.js @@ -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] || "?"; diff --git a/frontend/src/components/WeaponIconComponent.js b/frontend/src/components/WeaponIconComponent.js index ba3aeed..e11016a 100644 --- a/frontend/src/components/WeaponIconComponent.js +++ b/frontend/src/components/WeaponIconComponent.js @@ -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] || "?"; diff --git a/frontend/src/lib/DataSource.js b/frontend/src/lib/DataSource.js new file mode 100644 index 0000000..40506dd --- /dev/null +++ b/frontend/src/lib/DataSource.js @@ -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; diff --git a/frontend/src/lib/defs.js b/frontend/src/lib/defs.js index f4694d0..6c14442 100644 --- a/frontend/src/lib/defs.js +++ b/frontend/src/lib/defs.js @@ -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", diff --git a/frontend/src/main.js b/frontend/src/main.js index 6732893..111a636 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -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( - + diff --git a/frontend/src/reducers/dashboard.js b/frontend/src/reducers/dashboard.js new file mode 100644 index 0000000..3d3abdd --- /dev/null +++ b/frontend/src/reducers/dashboard.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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; diff --git a/frontend/src/reducers/index.js b/frontend/src/reducers/index.js index 97fb9c9..cceaada 100644 --- a/frontend/src/reducers/index.js +++ b/frontend/src/reducers/index.js @@ -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 }); diff --git a/frontend/src/reducers/navigationBar.js b/frontend/src/reducers/navigationBar.js new file mode 100644 index 0000000..aa11112 --- /dev/null +++ b/frontend/src/reducers/navigationBar.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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; diff --git a/frontend/src/reducers/playerGame.js b/frontend/src/reducers/playerGame.js new file mode 100644 index 0000000..2fd35b8 --- /dev/null +++ b/frontend/src/reducers/playerGame.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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; diff --git a/frontend/src/reducers/playerStats.js b/frontend/src/reducers/playerStats.js new file mode 100644 index 0000000..6d81f6a --- /dev/null +++ b/frontend/src/reducers/playerStats.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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; diff --git a/frontend/src/lib/BaseView.js b/frontend/src/reducers/players.js similarity index 60% rename from frontend/src/lib/BaseView.js rename to frontend/src/reducers/players.js index 752c691..f3d6435 100644 --- a/frontend/src/lib/BaseView.js +++ b/frontend/src/reducers/players.js @@ -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; diff --git a/frontend/src/reducers/sessions.js b/frontend/src/reducers/sessions.js new file mode 100644 index 0000000..6af63e2 --- /dev/null +++ b/frontend/src/reducers/sessions.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017 Tomek Wójcik + * + * 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; diff --git a/frontend/src/reducers/settings.js b/frontend/src/reducers/settings.js index c230099..f3b97f1 100644 --- a/frontend/src/reducers/settings.js +++ b/frontend/src/reducers/settings.js @@ -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; diff --git a/frontend/src/views/AboutModalView.js b/frontend/src/views/AboutModalView.js index 5c5087c..e68ae01 100644 --- a/frontend/src/views/AboutModalView.js +++ b/frontend/src/views/AboutModalView.js @@ -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 ( @@ -60,7 +60,7 @@ class AboutModalView extends React.Component { } } -AboutModalView.PropTypes = { +AboutModalView.propTypes = { show: React.PropTypes.bool.isRequired, onHide: React.PropTypes.func.isRequired }; diff --git a/frontend/src/views/DashboardView.js b/frontend/src/views/DashboardView.js index 416f457..5f2fb8c 100644 --- a/frontend/src/views/DashboardView.js +++ b/frontend/src/views/DashboardView.js @@ -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 ( @@ -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 { + categories={this.props.categories} + series={this.props.series} + subtitle={this.props.day} />
@@ -138,7 +118,7 @@ class DashboardView extends BaseView { + scores={this.props.fraggersOfTheMonth} />
@@ -146,14 +126,29 @@ class DashboardView extends BaseView { + scores={this.props.emosOfTheMonth} />
- + ); } } -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; diff --git a/frontend/src/views/ErrorView.js b/frontend/src/views/ErrorView.js index 2ec83b7..abeca8b 100644 --- a/frontend/src/views/ErrorView.js +++ b/frontend/src/views/ErrorView.js @@ -22,7 +22,7 @@ import React from "react"; -class ErrorView extends React.Component { +class ErrorView extends React.PureComponent { render () { return (
@@ -44,4 +44,8 @@ class ErrorView extends React.Component { } } +ErrorView.propTypes = { + params: React.PropTypes.object +}; + export default ErrorView; diff --git a/frontend/src/views/NavigationBarView.js b/frontend/src/views/NavigationBarView.js index 45dd004..d685171 100644 --- a/frontend/src/views/NavigationBarView.js +++ b/frontend/src/views/NavigationBarView.js @@ -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 (
@@ -160,20 +144,38 @@ class NavigationBarView extends React.Component { - - + +
); } } -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; diff --git a/frontend/src/views/PlayerGameView.js b/frontend/src/views/PlayerGameView.js index c5e96d9..0cc4fff 100644 --- a/frontend/src/views/PlayerGameView.js +++ b/frontend/src/views/PlayerGameView.js @@ -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 ( -

{this.props.params.player} stats on {this.state.map}

+

{this.props.params.player} stats on {this.props.map}

@@ -139,15 +92,15 @@ class PlayerGameView extends BaseView {
- {underscore.keys(this.state.weaponStats).length == 0 && + {underscore.keys(this.props.weaponStats).length == 0 && } {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 ( @@ -176,15 +129,15 @@ class PlayerGameView extends BaseView { - {underscore.keys(this.state.itemStats).length == 0 && + {underscore.keys(this.props.itemStats).length == 0 && } {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 ( @@ -211,15 +164,15 @@ class PlayerGameView extends BaseView { - {underscore.keys(this.state.powerupStats).length == 0 && + {underscore.keys(this.props.powerupStats).length == 0 && } {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 ( @@ -235,10 +188,38 @@ class PlayerGameView extends BaseView { - + ); } } -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; diff --git a/frontend/src/views/PlayerStatsView.js b/frontend/src/views/PlayerStatsView.js index fce69fa..308b738 100644 --- a/frontend/src/views/PlayerStatsView.js +++ b/frontend/src/views/PlayerStatsView.js @@ -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 {
    @@ -314,16 +265,42 @@ class PlayerStatsView extends React.Component { + mode={this.props.mode} + sessionData={this.props.sessionWinsChartData} + mapData={this.props.mapWinsChartData} + dispatch={this.props.dispatch} /> + mode={this.props.mode} + sessionData={this.props.sessionAccuracyChartData} + mapData={this.props.mapAccuracyChartData} + dispatch={this.props.dispatch} /> ); } } -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; diff --git a/frontend/src/views/PlayersView.js b/frontend/src/views/PlayersView.js index 6349e32..d7883c6 100644 --- a/frontend/src/views/PlayersView.js +++ b/frontend/src/views/PlayersView.js @@ -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 {

    Players

      - {this.state.players.map((item, index) => { + {this.props.players.map((item, index) => { return (
    - + ); } } -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; + diff --git a/frontend/src/views/SessionsView.js b/frontend/src/views/SessionsView.js index 3d08f35..9461d5a 100644 --- a/frontend/src/views/SessionsView.js +++ b/frontend/src/views/SessionsView.js @@ -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 (
No stats :(
No stats :(
No stats :(
@@ -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 ( - {this.state.ready && + {!this.props.loading &&
+ previousDay={this.props.previousDay} + nextDay={this.props.nextDay} />

Results

+ categories={this.props.categories} + series={this.props.series} + subtitle={this.props.day} />

Stats

+ games={this.props.games} /> + previousDay={this.props.previousDay} + nextDay={this.props.nextDay} />
} - +
); } } -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; diff --git a/frontend/src/views/SettingsModalView.js b/frontend/src/views/SettingsModalView.js index d3c1bae..2bf3313 100644 --- a/frontend/src/views/SettingsModalView.js +++ b/frontend/src/views/SettingsModalView.js @@ -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 diff --git a/q3stats/web_app/blueprints/api_v1/views/players.py b/q3stats/web_app/blueprints/api_v1/views/players.py index aa32e95..7ec5f55 100644 --- a/q3stats/web_app/blueprints/api_v1/views/players.py +++ b/q3stats/web_app/blueprints/api_v1/views/players.py @@ -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), diff --git a/tests_web_app/test_get_api_v1_player_game.py b/tests_web_app/test_get_api_v1_player_game.py index db0861f..75cac51 100644 --- a/tests_web_app/test_get_api_v1_player_game.py +++ b/tests_web_app/test_get_api_v1_player_game.py @@ -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} diff --git a/tests_web_app/tmp/.placeholder b/tests_web_app/tmp/.placeholder new file mode 100644 index 0000000..e69de29