Added token edit modal, refactored map and token data providers

Refactored image drop code into reusable component as well
This commit is contained in:
Mitchell McCaffrey
2020-05-19 16:21:01 +10:00
parent 1774b459dc
commit 7b98370e4c
17 changed files with 567 additions and 238 deletions

View File

@@ -1,32 +1,18 @@
import React, { useRef, useState, useEffect, useContext } from "react";
import { Box, Button, Flex, Label, Text } from "theme-ui";
import React, { useRef, useState, useContext } from "react";
import { Button, Flex, Label } from "theme-ui";
import shortid from "shortid";
import Modal from "../components/Modal";
import MapTiles from "../components/map/MapTiles";
import MapSettings from "../components/map/MapSettings";
import ImageDrop from "../components/ImageDrop";
import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
import usePrevious from "../helpers/usePrevious";
import blobToBuffer from "../helpers/blobToBuffer";
import { maps as defaultMaps } from "../maps";
import MapDataContext from "../contexts/MapDataContext";
import AuthContext from "../contexts/AuthContext";
const defaultMapSize = 22;
const defaultMapState = {
tokens: {},
// An index into the draw actions array to which only actions before the
// index will be performed (used in undo and redo)
mapDrawActionIndex: -1,
mapDrawActions: [],
fogDrawActionIndex: -1,
fogDrawActions: [],
// Flags to determine what other people can edit
editFlags: ["drawing", "tokens"],
};
const defaultMapProps = {
// Grid type
// TODO: add support for hex horizontal and hex vertical
@@ -42,68 +28,26 @@ function SelectMapModal({
// The map currently being view in the map screen
currentMap,
}) {
const { database } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const wasOpen = usePrevious(isOpen);
const {
maps,
mapStates,
addMap,
removeMap,
resetMap,
updateMap,
updateMapState,
} = useContext(MapDataContext);
const [imageLoading, setImageLoading] = useState(false);
// The map selected in the modal
const [selectedMap, setSelectedMap] = useState(null);
const [selectedMapState, setSelectedMapState] = useState(null);
const [maps, setMaps] = useState([]);
// Load maps from the database and ensure state is properly setup
useEffect(() => {
if (!userId || !database) {
return;
}
async function getDefaultMaps() {
const defaultMapsWithIds = [];
for (let i = 0; i < defaultMaps.length; i++) {
const defaultMap = defaultMaps[i];
const id = `__default-${defaultMap.name}`;
defaultMapsWithIds.push({
...defaultMap,
id,
owner: userId,
// Emulate the time increasing to avoid sort errors
created: Date.now() + i,
lastModified: Date.now() + i,
...defaultMapProps,
});
// Add a state for the map if there isn't one already
const state = await database.table("states").get(id);
if (!state) {
await database.table("states").add({ ...defaultMapState, mapId: id });
}
}
return defaultMapsWithIds;
}
const [selectedMapId, setSelectedMapId] = useState(null);
async function loadMaps() {
let storedMaps = await database
.table("maps")
.where({ owner: userId })
.toArray();
const sortedMaps = storedMaps.sort((a, b) => b.created - a.created);
const defaultMapsWithIds = await getDefaultMaps();
const allMaps = [...sortedMaps, ...defaultMapsWithIds];
setMaps(allMaps);
// reload map state as is may have changed while the modal was closed
if (selectedMap) {
const state = await database.table("states").get(selectedMap.id);
if (state) {
setSelectedMapState(state);
}
}
}
if (!wasOpen && isOpen) {
loadMaps();
}
}, [userId, database, isOpen, wasOpen, selectedMap]);
const selectedMap = maps.find((map) => map.id === selectedMapId);
const selectedMapState = mapStates.find(
(state) => state.mapId === selectedMapId
);
const fileInputRef = useRef();
@@ -180,108 +124,55 @@ function SelectMapModal({
}
async function handleMapAdd(map) {
await database.table("maps").add(map);
const state = { ...defaultMapState, mapId: map.id };
await database.table("states").add(state);
setMaps((prevMaps) => [map, ...prevMaps]);
setSelectedMap(map);
setSelectedMapState(state);
await addMap(map);
setSelectedMapId(map.id);
}
async function handleMapRemove(id) {
await database.table("maps").delete(id);
await database.table("states").delete(id);
setMaps((prevMaps) => {
const filtered = prevMaps.filter((map) => map.id !== id);
setSelectedMap(filtered[0]);
database.table("states").get(filtered[0].id).then(setSelectedMapState);
return filtered;
});
await removeMap(id);
setSelectedMapId(null);
// Removed the map from the map screen if needed
if (currentMap && currentMap.id === selectedMap.id) {
if (currentMap && currentMap.id === selectedMapId) {
onMapChange(null, null);
}
}
async function handleMapSelect(map) {
const state = await database.table("states").get(map.id);
setSelectedMapState(state);
setSelectedMap(map);
function handleMapSelect(map) {
setSelectedMapId(map.id);
}
async function handleMapReset(id) {
const state = { ...defaultMapState, mapId: id };
await database.table("states").put(state);
setSelectedMapState(state);
const newState = await resetMap(id);
// Reset the state of the current map if needed
if (currentMap && currentMap.id === selectedMap.id) {
onMapStateChange(state);
if (currentMap && currentMap.id === selectedMapId) {
onMapStateChange(newState);
}
}
async function handleSubmit(e) {
e.preventDefault();
if (selectedMap) {
async function handleDone() {
if (selectedMapId) {
onMapChange(selectedMap, selectedMapState);
onDone();
}
onDone();
}
/**
* Drag and Drop
*/
const [dragging, setDragging] = useState(false);
function handleImageDragEnter(event) {
event.preventDefault();
event.stopPropagation();
setDragging(true);
}
function handleImageDragLeave(event) {
event.preventDefault();
event.stopPropagation();
setDragging(false);
}
function handleImageDrop(event) {
event.preventDefault();
event.stopPropagation();
const file = event.dataTransfer.files[0];
if (file && file.type.startsWith("image")) {
handleImageUpload(file);
}
setDragging(false);
}
/**
* Map settings
*/
const [showMoreSettings, setShowMoreSettings] = useState(false);
async function handleMapSettingsChange(key, value) {
const change = { [key]: value, lastModified: Date.now() };
database.table("maps").update(selectedMap.id, change);
const newMap = { ...selectedMap, ...change };
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const i = newMaps.findIndex((map) => map.id === selectedMap.id);
if (i > -1) {
newMaps[i] = newMap;
}
return newMaps;
});
setSelectedMap(newMap);
await updateMap(selectedMapId, { [key]: value });
}
async function handleMapStateSettingsChange(key, value) {
database.table("states").update(selectedMap.id, { [key]: value });
setSelectedMapState((prevState) => ({ ...prevState, [key]: value }));
await updateMapState(selectedMapId, { [key]: value });
}
return (
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
<Box as="form" onSubmit={handleSubmit} onDragEnter={handleImageDragEnter}>
<ImageDrop onDrop={handleImageUpload} dropText="Drop map to upload">
<input
onChange={(event) => handleImageUpload(event.target.files[0])}
type="file"
@@ -305,7 +196,7 @@ function SelectMapModal({
selectedMapState={selectedMapState}
onMapSelect={handleMapSelect}
onMapReset={handleMapReset}
onSubmit={handleSubmit}
onDone={handleDone}
/>
<MapSettings
map={selectedMap}
@@ -315,35 +206,15 @@ function SelectMapModal({
showMore={showMoreSettings}
onShowMoreChange={setShowMoreSettings}
/>
<Button variant="primary" disabled={imageLoading}>
<Button
variant="primary"
disabled={imageLoading}
onClick={handleDone}
>
Done
</Button>
{dragging && (
<Flex
bg="muted"
sx={{
position: "absolute",
top: 0,
right: 0,
left: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
cursor: "copy",
}}
onDragLeave={handleImageDragLeave}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
}}
onDrop={handleImageDrop}
>
<Text sx={{ pointerEvents: "none" }}>Drop map to upload</Text>
</Flex>
)}
</Flex>
</Box>
</ImageDrop>
</Modal>
);
}