Bag.jsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import React, { memo } from "react";
  2. import styled, { css } from "styled-components";
  3. import { useItemActions, useItemInteraction, useWire } from "react-sync-board";
  4. import { opacify } from "color2k";
  5. import { media2Url } from "../mediaLibrary";
  6. import { isItemInsideElement, getItemElement } from "../utils";
  7. const BagWrapper = styled.div`
  8. ${({
  9. width,
  10. height,
  11. labelBackgroundColor = "#cccccc33",
  12. }) => css`
  13. width: ${width}px;
  14. height: ${height}px;
  15. position: relative;
  16. & .bag__label {
  17. font-size: 1.5em;
  18. user-select: none;
  19. background-color: ${opacify(labelBackgroundColor, 1)};
  20. position: absolute;
  21. border-radius: 0.5em;
  22. color: var(--color-darkGrey);
  23. }
  24. & .bag__label.left {
  25. padding: 1em 0em;
  26. top: 1em;
  27. left: -1em;
  28. letter-spacing: -3px;
  29. writing-mode: vertical-rl;
  30. text-orientation: upright;
  31. }
  32. & .bag__label.top {
  33. padding: 0em 1em;
  34. top: -1em;
  35. left: 1em;
  36. }
  37. `}
  38. `;
  39. const BagImage = styled.img`
  40. transition: opacity 300ms;
  41. pointer-events: none;
  42. display: ${({ visible }) => (visible ? 'block' : 'none')};
  43. `;
  44. const ItemCounter = styled.div`
  45. position: absolute;
  46. top: 50%;
  47. left: 50%;
  48. transform: translate(-50%, -50%);
  49. font-size: ${({ counterSize }) => `${counterSize}px`}
  50. `;
  51. const Bag = ({
  52. families,
  53. storedItems = [],
  54. label,
  55. width,
  56. height,
  57. bagImage = "/bag.png",
  58. emptyBagImage = "/bag_empty.png",
  59. labelPosition = "left",
  60. countersize = 50,
  61. countervisible = true,
  62. labelBackgroundColor = "#cccccc33",
  63. id,
  64. setState,
  65. }) => {
  66. const { isMaster } = useWire("board");
  67. const { register } = useItemInteraction("place");
  68. const {
  69. pushItem,
  70. getItems,
  71. batchUpdateItems,
  72. removeItems,
  73. } = useItemActions();
  74. const bagRef = React.useRef(null);
  75. const noFamilies = !Array.isArray(families) || families.length === 0;
  76. const imageContent = media2Url(bagImage) || "/bag.png";
  77. const emptyImageContent = media2Url(emptyBagImage) || "/bag_empty.png";
  78. const size = {};
  79. if (width) {
  80. size.width = width;
  81. }
  82. if (height) {
  83. size.height = height;
  84. }
  85. const onInsideItem = React.useCallback(
  86. async (itemIds) => {
  87. const items = await getItems(itemIds);
  88. const insideItems = items
  89. .filter((item) => noFamilies || families.includes(item.groupId))
  90. .filter(({ id: itemId }) =>
  91. // Skip the container itself.
  92. itemId !== id &&
  93. isItemInsideElement(getItemElement(itemId), bagRef.current)
  94. )
  95. .map(({ id }) => id);
  96. if (!insideItems.length) return;
  97. const newItems = await getItems(insideItems);
  98. await removeItems(newItems.map(item => item.id));
  99. storedItems = newItems.concat(storedItems);
  100. setState((prevState) => ({
  101. ...prevState,
  102. storedItems: storedItems,
  103. }));
  104. }, [families, storedItems, id, setState, getItems, removeItems]
  105. );
  106. const onExtract = React.useCallback(
  107. async () => {
  108. const [bag] = await getItems([id]);
  109. storedItems = [...storedItems];
  110. const item = storedItems.shift();
  111. if (item) {
  112. setState((prevState) => ({
  113. ...prevState,
  114. storedItems: storedItems,
  115. }));
  116. await pushItem({
  117. ...item,
  118. x: bag.x + bagRef.current.clientWidth / 3.0,
  119. y: bag.y + bagRef.current.clientHeight / 3.0,
  120. });
  121. }
  122. }, [id, bagRef, storedItems, setState, pushItem]
  123. );
  124. const extract = (e) => {
  125. e.stopPropagation();
  126. onExtract();
  127. }
  128. React.useEffect(() => {
  129. const unregisterList = [];
  130. unregisterList.push(register(onInsideItem));
  131. return () => {
  132. unregisterList.forEach((callback) => callback());
  133. };
  134. }, [onInsideItem, register]);
  135. return (
  136. <BagWrapper onDoubleClick={extract} ref={bagRef}
  137. labelBackgroundColor={labelBackgroundColor}
  138. width={width}
  139. height={height}
  140. >
  141. {countervisible && (<ItemCounter counterSize={countersize} >{storedItems.length}</ItemCounter>)}
  142. <BagImage
  143. visible={storedItems.length}
  144. src={imageContent}
  145. alt=""
  146. draggable={false}
  147. {...size}
  148. />
  149. <BagImage
  150. visible={!storedItems.length}
  151. src={emptyImageContent}
  152. alt=""
  153. draggable={false}
  154. {...size}
  155. />
  156. <div className={`bag__label ${labelPosition}`}>{label}</div>
  157. </BagWrapper>
  158. );
  159. };
  160. export default memo(Bag);