diff --git a/package.json b/package.json index efa6c92..37cb5a6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@material-ui/icons": "^4.11.0", "@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/pickers": "3.2.10", + "@nivo/bar": "0.64.0", "@nivo/core": "0.64.0", "@nivo/line": "0.64.0", "@testing-library/jest-dom": "^5.11.4", diff --git a/src/common/Chart/BarChart.tsx b/src/common/Chart/BarChart.tsx new file mode 100644 index 0000000..f8b8b6a --- /dev/null +++ b/src/common/Chart/BarChart.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { LINE_CHART } from '@config/namespaces'; +import { darkTheme } from './theme'; + +import { ResponsiveBar, BarSvgProps } from '@nivo/bar'; +import { BasicTooltip } from '@nivo/tooltip'; +import { Box, Typography } from '@material-ui/core'; +import { Skeleton } from '@material-ui/lab'; + +export interface Props extends BarSvgProps { + loading?: boolean; +} + +const BarChart = ({ data, loading, tooltipFormat, ...rest }: Props) => { + const { t } = useTranslation(LINE_CHART); + + if (loading) { + return ; + } + + if (data.length === 0) { + return ( + + + {t('emptyDataSourceMessage')} + + + ); + } + return ( + { + return ( + + ); + }} + {...rest} + theme={darkTheme} + /> + ); +}; + +export default BarChart; diff --git a/src/common/Chart/LineChart.tsx b/src/common/Chart/LineChart.tsx index 1581f41..6c85a88 100644 --- a/src/common/Chart/LineChart.tsx +++ b/src/common/Chart/LineChart.tsx @@ -6,7 +6,6 @@ import { darkTheme } from './theme'; import { ResponsiveLine, LineSvgProps } from '@nivo/line'; import { Box, Typography } from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; -import PointTooltip from './PointTooltip'; export interface Props extends LineSvgProps { loading?: boolean; @@ -35,7 +34,7 @@ const LineChart = ({ data, loading, ...rest }: Props) => { } return ( +
); diff --git a/src/common/Chart/theme.ts b/src/common/Chart/theme.ts index e3b2181..5d0a03d 100644 --- a/src/common/Chart/theme.ts +++ b/src/common/Chart/theme.ts @@ -12,6 +12,12 @@ export const darkTheme: Theme = { strokeWidth: 1, }, }, + textColor: '#fff', + tooltip: { + container: { + backgroundColor: '#000', + }, + }, axis: { legend: { text: { diff --git a/src/features/ServerPage/common/PageLayout/components/Sidebar/Sidebar.tsx b/src/features/ServerPage/common/PageLayout/components/Sidebar/Sidebar.tsx index 6c5467b..8a23b0b 100644 --- a/src/features/ServerPage/common/PageLayout/components/Sidebar/Sidebar.tsx +++ b/src/features/ServerPage/common/PageLayout/components/Sidebar/Sidebar.tsx @@ -19,6 +19,7 @@ import { Map as MapIcon, Grade as GradeIcon, Beenhere as BeenhereIcon, + Fireplace as FireplaceIcon, } from '@material-ui/icons'; import DevNote from '@common/DevNote/DevNote'; import Nav from './components/Nav/Nav'; @@ -123,6 +124,13 @@ const Sidebar = ({ t, className, open, variant, onClose, onOpen }: Props) => { Icon: , exact: true, }, + { + name: t('pageLayout.sidebar.routes.warStats'), + to: ROUTES.SERVER_PAGE.WAR_STATS_PAGE, + params: { key }, + Icon: , + exact: true, + }, ]; return ( diff --git a/src/features/ServerPage/features/WarStatsPage/WarStatsPage.tsx b/src/features/ServerPage/features/WarStatsPage/WarStatsPage.tsx index 2e7da1b..f2feff7 100644 --- a/src/features/ServerPage/features/WarStatsPage/WarStatsPage.tsx +++ b/src/features/ServerPage/features/WarStatsPage/WarStatsPage.tsx @@ -22,19 +22,21 @@ import ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout' import Spinner from '@common/Spinner/Spinner'; import Card from './components/Card/Card'; import OneSide from './components/OneSide/OneSide'; -import Result from './components/Result/Result'; +import Results from './components/Results/Results'; import { EnnoblementsQueryResult, EnnoblementsQueryVariables, - Result as ResultT, + Results as ResultsT, SideResult, + Player, + Tribe, } from './types'; function WarStatsPage() { const now = useRef(new Date()); const [isSubmitting, setIsSubmitting] = useState(false); - const [result, setResult] = useState(null); + const [results, setResults] = useState(null); const [query, setQuery] = useQueryParams({ ennobledAtGTE: withDefault(DateTimeParam, subDays(now.current, 1)), ennobledAtLTE: withDefault(DateTimeParam, now.current), @@ -70,13 +72,17 @@ function WarStatsPage() { totalGained = 0, totalLost = 0, tribesGained = 0, - playersGained = 0 + playersGained = 0, + players: Player[] = [], + tribes: Tribe[] = [] ): SideResult => { return { gained: totalGained, lost: totalLost, againstOppositeSide: tribesGained + playersGained, - difference: Math.abs(totalGained - totalLost), + difference: totalGained - totalLost, + players, + tribes, }; }; @@ -171,15 +177,19 @@ function WarStatsPage() { data.sideOneTotalGained?.total, data.sideOneTotalLost?.total, data.sideOneTribes?.total, - data.sideOnePlayers?.total + data.sideOnePlayers?.total, + sideOnePlayers, + sideOneTribes ); const computedSideTwoResult: SideResult = getSideResult( data.sideTwoTotalGained?.total, data.sideTwoTotalLost?.total, data.sideTwoTribes?.total, - data.sideTwoPlayers?.total + data.sideTwoPlayers?.total, + sideTwoPlayers, + sideTwoTribes ); - setResult({ + setResults({ sideOne: computedSideOneResult, sideTwo: computedSideTwoResult, difference: Math.abs( @@ -303,9 +313,9 @@ function WarStatsPage() { - {result && ( + {results && ( - + )} diff --git a/src/features/ServerPage/features/WarStatsPage/components/OneSide/OneSide.tsx b/src/features/ServerPage/features/WarStatsPage/components/OneSide/OneSide.tsx index 4cc0c0c..8207181 100644 --- a/src/features/ServerPage/features/WarStatsPage/components/OneSide/OneSide.tsx +++ b/src/features/ServerPage/features/WarStatsPage/components/OneSide/OneSide.tsx @@ -164,7 +164,7 @@ function OneSide({
- {t('oneSide.players')} + {t('players')}
- {t('oneSide.tribes')} + {t('tribes')} - - - {t('sections.result')} - - - - ); -} - -export default Result; diff --git a/src/features/ServerPage/features/WarStatsPage/components/Results/ChartWrapper.tsx b/src/features/ServerPage/features/WarStatsPage/components/Results/ChartWrapper.tsx new file mode 100644 index 0000000..2960f3b --- /dev/null +++ b/src/features/ServerPage/features/WarStatsPage/components/Results/ChartWrapper.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export interface Props { + children?: React.ReactNode; +} + +function ChartWrapper({ children }: Props) { + return
{children}
; +} + +export default ChartWrapper; diff --git a/src/features/ServerPage/features/WarStatsPage/components/Results/OneSideResult.tsx b/src/features/ServerPage/features/WarStatsPage/components/Results/OneSideResult.tsx new file mode 100644 index 0000000..ab07c7d --- /dev/null +++ b/src/features/ServerPage/features/WarStatsPage/components/Results/OneSideResult.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import formatNumber from '@utils/formatNumber'; +import * as ROUTES from '@config/routes'; + +import { Typography } from '@material-ui/core'; +import Link from '@common/Link/Link'; +import { TFunction } from 'i18next'; +import { SideResult } from '../../types'; + +export interface Props { + data: SideResult; + t: TFunction; + server: string; + title: string; +} + +function OneSideResult({ data, t, server, title }: Props) { + return ( +
+ + {title} + + {data.players.length > 0 && ( + + {t('players')}:{' '} + {data.players.map(player => ( + + {player.name}{' '} + + ))} + + )} + {data.tribes.length > 0 && ( + + {t('tribes')}:{' '} + {data.tribes.map(tribe => ( + + {tribe.tag}{' '} + + ))} + + )} + + {t('results.gained')}:{' '} + {formatNumber('commas', data.gained)} + + + {t('results.lost')}:{' '} + {formatNumber('commas', data.lost)} + + + {t('results.difference')}:{' '} + {formatNumber('commas', data.difference)} + +
+ ); +} + +export default OneSideResult; diff --git a/src/features/ServerPage/features/WarStatsPage/components/Results/Results.tsx b/src/features/ServerPage/features/WarStatsPage/components/Results/Results.tsx new file mode 100644 index 0000000..f322f3e --- /dev/null +++ b/src/features/ServerPage/features/WarStatsPage/components/Results/Results.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import formatNumber from '@utils/formatNumber'; +import { SERVER_PAGE } from '@config/namespaces'; + +import { useTheme } from '@material-ui/core/styles'; +import { + Card, + CardContent, + Typography, + useMediaQuery, + Grid, +} from '@material-ui/core'; +import BarChart from '@common/Chart/BarChart'; +import OneSideResult from './OneSideResult'; +import ChartWrapper from './ChartWrapper'; + +import { Results as ResultsT } from '../../types'; + +export interface Props { + data: ResultsT; + server: string; +} + +function Results({ data, server }: Props) { + const theme = useTheme(); + const isWidthDown750 = useMediaQuery(theme.breakpoints.down(750)); + const { t } = useTranslation(SERVER_PAGE.WAR_STATS_PAGE); + const tooltipFormat = (v: string | number | Date) => + typeof v === 'string' || typeof v === 'number' + ? formatNumber('commas', v) + : v.toLocaleString(); + + return ( + + + + {t('sections.results')} + + + + + + + + + + + {t('results.ennoblementsAgainstOppositeSide')} + + + {t('results.sideOne')}:{' '} + + {formatNumber('commas', data.sideOne.againstOppositeSide)} + + + + {t('results.sideTwo')}:{' '} + + {formatNumber('commas', data.sideTwo.againstOppositeSide)} + + + + {t('results.difference')}:{' '} + {formatNumber('commas', data.difference)} + + + + + '' : undefined, + }} + axisLeft={{ + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + format: tooltipFormat, + }} + labelSkipWidth={12} + labelSkipHeight={12} + animate={true} + motionStiffness={90} + motionDamping={15} + /> + + + + ); +} + +export default Results; diff --git a/src/features/ServerPage/features/WarStatsPage/types.ts b/src/features/ServerPage/features/WarStatsPage/types.ts index 91d8328..1c340cf 100644 --- a/src/features/ServerPage/features/WarStatsPage/types.ts +++ b/src/features/ServerPage/features/WarStatsPage/types.ts @@ -66,9 +66,11 @@ export type SideResult = { lost: number; difference: number; againstOppositeSide: number; + players: Player[]; + tribes: Tribe[]; }; -export type Result = { +export type Results = { sideOne: SideResult; sideTwo: SideResult; difference: number; diff --git a/src/libs/i18n/en/server-page/common.ts b/src/libs/i18n/en/server-page/common.ts index ceae7bc..78be560 100644 --- a/src/libs/i18n/en/server-page/common.ts +++ b/src/libs/i18n/en/server-page/common.ts @@ -23,6 +23,7 @@ const translations = { }, ennoblements: 'Ennoblements', map: 'Map tool', + warStats: 'War stats', }, serverInfo: { numberOfPlayers: '{{num}} player', diff --git a/src/libs/i18n/en/server-page/war-stats-page.ts b/src/libs/i18n/en/server-page/war-stats-page.ts index b19cd57..92ac368 100644 --- a/src/libs/i18n/en/server-page/war-stats-page.ts +++ b/src/libs/i18n/en/server-page/war-stats-page.ts @@ -8,17 +8,29 @@ const translations = { settings: 'Settings', sideOne: 'Side one', sideTwo: 'Side two', - result: 'Result', - }, - oneSide: { - players: 'Players', - tribes: 'Tribes', + results: 'Results', }, + players: 'Players', + tribes: 'Tribes', loading: 'Loading...', noOptions: 'No options', buttons: { generateStats: 'Generate stats', }, + results: { + sideOne: 'Side one', + sideTwo: 'Side two', + lost: 'Lost', + gained: 'Gained', + difference: 'Difference', + ennoblementsAgainstOppositeSide: 'Ennoblements against opposite side', + chart: { + sideOneGained: 'Side one - gained', + sideOneLost: 'Side one - lost', + sideTwoGained: 'Side two - gained', + sideTwoLost: 'Side two - lost', + }, + }, }; export default translations; diff --git a/src/libs/i18n/init.ts b/src/libs/i18n/init.ts index 029cbfc..6253b56 100644 --- a/src/libs/i18n/init.ts +++ b/src/libs/i18n/init.ts @@ -16,6 +16,7 @@ const init = (): i18nT => { load: 'languageOnly', detection: { order: ['subdomain'], + lookupFromSubdomainIndex: 0, }, resources: { en, diff --git a/src/libs/i18n/pl/server-page/common.ts b/src/libs/i18n/pl/server-page/common.ts index 461e75a..490dd81 100644 --- a/src/libs/i18n/pl/server-page/common.ts +++ b/src/libs/i18n/pl/server-page/common.ts @@ -23,6 +23,7 @@ const translations = { }, ennoblements: 'Przejęcia', map: 'Narzędzie mapy', + warStats: 'Statystyki wojenne', }, serverInfo: { numberOfPlayers_0: '{{num}} gracz', diff --git a/src/libs/i18n/pl/server-page/war-stats-page.ts b/src/libs/i18n/pl/server-page/war-stats-page.ts index 4227e2c..304a23f 100644 --- a/src/libs/i18n/pl/server-page/war-stats-page.ts +++ b/src/libs/i18n/pl/server-page/war-stats-page.ts @@ -8,17 +8,29 @@ const translations = { settings: 'Opcje', sideOne: 'Strona pierwsza', sideTwo: 'Strona druga', - result: 'Wynik', - }, - oneSide: { - players: 'Gracze', - tribes: 'Plemiona', + results: 'Wyniki', }, + players: 'Gracze', + tribes: 'Plemiona', loading: 'Ładowanie...', noOptions: 'Brak opcji', buttons: { generateStats: 'Wygeneruj statystyki', }, + results: { + sideOne: 'Strona pierwsza', + sideTwo: 'Strona druga', + lost: 'Stracone', + gained: 'Zdobyte', + difference: 'Różnica', + ennoblementsAgainstOppositeSide: 'Przejęcia przeciwnej strony', + chart: { + sideOneGained: 'Strona pierwsza - zdobyte', + sideOneLost: 'Strona pierwsza - stracone', + sideTwoGained: 'Strona druga - zdobyte', + sideTwoLost: 'Strona druga - stracone', + }, + }, }; export default translations; diff --git a/yarn.lock b/yarn.lock index e579a8e..6cdb3ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1428,6 +1428,24 @@ d3-time-format "^2.1.3" react-spring "^8.0.27" +"@nivo/bar@0.64.0": + version "0.64.0" + resolved "https://registry.yarnpkg.com/@nivo/bar/-/bar-0.64.0.tgz#ed6e793792fe4de62878be3645a5c3e6d437a0d7" + integrity sha512-/AFeuvlok5+Hx3umEEtBXU3I5Pmo9oauWeqBrEejsA2Vb/Te/tdsruiSBI6d2s69XVnGU0i/E9lmC7HmcIp+/Q== + dependencies: + "@nivo/annotations" "0.64.0" + "@nivo/axes" "0.64.0" + "@nivo/colors" "0.64.0" + "@nivo/core" "0.64.0" + "@nivo/legends" "0.64.0" + "@nivo/scales" "0.64.0" + "@nivo/tooltip" "0.64.0" + d3-scale "^3.0.0" + d3-shape "^1.2.2" + lodash "^4.17.11" + react-motion "^0.5.2" + recompose "^0.30.0" + "@nivo/colors@0.64.0": version "0.64.0" resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.64.0.tgz#b754950809738bd8add710f56bcd344689347743" @@ -4196,7 +4214,7 @@ d3-scale@^3.0.0: d3-time "1 - 2" d3-time-format "2 - 3" -d3-shape@^1.3.5: +d3-shape@^1.2.2, d3-shape@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==