MultiSaveButton.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import React, { useState } from "react";
  2. import styled from "styled-components";
  3. import loading from "assets/loading.gif";
  4. import Description from "./Description";
  5. type MultiSelectOption = {
  6. text: string;
  7. onClick: () => void;
  8. description: string;
  9. };
  10. type Props = {
  11. options: MultiSelectOption[];
  12. disabled?: boolean;
  13. status?: string | null;
  14. color?: string;
  15. rounded?: boolean;
  16. helper?: string | null;
  17. saveText?: string | null;
  18. // Makes flush with corner if not within a modal
  19. makeFlush?: boolean;
  20. clearPosition?: boolean;
  21. statusPosition?: "right" | "left";
  22. // Provide the classname to modify styles from other components
  23. className?: string;
  24. successText?: string;
  25. expandTo?: OptionsWrapperProps["expandTo"];
  26. };
  27. const MultiSaveButton: React.FC<Props> = (props) => {
  28. const [currOptionIndex, setCurrOptionIndex] = useState<number>(0);
  29. const [isDropdownExpanded, setIsDropdownExpanded] = useState(false);
  30. const renderStatus = () => {
  31. if (props.status) {
  32. if (props.status === "successful") {
  33. return (
  34. <StatusWrapper position={props.statusPosition} successful={true}>
  35. <i className="material-icons">done</i>
  36. <StatusTextWrapper>
  37. {props?.successText || "Successfully updated"}
  38. </StatusTextWrapper>
  39. </StatusWrapper>
  40. );
  41. } else if (props.status === "loading") {
  42. return (
  43. <StatusWrapper position={props.statusPosition} successful={false}>
  44. <LoadingGif src={loading} />
  45. <StatusTextWrapper>
  46. {props.saveText || "Updating . . ."}
  47. </StatusTextWrapper>
  48. </StatusWrapper>
  49. );
  50. } else if (props.status === "error") {
  51. return (
  52. <StatusWrapper position={props.statusPosition} successful={false}>
  53. <i className="material-icons">error_outline</i>
  54. <StatusTextWrapper>Could not update</StatusTextWrapper>
  55. </StatusWrapper>
  56. );
  57. } else {
  58. return (
  59. <StatusWrapper position={props.statusPosition} successful={false}>
  60. <i className="material-icons">error_outline</i>
  61. <StatusTextWrapper>{props.status}</StatusTextWrapper>
  62. </StatusWrapper>
  63. );
  64. }
  65. } else if (props.helper) {
  66. return (
  67. <StatusWrapper position={props.statusPosition} successful={true}>
  68. {props.helper}
  69. </StatusWrapper>
  70. );
  71. }
  72. };
  73. const renderDropdown = () => {
  74. if (isDropdownExpanded) {
  75. return (
  76. <>
  77. <DropdownOverlay onClick={() => setIsDropdownExpanded(false)} />
  78. <OptionWrapper
  79. expandTo={props.expandTo || "right"}
  80. dropdownWidth="400px"
  81. dropdownMaxHeight="300px"
  82. onClick={() => setIsDropdownExpanded(false)}
  83. >
  84. {renderOptionList()}
  85. </OptionWrapper>
  86. </>
  87. );
  88. }
  89. };
  90. const renderOptionList = () => {
  91. return props.options.map((option, i, originalArray) => {
  92. return (
  93. <Option
  94. key={i}
  95. selected={option.text === originalArray[currOptionIndex]?.text}
  96. onClick={() => setCurrOptionIndex(i)}
  97. lastItem={i === originalArray.length - 1}
  98. >
  99. {option.text}
  100. <OptionDescription margin="8px 0 0 0">
  101. {option.description}
  102. </OptionDescription>
  103. </Option>
  104. );
  105. });
  106. };
  107. return (
  108. <DropdownSelector>
  109. <ButtonWrapper
  110. makeFlush={props.makeFlush}
  111. clearPosition={props.clearPosition}
  112. className={props.className}
  113. >
  114. {props.statusPosition !== "right" && <div>{renderStatus()}</div>}
  115. <Button
  116. rounded={props.rounded}
  117. disabled={props.disabled}
  118. onClick={props.options[currOptionIndex]?.onClick}
  119. color={props.color || "#5561C0"}
  120. >
  121. {props.options[currOptionIndex]?.text}
  122. </Button>
  123. <DropdownButton
  124. disabled={props.disabled}
  125. color={props.color || "#5561C0"}
  126. onClick={() => setIsDropdownExpanded(!isDropdownExpanded)}
  127. >
  128. <i className="material-icons expand-icon">
  129. {isDropdownExpanded ? "expand_less" : "expand_more"}
  130. </i>
  131. </DropdownButton>
  132. {props.statusPosition === "right" && <div>{renderStatus()}</div>}
  133. </ButtonWrapper>
  134. {renderDropdown()}
  135. </DropdownSelector>
  136. );
  137. };
  138. export default MultiSaveButton;
  139. const LoadingGif = styled.img`
  140. width: 15px;
  141. height: 15px;
  142. margin-right: 9px;
  143. margin-bottom: 0px;
  144. `;
  145. const StatusTextWrapper = styled.p`
  146. display: -webkit-box;
  147. line-clamp: 2;
  148. -webkit-line-clamp: 2;
  149. -webkit-box-orient: vertical;
  150. line-height: 19px;
  151. margin: 0;
  152. `;
  153. type StatusWrapperProps = {
  154. successful: boolean;
  155. position: "right" | "left";
  156. };
  157. // TODO: prevent status re-render on form refresh to allow animation
  158. // animation: statusFloatIn 0.5s;
  159. const StatusWrapper = styled.div<StatusWrapperProps>`
  160. display: flex;
  161. align-items: center;
  162. font-family: "Work Sans", sans-serif;
  163. font-size: 13px;
  164. color: #ffffff55;
  165. ${(props) => {
  166. if (props.position !== "right") {
  167. return "margin-right: 25px;";
  168. }
  169. return "margin-left: 25px;";
  170. }}
  171. max-width: 500px;
  172. overflow: hidden;
  173. text-overflow: ellipsis;
  174. > i {
  175. font-size: 18px;
  176. margin-right: 10px;
  177. float: left;
  178. color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
  179. }
  180. animation-fill-mode: forwards;
  181. @keyframes statusFloatIn {
  182. from {
  183. opacity: 0;
  184. transform: translateY(10px);
  185. }
  186. to {
  187. opacity: 1;
  188. transform: translateY(0px);
  189. }
  190. }
  191. `;
  192. const ButtonWrapper = styled.div`
  193. ${(props: { makeFlush: boolean; clearPosition?: boolean }) => {
  194. const baseStyles = `
  195. display: flex;
  196. align-items: center;
  197. z-index: 99;
  198. `;
  199. if (props.clearPosition) {
  200. return baseStyles;
  201. }
  202. if (!props.makeFlush) {
  203. return `
  204. ${baseStyles}
  205. position: absolute;
  206. justify-content: flex-end;
  207. bottom: 25px;
  208. right: 27px;
  209. left: 27px;
  210. `;
  211. }
  212. return `
  213. ${baseStyles}
  214. position: absolute;
  215. justify-content: flex-end;
  216. bottom: 5px;
  217. right: 0;
  218. `;
  219. }}
  220. `;
  221. type ButtonProps = {
  222. disabled: boolean;
  223. color: string;
  224. rounded: boolean;
  225. };
  226. const Button = styled.button<ButtonProps>`
  227. height: 35px;
  228. font-size: 13px;
  229. font-weight: 500;
  230. font-family: "Work Sans", sans-serif;
  231. color: white;
  232. display: flex;
  233. align-items: center;
  234. padding: 6px 20px 7px 20px;
  235. text-align: left;
  236. border: 0;
  237. border-radius: ${(props) => (props.rounded ? "100px" : "5px 0 0 5px")};
  238. background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
  239. cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
  240. user-select: none;
  241. :focus {
  242. outline: 0;
  243. }
  244. :hover {
  245. filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
  246. }
  247. > i {
  248. color: white;
  249. width: 18px;
  250. height: 18px;
  251. font-weight: 600;
  252. font-size: 14px;
  253. border-radius: 20px;
  254. display: flex;
  255. align-items: center;
  256. margin-right: 10px;
  257. margin-left: -5px;
  258. justify-content: center;
  259. }
  260. `;
  261. const DropdownSelector = styled.div`
  262. font-size: 13px;
  263. font-weight: 500;
  264. position: relative;
  265. color: #ffffff;
  266. display: flex;
  267. align-items: center;
  268. cursor: pointer;
  269. border-radius: 5px;
  270. :hover {
  271. > i {
  272. background: #ffffff22;
  273. }
  274. }
  275. > i {
  276. border-radius: 20px;
  277. font-size: 20px;
  278. margin-left: 10px;
  279. }
  280. `;
  281. const DropdownLabel = styled.div`
  282. white-space: nowrap;
  283. text-overflow: ellipsis;
  284. overflow: hidden;
  285. max-width: 200px;
  286. `;
  287. const DropdownOverlay = styled.div`
  288. position: fixed;
  289. width: 100%;
  290. height: 100%;
  291. z-index: 10;
  292. left: 0px;
  293. top: 0px;
  294. cursor: default;
  295. `;
  296. type OptionsWrapperProps = {
  297. expandTo: "left" | "right";
  298. dropdownWidth: string;
  299. dropdownMaxHeight: string;
  300. };
  301. const OptionWrapper = styled.div<OptionsWrapperProps>`
  302. position: absolute;
  303. ${(props) => (props.expandTo === "right" ? "left: 0" : "right: 0")};
  304. top: calc(100% + 10px);
  305. background: #26282f;
  306. width: ${(props) => props.dropdownWidth};
  307. max-height: ${(props) => props.dropdownMaxHeight || "300px"};
  308. border-radius: 3px;
  309. z-index: 999;
  310. overflow-y: auto;
  311. margin-bottom: 20px;
  312. box-shadow: 0px 4px 10px 0px #00000088;
  313. `;
  314. const Option = styled.div`
  315. width: 100%;
  316. border-top: 1px solid #00000000;
  317. border-bottom: 1px solid
  318. ${(props: { selected: boolean; lastItem: boolean }) =>
  319. props.lastItem ? "#ffffff00" : "#ffffff15"};
  320. font-size: 13px;
  321. padding-top: 9px;
  322. align-items: center;
  323. cursor: pointer;
  324. padding: 10px;
  325. background: ${(props: { selected: boolean; lastItem: boolean }) =>
  326. props.selected ? "#ffffff11" : ""};
  327. :hover {
  328. background: #ffffff22;
  329. }
  330. `;
  331. const DropdownButton = styled.div<{
  332. disabled: boolean;
  333. color: string;
  334. }>`
  335. height: 35px;
  336. border-radius: 0 5px 5px 0;
  337. color: white;
  338. background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
  339. margin-left: 1px;
  340. padding: 9px;
  341. > i {
  342. font-size: 18px;
  343. }
  344. :hover {
  345. filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
  346. }
  347. `;
  348. const OptionDescription = styled(Description)`
  349. font-weight: 400;
  350. line-height: 150%;
  351. `;