Browse Source

Disconnecting old board

Jeremie Pardou-Piquemal 2 years ago
parent
commit
4f5b267fc1
67 changed files with 51 additions and 5295 deletions
  1. 7 7
      src/MainRoute.jsx
  2. 0 118
      src/components/AddItemButton.jsx
  3. 0 67
      src/components/DownloadLink.jsx
  4. 0 74
      src/components/EditInfoButton.jsx
  5. 0 82
      src/components/ImageDropNPaste.jsx
  6. 0 209
      src/components/ItemLibrary.jsx
  7. 0 155
      src/components/MainView.jsx
  8. 0 77
      src/components/NewItems.jsx
  9. 0 391
      src/components/SelectedItemsPane.jsx
  10. 0 50
      src/components/SubscribeGameEvents.jsx
  11. 0 50
      src/components/SubscribeSessionEvents.jsx
  12. 0 181
      src/components/board/ActionPane.jsx
  13. 0 79
      src/components/board/Board.jsx
  14. 0 70
      src/components/board/Cursors/Cursor.jsx
  15. 0 43
      src/components/board/Cursors/CursorPane.jsx
  16. 0 89
      src/components/board/Cursors/Cursors.jsx
  17. 0 522
      src/components/board/Gesture.jsx
  18. 0 182
      src/components/board/Items/Item.jsx
  19. 0 54
      src/components/board/Items/ItemFormFactory.jsx
  20. 0 52
      src/components/board/Items/ItemList.jsx
  21. 0 77
      src/components/board/Items/SubscribeItemEvents.jsx
  22. 0 3
      src/components/board/Items/index.jsx
  23. 0 93
      src/components/board/Items/useItemActions.jsx
  24. 0 43
      src/components/board/Items/useItemInteraction.js
  25. 0 374
      src/components/board/Items/useItems.jsx
  26. 0 347
      src/components/board/PanZoomRotate.jsx
  27. 0 211
      src/components/board/Selector.jsx
  28. 0 74
      src/components/board/atoms.jsx
  29. 0 11
      src/components/board/index.jsx
  30. 0 50
      src/components/board/usePositionNavigator.jsx
  31. 0 95
      src/components/hooks/useC2C.jsx
  32. 0 17
      src/components/hooks/useNotify.js
  33. 0 19
      src/components/hooks/usePrevious.jsx
  34. 0 11
      src/components/hooks/useToggle.js
  35. 0 113
      src/components/mediaLibrary/ImageField.jsx
  36. 0 25
      src/components/mediaLibrary/MediaLibraryButton.jsx
  37. 0 199
      src/components/mediaLibrary/MediaLibraryModal.jsx
  38. 0 50
      src/components/mediaLibrary/MediaLibraryProvider.jsx
  39. 0 24
      src/components/mediaLibrary/index.js
  40. 0 44
      src/components/message/Composer.jsx
  41. 0 54
      src/components/message/Message.jsx
  42. 0 85
      src/components/message/MessageButton.jsx
  43. 0 97
      src/components/message/MessageList.jsx
  44. 0 1
      src/components/message/index.js
  45. 0 108
      src/components/message/useMessage.js
  46. 0 40
      src/components/useBoardConfig.jsx
  47. 0 107
      src/components/users/SubscribeUserEvents.jsx
  48. 0 39
      src/components/users/UserCircle.jsx
  49. 0 78
      src/components/users/UserConfig.jsx
  50. 0 92
      src/components/users/UserList.jsx
  51. 0 40
      src/components/users/atoms.jsx
  52. 0 5
      src/components/users/index.jsx
  53. 0 36
      src/components/users/useUsers.jsx
  54. 0 67
      src/components/utils.js
  55. 1 1
      src/hooks/useSession.jsx
  56. 1 1
      src/views/AutoSaveSession.jsx
  57. 1 1
      src/views/BoardView/BoardView.jsx
  58. 1 1
      src/views/BoardView/LoadSessionModal.jsx
  59. 1 1
      src/views/BoardView/NavBar.jsx
  60. 1 1
      src/views/BoardView/WelcomeModal.jsx
  61. 1 1
      src/views/GameListItem.jsx
  62. 2 2
      src/views/GameListView.jsx
  63. 1 1
      src/views/GameStudio.jsx
  64. 3 3
      src/views/Home.jsx
  65. 1 1
      src/views/RoomView/RoomNavBar.jsx
  66. 0 0
      src/views/Spinner.jsx
  67. 30 0
      src/views/utils.js

+ 7 - 7
src/MainRoute.jsx

@@ -8,10 +8,10 @@ import "react-toastify/dist/ReactToastify.css";
 import "./react-confirm-alert.css";
 
 import Home from "./views/Home";
-import GameView from "./views/GameView";
-import RoomWrapperView from "./views/RoomWrapperView";
+// import GameView from "./views/GameView";
+// import RoomWrapperView from "./views/RoomWrapperView";
 import AuthView from "./views/AuthView";
-import RoomView from "./views/RoomView";
+// import RoomView from "./views/RoomView";
 
 import { Provider as SocketIOProvider } from "@scripters/use-socket.io";
 
@@ -65,7 +65,7 @@ const MainRoute = () => {
           );
         }}
       </Route>
