ItemLibrary.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import React, { memo } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { nanoid } from "nanoid";
  4. import styled from "styled-components";
  5. import { useRecoilCallback } from "recoil";
  6. import debounce from "lodash.debounce";
  7. import { useItemBaseActions } from "react-sync-board";
  8. import { search } from "../views/utils";
  9. import Chevron from "./ui/Chevron";
  10. const StyledItemList = styled.ul`
  11. display: flex;
  12. flex-flow: row wrap;
  13. list-style: none;
  14. margin: 0;
  15. padding: 0;
  16. & li.group {
  17. background-color: rgba(0, 0, 0, 0.1);
  18. padding: 0 0.5em;
  19. flex-basis: 100%;
  20. }
  21. overflow: visible;
  22. `;
  23. const StyledItem = styled.li`
  24. display: block;
  25. padding: 0.5em;
  26. margin: 0.2em;
  27. cursor: pointer;
  28. opacity: 0.7;
  29. &:hover {
  30. opacity: 1;
  31. }
  32. & > div {
  33. display: flex;
  34. flex-direction: column;
  35. align-items: center;
  36. pointer-events: none;
  37. max-width: 80px;
  38. & > span {
  39. margin-top: 0.2em;
  40. text-align: center;
  41. max-width: 80px;
  42. white-space: nowrap;
  43. overflow: hidden;
  44. text-overflow: ellipsis;
  45. padding: 0.2em 0.5em;
  46. }
  47. }
  48. &:hover > div > span {
  49. z-index: 2;
  50. max-width: none;
  51. overflow: visible;
  52. background-color: #222;
  53. box-shadow: 0px 3px 6px #00000029;
  54. }
  55. `;
  56. const size = 60;
  57. const NewItem = memo(({ type, template, component: Component, name }) => {
  58. const { pushItem } = useItemBaseActions();
  59. const addItem = React.useCallback(async () => {
  60. pushItem({
  61. ...template,
  62. id: nanoid(),
  63. type,
  64. });
  65. }, [pushItem, template, type]);
  66. return (
  67. <StyledItem onClick={addItem}>
  68. <div>
  69. <Component {...template} width={size} height={size} size={size} />
  70. <span>{name}</span>
  71. </div>
  72. </StyledItem>
  73. );
  74. });
  75. NewItem.displayName = "NewItem";
  76. const SubItemList = ({ name, items }) => {
  77. const { t } = useTranslation();
  78. const [open, setOpen] = React.useState(false);
  79. const { pushItems } = useItemBaseActions();
  80. const addItems = useRecoilCallback(
  81. async (itemsToAdd) => {
  82. pushItems(
  83. itemsToAdd.map(({ template }) => ({ ...template, id: nanoid() }))
  84. );
  85. },
  86. [pushItems]
  87. );
  88. return (
  89. <>
  90. <h3
  91. onClick={() => setOpen((prev) => !prev)}
  92. style={{ cursor: "pointer" }}
  93. >
  94. {open ? (
  95. <Chevron orientation="bottom" color="#8c8c8c" />
  96. ) : (
  97. <Chevron color="#8c8c8c" />
  98. )}{" "}
  99. {name}{" "}
  100. <span
  101. style={{ fontSize: "0.6em" }}
  102. onClick={(e) => {
  103. e.preventDefault();
  104. e.stopPropagation();
  105. addItems(items);
  106. }}
  107. >
  108. [{t("Add all")}]
  109. </span>
  110. </h3>
  111. {open && <ItemList items={items} />}
  112. </>
  113. );
  114. };
  115. const ItemList = ({ items }) => (
  116. <StyledItemList>
  117. {items.map((node) => {
  118. if (node.type) {
  119. return <NewItem {...node} key={node.uid} />;
  120. }
  121. // it's a group
  122. return (
  123. <li key={`group_${node.name}`} className="group">
  124. <SubItemList {...node} />
  125. </li>
  126. );
  127. })}
  128. </StyledItemList>
  129. );
  130. const MemoizedItemList = memo(ItemList);
  131. const filterItems = (filter, nodes) =>
  132. nodes.reduce((acc, node) => {
  133. if (node.type) {
  134. if (search(filter, node.name)) {
  135. acc.push(node);
  136. }
  137. return acc;
  138. }
  139. const filteredItems = filterItems(filter, node.items);
  140. if (filteredItems.length) {
  141. acc.push({ ...node, items: filteredItems });
  142. }
  143. return acc;
  144. }, []);
  145. const ItemLibrary = ({ items }) => {
  146. const { t } = useTranslation();
  147. const [filter, setFilter] = React.useState("");
  148. const [filteredItems, setFilteredItems] = React.useState(items);
  149. // eslint-disable-next-line react-hooks/exhaustive-deps
  150. const debouncedFilterItems = React.useCallback(
  151. debounce((filterToApply, itemsToFilter) => {
  152. setFilteredItems(filterItems(filterToApply, itemsToFilter));
  153. }, 500),
  154. []
  155. );
  156. React.useEffect(() => {
  157. debouncedFilterItems(filter, items);
  158. }, [debouncedFilterItems, filter, items]);
  159. return (
  160. <>
  161. <input
  162. onChange={(e) => setFilter(e.target.value)}
  163. style={{ marginBottom: "1em" }}
  164. placeholder={t("Search...")}
  165. />
  166. <MemoizedItemList items={filteredItems} />
  167. </>
  168. );
  169. };
  170. export default memo(ItemLibrary);