[WIP]: /server/:key/player/:id
- show player's servers and name changes
This commit is contained in:
parent
ece717a421
commit
bec5f6bb22
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Reference in New Issue
Block a user