-      <Route path="/session/:sessionId">
+      {/*<Route path="/session/:sessionId">
         {({
           location: { search },
           match: {
@@ -82,14 +82,14 @@ const MainRoute = () => {
             </WithSocketIO>
           );
         }}
-      </Route>
-      {/* Game edition */}
+      </Route>*/}
+      {/* Game edition/}
       <Route path="/game/:gameId?">
         <WithSocketIO>
           <GameView />
         </WithSocketIO>
       </Route>
-      {/* Room routes */}
+      {/* Room routes}
       <Route path="/room/:roomId">
         {({
           match: {

+ 0 - 118
src/components/AddItemButton.jsx

@@ -1,118 +0,0 @@
-import React from "react";
-import { nanoid } from "nanoid";
-import { useRecoilValue } from "recoil";
-import { useTranslation } from "react-i18next";
-import ItemLibrary from "./ItemLibrary";
-
-import { AvailableItemListAtom } from "./board";
-
-import Touch from "./ui/Touch";
-import SidePanel from "./ui/SidePanel";
-
-// Keep compatibility with previous availableItems shape
-const migrateAvailableItemList = (old) => {
-  const groupMap = old.reduce((acc, { groupId, ...item }) => {
-    if (!acc[groupId]) {
-      acc[groupId] = [];
-    }
-    acc[groupId].push(item);
-    return acc;
-  }, {});
-  return Object.keys(groupMap).map((name) => ({
-    name,
-    items: groupMap[name],
-  }));
-};
-
-const adaptItem = (item, itemMap) => ({
-  type: item.type,
-  template: item,
-  component: itemMap[item.type].component,
-  name: item.name || item.label || item.text || itemMap[item.type].name,
-  uid: nanoid(),
-});
-
-const adaptAvailableItems = (nodes, itemMap) => {
-  return nodes.map((node) => {
-    if (node.type) {
-      return adaptItem(node, itemMap);
-    } else {
-      return { ...node, items: adaptAvailableItems(node.items, itemMap) };
-    }
-  });
-};
-
-const AddItemButton = ({ itemMap }) => {
-  const { t } = useTranslation();
-
-  const availableItemList = useRecoilValue(AvailableItemListAtom);
-  const [showAddPanel, setShowAddPanel] = React.useState(false);
-  const [tab, setTab] = React.useState("standard");
-
-  const defaultItemLibrary = React.useMemo(
-    () =>
-      Object.keys(itemMap).map((key) => ({
-        type: key,
-        ...itemMap[key],
-        uid: nanoid(),
-      })),
-    [itemMap]
-  );
-
-  const availableItemLibrary = React.useMemo(() => {
-    let itemList = availableItemList;
-    if (itemList.length && itemList[0].groupId) {
-      itemList = migrateAvailableItemList(itemList);
-    }
-    return adaptAvailableItems(itemList, itemMap);
-  }, [availableItemList, itemMap]);
-
-  return (
-    <>
-      <Touch
-        onClick={() => setShowAddPanel((prev) => !prev)}
-        alt={t("Add item")}
-        title={t("Add item")}
-        label={t("Add")}
-        icon={showAddPanel ? "cross" : "plus"}
-      />
-      <SidePanel
-        open={showAddPanel}
-        onClose={() => {
-          setShowAddPanel(false);
-        }}
-        position="right"
-        width="33%"
-      >
-        <nav className="tabs">
-          {
-            // eslint-disable-next-line
-              <a
-              onClick={() => setTab("standard")}
-              className={tab === "standard" ? "active" : ""}
-              style={{ cursor: "pointer" }}
-            >
-              {t("Standard")}
-            </a>
-          }
-          {availableItemList && availableItemList.length > 0 && (
-            // eslint-disable-next-line
-              <a
-              onClick={() => setTab("other")}
-              className={tab === "other" ? "active" : ""}
-              style={{ cursor: "pointer" }}
-            >
-              {t("Other")}
-            </a>
-          )}
-        </nav>
-        <section className="content">
-          {tab === "standard" && <ItemLibrary items={defaultItemLibrary} />}
-          {tab === "other" && <ItemLibrary items={availableItemLibrary} />}
-        </section>
-      </SidePanel>
-    </>
-  );
-};
-
-export default AddItemButton;

+ 0 - 67
src/components/DownloadLink.jsx

@@ -1,67 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-const generateDownloadURI = (data) => {
-  return (
-    "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data))
-  );
-};
-
-export const DownloadLink = ({ getData = () => {} }) => {
-  const { t } = useTranslation();
-
-  const [downloadURI, setDownloadURI] = React.useState("");
-  const [date, setDate] = React.useState(Date.now());
-  const [genOnce, setGenOnce] = React.useState(false);
-
-  const updateSaveLink = React.useCallback(async () => {
-    const data = await getData();
-    if (data.items.length) {
-      setDownloadURI(generateDownloadURI(data));
-      setDate(Date.now());
-      setGenOnce(true);
-    }
-  }, [getData]);
-
-  React.useEffect(() => {
-    let mounted = true;
-
-    const cancel = setInterval(() => {
-      if (!mounted) return;
-      updateSaveLink();
-    }, 2000);
-
-    updateSaveLink();
-
-    return () => {
-      mounted = false;
-      setGenOnce(false);
-      clearInterval(cancel);
-    };
-  }, [updateSaveLink]);
-
-  return (
-    <>
-      {genOnce && (
-        <a
-          className="button success icon"
-          href={downloadURI}
-          download={`airboardgame_${date}.json`}
-        >
-          {t("Export")}
-          <img
-            src={"https://icongr.am/entypo/download.svg?size=20&color=f9fbfa"}
-            alt="icon"
-          />
-        </a>
-      )}
-      {!genOnce && (
-        <button className="button" disabled>
-          {t("Generating export")}...
-        </button>
-      )}
-    </>
-  );
-};
-
-export default DownloadLink;

+ 0 - 74
src/components/EditInfoButton.jsx

@@ -1,74 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import styled from "styled-components";
-import { Form } from "react-final-form";
-
-import Touch from "./ui/Touch";
-import Modal from "./ui/Modal";
-import AutoSave from "./ui/formUtils/AutoSave";
-
-import useBoardConfig from "./useBoardConfig";
-
-const BoardConfigForm = styled.div`
-  display: flex;
-  flex-direction: column;
-  & .trash {
-    float: right;
-  }
-`;
-
-const BoardConfig = ({ BoardFormComponent }) => {
-  const [, setBoardConfig] = useBoardConfig();
-
-  const onSubmitHandler = React.useCallback(
-    (data) => {
-      setBoardConfig((prev) => ({
-        ...prev,
-        ...data,
-      }));
-    },
-    [setBoardConfig]
-  );
-
-  return (
-    <Form
-      onSubmit={onSubmitHandler}
-      render={() => (
-        <BoardConfigForm>
-          <AutoSave save={onSubmitHandler} />
-          <BoardFormComponent />
-        </BoardConfigForm>
-      )}
-    />
-  );
-};
-
-const EditInfoButton = ({ BoardFormComponent }) => {
-  const { t } = useTranslation();
-
-  const [show, setShow] = React.useState(false);
-
-  return (
-    <>
-      <Touch
-        onClick={() => setShow((prev) => !prev)}
-        alt={t("Edit game info")}
-        title={t("Edit game info")}
-        label={t("Edit game info")}
-        icon={"cog"}
-      />
-      <Modal
-        title={t("Edit game information")}
-        setShow={setShow}
-        show={show}
-        position="left"
-      >
-        <section>
-          <BoardConfig BoardFormComponent={BoardFormComponent} />
-        </section>
-      </Modal>
-    </>
-  );
-};
-
-export default EditInfoButton;

+ 0 - 82
src/components/ImageDropNPaste.jsx

@@ -1,82 +0,0 @@
-import React from "react";
-import { useDropzone } from "react-dropzone";
-import { nanoid } from "nanoid";
-import { useRecoilCallback } from "recoil";
-import { useTranslation } from "react-i18next";
-
-import { PanZoomRotateAtom } from "./board";
-import { useItems } from "../components/board/Items";
-import { useMediaLibrary } from "../components/mediaLibrary";
-import Waiter from "./ui/Waiter";
-
-const ImageDropNPaste = ({ children }) => {
-  const { t } = useTranslation();
-  const [uploading, setUploading] = React.useState(false);
-  const { pushItem } = useItems();
-
-  const { addMedia, libraries } = useMediaLibrary();
-
-  const addImageItem = useRecoilCallback(
-    ({ snapshot }) => async (media) => {
-      const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
-      pushItem({
-        type: "image",
-        x: centerX,
-        y: centerY,
-        id: nanoid(),
-        content: media,
-      });
-    },
-    [pushItem]
-  );
-
-  const onDrop = React.useCallback(
-    async (acceptedFiles) => {
-      setUploading(true);
-      await Promise.all(
-        acceptedFiles.map(async (file) => {
-          const media = await addMedia(libraries[0], file);
-          await addImageItem(media);
-        })
-      );
-      setUploading(false);
-    },
-    [addImageItem, addMedia, libraries]
-  );
-
-  const { getRootProps } = useDropzone({ onDrop });
-
-  const onPaste = React.useCallback(
-    async (e) => {
-      const items = e.clipboardData.items;
-      setUploading(true);
-      for (var i = 0; i < items.length; i++) {
-        const item = items[i];
-        if (item.type.indexOf("image") !== -1) {
-          const file = item.getAsFile();
-          const location = await addMedia(libraries[0], file);
-          await addImageItem(location);
-        }
-      }
-      setUploading(false);
-    },
-    [addImageItem, addMedia, libraries]
-  );
-
-  React.useEffect(() => {
-    window.addEventListener("paste", onPaste, false);
-
-    return () => {
-      window.removeEventListener("paste", onPaste);
-    };
-  }, [onPaste]);
-
-  return (
-    <div {...getRootProps()}>
-      {children}
-      {uploading && <Waiter message={t("Uploading image(s)...")} />}
-    </div>
-  );
-};
-
-export default ImageDropNPaste;

+ 0 - 209
src/components/ItemLibrary.jsx

@@ -1,209 +0,0 @@
-import React, { memo } from "react";
-import { useTranslation } from "react-i18next";
-import { nanoid } from "nanoid";
-import styled from "styled-components";
-import { useRecoilCallback } from "recoil";
-import { debounce } from "lodash";
-
-import { useItems } from "../components/board/Items";
-import useToggle from "./hooks/useToggle";
-import { search } from "./utils";
-
-import Chevron from "./ui/Chevron";
-import { PanZoomRotateAtom } from "./board";
-
-const StyledItemList = styled.ul`
-  display: flex;
-  flex-flow: row wrap;
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  & li.group {
-    background-color: rgba(0, 0, 0, 0.1);
-    padding: 0 0.5em;
-    flex-basis: 100%;
-  }
-  overflow: visible;
-`;
-
-const StyledItem = styled.li`
-  display: block;
-  padding: 0.5em;
-  margin: 0.2em;
-  cursor: pointer;
-  opacity: 0.7;
-
-  &:hover {
-    opacity: 1;
-  }
-  & > div {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    pointer-events: none;
-    max-width: 80px;
-    & > span {
-      margin-top: 0.2em;
-      text-align: center;
-      max-width: 80px;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      padding: 0.2em 0.5em;
-    }
-  }
-  &:hover > div > span {
-    z-index: 2;
-    max-width: none;
-    overflow: visible;
-    background-color: #222;
-    box-shadow: 0px 3px 6px #00000029;
-  }
-`;
-
-const size = 60;
-
-const NewItem = memo(({ type, template, component: Component, name }) => {
-  const { pushItem } = useItems();
-
-  const addItem = useRecoilCallback(
-    ({ snapshot }) => async () => {
-      const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
-      pushItem({
-        ...template,
-        x: centerX,
-        y: centerY,
-        id: nanoid(),
-        type,
-      });
-    },
-    [pushItem, template, type]
-  );
-
-  return (
-    <>
-      <StyledItem onClick={addItem}>
-        <div>
-          <Component {...template} width={size} height={size} size={size} />
-          <span>{name}</span>
-        </div>
-      </StyledItem>
-    </>
-  );
-});
-
-NewItem.displayName = "NewItem";
-
-const SubItemList = ({ name, items }) => {
-  const { t } = useTranslation();
-  const [open, toggleOpen] = useToggle(false);
-  const { pushItem } = useItems();
-
-  const addItems = useRecoilCallback(
-    ({ snapshot }) => async (items) => {
-      const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
-      items.forEach(({ template }, index) => {
-        pushItem({
-          ...template,
-          x: centerX + 2 * index,
-          y: centerY + 2 * index,
-          id: nanoid(),
-        });
-      });
-    },
-    [pushItem]
-  );
-
-  return (
-    <>
-      <h3 onClick={toggleOpen} style={{ cursor: "pointer" }}>
-        {open ? (
-          <Chevron orientation="bottom" color="#8c8c8c" />
-        ) : (
-          <Chevron color="#8c8c8c" />
-        )}{" "}
-        {name}{" "}
-        <span
-          style={{ fontSize: "0.6em" }}
-          onClick={(e) => {
-            e.preventDefault();
-            e.stopPropagation();
-            addItems(items);
-          }}
-        >
-          [{t("Add all")}]
-        </span>
-      </h3>
-      {open && <ItemList items={items} />}
-    </>
-  );
-};
-
-const ItemList = ({ items }) => {
-  return (
-    <StyledItemList>
-      {items.map((node) => {
-        if (node.type) {
-          return <NewItem {...node} key={node.uid} />;
-        } else {
-          // it's a group
-          return (
-            <li key={`group_${node.name}`} className="group">
-              <SubItemList {...node} />
-            </li>
-          );
-        }
-      })}
-    </StyledItemList>
-  );
-};
-
-const MemoizedItemList = memo(ItemList);
-
-const filterItems = (filter, nodes) => {
-  return nodes.reduce((acc, node) => {
-    if (node.type) {
-      if (search(filter, node.name)) {
-        acc.push(node);
-      }
-      return acc;
-    } else {
-      const filteredItems = filterItems(filter, node.items);
-      if (filteredItems.length) {
-        acc.push({ ...node, items: filteredItems });
-      }
-      return acc;
-    }
-  }, []);
-};
-
-const ItemLibrary = ({ items }) => {
-  const { t } = useTranslation();
-  const [filter, setFilter] = React.useState("");
-  const [filteredItems, setFilteredItems] = React.useState(items);
-
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const debouncedFilterItems = React.useCallback(
-    debounce((filter, items) => {
-      setFilteredItems(filterItems(filter, items));
-    }, 500),
-    []
-  );
-
-  React.useEffect(() => {
-    debouncedFilterItems(filter, items);
-  }, [debouncedFilterItems, filter, items]);
-
-  return (
-    <>
-      <input
-        onChange={(e) => setFilter(e.target.value)}
-        style={{ marginBottom: "1em" }}
-        placeholder={t("Search...")}
-      />
-      <MemoizedItemList items={filteredItems} />
-    </>
-  );
-};
-
-export default memo(ItemLibrary);

+ 0 - 155
src/components/MainView.jsx

@@ -1,155 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-import { useTranslation } from "react-i18next";
-
-import { Board } from "./board";
-import SelectedItemsPane from "./SelectedItemsPane";
-import { useUsers } from "./users";
-import Touch from "./ui/Touch";
-
-import { MediaLibraryProvider } from "./mediaLibrary";
-import ImageDropNPaste from "./ImageDropNPaste";
-import AddItemButton from "./AddItemButton";
-import { MessageButton } from "./message";
-import { insideClass } from "./utils";
-import EditInfoButton from "./EditInfoButton";
-
-const StyledBoardView = styled.div`
-  width: 100vw;
-  height: 100vh;
-  overflow: hidden;
-`;
-
-const BoardContainer = styled.div`
-  position: relative;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  box-sizing: border-box;
-  background-color: var(--color-darkGrey);
-`;
-
-const ActionBar = styled.div`
-  position: fixed;
-  bottom: 1em;
-  right: 0em;
-  display: flex;
-  width: 100%;
-  text-shadow: 1px 1px 2px #222;
-  font-size: 0.8em;
-  pointer-events: none;
-
-  & > *:not(.spacer) {
-    padding: 0 1.5em;
-    pointer-events: all;
-  }
-
-  & .spacer {
-    flex: 1;
-  }
-
-  @media screen and (max-width: 640px) {
-    & > *:not(.spacer) {
-      padding: 0 0.5em;
-    }
-    & .spacer {
-      padding: 0;
-    }
-  }
-
-  @media screen and (max-width: 420px) {
-    & > *:not(.spacer) {
-      padding: 0 0.2em;
-    }
-  }
-`;
-
-export const MainView = ({
-  edit: editMode = false,
-  mediaLibraries,
-  mediaHandlers,
-  itemMap,
-  actionMap,
-  ItemFormComponent,
-  BoardFormComponent,
-}) => {
-  const { t } = useTranslation();
-  const { currentUser, localUsers: users } = useUsers();
-
-  const [moveFirst, setMoveFirst] = React.useState(false);
-  const [hideMenu, setHideMenu] = React.useState(false);
-
-  React.useEffect(() => {
-    // Chrome-related issue.
-    // Making the wheel event non-passive, which allows to use preventDefault() to prevent
-    // the browser original zoom  and therefore allowing our custom one.
-    // More detail at https://github.com/facebook/react/issues/14856
-    const cancelWheel = (event) => {
-      if (insideClass(event.target, "board")) event.preventDefault();
-    };
-
-    document.body.addEventListener("wheel", cancelWheel, { passive: false });
-
-    return () => {
-      document.body.removeEventListener("wheel", cancelWheel);
-    };
-  }, []);
-
-  return (
-    <StyledBoardView>
-      <MediaLibraryProvider libraries={mediaLibraries} {...mediaHandlers}>
-        <BoardContainer>
-          <ImageDropNPaste>
-            <Board
-              user={currentUser}
-              users={users}
-              itemMap={itemMap}
-              moveFirst={moveFirst}
-              hideMenu={hideMenu}
-            />
-          </ImageDropNPaste>
-          <SelectedItemsPane
-            hideMenu={hideMenu}
-            itemMap={itemMap}
-            actionMap={actionMap}
-            ItemFormComponent={ItemFormComponent}
-          />
-        </BoardContainer>
-        <ActionBar>
-          {!editMode && <MessageButton />}
-          {editMode && (
-            <EditInfoButton BoardFormComponent={BoardFormComponent} />
-          )}
-          <div className="spacer" />
-          <Touch
-            onClick={() => setMoveFirst(false)}
-            alt={t("Select mode")}
-            label={t("Select")}
-            title={t("Switch to select mode")}
-            icon={"mouse-pointer"}
-            active={!moveFirst}
-          />
-          <Touch
-            onClick={() => setMoveFirst(true)}
-            alt={t("Move mode")}
-            label={t("Move")}
-            title={t("Switch to move mode")}
-            icon={"hand"}
-            active={moveFirst}
-          />
-          <Touch
-            onClick={() => setHideMenu((prev) => !prev)}
-            alt={hideMenu ? t("Show menu") : t("Hide menu")}
-            label={hideMenu ? t("Show menu") : t("Hide menu")}
-            title={hideMenu ? t("Show action menu") : t("Hide action menu")}
-            icon={hideMenu ? "eye-with-line" : "eye"}
-          />
-          <div className="spacer" />
-          <AddItemButton itemMap={itemMap} />
-        </ActionBar>
-      </MediaLibraryProvider>
-    </StyledBoardView>
-  );
-};
-
-export default MainView;

+ 0 - 77
src/components/NewItems.jsx

@@ -1,77 +0,0 @@
-import React, { memo } from "react";
-import { nanoid } from "nanoid";
-import { useRecoilCallback } from "recoil";
-import styled from "styled-components";
-
-import { useItems } from "./board/Items";
-import { PanZoomRotateAtom } from "./board";
-import { itemMap } from "./boardComponents";
-
-const ItemList = styled.div`
-  display: flex;
-  flex-wrap: wrap;
-  align-items: end;
-  justify-content: space-around;
-`;
-
-const Item = styled.div`
-  padding: 1em;
-  margin: 0.3em;
-  cursor: pointer;
-  opacity: 0.7;
-  &:hover {
-    opacity: 1;
-  }
-  & > div {
-    display: flex;
-    align-items: center;
-    flex-direction: column;
-  }
-`;
-
-const size = 70;
-
-const NewItem = memo(({ type }) => {
-  const { pushItem } = useItems();
-
-  const addItem = useRecoilCallback(
-    ({ snapshot }) => async () => {
-      const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
-      pushItem({
-        ...itemMap[type].template,
-        x: centerX,
-        y: centerY,
-        id: nanoid(),
-        type,
-      });
-    },
-    [pushItem, type]
-  );
-
-  const Component = itemMap[type].component;
-
-  return (
-    <>
-      <Item onClick={addItem}>
-        <div style={{ pointerEvents: "none" }}>
-          <Component width={size} height={size} size={size} />
-          <span>{itemMap[type].label}</span>
-        </div>
-      </Item>
-    </>
-  );
-});
-
-NewItem.displayName = "NewItem";
-
-const NewItems = () => {
-  return (
-    <ItemList>
-      {Object.keys(itemMap).map((type) => (
-        <NewItem type={type} key={type} />
-      ))}
-    </ItemList>
-  );
-};
-
-export default memo(NewItems);

+ 0 - 391
src/components/SelectedItemsPane.jsx

@@ -1,391 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-import { toast } from "react-toastify";
-import { useRecoilValue, useRecoilCallback } from "recoil";
-import debounce from "lodash.debounce";
-import { useTranslation } from "react-i18next";
-
-import { insideClass, hasClass } from "./utils";
-import SidePanel from "./ui/SidePanel";
-import { useItemActions } from "./board/Items/useItemActions";
-import {
-  SelectedItemsAtom,
-  PanZoomRotateAtom,
-  BoardStateAtom,
-  ItemMapAtom,
-} from "./board";
-import ItemFormFactory from "./board/Items/ItemFormFactory";
-
-// import { confirmAlert } from "react-confirm-alert";
-
-const ActionPane = styled.div.attrs(({ top, left, height }) => {
-  if (top < 120) {
-    return {
-      style: {
-        transform: `translate(${left}px, ${top + height + 5}px)`,
-      },
-    };
-  } else {
-    return {
-      style: {
-        transform: `translate(${left}px, ${top - 60}px)`,
-      },
-    };
-  }
-})`
-  top: 0;
-  left: 0;
-  user-select: none;
-  touch-action: none;
-  position: absolute;
-  display: flex;
-  background-color: var(--color-blueGrey);
-  justify-content: center;
-  align-items: center;
-  border-radius: 4px;
-  padding: 0.1em 0.5em;
-  transition: opacity 100ms;
-  opacity: ${({ hide }) => (hide ? 0 : 0.9)};
-  
-  box-shadow: 2px 2px 10px 0.3px rgba(0, 0, 0, 0.5);
-
-  &:hover{
-    opacity: 1;
-  }
-
-  & button{
-    margin 0 4px;
-    padding: 0em;
-    height: 50px
-  }
-  & .button.icon-only{
-    padding: 0em;
-    opacity: 0.5;
-  }
-  & button.icon-only:hover{
-    opacity: 1;
-  }
-  & .count{
-    color: var(--color-secondary);
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    line-height: 0.8em;
-  }
-  & .number{
-    font-size: 1.5em;
-    line-height: 1em;
-  }
-`;
-
-const CardContent = styled.div.attrs(() => ({ className: "content" }))`
-  display: flex;
-  flex-direction: column;
-  padding: 0.5em;
-`;
-
-const BoundingBoxZone = styled.div.attrs(({ top, left, height, width }) => ({
-  style: {
-    transform: `translate(${left}px, ${top}px)`,
-    height: `${height}px`,
-    width: `${width}px`,
-  },
-}))`
-  top: 0;
-  left: 0;
-  z-index: 210;
-  position: absolute;
-  background-color: hsla(0, 40%, 50%, 0%);
-  border: 1px dashed hsl(20, 55%, 40%);
-  pointer-events: none;
-`;
-
-const BoundingBox = ({
-  boundingBoxLast,
-  setBoundingBoxLast,
-  selectedItems,
-}) => {
-  const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
-  const itemMap = useRecoilValue(ItemMapAtom);
-
-  // Update selection bounding box
-  const updateBox = useRecoilCallback(
-    ({ snapshot }) => async () => {
-      const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-
-      if (selectedItems.length === 0) {
-        setBoundingBoxLast(null);
-        return;
-      }
-
-      let boundingBox = null;
-
-      selectedItems.forEach((itemId) => {
-        const elem = document.getElementById(itemId);
-
-        if (!elem) return;
-
-        const {
-          right: x2,
-          bottom: y2,
-          top: y,
-          left: x,
-        } = elem.getBoundingClientRect();
-
-        if (!boundingBox) {
-          boundingBox = { x, y, x2, y2 };
-        } else {
-          if (x < boundingBox.x) {
-            boundingBox.x = x;
-          }
-          if (y < boundingBox.y) {
-            boundingBox.y = y;
-          }
-          if (x2 > boundingBox.x2) {
-            boundingBox.x2 = x2;
-          }
-          if (y2 > boundingBox.y2) {
-            boundingBox.y2 = y2;
-          }
-        }
-      });
-
-      if (!boundingBox) {
-        setBoundingBoxLast(null);
-        return;
-      }
-
-      const newBB = {
-        top: boundingBox.y,
-        left: boundingBox.x,
-        height: boundingBox.y2 - boundingBox.y,
-        width: boundingBox.x2 - boundingBox.x,
-      };
-
-      setBoundingBoxLast((prevBB) => {
-        if (
-          !prevBB ||
-          prevBB.top !== newBB.top ||
-          prevBB.left !== newBB.left ||
-          prevBB.width !== newBB.width ||
-          prevBB.height !== newBB.height
-        ) {
-          return newBB;
-        }
-        return prevBB;
-      });
-    },
-    [setBoundingBoxLast]
-  );
-  // Debounced version of update box
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const updateBoxDelay = React.useCallback(
-    debounce(() => {
-      updateBox();
-    }, 300),
-    [updateBox]
-  );
-
-  React.useEffect(() => {
-    // Update selected elements bounding box
-    updateBox();
-    updateBoxDelay(); // Delay to update after board item animation like tap/untap.
-  }, [selectedItems, itemMap, panZoomRotate, updateBox, updateBoxDelay]);
-
-  if (!boundingBoxLast || selectedItems.length < 2) return null;
-
-  return <BoundingBoxZone {...boundingBoxLast} />;
-};
-
-export const SelectedItemsPane = ({
-  hideMenu = false,
-  actionMap,
-  itemMap,
-  ItemFormComponent,
-}) => {
-  const { availableActions } = useItemActions(itemMap);
-  const [showEdit, setShowEdit] = React.useState(false);
-
-  const { t } = useTranslation();
-
-  const selectedItems = useRecoilValue(SelectedItemsAtom);
-  const boardState = useRecoilValue(BoardStateAtom);
-  const [boundingBoxLast, setBoundingBoxLast] = React.useState(null);
-
-  React.useEffect(() => {
-    const onKeyUp = (e) => {
-      // Block shortcut if we are typing in a textarea or input
-      if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
-
-      Object.keys(actionMap).forEach((key) => {
-        const { shortcut, action, edit: whileEdit } = actionMap[key];
-        if (
-          availableActions.includes(key) &&
-          e.key === shortcut &&
-          showEdit === !!whileEdit
-        ) {
-          action();
-        }
-      });
-    };
-    document.addEventListener("keyup", onKeyUp);
-    return () => {
-      document.removeEventListener("keyup", onKeyUp);
-    };
-  }, [actionMap, availableActions, showEdit]);
-
-  const onDblClick = React.useCallback(
-    (e) => {
-      const foundElement = insideClass(e.target, "item");
-
-      // We dblclick outside of an element
-      if (!foundElement) return;
-
-      if (hasClass(foundElement, "locked")) {
-        toast.info(t("Long click to select locked elements"));
-        return;
-      }
-
-      const filteredActions = availableActions.filter(
-        (action) => !actionMap[action].disableDblclick
-      );
-
-      if (e.ctrlKey && filteredActions.length > 1) {
-        // Use second action
-        actionMap[filteredActions[1]].action();
-      } else {
-        if (filteredActions.length > 0) {
-          actionMap[filteredActions[0]].action();
-        }
-      }
-    },
-    [actionMap, availableActions, t]
-  );
-
-  React.useEffect(() => {
-    document.addEventListener("dblclick", onDblClick);
-    return () => {
-      document.removeEventListener("dblclick", onDblClick);
-    };
-  }, [onDblClick]);
-
-  if (hideMenu || selectedItems.length === 0) {
-    return null;
-  }
-
-  // Keep this code for later
-  /*const onRemove = () => {
-    confirmAlert({
-      title: t("Confirmation"),
-      message: t("Do you really want to remove selected items ?"),
-      buttons: [
-        {
-          label: t("Yes"),
-          onClick: remove,
-        },
-        {
-          label: t("No"),
-          onClick: () => {},
-        },
-      ],
-    });
-  };*/
-
-  let title = "";
-  if (selectedItems.length === 1) {
-    title = t("Edit item");
-  }
-  if (selectedItems.length > 1) {
-    title = t("Edit all items");
-  }
-
-  return (
-    <>
-      <SidePanel
-        key={selectedItems[0]}
-        open={showEdit && !boardState.selecting}
-        onClose={() => {
-          setShowEdit(false);
-        }}
-        title={title}
-        width="25%"
-      >
-        <CardContent>
-          <ItemFormFactory ItemFormComponent={ItemFormComponent} />
-        </CardContent>
-      </SidePanel>
-      {selectedItems.length && !hideMenu && (
-        <ActionPane
-          {...boundingBoxLast}
-          hide={
-            boardState.zooming || boardState.panning || boardState.movingItems
-          }
-        >
-          {(selectedItems.length > 1 || boardState.selecting) && (
-            <div className="count">
-              <span className="number">{selectedItems.length}</span>
-              <span>{t("Items")}</span>
-            </div>
-          )}
-          {!boardState.selecting &&
-            availableActions.map((action) => {
-              const {
-                label,
-                action: handler,
-                multiple,
-                edit: onlyEdit,
-                shortcut,
-                icon,
-              } = actionMap[action];
-              if (multiple && selectedItems.length < 2) return null;
-              if (onlyEdit && !showEdit) return null;
-              return (
-                <button
-                  className="button clear icon-only"
-                  key={action}
-                  onClick={() => handler()}
-                  title={label + (shortcut ? ` (${shortcut})` : "")}
-                >
-                  <img
-                    src={icon}
-                    style={{ width: "32px", height: "32px" }}
-                    alt={label}
-                  />
-                </button>
-              );
-            })}
-
-          {!boardState.selecting && (
-            <button
-              className="button clear icon-only"
-              onClick={() => setShowEdit((prev) => !prev)}
-              title={t("Edit")}
-            >
-              {!showEdit && (
-                <img
-                  src="https://icongr.am/feather/edit.svg?size=32&color=ffffff"
-                  alt={t("Edit")}
-                />
-              )}
-              {showEdit && (
-                <img
-                  src="https://icongr.am/feather/edit.svg?size=32&color=db5034"
-                  alt={t("Edit")}
-                />
-              )}
-            </button>
-          )}
-        </ActionPane>
-      )}
-      {!boardState.movingItems && (
-        <BoundingBox
-          boundingBoxLast={boundingBoxLast}
-          setBoundingBoxLast={setBoundingBoxLast}
-          selectedItems={selectedItems}
-        />
-      )}
-    </>
-  );
-};
-
-export default SelectedItemsPane;

+ 0 - 50
src/components/SubscribeGameEvents.jsx

