Added an indexedb database to store uploaded maps into

This commit is contained in:
Mitchell McCaffrey 2020-04-23 15:02:03 +10:00
parent 8681864ddc
commit 071cd3ea7f
9 changed files with 159 additions and 62 deletions

View File

@ -9,6 +9,7 @@
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2", "@testing-library/user-event": "^7.1.2",
"blob-to-buffer": "^1.2.8", "blob-to-buffer": "^1.2.8",
"dexie": "^2.0.4",
"gh-pages": "^2.2.0", "gh-pages": "^2.2.0",
"interactjs": "^1.9.7", "interactjs": "^1.9.7",
"js-binarypack": "^0.0.9", "js-binarypack": "^0.0.9",

View File

@ -14,7 +14,9 @@ function AddMapButton({ onMapChange }) {
} }
function handleDone(map) { function handleDone(map) {
onMapChange(map); if (map) {
onMapChange(map);
}
closeModal(); closeModal();
} }

View File

@ -1,9 +1,10 @@
import React from "react"; import React from "react";
import { Flex, Image as UIImage } from "theme-ui"; import { Flex, Image as UIImage, IconButton } from "theme-ui";
import AddIcon from "../../icons/AddIcon"; import AddIcon from "../../icons/AddIcon";
import RemoveIcon from "../../icons/RemoveIcon";
function MapSelect({ maps, selectedMap, onMapSelected, onMapAdd }) { function MapSelect({ maps, selectedMap, onMapSelect, onMapAdd, onMapRemove }) {
const tileProps = { const tileProps = {
m: 2, m: 2,
bg: "muted", bg: "muted",
@ -18,24 +19,38 @@ function MapSelect({ maps, selectedMap, onMapSelected, onMapAdd }) {
cursor: "pointer", cursor: "pointer",
}; };
// TODO move from passing index in to using DB ID function tile(map) {
function tile(map, index) {
return ( return (
<Flex // TODO: use DB key <Flex
key={map.source} key={map.id}
sx={{ sx={{
borderColor: "primary", borderColor: "primary",
borderStyle: index === selectedMap ? "solid" : "none", borderStyle: map.id === selectedMap ? "solid" : "none",
borderWidth: "4px", borderWidth: "4px",
position: "relative",
...tileStyle, ...tileStyle,
}} }}
{...tileProps} {...tileProps}
onClick={() => onMapSelected(index)} onClick={() => onMapSelect(map)}
> >
<UIImage <UIImage
sx={{ width: "100%", objectFit: "contain" }} sx={{ width: "100%", height: "100%", objectFit: "contain" }}
src={map.source} src={map.source}
/> />
{!map.default && map.id === selectedMap && (
<IconButton
aria-label="Remove Map"
title="Remove map"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onMapRemove(map.id);
}}
sx={{ position: "absolute", top: 0, right: 0 }}
>
<RemoveIcon />
</IconButton>
)}
</Flex> </Flex>
); );
} }
@ -54,7 +69,6 @@ function MapSelect({ maps, selectedMap, onMapSelected, onMapAdd }) {
flexGrow: 1, flexGrow: 1,
}} }}
> >
{maps.map((map, index) => tile(map, index))}
<Flex <Flex
onClick={onMapAdd} onClick={onMapAdd}
sx={{ sx={{
@ -70,9 +84,12 @@ function MapSelect({ maps, selectedMap, onMapSelected, onMapAdd }) {
...tileStyle, ...tileStyle,
}} }}
{...tileProps} {...tileProps}
aria-label="Add Map"
title="Add Map"
> >
<AddIcon /> <AddIcon />
</Flex> </Flex>
{maps.map(tile)}
</Flex> </Flex>
); );
} }

6
src/database.js Normal file
View File

@ -0,0 +1,6 @@
import Dexie from "dexie";
const db = new Dexie("OwlbearRodeoDB");
db.version(1).stores({ maps: "id" });
export default db;

18
src/icons/RemoveIcon.js Normal file
View File

@ -0,0 +1,18 @@
import React from "react";
function RemoveIcon() {
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="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z" />
</svg>
);
}
export default RemoveIcon;

View File

