This repository has been archived on 2022-09-04. You can view files and clone it, but cannot push or open issues or pull requests.
version.tribalwarshelp.com/src/features/ServerPage/features/MapPage/MapPage.tsx

478 lines
16 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import {
useQueryParams,
NumberParam,
withDefault,
BooleanParam,
} from 'use-query-params';
import { useTranslation } from 'react-i18next';
import { useApolloClient } from '@apollo/client';
import useTitle from 'libs/useTitle';
import useServer from '../../libs/ServerContext/useServer';
import useMarkers from './useMarkers';
import ColorParam from 'libs/serialize-query-params/ColorParam';
import { MAP_SERVICE } from 'config/app';
import { SERVER_PAGE } from 'config/namespaces';
import { PLAYERS, TRIBES } from './queries';
import { makeStyles } from '@material-ui/core/styles';
import {
Typography,
Container,
Button,
Grid,
TextField,
Checkbox,
FormControlLabel,
} from '@material-ui/core';
import Content from 'common/Content/Content';
import ColorInput from 'common/Form/ColorInput';
import Spinner from 'common/Spinner/Spinner';
import Map from './components/Map/Map';
import MarkerField from './components/MarkerField/MarkerField';
import Card from './components/Card/Card';
import {
QueryPlayersArgs,
QueryTribesArgs,
Tribe,
Player,
Query,
} from 'libs/graphql/types';
import { Settings, HasID } from './types';
function MapPage() {
const [mapURL, setMapURL] = useState<string>('');
const [query, setQuery] = useQueryParams({
showBarbarian: withDefault(BooleanParam, false),
largerMarkers: withDefault(BooleanParam, false),
markersOnly: withDefault(BooleanParam, false),
centerX: withDefault(NumberParam, 500),
centerY: withDefault(NumberParam, 500),
scale: withDefault(NumberParam, 1),
showGrid: withDefault(BooleanParam, true),
showContinentNumbers: withDefault(BooleanParam, true),
backgroundColor: withDefault(ColorParam, '#000000'),
gridLineColor: withDefault(ColorParam, '#ffffff'),
continentNumberColor: withDefault(ColorParam, '#ffffff'),
playerVillageColor: withDefault(ColorParam, '#FF0000'),
barbarianVillageColor: withDefault(ColorParam, '#808080'),
});
const client = useApolloClient();
const { key } = useServer();
const {
markers: tribeMarkers,
createDeleteMarkerHandler: createDeleteTribeMarkerHandler,
createUpdateMarkerColorHandler: createUpdateTribeMarkerColorHandler,
createUpdateMarkerItemHandler: createUpdateTribeMarkerItemHandler,
handleAddMarker: handleAddTribeMarker,
loading: loadingTribeMarkers,
} = useMarkers<Tribe, QueryTribesArgs>(client, {
paramName: 'tribe',
query: TRIBES,
dataKey: 'tribes',
getVariables: ids => ({ server: key, filter: { id: ids, exists: true } }),
});
const selectedTribeIDs = useMemo(() => {
return tribeMarkers.filter(m => m.item).map(m => m.item?.id ?? 0);
}, [tribeMarkers]);
const {
markers: playerMarkers,
createDeleteMarkerHandler: createDeletePlayerMarkerHandler,
createUpdateMarkerColorHandler: createUpdatePlayerMarkerColorHandler,
createUpdateMarkerItemHandler: createUpdatePlayerMarkerItemHandler,
handleAddMarker: handleAddPlayerMarker,
loading: loadingPlayerMarkers,
} = useMarkers<Player, QueryPlayersArgs>(client, {
paramName: 'player',
query: PLAYERS,
dataKey: 'players',
getVariables: ids => ({ server: key, filter: { id: ids, exists: true } }),
});
const selectedPlayerIDs = useMemo(() => {
return playerMarkers.filter(m => m.item).map(m => m.item?.id ?? 0);
}, [playerMarkers]);
const classes = useStyles();
const { t } = useTranslation(SERVER_PAGE.MAP_PAGE);
useTitle(t('title', { key }));
const loading = loadingTribeMarkers || loadingPlayerMarkers;
const createChangeSettingsHandler = (key: keyof Settings) => (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
colorOrChecked?: string | boolean
) => {
setQuery({
[key]:
typeof colorOrChecked === 'boolean' ||
typeof colorOrChecked === 'string'
? colorOrChecked
: e.target.value,
});
};
const searchPlayers = async (searchValue: string): Promise<Player[]> => {
try {
const { data } = await client.query<
Pick<Query, 'players'>,
QueryPlayersArgs
>({
query: PLAYERS,
variables: {
limit: 10,
filter: {
exists: true,
nameIEQ: searchValue + '%',
idNEQ: selectedPlayerIDs,
},
server: key,
offset: 0,
sort: ['points DESC'],
},
fetchPolicy: 'network-only',
});
return data.players?.items ?? [];
} catch (error) {
return [];
}
};
const searchTribes = async (searchValue: string): Promise<Tribe[]> => {
try {
const { data } = await client.query<
Pick<Query, 'tribes'>,
QueryTribesArgs
>({
query: TRIBES,
variables: {
limit: 10,
filter: {
exists: true,
tagIEQ: searchValue + '%',
idNEQ: selectedTribeIDs,
},
server: key,
offset: 0,
sort: ['points DESC'],
},
fetchPolicy: 'network-only',
});
return data.tribes?.items ?? [];
} catch (error) {
return [];
}
};
const encodeMarker = (id: number, color: string): string => {
return encodeURIComponent(id + ',' + color);
};
const handleSubmit = (e: React.FormEvent<{}>) => {
e.preventDefault();
let queryString = '';
Object.entries(query).forEach(
([key, value]: [string, string | number | boolean]) => {
queryString += key + '=' + encodeURIComponent(value) + '&';
}
);
playerMarkers.forEach(marker => {
if (!marker.item) return;
queryString +=
'player=' + encodeMarker(marker.item.id, marker.color) + '&';
});
tribeMarkers.forEach(marker => {
if (!marker.item) return;
queryString +=
'tribe=' + encodeMarker(marker.item.id, marker.color) + '&';
});
setMapURL(MAP_SERVICE + '/' + key + '?' + queryString);
};
const getOptionSelected = (option: HasID, value: HasID) =>
option && value ? option.id === value.id : false;
const tribeGetOptionLabel = (tribe: Tribe) => (tribe ? tribe.tag : '');
const playerGetOptionLabel = (player: Player) => (player ? player.name : '');
const isDisabled = (id: number, blacklist: number[]) => {
return blacklist.some(id2 => id === id2);
};
if (loading) {
return (
<Spinner
containerProps={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
minHeight: 'inherit',
paddingY: 3,
}}
description={t('loading')}
/>
);
}
return (
<Content component="div" minHeight={false}>
<Container>
<form onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Card>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
{t('sections.settings')}
</Typography>
<div className={classes.formGroup}>
<TextField
label={t('inputLabels.zoomLevel')}
type="number"
name="scale"
value={query.scale}
onChange={createChangeSettingsHandler('scale')}
fullWidth
variant="standard"
inputProps={{
min: 1,
max: 5,
step: '.1',
}}
/>
<TextField
label={t('inputLabels.centerX')}
type="number"
name="centerX"
value={query.centerX}
onChange={createChangeSettingsHandler('centerX')}
fullWidth
variant="standard"
inputProps={{
min: 0,
max: 1000,
step: '1',
}}
/>
<TextField
label={t('inputLabels.centerY')}
type="number"
name="centerY"
value={query.centerY}
onChange={createChangeSettingsHandler('centerY')}
fullWidth
variant="standard"
inputProps={{
min: 0,
max: 1000,
step: '1',
}}
/>
{[
{
name: 'backgroundColor',
color: query.backgroundColor,
onChange: createChangeSettingsHandler('backgroundColor'),
},
{
name: 'playerVillageColor',
color: query.playerVillageColor,
onChange: createChangeSettingsHandler('playerVillageColor'),
},
{
name: 'barbarianVillageColor',
color: query.barbarianVillageColor,
onChange: createChangeSettingsHandler(
'barbarianVillageColor'
),
},
{
name: 'gridLineColor',
color: query.gridLineColor,
onChange: createChangeSettingsHandler('gridLineColor'),
},
{
name: 'continentNumberColor',
color: query.continentNumberColor,
onChange: createChangeSettingsHandler(
'continentNumberColor'
),
},
].map(({ color, name, onChange }) => (
<ColorInput
key={name}
color={color}
onChange={onChange}
fullWidth
variant="standard"
name={name}
label={t('inputLabels.' + name)}
/>
))}
{[
{
name: 'markersOnly',
checked: query.markersOnly,
onChange: createChangeSettingsHandler('markersOnly'),
},
{
name: 'showBarbarian',
checked: query.showBarbarian,
onChange: createChangeSettingsHandler('showBarbarian'),
},
{
name: 'largerMarkers',
checked: query.largerMarkers,
onChange: createChangeSettingsHandler('largerMarkers'),
},
{
name: 'showGrid',
checked: query.showGrid,
onChange: createChangeSettingsHandler('showGrid'),
},
{
name: 'showContinentNumbers',
checked: query.showContinentNumbers,
onChange: createChangeSettingsHandler(
'showContinentNumbers'
),
},
].map(({ name, checked, onChange }) => {
return (
<FormControlLabel
key={name}
label={t('inputLabels.' + name)}
control={
<Checkbox
name={name}
checked={checked}
onChange={onChange}
/>
}
/>
);
})}
</div>
</Card>
<Card>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
{t('sections.tribeMarkers')}
</Typography>
<div className={classes.formGroup}>
{tribeMarkers.map(marker => {
return (
<MarkerField
key={marker.id}
onDelete={createDeleteTribeMarkerHandler(marker.id)}
onChange={createUpdateTribeMarkerItemHandler(marker.id)}
onChangeColor={createUpdateTribeMarkerColorHandler(
marker.id
)}
loadingText={t('loading')}
noOptionsText={t('noOptions')}
loadSuggestions={searchTribes}
getOptionLabel={tribeGetOptionLabel}
getOptionSelected={getOptionSelected}
getOptionDisabled={opt =>
isDisabled(opt.id, selectedTribeIDs)
}
color={marker.color}
value={marker.item}
/>
);
})}
<Button
variant="contained"
fullWidth
color="primary"
onClick={handleAddTribeMarker}
disabled={tribeMarkers.length >= 25}
>
{t('buttons.addMarker')}
</Button>
</div>
</Card>
<Card>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
{t('sections.playerMarkers')}
</Typography>
<div className={classes.formGroup}>
{playerMarkers.map(marker => {
return (
<MarkerField
key={marker.id}
onDelete={createDeletePlayerMarkerHandler(marker.id)}
onChange={createUpdatePlayerMarkerItemHandler(marker.id)}
onChangeColor={createUpdatePlayerMarkerColorHandler(
marker.id
)}
noOptionsText={t('noOptions')}
loadSuggestions={searchPlayers}
getOptionLabel={playerGetOptionLabel}
getOptionSelected={getOptionSelected}
getOptionDisabled={opt =>
isDisabled(opt.id, selectedPlayerIDs)
}
color={marker.color}
value={marker.item}
/>
);
})}
<Button
variant="contained"
fullWidth
color="primary"
onClick={handleAddPlayerMarker}
disabled={playerMarkers.length >= 25}
>
{t('buttons.addMarker')}
</Button>
</div>
</Card>
<Grid item xs={12}>
<Typography align="center" component="div">
<Button
type="submit"
size="large"
color="primary"
variant="contained"
>
{t('buttons.generateMap')}
</Button>
</Typography>
</Grid>
{mapURL && (
<Grid item xs={12}>
<Map src={mapURL} alt={key} t={t} />
</Grid>
)}
</Grid>
</form>
</Container>
</Content>
);
}
const useStyles = makeStyles(theme => ({
formGroup: {
'& > *': {
marginBottom: theme.spacing(1),
},
},
}));
export default MapPage;