@@ -1,50 +0,0 @@
-import React from "react";
-
-import useC2C from "./hooks/useC2C";
-
-import useBoardConfig from "./useBoardConfig";
-
-export const SubscribeGameEvents = ({ getGame, setGame }) => {
-  const { c2c, isMaster } = useC2C("board");
-
-  const [, setBoardConfig] = useBoardConfig();
-
-  // if first player register callback to allow other user to load game
-  React.useEffect(() => {
-    const unsub = [];
-    if (isMaster) {
-      c2c
-        .register("getGame", async () => {
-          return await getGame();
-        })
-        .then((unregister) => {
-          unsub.push(unregister);
-        });
-    }
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [c2c, getGame, isMaster]);
-
-  // Subscribe loadGame and updateBoardConfig events
-  React.useEffect(() => {
-    const unsub = [];
-    unsub.push(
-      c2c.subscribe("loadGame", (game) => {
-        setGame(game);
-      })
-    );
-    unsub.push(
-      c2c.subscribe("updateBoardConfig", (newConfig) => {
-        setBoardConfig(newConfig, false);
-      })
-    );
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [c2c, setBoardConfig, setGame]);
-
-  return null;
-};
-
-export default SubscribeGameEvents;

+ 0 - 50
src/components/SubscribeSessionEvents.jsx

@@ -1,50 +0,0 @@
-import React from "react";
-
-import useC2C from "./hooks/useC2C";
-
-import useBoardConfig from "./useBoardConfig";
-
-export const SubscribeSessionEvents = ({ getSession, setSession }) => {
-  const { c2c, isMaster } = useC2C("board");
-
-  const [, setBoardConfig] = useBoardConfig();
-
-  // if first player register callback to allow other user to load game
-  React.useEffect(() => {
-    const unsub = [];
-    if (isMaster) {
-      c2c
-        .register("getSession", async () => {
-          return await getSession();
-        })
-        .then((unregister) => {
-          unsub.push(unregister);
-        });
-    }
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [c2c, getSession, isMaster]);
-
-  // Subscribe loadSession and updateBoardConfig events
-  React.useEffect(() => {
-    const unsub = [];
-    unsub.push(
-      c2c.subscribe("loadSession", (session) => {
-        setSession(session);
-      })
-    );
-    unsub.push(
-      c2c.subscribe("updateBoardConfig", (newConfig) => {
-        setBoardConfig(newConfig, false);
-      })
-    );
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [c2c, setBoardConfig, setSession]);
-
-  return null;
-};
-
-export default SubscribeSessionEvents;

+ 0 - 181
src/components/board/ActionPane.jsx

@@ -1,181 +0,0 @@
-import React from "react";
-
-import {
-  BoardStateAtom,
-  SelectedItemsAtom,
-  PanZoomRotateAtom,
-  BoardConfigAtom,
-} from "./";
-import { useItems } from "./Items";
-import { useSetRecoilState, useRecoilCallback } from "recoil";
-import { insideClass, hasClass } from "../utils";
-
-import Gesture from "./Gesture";
-
-const ActionPane = ({ children }) => {
-  const { moveItems, placeItems } = useItems();
-
-  const setSelectedItems = useSetRecoilState(SelectedItemsAtom);
-  const setBoardState = useSetRecoilState(BoardStateAtom);
-
-  const wrapperRef = React.useRef(null);
-  const actionRef = React.useRef({});
-
-  // Use ref as pointer events are faster than react state management
-  const selectedItemRef = React.useRef({
-    items: [],
-  });
-
-  const onDragStart = useRecoilCallback(
-    ({ snapshot }) => async ({ target, ctrlKey, metaKey, event }) => {
-      // Allow text selection instead of moving
-      if (["INPUT", "TEXTAREA"].includes(target.tagName)) return;
-
-      const foundElement = insideClass(target, "item");
-
-      if (foundElement && !hasClass(foundElement, "locked")) {
-        event.stopPropagation();
-
-        const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-
-        selectedItemRef.current.items = selectedItems;
-
-        if (!selectedItems.includes(foundElement.id)) {
-          if (ctrlKey || metaKey) {
-            selectedItemRef.current.items = [...selectedItems, foundElement.id];
-            setSelectedItems((prev) => [...prev, foundElement.id]);
-          } else {
-            selectedItemRef.current.items = [foundElement.id];
-            setSelectedItems([foundElement.id]);
-          }
-        }
-
-        Object.assign(actionRef.current, {
-          moving: true,
-          remainX: 0,
-          remainY: 0,
-        });
-      }
-    },
-    [setSelectedItems]
-  );
-
-  const onDrag = useRecoilCallback(
-    ({ snapshot }) => async ({ deltaX, deltaY }) => {
-      if (actionRef.current.moving) {
-        const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
-        const moveX = actionRef.current.remainX + deltaX / panZoomRotate.scale;
-        const moveY = actionRef.current.remainY + deltaY / panZoomRotate.scale;
-
-        moveItems(
-          selectedItemRef.current.items,
-          {
-            x: moveX,
-            y: moveY,
-          },
-          true
-        );
-
-        setBoardState((prev) =>
-          !prev.movingItems ? { ...prev, movingItems: true } : prev
-        );
-      }
-    },
-    [moveItems, setBoardState]
-  );
-
-  const onDragEnd = useRecoilCallback(
-    ({ snapshot }) => async () => {
-      if (actionRef.current.moving) {
-        const { gridSize: boardGridSize = 1 } = await snapshot.getPromise(
-          BoardConfigAtom
-        );
-        const gridSize = boardGridSize || 1; // avoid 0 grid size
-
-        actionRef.current = { moving: false };
-        placeItems(selectedItemRef.current.items, {
-          type: "grid",
-          size: gridSize,
-        });
-        setBoardState((prev) => ({ ...prev, movingItems: false }));
-      }
-    },
-    [placeItems, setBoardState]
-  );
-
-  const onKeyDown = useRecoilCallback(
-    ({ snapshot }) => async (e) => {
-      // Block shortcut if we are typing in a textarea or input
-      if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
-
-      const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-
-      if (selectedItems.length) {
-        const { gridSize: boardGridSize = 1 } = await snapshot.getPromise(
-          BoardConfigAtom
-        );
-        let moveX = 0;
-        let moveY = 0;
-        switch (e.key) {
-          case "ArrowLeft":
-            // Left pressed
-            moveX = -10;
-            break;
-          case "ArrowRight":
-            moveX = 10;
-            // Right pressed
-            break;
-          case "ArrowUp":
-            // Up pressed
-            moveY = -10;
-            break;
-          case "ArrowDown":
-            // Down pressed
-            moveY = 10;
-            break;
-        }
-        if (moveX || moveY) {
-          if (e.shiftKey) {
-            moveX = moveX * 5;
-            moveY = moveY * 5;
-          }
-          if (e.ctrlKey || e.altKey || e.metaKey) {
-            moveX = moveX / 10;
-            moveY = moveY / 10;
-          }
-          moveItems(
-            selectedItems,
-            {
-              x: moveX,
-              y: moveY,
-            },
-            true
-          );
-          const gridSize = boardGridSize || 1; // avoid 0 grid size
-
-          placeItems(selectedItems, {
-            type: "grid",
-            size: gridSize,
-          });
-          e.preventDefault();
-        }
-      }
-    },
-    [moveItems, placeItems]
-  );
-
-  React.useEffect(() => {
-    document.addEventListener("keydown", onKeyDown);
-    return () => {
-      document.removeEventListener("keydown", onKeyDown);
-    };
-  }, [onKeyDown]);
-
-  return (
-    <Gesture onDragStart={onDragStart} onDrag={onDrag} onDragEnd={onDragEnd}>
-      <div ref={wrapperRef}>{children}</div>
-    </Gesture>
-  );
-};
-
-export default ActionPane;

+ 0 - 79
src/components/board/Board.jsx

@@ -1,79 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import { ItemList, SubscribeItemEvents } from "./Items";
-import Selector from "./Selector";
-import ActionPane from "./ActionPane";
-import CursorPane from "./Cursors/CursorPane";
-import PanZoomRotate from "./PanZoomRotate";
-
-import styled from "styled-components";
-import { useRecoilValue } from "recoil";
-import { BoardConfigAtom } from "./atoms";
-
-/*
-
-  #2C3749 - #13131B
-  background: radial-gradient(circle, #2c3749, #13131b 100%), url(/board.png);
-  background-blend-mode: multiply;
-  box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px,
-    rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;
-  
-*/
-
-const Placeholder = styled.p`
-  position: fixed;
-  top: 40vh;
-  width: 100vw;
-  text-align: center;
-  color: hsl(0, 0%, 70%);
-`;
-
-const StyledBoard = styled.div.attrs(() => ({ className: "board" }))`
-  position: relative;
-  background: radial-gradient(
-      circle,
-      hsla(218, 30%, 40%, 0.7),
-      hsla(218, 40%, 40%, 0.05) 100%
-    ),
-    url(/board.png);
-
-  border: 1px solid transparent;
-
-  width: ${({ size }) => size}px;
-  height: ${({ size }) => size}px;
-
-  border-radius: 2px;
-
-  box-shadow: 0px 3px 6px #00000029;
-  user-select: none;
-`;
-
-export const Board = ({ user, users, itemMap, moveFirst = true }) => {
-  const { t } = useTranslation();
-
-  const config = useRecoilValue(BoardConfigAtom);
-
-  if (!config.size) {
-    return <Placeholder>{t("Please select or load a game")}</Placeholder>;
-  }
-
-  return (
-    <>
-      <SubscribeItemEvents />
-      <PanZoomRotate moveFirst={moveFirst}>
-        <Selector moveFirst={moveFirst}>
-          <ActionPane>
-            <CursorPane user={user} users={users}>
-              <StyledBoard size={config.size}>
-                <ItemList itemMap={itemMap} />
-              </StyledBoard>
-            </CursorPane>
-          </ActionPane>
-        </Selector>
-      </PanZoomRotate>
-    </>
-  );
-};
-
-export default Board;

+ 0 - 70
src/components/board/Cursors/Cursor.jsx

@@ -1,70 +0,0 @@
-import React from "react";
-
-import styled from "styled-components";
-
-import { readableColorIsBlack } from "color2k";
-
-const StyledCursor = styled.div.attrs(({ top, left }) => ({
-  style: {
-    transform: `translate(${left}px, ${top}px)`,
-  },
-}))`
-  top: 0;
-  left: 0;
-  position: fixed;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  z-index: 210;
-`;
-
-const CursorName = styled.div`
-  color: ${({ textColor }) => textColor};
-  font-weight: bold;
-  padding: 0 0.5em;
-  border-radius: 2px;
-  max-width: 5em;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-left: -0.5em;
-  margin-top: 1.7em;
-  whitespace: nowrap;
-  background-color: ${({ color }) => color};
-`;
-
-export const Cursor = ({ color = "#666", size = 40, pos, text }) => {
-  const textColor = readableColorIsBlack(color) ? "#222" : "#EEE";
-  return (
-    <StyledCursor top={pos.y - 10} left={pos.x - 5}>
-      <svg
-        version="1.1"
-        id="Layer_1"
-        xmlns="http://www.w3.org/2000/svg"
-        viewBox="1064.7701 445.5539 419.8101 717.0565"
-        width={size}
-        height={size}
-      >
-        <path
-          d="m 1197.1015,869.718 -62.2719,-154.49286 133.3276,-9.05842 -257.6915,-253.12748 1.2392,356.98609 88.2465,-79.47087 61.702,152.78366 z"
-          style={{
-            fill: textColor,
-          }}
-        />
-        <path
-          d="m 1193.0939,861.12419 -62.2719,-154.49286 133.3276,-9.05842 -257.6915,-253.12748 1.2392,356.98609 88.2465,-79.47087 61.702,152.78366 z"
-          style={{
-            fill: "white",
-            stroke: "#111",
-            strokeWidth: 20,
-          }}
-        />
-      </svg>
-
-      <CursorName color={color} textColor={textColor}>
-        {text}
-      </CursorName>
-    </StyledCursor>
-  );
-};
-
-export default Cursor;

+ 0 - 43
src/components/board/Cursors/CursorPane.jsx

@@ -1,43 +0,0 @@
-import React from "react";
-import Cursors from "./Cursors";
-import useC2C from "../../hooks/useC2C";
-import { PanZoomRotateAtom } from "../";
-import { useRecoilValue } from "recoil";
-
-export const Board = ({ children, user, users }) => {
-  const { c2c } = useC2C("board");
-  const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
-
-  const publish = React.useCallback(
-    (newPos) => {
-      c2c.publish("cursorMove", {
-        userId: user.id,
-        pos: newPos,
-      });
-    },
-    [c2c, user.id]
-  );
-
-  const onMouseMove = (e) => {
-    const { top, left } = e.currentTarget.getBoundingClientRect();
-    publish({
-      x: (e.clientX - left) / panZoomRotate.scale,
-      y: (e.clientY - top) / panZoomRotate.scale,
-    });
-  };
-
-  const onLeave = () => {
-    c2c.publish("cursorOff", {
-      userId: user.id,
-    });
-  };
-
-  return (
-    <div onMouseMove={onMouseMove} onMouseLeave={onLeave}>
-      {children}
-      <Cursors users={users} />
-    </div>
-  );
-};
-
-export default Board;

+ 0 - 89
src/components/board/Cursors/Cursors.jsx

@@ -1,89 +0,0 @@
-import React from "react";
-import useC2C from "../../../components/hooks/useC2C";
-import Cursor from "./Cursor";
-
-export const Cursors = ({ users }) => {
-  const { c2c } = useC2C("board");
-  const [cursors, setCursors] = React.useState({});
-
-  const preventRef = React.useRef(false);
-
-  const usersById = React.useMemo(() => {
-    return users.reduce((acc, user) => {
-      acc[user.id] = user;
-      return acc;
-    }, {});
-  }, [users]);
-
-  // Prevent race condition when removing user
-  const currentCursor = React.useMemo(() => {
-    return users.reduce((acc, user) => {
-      if (cursors[user.id]) {
-        acc[user.id] = cursors[user.id];
-      }
-      return acc;
-    }, {});
-  }, [users, cursors]);
-
-  React.useEffect(() => {
-    setCursors((prevCursors) => {
-      return users.reduce((acc, user) => {
-        if (prevCursors[user.id]) {
-          acc[user.id] = prevCursors[user.id];
-        }
-        return acc;
-      }, {});
-    });
-  }, [users]);
-
-  React.useEffect(() => {
-    const unsub = [];
-    unsub.push(
-      c2c.subscribe("cursorMove", ({ userId, pos }) => {
-        // Avoid move after cursor off
-        if (preventRef.current) return;
-
-        setCursors((prevCursors) => {
-          return {
-            ...prevCursors,
-            [userId]: pos,
-          };
-        });
-      })
-    );
-    unsub.push(
-      c2c.subscribe("cursorOff", ({ userId }) => {
-        setCursors((prevCursors) => {
-          const newCursors = {
-            ...prevCursors,
-          };
-          delete newCursors[userId];
-          return newCursors;
-        });
-        // Prevent next moves
-        preventRef.current = true;
-        setTimeout(() => {
-          preventRef.current = false;
-        }, 100);
-      })
-    );
-    return () => {
-      unsub.map((c) => c());
-    };
-  }, [c2c]);
-
-  return (
-    <div>
-      {Object.entries(currentCursor).map(([userId, pos]) => (
-        <Cursor
-          key={userId}
-          pos={pos}
-          text={usersById[userId].name}
-          color={usersById[userId].color}
-        />
-      ))}
-    </div>
-  );
-};
-
-export default Cursors;

+ 0 - 522
src/components/board/Gesture.jsx

@@ -1,522 +0,0 @@
-import React from "react";
-import platform from "platform";
-
-export const isMacOS = () => {
-  return platform.os.family === "OS X";
-};
-
-// From https://stackoverflow.com/questions/20110224/what-is-the-height-of-a-line-in-a-wheel-event-deltamode-dom-delta-line
-const getScrollLineHeight = () => {
-  const iframe = document.createElement("iframe");
-  iframe.src = "#";
-  document.body.appendChild(iframe);
-
-  // Write content in Iframe
-  const idoc = iframe.contentWindow.document;
-  idoc.open();
-  idoc.write(
-    "<!DOCTYPE html><html><head></head><body><span>a</span></body></html>"
-  );
-  idoc.close();
-
-  const scrollLineHeight = idoc.body.firstElementChild.offsetHeight;
-  document.body.removeChild(iframe);
-
-  return scrollLineHeight;
-};
-
-const LINE_HEIGHT = getScrollLineHeight();
-// Reasonable default from https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
-const PAGE_HEIGHT = 800;
-
-const otherPointer = (pointers, currentPointer) => {
-  const p2 = Object.keys(pointers)
-    .map((p) => Number(p))
-    .find((pointer) => pointer !== currentPointer);
-  return pointers[p2];
-};
-
-const computeDistance = ([x1, y1], [x2, y2]) => {
-  const distanceX = Math.abs(x1 - x2);
-  const distanceY = Math.abs(y1 - y2);
-
-  return Math.hypot(distanceX, distanceY);
-};
-
-const empty = () => {};
-
-const Gesture = ({
-  children,
-  onDrag = empty,
-  onDragStart = empty,
-  onDragEnd = empty,
-  onPan = empty,
-  onTap = empty,
-  onLongTap = empty,
-  onDoubleTap = empty,
-  onZoom,
-}) => {
-  const wrapperRef = React.useRef(null);
-  const stateRef = React.useRef({
-    moving: false,
-    pointers: {},
-    mainPointer: undefined,
-  });
-  const queueRef = React.useRef([]);
-
-  // Queue event to avoid async mess
-  const queue = React.useCallback((callback) => {
-    queueRef.current.push(async () => {
-      await callback();
-      queueRef.current.shift();
-      if (queueRef.current.length !== 0) {
-        await queueRef.current[0]();
-      }
-    });
-    if (queueRef.current.length === 1) {
-      queueRef.current[0]();
-    }
-  }, []);
-
-  const onWheel = React.useCallback(
-    (e) => {
-      const {
-        deltaX,
-        deltaY,
-        clientX,
-        clientY,
-        deltaMode,
-        ctrlKey,
-        altKey,
-        metaKey,
-        target,
-      } = e;
-
-      // On a MacOs trackpad, the pinch gesture sets the ctrlKey to true.
-      // In that situation, we want to use the custom scaling, not the browser default zoom.
-      // Hence in this situation we avoid to return immediately.
-      if (altKey || (ctrlKey && !isMacOS())) {
-        return;
-      }
-
-      // On a trackpad, the pinch and pan events are differentiated by the crtlKey value.
-      // On a pinch gesture, the ctrlKey is set to true, so we want to have a scaling effect.
-      // If we are only moving the fingers in the same direction, a pan is needed.
-      // Ref: https://medium.com/@auchenberg/detecting-multi-touch-trackpad-gestures-in-javascript-a2505babb10e
-      if (isMacOS() && !ctrlKey) {
-        queue(() =>
-          onPan({
-            deltaX: -2 * deltaX,
-            deltaY: -2 * deltaY,
-            button: 1,
-            ctrlKey,
-            metaKey,
-            target,
-            event: e,
-          })
-        );
-      } else {
-        // Quit if onZoom is not set
-        if (onZoom === undefined || !deltaY) return;
-
-        let scale = deltaY;
-
-        switch (deltaMode) {
-          case 1: // Pixel
-            scale *= LINE_HEIGHT;
-            break;
-          case 2:
-            scale *= PAGE_HEIGHT;
-            break;
-          default:
-        }
-
-        if (isMacOS()) {
-          scale *= 2;
-        }
-
-        queue(() => onZoom({ scale, clientX, clientY, event: e }));
-      }
-    },
-    [onPan, onZoom, queue]
-  );
-
-  const onPointerDown = React.useCallback(
-    ({
-      target,
-      button,
-      clientX,
-      clientY,
-      pointerId,
-      altKey,
-      ctrlKey,
-      metaKey,
-      isPrimary,
-    }) => {
-      // Add pointer to map
-      stateRef.current.pointers[pointerId] = { clientX, clientY };
-
-      if (isPrimary) {
-        // Clean mainPoint on primary pointer
-        stateRef.current.mainPointer = undefined;
-      }
-
-      if (stateRef.current.mainPointer !== undefined) {
-        if (stateRef.current.mainPointer !== pointerId) {
-          // This is not the main pointer
-          try {
-            const { clientX: clientX2, clientY: clientY2 } = otherPointer(
-              stateRef.current.pointers,
-              pointerId
-            );
-            const newClientX = (clientX2 + clientX) / 2;
-            const newClientY = (clientY2 + clientY) / 2;
-
-            const distance = computeDistance(
-              [clientX2, clientY2],
-              [clientX, clientY]
-            );
-
-            // We update previous position as the new position is the center beetween both finger
-            Object.assign(stateRef.current, {
-              pressed: true,
-              moving: false,
-              gestureStart: false,
-              startX: clientX,
-              startY: clientY,
-              prevX: newClientX,
-              prevY: newClientY,
-              startDistance: distance,
-              prevDistance: distance,
-            });
-          } catch (e) {
-            console.log("Error while getting other pointer. Ignoring", e);
-            stateRef.current.mainPointer === undefined;
-          }
-        }
-
-        return;
-      }
-
-      // We set the mainpointer
-      stateRef.current.mainPointer = pointerId;
-
-      // And prepare move
-      Object.assign(stateRef.current, {
-        pressed: true,
-        moving: false,
-        gestureStart: false,
-        startX: clientX,
-        startY: clientY,
-        prevX: clientX,
-        prevY: clientY,
-        currentButton: button,
-        target,
-        timeStart: Date.now(),
-        longTapTimeout: setTimeout(async () => {
-          stateRef.current.noTap = true;
-          queue(() =>
-            onLongTap({
-              clientX,
-              clientY,
-              altKey,
-              ctrlKey,
-              metaKey,
-              target,
-            })
-          );
-        }, 750),
-      });
-
-      try {
-        target.setPointerCapture(pointerId);
-      } catch (e) {
-        console.log("Fail to capture pointer", e);
-      }
-    },
-    [onLongTap, queue]
-  );
-
-  const onPointerMove = React.useCallback(
-    (e) => {
-      if (stateRef.current.pressed) {
-        const {
-          pointerId,
-          clientX: eventClientX,
-          clientY: eventClientY,
-          altKey,
-          ctrlKey,
-          metaKey,
-          pointerType,
-        } = e;
-
-        if (stateRef.current.mainPointer !== pointerId) {
-          // Event from other pointer
-          stateRef.current.pointers[pointerId] = {
-            clientX: eventClientX,
-            clientY: eventClientY,
-          };
-          return;
-        }
-
-        stateRef.current.moving = true;
-
-        // Do we have two fingers ?
-        const twoFingers = Object.keys(stateRef.current.pointers).length === 2;
-
-        let clientX, clientY, distance;
-
-        if (twoFingers) {
-          // Find other pointerId
-          const { clientX: clientX2, clientY: clientY2 } = otherPointer(
-            stateRef.current.pointers,
-            pointerId
-          );
-
-          // Update client X with the center of each touch
-          clientX = (clientX2 + eventClientX) / 2;
-          clientY = (clientY2 + eventClientY) / 2;
-          distance = computeDistance(
-            [clientX2, clientY2],
-            [eventClientX, eventClientY]
-          );
-        } else {
-          clientX = eventClientX;
-          clientY = eventClientY;
-        }
-
-        // We drag if
-        // On non touch device
-        //   - Button is 0
-        //   - Alt key is no pressed
-        // or on touch devices
-        //   - We use only one finger
-        const shouldDrag =
-          pointerType !== "touch"
-            ? stateRef.current.currentButton === 0 && !altKey
-            : !twoFingers;
-
-        if (shouldDrag) {
-          // Send drag start on first move
-          if (!stateRef.current.gestureStart) {
-            wrapperRef.current.style.cursor = "move";
-            stateRef.current.gestureStart = true;
-            // Clear tap timeout
-            clearTimeout(stateRef.current.longTapTimeout);
-
-            queue(() =>
-              onDragStart({
-                deltaX: 0,
-                deltaY: 0,
-                startX: stateRef.current.startX,
-                startY: stateRef.current.startY,
-                distanceX: 0,
-                distanceY: 0,
-                button: stateRef.current.currentButton,
-                altKey,
-                ctrlKey,
-                metaKey,
-                target: stateRef.current.target,
-                event: e,
-              })
-            );
-          }
-          // Create closure
-          const deltaX = clientX - stateRef.current.prevX;
-          const deltaY = clientY - stateRef.current.prevY;
-          const distanceX = clientX - stateRef.current.startX;
-          const distanceY = clientY - stateRef.current.startY;
-
-          // Drag event
-          queue(() =>
-            onDrag({
-              deltaX,
-              deltaY,
-              startX: stateRef.current.startX,
-              startY: stateRef.current.startY,
-              distanceX,
-              distanceY,
-              button: stateRef.current.currentButton,
-              altKey,
-              ctrlKey,
-              metaKey,
-              target: stateRef.current.target,
-              event: e,
-            })
-          );
-        } else {
-          if (!stateRef.current.gestureStart) {
-            wrapperRef.current.style.cursor = "move";
-            stateRef.current.gestureStart = true;
-            // Clear tap timeout on first move
-            clearTimeout(stateRef.current.longTapTimeout);
-          }
-
-          // Create closure
-          const deltaX = clientX - stateRef.current.prevX;
-          const deltaY = clientY - stateRef.current.prevY;
-          const target = stateRef.current.target;
-
-          // Pan event
-          queue(() =>
-            onPan({
-              deltaX,
-              deltaY,
-              button: stateRef.current.currentButton,
-              altKey,
-              ctrlKey,
-              metaKey,
-              target,
-              event: e,
-            })
-          );
-
-          if (
-            twoFingers &&
-            distance !== stateRef.current.prevDistance &&
-            onZoom
-          ) {
-            const scale = stateRef.current.prevDistance - distance;
-
-            if (Math.abs(scale) > 0) {
-              queue(() =>
-                onZoom({
-                  scale,
-                  clientX,
-                  clientY,
-                  event: e,
-                })
-              );
-              stateRef.current.prevDistance = distance;
-            }
-          }
-        }
-
-        stateRef.current.prevX = clientX;
-        stateRef.current.prevY = clientY;
-      }
-    },
-    [onDrag, onDragStart, onPan, onZoom, queue]
-  );
-
-  const onPointerUp = React.useCallback(
-    (e) => {
-      const {
-        clientX,
-        clientY,
-        altKey,
-        ctrlKey,
-        metaKey,
-        target,
-        pointerId,
-      } = e;
-
-      if (!stateRef.current.pointers[pointerId]) {
-        // Pointer already gone previously with another event
-        // ignoring it
-        return;
-      }
-
-      // Remove pointer from map
-      delete stateRef.current.pointers[pointerId];
-
-      if (stateRef.current.mainPointer !== pointerId) {
-        // If this is not the main pointer we quit here
-        return;
-      }
-
-      while (Object.keys(stateRef.current.pointers).length > 0) {
-        // If was main pointer but we have another one, this one become main
-        stateRef.current.mainPointer = Number(
-          Object.keys(stateRef.current.pointers)[0]
-        );
-        try {
-          stateRef.current.target.setPointerCapture(
-            stateRef.current.mainPointer
-          );
-          return;
-        } catch (e) {
-          console.log("Fails to set pointer capture", e);
-          stateRef.current.mainPointer = undefined;
-          delete stateRef.current.pointers[
-            Object.keys(stateRef.current.pointers)[0]
-          ];
-        }
-      }
-
-      stateRef.current.mainPointer = undefined;
-      stateRef.current.pressed = false;
-
-      // Clear longTap
-      clearTimeout(stateRef.current.longTapTimeout);
-
-      if (stateRef.current.moving) {
-        // If we were moving, send drag end event
-        stateRef.current.moving = false;
-        queue(() =>
-          onDragEnd({
-            deltaX: clientX - stateRef.current.prevX,
-            deltaY: clientY - stateRef.current.prevY,
-            startX: stateRef.current.startX,
-            startY: stateRef.current.startY,
-            distanceX: clientX - stateRef.current.startX,
-            distanceY: clientY - stateRef.current.startY,
-            button: stateRef.current.currentButton,
-            altKey,
-            ctrlKey,
-            metaKey,
-            event: e,
-          })
-        );
-        wrapperRef.current.style.cursor = "auto";
-      } else {
-        const now = Date.now();
-
-        if (stateRef.current.noTap) {
-          stateRef.current.noTap = false;
-        } else {
-          // Send tap event only if time less than 300ms
-          if (stateRef.current.timeStart - now < 300) {
-            queue(() =>
-              onTap({
-                clientX,
-                clientY,
-                altKey,
-                ctrlKey,
-                metaKey,
-                target,
-              })
-            );
-          }
-        }
-      }
-    },
-    [onDragEnd, onTap, queue]
-  );
-
-  const onDoubleTapHandler = React.useCallback(
-    (event) => {
-      onDoubleTap(event);
-    },
-    [onDoubleTap]
-  );
-
-  return (
-    <div
-      onWheel={onWheel}
-      onPointerDown={onPointerDown}
-      onPointerMove={onPointerMove}
-      onPointerUp={onPointerUp}
-      onPointerOut={onPointerUp}
-      onPointerLeave={onPointerUp}
-      onPointerCancel={onPointerUp}
-      onDoubleClick={onDoubleTapHandler}
-      style={{ touchAction: "none" }}
-      ref={wrapperRef}
-    >
-      {children}
-    </div>
-  );
-};
-
-export default Gesture;

+ 0 - 182
src/components/board/Items/Item.jsx

@@ -1,182 +0,0 @@
-import React, { memo } from "react";
-
-import styled from "styled-components";
-import lockIcon from "../../../images/lock.svg";
-
-const ItemWrapper = styled.div.attrs(({ rotation, locked, selected }) => {
-  let className = "item";
-  if (locked) {
-    className += " locked";
-  }
-  if (selected) {
-    className += " selected";
-  }
-  return {
-    className,
-    style: {
-      transform: `rotate(${rotation}deg)`,
-    },
-  };
-})`
-  display: inline-block;
-  transition: transform 150ms;
-  user-select: none;
-
-  & .corner {
-    position: absolute;
-    width: 0px;
-    height: 0px;
-  }
-
-  & .top-left {
-    top: 0;
-    left: 0;
-  }
-  & .top-right {
-    top: 0;
-    right: 0;
-  }
-  & .bottom-left {
-    bottom: 0;
-    left: 0;
-  }
-  & .bottom-right {
-    bottom: 0;
-    right: 0;
-  }
-
-  padding: 4px;
-
-  &.selected {
-    border: 2px dashed var(--color-primary);
-    padding: 2px;
-    cursor: pointer;
-  }
-
-  &.locked::after {
-    content: "";
-    position: absolute;
-    width: 24px;
-    height: 30px;
-    top: 4px;
-    right: 4px;
-    opacity: 0.1;
-    background-image: url(${lockIcon});
-    background-size: cover;
-    user-select: none;
-  }
-
-  &.locked:hover::after {
-    opacity: 0.3;
-  }
-`;
-
-const Item = ({
-  setState,
-  state: { type, rotation = 0, id, locked, layer, ...rest } = {},
-  animate = "hvr-pop",
-  isSelected,
-  itemMap,
-  unlocked,
-}) => {
-  const itemRef = React.useRef(null);
-  const isMountedRef = React.useRef(false);
-  const animateRef = React.useRef(null);
-
-  const Component = itemMap[type].component || null;
-
-  const updateState = React.useCallback(
-    (callbackOrItem, sync = true) => setState(id, callbackOrItem, sync),
-    [setState, id]
-  );
-
-  // Update actual size when update
-  React.useEffect(() => {
-    isMountedRef.current = true;
-    return () => {
-      isMountedRef.current = false;
-    };
-  }, []);
-
-  React.useEffect(() => {
-    animateRef.current.className = animate;
-  }, [animate]);
-
-  const removeClass = (e) => {
-    e.target.className = "";
-  };
-
-  return (
-    <ItemWrapper
-      rotation={rotation}
-      locked={locked && !unlocked}
-      selected={isSelected}
-      ref={itemRef}
-      layer={layer}
-      id={id}
-    >
-      <div
-        ref={animateRef}
-        onAnimationEnd={removeClass}
-        onKeyDown={(e) => e.stopPropagation()}
-        onKeyUp={(e) => e.stopPropagation()}
-      >
-        <Component {...rest} setState={updateState} />
-        <div className="corner top-left"></div>
-        <div className="corner top-right"></div>
-        <div className="corner bottom-left"></div>
-        <div className="corner bottom-right"></div>
-      </div>
-    </ItemWrapper>
-  );
-};
-
-const MemoizedItem = memo(
-  Item,
-  (
-    {
-      state: prevState,
-      setState: prevSetState,
-      isSelected: prevIsSelected,
-      unlocked: prevUnlocked,
-    },
-    {
-      state: nextState,
-      setState: nextSetState,
-      isSelected: nextIsSelected,
-      unlocked: nextUnlocked,
-    }
-  ) => {
-    return (
-      prevIsSelected === nextIsSelected &&
-      prevUnlocked === nextUnlocked &&
-      prevSetState === nextSetState &&
-      JSON.stringify(prevState) === JSON.stringify(nextState)
-    );
-  }
-);
-
-// Exclude positionning from memoization
-const PositionedItem = ({
-  state: { x, y, layer, moving, ...stateRest } = {},
-  ...rest
-}) => {
-  return (
-    <div
-      style={{
-        transform: `translate(${x}px, ${y}px)`,
-        display: "inline-block",
-        zIndex: ((layer || 0) + 4) * 10 + 100 + (moving ? 5 : 0), // Items z-index between 100 and 200
-        position: "absolute",
-        top: 0,
-        left: 0,
-      }}
-    >
-      <MemoizedItem {...rest} state={stateRest} />
-    </div>
-  );
-};
-
-const MemoizedPositionedItem = memo(PositionedItem);
-
-export default MemoizedPositionedItem;

+ 0 - 54
src/components/board/Items/ItemFormFactory.jsx

@@ -1,54 +0,0 @@
-import React from "react";
-import { useRecoilValue } from "recoil";
-
-import { Form } from "react-final-form";
-
-import { ItemMapAtom, SelectedItemsAtom } from "../atoms";
-import { useItems } from "./";
-
-import AutoSave from "../../ui/formUtils/AutoSave";
-
-export const getFormFieldComponent = (type, itemMap) => {
-  if (type in itemMap) {
-    return itemMap[type].form;
-  }
-  return () => null;
-};
-
-const ItemFormFactory = ({ ItemFormComponent }) => {
-  const { batchUpdateItems } = useItems();
-
-  const selectedItems = useRecoilValue(SelectedItemsAtom);
-  const itemMap = useRecoilValue(ItemMapAtom);
-
-  const onSubmitHandler = React.useCallback(
-    (formValues) => {
-      batchUpdateItems(selectedItems, (item) => ({
-        ...item,
-        ...formValues,
-      }));
-    },
-    [batchUpdateItems, selectedItems]
-  );
-
-  return (
-    <Form
-      onSubmit={onSubmitHandler}
-      render={() => (
-        <div
-          style={{
-            display: "flex",
-            flexDirection: "column",
-          }}
-        >
-          <AutoSave save={onSubmitHandler} />
-          <ItemFormComponent
-            items={selectedItems.map((itemId) => itemMap[itemId])}
-          />
-        </div>
-      )}
-    />
-  );
-};
-
-export default React.memo(ItemFormFactory);

+ 0 - 52
src/components/board/Items/ItemList.jsx

@@ -1,52 +0,0 @@
-import React from "react";
-import Item from "./Item";
-import useItems from "./useItems";
-import { ItemListAtom, ItemMapAtom, SelectedItemsAtom } from "../";
-import { useRecoilValue } from "recoil";
-
-/** Allow to operate on locked items while u or l key is pressed  */
-const useUnlock = () => {
-  const [unlock, setUnlock] = React.useState(false);
-
-  React.useEffect(() => {
-    const onKeyDown = (e) => {
-      if (e.key === "u" || e.key === "l") {
-        setUnlock(true);
-      }
-    };
-    const onKeyUp = (e) => {
-      if (e.key === "u" || e.key === "l") {
-        setUnlock(false);
-      }
-    };
-    document.addEventListener("keydown", onKeyDown);
-    document.addEventListener("keyup", onKeyUp);
-    return () => {
-      document.removeEventListener("keydown", onKeyDown);
-      document.removeEventListener("keyup", onKeyUp);
-    };
-  }, []);
-
-  return unlock;
-};
-
-const ItemList = ({ itemMap: itemMapConfig }) => {
-  const { updateItem } = useItems();
-  const itemList = useRecoilValue(ItemListAtom);
-  const itemMap = useRecoilValue(ItemMapAtom);
-  const selectedItems = useRecoilValue(SelectedItemsAtom);
-  const unlocked = useUnlock();
-
-  return itemList.map((itemId) => (
-    <Item
-      key={itemId}
-      state={itemMap[itemId]}
-      setState={updateItem}
-      isSelected={selectedItems.includes(itemId)}
-      unlocked={unlocked}
-      itemMap={itemMapConfig}
-    />
-  ));
-};
-
-export default ItemList;

+ 0 - 77
src/components/board/Items/SubscribeItemEvents.jsx

@@ -1,77 +0,0 @@
-import React from "react";
-import useC2C from "../../../components/hooks/useC2C";
-import useItems from "./useItems";
-import { useSetRecoilState } from "recoil";
-
-import { ItemMapAtom } from "../";
-
-export const SubcribeItemEvents = () => {
-  const { c2c } = useC2C("board");
-
-  const setItemMap = useSetRecoilState(ItemMapAtom);
-
-  const {
-    updateItemOrder,
-    moveItems,
-    removeItems,
-    insertItemBefore,
-  } = useItems();
-
-  const batchUpdate = React.useCallback(
-    (updatedItems) => {
-      setItemMap((prevItemMap) => {
-        return { ...prevItemMap, ...updatedItems };
-      });
-    },
-    [setItemMap]
-  );
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe("batchItemsUpdate", (updatedItems) => {
-      batchUpdate(updatedItems);
-    });
-    return unsub;
-  }, [c2c, batchUpdate]);
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe(
-      "selectedItemsMove",
-      ({ itemIds, posDelta }) => {
-        moveItems(itemIds, posDelta, false);
-      }
-    );
-    return unsub;
-  }, [c2c, moveItems]);
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe("updateItemListOrder", (itemIds) => {
-      updateItemOrder(itemIds, false);
-    });
-    return unsub;
-  }, [c2c, updateItemOrder]);
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe("pushItem", (newItem) => {
-      insertItemBefore(newItem, null, false);
-    });
-    return unsub;
-  }, [c2c, insertItemBefore]);
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe("insertItemBefore", ([newItem, beforeId]) => {
-      insertItemBefore(newItem, beforeId, false);
-    });
-    return unsub;
-  }, [c2c, insertItemBefore]);
-
-  React.useEffect(() => {
-    const unsub = c2c.subscribe("removeItems", (itemIds) => {
-      removeItems(itemIds, false);
-    });
-    return unsub;
-  }, [c2c, removeItems]);
-
-  return null;
-};
-
-export default SubcribeItemEvents;

