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

@ -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() {
<FAQ />
</Route>
<Route path="/game/:id">
<Game />
<MapDataProvider>
<TokenDataProvider>
<Game />
</TokenDataProvider>
</MapDataProvider>
</Route>
<Route path="/">
<Home />

View File

@ -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 (
<Box onDragEnter={handleImageDragEnter}>
{children}
{dragging && (
<Flex
bg="overlay"
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" }}>
{dropText || "Drop image to upload"}
</Text>
</Flex>
)}
</Box>
);
}
export default ImageDrop;

View File

@ -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) {

View File

@ -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({
<Flex my={1}>
<Label>
<Checkbox
checked={
mapState !== null && mapState.editFlags.includes("fog")
}
disabled={mapState === null}
checked={mapState && mapState.editFlags.includes("fog")}
disabled={!mapState}
onChange={(e) => handleFlagChange(e, "fog")}
/>
Fog
</Label>
<Label>
<Checkbox
checked={
mapState !== null && mapState.editFlags.includes("drawing")
}
checked={mapState && mapState.editFlags.includes("drawing")}
disabled={mapState === null}
onChange={(e) => handleFlagChange(e, "drawing")}
/>
@ -81,10 +77,8 @@ function MapSettings({
</Label>
<Label>
<Checkbox
checked={
mapState !== null && mapState.editFlags.includes("tokens")
}
disabled={mapState === null}
checked={mapState && mapState.editFlags.includes("tokens")}
disabled={!mapState}
onChange={(e) => handleFlagChange(e, "tokens")}
/>
Tokens
@ -97,7 +91,7 @@ function MapSettings({
name="name"
value={(map && map.name) || ""}
onChange={(e) => onSettingsChange("name", e.target.value)}
disabled={map === null || map.type === "default"}
disabled={!map || map.type === "default"}
my={1}
/>
</Box>
@ -115,7 +109,7 @@ function MapSettings({
}}
aria-label={showMore ? "Show Less" : "Show More"}
title={showMore ? "Show Less" : "Show More"}
disabled={map === null}
disabled={!map}
>
<ExpandMoreIcon />
</IconButton>

View File

@ -15,7 +15,7 @@ function MapTile({
onMapSelect,
onMapRemove,
onMapReset,
onSubmit,
onDone,
}) {
const mapSource = useDataSource(map, defaultMapSources);
const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false);
@ -108,7 +108,7 @@ function MapTile({
}}
onDoubleClick={(e) => {
if (!isMapTileMenuOpen) {
onSubmit(e);
onDone(e);
}
}}
>

View File

@ -17,7 +17,7 @@ function MapTiles({
onMapAdd,
onMapRemove,
onMapReset,
onSubmit,
onDone,
}) {
const { databaseStatus } = useContext(DatabaseContext);
return (
@ -69,7 +69,7 @@ function MapTiles({
onMapSelect={onMapSelect}
onMapRemove={onMapRemove}
onMapReset={onMapReset}
onSubmit={onSubmit}
onDone={onDone}
/>
))}
</Flex>

View File

@ -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 }) {
<SelectMapIcon />
</IconButton>
<SelectMapModal
isOpen={isAddModalOpen}
isOpen={isModalOpen}
onRequestClose={closeModal}
onDone={handleDone}
onMapChange={onMapChange}

View File

@ -0,0 +1,38 @@
import React, { useState } from "react";
import { IconButton } from "theme-ui";
import SelectTokensIcon from "../../icons/SelectTokensIcon";
import SelectTokensModal from "../../modals/SelectTokensModal";
function SelectTokensButton() {
const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
setIsModalOpen(true);
}
function closeModal() {
setIsModalOpen(false);
}
function handleDone() {
closeModal();
}
return (
<>
<IconButton
aria-label="Edit Tokens"
title="Edit Tokens"
onClick={openModal}
>
<SelectTokensIcon />
</IconButton>
<SelectTokensModal
isOpen={isModalOpen}
onRequestClose={closeModal}
onDone={handleDone}
/>
</>
);
}
export default SelectTokensButton;

View File

@ -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 (
<Flex
sx={{
borderColor: "primary",
borderStyle: isSelected ? "solid" : "none",
borderWidth: "4px",
position: "relative",
width: "150px",
height: "150px",
borderRadius: "4px",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
}}
m={2}
bg="muted"
>
<Image
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
src={tokenSource}
/>
<Flex
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
background:
"linear-gradient(to bottom, rgba(0,0,0,0) 70%, rgba(0,0,0,0.65) 100%);",
alignItems: "flex-end",
justifyContent: "center",
}}
p={2}
>
<Text
as="p"
variant="heading"
color="hsl(210, 50%, 96%)"
sx={{ textAlign: "center" }}
>
{token.name}
</Text>
</Flex>
</Flex>
);
}
export default TokenTile;

View File

@ -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 (
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
<Flex
py={2}
bg="muted"
sx={{
flexWrap: "wrap",
width: "500px",
borderRadius: "4px",
}}
>
<Flex
onClick={onTokenAdd}
sx={{
":hover": {
color: "primary",
},
":focus": {
outline: "none",
},
":active": {
color: "secondary",
},
width: "150px",
height: "150px",
borderRadius: "4px",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
}}
m={2}
bg="muted"
aria-label="Add Token"
title="Add Token"
>
<AddIcon large />
</Flex>
{tokens.map((token) => (
<TokenTile key={token.id} token={token} />
))}
</Flex>
</SimpleBar>
);
}
export default TokenTiles;

View File

@ -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",
}}
>
<SimpleBar style={{ height: "calc(100% - 58px)", overflowX: "hidden" }}>
<SimpleBar style={{ height: "calc(100% - 48px)", overflowX: "hidden" }}>
{tokens.map((token) => (
<ListToken
key={token.id}
@ -53,15 +57,23 @@ function Tokens({ onCreateMapTokenState, tokens }) {
/>
))}
</SimpleBar>
<Box pt={1} bg="muted" sx={{ height: "58px" }}>
<NumberInput
<Flex
bg="muted"
sx={{
justifyContent: "center",
height: "48px",
alignItems: "center",
}}
>
<SelectTokensButton />
{/* <NumberInput
value={tokenSize}
onChange={setTokenSize}
title="Size"
min={1}
max={9}
/>
</Box>
/> */}
</Flex>
</Box>
<ProxyToken
tokenClassName={listTokenClassName}

View File

@ -0,0 +1,146 @@
import React, { useEffect, useState, useContext } from "react";
import AuthContext from "./AuthContext";
import DatabaseContext from "./DatabaseContext";
import { maps as defaultMaps } from "../maps";
const MapDataContext = React.createContext();
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"],
};
export function MapDataProvider({ children }) {
const { database } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext);
const [maps, setMaps] = useState([]);
const [mapStates, setMapStates] = 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,
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 (
<MapDataContext.Provider value={value}>{children}</MapDataContext.Provider>
);
}
export default MapDataContext;

