Browse Source

refactor: improve parsing logic

AykutSarac 2 years ago
parent
commit
59789aaeb1

+ 3 - 3
next.config.js

@@ -12,9 +12,9 @@ const nextConfig = {
   reactStrictMode: true,
   reactStrictMode: true,
   exportPathMap: async () => ({
   exportPathMap: async () => ({
     "/": { page: "/" },
     "/": { page: "/" },
-    "/editor": { page: "/Editor" },
-    "/widget": { page: "/Widget" },
-    "/embed": { page: "/Embed" },
+    "/editor": { page: "/editor", query: { json: 'string' } },
+    "/widget": { page: "/widget" },
+    "/embed": { page: "/embed" },
   }),
   }),
 };
 };
 
 

+ 4 - 0
package.json

@@ -22,6 +22,8 @@
     "html-to-image": "^1.10.8",
     "html-to-image": "^1.10.8",
     "jsonc-parser": "^3.2.0",
     "jsonc-parser": "^3.2.0",
     "jwt-decode": "^3.1.2",
     "jwt-decode": "^3.1.2",
+    "lodash.debounce": "^4.0.8",
+    "lz-string": "^1.4.4",
     "next": "^12.3.1",
     "next": "^12.3.1",
     "next-transpile-modules": "^9.1.0",
     "next-transpile-modules": "^9.1.0",
     "react": "^18.2.0",
     "react": "^18.2.0",
