add a new route - /server/:key/ranking/player/od

This commit is contained in:
Dawid Wysokiński 2020-12-20 12:21:48 +01:00
parent cdd2bd3ecf
commit 54febb4808
15 changed files with 432 additions and 2 deletions

View File

@ -28,7 +28,7 @@ export interface Props<T> {
idFieldName?: string;
getRowKey?: (row: T, index: number) => string | number | null | undefined;
onRequestSort?: (
property: string,
orderBy: string,
orderDirection: OrderDirection
) => void | Promise<void>;
onSelect?: (rows: T[]) => void;
@ -138,6 +138,7 @@ function Table<T extends object>({
count={footerProps?.count ?? data.length}
size={size}
{...footerProps}
page={loading ? 0 : footerProps?.page ?? 0}
rowsPerPage={rowsPerPage}
/>
)}

View File

@ -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',
},
},
};

View File

@ -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() {
<Route exact path={SERVER_PAGE.RANKING_PAGE.PLAYER_PAGE.INDEX_PAGE}>
<IndexPage />
</Route>
<Route exact path={SERVER_PAGE.RANKING_PAGE.PLAYER_PAGE.OD_PAGE}>
<ODPage />
</Route>
<Route path="*">
<NotFoundPage />
</Route>

View File

@ -32,4 +32,4 @@ export const COLUMNS: Column<Player>[] = [
},
];
export const LIMIT = 5;
export const LIMIT = 25;

View File

@ -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 (
<PageLayout>
<Container>
<Ranking t={t} server={key} />
</Container>
</PageLayout>
);
}
export default ODPage;

View File

@ -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 (
<Paper>
<TableToolbar style={{ justifyContent: 'flex-end' }}>
<SearchInput
variant="outlined"
size="small"
placeholder={t<string>('ranking.searchInputPlaceholder')}
value={q}
onChange={e => {
setQ(e.target.value);
}}
onResetValue={() => setQ('')}
/>
</TableToolbar>
<Table
columns={COLUMNS.map((column, index) => {
const newCol = {
...column,
label: column.label ? t<string>(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) => (
<PlayerProfileLink player={player} server={server} />
);
}
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 });
});
},
}}
/>
</Paper>
);
}
export default Top5Players;

View File

@ -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<Player>[] = [
{
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');

View File

@ -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
}
}
`;

View File

@ -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<Player[]>;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, DecodedSort | undefined> = {
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;

View File

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