[WIP]: /server/:key/player/:id

- show player's servers and name changes
This commit is contained in:
Dawid Wysokiński 2020-12-04 20:10:02 +01:00
parent ece717a421
commit bec5f6bb22
10 changed files with 276 additions and 131 deletions

View File

@ -1,10 +1,12 @@
import React from 'react';
import { get, isString, isNumber } from 'lodash';
import { format } from 'date-fns';
import { Action, Column } from './types';
import { DATE_FORMAT } from '@config/app';
import { TableRow, TableCell, Checkbox, Tooltip } from '@material-ui/core';
import { Action, Column } from './types';
export interface Props<T> {
actions: Action[];
columns: Column<T>[];
@ -35,10 +37,13 @@ function EnhancedTableRow<T extends object>({
type: 'datetime' | 'date' | 'normal'
) => {
if ((isString(v) || isNumber(v)) && type === 'date') {
return format(new Date(v), 'yyyy-MM-dd');
return format(new Date(v), DATE_FORMAT.DAY_MONTH_AND_YEAR);
}
if ((isString(v) || isNumber(v)) && type === 'datetime') {
return format(new Date(v), 'yyyy-MM-dd HH:mm:ss');
return format(
new Date(v),
DATE_FORMAT.HOUR_MINUTES_SECONDS_DAY_MONTH_AND_YEAR
);
}
return v;
};

View File

@ -12,3 +12,10 @@ export const SERVER_STATUS = {
export const TWHELP = process.env.TWHelp ?? 'https://tribalwarshelp.com';
export const AUTHOR = 'Dawid Wysokiński';
export const DATE_FORMAT = {
MONTH_AND_YEAR: 'yyyy-MM',
DAY_MONTH_AND_YEAR: 'yyyy-MM-dd',
HOUR_MINUTES_DAY_MONTH_AND_YEAR: 'yyyy-MM-dd HH:mm',
HOUR_MINUTES_SECONDS_DAY_MONTH_AND_YEAR: 'yyyy-MM-dd HH:mm',
};

View File

@ -21,57 +21,6 @@ import ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout'
import background from './profile-background-dark.png';
const useStyles = makeStyles(theme => ({
header: {
width: '100%',
minHeight: theme.spacing(30),
backgroundPosition: 'center',
backgroundImage: `url(${background})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
display: 'flex',
justifyContent: 'flex-end',
flexDirection: 'column',
boxShadow: theme.shadows[4],
},
playerNameContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'& > *:not(:last-child)': {
marginRight: theme.spacing(2),
},
[theme.breakpoints.down('xs')]: {
flexDirection: 'column',
'& > *': {
marginRight: 0,
marginBottom: theme.spacing(1),
},
},
},
toolbar: {
[theme.breakpoints.down('xs')]: {
textAlign: 'center',
},
},
chipContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
'& > *': {
margin: theme.spacing(0.5),
},
},
content: {
height: '100%',
padding: theme.spacing(3, 0),
'&.no-padding': {
padding: '0 0',
},
},
}));
export interface Props {
children: React.ReactNode;
}
@ -210,4 +159,56 @@ function PageLayout({ children }: Props) {
);
}
const useStyles = makeStyles(theme => ({
header: {
width: '100%',
minHeight: theme.spacing(30),
backgroundPosition: 'center',
backgroundImage: `url(${background})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
display: 'flex',
justifyContent: 'flex-end',
flexDirection: 'column',
boxShadow: theme.shadows[4],
},
playerNameContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'& > *:not(:last-child)': {
marginRight: theme.spacing(2),
},
[theme.breakpoints.down('xs')]: {
flexDirection: 'column',
'& > *': {
marginRight: 0,
marginBottom: theme.spacing(1),
},
},
},
toolbar: {
[theme.breakpoints.down('xs')]: {
textAlign: 'center',
},
},
chipContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
'& > *': {
margin: theme.spacing(0.5),
},
},
content: {
height: '100%',
padding: theme.spacing(3, 0),
'&.no-padding': {
padding: '0 0',
},
},
}));
export default PageLayout;

View File

@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
import useTitle from '@libs/useTitle';
import useServer from '@features/ServerPage/libs/ServerContext/useServer';
import usePlayer from '../../libs/PlayerPageContext/usePlayer';
import { DATE_FORMAT } from '@config/app';
import { SERVER_PAGE } from '@config/namespaces';
import * as ROUTES from '@config/routes';
import { makeStyles } from '@material-ui/core/styles';
import {
@ -14,9 +16,12 @@ import {
Card,
CardContent,
Typography,
Chip,
} from '@material-ui/core';
import Link from '@common/Link/Link';
import PageLayout from '../../common/PageLayout/PageLayout';
import Statistics from './components/Statistics/Statistics';
import NameChanges from './components/NameChanges/NameChanges';
function IndexPage() {
const classes = useStyles();
@ -29,13 +34,16 @@ function IndexPage() {
<PageLayout>
<Container>
<Grid container spacing={2}>
<Grid component={Hidden} smDown implementation="css" item xs={12}>
<Grid component={Hidden} xsDown implementation="css" item xs={12}>
<Statistics server={key} playerID={player.id} t={t} />
</Grid>
{[
{
field: 'joinedAt',
value: format(new Date(player.joinedAt), 'yyyy-MM-dd HH:mm'),
value: format(
new Date(player.joinedAt),
DATE_FORMAT.DAY_MONTH_AND_YEAR
),
},
{
field: 'points',
@ -70,25 +78,34 @@ function IndexPage() {
{
field: 'deletedAt',
value: player.deletedAt
? format(new Date(player.deletedAt), 'yyyy-MM-dd HH:mm')
? format(
new Date(player.deletedAt),
DATE_FORMAT.DAY_MONTH_AND_YEAR
)
: '-',
},
{
field: 'bestRank',
subtitle: format(new Date(player.bestRankAt), 'yyyy-MM-dd HH:mm'),
subtitle: format(
new Date(player.bestRankAt),
DATE_FORMAT.HOUR_MINUTES_DAY_MONTH_AND_YEAR
),
value: `${player.bestRank.toString()}`,
},
{
field: 'mostPoints',
subtitle: format(
new Date(player.mostPointsAt),
'yyyy-MM-dd HH:mm'
DATE_FORMAT.HOUR_MINUTES_DAY_MONTH_AND_YEAR
),
value: `${player.mostPoints.toLocaleString()}`,
},
{
field: 'mostVillages',
subtitle: format(new Date(player.bestRankAt), 'yyyy-MM-dd HH:mm'),
subtitle: format(
new Date(player.bestRankAt),
DATE_FORMAT.HOUR_MINUTES_DAY_MONTH_AND_YEAR
),
value: `${player.mostVillages.toLocaleString()}`,
},
].map(({ field, value, subtitle }) => {
@ -100,7 +117,7 @@ function IndexPage() {
{t('fields.' + field)}
<br />
{subtitle && (
<Typography variant="subtitle1" component="span">
<Typography variant="subtitle2" component="span">
{subtitle}
</Typography>
)}
@ -111,19 +128,56 @@ function IndexPage() {
</Grid>
);
})}
<Grid item xs={12} md={4}>
<Card>
<CardContent>
<Typography variant="h5" gutterBottom>
{t('fields.servers')}
</Typography>
<div className={classes.serverContainer}>
{[...player.servers].sort().map(server => {
return (
<Link
key={server}
to={ROUTES.SERVER_PAGE.PLAYER_PAGE.INDEX_PAGE}
params={{ key: server, id: player.id }}
>
<Chip
className={classes.chip}
color="secondary"
label={server}
clickable
/>
</Link>
);
})}
</div>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={8}>
<NameChanges t={t} nameChanges={player.nameChanges} />
</Grid>
</Grid>
</Container>
</PageLayout>
);
}
const useStyles = makeStyles(() => ({
const useStyles = makeStyles(theme => ({
card: {
height: '100%',
},
cardContent: {
height: '100%',
},
serverContainer: {
textAlign: 'justify',
},
chip: {
margin: theme.spacing(0.5),
cursor: 'pointer',
},
}));
export default IndexPage;

View File

@ -0,0 +1,48 @@
import React from 'react';
import { Card, CardContent, Typography } from '@material-ui/core';
import Table from '@common/Table/Table';
import { NameChange } from '../../../../libs/PlayerPageContext/types';
import { TFunction } from 'i18next';
export interface Props {
t: TFunction;
nameChanges: NameChange[];
}
function NameChanges({ t, nameChanges }: Props) {
return (
<Card style={{ height: '100%' }}>
<CardContent>
<Typography variant="h5">{t('nameChanges.title')}</Typography>
</CardContent>
<Table
columns={[
{
field: 'changeDate',
label: t('nameChanges.columns.changeDate'),
type: 'date',
},
{
field: 'newName',
label: t('nameChanges.columns.newName'),
},
{
field: 'oldName',
label: t('nameChanges.columns.oldName'),
},
]}
data={nameChanges}
size="small"
hideFooter
footerProps={{
rowsPerPage: nameChanges.length,
rowsPerPageOptions: [nameChanges.length],
}}
/>
</Card>
);
}
export default NameChanges;

View File

@ -3,12 +3,14 @@ import { useQuery } from '@apollo/client';
import { PLAYER_HISTORY } from './queries';
import { LIMIT } from './constants';
import { Paper } from '@material-ui/core';
import { Serie } from '@nivo/line';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { Paper, useMediaQuery } from '@material-ui/core';
import { Skeleton } from '@material-ui/lab';
import LineChart from '@common/Chart/LineChart';
import ModeSelector from '@features/ServerPage/common/ModeSelector/ModeSelector';
import { TFunction } from 'i18next';
import { Serie } from '@nivo/line';
import { PlayerHistoryQueryVariables } from '@libs/graphql/types';
import { Mode, PlayerHistory } from './types';
@ -20,7 +22,9 @@ export interface Props {
function Statistics({ t, server, playerID }: Props) {
const [mode, setMode] = useState<Mode>('points');
const { loading: loadingData, data: queryRes } = useQuery<
const theme = useTheme();
const isMobileDevice = useMediaQuery(theme.breakpoints.down('sm'));
const { loading, data: queryRes } = useQuery<
PlayerHistory,
PlayerHistoryQueryVariables
>(PLAYER_HISTORY, {
@ -35,10 +39,12 @@ function Statistics({ t, server, playerID }: Props) {
},
});
const items = useMemo(
() => [...(queryRes?.playerHistory?.items ?? [])].reverse(),
[queryRes]
() =>
[...(queryRes?.playerHistory?.items ?? [])]
.slice(0, isMobileDevice ? 20 : undefined)
.reverse(),
[queryRes, isMobileDevice]
);
const loading = loadingData && items.length === 0;
const data = useMemo<Serie[]>(() => {
if (loading) return [];
return [
@ -101,63 +107,67 @@ function Statistics({ t, server, playerID }: Props) {
},
]}
/>
<div style={{ height: '300px' }}>
<LineChart
data={data}
margin={{ top: 20, right: 90, bottom: 50, left: 85 }}
xScale={{
type: 'time',
precision: 'day',
}}
yScale={{
type: 'linear',
min: 'auto',
max: 'auto',
stacked: true,
reverse: false,
}}
xFormat="time:%Y-%m-%d"
axisBottom={{
tickSize: 5,
tickValues: 5,
tickPadding: 5,
tickRotation: 0,
format: '%Y-%m-%d',
}}
axisLeft={{
legendOffset: -42,
legendPosition: 'middle',
tickSize: 0,
tickPadding: 4,
format: (v: string | number | Date) => v.toLocaleString(),
}}
pointSize={10}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
colors={{ scheme: 'nivo' }}
yFormat={(v: string | number | Date) => v.toLocaleString()}
legends={[
{
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 90,
translateY: 0,
itemsSpacing: 0,
itemDirection: 'left-to-right',
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
symbolBorderColor: 'rgba(0, 0, 0, .5)',
},
]}
/>
</div>
{loading ? (
<Skeleton height={300} variant="rect" />
) : (
<div style={{ height: '300px' }}>
<LineChart
data={data}
margin={{ top: 20, right: 90, bottom: 50, left: 85 }}
xScale={{
type: 'time',
precision: 'day',
}}
yScale={{
type: 'linear',
min: 'auto',
max: 'auto',
stacked: true,
reverse: false,
}}
xFormat="time:%Y-%m-%d"
axisBottom={{
tickSize: 5,
tickValues: 5,
tickPadding: 5,
tickRotation: 0,
format: '%Y-%m-%d',
}}
axisLeft={{
legendOffset: -42,
legendPosition: 'middle',
tickSize: 0,
tickPadding: 4,
format: (v: string | number | Date) => v.toLocaleString(),
}}
pointSize={10}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
colors={{ scheme: 'nivo' }}
yFormat={(v: string | number | Date) => v.toLocaleString()}
legends={[
{
anchor: 'bottom-right',
direction: 'column',
justify: false,
translateX: 90,
translateY: 0,
itemsSpacing: 0,
itemDirection: 'left-to-right',
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
symbolBorderColor: 'rgba(0, 0, 0, .5)',
},
]}
/>
</div>
)}
</Paper>
);
}

View File

@ -24,7 +24,7 @@ function Provider({ children }: Props) {
PlayerQueryResult,
PlayerQueryVariables
>(PLAYER, {
fetchPolicy: 'cache-first',
fetchPolicy: 'cache-and-network',
variables: {
id: parseInt(id, 10),
server: key,

View File

@ -1,3 +1,9 @@
export type NameChange = {
oldName: string;
newName: string;
changeDate: Date | string;
};
export type Player = {
id: number;
name: string;
@ -23,11 +29,7 @@ export type Player = {
joinedAt: Date | string;
deletedAt?: Date | string;
servers: string[];
nameChanges: {
oldName: string;
newName: string;
changeDate: Date | string;
}[];
nameChanges: NameChange[];
tribe?: {
id: number;
tag: string;

View File

@ -23,6 +23,15 @@ const translations = {
bestRank: 'Best rank',
mostPoints: 'Most points',
mostVillages: 'Most villages',
servers: 'Servers',
},
nameChanges: {
title: 'Name changes',
columns: {
changeDate: 'Date',
newName: 'New name',
oldName: 'Old name',
},
},
};

View File

@ -23,6 +23,15 @@ const translations = {
bestRank: 'Najlepszy ranking',
mostPoints: 'Najwięcej punktów',
mostVillages: 'Najwięcej wiosek',
servers: 'Serwery',
},
nameChanges: {
title: 'Zmiany nicków',
columns: {
changeDate: 'Data',
newName: 'Nowy nick',
oldName: 'Stary nick',
},
},
};