MultiSaveButton.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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-size: 13px;
  163. color: #ffffff55;
  164. ${(props) => {
  165. if (props.position !== "right") {
  166. return "margin-right: 25px;";
  167. }
  168. return "margin-left: 25px;";
  169. }}
  170. max-width: 500px;
  171. overflow: hidden;
  172. text-overflow: ellipsis;
  173. > i {
  174. font-size: 18px;
  175. margin-right: 10px;
  176. float: left;
  177. color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
  178. }
  179. animation-fill-mode: forwards;
  180. @keyframes statusFloatIn {
  181. from {
  182. opacity: 0;
  183. transform: translateY(10px);
  184. }
  185. to {
  186. opacity: 1;
  187. transform: translateY(0px);
  188. }
  189. }
  190. `;
  191. const ButtonWrapper = styled.div`
  192. ${(props: { makeFlush: boolean; clearPosition?: boolean }) => {
  193. const baseStyles = `
  194. display: flex;
  195. align-items: center;
  196. z-index: 99;
  197. `;
  198. if (props.clearPosition) {
  199. return baseStyles;
  200. }
  201. if (!props.makeFlush) {
  202. return `
  203. ${baseStyles}
  204. position: absolute;
  205. justify-content: flex-end;
  206. bottom: 25px;
  207. right: 27px;
  208. left: 27px;
  209. `;
  210. }
  211. return `
  212. ${baseStyles}
  213. position: absolute;
  214. justify-content: flex-end;
  215. bottom: 5px;
  216. right: 0;
  217. `;
  218. }}
  219. `;
  220. type ButtonProps = {
  221. disabled: boolean;
  222. color: string;
  223. rounded: boolean;
  224. };
  225. const Button = styled.button<ButtonProps>`
  226. height: 35px;
  227. font-size: 13px;
  228. font-weight: 500;
  229. color: white;
  230. display: flex;
  231. align-items: center;
  232. padding: 6px 20px 7px 20px;
  233. text-align: left;
  234. border: 0;
  235. border-radius: ${(props) => (props.rounded ? "100px" : "5px 0 0 5px")};
  236. background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
  237. cursor: ${(props) => (!props.disabled ? "pointer" : "default")};
  238. user-select: none;
  239. :focus {
  240. outline: 0;
  241. }
  242. :hover {
  243. filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
  244. }
  245. > i {
  246. color: white;
  247. width: 18px;
  248. height: 18px;
  249. font-weight: 600;
  250. font-size: 14px;
  251. border-radius: 20px;
  252. display: flex;
  253. align-items: center;
  254. margin-right: 10px;
  255. margin-left: -5px;
  256. justify-content: center;
  257. }
  258. `;
  259. const DropdownSelector = styled.div`
  260. font-size: 13px;
  261. font-weight: 500;
  262. position: relative;
  263. color: #ffffff;
  264. display: flex;
  265. align-items: center;
  266. cursor: pointer;
  267. border-radius: 5px;
  268. :hover {
  269. > i {
  270. background: #ffffff22;
  271. }
  272. }
  273. > i {
  274. border-radius: 20px;
  275. font-size: 20px;
  276. margin-left: 10px;
  277. }
  278. `;
  279. const DropdownLabel = styled.div`
  280. white-space: nowrap;
  281. text-overflow: ellipsis;
  282. overflow: hidden;
  283. max-width: 200px;
  284. `;
  285. const DropdownOverlay = styled.div`
  286. position: fixed;
  287. width: 100%;
  288. height: 100%;
  289. z-index: 10;
  290. left: 0px;
  291. top: 0px;
  292. cursor: default;
  293. `;
  294. type OptionsWrapperProps = {
  295. expandTo: "left" | "right";
  296. dropdownWidth: string;
  297. dropdownMaxHeight: string;
  298. };
  299. const OptionWrapper = styled.div<OptionsWrapperProps>`
  300. position: absolute;
  301. ${(props) => (props.expandTo === "right" ? "left: 0" : "right: 0")};
  302. top: calc(100% + 10px);
  303. background: #26282f;
  304. width: ${(props) => props.dropdownWidth};
  305. max-height: ${(props) => props.dropdownMaxHeight || "300px"};
  306. border-radius: 3px;
  307. z-index: 999;
  308. overflow-y: auto;
  309. margin-bottom: 20px;
  310. box-shadow: 0px 4px 10px 0px #00000088;
  311. `;
  312. const Option = styled.div`
  313. width: 100%;
  314. border-top: 1px solid #00000000;
  315. border-bottom: 1px solid
  316. ${(props: { selected: boolean; lastItem: boolean }) =>
  317. props.lastItem ? "#ffffff00" : "#ffffff15"};
  318. font-size: 13px;
  319. padding-top: 9px;
  320. align-items: center;
  321. cursor: pointer;
  322. padding: 10px;
  323. background: ${(props: { selected: boolean; lastItem: boolean }) =>
  324. props.selected ? "#ffffff11" : ""};
  325. :hover {
  326. background: #ffffff22;
  327. }
  328. `;
  329. const DropdownButton = styled.div<{
  330. disabled: boolean;
  331. color: string;
  332. }>`
  333. height: 35px;
  334. border-radius: 0 5px 5px 0;
  335. color: white;
  336. background: ${(props) => (!props.disabled ? props.color : "#aaaabb")};
  337. margin-left: 1px;
  338. padding: 9px;
  339. > i {
  340. font-size: 18px;
  341. }
  342. :hover {
  343. filter: ${(props) => (!props.disabled ? "brightness(120%)" : "")};
  344. }
  345. `;
  346. const OptionDescription = styled(Description)`
  347. font-weight: 400;
  348. line-height: 150%;
  349. `;