From 54febb4808c5f8e800feac35fadcf40c88e3f782 Mon Sep 17 00:00:00 2001 From: Kichiyaki Date: Sun, 20 Dec 2020 12:21:48 +0100 Subject: [PATCH] add a new route - /server/:key/ranking/player/od --- src/common/Table/Table.tsx | 3 +- src/config/namespaces.ts | 1 + .../features/PlayerPage/PlayerPage.tsx | 4 + .../IndexPage/components/Ranking/constants.ts | 2 +- .../PlayerPage/features/ODPage/ODPage.tsx | 24 ++++ .../ODPage/components/Ranking/Ranking.tsx | 125 ++++++++++++++++++ .../ODPage/components/Ranking/constants.ts | 48 +++++++ .../ODPage/components/Ranking/queries.ts | 37 ++++++ .../ODPage/components/Ranking/types.ts | 22 +++ .../ODPage/components/Ranking/usePlayers.ts | 47 +++++++ .../ranking-page/player-page/index.ts | 2 + .../ranking-page/player-page/od-page.ts | 16 +++ .../ranking-page/player-page/od-page.ts | 16 +++ src/libs/serialize-query-params/SortParam.ts | 57 ++++++++ src/utils/mapPlayerOrTribeRanking.ts | 30 +++++ 15 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/ODPage.tsx create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/Ranking.tsx create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/constants.ts create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/queries.ts create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/types.ts create mode 100644 src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/usePlayers.ts create mode 100644 src/libs/i18n/en/server-page/ranking-page/player-page/od-page.ts create mode 100644 src/libs/i18n/pl/server-page/ranking-page/player-page/od-page.ts create mode 100644 src/libs/serialize-query-params/SortParam.ts create mode 100644 src/utils/mapPlayerOrTribeRanking.ts diff --git a/src/common/Table/Table.tsx b/src/common/Table/Table.tsx index 5d7c413..b98fc28 100644 --- a/src/common/Table/Table.tsx +++ b/src/common/Table/Table.tsx @@ -28,7 +28,7 @@ export interface Props { idFieldName?: string; getRowKey?: (row: T, index: number) => string | number | null | undefined; onRequestSort?: ( - property: string, + orderBy: string, orderDirection: OrderDirection ) => void | Promise; onSelect?: (rows: T[]) => void; @@ -138,6 +138,7 @@ function Table({ count={footerProps?.count ?? data.length} size={size} {...footerProps} + page={loading ? 0 : footerProps?.page ?? 0} rowsPerPage={rowsPerPage} /> )} diff --git a/src/config/namespaces.ts b/src/config/namespaces.ts index 89e5e6d..74a929f 100644 --- a/src/config/namespaces.ts +++ b/src/config/namespaces.ts @@ -28,6 +28,7 @@ export const SERVER_PAGE = { COMMON: 'server-page/ranking-page/common', PLAYER_PAGE: { INDEX_PAGE: 'server-page/ranking-page/player-page/index-page', + OD_PAGE: 'server-page/ranking-page/player-page/od-page', }, }, }; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/PlayerPage.tsx b/src/features/ServerPage/features/RankingPage/features/PlayerPage/PlayerPage.tsx index 82ae77b..12ec078 100644 --- a/src/features/ServerPage/features/RankingPage/features/PlayerPage/PlayerPage.tsx +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/PlayerPage.tsx @@ -3,6 +3,7 @@ import { SERVER_PAGE } from '@config/routes'; import { Switch, Route } from 'react-router-dom'; import IndexPage from './features/IndexPage/IndexPage'; +import ODPage from './features/ODPage/ODPage'; import NotFoundPage from '../../../NotFoundPage/NotFoundPage'; function PlayerPage() { @@ -11,6 +12,9 @@ function PlayerPage() { + + + diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/IndexPage/components/Ranking/constants.ts b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/IndexPage/components/Ranking/constants.ts index d6a264e..a78accf 100644 --- a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/IndexPage/components/Ranking/constants.ts +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/IndexPage/components/Ranking/constants.ts @@ -32,4 +32,4 @@ export const COLUMNS: Column[] = [ }, ]; -export const LIMIT = 5; +export const LIMIT = 25; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/ODPage.tsx b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/ODPage.tsx new file mode 100644 index 0000000..4a78bc2 --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/ODPage.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import useTitle from '@libs/useTitle'; +import useServer from '@features/ServerPage/libs/ServerContext/useServer'; +import { SERVER_PAGE } from '@config/namespaces'; + +import { Container } from '@material-ui/core'; +import PageLayout from '@features/ServerPage/features/RankingPage/common/PageLayout/PageLayout'; +import Ranking from './components/Ranking/Ranking'; + +function ODPage() { + const { key } = useServer(); + const { t } = useTranslation(SERVER_PAGE.RANKING_PAGE.PLAYER_PAGE.OD_PAGE); + useTitle(t('title', { key })); + return ( + + + + + + ); +} + +export default ODPage; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/Ranking.tsx b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/Ranking.tsx new file mode 100644 index 0000000..ff4e691 --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/Ranking.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import { + useQueryParams, + NumberParam, + withDefault, + StringParam, +} from 'use-query-params'; +import { useDebouncedCallback } from 'use-debounce'; +import useUpdateEffect from '@libs/useUpdateEffect'; +import SortParam from '@libs/serialize-query-params/SortParam'; +import usePlayers from './usePlayers'; +import { validateRowsPerPage } from '@common/Table/helpers'; +import mapPlayerOrTribeRanking from '@utils/mapPlayerOrTribeRanking'; +import { COLUMNS, LIMIT, DEFAULT_SORT } from './constants'; + +import { Paper } from '@material-ui/core'; +import Table from '@common/Table/Table'; +import TableToolbar from '@common/Table/TableToolbar'; +import SearchInput from '@common/Form/SearchInput'; +import PlayerProfileLink from '@features/ServerPage/common/PlayerProfileLink/PlayerProfileLink'; + +import { TFunction } from 'i18next'; +import { Player } from './types'; + +export interface Props { + server: string; + t: TFunction; +} + +function Top5Players({ server, t }: Props) { + const [query, setQuery] = useQueryParams({ + page: withDefault(NumberParam, 0), + limit: withDefault(NumberParam, LIMIT), + q: withDefault(StringParam, ''), + sort: withDefault(SortParam, DEFAULT_SORT), + }); + const limit = validateRowsPerPage(query.limit); + const [q, setQ] = useState(query.q); + const debouncedSetQuery = useDebouncedCallback( + value => setQuery({ q: value }), + 1000 + ); + useUpdateEffect(() => { + debouncedSetQuery.callback(q); + }, [q]); + const { players, total, loading } = usePlayers( + query.page, + limit, + server, + query.q, + query.sort.toString() + ); + + return ( + + + ('ranking.searchInputPlaceholder')} + value={q} + onChange={e => { + setQ(e.target.value); + }} + onResetValue={() => setQ('')} + /> + + { + const newCol = { + ...column, + label: column.label ? t(column.label) : '', + }; + if (index === 0) { + newCol.valueFormatter = (player: Player, index: number) => { + return mapPlayerOrTribeRanking( + player, + query.sort.orderBy, + query.page * query.limit + (index + 1) + ); + }; + } + if (index === 1) { + newCol.valueFormatter = (player: Player) => ( + + ); + } + return newCol; + })} + loading={loading} + data={players} + size="small" + orderBy={query.sort.orderBy} + orderDirection={query.sort.orderDirection} + onRequestSort={(orderBy, orderDirection) => { + setQuery({ + sort: SortParam.decode(orderBy + ' ' + orderDirection), + page: 0, + }); + }} + footerProps={{ + page: query.page, + rowsPerPage: limit, + count: total, + onChangePage: page => { + if (window.scrollTo) { + window.scrollTo({ top: 0, behavior: `smooth` }); + } + setQuery({ page }); + }, + onChangeRowsPerPage: rowsPerPage => { + if (window.scrollTo) { + window.scrollTo({ top: 0, behavior: `smooth` }); + } + requestAnimationFrame(() => { + setQuery({ limit: rowsPerPage, page: 0 }); + }); + }, + }} + /> + + ); +} + +export default Top5Players; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/constants.ts b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/constants.ts new file mode 100644 index 0000000..045e74d --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/constants.ts @@ -0,0 +1,48 @@ +import { decodeSort } from '@libs/serialize-query-params/SortParam'; +import { Column } from '@common/Table/types'; +import { Player } from './types'; + +export const COLUMNS: Column[] = [ + { + field: 'rank', + label: 'ranking.columns.rank', + sortable: false, + }, + { + field: 'name', + label: 'ranking.columns.name', + sortable: false, + }, + { + field: 'scoreAtt', + label: 'ranking.columns.scoreAtt', + sortable: true, + valueFormatter: (player: Player) => + `${player.scoreAtt.toLocaleString()} (#${player.rankAtt})`, + }, + { + field: 'scoreDef', + label: 'ranking.columns.scoreDef', + sortable: true, + valueFormatter: (player: Player) => + `${player.scoreDef.toLocaleString()} (#${player.rankDef})`, + }, + { + field: 'scoreSup', + label: 'ranking.columns.scoreSup', + sortable: true, + valueFormatter: (player: Player) => + `${player.scoreSup.toLocaleString()} (#${player.rankSup})`, + }, + { + field: 'scoreTotal', + label: 'ranking.columns.scoreTotal', + sortable: true, + valueFormatter: (player: Player) => + `${player.scoreTotal.toLocaleString()} (#${player.rankTotal})`, + }, +]; + +export const LIMIT = 25; + +export const DEFAULT_SORT = decodeSort('scoreAtt DESC'); diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/queries.ts b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/queries.ts new file mode 100644 index 0000000..4c55269 --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/queries.ts @@ -0,0 +1,37 @@ +import { gql } from '@apollo/client'; + +export const PLAYERS = gql` + query players( + $server: String! + $filter: PlayerFilter + $sort: [String!] + $limit: Int + $offset: Int + ) { + players( + server: $server + filter: $filter + sort: $sort + limit: $limit + offset: $offset + ) { + items { + id + name + scoreAtt + rankAtt + scoreDef + rankDef + scoreSup + rankSup + scoreTotal + rankTotal + tribe { + id + tag + } + } + total + } + } +`; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/types.ts b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/types.ts new file mode 100644 index 0000000..8ed0222 --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/types.ts @@ -0,0 +1,22 @@ +import { List } from '@libs/graphql/types'; + +export type Player = { + id: number; + name: string; + scoreAtt: number; + rankAtt: number; + scoreDef: number; + rankDef: number; + scoreSup: number; + rankSup: number; + scoreTotal: number; + rankTotal: number; + tribe?: { + id: number; + tag: string; + }; +}; + +export type PlayerList = { + players?: List; +}; diff --git a/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/usePlayers.ts b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/usePlayers.ts new file mode 100644 index 0000000..547ea91 --- /dev/null +++ b/src/features/ServerPage/features/RankingPage/features/PlayerPage/features/ODPage/components/Ranking/usePlayers.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@apollo/client'; +import { PLAYERS } from './queries'; + +import { PlayersQueryVariables } from '@libs/graphql/types'; +import { Player, PlayerList } from './types'; + +export type QueryResult = { + players: Player[]; + loading: boolean; + total: number; +}; + +const usePlayers = ( + page: number, + limit: number, + server: string, + q: string, + sort: string +): QueryResult => { + const { loading: loadingPlayers, data } = useQuery< + PlayerList, + PlayersQueryVariables + >(PLAYERS, { + fetchPolicy: 'cache-and-network', + variables: { + limit, + offset: page * limit, + sort: [sort], + filter: { + exists: true, + nameIEQ: '%' + q + '%', + }, + server, + }, + }); + const players = data?.players?.items ?? []; + const loading = loadingPlayers && players.length === 0; + const total = data?.players?.total ?? 0; + + return { + players, + loading, + total, + }; +}; + +export default usePlayers; diff --git a/src/libs/i18n/en/server-page/ranking-page/player-page/index.ts b/src/libs/i18n/en/server-page/ranking-page/player-page/index.ts index 17759ee..a223a08 100644 --- a/src/libs/i18n/en/server-page/ranking-page/player-page/index.ts +++ b/src/libs/i18n/en/server-page/ranking-page/player-page/index.ts @@ -1,8 +1,10 @@ import * as NAMESPACES from '@config/namespaces'; import indexPage from './index-page'; +import odPage from './od-page'; const translations = { [NAMESPACES.SERVER_PAGE.RANKING_PAGE.PLAYER_PAGE.INDEX_PAGE]: indexPage, + [NAMESPACES.SERVER_PAGE.RANKING_PAGE.PLAYER_PAGE.OD_PAGE]: odPage, }; export default translations; diff --git a/src/libs/i18n/en/server-page/ranking-page/player-page/od-page.ts b/src/libs/i18n/en/server-page/ranking-page/player-page/od-page.ts new file mode 100644 index 0000000..2515cf1 --- /dev/null +++ b/src/libs/i18n/en/server-page/ranking-page/player-page/od-page.ts @@ -0,0 +1,16 @@ +const translations = { + title: 'Player OD ranking - {{key}}', + ranking: { + columns: { + rank: 'Rank', + name: 'Name', + scoreAtt: 'ODA', + scoreDef: 'ODD', + scoreSup: 'ODS', + scoreTotal: 'OD', + }, + searchInputPlaceholder: 'Search player', + }, +}; + +export default translations; diff --git a/src/libs/i18n/pl/server-page/ranking-page/player-page/od-page.ts b/src/libs/i18n/pl/server-page/ranking-page/player-page/od-page.ts new file mode 100644 index 0000000..71727a9 --- /dev/null +++ b/src/libs/i18n/pl/server-page/ranking-page/player-page/od-page.ts @@ -0,0 +1,16 @@ +const translations = { + title: 'Ranking pokonanych przeciwników (gracz) - {{key}}', + ranking: { + columns: { + rank: 'Ranking', + name: 'Nazwa', + scoreAtt: 'Agresor', + scoreDef: 'Obrońca', + scoreSup: 'Wspierający', + scoreTotal: 'Pokonani ogólnie', + }, + searchInputPlaceholder: 'Wyszukaj gracza', + }, +}; + +export default translations; diff --git a/src/libs/serialize-query-params/SortParam.ts b/src/libs/serialize-query-params/SortParam.ts new file mode 100644 index 0000000..4e6a89d --- /dev/null +++ b/src/libs/serialize-query-params/SortParam.ts @@ -0,0 +1,57 @@ +import { QueryParamConfig } from 'use-query-params'; + +export type DecodedSort = { + orderBy: string; + orderDirection?: 'asc' | 'desc'; + toString: () => string; +}; + +const validateOrderDirection = (od: string): DecodedSort['orderDirection'] => { + od = od.toLowerCase(); + return od === 'asc' ? 'asc' : od === 'desc' ? 'desc' : 'asc'; +}; + +const isDecoded = (obj: any): obj is DecodedSort => { + return ( + typeof obj.orderBy === 'string' && + typeof obj.toString === 'function' && + (!obj.orderDirection || ['asc', 'desc'].includes(obj.orderDirection)) + ); +}; + +export const decodeSort = (value: string): DecodedSort => { + const [orderBy, orderDirection] = value.split(' '); + return { + orderBy, + orderDirection: validateOrderDirection(orderDirection), + toString(): string { + return this.orderBy + ' ' + this.orderDirection; + }, + }; +}; + +const SortParam: QueryParamConfig = { + encode(value: string): string { + return value; + }, + + decode( + value: + | string + | (string | DecodedSort | null)[] + | null + | undefined + | DecodedSort + ): DecodedSort | undefined { + const v = Array.isArray(value) ? value[0] : value; + if (!v) { + return undefined; + } + if (isDecoded(v)) { + return v; + } + return decodeSort(v); + }, +}; + +export default SortParam; diff --git a/src/utils/mapPlayerOrTribeRanking.ts b/src/utils/mapPlayerOrTribeRanking.ts new file mode 100644 index 0000000..da14d3c --- /dev/null +++ b/src/utils/mapPlayerOrTribeRanking.ts @@ -0,0 +1,30 @@ +interface PlayerOrTribe { + rankAtt?: number; + rankDef?: number; + rankSup?: number; + rankTotal?: number; + rank?: number; +} + +const mapPlayerOrTribeRanking = ( + obj: PlayerOrTribe, + val: string, + def: number = 0 +): number => { + switch (val) { + case 'scoreAtt': + return obj.rankAtt ?? def; + case 'scoreDef': + return obj.rankDef ?? def; + case 'scoreSup': + return obj.rankSup ?? def; + case 'scoreTotal': + return obj.rankTotal ?? def; + case 'points': + return obj.rank ?? def; + default: + return def; + } +}; + +export default mapPlayerOrTribeRanking;