Browse Source

Merge pull request #32 from AykutSarac/export-image

Export Graph As Image
Aykut Saraç 3 năm trước cách đây
mục cha
commit
7e233c74f4

+ 1 - 0
package.json

@@ -23,6 +23,7 @@
     "react-icons": "^4.3.1",
     "react-zoom-pan-pinch": "^2.1.3",
     "reaflow": "^5.0.4",
+    "save-html-as-image": "^1.7.1",
     "styled-components": "^5.3.5"
   },
   "devDependencies": {

+ 51 - 54
src/components/Sidebar/index.tsx

@@ -1,29 +1,22 @@
 import React from "react";
+import toast from "react-hot-toast";
 import Link from "next/link";
 import styled from "styled-components";
-import { FaFileImport } from "react-icons/fa";
-import {
-  MdUnfoldMore,
-  MdUnfoldLess,
-  MdAutoFixHigh,
-  MdOutlineAutoFixOff,
-} from "react-icons/md";
+import { CanvasDirection } from "reaflow";
+import { TiFlowMerge } from "react-icons/ti";
+import { BsList } from "react-icons/bs";
+import { MdUploadFile } from "react-icons/md";
+import { RiPatreonFill } from "react-icons/ri";
+import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
 import {
-  AiFillHome,
-  AiFillDelete,
+  AiOutlineDelete,
   AiFillGithub,
   AiOutlineTwitter,
+  AiOutlineSave,
+  AiOutlineFileAdd,
 } from "react-icons/ai";
-import {
-  CgArrowLongDownE,
-  CgArrowLongLeftE,
-  CgArrowLongRightE,
-  CgArrowLongUpE,
-} from "react-icons/cg";
 
-import { CanvasDirection } from "reaflow";
-import toast from "react-hot-toast";
-import { Tooltip } from "../Tooltip";
+import { Tooltip } from "src/components/Tooltip";
 import { ConfigActionType } from "src/reducer/reducer";
 import { useConfig } from "src/hocs/config";
 import { useRouter } from "next/router";
@@ -33,7 +26,7 @@ const StyledSidebar = styled.div`
   justify-content: space-between;
   flex-direction: column;
   align-items: center;
-  width: 42px;
+  width: 36px;
   background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
   padding: 8px;
   border-right: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
@@ -64,6 +57,10 @@ const StyledText = styled.span<{ secondary?: boolean }>`
     secondary ? theme.INTERACTIVE_NORMAL : theme.ORANGE};
 `;
 
+const StyledFlowIcon = styled(TiFlowMerge)<{ rotate: number }>`
+  transform: rotate(${({ rotate }) => `${rotate}deg`});
+`;
+
 const StyledTopWrapper = styled.nav`
   display: flex;
   justify-content: space-between;
@@ -101,15 +98,15 @@ const StyledImportFile = styled.label`
   }
 `;
 
