Added token edit modal, refactored map and token data providers
Refactored image drop code into reusable component as well
This commit is contained in:
parent
1774b459dc
commit
7b98370e4c
@ -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">
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
|
61
src/components/ImageDrop.js
Normal file
61
src/components/ImageDrop.js
Normal 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;
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
38
src/components/token/SelectTokensButton.js
Normal file
38
src/components/token/SelectTokensButton.js
Normal 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;
|
59
src/components/token/TokenTile.js
Normal file
59
src/components/token/TokenTile.js
Normal 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;
|
55
src/components/token/TokenTiles.js
Normal file
55
src/components/token/TokenTiles.js
Normal 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;
|
@ -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}
|
||||
|
146
src/contexts/MapDataContext.js
Normal file
146
src/contexts/MapDataContext.js
Normal 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;
|
40
src/contexts/TokenDataContext.js
Normal file
40
src/contexts/TokenDataContext.js
Normal 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;
|
18
src/icons/SelectTokensIcon.js
Normal file
18
src/icons/SelectTokensIcon.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
48
src/modals/SelectTokensModal.js
Normal file
48
src/modals/SelectTokensModal.js
Normal 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;
|
@ -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)}>
|
||||
|
Loading…
Reference in New Issue
Block a user