diff --git a/src/App.js b/src/App.js
index c666293..f3b0fa2 100644
--- a/src/App.js
+++ b/src/App.js
@@ -11,6 +11,8 @@ import ReleaseNotes from "./routes/ReleaseNotes";
import { AuthProvider } from "./contexts/AuthContext";
import { DatabaseProvider } from "./contexts/DatabaseContext";
+import { MapDataProvider } from "./contexts/MapDataContext";
+import { TokenDataProvider } from "./contexts/TokenDataContext";
function App() {
return (
@@ -29,7 +31,11 @@ function App() {
-
+
+
+
+
+
diff --git a/src/components/ImageDrop.js b/src/components/ImageDrop.js
new file mode 100644
index 0000000..da41987
--- /dev/null
+++ b/src/components/ImageDrop.js
@@ -0,0 +1,61 @@
+import React, { useState } from "react";
+import { Box, Flex, Text } from "theme-ui";
+
+function ImageDrop({ onDrop, dropText, children }) {
+ 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")) {
+ onDrop(file);
+ }
+ setDragging(false);
+ }
+
+ return (
+
+ {children}
+ {dragging && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ e.dataTransfer.dropEffect = "copy";
+ }}
+ onDrop={handleImageDrop}
+ >
+
+ {dropText || "Drop image to upload"}
+
+
+ )}
+
+ );
+}
+
+export default ImageDrop;
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index a45244f..9754c37 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -13,6 +13,7 @@ import useDataSource from "../../helpers/useDataSource";
import MapInteraction from "./MapInteraction";
import AuthContext from "../../contexts/AuthContext";
+import TokenDataContext from "../../contexts/TokenDataContext";
import { mapSources as defaultMapSources } from "../../maps";
@@ -22,7 +23,6 @@ const mapTokenMenuClassName = "map-token__menu";
function Map({
map,
mapState,
- tokens,
onMapTokenStateChange,
onMapTokenStateRemove,
onMapChange,
@@ -39,6 +39,8 @@ function Map({
loading,
}) {
const { userId } = useContext(AuthContext);
+ const { tokens } = useContext(TokenDataContext);
+
const mapSource = useDataSource(map, defaultMapSources);
function handleProxyDragEnd(isOnMap, tokenState) {
diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js
index 230d327..dcefccb 100644
--- a/src/components/map/MapSettings.js
+++ b/src/components/map/MapSettings.js
@@ -34,7 +34,7 @@ function MapSettings({
onChange={(e) =>
onSettingsChange("gridX", parseInt(e.target.value))
}
- disabled={map === null || map.type === "default"}
+ disabled={!map || map.type === "default"}
min={1}
my={1}
/>
@@ -48,7 +48,7 @@ function MapSettings({
onChange={(e) =>
onSettingsChange("gridY", parseInt(e.target.value))
}
- disabled={map === null || map.type === "default"}
+ disabled={!map || map.type === "default"}
min={1}
my={1}
/>
@@ -61,19 +61,15 @@ function MapSettings({
diff --git a/src/components/map/SelectMapButton.js b/src/components/map/SelectMapButton.js
index 2e9dc87..536323f 100644
--- a/src/components/map/SelectMapButton.js
+++ b/src/components/map/SelectMapButton.js
@@ -5,12 +5,12 @@ import SelectMapModal from "../../modals/SelectMapModal";
import SelectMapIcon from "../../icons/SelectMapIcon";
function SelectMapButton({ onMapChange, onMapStateChange, currentMap }) {
- const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
- setIsAddModalOpen(true);
+ setIsModalOpen(true);
}
function closeModal() {
- setIsAddModalOpen(false);
+ setIsModalOpen(false);
}
function handleDone() {
@@ -27,7 +27,7 @@ function SelectMapButton({ onMapChange, onMapStateChange, currentMap }) {
+
+
+
+
+ >
+ );
+}
+
+export default SelectTokensButton;
diff --git a/src/components/token/TokenTile.js b/src/components/token/TokenTile.js
new file mode 100644
index 0000000..1eb9e75
--- /dev/null
+++ b/src/components/token/TokenTile.js
@@ -0,0 +1,59 @@
+import React from "react";
+import { Flex, Image, Text } from "theme-ui";
+
+import useDataSource from "../../helpers/useDataSource";
+
+import { tokenSources as defaultTokenSources } from "../../tokens";
+
+function TokenTile({ token, isSelected }) {
+ const tokenSource = useDataSource(token, defaultTokenSources);
+
+ return (
+
+
+
+
+ {token.name}
+
+
+
+ );
+}
+
+export default TokenTile;
diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js
new file mode 100644
index 0000000..e9f77f1
--- /dev/null
+++ b/src/components/token/TokenTiles.js
@@ -0,0 +1,55 @@
+import React from "react";
+import { Flex } from "theme-ui";
+import SimpleBar from "simplebar-react";
+
+import AddIcon from "../../icons/AddIcon";
+
+import TokenTile from "./TokenTile";
+
+function TokenTiles({ tokens, onTokenAdd }) {
+ return (
+
+
+
+
+
+ {tokens.map((token) => (
+
+ ))}
+
+
+ );
+}
+
+export default TokenTiles;
diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js
index e4ae82a..20405f8 100644
--- a/src/components/token/Tokens.js
+++ b/src/components/token/Tokens.js
@@ -1,5 +1,5 @@
import React, { useState, useContext } from "react";
-import { Box } from "theme-ui";
+import { Box, Flex } from "theme-ui";
import shortid from "shortid";
import SimpleBar from "simplebar-react";
@@ -7,23 +7,27 @@ import ListToken from "./ListToken";
import ProxyToken from "./ProxyToken";
import NumberInput from "../NumberInput";
+import SelectTokensButton from "./SelectTokensButton";
+
import { fromEntries } from "../../helpers/shared";
import AuthContext from "../../contexts/AuthContext";
+import TokenDataContext from "../../contexts/TokenDataContext";
const listTokenClassName = "list-token";
-function Tokens({ onCreateMapTokenState, tokens }) {
- const [tokenSize, setTokenSize] = useState(1);
+function Tokens({ onMapTokenStateCreate }) {
const { userId } = useContext(AuthContext);
+ const { tokens } = useContext(TokenDataContext);
+
+ const [tokenSize, setTokenSize] = useState(1);
function handleProxyDragEnd(isOnMap, token) {
- if (isOnMap && onCreateMapTokenState) {
+ if (isOnMap && onMapTokenStateCreate) {
// Create a token state from the dragged token
- onCreateMapTokenState({
+ onMapTokenStateCreate({
id: shortid.generate(),
tokenId: token.id,
- type: token.type,
owner: userId,
size: tokenSize,
label: "",
@@ -44,7 +48,7 @@ function Tokens({ onCreateMapTokenState, tokens }) {
overflow: "hidden",
}}
>
-
+
{tokens.map((token) => (
))}
-
-
+
+ {/*
-
+ /> */}
+
{
+ 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,
+ gridType: "grid",
+ });
+ // 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;
+ }
+
+ 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);
+ const storedStates = await database.table("states").toArray();
+ setMapStates(storedStates);
+ }
+
+ loadMaps();
+ }, [userId, database]);
+
+ async function addMap(map) {
+ await database.table("maps").add(map);
+ const state = { ...defaultMapState, mapId: map.id };
+ await database.table("states").add(state);
+ setMaps((prevMaps) => [map, ...prevMaps]);
+ setMapStates((prevStates) => [state, ...prevStates]);
+ }
+
+ async function removeMap(id) {
+ await database.table("maps").delete(id);
+ await database.table("states").delete(id);
+ setMaps((prevMaps) => {
+ const filtered = prevMaps.filter((map) => map.id !== id);
+ return filtered;
+ });
+ setMapStates((prevMapsStates) => {
+ const filtered = prevMapsStates.filter((state) => state.mapId !== id);
+ return filtered;
+ });
+ }
+
+ async function resetMap(id) {
+ const state = { ...defaultMapState, mapId: id };
+ await database.table("states").put(state);
+ setMapStates((prevMapStates) => {
+ const newStates = [...prevMapStates];
+ const i = newStates.findIndex((state) => state.mapId === id);
+ if (i > -1) {
+ newStates[i] = state;
+ }
+ return newStates;
+ });
+ return state;
+ }
+
+ async function updateMap(id, update) {
+ const change = { ...update, lastModified: Date.now() };
+ await database.table("maps").update(id, change);
+ setMaps((prevMaps) => {
+ const newMaps = [...prevMaps];
+ const i = newMaps.findIndex((map) => map.id === id);
+ if (i > -1) {
+ newMaps[i] = { ...newMaps[i], ...change };
+ }
+ return newMaps;
+ });
+ }
+
+ async function updateMapState(id, update) {
+ await database.table("states").update(id, update);
+ setMapStates((prevMapStates) => {
+ const newStates = [...prevMapStates];
+ const i = newStates.findIndex((state) => state.mapId === id);
+ if (i > -1) {
+ newStates[i] = { ...newStates[i], ...update };
+ }
+ return newStates;
+ });
+ }
+
+ const value = {
+ maps,
+ mapStates,
+ addMap,
+ removeMap,
+ resetMap,
+ updateMap,
+ updateMapState,
+ };
+ return (
+ {children}
+ );
+}
+
+export default MapDataContext;
diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js
new file mode 100644
index 0000000..738d8e3
--- /dev/null
+++ b/src/contexts/TokenDataContext.js
@@ -0,0 +1,40 @@
+import React, { useEffect, useState, useContext } from "react";
+
+import AuthContext from "./AuthContext";
+import DatabaseContext from "./DatabaseContext";
+
+import { tokens as defaultTokens } from "../tokens";
+
+const TokenDataContext = React.createContext();
+
+export function TokenDataProvider({ children }) {
+ const { database } = useContext(DatabaseContext);
+ const { userId } = useContext(AuthContext);
+
+ const [tokens, setTokens] = useState([]);
+
+ useEffect(() => {
+ if (!userId) {
+ return;
+ }
+ const defaultTokensWithIds = [];
+ for (let defaultToken of defaultTokens) {
+ defaultTokensWithIds.push({
+ ...defaultToken,
+ id: `__default-${defaultToken.key}`,
+ owner: userId,
+ });
+ }
+ setTokens(defaultTokensWithIds);
+ }, [userId]);
+
+ const value = { tokens };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default TokenDataContext;
diff --git a/src/icons/SelectTokensIcon.js b/src/icons/SelectTokensIcon.js
new file mode 100644
index 0000000..0637422
--- /dev/null
+++ b/src/icons/SelectTokensIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SelectMapIcon() {
+ return (
+
+ );
+}
+
+export default SelectMapIcon;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index cb0cbcb..9a84cee 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -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 (
-
+
handleImageUpload(event.target.files[0])}
type="file"
@@ -305,7 +196,7 @@ function SelectMapModal({
selectedMapState={selectedMapState}
onMapSelect={handleMapSelect}
onMapReset={handleMapReset}
- onSubmit={handleSubmit}
+ onDone={handleDone}
/>
-
+
);
}
diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js
new file mode 100644
index 0000000..09a8133
--- /dev/null
+++ b/src/modals/SelectTokensModal.js
@@ -0,0 +1,48 @@
+import React, { useRef, useContext } from "react";
+import { Flex, Label } from "theme-ui";
+
+import Modal from "../components/Modal";
+import ImageDrop from "../components/ImageDrop";
+
+import TokenTiles from "../components/token/TokenTiles";
+
+import TokenDataContext from "../contexts/TokenDataContext";
+
+function SelectTokensModal({ isOpen, onRequestClose }) {
+ const { tokens } = useContext(TokenDataContext);
+ const fileInputRef = useRef();
+
+ function openImageDialog() {
+ if (fileInputRef.current) {
+ fileInputRef.current.click();
+ }
+ }
+
+ function handleImageUpload(image) {}
+
+ return (
+
+
+ handleImageUpload(event.target.files[0])}
+ type="file"
+ accept="image/*"
+ style={{ display: "none" }}
+ ref={fileInputRef}
+ />
+
+
+
+
+
+
+ );
+}
+
+export default SelectTokensModal;
diff --git a/src/routes/Game.js b/src/routes/Game.js
index 657eec0..390f0ff 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -17,8 +17,7 @@ import AuthModal from "../modals/AuthModal";
import AuthContext from "../contexts/AuthContext";
import DatabaseContext from "../contexts/DatabaseContext";
-
-import { tokens as defaultTokens } from "../tokens";
+import MapDataContext from "../contexts/MapDataContext";
function Game() {
const { database } = useContext(DatabaseContext);
@@ -71,6 +70,7 @@ function Game() {
}
}
+ const { updateMapState } = useContext(MapDataContext);
// Sync the map state to the database after 500ms of inactivity
const debouncedMapState = useDebounce(mapState, 500);
useEffect(() => {
@@ -81,11 +81,9 @@ function Game() {
map.owner === userId &&
database
) {
- database
- .table("states")
- .update(debouncedMapState.mapId, debouncedMapState);
+ updateMapState(debouncedMapState.mapId, debouncedMapState);
}
- }, [map, debouncedMapState, userId, database]);
+ }, [map, debouncedMapState, userId, database, updateMapState]);
function handleMapChange(newMap, newMapState) {
setMapState(newMapState);
@@ -116,7 +114,7 @@ function Game() {
}
}
- async function handleMapTokenStateChange(token) {
+ function handleMapTokenStateChange(token) {
if (mapState === null) {
return;
}
@@ -438,30 +436,15 @@ function Game() {
}
}, [stream, peers, handleStreamEnd]);
- /**
- * Token data
- */
- const [tokens, setTokens] = useState([]);
- useEffect(() => {
- if (!userId) {
- return;
- }
- const defaultTokensWithIds = [];
- for (let defaultToken of defaultTokens) {
- defaultTokensWithIds.push({
- ...defaultToken,
- id: `__default-${defaultToken.key}`,
- owner: userId,
- });
- }
- setTokens(defaultTokensWithIds);
- }, [userId]);
-
return (
<>
-
+
setPeerError(null)}>