index.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import React from "react";
  2. import { FiCopy, FiDownload } from "react-icons/fi";
  3. import { toBlob, toPng } from "html-to-image";
  4. import { Button } from "src/components/Button";
  5. import { Input } from "src/components/Input";
  6. import { Modal, ModalProps } from "src/components/Modal";
  7. import { TwitterPicker } from "react-color";
  8. import { TwitterPickerStylesProps } from "react-color/lib/components/twitter/Twitter";
  9. import styled from "styled-components";
  10. import toast from "react-hot-toast";
  11. const ColorPickerStyles: Partial<TwitterPickerStylesProps> = {
  12. card: {
  13. background: "transparent",
  14. boxShadow: "none",
  15. },
  16. body: {
  17. padding: 0,
  18. },
  19. input: {
  20. background: "rgba(0, 0, 0, 0.2)",
  21. boxShadow: "none",
  22. textTransform: "none",
  23. whiteSpace: "nowrap",
  24. textOverflow: "ellipsis",
  25. },
  26. hash: {
  27. background: "rgba(180, 180, 180, 0.3)",
  28. },
  29. };
  30. const defaultColors = [
  31. "#B80000",
  32. "#DB3E00",
  33. "#FCCB00",
  34. "#008B02",
  35. "#006B76",
  36. "#1273DE",
  37. "#004DCF",
  38. "#5300EB",
  39. "#EB9694",
  40. "#FAD0C3",
  41. "#FEF3BD",
  42. "#C1E1C5",
  43. "#BEDADC",
  44. "#C4DEF6",
  45. "#BED3F3",
  46. "#D4C4FB",
  47. "transparent",
  48. ];
  49. function downloadURI(uri: string, name: string) {
  50. var link = document.createElement("a");
  51. link.download = name;
  52. link.href = uri;
  53. document.body.appendChild(link);
  54. link.click();
  55. document.body.removeChild(link);
  56. }
  57. const StyledContainer = styled.div`
  58. display: flex;
  59. flex-direction: column;
  60. gap: 16px;
  61. padding: 12px 0;
  62. border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
  63. font-size: 12px;
  64. line-height: 16px;
  65. font-weight: 600;
  66. text-transform: uppercase;
  67. color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
  68. &:first-of-type {
  69. padding-top: 0;
  70. border: none;
  71. }
  72. `;
  73. const StyledColorWrapper = styled.div`
  74. display: flex;
  75. justify-content: space-between;
  76. `;
  77. const StyledColorIndicator = styled.div<{ color: string }>`
  78. flex: 1;
  79. width: 100%;
  80. height: auto;
  81. border-radius: 6px;
  82. background: ${({ color }) => color};
  83. border: 1px solid;
  84. border-color: rgba(0, 0, 0, 0.1);
  85. `;
  86. export const DownloadModal: React.FC<ModalProps> = ({
  87. visible,
  88. setVisible,
  89. }) => {
  90. const [fileDetails, setFileDetails] = React.useState({
  91. filename: "jsonvisio.com",
  92. backgroundColor: "transparent",
  93. quality: 1,
  94. });
  95. const clipboardImage = async () => {
  96. try {
  97. toast.loading("Copying to clipboard...", { id: "toastClipboard" });
  98. const imageElement = document.querySelector(
  99. "svg[id*='ref']"
  100. ) as HTMLElement;
  101. const blob = await toBlob(imageElement, {
  102. quality: fileDetails.quality,
  103. backgroundColor: fileDetails.backgroundColor,
  104. });
  105. if (!blob) return;
  106. navigator.clipboard.write([
  107. new ClipboardItem({
  108. [blob.type]: blob,
  109. }),
  110. ]);
  111. toast.success("Copied to clipboard");
  112. } catch (error) {
  113. toast.error("Failed to copy to clipboard");
  114. } finally {
  115. toast.dismiss("toastClipboard");
  116. setVisible(false);
  117. }
  118. };
  119. const exportAsImage = async () => {
  120. try {
  121. toast.loading("Downloading...", { id: "toastDownload" });
  122. const imageElement = document.querySelector(
  123. "svg[id*='ref']"
  124. ) as HTMLElement;
  125. const dataURI = await toPng(imageElement, {
  126. quality: fileDetails.quality,
  127. backgroundColor: fileDetails.backgroundColor,
  128. });
  129. downloadURI(dataURI, `${fileDetails.filename}.png`);
  130. } catch (error) {
  131. toast.error("Failed to download image!");
  132. } finally {
  133. toast.dismiss("toastDownload");
  134. setVisible(false);
  135. }
  136. };
  137. const updateDetails = (
  138. key: keyof typeof fileDetails,
  139. value: string | number
  140. ) => setFileDetails({ ...fileDetails, [key]: value });
  141. return (
  142. <Modal visible={visible} setVisible={setVisible}>
  143. <Modal.Header>Download Image</Modal.Header>
  144. <Modal.Content>
  145. <StyledContainer>
  146. File Name
  147. <StyledColorWrapper>
  148. <Input
  149. placeholder="File Name"
  150. value={fileDetails.filename}
  151. onChange={(e) => updateDetails("filename", e.target.value)}
  152. />
  153. </StyledColorWrapper>
  154. </StyledContainer>
  155. <StyledContainer>
  156. Background Color
  157. <StyledColorWrapper>
  158. <TwitterPicker
  159. triangle="hide"
  160. colors={defaultColors}
  161. color={fileDetails.backgroundColor}
  162. onChange={(color) => updateDetails("backgroundColor", color.hex)}
  163. styles={{
  164. default: ColorPickerStyles,
  165. }}
  166. />
  167. <StyledColorIndicator color={fileDetails.backgroundColor} />
  168. </StyledColorWrapper>
  169. </StyledContainer>
  170. </Modal.Content>
  171. <Modal.Controls setVisible={setVisible}>
  172. <Button status="SECONDARY" onClick={clipboardImage}>
  173. <FiCopy size={18} /> Clipboard
  174. </Button>
  175. <Button status="SUCCESS" onClick={exportAsImage}>
  176. <FiDownload size={18} />
  177. Download
  178. </Button>
  179. </Modal.Controls>
  180. </Modal>
  181. );
  182. };