+ 0 - 3
src/components/board/Items/index.jsx

@@ -1,3 +0,0 @@
-export { default as ItemList } from "./ItemList";
-export { default as useItems } from "./useItems";
-export { default as SubscribeItemEvents } from "./SubscribeItemEvents";

+ 0 - 93
src/components/board/Items/useItemActions.jsx

@@ -1,93 +0,0 @@
-import React from "react";
-
-import { useRecoilValue, useRecoilCallback } from "recoil";
-import { SelectedItemsAtom } from "../";
-
-import intersection from "lodash.intersection";
-import { ItemMapAtom } from "../";
-
-export const getDefaultActionsFromItem = (item, itemMap) => {
-  if (item.type in itemMap) {
-    const actions = itemMap[item.type].defaultActions;
-    if (typeof actions === "function") {
-      return actions(item);
-    }
-    return actions;
-  }
-
-  return [];
-};
-
-export const getAvailableActionsFromItem = (item, itemMap) => {
-  if (item.type in itemMap) {
-    const actions = itemMap[item.type].availableActions;
-    if (typeof actions === "function") {
-      return actions(item);
-    }
-    return actions;
-  }
-
-  return [];
-};
-
-export const getActionsFromItem = (item, itemMap) => {
-  const { actions = getDefaultActionsFromItem(item, itemMap) } = item;
-  // Filter availableActions to keep same order
-  return getAvailableActionsFromItem(item, itemMap).filter((action) =>
-    actions.includes(action)
-  );
-};
-
-export const useItemActions = (itemMap) => {
-  const selected = useRecoilValue(SelectedItemsAtom);
-  const [availableActions, setAvailableActions] = React.useState([]);
-  const isMountedRef = React.useRef(false);
-
-  const getItemListOrSelected = useRecoilCallback(
-    ({ snapshot }) => async (itemIds) => {
-      const itemMap = await snapshot.getPromise(ItemMapAtom);
-      if (itemIds) {
-        return [itemIds, itemIds.map((id) => itemMap[id])];
-      } else {
-        const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-        return [selectedItems, selectedItems.map((id) => itemMap[id])];
-      }
-    },
-    []
-  );
-
-  React.useEffect(() => {
-    // Mounted guard
-    isMountedRef.current = true;
-    return () => {
-      isMountedRef.current = false;
-    };
-  }, []);
-
-  const updateAvailableActions = React.useCallback(async () => {
-    const [selectedItemIds, selectedItemList] = await getItemListOrSelected();
-    if (selectedItemIds.length > 0) {
-      // Prevent set state on unmounted component
-      if (!isMountedRef.current) return;
-
-      const allActions = selectedItemList.reduce((acc, item) => {
-        return intersection(acc, getActionsFromItem(item, itemMap));
-      }, getActionsFromItem(selectedItemList[0], itemMap));
-
-      setAvailableActions(allActions);
-    } else {
-      setAvailableActions([]);
-    }
-  }, [getItemListOrSelected, itemMap]);
-
-  // Update available actions when selection change
-  React.useEffect(() => {
-    updateAvailableActions();
-  }, [updateAvailableActions, selected]);
-
-  return {
-    availableActions,
-  };
-};
-
-export default useItemActions;