View File

@ -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 (
<TokenDataContext.Provider value={value}>
{children}
</TokenDataContext.Provider>
);
}
export default TokenDataContext;

View File

@ -0,0 +1,18 @@
import React from "react";
function SelectMapIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4 11h-3v3c0 .55-.45 1-1 1s-1-.45-1-1v-3H8c-.55 0-1-.45-1-1s.45-1 1-1h3V8c0-.55.45-1 1-1s1 .45 1 1v3h3c.55 0 1 .45 1 1s-.45 1-1 1z" />
</svg>
);
}
export default SelectMapIcon;

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>
);
}

View File

@ -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 (
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
<ImageDrop onDrop={handleImageUpload} dropText="Drop token to upload">
<input
onChange={(event) => handleImageUpload(event.target.files[0])}
type="file"
accept="image/*"
style={{ display: "none" }}
ref={fileInputRef}
/>
<Flex
sx={{
flexDirection: "column",
}}
>
<Label pt={2} pb={1}>
Edit or import a token
</Label>
<TokenTiles tokens={tokens} onTokenAdd={openImageDialog} />
</Flex>
</ImageDrop>
</Modal>
);
}
export default SelectTokensModal;

View File

@ -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 (
<>
<Flex sx={{ flexDirection: "column", height: "100%" }}>
<Flex
sx={{ justifyContent: "space-between", flexGrow: 1, height: "100%" }}
sx={{
justifyContent: "space-between",
flexGrow: 1,
height: "100%",
}}
>
<Party
nickname={nickname}
@ -476,7 +459,6 @@ function Game() {
<Map
map={map}
mapState={mapState}
tokens={tokens}
loading={mapLoading}
onMapTokenStateChange={handleMapTokenStateChange}
onMapTokenStateRemove={handleMapTokenStateRemove}
@ -492,10 +474,7 @@ function Game() {
allowFogDrawing={canEditFogDrawing}
disabledTokens={disabledMapTokens}
/>
<Tokens
tokens={tokens}
onCreateMapTokenState={handleMapTokenStateChange}
/>
<Tokens onMapTokenStateCreate={handleMapTokenStateChange} />
</Flex>
</Flex>
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>