add a new route - /server/:key/ranking/player/od
This commit is contained in:
parent
cdd2bd3ecf
commit
54febb4808
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -32,4 +32,4 @@ export const COLUMNS: Column<Player>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export const LIMIT = 5;
|
||||
export const LIMIT = 25;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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');
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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[]>;
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
57
src/libs/serialize-query-params/SortParam.ts
Normal file
57
src/libs/serialize-query-params/SortParam.ts
Normal 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;
|
30
src/utils/mapPlayerOrTribeRanking.ts
Normal file
30
src/utils/mapPlayerOrTribeRanking.ts
Normal 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;
|
Reference in New Issue
Block a user