+ 0 - 43
src/components/board/Items/useItemInteraction.js

@@ -1,43 +0,0 @@
-import React from "react";
-import { useSetRecoilState, useRecoilCallback } from "recoil";
-
-import { ItemInteractionsAtom } from "../atoms";
-
-const useItemInteraction = (interaction) => {
-  const setInteractions = useSetRecoilState(ItemInteractionsAtom);
-
-  const register = React.useCallback(
-    (callback) => {
-      setInteractions((prev) => {
-        if (!prev[interaction]) {
-          prev[interaction] = [];
-        }
-        const newInter = [...prev[interaction]];
-        newInter.push(callback);
-        return {
-          ...prev,
-          [interaction]: newInter,
-        };
-      });
-      return () => {
-        setInteractions((prev) => ({
-          ...prev,
-          [interaction]: prev[interaction].filter((c) => c !== callback),
-        }));
-      };
-    },
-    [interaction, setInteractions]
-  );
-
-  const call = useRecoilCallback(({ snapshot }) => async (items) => {
-    const itemInteractions = await snapshot.getPromise(ItemInteractionsAtom);
-    if (!itemInteractions[interaction]) return;
-    itemInteractions[interaction].forEach((callback) => {
-      callback(items);
-    });
-  });
-
-  return { register, call };
-};
-
-export default useItemInteraction;

+ 0 - 374
src/components/board/Items/useItems.jsx

@@ -1,374 +0,0 @@
-import React from "react";
-import useC2C from "../../../components/hooks/useC2C";
-import { useSetRecoilState, useRecoilCallback } from "recoil";
-
-import { ItemListAtom, SelectedItemsAtom, ItemMapAtom } from "../";
-import useItemInteraction from "./useItemInteraction";
-
-const useItems = () => {
-  const { c2c } = useC2C("board");
-  const { call: callPlaceInteractions } = useItemInteraction("place");
-
-  const setItemList = useSetRecoilState(ItemListAtom);
-  const setItemMap = useSetRecoilState(ItemMapAtom);
-  const setSelectItems = useSetRecoilState(SelectedItemsAtom);
-
-  const batchUpdateItems = useRecoilCallback(
-    ({ snapshot }) => async (itemIds, callbackOrItem, sync = true) => {
-      let callback = callbackOrItem;
-      if (typeof callbackOrItem === "object") {
-        callback = () => callbackOrItem;
-      }
-      const itemList = await snapshot.getPromise(ItemListAtom);
-
-      const orderedItemIds = itemList.filter((id) => itemIds.includes(id));
-
-      setItemMap((prevItemMap) => {
-        const result = { ...prevItemMap };
-        const updatedItems = {};
-        orderedItemIds.forEach((id) => {
-          const newItem = { ...callback(prevItemMap[id]) };
-          result[id] = newItem;
-          updatedItems[id] = newItem;
-        });
-        if (sync) {
-          c2c.publish("batchItemsUpdate", updatedItems);
-        }
-        return result;
-      });
-    },
-    [c2c, setItemMap]
-  );
-
-  const setItemListFull = React.useCallback(
-    (items) => {
-      setItemMap(
-        items.reduce((acc, item) => {
-          if (item && item.id) {
-            acc[item.id] = item;
-          }
-          return acc;
-        }, {})
-      );
-      setItemList(items.map(({ id }) => id));
-    },
-    [setItemList, setItemMap]
-  );
-
-  const updateItem = React.useCallback(
-    (id, callbackOrItem, sync = true) => {
-      batchUpdateItems([id], callbackOrItem, sync);
-    },
-    [batchUpdateItems]
-  );
-
-  const moveItems = React.useCallback(
-    (itemIds, posDelta, sync = true) => {
-      setItemMap((prevItemMap) => {
-        const result = { ...prevItemMap };
-        itemIds.forEach((id) => {
-          const item = prevItemMap[id];
-
-          result[id] = {
-            ...item,
-            x: item.x + posDelta.x,
-            y: item.y + posDelta.y,
-            moving: true,
-          };
-        });
-        return result;
-      });
-
-      if (sync) {
-        c2c.publish("selectedItemsMove", {
-          itemIds,
-          posDelta,
-        });
-      }
-    },
-    [c2c, setItemMap]
-  );
-
-  const putItemsOnTop = React.useCallback(
-    (itemIdsToMove, sync = true) => {
-      setItemList((prevItemList) => {
-        const filtered = prevItemList.filter(
-          (id) => !itemIdsToMove.includes(id)
-        );
-        const toBePutOnTop = prevItemList.filter((id) =>
-          itemIdsToMove.includes(id)
-        );
-        const result = [...filtered, ...toBePutOnTop];
-        if (sync) {
-          c2c.publish("updateItemListOrder", result);
-        }
-        return result;
-      });
-    },
-    [setItemList, c2c]
-  );
-
-  const stickOnGrid = React.useCallback(
-    (itemIds, { type: globalType, size: globalSize } = {}, sync = true) => {
-      const updatedItems = {};
-      setItemMap((prevItemMap) => {
-        const result = { ...prevItemMap };
-        itemIds.forEach((id) => {
-          const item = prevItemMap[id];
-          const elem = document.getElementById(id);
-
-          const { type: itemType, size: itemSize } = item.grid || {};
-          let type = globalType;
-          let size = globalSize || 1;
-          // If item specific
-          if (itemType) {
-            type = itemType;
-            size = itemSize;
-          }
-
-          const [centerX, centerY] = [
-            item.x + elem.clientWidth / 2,
-            item.y + elem.clientHeight / 2,
-          ];
-
-          let newX, newY, sizeX, sizeY, px1, px2, py1, py2, h, diff1, diff2;
-          h = size / 1.1547;
-
-          switch (type) {
-            case "grid":
-              newX = Math.round(centerX / size) * size;
-              newY = Math.round(centerY / size) * size;
-              break;
-            case "hexH":
-              sizeX = 2 * h;
-              sizeY = 3 * size;
-              px1 = Math.round(centerX / sizeX) * sizeX;
-              py1 = Math.round(centerY / sizeY) * sizeY;
-
-              px2 = px1 > centerX ? px1 - h : px1 + h;
-              py2 = py1 > centerY ? py1 - 1.5 * size : py1 + 1.5 * size;
-
-              diff1 = Math.hypot(...[px1 - centerX, py1 - centerY]);
-              diff2 = Math.hypot(...[px2 - centerX, py2 - centerY]);
-
-              if (diff1 < diff2) {
-                newX = px1;
-                newY = py1;
-              } else {
-                newX = px2;
-                newY = py2;
-              }
-              break;
-            case "hexV":
-              sizeX = 3 * size;
-              sizeY = 2 * h;
-              px1 = Math.round(centerX / sizeX) * sizeX;
-              py1 = Math.round(centerY / sizeY) * sizeY;
-
-              px2 = px1 > centerX ? px1 - 1.5 * size : px1 + 1.5 * size;
-              py2 = py1 > centerY ? py1 - h : py1 + h;
-
-              diff1 = Math.hypot(...[px1 - centerX, py1 - centerY]);
-              diff2 = Math.hypot(...[px2 - centerX, py2 - centerY]);
-
-              if (diff1 < diff2) {
-                newX = px1;
-                newY = py1;
-              } else {
-                newX = px2;
-                newY = py2;
-              }
-              break;
-            default:
-              newX = item.x;
-              newY = item.y;
-          }
-
-          result[id] = {
-            ...item,
-            x: newX - elem.clientWidth / 2,
-            y: newY - elem.clientHeight / 2,
-          };
-          updatedItems[id] = result[id];
-        });
-        return result;
-      });
-
-      if (sync) {
-        c2c.publish("batchItemsUpdate", updatedItems);
-      }
-    },
-    [c2c, setItemMap]
-  );
-
-  const placeItems = React.useCallback(
-    (itemIds, gridConfig, sync = true) => {
-      // Put moved items on top
-      putItemsOnTop(itemIds, sync);
-      // Remove moving state
-      batchUpdateItems(
-        itemIds,
-        (item) => {
-          const newItem = { ...item };
-          delete newItem["moving"];
-          return newItem;
-        },
-        sync
-      );
-      stickOnGrid(itemIds, gridConfig, sync);
-      callPlaceInteractions(itemIds);
-    },
-    [batchUpdateItems, callPlaceInteractions, putItemsOnTop, stickOnGrid]
-  );
-
-  const updateItemOrder = React.useCallback(
-    (newOrder, sync = true) => {
-      setItemList(newOrder);
-      if (sync) {
-        c2c.publish("updateItemListOrder", newOrder);
-      }
-    },
-    [c2c, setItemList]
-  );
-
-  const reverseItemsOrder = React.useCallback(
-    (itemIdsToReverse, sync = true) => {
-      setItemList((prevItemList) => {
-        const toBeReversed = prevItemList.filter((id) =>
-          itemIdsToReverse.includes(id)
-        );
-        const result = prevItemList.map((itemId) => {
-          if (itemIdsToReverse.includes(itemId)) {
-            return toBeReversed.pop();
-          }
-          return itemId;
-        });
-        if (sync) {
-          c2c.publish("updateItemListOrder", result);
-        }
-        return result;
-      });
-    },
-    [setItemList, c2c]
-  );
-
-  const swapItems = useRecoilCallback(
-    ({ snapshot }) => async (fromIds, toIds, sync = true) => {
-      const itemMap = await snapshot.getPromise(ItemMapAtom);
-      const fromItems = fromIds.map((id) => itemMap[id]);
-      const toItems = toIds.map((id) => itemMap[id]);
-
-      const replaceMapItems = toIds.reduce((theMap, id) => {
-        theMap[id] = fromItems.shift();
-        return theMap;
-      }, {});
-
-      setItemMap((prevItemMap) => {
-        const updatedItems = toItems.reduce((prev, toItem) => {
-          const replaceBy = replaceMapItems[toItem.id];
-          const newItem = {
-            ...toItem,
-            x: replaceBy.x,
-            y: replaceBy.y,
-          };
-          prev[toItem.id] = newItem;
-          return prev;
-        }, {});
-        if (sync) {
-          c2c.publish("batchItemsUpdate", updatedItems);
-        }
-        return { ...prevItemMap, ...updatedItems };
-      });
-
-      const replaceMap = fromIds.reduce((theMap, id) => {
-        theMap[id] = toIds.shift();
-        return theMap;
-      }, {});
-
-      setItemList((prevItemList) => {
-        const result = prevItemList.map((itemId) => {
-          if (fromIds.includes(itemId)) {
-            return replaceMap[itemId];
-          }
-          return itemId;
-        });
-
-        if (sync) {
-          c2c.publish("updateItemListOrder", result);
-        }
-        return result;
-      });
-    },
-    [c2c, setItemList, setItemMap]
-  );
-
-  const insertItemBefore = useRecoilCallback(
-    () => (newItem, beforeId, sync = true) => {
-      setItemMap((prevItemMap) => ({
-        ...prevItemMap,
-        [newItem.id]: newItem,
-      }));
-
-      setItemList((prevItemList) => {
-        if (beforeId) {
-          const insertAt = prevItemList.findIndex((id) => id === beforeId);
-
-          const newItemList = [...prevItemList];
-          newItemList.splice(insertAt, 0, newItem.id);
-          return newItemList;
-        } else {
-          return [...prevItemList, newItem.id];
-        }
-      });
-      if (sync) {
-        c2c.publish("insertItemBefore", [newItem, beforeId]);
-      }
-    },
-    [c2c, setItemList, setItemMap]
-  );
-
-  const removeItems = React.useCallback(
-    (itemsIdToRemove, sync = true) => {
-      // Remove from selected items first
-      setSelectItems((prevList) => {
-        return prevList.filter((id) => !itemsIdToRemove.includes(id));
-      });
-
-      setItemList((prevItemList) => {
-        return prevItemList.filter(
-          (itemId) => !itemsIdToRemove.includes(itemId)
-        );
-      });
-
-      setItemMap((prevItemMap) => {
-        const result = { ...prevItemMap };
-        itemsIdToRemove.forEach((id) => {
-          delete result[id];
-        });
-        return result;
-      });
-
-      if (sync) {
-        c2c.publish("removeItems", itemsIdToRemove);
-      }
-    },
-    [c2c, setItemList, setItemMap, setSelectItems]
-  );
-
-  return {
-    putItemsOnTop,
-    batchUpdateItems,
-    updateItemOrder,
-    moveItems,
-    placeItems,
-    updateItem,
-    swapItems,
-    reverseItemsOrder,
-    setItemList: setItemListFull,
-    pushItem: insertItemBefore,
-    removeItems,
-    insertItemBefore,
-  };
-};
-
-export default useItems;

+ 0 - 347
src/components/board/PanZoomRotate.jsx

