add VillagePage

This commit is contained in:
Dawid Wysokiński 2020-12-19 13:23:23 +01:00
parent a0ed063e23
commit 882c479a08
26 changed files with 491 additions and 22 deletions

View File

@ -8,17 +8,20 @@ export const SERVER_PAGE = {
INDEX_PAGE: 'server-page/index-page',
PLAYER_PAGE: {
COMMON: 'server-page/player-page/common',
INDEX_PAGE: '/server-page/player-page/index-page',
HISTORY_PAGE: '/server-page/player-page/history-page',
TRIBE_CHANGES_PAGE: '/server-page/player-page/tribe-changes-page',
ENNOBLEMENTS_PAGE: '/server-page/player-page/ennoblements-page',
INDEX_PAGE: 'server-page/player-page/index-page',
HISTORY_PAGE: 'server-page/player-page/history-page',
TRIBE_CHANGES_PAGE: 'server-page/player-page/tribe-changes-page',
ENNOBLEMENTS_PAGE: 'server-page/player-page/ennoblements-page',
},
TRIBE_PAGE: {
COMMON: 'server-page/tribe-page/common',
INDEX_PAGE: '/server-page/tribe-page/index-page',
MEMBERS_PAGE: '/server-page/tribe-page/members-page',
HISTORY_PAGE: '/server-page/tribe-page/history-page',
TRIBE_CHANGES_PAGE: '/server-page/tribe-page/tribe-changes-page',
ENNOBLEMENTS_PAGE: '/server-page/tribe-page/ennoblements-page',
INDEX_PAGE: 'server-page/tribe-page/index-page',
MEMBERS_PAGE: 'server-page/tribe-page/members-page',
HISTORY_PAGE: 'server-page/tribe-page/history-page',
TRIBE_CHANGES_PAGE: 'server-page/tribe-page/tribe-changes-page',
ENNOBLEMENTS_PAGE: 'server-page/tribe-page/ennoblements-page',
},
VILLAGE_PAGE: {
INDEX_PAGE: 'server-page/village-page/index-page',
},
};

View File

