add SearchPage functionality

This commit is contained in:
Dawid Wysokiński 2020-12-26 16:48:35 +01:00
parent 646f4d9ea4
commit 8e1c1f8f2b
21 changed files with 557 additions and 8 deletions

View File

@ -14,6 +14,7 @@ import {
Link as MUILink,
InputAdornment,
IconButton,
AppBarProps,
} from '@material-ui/core';
import { Search as SearchIcon, Input as InputIcon } from '@material-ui/icons';
import Link from '@common/Link/Link';
@ -24,12 +25,14 @@ export interface Props {
showLinkToHomePage?: boolean;
hideVersionSelectorOnMobile?: boolean;
defaultQ?: string;
appBarProps?: AppBarProps;
}
export default function Header({
showLinkToHomePage = true,
hideVersionSelectorOnMobile = false,
defaultQ = '',
appBarProps = {},
}: Props) {
const [q, setQ] = useState<string>(defaultQ);
const { t } = useTranslation(NAMESPACES.COMMON);
@ -47,7 +50,7 @@ export default function Header({
</div>
);
return (
<AppBar position="fixed">
<AppBar position="fixed" {...appBarProps}>
<Container>
<Toolbar disableGutters className={classes.toolbar}>
<form className={classes.form}>

View File

@ -1,4 +1,4 @@
export const DEFAULT_LANGUAGE = process.env.REACT_APP_DEFAULT_LANGUAGE ?? 'en';
export const DEFAULT_LANGUAGE = process.env.REACT_APP_DEFAULT_LANGUAGE ?? 'pl';
export const NAME = 'TWHelp';

View File

@ -2,6 +2,7 @@ export const COMMON = 'common';
export const TABLE = 'table';
export const LINE_CHART = 'line-chart';
export const INDEX_PAGE = 'index-page';
export const SEARCH_PAGE = 'search-page';
export const NOT_FOUND_PAGE = 'not-found-page';
export const SERVER_PAGE = {
COMMON: 'server-page/common',

View File

@ -1,17 +1,83 @@
import React from 'react';
import { useQueryParams, withDefault, StringParam } from 'use-query-params';
import {
useQueryParams,
withDefault,
StringParam,
NumberParam,
} from 'use-query-params';
import { useTranslation } from 'react-i18next';
import useTitle from '@libs/useTitle';
import useScrollToElement from '@libs/useScrollToElement';
import { validateRowsPerPage } from '@common/Table/helpers';
import extractVersionCodeFromHostname from '@utils/extractVersionCodeFromHostname';
import { SEARCH_PAGE } from '@config/namespaces';
import { MODES, LIMIT } from './constants';
import { Container, Paper, Tabs, Tab } from '@material-ui/core';
import MainLayout from '@common/MainLayout/MainLayout';
import PlayerTable from './components/PlayerTable/PlayerTable';
import TribeTable from './components/TribeTable/TribeTable';
function SearchPage() {
const [query] = useQueryParams({
const [query, setQuery] = useQueryParams({
q: withDefault(StringParam, ''),
mode: withDefault(StringParam, MODES.PLAYER),
page: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, LIMIT),
});
const limit = validateRowsPerPage(query.limit);
const version = extractVersionCodeFromHostname(window.location.hostname);
const { t } = useTranslation(SEARCH_PAGE);
useTitle(t('title', { query: query.q }));
useScrollToElement(
document.documentElement,
[query.q, query.mode, query.page, limit],
{ behavior: 'auto', block: 'start' }
);
const currentTab = Object.values(MODES).findIndex(m => query.mode === m);
const handleTabChange = (_e: React.ChangeEvent<{}>, newTab: number) => {
const newMode = Object.values(MODES)[newTab];
if (newMode !== query.mode) {
setQuery({ mode: Object.values(MODES)[newTab], page: 0, limit: LIMIT });
}
};
const handlePageChange = (page: number) => {
setQuery({ page });
};
const handleRowsPerPageChange = (rowsPerPage: number) => {
setQuery({ limit: rowsPerPage, page: 0 });
};
const tableProps = {
t: t,
q: query.q,
limit: limit,
page: query.page,
version: version,
onChangePage: handlePageChange,
onChangeRowsPerPage: handleRowsPerPageChange,
};
return (
<MainLayout
headerProps={{ hideVersionSelectorOnMobile: true, defaultQ: query.q }}
>
SearchPage
<Container>
<Paper>
<Tabs centered value={currentTab} onChange={handleTabChange}>
<Tab label={t('modes.player')} />
<Tab label={t('modes.tribe')} />
</Tabs>
{query.mode === MODES.TRIBE ? (
<TribeTable {...tableProps} />
) : (
<PlayerTable {...tableProps} />
)}
</Paper>
</Container>
</MainLayout>
);
}

View File

@ -0,0 +1,68 @@
import React from 'react';
import usePlayers from './usePlayers';
import { COLUMNS } from './constants';
import Table from '@common/Table/Table';
import { Props as TableFooterProps } from '@common/Table/TableFooter';
import PlayerProfileLink from '@features/ServerPage/common/PlayerProfileLink/PlayerProfileLink';
import { TFunction } from 'i18next';
import { Player } from './types';
export interface Props {
t: TFunction;
page: number;
limit: number;
q: string;
onChangePage: TableFooterProps['onChangePage'];
onChangeRowsPerPage: TableFooterProps['onChangeRowsPerPage'];
version: string;
}
function PlayerTable({
t,
q,
limit,
page,
onChangePage,
onChangeRowsPerPage,
version,
}: Props) {
const { players, total, loading } = usePlayers(version, page, limit, q);
return (
<Table
columns={COLUMNS.map((column, index) => ({
...column,
valueFormatter:
index === 1
? (player: Player) => (
<PlayerProfileLink
player={player}
tribe={
player.tribeID && player.tribeTag
? { id: player.tribeID, tag: player.tribeTag }
: undefined
}
server={player.server}
/>
)
: column.valueFormatter,
label: column.label ? t<string>(column.label) : '',
}))}
loading={loading}
data={players}
size="small"
getRowKey={(row: Player) => row.server + row.id}
footerProps={{
page,
rowsPerPage: limit,
count: total,
onChangePage,
onChangeRowsPerPage,
}}
/>
);
}
export default PlayerTable;

View File

@ -0,0 +1,38 @@
import formatNumber from '@utils/formatNumber';
import { Column } from '@common/Table/types';
import { Player } from './types';
export const COLUMNS: Column<Player>[] = [
{
field: 'server',
label: 'playerTable.columns.server',
sortable: false,
},
{
field: 'name',
label: 'playerTable.columns.name',
sortable: false,
},
{
field: 'bestRank',
label: 'playerTable.columns.bestRank',
sortable: false,
valueFormatter: (player: Player) => player.bestRank,
},
{
field: 'mostPoints',
label: 'playerTable.columns.mostPoints',
sortable: false,
valueFormatter: (player: Player) =>
formatNumber('commas', player.mostPoints),
},
{
field: 'mostVillages',
label: 'playerTable.columns.mostVillages',
sortable: false,
valueFormatter: (player: Player) =>
formatNumber('commas', player.mostVillages),
},
];
export const LIMIT = 25;

View File

@ -0,0 +1,33 @@
import { gql } from '@apollo/client';
export const SEARCH_PLAYER = gql`
query searchPlayer(
$version: String!
$name: String
$id: Int
$sort: [String!]
$limit: Int
$offset: Int
) {
foundPlayers: searchPlayer(
version: $version
name: $name
id: $id
sort: $sort
limit: $limit
offset: $offset
) {
items {
server
id
name
bestRank
mostPoints
mostVillages
tribeID
tribeTag
}
total
}
}
`;

View File

@ -0,0 +1,16 @@
import { List } from '@libs/graphql/types';
export type Player = {
server: string;
id: number;
name: string;
bestRank: number;
mostPoints: number;
mostVillages: number;
tribeID: number;
tribeTag: string;
};
export type PlayerList = {
foundPlayers?: List<Player[]>;
};

View File

@ -0,0 +1,47 @@
import { useQuery } from '@apollo/client';
import { SEARCH_PLAYER } from './queries';
import { SearchPlayerQueryVariables } from '@libs/graphql/types';
import { Player, PlayerList } from './types';
export type QueryResult = {
players: Player[];
loading: boolean;
total: number;
};
const usePlayers = (
version: string,
page: number,
limit: number,
q: string
): QueryResult => {
const id = parseInt(q, 10);
const skip = q.trim() === '';
const { loading: loadingPlayers, data } = useQuery<
PlayerList,
SearchPlayerQueryVariables
>(SEARCH_PLAYER, {
fetchPolicy: 'cache-and-network',
variables: {
limit,
offset: page * limit,
sort: ['server ASC', 'mostPoints DESC'],
version,
name: '%' + q + '%',
id: !isNaN(id) && id > 0 ? id : undefined,
},
skip,
});
const players = data?.foundPlayers?.items ?? [];
const loading = loadingPlayers && players.length === 0 && !skip;
const total = data?.foundPlayers?.total ?? 0;
return {
players,
loading,
total,
};
};
export default usePlayers;

View File

@ -0,0 +1,66 @@
import React from 'react';
import useTribes from './useTribes';
import * as ROUTES from '@config/routes';
import { COLUMNS } from './constants';
import Table from '@common/Table/Table';
import Link from '@common/Link/Link';
import { Props as TableFooterProps } from '@common/Table/TableFooter';
import { TFunction } from 'i18next';
import { Tribe } from './types';
export interface Props {
t: TFunction;
page: number;
limit: number;
q: string;
onChangePage: TableFooterProps['onChangePage'];
onChangeRowsPerPage: TableFooterProps['onChangeRowsPerPage'];
version: string;
}
function TribeTable({
t,
q,
limit,
page,
onChangePage,
onChangeRowsPerPage,
version,
}: Props) {
const { tribes, total, loading } = useTribes(version, page, limit, q);
return (
<Table
columns={COLUMNS.map((column, index) => ({
...column,
valueFormatter:
index === 1
? (tribe: Tribe) => (
<Link
to={ROUTES.SERVER_PAGE.TRIBE_PAGE.INDEX_PAGE}
params={{ key: tribe.server, id: tribe.id }}
>
{tribe.name} ({tribe.tag})
</Link>
)
: column.valueFormatter,
label: column.label ? t<string>(column.label) : '',
}))}
loading={loading}
data={tribes}
size="small"
getRowKey={(row: Tribe) => row.server + row.id}
footerProps={{
page,
rowsPerPage: limit,
count: total,
onChangePage,
onChangeRowsPerPage,
}}
/>
);
}
export default TribeTable;

View File

@ -0,0 +1,37 @@
import formatNumber from '@utils/formatNumber';
import { Column } from '@common/Table/types';
import { Tribe } from './types';
export const COLUMNS: Column<Tribe>[] = [
{
field: 'server',
label: 'tribeTable.columns.server',
sortable: false,
},
{
field: 'name',
label: 'tribeTable.columns.name',
sortable: false,
},
{
field: 'bestRank',
label: 'tribeTable.columns.bestRank',
sortable: false,
valueFormatter: (tribe: Tribe) => tribe.bestRank,
},
{
field: 'mostPoints',
label: 'tribeTable.columns.mostPoints',
sortable: false,
valueFormatter: (tribe: Tribe) => formatNumber('commas', tribe.mostPoints),
},
{
field: 'mostVillages',
label: 'tribeTable.columns.mostVillages',
sortable: false,
valueFormatter: (tribe: Tribe) =>
formatNumber('commas', tribe.mostVillages),
},
];
export const LIMIT = 25;

View File

@ -0,0 +1,30 @@
import { gql } from '@apollo/client';
export const SEARCH_TRIBE = gql`
query searchTribe(
$version: String!
$query: String!
$sort: [String!]
$limit: Int
$offset: Int
) {
foundTribes: searchTribe(
version: $version
query: $query
sort: $sort
limit: $limit
offset: $offset
) {
items {
server
id
tag
name
bestRank
mostPoints
mostVillages
}
total
}
}
`;

View File

@ -0,0 +1,15 @@
import { List } from '@libs/graphql/types';
export type Tribe = {
server: string;
id: number;
name: string;
tag: string;
bestRank: number;
mostPoints: number;
mostVillages: number;
};
export type TribeList = {
foundTribes?: List<Tribe[]>;
};

View File

@ -0,0 +1,45 @@
import { useQuery } from '@apollo/client';
import { SEARCH_TRIBE } from './queries';
import { SearchTribeQueryVariables } from '@libs/graphql/types';
import { Tribe, TribeList } from './types';
export type QueryResult = {
tribes: Tribe[];
loading: boolean;
total: number;
};
const useTribes = (
version: string,
page: number,
limit: number,
q: string
): QueryResult => {
const skip = q.trim() === '';
const { loading: loadingTribes, data } = useQuery<
TribeList,
SearchTribeQueryVariables
>(SEARCH_TRIBE, {
fetchPolicy: 'cache-and-network',
variables: {
limit,
offset: page * limit,
sort: ['server ASC', 'mostPoints DESC'],
version,
query: '%' + q + '%',
},
skip,
});
const tribes = data?.foundTribes?.items ?? [];
const loading = loadingTribes && tribes.length === 0 && !skip;
const total = data?.foundTribes?.total ?? 0;
return {
tribes,
loading,
total,
};
};
export default useTribes;

View File

@ -0,0 +1,6 @@
export const MODES = {
PLAYER: 'player',
TRIBE: 'tribe',
};
export const LIMIT = 25;

View File

@ -7,7 +7,16 @@ import {
const createClient = (uri: string): ApolloClient<NormalizedCacheObject> => {
return new ApolloClient({
uri: uri,
cache: new InMemoryCache(),
cache: new InMemoryCache({
typePolicies: {
FoundPlayer: {
keyFields: ['server', 'id'],
},
FoundTribe: {
keyFields: ['server', 'id'],
},
},
}),
});
};

View File

@ -3,7 +3,7 @@ export type List<T> = {
items: T;
};
type QueryVariables<T> = {
type QueryVariables<T = undefined> = {
sort?: string[];
limit?: number;
offset?: number;
@ -153,3 +153,14 @@ export type VillageQueryVariables = {
server: string;
id: number;
};
export type SearchPlayerQueryVariables = Omit<QueryVariables, 'filter'> & {
version: string;
name?: string;
id?: number;
};
export type SearchTribeQueryVariables = Omit<QueryVariables, 'filter'> & {
version: string;
query: string;
};

View File

@ -1,6 +1,7 @@
import * as NAMESPACES from '@config/namespaces';
import common from './common';
import indexPage from './index-page';
import searchPage from './search-page';
import notFoundPage from './not-found-page';
import serverPage from './server-page';
import table from './table';
@ -9,6 +10,7 @@ import lineChart from './line-chart';
const translations = {
[NAMESPACES.COMMON]: common,
[NAMESPACES.INDEX_PAGE]: indexPage,
[NAMESPACES.SEARCH_PAGE]: searchPage,
[NAMESPACES.NOT_FOUND_PAGE]: notFoundPage,
[NAMESPACES.TABLE]: table,
[NAMESPACES.LINE_CHART]: lineChart,

View File

@ -0,0 +1,27 @@
const translations = {
title: 'Search - {{query}}',
modes: {
player: 'Player',
tribe: 'Tribe',
},
playerTable: {
columns: {
server: 'Server',
name: 'Name',
bestRank: 'Best rank',
mostPoints: 'Most points',
mostVillages: 'Most villages',
},
},
tribeTable: {
columns: {
server: 'Server',
name: 'Name',
bestRank: 'Best rank',
mostPoints: 'Most points',
mostVillages: 'Most villages',
},
},
};
export default translations;

View File

@ -1,6 +1,7 @@
import * as NAMESPACES from '@config/namespaces';
import common from './common';
import indexPage from './index-page';
import searchPage from './search-page';
import notFoundPage from './not-found-page';
import serverPage from './server-page';
import table from './table';
@ -9,9 +10,10 @@ import lineChart from './line-chart';
const translations = {
[NAMESPACES.COMMON]: common,
[NAMESPACES.INDEX_PAGE]: indexPage,
[NAMESPACES.SEARCH_PAGE]: searchPage,
[NAMESPACES.NOT_FOUND_PAGE]: notFoundPage,
[NAMESPACES.LINE_CHART]: lineChart,
[NAMESPACES.TABLE]: table,
[NAMESPACES.LINE_CHART]: lineChart,
...serverPage,
};

View File

@ -0,0 +1,27 @@
const translations = {
title: 'Wyszukiwanie - {{query}}',
modes: {
player: 'Gracz',
tribe: 'Plemię',
},
playerTable: {
columns: {
server: 'Serwer',
name: 'Nazwa',
bestRank: 'Najlepszy ranking',
mostPoints: 'Najwięcej punktów',
mostVillages: 'Najwięcej wiosek',
},
},
tribeTable: {
columns: {
server: 'Serwer',
name: 'Nazwa',
bestRank: 'Najlepszy ranking',
mostPoints: 'Najwięcej punktów',
mostVillages: 'Najwięcej wiosek',
},
},
};
export default translations;