119 lines
3.9 KiB
TypeScript
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);
|
|
}}
|
|
/>
|
|
}
|
|
</>;
|
|
}
|