servicepoint-tanks/tank-frontend/src/MapChooser.tsx
2024-05-07 13:21:10 +02:00

119 lines
3.9 KiB
TypeScript

import {useState} from 'react';
import {useMutation, useQuery} from '@tanstack/react-query';
import {makeApiUrl, MapInfo} from './serverCalls';
import Dialog from './components/Dialog.tsx';
import PixelGridCanvas from './components/PixelGridCanvas.tsx';
import Column from './components/Column.tsx';
import Button from './components/Button.tsx';
import Row from './components/Row.tsx';
import './MapChooser.css';
function base64ToArrayBuffer(base64: string) {
const binaryString = atob(base64);
const bytes = new Uint8ClampedArray(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function MapPreview({mapName, highlight, onClick}: {
readonly mapName: string,
readonly highlight: boolean,
readonly onClick: () => void
}) {
const query = useQuery({
queryKey: ['get-map', mapName],
queryFn: async () => {
const url = makeApiUrl(`/map/${mapName}`);
const response = await fetch(url, {method: 'GET'});
if (!response.ok)
throw new Error(`response failed with code ${response.status} (${response.status})${await response.text()}`);
return await response.json() as MapInfo;
}
});
if (query.isError)
return <p>{query.error.message}</p>;
else if (query.isPending)
return <p>loading...</p>;
const {name, preview} = query.data;
return <Column
key={mapName}
className={'MapChooser-Preview' + (highlight ? ' MapChooser-Preview-Highlight' : '')}
onClick={onClick}
>
<PixelGridCanvas pixels={base64ToArrayBuffer(preview)}/>
<p>{name}</p>
</Column>;
}
function MapChooserDialog({mapNames, onClose, onConfirm}: {
readonly mapNames: string[];
readonly onConfirm: (mapName: string) => void;
readonly onClose: () => void;
}) {
const [chosenMap, setChosenMap] = useState<string>();
return <Dialog title='Choose a map' onClose={onClose}>
<Row className="MapChooser-Row overflow-scroll">
{mapNames.map(name => <MapPreview
key={name}
mapName={name}
highlight={chosenMap == name}
onClick={() => setChosenMap(name)}
/>)}
</Row>
<Row>
<div className="flex-grow"/>
<Button text="« cancel" onClick={onClose}/>
<Button text="√ confirm" disabled={!chosenMap} onClick={() => chosenMap && onConfirm(chosenMap)}/>
</Row>
</Dialog>;
}
export default function MapChooser({}: {}) {
const query = useQuery({
queryKey: ['get-maps'],
queryFn: async () => {
const url = makeApiUrl('/map');
const response = await fetch(url, {method: 'GET'});
if (!response.ok)
throw new Error(`response failed with code ${response.status} (${response.status})${await response.text()}`);
return await response.json() as string[];
}
});
const postMap = useMutation({
retry: false,
mutationFn: async (map: string) => {
const url = makeApiUrl('/map');
url.searchParams.set('name', map);
const response = await fetch(url, {method: 'POST'});
if (!response.ok)
throw new Error(`${response.status} (${response.statusText}): ${await response.text()}`);
}
});
const [open, setOpen] = useState(false);
return <>
<Button text="▓ Change map"
disabled={!query.isSuccess || postMap.isPending}
onClick={() => setOpen(true)}/>
{query.isSuccess && open &&
<MapChooserDialog
mapNames={query.data!}
onClose={() => setOpen(false)}
onConfirm={name => {
setOpen(false);
postMap.mutate(name);
}}
/>
}
</>;
}