index.tsx 5.2 KB

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