@@ -1,347 +0,0 @@
-import React from "react";
-
-import {
-  useRecoilState,
-  useRecoilValue,
-  useSetRecoilState,
-  useRecoilCallback,
-} from "recoil";
-import {
-  BoardConfigAtom,
-  BoardStateAtom,
-  PanZoomRotateAtom,
-  SelectedItemsAtom,
-} from "./";
-import { insideClass } from "../utils";
-
-import usePrevious from "../hooks/usePrevious";
-
-import styled from "styled-components";
-
-import debounce from "lodash.debounce";
-
-import Gesture from "./Gesture";
-import usePositionNavigator from "./usePositionNavigator";
-
-const TOLERANCE = 100;
-
-const Pane = styled.div.attrs(({ translateX, translateY, scale, rotate }) => ({
-  style: {
-    transform: `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotate}deg)`,
-  },
-  className: "board-pane",
-}))`
-  transform-origin: top left;
-  display: inline-block;
-`;
-
-const PanZoomRotate = ({ children, moveFirst }) => {
-  const [scaleBoundaries, setScaleBoundaries] = React.useState([0.1, 8]);
-  const [dim, setDim] = useRecoilState(PanZoomRotateAtom);
-  const config = useRecoilValue(BoardConfigAtom);
-  const setBoardState = useSetRecoilState(BoardStateAtom);
-  const prevDim = usePrevious(dim);
-
-  // Hooks to save/restore position
-  usePositionNavigator();
-
-  const [scale, setScale] = React.useState({
-    scale: 1,
-    x: 0,
-    y: 0,
-  });
-
-  const wrappedRef = React.useRef(null);
-
-  // React on scale change
-  React.useLayoutEffect(() => {
-    setDim((prevDim) => {
-      const { top, left } = wrappedRef.current.getBoundingClientRect();
-
-      const displayX = scale.x - left;
-      const deltaX = displayX - (displayX / prevDim.scale) * scale.scale;
-      const displayY = scale.y - top;
-      const deltaY = displayY - (displayY / prevDim.scale) * scale.scale;
-
-      return {
-        ...prevDim,
-        scale: scale.scale,
-        translateX: prevDim.translateX + deltaX,
-        translateY: prevDim.translateY + deltaY,
-      };
-    });
-  }, [scale, setDim]);
-
-  // Center board on game loading
-  React.useEffect(() => {
-    const { innerHeight, innerWidth } = window;
-
-    const minSize = Math.min(innerHeight, innerWidth);
-
-    const newScale = (minSize / config.size) * 0.8;
-
-    setScaleBoundaries([newScale * 0.8, Math.max(newScale * 30, 8)]);
-
-    setDim((prev) => ({
-      ...prev,
-      scale: newScale,
-      translateX: innerWidth / 2 - (config.size / 2) * newScale,
-      translateY: innerHeight / 2 - (config.size / 2) * newScale,
-    }));
-
-    setScale((prev) => {
-      return { ...prev, scale: newScale, x: 0, y: 0 };
-    });
-    // We only want to do it at component mount
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [config.size]);
-
-  // Keep board inside viewport
-  React.useEffect(() => {
-    const { width, height } = wrappedRef.current.getBoundingClientRect();
-    const { innerHeight, innerWidth } = window;
-
-    const newDim = {};
-
-    if (dim.translateX > innerWidth - TOLERANCE) {
-      newDim.translateX = innerWidth - TOLERANCE;
-    }
-    if (dim.translateX + width < TOLERANCE) {
-      newDim.translateX = TOLERANCE - width;
-    }
-    if (dim.translateY > innerHeight - TOLERANCE) {
-      newDim.translateY = innerHeight - TOLERANCE;
-    }
-    if (dim.translateY + height < TOLERANCE) {
-      newDim.translateY = TOLERANCE - height;
-    }
-    if (Object.keys(newDim).length > 0) {
-      setDim((prevDim) => ({
-        ...prevDim,
-        ...newDim,
-      }));
-    }
-  }, [dim.translateX, dim.translateY, setDim]);
-
-  // Debounce set center to avoid too many render
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const debouncedUpdateCenter = React.useCallback(
-    debounce(() => {
-      const { innerHeight, innerWidth } = window;
-      setDim((prevDim) => {
-        return {
-          ...prevDim,
-          centerX: (innerWidth / 2 - prevDim.translateX) / prevDim.scale,
-          centerY: (innerHeight / 2 - prevDim.translateY) / prevDim.scale,
-        };
-      });
-    }, 300),
-    [setDim]
-  );
-
-  React.useEffect(() => {
-    debouncedUpdateCenter();
-  }, [debouncedUpdateCenter, dim.translateX, dim.translateY]);
-
-  const zoomTo = React.useCallback(
-    (factor, zoomCenter) => {
-      let center = zoomCenter;
-      if (!center) {
-        const { innerHeight, innerWidth } = window;
-        center = {
-          x: innerWidth / 2,
-          y: innerHeight / 2,
-        };
-      }
-
-      setScale((prevScale) => {
-        let newScale = prevScale.scale * factor;
-        if (newScale > scaleBoundaries[1]) {
-          newScale = scaleBoundaries[1];
-        }
-
-        if (newScale < scaleBoundaries[0]) {
-          newScale = scaleBoundaries[0];
-        }
-
-        return {
-          scale: newScale,
-          ...center,
-        };
-      });
-    },
-    [scaleBoundaries]
-  );
-
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const updateBoardStateZoomingDelay = React.useCallback(
-    debounce((newState) => {
-      setBoardState(newState);
-    }, 300),
-    [setBoardState]
-  );
-
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const updateBoardStatePanningDelay = React.useCallback(
-    debounce((newState) => {
-      setBoardState(newState);
-    }, 200),
-    [setBoardState]
-  );
-
-  // Update boardState on zoom or pan
-  React.useEffect(() => {
-    if (!prevDim) {
-      return;
-    }
-    if (prevDim.scale !== dim.scale) {
-      setBoardState((prev) =>
-        !prev.zooming ? { ...prev, zooming: true } : prev
-      );
-      updateBoardStateZoomingDelay((prev) =>
-        prev.zooming ? { ...prev, zooming: false } : prev
-      );
-    }
-    if (
-      prevDim.translateY !== dim.translateY ||
-      prevDim.translateX !== dim.translateX
-    ) {
-      setBoardState((prev) =>
-        !prev.panning ? { ...prev, panning: true } : prev
-      );
-      updateBoardStatePanningDelay((prev) =>
-        prev.panning ? { ...prev, panning: false } : prev
-      );
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [dim, updateBoardStatePanningDelay, updateBoardStateZoomingDelay]);
-
-  const onZoom = React.useCallback(
-    ({ clientX, clientY, scale }) => {
-      zoomTo(1 - scale / 200, { x: clientX, y: clientY });
-    },
-    [zoomTo]
-  );
-
-  const onPan = React.useCallback(
-    ({ deltaX, deltaY }) => {
-      setDim((prevDim) => {
-        return {
-          ...prevDim,
-          translateX: prevDim.translateX + deltaX,
-          translateY: prevDim.translateY + deltaY,
-        };
-      });
-    },
-    [setDim]
-  );
-
-  const onDrag = React.useCallback(
-    (state) => {
-      const { target } = state;
-
-      const outsideItem =
-        !insideClass(target, "item") || insideClass(target, "locked");
-
-      if (moveFirst && outsideItem) {
-        onPan(state);
-      }
-    },
-    [moveFirst, onPan]
-  );
-
-  const onKeyDown = useRecoilCallback(
-    ({ snapshot }) => async (e) => {
-      // Block shortcut if we are typing in a textarea or input
-      if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
-
-      let moveX = 0;
-      let moveY = 0;
-      let zoom = 1;
-      switch (e.key) {
-        case "ArrowLeft":
-          moveX = 10;
-          break;
-        case "ArrowRight":
-          moveX = -10;
-          break;
-        case "ArrowUp":
-          moveY = 10;
-          break;
-        case "ArrowDown":
-          moveY = -10;
-          break;
-        case "PageUp":
-          zoom = 1.2;
-          break;
-        case "PageDown":
-          zoom = 0.8;
-          break;
-      }
-      if (moveX || moveY || zoom !== 1) {
-        // Don't move board if moving item
-        const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-        if (zoom === 1 && selectedItems.length) {
-          return;
-        }
-        if (e.shiftKey) {
-          moveX = moveX * 5;
-          moveY = moveY * 5;
-        }
-        if (e.ctrlKey || e.altKey || e.metaKey) {
-          moveX = moveX / 5;
-          moveY = moveY / 5;
-        }
-        setDim((prev) => ({
-          ...prev,
-          translateY: prev.translateY + moveY,
-          translateX: prev.translateX + moveX,
-        }));
-
-        zoomTo(zoom);
-
-        e.preventDefault();
-      }
-      // Temporally zoom
-      if (e.key === " " && !e.repeat) {
-        zoomTo(3);
-      }
-    },
-    [setDim, zoomTo]
-  );
-
-  const onKeyUp = React.useCallback(
-    (e) => {
-      // Zoom out on release
-      if (e.key === " ") {
-        zoomTo(1 / 3);
-      }
-    },
-    [zoomTo]
-  );
-
-  React.useEffect(() => {
-    document.addEventListener("keydown", onKeyDown);
-    document.addEventListener("keyup", onKeyUp);
-    return () => {
-      document.removeEventListener("keydown", onKeyDown);
-      document.removeEventListener("keyup", onKeyUp);
-    };
-  }, [onKeyDown, onKeyUp]);
-
-  return (
-    <Gesture onPan={onPan} onZoom={onZoom} onDrag={onDrag}>
-      <Pane
-        {...dim}
-        ref={wrappedRef}
-        onContextMenu={(e) => {
-          e.preventDefault();
-        }}
-      >
-        {children}
-      </Pane>
-    </Gesture>
-  );
-};
-
-export default PanZoomRotate;

+ 0 - 211
src/components/board/Selector.jsx

@@ -1,211 +0,0 @@
-import React from "react";
-import throttle from "lodash.throttle";
-import styled from "styled-components";
-import { useRecoilValue, useSetRecoilState, useRecoilCallback } from "recoil";
-
-import { insideClass, isItemInsideElement } from "../utils";
-
-import {
-  PanZoomRotateAtom,
-  BoardConfigAtom,
-  ItemMapAtom,
-  BoardStateAtom,
-  SelectedItemsAtom,
-} from "./";
-
-import Gesture from "./Gesture";
-
-const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
-  className: "selector",
-  style: {
-    transform: `translate(${left}px, ${top}px)`,
-    height: `${height}px`,
-    width: `${width}px`,
-  },
-}))`
-  z-index: 210;
-  position: absolute;
-  background-color: hsla(0, 40%, 50%, 10%);
-  border: 2px solid hsl(0, 55%, 40%);
-`;
-
-const findSelected = (itemMap) => {
-  const selector = document.body.querySelector(".selector");
-  if (!selector) {
-    return [];
-  }
-
-  return Array.from(document.getElementsByClassName("item"))
-    .filter((elem) => {
-      const { id } = elem;
-      const item = itemMap[id];
-      if (!item) {
-        // Avoid to find item that are not yet removed from DOM
-        console.error(`Missing item ${id}`);
-        return false;
-      }
-      if (item.locked) {
-        return false;
-      }
-      return isItemInsideElement(elem, selector);
-    })
-    .map((elem) => elem.id);
-};
-
-const Selector = ({ children, moveFirst }) => {
-  const setSelected = useSetRecoilState(SelectedItemsAtom);
-  const setBoardState = useSetRecoilState(BoardStateAtom);
-
-  const [selector, setSelector] = React.useState({});
-  const [emptySelection] = React.useState([]);
-
-  const wrapperRef = React.useRef(null);
-  const stateRef = React.useRef({
-    moving: false,
-  });
-
-  const config = useRecoilValue(BoardConfigAtom);
-
-  // Reset selection on game loading
-  React.useEffect(() => {
-    setSelected(emptySelection);
-  }, [config, emptySelection, setSelected]);
-
-  const throttledSetSelected = useRecoilCallback(
-    ({ snapshot }) =>
-      throttle(async () => {
-        if (stateRef.current.moving) {
-          const itemMap = await snapshot.getPromise(ItemMapAtom);
-          const selected = findSelected(itemMap);
-
-          setSelected((prevSelected) => {
-            if (JSON.stringify(prevSelected) !== JSON.stringify(selected)) {
-              return selected;
-            }
-            return prevSelected;
-          });
-        }
-      }, 300),
-    [setSelected]
-  );
-
-  React.useEffect(() => {
-    throttledSetSelected();
-  }, [selector, throttledSetSelected]);
-
-  // Reset selected on unmount
-  React.useEffect(() => {
-    return () => {
-      setSelected(emptySelection);
-    };
-  }, [setSelected, emptySelection]);
-
-  const onDragStart = ({ button, altKey, ctrlKey, metaKey, target }) => {
-    const outsideItem =
-      !insideClass(target, "item") || insideClass(target, "locked");
-
-    const metaKeyPressed = altKey || ctrlKey || metaKey;
-
-    const goodButton = moveFirst
-      ? button === 1 || (button === 0 && metaKeyPressed)
-      : button === 0 && !metaKeyPressed;
-
-    if (goodButton && (outsideItem || moveFirst)) {
-      stateRef.current.moving = true;
-      setBoardState((prev) => ({ ...prev, selecting: true }));
-      wrapperRef.current.style.cursor = "crosshair";
-    }
-  };
-
-  const onDrag = useRecoilCallback(
-    ({ snapshot }) => async ({ distanceY, distanceX, startX, startY }) => {
-      if (stateRef.current.moving) {
-        const { top, left } = wrapperRef.current.getBoundingClientRect();
-
-        const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
-
-        const displayX = (startX - left) / panZoomRotate.scale;
-        const displayY = (startY - top) / panZoomRotate.scale;
-
-        const displayDistanceX = distanceX / panZoomRotate.scale;
-        const displayDistanceY = distanceY / panZoomRotate.scale;
-
-        if (displayDistanceX > 0) {
-          stateRef.current.left = displayX;
-          stateRef.current.width = displayDistanceX;
-        } else {
-          stateRef.current.left = displayX + displayDistanceX;
-          stateRef.current.width = -displayDistanceX;
-        }
-        if (displayDistanceY > 0) {
-          stateRef.current.top = displayY;
-          stateRef.current.height = displayDistanceY;
-        } else {
-          stateRef.current.top = displayY + displayDistanceY;
-          stateRef.current.height = -displayDistanceY;
-        }
-
-        setSelector({ ...stateRef.current, moving: true });
-      }
-    },
-    []
-  );
-
-  const onDragEnd = () => {
-    if (stateRef.current.moving) {
-      setBoardState((prev) => ({ ...prev, selecting: false }));
-      stateRef.current.moving = false;
-      setSelector({ moving: false });
-      wrapperRef.current.style.cursor = "auto";
-    }
-  };
-
-  const onLongTap = React.useCallback(
-    ({ target }) => {
-      const foundElement = insideClass(target, "item");
-      if (foundElement) {
-        setSelected([foundElement.id]);
-      }
-    },
-    [setSelected]
-  );
-
-  const onTap = useRecoilCallback(
-    ({ snapshot }) => async ({ target, ctrlKey, metaKey }) => {
-      const foundItem = insideClass(target, "item");
-      if (
-        (!foundItem || insideClass(foundItem, "locked")) &&
-        insideClass(target, "board")
-      ) {
-        setSelected(emptySelection);
-      } else {
-        const selectedItems = await snapshot.getPromise(SelectedItemsAtom);
-        if (foundItem && !selectedItems.includes(foundItem.id)) {
-          if (ctrlKey || metaKey) {
-            setSelected((prev) => [...prev, foundItem.id]);
-          } else {
-            setSelected([foundItem.id]);
-          }
-        }
-      }
-    },
-    [emptySelection, setSelected]
-  );
-
-  return (
-    <Gesture
-      onDragStart={onDragStart}
-      onDrag={onDrag}
-      onDragEnd={onDragEnd}
-      onTap={onTap}
-      onLongTap={onLongTap}
-    >
-      <div ref={wrapperRef}>
-        {selector.moving && <SelectorZone {...selector} />}
-        {children}
-      </div>
-    </Gesture>
-  );
-};
-
-export default Selector;

+ 0 - 74
src/components/board/atoms.jsx

@@ -1,74 +0,0 @@
-import { atom, selector } from "recoil";
-
-export const AvailableItemListAtom = atom({
-  key: "availableItemList",
-  default: [],
-});
-
-export const BoardConfigAtom = atom({
-  key: "boardConfig",
-  default: {},
-});
-
-export const BoardStateAtom = atom({
-  key: "boardState",
-  default: {
-    movingItems: false,
-    selecting: false,
-    zooming: false,
-    panning: false,
-  },
-});
-
-export const ItemListAtom = atom({
-  key: "itemList",
-  default: [],
-});
-
-export const ItemMapAtom = atom({
-  key: "ItemMap",
-  default: {},
-});
-
-export const AllItemsSelector = selector({
-  key: "AllItemsSelector",
-  get: ({ get }) => {
-    const itemMap = get(ItemMapAtom);
-    return get(ItemListAtom)
-      .map((id) => itemMap[id])
-      .filter((item) => item); // This filter clean the selection of missing items
-  },
-});
-
-export const PanZoomRotateAtom = atom({
-  key: "PanZoomRotate",
-  default: {
-    translateX: 0,
-    translateY: 0,
-    scale: 1,
-    rotate: 0,
-    centerX: 0,
-    centerY: 0,
-  },
-});
-
-export const ItemInteractionsAtom = atom({
-  key: "itemInteractions",
-  default: {},
-});
-
-export const SelectedItemsAtom = atom({
-  key: "selectedItems",
-  default: [],
-});
-
-export default {
-  ItemListAtom,
-  BoardConfigAtom,
-  AvailableItemListAtom,
-  AllItemsSelector,
-  ItemMapAtom,
-  ItemInteractionsAtom,
-  PanZoomRotateAtom,
-  SelectedItemsAtom,
-};

+ 0 - 11
src/components/board/index.jsx

@@ -1,11 +0,0 @@
-export { default as Board } from "./Board";
-export {
-  AvailableItemListAtom,
-  SelectedItemsAtom,
-  BoardConfigAtom,
-  ItemListAtom,
-  ItemMapAtom,
-  AllItemsSelector,
-  BoardStateAtom,
-  PanZoomRotateAtom,
-} from "./atoms";

+ 0 - 50
src/components/board/usePositionNavigator.jsx

@@ -1,50 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { toast } from "react-toastify";
-
-import { useSetRecoilState, useRecoilCallback } from "recoil";
-import { PanZoomRotateAtom } from "./";
-
-const digitCodes = [...Array(5).keys()].map((id) => `Digit${id}`);
-
-const usePositionNavigator = () => {
-  const { t } = useTranslation();
-  const setDim = useSetRecoilState(PanZoomRotateAtom);
-  const [positions, setPositions] = React.useState({});
-
-  const onKeyDown = useRecoilCallback(
-    ({ snapshot }) => async (e) => {
-      // Block shortcut if we are typing in a textarea or input
-      if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
-
-      if (digitCodes.includes(e.code)) {
-        const positionKey = e.code;
-        const dim = await snapshot.getPromise(PanZoomRotateAtom);
-        if (e.altKey || e.metaKey || e.ctrlKey) {
-          setPositions((prev) => ({ ...prev, [positionKey]: { ...dim } }));
-          toast.info(t("Position saved!"), {
-            autoClose: 800,
-            hideProgressBar: true,
-          });
-        } else {
-          if (positions[positionKey]) {
-            setDim((prev) => ({ ...prev, ...positions[positionKey] }));
-          }
-        }
-        e.preventDefault();
-      }
-    },
-    [positions, setDim, t]
-  );
-
-  React.useEffect(() => {
-    document.addEventListener("keydown", onKeyDown);
-    return () => {
-      document.removeEventListener("keydown", onKeyDown);
-    };
-  }, [onKeyDown]);
-
-  return null;
-};
-
-export default usePositionNavigator;

+ 0 - 95
src/components/hooks/useC2C.jsx

@@ -1,95 +0,0 @@
-import React, { useContext } from "react";
-import { useSocket } from "@scripters/use-socket.io";
-import { join } from "client2client.io";
-import { useTranslation } from "react-i18next";
-
-import Waiter from "../ui/Waiter";
-
-const contextMap = {};
-
-export const C2CProvider = ({ room, channel = "default", children }) => {
-  const { t } = useTranslation();
-  const socket = useSocket();
-  const [joined, setJoined] = React.useState(false);
-  const [isMaster, setIsMaster] = React.useState(false);
-  const [c2c, setC2c] = React.useState(null);
-  const roomRef = React.useRef(null);
-  const mountedRef = React.useRef(false);
-
-  React.useEffect(() => {
-    mountedRef.current = true;
-    return () => {
-      mountedRef.current = false;
-    };
-  }, []);
-
-  if (!contextMap[channel]) {
-    contextMap[channel] = React.createContext({
-      joined: false,
-      isMaster: false,
-    });
-  }
-  const Context = contextMap[channel];
-
-  React.useEffect(() => {
-    const disconnect = () => {
-      console.log(`Disconnected from ${channel}…`);
-      if (!mountedRef.current) return;
-      setJoined(false);
-      setIsMaster(false);
-    };
-
-    socket.on("disconnect", disconnect);
-    return () => {
-      socket.off("disconnect", disconnect);
-    };
-  }, [channel, socket]);
-
-  React.useEffect(() => {
-    // Connect
-    if (!socket) {
-      return;
-    }
-    if (!socket.connected) {
-      socket.connect();
-    }
-    join({
-      socket,
-      room,
-      onMaster: () => {
-        console.log(`Is now master on channel ${channel}…`);
-        if (!mountedRef.current) return;
-        setIsMaster(true);
-      },
-      onJoined: (newRoom) => {
-        console.log(`Connected on channel ${channel}…`);
-        roomRef.current = newRoom;
-
-        if (!mountedRef.current) return;
-        setC2c(newRoom);
-        setJoined(true);
-      },
-    });
-
-    return () => {
-      roomRef.current.leave();
-    };
-  }, [channel, room, socket]);
-
-  if (!joined || !c2c) {
-    return <Waiter message={t("Waiting for connection…")} />;
-  }
-
-  return (
-    <Context.Provider value={{ c2c, joined, isMaster, room }}>
-      {children}
-    </Context.Provider>
-  );
-};
-
-export const useC2C = (channel = "default") => {
-  const Context = contextMap[channel];
-  return useContext(Context);
-};
-
-export default useC2C;

+ 0 - 17
src/components/hooks/useNotify.js

@@ -1,17 +0,0 @@
-import React from "react";
-
-const useNotify = () => {
-  const [count, setCount] = React.useState(0);
-
-  const add = React.useCallback(() => {
-    setCount((prev) => prev + 1);
-  }, []);
-
-  const reset = React.useCallback(() => {
-    setCount(0);
-  }, []);
-
-  return { count, add, reset };
-};
-
-export default useNotify;

+ 0 - 19
src/components/hooks/usePrevious.jsx

@@ -1,19 +0,0 @@
-import { useEffect, useRef } from "react";
-
-const usePrevious = (value) => {
-  // The ref object is a generic container whose current property is mutable ...
-  // ... and can hold any value, similar to an instance property on a class
-
-  const ref = useRef();
-  // Store current value in ref
-
-  useEffect(() => {
-    ref.current = value;
-  }, [value]); // Only re-run if value changes
-
-  // Return previous value (happens before update in useEffect above)
-
-  return ref.current;
-};
-
-export default usePrevious;

+ 0 - 11
src/components/hooks/useToggle.js

@@ -1,11 +0,0 @@
-import React from "react";
-
-const useToggle = (defaultValue = true) => {
-  const [value, setValue] = React.useState(defaultValue);
-  const toggle = React.useCallback(() => {
-    setValue((prev) => !prev);
-  }, []);
-  return [value, toggle];
-};
-
-export default useToggle;

+ 0 - 113
src/components/mediaLibrary/ImageField.jsx

@@ -1,113 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import styled from "styled-components";
-import { MediaLibraryButton, media2Url } from "./";
-import backgroundGrid from "../../images/background-grid.png";
-
-const StyledImageField = styled.div`
-  & .typeSelect {
-    padding: 0.5em;
-  }
-  & .imgContainer {
-    margin: 0 1em;
-    position: relative;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: column;
-    gap: 0.5em;
-  }
-`;
-
-const Thumbnail = styled.img`
-  max-height: 100px;
-  display: block;
-  background-image: url(${backgroundGrid});
-`;
-
-const ImageField = ({ value, onChange }) => {
-  const { t } = useTranslation();
-  let type, content;
-
-  // Manage compat with old version
-  if (typeof value === "object") {
-    type = value.type;
-    content = value.content;
-  } else {
-    if (value) {
-      type = "external";
-      content = value;
-    } else {
-      type = "empty";
-      content = null;
-    }
-  }
-
-  const handleInputChange = (e) => {
-    onChange({ type, content: e.target.value });
-  };
-
-  const handleTypeChange = (e) => {
-    onChange({ type: e.target.value, content: "" });
-  };
-
-  const handleMediaSelect = (key) => {
-    onChange({ type: "local", content: key });
-  };
-
-  const url = media2Url(value);
-
-  return (
-    <StyledImageField>
-      <form className="typeSelect">
-        <label>
-          <input
-            type="radio"
-            value="empty"
-            onChange={handleTypeChange}
-            checked={type === "empty"}
-          />
-          {t("No image")}
-        </label>
-        <label>
-          <input
-            type="radio"
-            value="local"
-            onChange={handleTypeChange}
-            checked={type === "local"}
-          />
-          {t("Library")}
-        </label>
-        <label>
-          <input
-            type="radio"
-            value="external"
-            checked={type === "external"}
-            onChange={handleTypeChange}
-          />
-          {t("External")}
-        </label>
-      </form>
-
-      <div className="imgContainer" onClick={(e) => e.preventDefault()}>
-        {type !== "empty" && content && <Thumbnail src={url} />}
-
-        {type === "external" && (
-          <input
-            value={content}
-            placeholder={t("Enter an image url...")}
-            onChange={handleInputChange}
-          />
-        )}
-        {type === "local" && (
-          <MediaLibraryButton
-            onSelect={handleMediaSelect}
-            label={content ? t("Change image") : t("Select image")}
-          />
-        )}
-      </div>
-    </StyledImageField>
-  );
-};
-
-export default ImageField;