@ -10,40 +10,41 @@ const defaultProps = {
gridY: 22, gridY: 22,
width: 1024, width: 1024,
height: 1024, height: 1024,
default: true,
}; };
export const blank = { export const blank = {
...defaultProps, ...defaultProps,
source: blankImage, source: blankImage,
name: "Blank Grid 22x22", id: "Blank Grid 22x22",
}; };
export const grass = { export const grass = {
...defaultProps, ...defaultProps,
source: grassImage, source: grassImage,
name: "Grass Grid 22x22", id: "Grass Grid 22x22",
}; };
export const sand = { export const sand = {
...defaultProps, ...defaultProps,
source: sandImage, source: sandImage,
name: "Sand Grid 22x22", id: "Sand Grid 22x22",
}; };
export const stone = { export const stone = {
...defaultProps, ...defaultProps,
source: stoneImage, source: stoneImage,
name: "Stone Grid 22x22", id: "Stone Grid 22x22",
}; };
export const water = { export const water = {
...defaultProps, ...defaultProps,
source: waterImage, source: waterImage,
name: "Water Grid 22x22", id: "Water Grid 22x22",
}; };
export const wood = { export const wood = {
...defaultProps, ...defaultProps,
source: woodImage, source: woodImage,
name: "Wood Grid 22x22", id: "Wood Grid 22x22",
}; };

View File

