add a new page - MapPage
This commit is contained in:
parent
ec933ce417
commit
8dd4b1e1bf
|
@ -32,6 +32,7 @@
|
|||
"typescript": "^4.0.3",
|
||||
"use-debounce": "^5.0.1",
|
||||
"use-query-params": "^1.1.8",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -61,6 +62,7 @@
|
|||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"babel-plugin-module-resolver": "^4.0.0",
|
||||
"babel-plugin-transform-imports": "^2.0.0",
|
||||
"customize-cra": "^1.0.0",
|
||||
|
|
|
@ -12,6 +12,9 @@ export const SERVER_STATUS = {
|
|||
export const TWHELP =
|
||||
process.env.REACT_APP_TWHELP ?? 'https://tribalwarshelp.com';
|
||||
|
||||
export const MAP_SERVICE =
|
||||
process.env.REACT_APP_MAP_SERVICE ?? 'https://api.tribalwarshelp.com/map';
|
||||
|
||||
export const AUTHOR = 'Dawid Wysokiński';
|
||||
|
||||
export const DATE_FORMAT = {
|
||||
|
|
|
@ -8,6 +8,7 @@ import PlayerPage from './features/PlayerPage/PlayerPage';
|
|||
import TribePage from './features/TribePage/TribePage';
|
||||
import VillagePage from './features/VillagePage/VillagePage';
|
||||
import RankingPage from './features/RankingPage/RankingPage';
|
||||
import MapPage from './features/MapPage/MapPage';
|
||||
import NotFoundPage from '../NotFoundPage/NotFoundPage';
|
||||
|
||||
const EnhancedRoute = ({ children, ...rest }: RouteProps) => {
|
||||
|
@ -36,6 +37,9 @@ function ServerPage() {
|
|||
<EnhancedRoute path={SERVER_PAGE.RANKING_PAGE.BASE}>
|
||||
<RankingPage />
|
||||
</EnhancedRoute>
|
||||
<EnhancedRoute path={SERVER_PAGE.MAP_PAGE}>
|
||||
<MapPage />
|
||||
</EnhancedRoute>
|
||||
<Route path="*">
|
||||
<NotFoundPage />
|
||||
</Route>
|
||||
|
|
404
src/features/ServerPage/features/MapPage/MapPage.tsx
Normal file
404
src/features/ServerPage/features/MapPage/MapPage.tsx
Normal file
|
@ -0,0 +1,404 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import useServer from '../../libs/ServerContext/useServer';
|
||||
import useMarkers from './useMarkers';
|
||||
import { MAP_SERVICE } from '@config/app';
|
||||
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 ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout';
|
||||
import Map from './components/Map/Map';
|
||||
import MarkerField from './components/MarkerField/MarkerField';
|
||||
|
||||
import {
|
||||
PlayersQueryVariables,
|
||||
TribesQueryVariables,
|
||||
} from '@libs/graphql/types';
|
||||
import {
|
||||
Tribe,
|
||||
Player,
|
||||
PlayerList,
|
||||
TribeList,
|
||||
Settings,
|
||||
PlayerMarker,
|
||||
TribeMarker,
|
||||
} from './types';
|
||||
|
||||
function MapPage() {
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
showBarbarian: false,
|
||||
largerMarkers: false,
|
||||
markersOnly: false,
|
||||
centerX: 500,
|
||||
centerY: 500,
|
||||
scale: 1,
|
||||
showGrid: true,
|
||||
showContinentNumbers: true,
|
||||
backgroundColor: '#000000',
|
||||
gridLineColor: '#ffffff',
|
||||
continentNumberColor: '#ffffff',
|
||||
});
|
||||
const {
|
||||
markers: tribeMarkers,
|
||||
createDeleteMarkerHandler: createDeleteTribeMarkerHandler,
|
||||
createUpdateMarkerColorHandler: createUpdateTribeMarkerColorHandler,
|
||||
createUpdateMarkerItemHandler: createUpdateTribeMarkerItemHandler,
|
||||
handleAddMarker: handleAddTribeMarker,
|
||||
} = useMarkers<Tribe>();
|
||||
const {
|
||||
markers: playerMarkers,
|
||||
createDeleteMarkerHandler: createDeletePlayerMarkerHandler,
|
||||
createUpdateMarkerColorHandler: createUpdatePlayerMarkerColorHandler,
|
||||
createUpdateMarkerItemHandler: createUpdatePlayerMarkerItemHandler,
|
||||
handleAddMarker: handleAddPlayerMarker,
|
||||
} = useMarkers<Player>();
|
||||
const [mapURL, setMapURL] = useState<string>('');
|
||||
const { key } = useServer();
|
||||
const classes = useStyles();
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleSettingsChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
[e.target.name]:
|
||||
e.target.type === 'checkbox' && 'checked' in e.target
|
||||
? e.target.checked
|
||||
: e.target.value,
|
||||
});
|
||||
};
|
||||
const debouncedHandleSettingsChange = useDebouncedCallback(
|
||||
handleSettingsChange,
|
||||
500
|
||||
);
|
||||
|
||||
const callDebouncedHandleSettingsChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => {
|
||||
e.persist();
|
||||
debouncedHandleSettingsChange.callback(e);
|
||||
};
|
||||
|
||||
const searchPlayers = async (searchValue: string): Promise<Player[]> => {
|
||||
try {
|
||||
const { data } = await client.query<PlayerList, PlayersQueryVariables>({
|
||||
query: PLAYERS,
|
||||
variables: {
|
||||
limit: 10,
|
||||
filter: {
|
||||
exists: true,
|
||||
nameIEQ: searchValue + '%',
|
||||
},
|
||||
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<TribeList, TribesQueryVariables>({
|
||||
query: TRIBES,
|
||||
variables: {
|
||||
limit: 10,
|
||||
filter: {
|
||||
exists: true,
|
||||
tagIEQ: searchValue + '%',
|
||||
},
|
||||
server: key,
|
||||
offset: 0,
|
||||
sort: ['points DESC'],
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
return data.tribes?.items ?? [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const encodeMarker = (marker: PlayerMarker | TribeMarker): string => {
|
||||
return encodeURIComponent(marker.item?.id + ',' + marker.color);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<{}>) => {
|
||||
e.preventDefault();
|
||||
|
||||
let searchParams = '';
|
||||
Object.entries(settings).forEach(
|
||||
([key, value]: [string, string | number | boolean]) => {
|
||||
searchParams += key + '=' + encodeURIComponent(value) + '&';
|
||||
}
|
||||
);
|
||||
playerMarkers.forEach(marker => {
|
||||
if (!marker.item) return;
|
||||
searchParams += 'player=' + encodeMarker(marker) + '&';
|
||||
});
|
||||
tribeMarkers.forEach(marker => {
|
||||
if (!marker.item) return;
|
||||
searchParams += 'tribe=' + encodeMarker(marker) + '&';
|
||||
});
|
||||
|
||||
setMapURL(MAP_SERVICE + '/' + key + '?' + searchParams);
|
||||
};
|
||||
|
||||
const tribeGetOptionLabel = (tribe: Tribe) => (tribe ? tribe.tag : '');
|
||||
const tribeGetOptionSelected = (option: Tribe, value: Tribe) =>
|
||||
option && value ? option.tag === value.tag : false;
|
||||
const playerGetOptionLabel = (player: Player) => (player ? player.name : '');
|
||||
const playerGetOptionSelected = (option: Player, value: Player) =>
|
||||
option && value ? option.name === value.name : false;
|
||||
|
||||
return (
|
||||
<ServerPageLayout>
|
||||
<Container>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h3"
|
||||
align="center"
|
||||
gutterBottom
|
||||
>
|
||||
Settings
|
||||
</Typography>
|
||||
<div className={classes.formGroup}>
|
||||
<TextField
|
||||
label="Zoom level"
|
||||
type="number"
|
||||
name="scale"
|
||||
value={settings.scale}
|
||||
onChange={handleSettingsChange}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: '.01',
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Center X"
|
||||
type="number"
|
||||
name="centerX"
|
||||
value={settings.centerX}
|
||||
onChange={handleSettingsChange}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: '.01',
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Center Y"
|
||||
type="number"
|
||||
name="centerY"
|
||||
value={settings.centerY}
|
||||
onChange={handleSettingsChange}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: '.01',
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Background color"
|
||||
name="backgroundColor"
|
||||
onChange={callDebouncedHandleSettingsChange}
|
||||
fullWidth
|
||||
type="color"
|
||||
variant="standard"
|
||||
/>
|
||||
<TextField
|
||||
label="Grid line color"
|
||||
name="gridLineColor"
|
||||
onChange={callDebouncedHandleSettingsChange}
|
||||
defaultValue={settings.gridLineColor}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
type="color"
|
||||
/>
|
||||
<TextField
|
||||
label="Continent number color"
|
||||
name="continentNumberColor"
|
||||
onChange={callDebouncedHandleSettingsChange}
|
||||
defaultValue={settings.continentNumberColor}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
type="color"
|
||||
/>
|
||||
<FormControlLabel
|
||||
label="Markers only"
|
||||
control={
|
||||
<Checkbox
|
||||
name="markersOnly"
|
||||
checked={settings.markersOnly}
|
||||
onChange={handleSettingsChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label="Show barbarian villages"
|
||||
control={
|
||||
<Checkbox
|
||||
name="showBarbarian"
|
||||
checked={settings.showBarbarian}
|
||||
onChange={handleSettingsChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label="Larger markers"
|
||||
control={
|
||||
<Checkbox
|
||||
name="largerMarkers"
|
||||
checked={settings.largerMarkers}
|
||||
onChange={handleSettingsChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label="Continent grid"
|
||||
control={
|
||||
<Checkbox
|
||||
name="showGrid"
|
||||
checked={settings.showGrid}
|
||||
onChange={handleSettingsChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
label="Continent numbers"
|
||||
control={
|
||||
<Checkbox
|
||||
name="showContinentNumbers"
|
||||
checked={settings.showContinentNumbers}
|
||||
onChange={handleSettingsChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h3"
|
||||
align="center"
|
||||
gutterBottom
|
||||
>
|
||||
Tribe markers
|
||||
</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
|
||||
)}
|
||||
loadSuggestions={searchTribes}
|
||||
getOptionLabel={tribeGetOptionLabel}
|
||||
getOptionSelected={tribeGetOptionSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
color="secondary"
|
||||
onClick={handleAddTribeMarker}
|
||||
disabled={tribeMarkers.length >= 100}
|
||||
>
|
||||
Add marker
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h3"
|
||||
align="center"
|
||||
gutterBottom
|
||||
>
|
||||
Player markers
|
||||
</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
|
||||
)}
|
||||
loadSuggestions={searchPlayers}
|
||||
getOptionLabel={playerGetOptionLabel}
|
||||
getOptionSelected={playerGetOptionSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
color="secondary"
|
||||
onClick={handleAddPlayerMarker}
|
||||
disabled={playerMarkers.length >= 100}
|
||||
>
|
||||
Add marker
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography align="center" component="div">
|
||||
<Button
|
||||
type="submit"
|
||||
size="large"
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
>
|
||||
Generate new map
|
||||
</Button>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
{mapURL && <Map src={mapURL} alt={key} />}
|
||||
</Container>
|
||||
</ServerPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
formGroup: {
|
||||
'& > *': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default MapPage;
|
|
@ -0,0 +1,82 @@
|
|||
import React, { memo, useState, useEffect } from 'react';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import Spinner from '@common/Spinner/Spinner';
|
||||
|
||||
export interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
function Map({ src = '', alt = 'Map', maxWidth = 1000 }: Props) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{loading ? (
|
||||
<Alert severity="warning">It may take a while to generate a map!</Alert>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
URL:{' '}
|
||||
<Link className={classes.link} href={src}>
|
||||
{src}
|
||||
</Link>
|
||||
</Alert>
|
||||
)}
|
||||
<div
|
||||
className={classes.imageWrapper}
|
||||
style={{ maxWidth: `${maxWidth}px` }}
|
||||
>
|
||||
{loading && (
|
||||
<Spinner
|
||||
size={150}
|
||||
containerProps={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
display: loading ? 'none' : 'block',
|
||||
}}
|
||||
onLoad={() => setLoading(false)}
|
||||
className={classes.img}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
img: {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
link: {
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
container: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
imageWrapper: {
|
||||
marginTop: theme.spacing(1),
|
||||
margin: 'auto',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(Map);
|
|
@ -0,0 +1,96 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import useUpdateEffect from '@libs/useUpdateEffect';
|
||||
|
||||
import { TextField, Box, IconButton } from '@material-ui/core';
|
||||
import { Autocomplete } from '@material-ui/lab';
|
||||
import { Delete as DeleteIcon } from '@material-ui/icons';
|
||||
|
||||
export interface Props<T> {
|
||||
onDelete: () => void;
|
||||
onChange: (e: React.ChangeEvent<{}>, value: T | null) => void;
|
||||
onChangeColor: (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
) => void;
|
||||
getOptionLabel: (opt: T) => string;
|
||||
getOptionSelected: (opt: T, value: T) => boolean;
|
||||
loadSuggestions: (value: string) => Promise<T[]>;
|
||||
}
|
||||
|
||||
function MarkerField<T extends object>({
|
||||
onDelete,
|
||||
onChange,
|
||||
loadSuggestions,
|
||||
onChangeColor,
|
||||
getOptionSelected,
|
||||
getOptionLabel,
|
||||
}: Props<T>) {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [suggestions, setSuggestions] = useState<T[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const debouncedOnChangeColor = useDebouncedCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
|
||||
onChangeColor(e),
|
||||
500
|
||||
);
|
||||
const debouncedLoadSuggestions = useDebouncedCallback(
|
||||
(searchValue: string) => {
|
||||
setLoading(true);
|
||||
loadSuggestions(searchValue)
|
||||
.then(data => {
|
||||
setSuggestions(data);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
},
|
||||
1000
|
||||
);
|
||||
useUpdateEffect(() => {
|
||||
debouncedLoadSuggestions.callback(searchValue);
|
||||
}, [searchValue, debouncedLoadSuggestions]);
|
||||
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-end">
|
||||
<Autocomplete
|
||||
options={suggestions}
|
||||
getOptionLabel={getOptionLabel}
|
||||
fullWidth
|
||||
autoSelect
|
||||
autoHighlight
|
||||
loading={loading}
|
||||
getOptionSelected={getOptionSelected}
|
||||
onChange={onChange}
|
||||
renderInput={params => {
|
||||
return (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="standard"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<IconButton onClick={onDelete} size="small">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
type="text"
|
||||
name="value"
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{ width: '40%' }}
|
||||
type="color"
|
||||
name="color"
|
||||
variant="standard"
|
||||
onChange={e => {
|
||||
e.persist();
|
||||
debouncedOnChangeColor.callback(e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MarkerField;
|
47
src/features/ServerPage/features/MapPage/queries.ts
Normal file
47
src/features/ServerPage/features/MapPage/queries.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const PLAYERS = gql`
|
||||
query players(
|
||||
$server: String!
|
||||
$filter: PlayerFilter!
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$sort: [String!]
|
||||
) {
|
||||
players(
|
||||
server: $server
|
||||
filter: $filter
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
sort: $sort
|
||||
) {
|
||||
items {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TRIBES = gql`
|
||||
query tribes(
|
||||
$server: String!
|
||||
$filter: TribeFilter!
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$sort: [String!]
|
||||
) {
|
||||
tribes(
|
||||
server: $server
|
||||
filter: $filter
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
sort: $sort
|
||||
) {
|
||||
items {
|
||||
id
|
||||
tag
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
38
src/features/ServerPage/features/MapPage/types.ts
Normal file
38
src/features/ServerPage/features/MapPage/types.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { List } from '@libs/graphql/types';
|
||||
|
||||
export type Tribe = {
|
||||
id: number;
|
||||
tag: string;
|
||||
};
|
||||
export type TribeList = {
|
||||
tribes?: List<Tribe[]>;
|
||||
};
|
||||
export type Player = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
export type PlayerList = {
|
||||
players?: List<Player[]>;
|
||||
};
|
||||
|
||||
export type Marker<T = undefined> = {
|
||||
id: string;
|
||||
item?: T | null;
|
||||
color: string;
|
||||
};
|
||||
export type PlayerMarker = Marker<Player>;
|
||||
export type TribeMarker = Marker<Tribe>;
|
||||
|
||||
export type Settings = Object & {
|
||||
showBarbarian: boolean;
|
||||
largerMarkers: boolean;
|
||||
markersOnly: boolean;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
scale: number;
|
||||
showGrid: boolean;
|
||||
showContinentNumbers: boolean;
|
||||
backgroundColor: string;
|
||||
gridLineColor: string;
|
||||
continentNumberColor: string;
|
||||
};
|
75
src/features/ServerPage/features/MapPage/useMarkers.ts
Normal file
75
src/features/ServerPage/features/MapPage/useMarkers.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Marker } from './types';
|
||||
|
||||
export type MarkerBag<T> = {
|
||||
markers: Marker<T>[];
|
||||
handleAddMarker: () => void;
|
||||
createUpdateMarkerItemHandler: (
|
||||
id: string
|
||||
) => (e: React.ChangeEvent<{}>, value: T | null) => void;
|
||||
createUpdateMarkerColorHandler: (
|
||||
id: string
|
||||
) => (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
createDeleteMarkerHandler: (id: string) => () => void;
|
||||
};
|
||||
|
||||
const useMarkers = <T>(): MarkerBag<T> => {
|
||||
const [markers, setMarkers] = useState<Marker<T>[]>([]);
|
||||
|
||||
const getNewMarker = (): Marker<T> => ({
|
||||
id: uuidv4(),
|
||||
item: undefined,
|
||||
color: '#000000',
|
||||
});
|
||||
|
||||
const handleAddMarker = () => {
|
||||
setMarkers([...markers, getNewMarker()]);
|
||||
};
|
||||
|
||||
const createDeleteMarkerHandler = (id: string) => () => {
|
||||
setMarkers(markers.filter(marker => marker.id !== id));
|
||||
};
|
||||
|
||||
const createUpdateMarkerItemHandler = (id: string) => (
|
||||
e: React.ChangeEvent<{}>,
|
||||
item: T | null
|
||||
): void => {
|
||||
setMarkers(
|
||||
markers.map(marker => {
|
||||
if (marker.id !== id) return marker;
|
||||
if (item || item === null) {
|
||||
return {
|
||||
...marker,
|
||||
item,
|
||||
};
|
||||
}
|
||||
return marker;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const createUpdateMarkerColorHandler = (id: string) => (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
||||
): void => {
|
||||
setMarkers(
|
||||
markers.map(marker => {
|
||||
if (marker.id !== id) return marker;
|
||||
return {
|
||||
...marker,
|
||||
color: e.target.value,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
markers,
|
||||
handleAddMarker,
|
||||
createUpdateMarkerItemHandler,
|
||||
createUpdateMarkerColorHandler,
|
||||
createDeleteMarkerHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMarkers;
|
100
yarn.lock
100
yarn.lock
|
@ -1384,30 +1384,30 @@
|
|||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0 || ^17.0.0"
|
||||
|
||||
"@nivo/annotations@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.65.1.tgz#d9bdf6b66b636183191109446759c06cc2e778b2"
|
||||
integrity sha512-JNViguRGDm8vt6N5JhV/ugPsnesFuL826CWUnPfqkHj06eLn4fnsnGKBnOqxXwNSrV4GbJJBBfrzWOpz3drW/g==
|
||||
"@nivo/annotations@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.67.0.tgz#81f309d4427295a8afe847989480c4f3a71137d8"
|
||||
integrity sha512-Qfracwd9we2nWBaNOAp0Lnt9uc86yRFDcQI3jsOdjFdzDA4lRwhkD85Lq477w1XrU7mtpjeRGsaKTCJtaorWEg==
|
||||
dependencies:
|
||||
"@nivo/colors" "0.65.1"
|
||||
"@nivo/colors" "0.67.0"
|
||||
lodash "^4.17.11"
|
||||
react-spring "9.0.0-rc.3"
|
||||
|
||||
"@nivo/axes@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.65.1.tgz#ea9576fe9d7b9ac84430cc91e0b30af3ee9bba76"
|
||||
integrity sha512-H2/8wM9xwn/F0GH47IgLmPaw6TuLqRIIojfPvQNM1gWgJYaBsghPxGfrXF2f8URjwRpihptbrlDVmg3OwRykVQ==
|
||||
"@nivo/axes@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.67.0.tgz#5892a8c90f782e478015865f779b62f2e74b7665"
|
||||
integrity sha512-csqEgFoJQGoB5/TDSy7SXCy/2QSdrfSa51AMU/RhzpnZ35wNO/zHkEfP+ApvB8nuuwVuBNOShio80PNZOOD+Og==
|
||||
dependencies:
|
||||
"@nivo/scales" "0.65.0"
|
||||
"@nivo/scales" "0.67.0"
|
||||
d3-format "^1.4.4"
|
||||
d3-time "^1.0.11"
|
||||
d3-time-format "^2.1.3"
|
||||
react-spring "9.0.0-rc.3"
|
||||
|
||||
"@nivo/colors@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.65.1.tgz#d55ec085c991358d88e3bce7c94be468e58b3817"
|
||||
integrity sha512-1dU6fuHYUDbU2oqOA8jzlYI9qXOajgbwH2cs8s9XktHvC3rfxp6ZedqAI+FxqKuk9QFaAxcyFftYUVlpdsFFkQ==
|
||||
"@nivo/colors@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.67.0.tgz#5a2adc4b55406466efb8bbafecc08249e4e94e69"
|
||||
integrity sha512-y8x76SzQ4HYm6kkWYfBGQSE5fy/jtqGhCBjivlKsouU9Hv2G8g4a2JVMsfadJQHQIUjIsM/7Q0Y7zasFM9wkgA==
|
||||
dependencies:
|
||||
d3-color "^2.0.0"
|
||||
d3-scale "^3.0.0"
|
||||
|
@ -1415,10 +1415,10 @@
|
|||
lodash "^4.17.11"
|
||||
react-motion "^0.5.2"
|
||||
|
||||
"@nivo/core@^0.65.0":
|
||||
version "0.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.65.0.tgz#a356dcd740d7f70027edece36828185741fc3ff9"
|
||||
integrity sha512-9ag4Stx0aWkiIFArg6X+eTDzZEBJyx08UhIfxvUcMnkda8OcYlAPrnp2K7sV5d7egIrnsxBpDt2lx/cC7XL/pg==
|
||||
"@nivo/core@^0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.67.0.tgz#29f07b8bed2bcefb9402d14a9abe2895c5b2bfa2"
|
||||
integrity sha512-F8amsf/MnIuZLoN6ikla8bKlUkUI3HVhy6R2qMF2jDS5xnYch3Sno9OPTTuR8RnYGCr57yClnvFuJzw251GE6g==
|
||||
dependencies:
|
||||
d3-color "^2.0.0"
|
||||
d3-format "^1.4.4"
|
||||
|
@ -1433,49 +1433,49 @@
|
|||
recompose "^0.30.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
"@nivo/legends@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.65.1.tgz#6a49329a8a7939d889345da95974d8520c2d4ede"
|
||||
integrity sha512-ulJmhu9ZO5b5qp5YhMbEZ6+2PZfahd9jkXECSizwKyZ49FsTptiyLx8H4NTkmk2fmonhRt/W6IxEaUNG0/12Lg==
|
||||
"@nivo/legends@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.67.0.tgz#9cbe99d8929c6addafd8253f65b7e040d9d4bb6c"
|
||||
integrity sha512-W6JWMSiiAPsDhuofwq7x3ERA2fFJ2J/aj+Y3lvrZh5OAsG6/PCljwhlhMklNgYoN6bT/+Jgtut3Agdl0Dlzspg==
|
||||
dependencies:
|
||||
lodash "^4.17.11"
|
||||
recompose "^0.30.0"
|
||||
|
||||
"@nivo/line@^0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.65.1.tgz#9efced4734d6d16a89a1423069f81eb58a2b6103"
|
||||
integrity sha512-dcw3fxQeFE6NHLexYLKipvlOSG7/gUoHTt8mpcrjxQfThR3ugAjPW6O/1frVcrKYzouLxDTWpl9C7uWupgtzaQ==
|
||||
"@nivo/line@^0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.67.0.tgz#c9abde9e1ac4f758377775e1d266a40aedb5c1ea"
|
||||
integrity sha512-+iZpvVekl6GXZl+GnqL5kvtIhNl05tkcCJ6ajheVmOuzaFV7J9xBk8E3bFqNtFDM+vt4b0ggujW8zrlrZvrwUg==
|
||||
dependencies:
|
||||
"@nivo/annotations" "0.65.1"
|
||||
"@nivo/axes" "0.65.1"
|
||||
"@nivo/colors" "0.65.1"
|
||||
"@nivo/legends" "0.65.1"
|
||||
"@nivo/scales" "0.65.0"
|
||||
"@nivo/tooltip" "0.65.1"
|
||||
"@nivo/voronoi" "0.65.1"
|
||||
"@nivo/annotations" "0.67.0"
|
||||
"@nivo/axes" "0.67.0"
|
||||
"@nivo/colors" "0.67.0"
|
||||
"@nivo/legends" "0.67.0"
|
||||
"@nivo/scales" "0.67.0"
|
||||
"@nivo/tooltip" "0.67.0"
|
||||
"@nivo/voronoi" "0.67.0"
|
||||
d3-shape "^1.3.5"
|
||||
react-spring "9.0.0-rc.3"
|
||||
|
||||
"@nivo/scales@0.65.0":
|
||||
version "0.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.65.0.tgz#60eb790680b49d06396c403c08cfbef68fb7e82f"
|
||||
integrity sha512-0mRyvPJg3LGKkXraH8sQPvDVnLk2kmRNVjJKPRXWZ9ezMJUTfQb802p74/t/lo/AtAyscN7/sIfMsJ5ZJW4jkQ==
|
||||
"@nivo/scales@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.67.0.tgz#8e79e990ebd555bdce7c3dcdd73b8ad46cb0a74a"
|
||||
integrity sha512-hXDeh3GzjZA3CcvJp14vCp0/QeoYMxVC2dZl55rIJdW200M+ET/ZtJQA2W1mDOGQUxR9z1flFvGJA+EqXhThwQ==
|
||||
dependencies:
|
||||
d3-scale "^3.0.0"
|
||||
d3-time-format "^2.1.3"
|
||||
lodash "^4.17.11"
|
||||
|
||||
"@nivo/tooltip@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.65.1.tgz#7b3215e54dbfbaa2b4066b764f74c85137a8a576"
|
||||
integrity sha512-m08E1ZSoSUS7O8Bi/6ewXM87w+t09ESNobFL6YTRro1vhnBZ2g8MlglIkXBkEP/mYqfyUMzw6DzT/tOaA3Cnlg==
|
||||
"@nivo/tooltip@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.67.0.tgz#c849ab7a3ee82d88ff6376894a2de5ef266c8e8d"
|
||||
integrity sha512-Z4h4Ks/fFd7Cl2uSG/trfSYLXaiQNyFX2wGJrE/UPzDSpf4XqaW8uEBaK3p46200WG2AgltG0fHrouGNyMJ+1g==
|
||||
dependencies:
|
||||
react-spring "9.0.0-rc.3"
|
||||
|
||||
"@nivo/voronoi@0.65.1":
|
||||
version "0.65.1"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.65.1.tgz#6a3ff0bb4d60ce78e96cfe3052fde622743e3ec5"
|
||||
integrity sha512-9WUbLS4f9TQ3v3k8fhzKT9U0LOqKtGH4QcYhMNJxzJgsvb1wZavjPFd/CT+RsV7OD3zOWn/Nd7QEqeFLHwsJnA==
|
||||
"@nivo/voronoi@0.67.0":
|
||||
version "0.67.0"
|
||||
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.67.0.tgz#07ec873846ec12aa62d1c1884e274ea114b4341b"
|
||||
integrity sha512-Z+e+fa92xWIhbpSRdQGJtei3bBHTbLnsmXtfje+Xz8LYpkyxp8R/iTs9cqzQ4KILp1Tv6EnQYWrFWNizN3fNGA==
|
||||
dependencies:
|
||||
d3-delaunay "^5.1.1"
|
||||
d3-scale "^3.0.0"
|
||||
|
@ -2048,6 +2048,11 @@
|
|||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/uuid@^8.3.0":
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
|
||||
|
||||
"@types/webpack-sources@*":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.0.0.tgz#08216ab9be2be2e1499beaebc4d469cec81e82a7"
|
||||
|
@ -11849,6 +11854,11 @@ uuid@^8.3.0:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
|
||||
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
|
||||
|
||||
uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
|
||||
|
|
Reference in New Issue
Block a user