+ 0 - 25
src/components/mediaLibrary/MediaLibraryButton.jsx

@@ -1,25 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import MediaLibraryModal from "./MediaLibraryModal";
-
-const MediaLibraryButton = ({ onSelect, label }) => {
-  const { t } = useTranslation();
-
-  const [showLibrary, setShowLibrary] = React.useState(false);
-
-  return (
-    <>
-      <button onClick={() => setShowLibrary((prev) => !prev)}>
-        {label || t("Select media")}
-      </button>
-
-      <MediaLibraryModal
-        show={showLibrary}
-        setShow={setShowLibrary}
-        onSelect={onSelect}
-      />
-    </>
-  );
-};
-
-export default MediaLibraryButton;

+ 0 - 199
src/components/mediaLibrary/MediaLibraryModal.jsx

@@ -1,199 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { useQuery, useMutation, useQueryClient } from "react-query";
-import styled from "styled-components";
-
-import { API_BASE } from "../../utils/settings";
-import backgroundGrid from "../../images/background-grid.png";
-
-import { confirmAlert } from "react-confirm-alert";
-import { toast } from "react-toastify";
-
-import Modal from "../ui/Modal";
-
-import { useMediaLibrary } from ".";
-import { useDropzone } from "react-dropzone";
-
-const ImageGrid = styled.div`
-  display: flex;
-  flex-wrap: wrap;
-  & > div {
-    position: relative;
-  }
-  & img {
-    height: 100px;
-    margin: 0.5em;
-    cursor: pointer;
-    border: 1px solid transparent;
-    background-image: url(${backgroundGrid});
-    &:hover {
-      filter: brightness(1.2);
-      border: 1px dotted var(--color-primary);
-    }
-  }
-  & .remove {
-    position: absolute;
-    top: 15px;
-    right: 5px;
-  }
-`;
-
-const MediaLibraryModal = ({ show, setShow, onSelect }) => {
-  const { t } = useTranslation();
-
-  const {
-    getLibraryMedia,
-    addMedia,
-    removeMedia,
-    libraries,
-  } = useMediaLibrary();
-
-  const queryClient = useQueryClient();
-  const [tab, setTab] = React.useState(libraries[0].id);
-
-  const currentLibrary = libraries.find(({ id }) => id === tab);
-
-  const { isLoading, data = [] } = useQuery(
-    `media__${tab}`,
-    () => getLibraryMedia(currentLibrary),
-    {
-      enabled: show,
-    }
-  );
-
-  const handleSelect = React.useCallback(
-    (media) => {
-      onSelect(media);
-      setShow(false);
-    },
-    [onSelect, setShow]
-  );
-
-  const uploadMediaMutation = useMutation(
-    async (files) => {
-      if (files.length === 1) {
-        return [await addMedia(currentLibrary, files[0])];
-      } else {
-        return Promise.all(files.map((file) => addMedia(currentLibrary, file)));
-      }
-    },
-    {
-      onSuccess: (result) => {
-        if (result.length === 1) {
-          // If only one file is processed
-          handleSelect(result[0].content);
-        }
-        queryClient.invalidateQueries(`media__${tab}`);
-      },
-    }
-  );
-
-  const onRemove = React.useCallback((key) => {
-    confirmAlert({
-      title: t("Confirmation"),
-      message: t("Do you really want to remove this media?"),
-      buttons: [
-        {
-          label: t("Yes"),
-          onClick: async () => {
-            try {
-              await removeMedia(key);
-              toast.success(t("Media deleted"), { autoClose: 1500 });
-            } catch (e) {
-              if (e.message === "Forbidden") {
-                toast.error(t("Action forbidden. Try logging in again."));
-              } else {
-                console.log(e);
-                toast.error(
-                  t("Error while deleting media. Try again later...")
-                );
-              }
-            }
-          },
-        },
-        {
-          label: t("No"),
-          onClick: () => {},
-        },
-      ],
-    });
-  }, []);
-
-  const { getRootProps, getInputProps } = useDropzone({
-    onDrop: uploadMediaMutation.mutate,
-  });
-
-  return (
-    <Modal
-      title={t("Media library")}
-      show={show}
-      setShow={setShow}
-      position="left"
-    >
-      <nav className="tabs">
-        {libraries.map(({ id, name }) => (
-          <a
-            onClick={() => setTab(id)}
-            className={tab === id ? "active" : ""}
-            style={{ cursor: "pointer" }}
-            key={id}
-          >
-            {name}
-          </a>
-        ))}
-      </nav>
-
-      <section>
-        {libraries.map(({ id, name }, index) => {
-          if (tab === id) {
-            return (
-              <div key={id}>
-                {index === 0 && (
-                  <>
-                    <h3>{t("Add file")}</h3>
-                    <div
-                      {...getRootProps()}
-                      style={{
-                        border: "3px dashed white",
-                        margin: "0.5em",
-                        padding: "0.5em",
-                        textAlign: "center",
-                      }}
-                    >
-                      <input {...getInputProps()} />
-                      <p>{t("Click or drag'n'drop file here")}</p>
-                    </div>
-                  </>
-                )}
-                <h3>{name}</h3>
-                {!isLoading && (
-                  <ImageGrid>
-                    {data.map((key) => (
-                      <div key={key}>
-                        <img
-                          src={`${API_BASE}/${key}`}
-                          onClick={() => handleSelect(key)}
-                        />
-                        <button
-                          onClick={() => onRemove(key)}
-                          className="button icon-only remove"
-                          title={t("Remove")}
-                        >
-                          X
-                        </button>
-                      </div>
-                    ))}
-                  </ImageGrid>
-                )}
-              </div>
-            );
-          } else {
-            return null;
-          }
-        })}
-      </section>
-    </Modal>
-  );
-};
-
-export default MediaLibraryModal;

+ 0 - 50
src/components/mediaLibrary/MediaLibraryProvider.jsx

@@ -1,50 +0,0 @@
-import React, { useContext } from "react";
-
-export const MediaLibraryContext = React.createContext({});
-
-const noop = () => {};
-
-export const MediaLibraryProvider = ({
-  children,
-  libraries = [],
-  uploadMedia = noop,
-  listMedia = noop,
-  deleteMedia = noop,
-}) => {
-  const addMedia = React.useCallback(
-    async ({ boxId, resourceId }, file) => {
-      const filePath = await uploadMedia(boxId, resourceId, file);
-      return {
-        type: "local",
-        content: filePath,
-      };
-    },
-    [uploadMedia]
-  );
-
-  const removeMedia = React.useCallback(
-    async (key) => {
-      return await deleteMedia(key);
-    },
-    [deleteMedia]
-  );
-
-  const getLibraryMedia = React.useCallback(
-    async ({ boxId, resourceId }) => listMedia(boxId, resourceId),
-    [listMedia]
-  );
-
-  return (
-    <MediaLibraryContext.Provider
-      value={{ addMedia, getLibraryMedia, libraries, removeMedia }}
-    >
-      {children}
-    </MediaLibraryContext.Provider>
-  );
-};
-
-export const useMediaLibrary = () => {
-  return useContext(MediaLibraryContext);
-};
-
-export default MediaLibraryProvider;

+ 0 - 24
src/components/mediaLibrary/index.js

@@ -1,24 +0,0 @@
-import { API_BASE } from "../../utils/settings";
-
-export { default as MediaLibraryProvider } from "./MediaLibraryProvider";
-export { useMediaLibrary } from "./MediaLibraryProvider";
-export { default as MediaLibraryButton } from "./MediaLibraryButton";
-export { default as ImageField } from "./ImageField";
-
-export const media2Url = (value) => {
-  if (value && typeof value === "object") {
-    switch (value.type) {
-      case "local":
-        return `${API_BASE}/${value.content}`;
-      case "external":
-        return value.content;
-      case "dataUrl":
-        return value.content;
-      case "empty":
-        return null;
-      default:
-      // do nothing
-    }
-  }
-  return value;
-};

+ 0 - 44
src/components/message/Composer.jsx

@@ -1,44 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import styled from "styled-components";
-
-const StyledComposer = styled.div`
-  padding: 0.5em;
-  flex: 0;
-  & form {
-    display: flex;
-  }
-  overflow: none;
-`;
-
-const Composer = ({ sendMessage }) => {
-  const { t } = useTranslation();
-  const [text, setText] = React.useState("");
-
-  const onSubmit = (e) => {
-    e.preventDefault();
-    if (text) {
-      setText("");
-      sendMessage(text);
-    }
-  };
-  const onChange = (e) => {
-    setText(e.target.value);
-  };
-  return (
-    <StyledComposer>
-      <form onSubmit={(e) => onSubmit(e)}>
-        <input
-          onChange={(e) => onChange(e)}
-          value={text}
-          type="text"
-          placeholder={t("Enter your message and press ENTER")}
-          autoFocus={true}
-        />
-        <button>{t("Send")}</button>
-      </form>
-    </StyledComposer>
-  );
-};
-
-export default Composer;

+ 0 - 54
src/components/message/Message.jsx

@@ -1,54 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-const StyledMessage = styled.div`
-  font-size: 1.2em;
-  & .line {
-    display: flex;
-    flex-direction: row;
-    padding: 0rem 0.2rem;
-    padding: 0.5rem 0;
-    & p {
-      margin: 0;
-    }
-  }
-  & .name {
-    padding-left: 0.5em;
-    font-size: 1.3em;
-    ${({ color }) => `color: ${color};`}
-    text-shadow: 0px 0px 1px var(--color-grey);
-  }
-  & .left-block {
-    padding: 0 1rem;
-    opacity: 0.05;
-  }
-  &:hover {
-    & .line {
-      background-color: var(--color-midGrey);
-    }
-    & .left-block {
-      opacity: 0.8;
-    }
-  }
-`;
-
-const Message = ({
-  first,
-  user: { name, color = "#dddddd" },
-  timestamp,
-  content,
-}) => {
-  return (
-    <StyledMessage color={color}>
-      {first && <div className="name">{name}</div>}
-      <div className="line">
-        <div className="left-block">
-          <span>{timestamp.format("HH:mm")}</span>
-        </div>
-        <p>{content}</p>
-      </div>
-    </StyledMessage>
-  );
-};
-
-export default Message;

+ 0 - 85
src/components/message/MessageButton.jsx

@@ -1,85 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import styled from "styled-components";
-
-import useMessage from "./useMessage";
-import useNotify from "../hooks/useNotify";
-
-import Touch from "../ui/Touch";
-import SidePanel from "../ui/SidePanel";
-
-import Composer from "./Composer";
-import MessageList from "./MessageList";
-
-const StyledChat = styled.div`
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-`;
-
-const NotifCount = styled.div`
-  width: 2em;
-  height: 2em;
-  border-radius: 100%;
-  position: absolute;
-  top: -5px;
-  right: 1.8em;
-  background-color: var(--color-success);
-  text-align: center;
-  line-height: 2em;
-  box-shadow: 3px 3px 6px #000000c0;
-`;
-
-export const MessageButton = () => {
-  const { t } = useTranslation();
-  const { add, reset, count } = useNotify();
-  const [showPanel, setShowPanel] = React.useState(false);
-
-  const onNewMessage = React.useCallback(() => {
-    if (!showPanel) {
-      add();
-    }
-  }, [add, showPanel]);
-
-  React.useEffect(() => {
-    if (showPanel) {
-      reset();
-    }
-  }, [reset, showPanel]);
-
-  const { messages, sendMessage } = useMessage(onNewMessage);
-
-  const countStr = count > 9 ? "9+" : `${count}`;
-
-  return (
-    <>
-      <div style={{ position: "relative" }}>
-        <Touch
-          onClick={() => setShowPanel((prev) => !prev)}
-          alt={t("Chat")}
-          title={t("Chat")}
-          label={t("Chat")}
-          icon={"message"}
-          active={showPanel}
-        />
-        {count > 0 && <NotifCount>{countStr}</NotifCount>}
-      </div>
-      <SidePanel
-        open={showPanel}
-        onClose={() => {
-          setShowPanel(false);
-        }}
-        position="left"
-        noMargin
-        title={t("Chat")}
-      >
-        <StyledChat>
-          <MessageList messages={messages} />
-          <Composer sendMessage={sendMessage} />
-        </StyledChat>
-      </SidePanel>
-    </>
-  );
-};
-
-export default MessageButton;

+ 0 - 97
src/components/message/MessageList.jsx

@@ -1,97 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-import { useUsers } from "../users";
-import Message from "./Message";
-
-const computeMessageGroup = (messages, userMap, maxTimeDiff = 30000) => {
-  if (!messages || messages.length === 0) return [];
-
-  const messageGroups = [];
-  let previousUser = messages[0].user.uid;
-  let previousUserName = messages[0].user.name;
-  let previousTime = messages[0].timestamp;
-  let currentGroup = [];
-
-  messages.forEach((message, index) => {
-    if (
-      message.user.uid !== previousUser ||
-      message.user.name !== previousUserName ||
-      message.timestamp.diff(previousTime) > maxTimeDiff
-    ) {
-      previousUser = message.user.uid;
-      previousUserName = message.user.name;
-      messageGroups.push({
-        id: currentGroup[0].uid,
-        group: currentGroup,
-      });
-      currentGroup = [];
-    }
-    previousTime = message.timestamp;
-
-    // Get user from current session
-    const messageWithUser = { ...message };
-    if (message.user.uid in userMap) {
-      messageWithUser.user = userMap[message.user.uid];
-    }
-
-    currentGroup.push(messageWithUser);
-    if (index === messages.length - 1) {
-      messageGroups.push({
-        id: currentGroup[0].uid,
-        group: currentGroup,
-      });
-    }
-  });
-  return messageGroups;
-};
-
-const StyledMessageList = styled.div`
-  height: 100%;
-  overflow: auto;
-`;
-
-const MessageList = ({ messages }) => {
-  const messageList = React.useRef(null);
-  const { users } = useUsers();
-
-  const userMap = React.useMemo(
-    () =>
-      users.reduce((acc, user) => {
-        acc[user.uid] = user;
-        return acc;
-      }, {}),
-    [users]
-  );
-
-  const messageGroups = React.useMemo(
-    () => computeMessageGroup(messages, userMap),
-    [messages, userMap]
-  );
-
-  React.useEffect(() => {
-    messageList.current.scrollTop = messageList.current.scrollHeight;
-  });
-
-  return (
-    <StyledMessageList ref={messageList}>
-      {messageGroups.map(({ id: groupUid, group }) => {
-        return (
-          <div key={groupUid}>
-            {group.map(({ uid: msgUid, user, timestamp, content }, index) => (
-              <Message
-                first={index === 0}
-                user={user}
-                timestamp={timestamp}
-                content={content}
-                key={msgUid}
-              />
-            ))}
-          </div>
-        );
-      })}
-    </StyledMessageList>
-  );
-};
-
-export default MessageList;

+ 0 - 1
src/components/message/index.js

@@ -1 +0,0 @@
-export { default as MessageButton } from "./MessageButton";

+ 0 - 108
src/components/message/useMessage.js