@ -1,5 +1,8 @@
import React, { useRef, useState, useEffect } from "react"; import React, { useRef, useState, useEffect } from "react";
import { Box, Button, Flex, Label, Input, Text } from "theme-ui"; import { Box, Button, Flex, Label, Input, Text } from "theme-ui";
import shortid from "shortid";
import db from "../database";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import MapSelect from "../components/map/MapSelect"; import MapSelect from "../components/map/MapSelect";
@ -11,23 +14,25 @@ const defaultMapSize = 22;
function AddMapModal({ isOpen, onRequestClose, onDone }) { function AddMapModal({ isOpen, onRequestClose, onDone }) {
const [imageLoading, setImageLoading] = useState(false); const [imageLoading, setImageLoading] = useState(false);
const [currentMap, setCurrentMap] = useState(-1); const [currentMapId, setCurrentMapId] = useState(null);
const [maps, setMaps] = useState(Object.values(defaultMaps)); const [maps, setMaps] = useState(Object.values(defaultMaps));
// Load maps from the database
useEffect(() => {
async function loadMaps() {
let storedMaps = await db.table("maps").toArray();
// reverse so maps are show in the order they were added
storedMaps.reverse();
for (let map of storedMaps) {
// Recreate image urls for each map
map.source = URL.createObjectURL(map.file);
}
setMaps((prevMaps) => [...storedMaps, ...prevMaps]);
}
loadMaps();
}, []);
const [gridX, setGridX] = useState(defaultMapSize); const [gridX, setGridX] = useState(defaultMapSize);
const [gridY, setGridY] = useState(defaultMapSize); const [gridY, setGridY] = useState(defaultMapSize);
useEffect(() => {
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const changedMap = newMaps[currentMap];
if (changedMap) {
changedMap.gridX = gridX;
changedMap.gridY = gridY;
}
return newMaps;
});
}, [gridX, gridY, currentMap]);
const fileInputRef = useRef(); const fileInputRef = useRef();
function handleImageUpload(file) { function handleImageUpload(file) {
@ -54,23 +59,15 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
let image = new Image(); let image = new Image();
setImageLoading(true); setImageLoading(true);
image.onload = function () { image.onload = function () {
setMaps((prevMaps) => { handleMapAdd({
const newMaps = [ file,
...prevMaps, gridX: fileGridX,
{ gridY: fileGridY,
file, width: image.width,
gridX: fileGridX, height: image.height,
gridY: fileGridY, source: url,
width: image.width, id: shortid.generate(),
height: image.height,
source: url,
},
];
setCurrentMap(newMaps.length - 1);
return newMaps;
}); });
setGridX(fileGridX);
setGridY(fileGridY);
setImageLoading(false); setImageLoading(false);
}; };
image.src = url; image.src = url;
@ -82,10 +79,60 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
} }
} }
function handleMapSelect(mapId) { async function handleMapAdd(map) {
setCurrentMap(mapId); await db.table("maps").add(map);
setGridX(maps[mapId].gridX); setMaps((prevMaps) => [map, ...prevMaps]);
setGridY(maps[mapId].gridY); setCurrentMapId(map.id);
setGridX(map.gridX);
setGridY(map.gridY);
}
async function handleMapRemove(id) {
await db.table("maps").delete(id);
setMaps((prevMaps) => {
const filtered = prevMaps.filter((map) => map.id !== id);
setCurrentMapId(filtered[0].id);
return filtered;
});
}
function handleMapSelect(map) {
setCurrentMapId(map.id);
setGridX(map.gridX);
setGridY(map.gridY);
}
function handleSubmit(e) {
e.preventDefault();
onDone(maps.find((map) => map.id === currentMapId));
}
async function handleGridXChange(e) {
const newX = e.target.value;
await db.table("maps").update(currentMapId, { gridX: newX });
setGridX(newX);
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const i = newMaps.findIndex((map) => map.id === currentMapId);
if (i > -1) {
newMaps[i].gridX = newX;
}
return newMaps;
});
}
async function handleGridYChange(e) {
const newY = e.target.value;
await db.table("maps").update(currentMapId, { gridY: newY });
setGridY(newY);
setMaps((prevMaps) => {
const newMaps = [...prevMaps];
const i = newMaps.findIndex((map) => map.id === currentMapId);
if (i > -1) {
newMaps[i].gridY = newY;
}
return newMaps;
});
} }
/** /**
@ -116,14 +163,7 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
return ( return (
<Modal isOpen={isOpen} onRequestClose={onRequestClose}> <Modal isOpen={isOpen} onRequestClose={onRequestClose}>
<Box <Box as="form" onSubmit={handleSubmit} onDragEnter={handleImageDragEnter}>
as="form"
onSubmit={(e) => {
e.preventDefault();
onDone(maps[currentMap]);
}}
onDragEnter={handleImageDragEnter}
>
<input <input
onChange={(event) => handleImageUpload(event.target.files[0])} onChange={(event) => handleImageUpload(event.target.files[0])}
type="file" type="file"
@ -142,8 +182,9 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
<MapSelect <MapSelect
maps={maps} maps={maps}
onMapAdd={openImageDialog} onMapAdd={openImageDialog}
selectedMap={currentMap} onMapRemove={handleMapRemove}
onMapSelected={handleMapSelect} selectedMap={currentMapId}
onMapSelect={handleMapSelect}
/> />
<Flex> <Flex>
<Box mb={2} mr={1} sx={{ flexGrow: 1 }}> <Box mb={2} mr={1} sx={{ flexGrow: 1 }}>
@ -152,7 +193,9 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
type="number" type="number"
name="gridX" name="gridX"
value={gridX} value={gridX}
onChange={(e) => setGridX(e.target.value)} onChange={handleGridXChange}
disabled={currentMapId === null}
min={1}
/> />
</Box> </Box>
<Box mb={2} ml={1} sx={{ flexGrow: 1 }}> <Box mb={2} ml={1} sx={{ flexGrow: 1 }}>
@ -161,7 +204,9 @@ function AddMapModal({ isOpen, onRequestClose, onDone }) {
type="number" type="number"
name="gridY" name="gridY"
value={gridY} value={gridY}
onChange={(e) => setGridY(e.target.value)} onChange={handleGridYChange}
disabled={currentMapId === null}
min={1}
/> />
</Box> </Box>
</Flex> </Flex>

View File

@ -180,6 +180,8 @@ export default {
}, },
"&:disabled": { "&:disabled": {
backgroundColor: "muted", backgroundColor: "muted",
color: "gray",
borderColor: "text",
}, },
}, },
}, },

View File

@ -4005,6 +4005,11 @@ detect-port-alt@1.1.6:
address "^1.0.1" address "^1.0.1"
debug "^2.6.0" debug "^2.6.0"
dexie@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-2.0.4.tgz#6027a5e05879424e8f9979d8c14e7420f27e3a11"
integrity sha512-aQ/s1U2wHxwBKRrt2Z/mwFNHMQWhESerFsMYzE+5P5OsIe5o1kgpFMWkzKTtkvkyyEni6mWr/T4HUJuY9xIHLA==
diff-sequences@^24.9.0: diff-sequences@^24.9.0:
version "24.9.0" version "24.9.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"