@@ -39,6 +41,8 @@
   "devDependencies": {
   "devDependencies": {
     "@testing-library/react": "^13.3.0",
     "@testing-library/react": "^13.3.0",
     "@trivago/prettier-plugin-sort-imports": "^3.3.0",
     "@trivago/prettier-plugin-sort-imports": "^3.3.0",
+    "@types/lodash.debounce": "^4.0.7",
+    "@types/lz-string": "^1.3.34",
     "@types/node": "^18.7.21",
     "@types/node": "^18.7.21",
     "@types/react": "18.0.21",
     "@types/react": "18.0.21",
     "@types/react-color": "^3.0.6",
     "@types/react-color": "^3.0.6",

+ 22 - 0
src/api/interceptor.tsx

@@ -0,0 +1,22 @@
+import axios from "axios";
+import toast from "react-hot-toast";
+
+const IS_DEVELOPMENT = process.env.NODE_ENV === "development";
+
+const HttpClient = axios.create({
+  baseURL: IS_DEVELOPMENT ? "https://815e-4e5a.c5-na.altogic.com/" : "",
+});
+
+HttpClient.interceptors.response.use(
+  response => response,
+  error => {
+    if (error.response.data?.errors) {
+      error.response.data?.errors.forEach(err => toast.error(err.message));
+    } else {
+      toast.error("An unexpected error occured!");
+    }
+    return Promise.reject(false);
+  }
+);
+
+export { HttpClient };

+ 4 - 1
src/components/ErrorContainer/index.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import React from "react";
 import { MdReportGmailerrorred, MdOutlineCheckCircleOutline } from "react-icons/md";
 import { MdReportGmailerrorred, MdOutlineCheckCircleOutline } from "react-icons/md";
+import useConfig from "src/store/useConfig";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 const StyledErrorWrapper = styled.div`
 const StyledErrorWrapper = styled.div`
@@ -40,7 +41,9 @@ const StyledError = styled.pre`
   white-space: pre-line;
   white-space: pre-line;
 `;
 `;
 
 
-export const ErrorContainer = ({ hasError }: { hasError: boolean }) => {
+export const ErrorContainer = () => {
+  const hasError = useConfig(state => state.hasError);
+
   return (
   return (
     <StyledErrorWrapper>
     <StyledErrorWrapper>
       <StyledErrorExpand error={hasError}>
       <StyledErrorExpand error={hasError}>

+ 10 - 32
src/components/Graph/index.tsx

@@ -83,12 +83,14 @@ const GraphComponent = ({
           (areaSize * 100) / (size.width * size.height) - 100
           (areaSize * 100) / (size.width * size.height) - 100
         );
         );
 
 
-        setSize({ width: layout.width + 400, height: layout.height + 400 });
-
         requestAnimationFrame(() => {
         requestAnimationFrame(() => {
+          setSize({
+            width: (layout.width as number) + 400,
+            height: (layout.height as number) + 400,
+          });
           setTimeout(() => {
           setTimeout(() => {
             setLoading(false);
             setLoading(false);
-            setTimeout(() => (changeRatio > 75 || isWidget) && centerView(), 0);
+            setTimeout(() => (changeRatio > 65 || isWidget) && centerView(), 0);
           }, 0);
           }, 0);
         });
         });
       }
       }
@@ -96,42 +98,18 @@ const GraphComponent = ({
     [size.width, size.height, setLoading, isWidget, centerView]
     [size.width, size.height, setLoading, isWidget, centerView]
   );
   );
 
 
-  // const onLayoutChange = React.useCallback(
-  //   (layout: ElkRoot) => {
-  //     if (layout.width && layout.height) {
-  //       const areaSize = layout.width * layout.height;
-  //       const changeRatio = Math.abs(
-  //         (areaSize * 100) / (size.width * size.height) - 100
-  //       );
-
-  //       const MIN_SCALE = Math.round((400_000 / areaSize) * 100) / 100;
-
-  //       const scale = MIN_SCALE > 2 ? 1 : MIN_SCALE <= 0 ? 0.1 : MIN_SCALE;
-
-  //       setMinScale(scale);
-  //       setSize({ width: layout.width + 400, height: layout.height + 400 });
-
-  //       requestAnimationFrame(() => {
-  //         setTimeout(() => {
-  //           setLoading(false);
-  //           setTimeout(() => (changeRatio > 50 || isWidget) && centerView(), 0);
-  //         }, 0);
-  //       });
-  //     }
-  //   },
-  //   [centerView, isWidget, setLoading, size.height, size.width]
-  // );
-
   const onCanvasClick = React.useCallback(() => {
   const onCanvasClick = React.useCallback(() => {
-    const input = document.querySelector("input:focus") as HTMLInputElement;
-    if (input) input.blur();
+    requestAnimationFrame(() => {
+      const input = document.querySelector("input:focus") as HTMLInputElement;
+      if (input) input.blur();
+    });
   }, []);
   }, []);
 
 
   if (nodes.length > 8_000) return <ErrorView />;
   if (nodes.length > 8_000) return <ErrorView />;
 
 
   return (
   return (
     <StyledEditorWrapper isWidget={isWidget} onContextMenu={e => e.preventDefault()}>
     <StyledEditorWrapper isWidget={isWidget} onContextMenu={e => e.preventDefault()}>
-      {loading && <Loading message="Painting graph..." />}
+      <Loading message="Painting graph..." />
       <TransformWrapper
       <TransformWrapper
         maxScale={2}
         maxScale={2}
         minScale={0.05}
         minScale={0.05}

+ 17 - 10
src/components/Loading/index.tsx

@@ -1,4 +1,5 @@
 import React from "react";
 import React from "react";
+import useGraph from "src/store/useGraph";
 import styled, { keyframes } from "styled-components";
 import styled, { keyframes } from "styled-components";
 
 
 interface LoadingProps {
 interface LoadingProps {
@@ -48,13 +49,19 @@ const StyledMessage = styled.div`
   font-weight: 500;
   font-weight: 500;
 `;
 `;
 
 
-export const Loading: React.FC<LoadingProps> = ({ message }) => (
-  <StyledLoading>
-    <StyledLogo>
-      <StyledText>JSON</StyledText> Crack
-    </StyledLogo>
-    <StyledMessage>
-      {message ?? "Preparing the environment for you..."}
-    </StyledMessage>
-  </StyledLoading>
-);
+export const Loading: React.FC<LoadingProps> = ({ message }) => {
+  const loading = useGraph(state => state.loading);
+
+  if (!loading) return null;
+
+  return (
+    <StyledLoading>
+      <StyledLogo>
+        <StyledText>JSON</StyledText> Crack
+      </StyledLogo>
+      <StyledMessage>
+        {message ?? "Preparing the environment for you..."}
+      </StyledMessage>
+    </StyledLoading>
+  );
+};

+ 36 - 43
src/components/MonacoEditor/index.tsx

@@ -1,11 +1,10 @@
 import React from "react";
 import React from "react";
 import Editor, { loader, Monaco } from "@monaco-editor/react";
 import Editor, { loader, Monaco } from "@monaco-editor/react";
-import { parse } from "jsonc-parser";
+import debounce from "lodash.debounce";
 import { Loading } from "src/components/Loading";
 import { Loading } from "src/components/Loading";
 import useConfig from "src/store/useConfig";
 import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
 import useGraph from "src/store/useGraph";
 import useStored from "src/store/useStored";
 import useStored from "src/store/useStored";
-import { parser } from "src/utils/jsonParser";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 loader.config({
 loader.config({
@@ -28,61 +27,55 @@ const StyledWrapper = styled.div`
   grid-template-rows: minmax(0, 1fr);
   grid-template-rows: minmax(0, 1fr);
 `;
 `;
 
 
-function handleEditorWillMount(monaco: Monaco) {
-  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
-    allowComments: true,
-    comments: "ignore",
-  });
-}
+export const MonacoEditor = () => {
+  const json = useGraph(state => state.json);
+  const setJson = useGraph(state => state.setJson);
+  const setConfig = useConfig(state => state.setConfig);
+  const [value, setValue] = React.useState<string | undefined>(json);
 
 
-export const MonacoEditor = ({
-  setHasError,
-}: {
-  setHasError: (value: boolean) => void;
-}) => {
-  const [value, setValue] = React.useState<string | undefined>("");
-  const setJson = useConfig(state => state.setJson);
-  const setGraphValue = useGraph(state => state.setGraphValue);
-
-  const json = useConfig(state => state.json);
-  const foldNodes = useConfig(state => state.foldNodes);
+  const hasError = useConfig(state => state.hasError);
   const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
   const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
 
 
-  React.useEffect(() => {
-    const { nodes, edges } = parser(json, foldNodes);
-
-    setGraphValue("nodes", nodes);
-    setGraphValue("edges", edges);
-    setValue(json);
-  }, [foldNodes, json, setGraphValue]);
-
-  React.useEffect(() => {
-    const formatTimer = setTimeout(() => {
-      if (!value) {
-        setHasError(false);
-        return setJson("{}");
-      }
+  const handleEditorWillMount = React.useCallback(
+    (monaco: Monaco) => {
+      monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
+        allowComments: true,
+        comments: "ignore",
+      });
 
 
-      const errors = [];
-      const parsedJSON = JSON.stringify(parse(value, errors), null, 2);
-      if (errors.length) return setHasError(true);
+      monaco.editor.onDidChangeMarkers(([uri]) => {
+        const markers = monaco.editor.getModelMarkers({ resource: uri });
+        setConfig("hasError", !!markers.length);
+      });
+    },
+    [setConfig]
+  );
 
 
-      setJson(parsedJSON);
-      setHasError(false);
-    }, 1200);
+  const debouncedSetJson = React.useMemo(
+    () =>
+      debounce(value => {
+        if (!value) return;
+        setJson(value);
+      }, 1200),
+    [setJson]
+  );
 
 
-    return () => clearTimeout(formatTimer);
-  }, [value, setJson, setHasError]);
+  React.useEffect(() => {
+    if (!hasError) debouncedSetJson(value);
+  }, [debouncedSetJson, hasError, value]);
 
 
   return (
   return (
     <StyledWrapper>
     <StyledWrapper>
       <Editor
       <Editor
         height="100%"
         height="100%"
         defaultLanguage="json"
         defaultLanguage="json"
-        value={value}
+        value={json}
         theme={lightmode}
         theme={lightmode}
         options={editorOptions}
         options={editorOptions}
-        onChange={setValue}
+        onChange={val => {
+          if (json) setConfig("hasChanges", true);
+          setValue(val);
+        }}
         loading={<Loading message="Loading Editor..." />}
         loading={<Loading message="Loading Editor..." />}
         beforeMount={handleEditorWillMount}
         beforeMount={handleEditorWillMount}
       />
       />

+ 6 - 12
src/components/Sidebar/index.tsx

@@ -24,7 +24,6 @@ import useGraph from "src/store/useGraph";
 import useModal from "src/store/useModal";
 import useModal from "src/store/useModal";
 import { getNextDirection } from "src/utils/getNextDirection";
 import { getNextDirection } from "src/utils/getNextDirection";
 import styled from "styled-components";
 import styled from "styled-components";
-import shallow from "zustand/shallow";
 
 
 const StyledSidebar = styled.div`
 const StyledSidebar = styled.div`
   display: flex;
   display: flex;
@@ -141,22 +140,17 @@ function rotateLayout(direction: "LEFT" | "RIGHT" | "DOWN" | "UP") {
 
 
 export const Sidebar: React.FC = () => {
 export const Sidebar: React.FC = () => {
   const setVisible = useModal(state => state.setVisible);
   const setVisible = useModal(state => state.setVisible);
-  const getJson = useConfig(state => state.getJson);
+  const getJson = useGraph(state => state.getJson);
   const setDirection = useGraph(state => state.setDirection);
   const setDirection = useGraph(state => state.setDirection);
   const setConfig = useConfig(state => state.setConfig);
   const setConfig = useConfig(state => state.setConfig);
   const centerView = useConfig(state => state.centerView);
   const centerView = useConfig(state => state.centerView);
   const collapseGraph = useGraph(state => state.collapseGraph);
   const collapseGraph = useGraph(state => state.collapseGraph);
   const expandGraph = useGraph(state => state.expandGraph);
   const expandGraph = useGraph(state => state.expandGraph);
 
 
-  const [graphCollapsed, direction] = useGraph(state => [
-    state.graphCollapsed,
-    state.direction,
-  ]);
-
-  const [foldNodes, hideEditor] = useConfig(
-    state => [state.foldNodes, state.hideEditor],
-    shallow
-  );
+  const graphCollapsed = useGraph(state => state.graphCollapsed);
+  const direction = useGraph(state => state.direction);
+  const foldNodes = useConfig(state => state.foldNodes);
+  const hideEditor = useConfig(state => state.hideEditor);
 
 
   const handleSave = () => {
   const handleSave = () => {
     const a = document.createElement("a");
     const a = document.createElement("a");
@@ -266,7 +260,7 @@ export const Sidebar: React.FC = () => {
             <VscAccount />
             <VscAccount />
           </StyledElement>
           </StyledElement>
         </Tooltip>
         </Tooltip>
-        <Tooltip title="Setings">
+        <Tooltip title="Settings">
           <StyledElement onClick={() => setVisible("settings")(true)}>
           <StyledElement onClick={() => setVisible("settings")(true)}>
             <VscSettingsGear />
             <VscSettingsGear />
           </StyledElement>
           </StyledElement>

+ 21 - 3
src/containers/Editor/BottomBar.tsx

@@ -1,10 +1,15 @@
 import React from "react";
 import React from "react";
+import { useRouter } from "next/router";
 import {
 import {
+  AiOutlineCloudSync,
   AiOutlineCloudUpload,
   AiOutlineCloudUpload,
   AiOutlineLink,
   AiOutlineLink,
   AiOutlineUnlock,
   AiOutlineUnlock,
 } from "react-icons/ai";
 } from "react-icons/ai";
 import { VscAccount } from "react-icons/vsc";
 import { VscAccount } from "react-icons/vsc";
+import { saveJson } from "src/services/db/json";
+import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import useModal from "src/store/useModal";
 import useModal from "src/store/useModal";
 import useUser from "src/store/useUser";
 import useUser from "src/store/useUser";
 import styled from "styled-components";
 import styled from "styled-components";
@@ -53,8 +58,21 @@ const StyledBottomBarItem = styled.button`
 `;
 `;
 
 
 export const BottomBar = () => {
 export const BottomBar = () => {
+  const { replace, query } = useRouter();
   const user = useUser(state => state.user);
   const user = useUser(state => state.user);
   const setVisible = useModal(state => state.setVisible);
   const setVisible = useModal(state => state.setVisible);
+  const getJson = useGraph(state => state.getJson);
+  const hasChanges = useConfig(state => state.hasChanges);
+  const setConfig = useConfig(state => state.setConfig);
+
+  const handleSaveJson = async () => {
+    if (hasChanges) {
+      await saveJson({ id: query.json, data: getJson() }).then(res => {
+        if (res.data._id) replace({ query: { json: res.data._id } });
+        setConfig("hasChanges", false);
+      });
+    }
+  };
 
 
   return (
   return (
     <StyledBottomBar>
     <StyledBottomBar>
@@ -63,9 +81,9 @@ export const BottomBar = () => {
           <VscAccount />
           <VscAccount />
           {user ? user.name : "Login"}
           {user ? user.name : "Login"}
         </StyledBottomBarItem>
         </StyledBottomBarItem>
-        <StyledBottomBarItem>
-          <AiOutlineCloudUpload />
-          Unsaved Changes
+        <StyledBottomBarItem onClick={handleSaveJson}>
+          {hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
+          {hasChanges ? "Unsaved Changes" : "Saved"}
         </StyledBottomBarItem>
         </StyledBottomBarItem>
         <StyledBottomBarItem>
         <StyledBottomBarItem>
           <AiOutlineUnlock />
           <AiOutlineUnlock />

+ 2 - 3
src/containers/Editor/JsonEditor/index.tsx

@@ -11,12 +11,11 @@ const StyledEditorWrapper = styled.div`
   user-select: none;
   user-select: none;
 `;
 `;
 export const JsonEditor: React.FC = () => {
 export const JsonEditor: React.FC = () => {
-  const [hasError, setHasError] = React.useState(false);
 
 
   return (
   return (
     <StyledEditorWrapper>
     <StyledEditorWrapper>
-      <ErrorContainer hasError={hasError} />
-      <MonacoEditor setHasError={setHasError} />
+      <ErrorContainer />
+      <MonacoEditor />
     </StyledEditorWrapper>
     </StyledEditorWrapper>
   );
   );
 };
 };

+ 2 - 2
src/containers/Modals/ClearModal/index.tsx

@@ -1,10 +1,10 @@
 import React from "react";
 import React from "react";
 import { Button } from "src/components/Button";
 import { Button } from "src/components/Button";
 import { Modal, ModalProps } from "src/components/Modal";
 import { Modal, ModalProps } from "src/components/Modal";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 
 
 export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
 export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useConfig(state => state.setJson);
+  const setJson = useGraph(state => state.setJson);
 
 
   const handleClear = () => {
   const handleClear = () => {
     setJson("{}");
     setJson("{}");

+ 2 - 2
src/containers/Modals/ImportModal/index.tsx

@@ -4,7 +4,7 @@ import { AiOutlineUpload } from "react-icons/ai";
 import { Button } from "src/components/Button";
 import { Button } from "src/components/Button";
 import { Input } from "src/components/Input";
 import { Input } from "src/components/Input";
 import { Modal, ModalProps } from "src/components/Modal";
 import { Modal, ModalProps } from "src/components/Modal";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 const StyledModalContent = styled(Modal.Content)`
 const StyledModalContent = styled(Modal.Content)`
@@ -43,7 +43,7 @@ const StyledUploadMessage = styled.h3`
 `;
 `;
 
 
 export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
 export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useConfig(state => state.setJson);
+  const setJson = useGraph(state => state.setJson);
   const [url, setURL] = React.useState("");
   const [url, setURL] = React.useState("");
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
 
 

+ 17 - 0
src/pages/Editor/index.tsx

@@ -1,8 +1,13 @@
 import React from "react";
 import React from "react";
 import Head from "next/head";
 import Head from "next/head";
+import { useRouter } from "next/router";
 import { Sidebar } from "src/components/Sidebar";
 import { Sidebar } from "src/components/Sidebar";
+import { defaultJson } from "src/constants/data";
 import { BottomBar } from "src/containers/Editor/BottomBar";
 import { BottomBar } from "src/containers/Editor/BottomBar";
 import Panes from "src/containers/Editor/Panes";
 import Panes from "src/containers/Editor/Panes";
+import { getJson } from "src/services/db/json";
+import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
 export const StyledPageWrapper = styled.div`
 export const StyledPageWrapper = styled.div`
@@ -25,6 +30,18 @@ export const StyledEditorWrapper = styled.div`
 `;
 `;
 
 
 const EditorPage: React.FC = () => {
 const EditorPage: React.FC = () => {
+  const { query, isReady } = useRouter();
+  const setJson = useGraph(state => state.setJson);
+  const setConfig = useConfig(state => state.setConfig);
+
+  React.useEffect(() => {
+    if (isReady) {
+      if (typeof query.json === "string") {
+        getJson(query.json).then(res => res && setJson(res));
+      } else setJson(defaultJson);
+    }
+  }, [isReady, query.json, setConfig, setJson]);
+
   return (
   return (
     <StyledEditorWrapper>
     <StyledEditorWrapper>
       <Head>
       <Head>

+ 3 - 4
src/pages/Widget/index.tsx

@@ -66,7 +66,7 @@ const WidgetPage = () => {
   const collapsedNodes = useGraph(state => state.collapsedNodes);
   const collapsedNodes = useGraph(state => state.collapsedNodes);
   const collapsedEdges = useGraph(state => state.collapsedEdges);
   const collapsedEdges = useGraph(state => state.collapsedEdges);
   const loading = useGraph(state => state.loading);
   const loading = useGraph(state => state.loading);
-  const setGraphValue = useGraph(state => state.setGraphValue);
+  const setNodeEdges = useGraph(state => state.setNodeEdges);
 
 
   const openModal = React.useCallback(() => setModalVisible(true), []);
   const openModal = React.useCallback(() => setModalVisible(true), []);
 
 
@@ -94,8 +94,7 @@ const WidgetPage = () => {
         if (!event.data?.json) return;
         if (!event.data?.json) return;
         const { nodes, edges } = parser(event.data.json);
         const { nodes, edges } = parser(event.data.json);
 
 
-        setGraphValue("nodes", nodes);
-        setGraphValue("edges", edges);
+        setNodeEdges(nodes, edges);
       } catch (error) {
       } catch (error) {
         console.error(error);
         console.error(error);
         toast.error("Invalid JSON!");
         toast.error("Invalid JSON!");
@@ -104,7 +103,7 @@ const WidgetPage = () => {
 
 
     window.addEventListener("message", handler);
     window.addEventListener("message", handler);
     return () => window.removeEventListener("message", handler);
     return () => window.removeEventListener("message", handler);
-  }, [setGraphValue]);
+  }, [setNodeEdges]);
 
 
   if (query.json)
   if (query.json)
     return (
     return (

+ 0 - 23
src/pages/_app.tsx

@@ -1,18 +1,14 @@
 import React from "react";
 import React from "react";
 import type { AppProps } from "next/app";
 import type { AppProps } from "next/app";
-import { useRouter } from "next/router";
 import { GoogleOAuthProvider } from "@react-oauth/google";
 import { GoogleOAuthProvider } from "@react-oauth/google";
 import { init } from "@sentry/nextjs";
 import { init } from "@sentry/nextjs";
-import { decompress } from "compress-json";
 import { Toaster } from "react-hot-toast";
 import { Toaster } from "react-hot-toast";
 import { GoogleAnalytics } from "src/components/GoogleAnalytics";
 import { GoogleAnalytics } from "src/components/GoogleAnalytics";
 import GlobalStyle from "src/constants/globalStyle";
 import GlobalStyle from "src/constants/globalStyle";
 import { darkTheme, lightTheme } from "src/constants/theme";
 import { darkTheme, lightTheme } from "src/constants/theme";
 import { ModalController } from "src/containers/ModalController";
 import { ModalController } from "src/containers/ModalController";
-import useConfig from "src/store/useConfig";
 import useStored from "src/store/useStored";
 import useStored from "src/store/useStored";
 import useUser from "src/store/useUser";
 import useUser from "src/store/useUser";
-import { isValidJson } from "src/utils/isValidJson";
 import { ThemeProvider } from "styled-components";
 import { ThemeProvider } from "styled-components";
 
 
 if (process.env.NODE_ENV !== "development") {
 if (process.env.NODE_ENV !== "development") {
@@ -23,29 +19,10 @@ if (process.env.NODE_ENV !== "development") {
 }
 }
 
 
 function JsonCrack({ Component, pageProps }: AppProps) {
 function JsonCrack({ Component, pageProps }: AppProps) {
-  const { query, pathname } = useRouter();
   const lightmode = useStored(state => state.lightmode);
   const lightmode = useStored(state => state.lightmode);
-  const setJson = useConfig(state => state.setJson);
   const [isRendered, setRendered] = React.useState(false);
   const [isRendered, setRendered] = React.useState(false);
   const checkSession = useUser(state => state.checkSession);
   const checkSession = useUser(state => state.checkSession);
 
 
-  React.useEffect(() => {
-    try {
-      if (pathname !== "editor") return;
-      const isJsonValid =
-        typeof query.json === "string" &&
-        isValidJson(decodeURIComponent(query.json));
-
-      if (isJsonValid) {
-        const jsonDecoded = decompress(JSON.parse(isJsonValid));
-        const jsonString = JSON.stringify(jsonDecoded);
-        setJson(jsonString);
-      }
-    } catch (error) {
-      console.error(error);
-    }
-  }, [pathname, query.json, setJson]);
-
   React.useEffect(() => {
   React.useEffect(() => {
     setRendered(true);
     setRendered(true);
   }, []);
   }, []);

+ 0 - 7
src/pages/_document.tsx

@@ -25,13 +25,6 @@ class MyDocument extends Document {
             rel="stylesheet"
             rel="stylesheet"
             crossOrigin="anonymous"
             crossOrigin="anonymous"
           />
           />
-          <link
-            rel="preload"
-            href="Mona-Sans.woff2"
-            as="font"
-            type="font/woff2"
-            crossOrigin="anonymous"
-          ></link>
         </Head>
         </Head>
         <body>
         <body>
           <Main />
           <Main />

+ 23 - 0
src/services/db/json.tsx

@@ -0,0 +1,23 @@
+import { compressToBase64, decompressFromBase64 } from "lz-string";
+import { HttpClient } from "src/api/interceptor";
+
+const saveJson = async ({ id, data }) => {
+  const compressedData = compressToBase64(data);
+
+  if (id) {
+    return await HttpClient.put<{ _id: string }>(`json/${id}`, {
+      data: compressedData,
+    });
+  }
+
+  return await HttpClient.post<{ _id: string }>("json", {
+    data: compressedData,
+  });
+};
+
+const getJson = async (id: string) =>
+  await HttpClient.get(`json/${id}`).then(res =>
+    decompressFromBase64(res.data.data)
+  );
+
+export { saveJson, getJson };

+ 2 - 8
src/store/useConfig.tsx

@@ -1,35 +1,29 @@
 import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
 import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
-import { defaultJson } from "src/constants/data";
 import create from "zustand";
 import create from "zustand";
 
 
 type StateType = keyof typeof initialStates;
 type StateType = keyof typeof initialStates;
 type StateKey<T extends StateType> = typeof initialStates[T];
 type StateKey<T extends StateType> = typeof initialStates[T];
 
 
 interface ConfigActions {
 interface ConfigActions {
-  setJson: (json: string) => void;
   setConfig: <T extends StateType, K extends StateKey<T>>(key: T, value: K) => void;
   setConfig: <T extends StateType, K extends StateKey<T>>(key: T, value: K) => void;
-  getJson: () => string;
   zoomIn: () => void;
   zoomIn: () => void;
   zoomOut: () => void;
   zoomOut: () => void;
   centerView: () => void;
   centerView: () => void;
 }
 }
 
 
 const initialStates = {
 const initialStates = {
-  json: defaultJson,
-  cursorMode: "move" as "move" | "navigation",
   foldNodes: false,
   foldNodes: false,
   hideEditor: false,
   hideEditor: false,
   performanceMode: true,
   performanceMode: true,
-  disableLoading: false,
   zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
   zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
+  hasChanges: false,
+  hasError: false
 };
 };
 
 
 export type Config = typeof initialStates;
 export type Config = typeof initialStates;
 
 
 const useConfig = create<Config & ConfigActions>()((set, get) => ({
 const useConfig = create<Config & ConfigActions>()((set, get) => ({
   ...initialStates,
   ...initialStates,
-  getJson: () => get().json,
-  setJson: (json: string) => set({ json }),
   zoomIn: () => {
   zoomIn: () => {
     const zoomPanPinch = get().zoomPanPinch;
     const zoomPanPinch = get().zoomPanPinch;
     if (zoomPanPinch) {
     if (zoomPanPinch) {

+ 16 - 9
src/store/useGraph.tsx

@@ -1,10 +1,14 @@
+import { parse } from "jsonc-parser";
 import { CanvasDirection } from "reaflow";
 import { CanvasDirection } from "reaflow";
 import { Graph } from "src/components/Graph";
 import { Graph } from "src/components/Graph";
 import { getChildrenEdges } from "src/utils/getChildrenEdges";
 import { getChildrenEdges } from "src/utils/getChildrenEdges";
 import { getOutgoers } from "src/utils/getOutgoers";
 import { getOutgoers } from "src/utils/getOutgoers";
+import { parser } from "src/utils/jsonParser";
 import create from "zustand";
 import create from "zustand";
+import useConfig from "./useConfig";
 
 
 const initialStates = {
 const initialStates = {
+  json: null as unknown as string,
   loading: false,
   loading: false,
   direction: "RIGHT" as CanvasDirection,
   direction: "RIGHT" as CanvasDirection,
   graphCollapsed: false,
   graphCollapsed: false,
@@ -17,14 +21,10 @@ const initialStates = {
 
 
 export type Graph = typeof initialStates;
 export type Graph = typeof initialStates;
 
 
-type StateType = keyof typeof initialStates;
-type StateKey<T extends StateType> = typeof initialStates[T];
-
 interface GraphActions {
 interface GraphActions {
-  setGraphValue: <T extends StateType, K extends StateKey<T>>(
-    key: T,
-    value: K
-  ) => void;
+  setJson: (json: string) => void;
+  getJson: () => string;
+  setNodeEdges: (nodes: NodeData[], edges: EdgeData[]) => void;
   setLoading: (loading: boolean) => void;
   setLoading: (loading: boolean) => void;
   setDirection: (direction: CanvasDirection) => void;
   setDirection: (direction: CanvasDirection) => void;
   expandNodes: (nodeId: string) => void;
   expandNodes: (nodeId: string) => void;
@@ -35,15 +35,22 @@ interface GraphActions {
 
 
 const useGraph = create<Graph & GraphActions>((set, get) => ({
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
   ...initialStates,
+  getJson: () => get().json,
+  setJson: (json: string) => {
+    const { nodes, edges } = parser(json, useConfig.getState().foldNodes);
+    set({ json: JSON.stringify(parse(json), null, 2) });
+    get().setNodeEdges(nodes, edges);
+  },
   setDirection: direction => set({ direction }),
   setDirection: direction => set({ direction }),
-  setGraphValue: (key, value) =>
+  setNodeEdges: (nodes, edges) =>
     set({
     set({
+      nodes,
+      edges,
       collapsedParents: [],
       collapsedParents: [],
       collapsedNodes: [],
       collapsedNodes: [],
       collapsedEdges: [],
       collapsedEdges: [],
       graphCollapsed: false,
       graphCollapsed: false,
       loading: true,
       loading: true,
-      [key]: value,
     }),
     }),
   setLoading: loading => set({ loading }),
   setLoading: loading => set({ loading }),
   expandNodes: nodeId => {
   expandNodes: nodeId => {

+ 17 - 0
yarn.lock

@@ -1577,6 +1577,23 @@
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
   integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
 
 
+"@types/lodash.debounce@^4.0.7":
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
+  integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*":
+  version "4.14.191"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
+  integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+
+"@types/lz-string@^1.3.34":
+  version "1.3.34"
+  resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5"
+  integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==
+
 "@types/minimatch@*":
 "@types/minimatch@*":
   version "5.1.2"
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"