-function getLayoutIcon(layout: CanvasDirection) {
-  if (layout === "LEFT") return <CgArrowLongLeftE />;
-  if (layout === "UP") return <CgArrowLongUpE />;
-  if (layout === "RIGHT") return <CgArrowLongRightE />;
-  return <CgArrowLongDownE />;
+function rotateLayout(layout: CanvasDirection) {
+  if (layout === "LEFT") return 90;
+  if (layout === "UP") return 180;
+  if (layout === "RIGHT") return 270;
+  return 360;
 }
 
 export const Sidebar: React.FC = () => {
-  const { settings, dispatch } = useConfig();
+  const { json, settings, dispatch } = useConfig();
   const router = useRouter();
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
 
@@ -123,11 +120,9 @@ export const Sidebar: React.FC = () => {
     toast.success(`Cleared JSON and removed from memory.`);
   };
 
-  const toggleAutoFormat = () => {
-    dispatch({ type: ConfigActionType.TOGGLE_AUTOFORMAT });
-    toast(
-      `Auto format has been ${settings.autoformat ? "disabled." : "enabled."}`
-    );
+  const handleSave = () => {
+    localStorage.setItem("json", json);
+    toast.success("Saved JSON successfully!");
   };
 
   const toggleExpandCollapse = () => {
@@ -160,47 +155,42 @@ export const Sidebar: React.FC = () => {
             </StyledLogo>
           </StyledElement>
         </Link>
-        <Tooltip title="Home">
-          <StyledElement onClick={() => router.push("/")}>
-            <AiFillHome />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Auto Format">
-          <StyledElement onClick={toggleAutoFormat}>
-            {settings.autoformat ? <MdAutoFixHigh /> : <MdOutlineAutoFixOff />}
+        <Tooltip title="Import File">
+          <StyledElement>
+            <StyledImportFile>
+              <input
+                key={jsonFile?.name}
+                onChange={handleFileChange}
+                type="file"
+                accept="application/JSON"
+              />
+              <AiOutlineFileAdd />
+            </StyledImportFile>
           </StyledElement>
         </Tooltip>
-        <Tooltip title="Change Layout">
+        <Tooltip title="Rotate Layout">
           <StyledElement
             onClick={() => dispatch({ type: ConfigActionType.TOGGLE_LAYOUT })}
           >
-            {getLayoutIcon(settings.layout)}
+            <StyledFlowIcon rotate={rotateLayout(settings.layout)} />
           </StyledElement>
         </Tooltip>
-        <Tooltip title="Toggle Compact Nodes">
+        <Tooltip title={settings.expand ? "Shrink Nodes" : "Expand Nodes"}>
           <StyledElement
             title="Toggle Expand/Collapse"
             onClick={toggleExpandCollapse}
           >
-            {settings.expand ? <MdUnfoldMore /> : <MdUnfoldLess />}
+            {settings.expand ? <CgArrowsMergeAltH /> : <CgArrowsShrinkH />}
           </StyledElement>
         </Tooltip>
         <Tooltip title="Clear JSON">
           <StyledElement onClick={handleClear}>
-            <AiFillDelete />
+            <AiOutlineDelete />
           </StyledElement>
         </Tooltip>
-        <Tooltip title="Import File">
-          <StyledElement>
-            <StyledImportFile>
-              <input
-                key={jsonFile?.name}
-                onChange={handleFileChange}
-                type="file"
-                accept="application/JSON"
-              />
-              <FaFileImport />
-            </StyledImportFile>
+        <Tooltip title="Save JSON">
+          <StyledElement onClick={handleSave}>
+            <AiOutlineSave />
           </StyledElement>
         </Tooltip>
       </StyledTopWrapper>
@@ -219,6 +209,13 @@ export const Sidebar: React.FC = () => {
             </a>
           </Link>
         </StyledElement>
+        <StyledElement>
+          <Link href="https://www.patreon.com/aykutsarac">
+            <a aria-label="Patreon" rel="me" target="_blank">
+              <RiPatreonFill />
+            </a>
+          </Link>
+        </StyledElement>
       </StyledBottomWrapper>
     </StyledSidebar>
   );

+ 1 - 1
src/components/Tooltip/index.tsx

@@ -15,7 +15,7 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
   position: absolute;
   top: 0;
   right: 0;
-  transform: translate(calc(100% + 15px), 25%);
+  transform: translate(calc(100% + 15px), 20%);
   z-index: 5;
   background: ${({ theme }) => theme.BACKGROUND_PRIMARY};
   color: ${({ theme }) => theme.TEXT_NORMAL};

+ 0 - 1
src/constants/data.ts

@@ -36,7 +36,6 @@ export const defaultJson = {
 export const defaultConfig: StorageConfig = {
   layout: "RIGHT",
   expand: true,
-  autoformat: true,
   hideEditor: false,
   zoomPanPinch: null,
   lightmode: false

+ 11 - 8
src/containers/Editor/Tools.tsx

@@ -1,11 +1,11 @@
 import React from "react";
-import toast from "react-hot-toast";
+import { saveAsPng } from "save-html-as-image";
 import {
   AiOutlineFullscreen,
-  AiOutlineSave,
   AiOutlineMinus,
   AiOutlinePlus,
 } from "react-icons/ai";
+import { FiDownload } from "react-icons/fi";
 import { HiOutlineSun, HiOutlineMoon } from "react-icons/hi";
 import { MdCenterFocusWeak } from "react-icons/md";
 import { Input } from "src/components/Input";
@@ -42,10 +42,6 @@ const StyledToolElement = styled.button`
 
 export const Tools: React.FC = () => {
   const { json, settings, dispatch } = useConfig();
-  const handleSave = () => {
-    localStorage.setItem("json", json);
-    toast.success("Saved JSON successfully!");
-  };
 
   const zoomIn = () => dispatch({ type: ConfigActionType.ZOOM_IN });
 
@@ -57,6 +53,13 @@ export const Tools: React.FC = () => {
 
   const toggleTheme = () => dispatch({ type: ConfigActionType.TOGGLE_THEME });
 
+  const exportAsImage = () => {
+    saveAsPng(document.querySelector("svg[id*='ref']"), {
+      filename: "jsonvisio.com",
+      printDate: true,
+    });
+  };
+
   return (
     <StyledTools>
       <StyledToolElement aria-label="fullscreen" onClick={toggleEditor}>
@@ -66,8 +69,8 @@ export const Tools: React.FC = () => {
         {settings.lightmode ? <HiOutlineMoon /> : <HiOutlineSun />}
       </StyledToolElement>
       <Input />
-      <StyledToolElement aria-label="save" onClick={handleSave}>
-        <AiOutlineSave />
+      <StyledToolElement aria-label="save" onClick={exportAsImage}>
+        <FiDownload />
       </StyledToolElement>
       <StyledToolElement aria-label="center canvas" onClick={centerView}>
         <MdCenterFocusWeak />

+ 17 - 28
src/containers/JsonEditor/index.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import Editor from "@monaco-editor/react";
 import parseJson from "parse-json";
 import styled from "styled-components";
-import { ErrorContainer } from "../../components/ErrorContainer/ErrorContainer";
+import { ErrorContainer } from "src/components/ErrorContainer/ErrorContainer";
 import { ConfigActionType } from "src/reducer/reducer";
 import { useConfig } from "src/hocs/config";
 import { Loading } from "src/components/Loading";
@@ -16,6 +16,7 @@ const StyledEditorWrapper = styled.div`
 `;
 
 const editorOptions = {
+  formatOnPaste: true,
   minimap: {
     enabled: false,
   },
@@ -35,36 +36,24 @@ export const JsonEditor: React.FC = () => {
   );
 
   React.useEffect(() => {
-    if (settings.autoformat) {
-      return setValue(JSON.stringify(JSON.parse(json), null, 2));
-    }
-
-    setValue(json);
-  }, [settings.autoformat, json]);
+    setValue(JSON.stringify(JSON.parse(json), null, 2));
+  }, [json]);
 
   React.useEffect(() => {
-    const formatTimer = setTimeout(
-      () => {
-        try {
-          if (value) {
-            const parsedJson = parseJson(value);
-
-            if (settings.autoformat) {
-              setValue(JSON.stringify(parsedJson, null, 2));
-            } else {
-              setValue(value);
-            }
-
-            dispatch({ type: ConfigActionType.SET_JSON, payload: value });
-          }
-
+    const formatTimer = setTimeout(() => {
+      try {
+        if (!value) {
           setError((err) => ({ ...err, message: "" }));
-        } catch (jsonError: any) {
-          setError((err) => ({ ...err, message: jsonError.message }));
+          return dispatch({ type: ConfigActionType.SET_JSON, payload: "[]" });
         }
-      },
-      settings.autoformat ? 1200 : 1800
-    );
+
+        parseJson(value);
+        dispatch({ type: ConfigActionType.SET_JSON, payload: value });
+        setError((err) => ({ ...err, message: "" }));
+      } catch (jsonError: any) {
+        setError((err) => ({ ...err, message: jsonError.message }));
+      }
+    }, 1500);
 
     return () => clearTimeout(formatTimer);
   }, [value, dispatch]);
@@ -76,8 +65,8 @@ export const JsonEditor: React.FC = () => {
         height="100%"
         defaultLanguage="json"
         value={value}
-        options={editorOptions}
         theme={editorTheme}
+        options={editorOptions}
         loading={<Loading message="Loading Editor..." />}
         onChange={(value) => setValue(value as string)}
       />

+ 0 - 10
src/reducer/reducer.ts

@@ -6,7 +6,6 @@ export enum ConfigActionType {
   SET_CONFIG,
   TOGGLE_LAYOUT,
   TOGGLE_EXPAND,
-  TOGGLE_AUTOFORMAT,
   TOGGLE_DOCK,
   TOGGLE_THEME,
   ZOOM_IN,
@@ -70,15 +69,6 @@ export const useConfigReducer: React.Reducer<AppConfig, ReducerAction> = (
       );
       return state;
 
-    case ConfigActionType.TOGGLE_AUTOFORMAT:
-      return {
-        ...state,
-        settings: {
-          ...state.settings,
-          autoformat: !state.settings.autoformat,
-        },
-      };
-
     case ConfigActionType.TOGGLE_DOCK:
       return {
         ...state,

+ 0 - 1
src/typings/global.ts

@@ -5,7 +5,6 @@ import { CanvasDirection } from "reaflow";
 export interface StorageConfig {
   layout: CanvasDirection;
   expand: boolean;
-  autoformat: boolean;
   hideEditor: boolean;
   zoomPanPinch: ReactZoomPanPinchRef | null;
   lightmode: boolean;

+ 24 - 0
yarn.lock

@@ -3298,6 +3298,11 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
 filelist@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83"
@@ -3664,6 +3669,11 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
   integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
 
+html-to-image@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.9.0.tgz#cb49bf9f4b37376771c85cfdd65863ae9420b268"
+  integrity sha512-9gaDCIYg62Ek07F2pBk76AHgYZ2gxq2YALU7rK3gNCqXuhu6cWzsOQqM7qGbjZiOzxGzrU1deDqZpAod2NEwbA==
+
 htmlparser2@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@@ -5606,6 +5616,20 @@ safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+save-html-as-image@^1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/save-html-as-image/-/save-html-as-image-1.7.1.tgz#6fa291e45a0308f1837ea90fd0b46c0ff5758501"
+  integrity sha512-9pM9ljvEppzrnGmiB+BmbzV4uncI84rfG+NEDK6CLyTvMAt6ANBIzEkwJsbKbsV09hB2Qpn6Lp4bvTrFYFWadg==
+  dependencies:
+    file-saver "^2.0.5"
+    html-to-image "^1.9.0"
+    save-svg-as-png "^1.4.17"
+
+save-svg-as-png@^1.4.17:
+  version "1.4.17"
+  resolved "https://registry.yarnpkg.com/save-svg-as-png/-/save-svg-as-png-1.4.17.tgz#294442002772a24f1db1bf8a2aaf7df4ab0cdc55"
+  integrity sha512-7QDaqJsVhdFPwviCxkgHiGm9omeaMBe1VKbHySWU6oFB2LtnGCcYS13eVoslUgq6VZC6Tjq/HddBd1K6p2PGpA==
+
 saxes@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"