@@ -1,108 +0,0 @@
-import React from "react";
-import { nanoid } from "nanoid";
-import { atom, useRecoilState, useRecoilCallback } from "recoil";
-
-import dayjs from "dayjs";
-
-import { useUsers } from "../users";
-import useC2C from "../../components/hooks/useC2C";
-
-export const MessagesAtom = atom({
-  key: "messages",
-  default: [],
-});
-
-const generateMsg = ({ user: { name, uid, color }, content }) => {
-  const newMessage = {
-    type: "message",
-    user: { name, uid, color },
-    content,
-    uid: nanoid(),
-    timestamp: dayjs().toISOString(),
-  };
-  return newMessage;
-};
-
-export const parseMessage = (message) => {
-  try {
-    return {
-      ...message,
-      timestamp: dayjs(message.timestamp),
-    };
-  } catch (e) {
-    console.warn("Discard message as it can't be decoded", e);
-  }
-  return null;
-};
-
-const noop = () => {};
-
-const useMessage = (onMessage = noop) => {
-  const [messages, setMessagesState] = useRecoilState(MessagesAtom);
-  const { c2c, isMaster } = useC2C("board");
-  const { currentUser } = useUsers();
-
-  const getMessage = useRecoilCallback(
-    ({ snapshot }) => async () => {
-      const currentMessages = await snapshot.getPromise(MessagesAtom);
-      return currentMessages;
-    },
-    []
-  );
-
-  const setMessages = React.useCallback(
-    (newMessages) => {
-      setMessagesState(newMessages.map((m) => parseMessage(m)));
-    },
-    [setMessagesState]
-  );
-
-  const initEvents = React.useCallback(
-    (unsub) => {
-      unsub.push(
-        c2c.subscribe("newMessage", (newMessage) => {
-          setMessagesState((prevMessages) => [
-            ...prevMessages,
-            parseMessage(newMessage),
-          ]);
-          onMessage(newMessage);
-        })
-      );
-      if (isMaster) {
-        c2c.register("getMessageHistory", getMessage).then((unregister) => {
-          unsub.push(unregister);
-        });
-      } else {
-        c2c.call("getMessageHistory").then((messageHistory) => {
-          setMessages(messageHistory);
-        });
-      }
-    },
-    [c2c, getMessage, isMaster, onMessage, setMessages, setMessagesState]
-  );
-
-  React.useEffect(() => {
-    const unsub = [];
-
-    initEvents(unsub);
-
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [initEvents]);
-
-  const sendMessage = React.useCallback(
-    (messageContent) => {
-      const newMessage = generateMsg({
-        user: currentUser,
-        content: messageContent,
-      });
-      if (newMessage) c2c.publish("newMessage", newMessage, true);
-    },
-    [c2c, currentUser]
-  );
-
-  return { messages, setMessages, sendMessage };
-};
-
-export default useMessage;

+ 0 - 40
src/components/useBoardConfig.jsx

@@ -1,40 +0,0 @@
-import React from "react";
-import { useRecoilState } from "recoil";
-import debounce from "lodash.debounce";
-
-import useC2C from "./hooks/useC2C";
-import { BoardConfigAtom } from "./board/atoms";
-
-export const useBoardConfig = () => {
-  const { c2c } = useC2C("board");
-  const [boardConfig, setBoardConfig] = useRecoilState(BoardConfigAtom);
-
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const debouncedPublishUpdate = React.useCallback(
-    debounce((newConfig) => {
-      c2c.publish("updateBoardConfig", newConfig);
-    }, 1000),
-    [c2c]
-  );
-
-  const setSyncBoardConfig = React.useCallback(
-    (callbackOrConfig, sync = true) => {
-      let callback = callbackOrConfig;
-      if (typeof callbackOrConfig === "object") {
-        callback = () => callbackOrConfig;
-      }
-      setBoardConfig((prev) => {
-        const newConfig = callback(prev);
-        if (sync) {
-          debouncedPublishUpdate(newConfig);
-        }
-        return newConfig;
-      });
-    },
-    [setBoardConfig, debouncedPublishUpdate]
-  );
-
-  return [boardConfig, setSyncBoardConfig];
-};
-
-export default useBoardConfig;

+ 0 - 107
src/components/users/SubscribeUserEvents.jsx

@@ -1,107 +0,0 @@
-import React from "react";
-
-import { useSetRecoilState, useRecoilState } from "recoil";
-import { userAtom, usersAtom } from "./atoms";
-
-import debounce from "lodash.debounce";
-
-import useC2C from "../hooks/useC2C";
-
-const SubscribeUserEvents = () => {
-  const usersRef = React.useRef([]);
-  const setUsers = useSetRecoilState(usersAtom);
-  const [currentUser, setCurrentUserState] = useRecoilState(userAtom);
-
-  const { c2c, isMaster } = useC2C("room");
-
-  React.useEffect(() => {
-    setCurrentUserState((prevUser) => ({
-      ...prevUser,
-      id: c2c.userId,
-    }));
-  }, [c2c.userId, setCurrentUserState]);
-
-  React.useEffect(() => {
-    if (!isMaster) {
-      const onGetUserList = (userList) => {
-        usersRef.current = userList;
-        setUsers(userList);
-      };
-
-      c2c.call("getUserList").then(onGetUserList, () => {
-        // retry later
-        setTimeout(() => {
-          c2c
-            .call("getUserList")
-            .then(onGetUserList, (error) => console.log(error));
-        }, 1000);
-      });
-    }
-  }, [c2c, isMaster, setUsers]);
-
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  const debouncedEmitUpdateUser = React.useCallback(
-    debounce((newUser) => {
-      c2c.publish("userUpdate", newUser, true);
-    }, 500),
-    [c2c]
-  );
-
-  React.useEffect(() => {
-    if (currentUser && currentUser.id) {
-      debouncedEmitUpdateUser(currentUser);
-    }
-  }, [currentUser, debouncedEmitUpdateUser]);
-
-  React.useEffect(() => {
-    const unsub = [];
-    if (isMaster) {
-      c2c
-        .register("getUserList", () => {
-          return usersRef.current;
-        })
-        .then((unregister) => {
-          unsub.push(unregister);
-        });
-
-      unsub.push(
-        c2c.subscribe("userUpdate", (user) => {
-          if (usersRef.current.find((u) => u.id === user.id)) {
-            const newUsers = usersRef.current.map((u) =>
-              u.id === user.id ? user : u
-            );
-            usersRef.current = newUsers;
-          } else {
-            const newUsers = [...usersRef.current, user];
-            usersRef.current = newUsers;
-          }
-          setUsers(usersRef.current);
-          c2c.publish("updateUserList", usersRef.current);
-        })
-      );
-    }
-    unsub.push(
-      c2c.subscribe("userLeave", (userId) => {
-        usersRef.current = usersRef.current.filter(({ id }) => id !== userId);
-        setUsers(usersRef.current);
-        if (isMaster) {
-          c2c.publish("updateUserList", usersRef.current);
-        }
-      })
-    );
-    unsub.push(
-      c2c.subscribe("updateUserList", (newList) => {
-        usersRef.current = newList;
-        setUsers(usersRef.current);
-      })
-    );
-
-    return () => {
-      unsub.forEach((u) => u());
-    };
-  }, [c2c, isMaster, setUsers]);
-
-  return null;
-};
-
-export default SubscribeUserEvents;

+ 0 - 39
src/components/users/UserCircle.jsx

@@ -1,39 +0,0 @@
-import React from "react";
-
-import styled from "styled-components";
-
-const StyledUserCircle = styled.div`
-  background-color: ${({ color }) => color};
-  width: 38px;
-  min-width: 38px;
-  height: 38px;
-  margin: 2px;
-  border-radius: 100%;
-  text-align: center;
-  line-height: 38px;
-  text-transform: capitalize;
-  ${({ isSelf }) => (isSelf ? "text-decoration: underline;" : "")};
-  ${({ isSelf }) =>
-    isSelf ? "border: 3px solid #777; line-height: 32px;" : ""};
-  cursor: ${({ isSelf }) => (isSelf ? "pointer" : "default")};
-
-  &:hover {
-    ${({ isSelf }) => (isSelf ? "filter: brightness(125%);" : "")}
-  }
-
-  @media screen and (max-width: 640px) {
-    & {
-      font-size: 0.5em;
-      width: 20px;
-      height: 20px;
-      line-height: 20px;
-    }
-  }
-`;
-
-const UserCircle = ({ name, ...rest }) => {
-  let pre = name.slice(0, 2).toLowerCase();
-  return <StyledUserCircle {...rest}>{pre}</StyledUserCircle>;
-};
-
-export default UserCircle;

+ 0 - 78
src/components/users/UserConfig.jsx

@@ -1,78 +0,0 @@
-import React from "react";
-import { SketchPicker } from "react-color";
-import { useTranslation } from "react-i18next";
-import styled from "styled-components";
-
-import Modal from "../ui/Modal";
-
-import UserCircle from "./UserCircle";
-
-const StyledInputName = styled.input`
-  &:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="color"]):not([type="button"]):not([type="reset"]) {
-    width: 12em;
-  }
-`;
-
-const emptyStyle = {};
-const emptyColors = [];
-
-const UserConfig = ({ user, setUser, editable, index }) => {
-  const { t } = useTranslation();
-
-  const [name, setName] = React.useState(user.name);
-  const [color, setColor] = React.useState(user.color);
-  const [showDetails, setShowDetails] = React.useState(false);
-
-  const handleChange = React.useCallback(
-    (e) => {
-      setName(e.target.value);
-      setUser((prevUser) => ({ ...prevUser, name: e.target.value }));
-    },
-    [setUser]
-  );
-
-  const handleChangecolor = React.useCallback((newColor) => {
-    setColor(newColor.hex);
-  }, []);
-
-  const handleChangecolorComplete = React.useCallback(
-    (newColor) => {
-      setColor(newColor.hex);
-      setUser((prevUser) => ({ ...prevUser, color: newColor.hex }));
-    },
-    [setUser]
-  );
-
-  return (
-    <>
-      <UserCircle
-        color={user.color}
-        onClick={() => editable && setShowDetails(true)}
-        title={user.name}
-        name={user.name || `${index}`}
-        isSelf={editable}
-      />
-      <Modal
-        title={t("User details")}
-        show={showDetails}
-        setShow={setShowDetails}
-      >
-        <label>{t("Username")}</label>
-        <StyledInputName value={name} onChange={handleChange} />
-
-        <label>{t("Color")}</label>
-        <SketchPicker
-          disableAlpha
-          presetColors={emptyColors}
-          color={color}
-          onChange={handleChangecolor}
-          onChangeComplete={handleChangecolorComplete}
-          styles={emptyStyle}
-          width={160}
-        />
-      </Modal>
-    </>
-  );
-};
-
-export default UserConfig;

+ 0 - 92
src/components/users/UserList.jsx

@@ -1,92 +0,0 @@
-import React from "react";
-import UserConfig from "./UserConfig";
-import useUsers from "./useUsers";
-import Touch from "../ui/Touch";
-import { useTranslation } from "react-i18next";
-
-import styled from "styled-components";
-import DropDown from "../ui/DropDown";
-
-const InlineUserList = styled.ul.attrs(() => ({ className: "uk-card" }))`
-  list-style: none;
-  margin: 0;
-  padding: 0;
-  display: flex;
-`;
-
-const InlineUserListItem = styled.li`
-  display: flex;
-  align-items: center;
-  position: relative;
-  margin-right: 3px;
-`;
-
-const UserList = styled.ul.attrs(() => {})`
-  list-style: none;
-  margin: 0;
-  padding: 1em;
-`;
-
-const UserListItem = styled.li`
-  display: flex;
-  max-width: 300px;
-  align-items: center;
-  padding: 0.4em 0;
-  & .name {
-    flex: 1;
-    margin-left: 1em;
-  }
-`;
-
-export const Users = () => {
-  const { t } = useTranslation();
-  const { currentUser, setCurrentUser, localUsers: users } = useUsers();
-  const [openUserlist, setOpenUserList] = React.useState(false);
-
-  const firstUsers = users.slice(0, 3);
-
-  return (
-    <InlineUserList>
-      {firstUsers.map((u, index) => (
-        <InlineUserListItem key={u.id}>
-          <UserConfig
-            index={index + 1}
-            user={u}
-            setUser={setCurrentUser}
-            editable={currentUser.id === u.id}
-          />
-        </InlineUserListItem>
-      ))}
-      {users.length > 3 && (
-        <InlineUserListItem key="last">
-          <div style={{ display: "inline" }}>
-            <Touch
-              onClick={() => {
-                setOpenUserList((prev) => !prev);
-              }}
-              icon="dots-three-horizontal"
-              title={t("All players")}
-            />
-            <DropDown open={openUserlist}>
-              <UserList>
-                {users.map((u, index) => (
-                  <UserListItem key={u.id}>
-                    <UserConfig
-                      index={index + 1}
-                      user={u}
-                      setUser={setCurrentUser}
-                      editable={currentUser.id === u.id}
-                    />
-                    <div className="name">{u.name}</div>
-                  </UserListItem>
-                ))}
-              </UserList>
-            </DropDown>
-          </div>
-        </InlineUserListItem>
-      )}
-    </InlineUserList>
-  );
-};
-
-export default Users;

+ 0 - 40
src/components/users/atoms.jsx

@@ -1,40 +0,0 @@
-import { nanoid } from "nanoid";
-import randomColor from "randomcolor";
-import { atom } from "recoil";
-
-export const getUser = () => {
-  if (localStorage.user) {
-    // Add some mandatory info if missing
-    const localUser = {
-      name: "Player",
-      color: randomColor({ luminosity: "dark" }),
-      uid: nanoid(),
-      ...JSON.parse(localStorage.user),
-    };
-    // Id is given by server
-    // delete localUser.id;
-    persistUser(localUser);
-    return localUser;
-  }
-  const newUser = {
-    name: "Player",
-    color: randomColor({ luminosity: "dark" }),
-    uid: nanoid(),
-  };
-  persistUser(newUser);
-  return newUser;
-};
-
-export const persistUser = (user) => {
-  localStorage.setItem("user", JSON.stringify(user));
-};
-
-export const userAtom = atom({
-  key: "user",
-  default: getUser(),
-});
-
-export const usersAtom = atom({
-  key: "users",
-  default: [],
-});

+ 0 - 5
src/components/users/index.jsx

@@ -1,5 +0,0 @@
-export { default as useUsers } from "./useUsers";
-export { default as SubscribeUserEvents } from "./SubscribeUserEvents";
-export { default as UserConfig } from "./UserConfig";
-export { default as UserList } from "./UserList";
-export { default as UserCircle } from "./UserCircle";

+ 0 - 36
src/components/users/useUsers.jsx

@@ -1,36 +0,0 @@
-import React from "react";
-import { useRecoilValue, useRecoilState } from "recoil";
-import { userAtom, usersAtom, persistUser } from "./atoms";
-
-const useUsers = () => {
-  const [currentUser, setCurrentUserState] = useRecoilState(userAtom);
-  const users = useRecoilValue(usersAtom);
-
-  const setCurrentUser = React.useCallback(
-    (callbackOrUser) => {
-      let callback = callbackOrUser;
-      if (typeof callbackOrUser === "object") {
-        callback = () => callbackOrUser;
-      }
-      setCurrentUserState((prevUser) => {
-        const newUser = {
-          ...callback(prevUser),
-          id: prevUser.id,
-          uid: prevUser.uid,
-        };
-        persistUser(newUser);
-        return newUser;
-      });
-    },
-    [setCurrentUserState]
-  );
-
-  const localUsers = React.useMemo(
-    () => users.filter(({ space }) => space === currentUser.space),
-    [currentUser.space, users]
-  );
-
-  return { currentUser, setCurrentUser, users, localUsers };
-};
-
-export default useUsers;

+ 0 - 67
src/components/utils.js

@@ -1,67 +0,0 @@
-import Diacritics from "diacritic";
-
-/**
- * Check if element or parent has className.
- * @param {DOMElement} element
- * @param {string} className
- */
-export const hasClass = (element, className) =>
-  typeof element.className === "string" &&
-  element.className.split(" ").includes(className);
-
-export const insideClass = (element, className) => {
-  if (hasClass(element, className)) {
-    return element;
-  }
-  if (!element.parentNode) {
-    return false;
-  }
-  return insideClass(element.parentNode, className);
-};
-
-export const isPointInsideRect = (point, rect) => {
-  return (
-    point.x > rect.left &&
-    point.x < rect.left + rect.width &&
-    point.y > rect.top &&
-    point.y < rect.top + rect.height
-  );
-};
-
-export const isItemInsideElement = (itemElement, otherElem) => {
-  const rect = otherElem.getBoundingClientRect();
-  const fourElem = Array.from(itemElement.querySelectorAll(".corner"));
-  return fourElem.every((corner) => {
-    const { top: y, left: x } = corner.getBoundingClientRect();
-    return isPointInsideRect({ x, y }, rect);
-  });
-};
-
-/**
- * Shuffles array in place.
- * @param {Array} a items An array containing the items.
- */
-export const shuffle = (a) => {
-  for (let i = a.length - 1; i > 0; i--) {
-    const j = Math.floor(Math.random() * (i + 1));
-    [a[i], a[j]] = [a[j], a[i]];
-  }
-  return a;
-};
-
-export const randInt = (min, max) => {
-  return Math.floor(Math.random() * (max - min + 1)) + min;
-};
-
-const cleanWord = (word) => {
-  return Diacritics.clean(word).toLowerCase();
-};
-
-export const search = (term, string) => {
-  let strings = string;
-  if (typeof string === "string") {
-    strings = [string];
-  }
-  const cleanedTerm = cleanWord(term);
-  return strings.some((s) => cleanWord(s).includes(cleanedTerm));
-};

+ 1 - 1
src/hooks/useSession.jsx

@@ -12,7 +12,7 @@ import {
 } from "../components/board";
 import { MessagesAtom, parseMessage } from "../components/message/useMessage";
 import useBoardConfig from "../components/useBoardConfig";
-import useC2C from "../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 
 export const SessionContext = React.createContext({});
 

+ 1 - 1
src/views/AutoSaveSession.jsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { useRecoilValue } from "recoil";
-import useC2C from "../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 import { MessagesAtom } from "../components/message/useMessage";
 import useTimeout from "../hooks/useTimeout";
 import useSession from "../hooks/useSession";

+ 1 - 1
src/views/BoardView/BoardView.jsx

@@ -2,7 +2,7 @@ import React from "react";
 
 import { SHOW_WELCOME } from "../../utils/settings";
 import MainView from "../../components/MainView";
-import useC2C from "../../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 
 import WelcomeModal from "./WelcomeModal";
 import NavBar from "./NavBar";

+ 1 - 1
src/views/BoardView/LoadSessionModal.jsx

@@ -1,7 +1,7 @@
 import React from "react";
 import { useTranslation } from "react-i18next";
 
-import useC2C from "../../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 import useSession from "../../hooks/useSession";
 
 import Modal from "../../components/ui/Modal";

+ 1 - 1
src/views/BoardView/NavBar.jsx

@@ -12,7 +12,7 @@ import WebConferenceButton from "../webconf/WebConferenceButton";
 
 import { getBestTranslationFromConfig } from "../../utils/api";
 import { ENABLE_WEBCONFERENCE } from "../../utils/settings";
-import useC2C from "../../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 import useLocalStorage from "../../hooks/useLocalStorage";
 
 import InfoModal from "./InfoModal";

+ 1 - 1
src/views/BoardView/WelcomeModal.jsx

@@ -6,7 +6,7 @@ import { toast } from "react-toastify";
 
 import Modal from "../../components/ui/Modal";
 
-import useC2C from "../../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 
 const StyledUrl = styled.div`
   background-color: var(--color-midGrey);

+ 1 - 1
src/views/GameListItem.jsx

@@ -6,7 +6,7 @@ import { deleteGame, getBestTranslationFromConfig } from "../utils/api";
 import { confirmAlert } from "react-confirm-alert";
 import { toast } from "react-toastify";
 import { useMutation, useQueryClient } from "react-query";
-import { media2Url } from "../components/mediaLibrary";
+import { media2Url } from "./utils";
 
 const Game = styled.li`
   position: relative;

+ 2 - 2
src/views/GameListView.jsx

@@ -4,14 +4,14 @@ import styled from "styled-components";
 import { useQuery } from "react-query";
 
 import SliderRange from "../components/ui/SliderRange";
-import Spinner from "../components/ui/Spinner";
+import Spinner from "./Spinner";
 
 import playerSVG from "../images/player.svg";
 import languageSVG from "../images/language.svg";
 import clockSVG from "../images/clock.svg";
 
 import { getGames } from "../utils/api";
-import { search } from "../components/utils";
+import { search } from "./utils";
 
 import GameListItem from "./GameListItem";
 import { StyledGameList } from "./StyledGameList";

+ 1 - 1
src/views/GameStudio.jsx

@@ -10,7 +10,7 @@ import useAuth from "../hooks/useAuth";
 import { StyledGameList } from "./StyledGameList";
 import NewGameItem from "./NewGameItem";
 import GameListItem from "./GameListItem";
-import Spinner from "../components/ui/Spinner";
+import Spinner from "./Spinner";
 
 const Filter = styled.div`
   & .incentive {

+ 3 - 3
src/views/Home.jsx

@@ -10,7 +10,7 @@ import useQueryParam from "../hooks/useQueryParam";
 import HomeNav from "./HomeNav";
 import AboutModal from "./AboutModal";
 import GameListView from "./GameListView";
-import GameStudio from "./GameStudio";
+// import GameStudio from "./GameStudio";
 
 const StyledHome = styled.div`
   min-height: 100vh;
@@ -54,9 +54,9 @@ const Home = () => {
           <Route exact path="/games">
             <GameListView />
           </Route>
-          <Route path="/studio">
+          {/*<Route path="/studio">
             <GameStudio />
-          </Route>
+  </Route>*/}
         </Switch>
         <footer>
           <button

+ 1 - 1
src/views/RoomView/RoomNavBar.jsx

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
 
 import { ENABLE_WEBCONFERENCE } from "../../utils/settings";
 import useLocalStorage from "../../hooks/useLocalStorage";
-import useC2C from "../../components/hooks/useC2C";
+import { useC2C } from "react-sync-board";
 
 import Touch from "../../components/ui/Touch";
 import { UserList } from "../../components/users";

+ 0 - 0
src/components/ui/Spinner.jsx → src/views/Spinner.jsx


+ 30 - 0
src/views/utils.js

@@ -0,0 +1,30 @@
+import Diacritics from "diacritic";
+
+const cleanWord = (word) => Diacritics.clean(word).toLowerCase();
+
+export const search = (term, string) => {
+  let strings = string;
+  if (typeof string === "string") {
+    strings = [string];
+  }
+  const cleanedTerm = cleanWord(term);
+  return strings.some((s) => cleanWord(s).includes(cleanedTerm));
+};
+
+export const media2Url = (value, apiBase) => {
+  if (value && typeof value === "object") {
+    switch (value.type) {
+      case "local":
+        return `${apiBase}/${value.content}`;
+      case "external":
+        return value.content;
+      case "dataUrl":
+        return value.content;
+      case "empty":
+        return null;
+      default:
+      // do nothing
+    }
+  }
+  return value;
+};