@ -6,6 +6,7 @@ import ServerProvider from './libs/ServerContext/Provider';
import IndexPage from './features/IndexPage/IndexPage';
import PlayerPage from './features/PlayerPage/PlayerPage';
import TribePage from './features/TribePage/TribePage';
import VillagePage from './features/VillagePage/VillagePage';
import NotFoundPage from '../NotFoundPage/NotFoundPage';
const EnhancedRoute = ({ children, ...rest }: RouteProps) => {
@ -28,6 +29,9 @@ function ServerPage() {
<EnhancedRoute path={SERVER_PAGE.TRIBE_PAGE.INDEX_PAGE}>
<TribePage />
</EnhancedRoute>
<EnhancedRoute exact path={SERVER_PAGE.VILLAGE_PAGE.INDEX_PAGE}>
<VillagePage />
</EnhancedRoute>
<Route path="*">
<NotFoundPage />
</Route>

View File

@ -0,0 +1,52 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import useTitle from '@libs/useTitle';
import useServer from '@features/ServerPage/libs/ServerContext/useServer';
import useVillage from './useVillage';
import { SERVER_PAGE } from '@config/namespaces';
import { Container } from '@material-ui/core';
import NotFoundPage from '@features/ServerPage/features/NotFoundPage/NotFoundPage';
import ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout';
import Spinner from '@common/Spinner/Spinner';
import PageLayout from './components/PageLayout/PageLayout';
import Ennoblements from './components/Ennoblements/Ennoblements';
function VillagePage() {
const { t } = useTranslation(SERVER_PAGE.VILLAGE_PAGE.INDEX_PAGE);
const { key } = useServer();
const { village, loading } = useVillage();
useTitle(t('title', { name: village?.fullName ?? '', key }), {
skip: !village,
});
if (loading) {
return (
<ServerPageLayout>
<Spinner
containerProps={{
marginTop: 5,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
/>
</ServerPageLayout>
);
}
if (!village) {
return <NotFoundPage title={t('villageNotFound')} />;
}
return (
<PageLayout t={t} server={key} village={village}>
<Container>
<Ennoblements t={t} server={key} villageID={village.id} />
</Container>
</PageLayout>
);
}
export default VillagePage;

View File

@ -0,0 +1,116 @@
import React from 'react';
import { useQuery } from '@apollo/client';
import { useQueryParams, NumberParam, withDefault } from 'use-query-params';
import { validateRowsPerPage } from '@common/Table/helpers';
import { ENNOBLEMENTS } from './queries';
import { LIMIT } from './constants';
import { Paper } from '@material-ui/core';
import Table from '@common/Table/Table';
import PlayerProfileLink from '@features/ServerPage/common/PlayerProfileLink/PlayerProfileLink';
import { TFunction } from 'i18next';
import { EnnoblementsQueryVariables } from '@libs/graphql/types';
import { Ennoblements as EnnoblementsT, Ennoblement } from './types';
export interface Props {
server: string;
villageID: number;
t: TFunction;
}
function Ennoblements({ t, server, villageID }: Props) {
const [query, setQuery] = useQueryParams({
page: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, LIMIT),
});
const limit = validateRowsPerPage(query.limit);
const { data: queryData, loading: queryLoading } = useQuery<
EnnoblementsT,
EnnoblementsQueryVariables
>(ENNOBLEMENTS, {
fetchPolicy: 'cache-and-network',
variables: {
limit,
offset: query.page * limit,
sort: ['ennobledAt DESC'],
filter: {
villageID: [villageID],
},
server,
},
});
const ennoblements = queryData?.ennoblements?.items ?? [];
const loading = ennoblements.length === 0 && queryLoading;
const total = queryData?.ennoblements?.total ?? 0;
return (
<Paper>
<Table
columns={[
{
field: 'ennobledAt',
label: t('ennoblements.columns.ennobledAt'),
sortable: false,
type: 'datetime',
},
{
field: 'oldOwner',
label: t('ennoblements.columns.oldOwner'),
sortable: false,
valueFormatter: (e: Ennoblement) => {
if (e.oldOwner) {
return (
<PlayerProfileLink
server={server}
player={e.oldOwner}
tribe={e.oldOwnerTribe}
/>
);
}
return '-';
},
},
{
field: 'newOwner',
label: t('ennoblements.columns.newOwner'),
sortable: false,
valueFormatter: (e: Ennoblement) => {
return (
<PlayerProfileLink
server={server}
player={e.newOwner}
tribe={e.newOwnerTribe}
/>
);
},
},
]}
loading={loading}
data={ennoblements}
size="small"
footerProps={{
page: loading ? 0 : query.page,
rowsPerPage: query.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 Ennoblements;

View File

@ -0,0 +1 @@
export const LIMIT = 25;

View File

@ -0,0 +1,46 @@
import { gql } from '@apollo/client';
export const ENNOBLEMENTS = gql`
query ennoblements(
$server: String!
$sort: [String!]
$limit: Int
$offset: Int
$filter: EnnoblementFilter
) {
ennoblements(
server: $server
filter: $filter
sort: $sort
limit: $limit
offset: $offset
) {
items {
village {
name
x
y
id
}
newOwner {
id
name
}
newOwnerTribe {
id
tag
}
oldOwner {
id
name
}
oldOwnerTribe {
id
tag
}
ennobledAt
}
total
}
}
`;

View File

@ -0,0 +1,31 @@
import { List } from '@libs/graphql/types';
export type Ennoblement = {
village: {
id: number;
name: string;
x: number;
y: number;
};
newOwner: {
id: number;
name: string;
};
newOwnerTribe?: {
id: number;
tag: string;
};
oldOwner?: {
id: number;
name: string;
};
oldOwnerTribe?: {
id: number;
tag: string;
};
ennobledAt: Date | string;
};
export type Ennoblements = {
ennoblements?: List<Ennoblement[]>;
};

View File

@ -0,0 +1,93 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import randomInteger from '@utils/randomInteger';
import { TFunction } from 'i18next';
import { Village } from '../../types';
import { makeStyles } from '@material-ui/core/styles';
import { Toolbar, Typography } from '@material-ui/core';
import ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout';
import PlayerProfileLink from '@features/ServerPage/common/PlayerProfileLink/PlayerProfileLink';
import background1 from './backgrounds/bg-1-dark.jpg';
import background2 from './backgrounds/bg-2-dark.jpg';
import background3 from './backgrounds/bg-3-dark.jpg';
export interface Props {
children: React.ReactNode;
village: Village;
server: string;
t: TFunction;
}
function PageLayout({ children, village, server, t }: Props) {
const classes = useStyles();
const bg = useMemo(() => {
return randomInteger(1, 3);
}, []);
return (
<ServerPageLayout noPadding>
<header className={clsx(classes.header, 'bg-' + bg)}>
<Toolbar className={classes.toolbar}>
<div>
<Typography variant="h3">
{village.fullName} (
{t('pageLayout.points', {
points: village.points.toLocaleString(),
})}
)
</Typography>
{village.player && (
<Typography variant="h4">
<PlayerProfileLink server={server} player={village.player} />
</Typography>
)}
</div>
</Toolbar>
</header>
<div className={classes.content}>{children}</div>
</ServerPageLayout>
);
}
const useStyles = makeStyles(theme => ({
header: {
width: '100%',
minHeight: theme.spacing(30),
backgroundPosition: 'center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
display: 'flex',
justifyContent: 'flex-end',
flexDirection: 'column',
boxShadow: theme.shadows[4],
'&.bg-1': {
backgroundImage: `url(${background1})`,
},
'&.bg-2': {
backgroundImage: `url(${background2})`,
},
'&.bg-3': {
backgroundImage: `url(${background3})`,
},
},
toolbar: {
[theme.breakpoints.down('sm')]: {
textAlign: 'center',
},
'& > div': {
width: '100%',
marginBottom: theme.spacing(1),
},
},
content: {
height: '100%',
padding: theme.spacing(3, 0),
'&.no-padding': {
padding: '0 0',
},
},
}));
export default PageLayout;

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@ -0,0 +1,22 @@
export type Village = {
id: number;
x: number;
y: number;
name: string;
bonus: number;
points: number;
fullName: string;
player?: {
id: number;
name: string;
tribe?: {
id: number;
tag: string;
};
};
};
export type Params = {
id: string;
key: string;
};

View File

@ -0,0 +1,64 @@
import { gql } from '@apollo/client';
import { useParams } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import buildVillageName from '@utils/buildVillageName';
import { VillageQueryVariables } from '@libs/graphql/types';
import { Village, Params } from './types';
const QUERY = gql`
query village($server: String!, $id: Int!) {
village(server: $server, id: $id) {
id
name
points
x
y
bonus
player {
id
name
tribe {
id
tag
}
}
}
}
`;
type VillageQueryResult = {
village?: Village;
};
export type QueryResult = {
village?: Village;
loading: boolean;
};
const useVillage = (): QueryResult => {
const { key, id } = useParams<Params>();
const { loading: loadingServers, data } = useQuery<
VillageQueryResult,
VillageQueryVariables
>(QUERY, {
fetchPolicy: 'cache-and-network',
variables: {
id: parseInt(id, 10),
server: key,
},
});
const village = data?.village ? data.village : undefined;
const loading = loadingServers && !village;
return {
village: village
? {
...village,
fullName: buildVillageName(village.name, village.x, village.y),
}
: undefined,
loading,
};
};
export default useVillage;

View File

@ -123,6 +123,7 @@ export type EnnoblementFilter = {
oldOwnerID?: number[];
oldOwnerTribeID?: number[];
};
villageID?: number[];
};
export type EnnoblementsQueryVariables = QueryVariablesWithServer<
@ -137,3 +138,8 @@ export type DailyTribeStatsFilter = {
export type DailyTribeStatsQueryVariables = QueryVariablesWithServer<
DailyTribeStatsFilter
>;
export type VillageQueryVariables = {
server: string;
id: number;
};

View File

@ -3,10 +3,12 @@ import common from './common';
import indexPage from './index-page';
import playerPage from './player-page';
import tribePage from './tribe-page';
import villagePage from './village-page';
const translations = {
[NAMESPACES.SERVER_PAGE.COMMON]: common,
[NAMESPACES.SERVER_PAGE.INDEX_PAGE]: indexPage,
[NAMESPACES.SERVER_PAGE.VILLAGE_PAGE.INDEX_PAGE]: villagePage,
...playerPage,
...tribePage,
};

View File

@ -2,7 +2,7 @@ const translations = {
title: `{{name}} - Ennoblements - {{key}}`,
ennoblements: {
columns: {
ennobledAt: 'Date',
ennobledAt: 'Date/time',
village: 'Village',
oldOwner: 'Old owner',
newOwner: 'New owner',

View File

@ -2,7 +2,7 @@ const translations = {
title: `{{name}} - Tribe changes - {{key}}`,
tribeChanges: {
columns: {
createdAt: 'Date',
createdAt: 'Date/time',
oldTribe: 'Old tribe',
newTribe: 'New tribe',
},

View File

@ -2,7 +2,7 @@ const translations = {
title: `{{tag}} - Ennoblements - {{key}}`,
ennoblements: {
columns: {
ennobledAt: 'Date',
ennobledAt: 'Date/time',
village: 'Village',
oldOwner: 'Old owner',
newOwner: 'New owner',

View File

@ -2,7 +2,7 @@ const translations = {
title: `{{tag}} - Tribe changes - {{key}}`,
tribeChanges: {
columns: {
createdAt: 'Date',
createdAt: 'Date/time',
oldTribe: 'Old tribe',
newTribe: 'New tribe',
player: 'Player',

View File

@ -0,0 +1,16 @@
const translations = {
title: '{{name}} - Village profile - {{key}}',
villageNotFound: 'Village not found',
pageLayout: {
points: '{{points}} pts',
},
ennoblements: {
columns: {
ennobledAt: 'Date/time',
oldOwner: 'Old owner',
newOwner: 'New owner',
},
},
};
export default translations;

View File

@ -3,10 +3,12 @@ import common from './common';
import indexPage from './index-page';
import playerPage from './player-page';
import tribePage from './tribe-page';
import villagePage from './village-page';
const translations = {
[NAMESPACES.SERVER_PAGE.COMMON]: common,
[NAMESPACES.SERVER_PAGE.INDEX_PAGE]: indexPage,
[NAMESPACES.SERVER_PAGE.VILLAGE_PAGE.INDEX_PAGE]: villagePage,
...playerPage,
...tribePage,
};

View File

@ -0,0 +1,16 @@
const translations = {
title: '{{name}} - Profil wioski - {{key}}',
villageNotFound: 'Wioska nie została znaleziona',
pageLayout: {
points: '{{points}} pkt',
},
ennoblements: {
columns: {
ennobledAt: 'Data',
oldOwner: 'Stary właściciel',
newOwner: 'Nowy właściciel',
},
},
};
export default translations;

View File

@ -1,26 +1,21 @@
import { useRef, useEffect } from 'react';
import { useEffect } from 'react';
import { NAME } from '@config/app';
export interface UseTitleOptions {
restoreOnUnmount?: boolean;
skip?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
restoreOnUnmount: false,
skip: false,
};
function useTitle(
title: string,
options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS
) {
const prevTitle = useRef(document.title);
useEffect(() => {
if (options.skip) return;
document.title = `${title} | ${NAME}`;
if (options && options.restoreOnUnmount) {
return () => {
document.title = prevTitle.current;
};
}
return () => {
document.title = NAME;
};