Browse Source

improve parsing structure

AykutSarac 2 years ago
parent
commit
a26b94f584

+ 1 - 0
src/components/Button/index.tsx

@@ -72,6 +72,7 @@ const StyledButtonContent = styled.div`
   gap: 8px;
   white-space: nowrap;
   text-overflow: ellipsis;
+  font-weight: 600;
 `;
 
 export const Button: React.FC<ButtonProps & ConditionalProps> = ({

+ 2 - 2
src/components/ErrorContainer/index.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { MdReportGmailerrorred, MdOutlineCheckCircleOutline } from "react-icons/md";
-import useConfig from "src/store/useConfig";
+import useJson from "src/store/useJson";
 import styled from "styled-components";
 
 const StyledErrorWrapper = styled.div`
@@ -42,7 +42,7 @@ const StyledError = styled.pre`
 `;
 
 export const ErrorContainer = () => {
-  const hasError = useConfig(state => state.hasError);
+  const hasError = useJson(state => state.hasError);
 
   return (
     <StyledErrorWrapper>

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

@@ -109,7 +109,7 @@ const GraphComponent = ({
 
   return (
     <StyledEditorWrapper isWidget={isWidget} onContextMenu={e => e.preventDefault()}>
-      <Loading message="Painting graph..." />
+      <Loading message="Painting graph..." loading={loading} />
       <TransformWrapper
         maxScale={2}
         minScale={0.05}

+ 2 - 4
src/components/Loading/index.tsx

@@ -1,8 +1,8 @@
 import React from "react";
-import useGraph from "src/store/useGraph";
 import styled, { keyframes } from "styled-components";
 
 interface LoadingProps {
+  loading?: boolean;
   message?: string;
 }
 
@@ -49,9 +49,7 @@ const StyledMessage = styled.div`
   font-weight: 500;
 `;
 
-export const Loading: React.FC<LoadingProps> = ({ message }) => {
-  const loading = useGraph(state => state.loading);
-
+export const Loading: React.FC<LoadingProps> = ({ loading = true, message }) => {
   if (!loading) return null;
 
   return (

+ 1 - 0
src/components/Modal/styles.tsx

@@ -52,6 +52,7 @@ export const ContentWrapper = styled.div`
   background: ${({ theme }) => theme.MODAL_BACKGROUND};
   padding: 16px;
   overflow: hidden auto;
+  max-height: 500px;
 `;
 
 export const ControlsWrapper = styled.div`

+ 18 - 20
src/components/MonacoEditor/index.tsx

@@ -2,8 +2,7 @@ import React from "react";
 import Editor, { loader, Monaco } from "@monaco-editor/react";
 import debounce from "lodash.debounce";
 import { Loading } from "src/components/Loading";
-import useConfig from "src/store/useConfig";
-import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
 
@@ -28,12 +27,13 @@ const StyledWrapper = styled.div`
 `;
 
 export const MonacoEditor = () => {
-  const json = useGraph(state => state.json);
-  const setJson = useGraph(state => state.setJson);
-  const setConfig = useConfig(state => state.setConfig);
+  const json = useJson(state => state.json);
+  const setJson = useJson(state => state.setJson);
+  const setError = useJson(state => state.setError);
+  const [loaded, setLoaded] = React.useState(false);
   const [value, setValue] = React.useState<string | undefined>(json);
 
-  const hasError = useConfig(state => state.hasError);
+  const hasError = useJson(state => state.hasError);
   const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
 
   const handleEditorWillMount = React.useCallback(
@@ -45,41 +45,39 @@ export const MonacoEditor = () => {
 
       monaco.editor.onDidChangeMarkers(([uri]) => {
         const markers = monaco.editor.getModelMarkers({ resource: uri });
-        setConfig("hasError", !!markers.length);
+        setError(!!markers.length);
       });
     },
-    [setConfig]
+    [setError]
   );
 
   const debouncedSetJson = React.useMemo(
-    () =>
-      debounce(value => {
-        if (!value || hasError) return;
-        setJson(value);
-      }, 1200),
+    () => debounce(value => {
+      if (hasError) return;
+      setJson(value || "[]");
+    }, 1200),
     [hasError, setJson]
   );
 
   React.useEffect(() => {
-    if (!hasError) debouncedSetJson(value);
+    if ((value || !hasError) && loaded) debouncedSetJson(value);
+    setLoaded(true);
 
     return () => debouncedSetJson.cancel();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [debouncedSetJson, hasError, value]);
 
   return (
     <StyledWrapper>
       <Editor
-        height="100%"
-        defaultLanguage="json"
         value={json}
         theme={lightmode}
         options={editorOptions}
-        onChange={val => {
-          setValue(val);
-          if (json) setConfig("hasChanges", true);
-        }}
+        onChange={setValue}
         loading={<Loading message="Loading Editor..." />}
         beforeMount={handleEditorWillMount}
+        defaultLanguage="json"
+        height="100%"
       />
     </StyledWrapper>
   );

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

@@ -242,7 +242,7 @@ export const Sidebar: React.FC = () => {
           </StyledElement>
         </Tooltip>
 
-        <Tooltip title="Clear JSON">
+        <Tooltip title="Delete JSON">
           <StyledElement onClick={() => setVisible("clear")(true)}>
             <AiOutlineDelete />
           </StyledElement>

+ 28 - 0
src/components/Spinner/index.tsx

@@ -0,0 +1,28 @@
+import React from 'react'
+import { CgSpinner } from 'react-icons/cg'
+import styled, { keyframes } from 'styled-components'
+
+const rotateAnimation = keyframes`
+  to { transform: rotate(360deg); }
+`;
+
+const StyledSpinnerWrapper = styled.div`
+    display: flex;
+    align-items: center;
+    padding: 25px;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+    
+    svg {
+        animation: ${rotateAnimation} 1s linear infinite;
+    }
+`;
+
+export const Spinner = () => {
+  return (
+    <StyledSpinnerWrapper>
+        <CgSpinner size={40} />
+    </StyledSpinnerWrapper>
+  )
+}

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

@@ -26,7 +26,7 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
   font-family: 'Mona Sans';
   font-size: 16px;
   user-select: none;
-  font-weight: 600;
+  font-weight: 500;
   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
     0 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
     0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);

+ 13 - 13
src/containers/Editor/BottomBar.tsx

@@ -9,14 +9,13 @@ import {
   AiOutlineUnlock,
 } from "react-icons/ai";
 import { VscAccount } from "react-icons/vsc";
-import { useJson } from "src/hooks/useFetchedJson";
 import { saveJson, updateJson } from "src/services/db/json";
-import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
 import useModal from "src/store/useModal";
 import useStored from "src/store/useStored";
 import useUser from "src/store/useUser";
 import styled from "styled-components";
+import useJson from "src/store/useJson";
 
 const StyledBottomBar = styled.div`
   display: flex;
@@ -67,27 +66,28 @@ const StyledImg = styled.img<{ light: boolean }>`
 
 export const BottomBar = () => {
   const { replace, query } = useRouter();
-  const { data } = useJson();
-
+  const data = useJson(state => state.data);
   const user = useUser(state => state.user);
-  const setVisible = useModal(state => state.setVisible);
-  const getJsonState = useGraph(state => state.getJson);
-  const hasChanges = useConfig(state => state.hasChanges);
-  const setConfig = useConfig(state => state.setConfig);
   const lightmode = useStored(state => state.lightmode);
-  const [isPrivate, setIsPrivate] = React.useState(true);
-
+  const hasChanges = useJson(state => state.hasChanges);
+  
+  const getJsonState = useGraph(state => state.getJson);
+  const setVisible = useModal(state => state.setVisible);
+  const setHasChanges = useJson(state => state.setHasChanges);
+  const [isPrivate, setIsPrivate] = React.useState(false);
+  
   React.useEffect(() => {
-    setIsPrivate(data?.data.private ?? true);
+    setIsPrivate(data?.private ?? false);
   }, [data]);
 
   const handleSaveJson = React.useCallback(() => {
     if (!user) return setVisible("login")(true);
+
     if (hasChanges) {
       toast.promise(
         saveJson({ id: query.json, data: getJsonState() }).then(res => {
           if (res.data._id) replace({ query: { json: res.data._id } });
-          setConfig("hasChanges", false);
+          setHasChanges(false);
         }),
         {
           loading: "Saving JSON...",
@@ -96,7 +96,7 @@ export const BottomBar = () => {
         }
       );
     }
-  }, [getJsonState, hasChanges, query.json, replace, setConfig, setVisible, user]);
+  }, [getJsonState, hasChanges, query.json, replace, setHasChanges, setVisible, user]);
 
   const handleLoginClick = () => {
     if (user) return setVisible("account")(true);

+ 1 - 1
src/containers/Editor/Panes.tsx

@@ -19,7 +19,7 @@ const LiveEditor = dynamic(() => import("src/containers/Editor/LiveEditor"), {
 const Panes: React.FC = () => {
   const fullscreen = useConfig(state => state.fullscreen);
   const setConfig = useConfig(state => state.setConfig);
-  const isMobile = window.innerWidth <= 768;
+  const isMobile = React.useMemo(() => window.innerWidth <= 768, []);
 
   React.useEffect(() => {
     if (isMobile) setConfig("fullscreen", true);

+ 2 - 2
src/containers/Home/index.tsx

@@ -290,9 +290,9 @@ const Footer = () => (
   <Styles.StyledFooter>
     <Styles.StyledFooterText>
       <img width="120" src="assets/icon.png" alt="icon" loading="lazy" />
-      <div>
+      <span>
       © {new Date().getFullYear()} JSON Crack - {pkg.version}
-      </div>
+      </span>
     </Styles.StyledFooterText>
     <Styles.StyledIconLinks>
       <Styles.StyledNavLink

+ 4 - 1
src/containers/Home/styles.tsx

@@ -109,6 +109,10 @@ export const StyledHeroSection = styled.section`
   gap: 1.5em;
   min-height: 40vh;
   padding: 0 3%;
+
+  h2 {
+    margin-bottom: 25px;
+  }
 `;
 
 export const StyledNavLink = styled.a`
@@ -141,7 +145,6 @@ export const StyledSubTitle = styled.h2`
   font-size: 2.5rem;
   max-width: 40rem;
   margin: 0;
-  margin-bottom: 25px;
 
   @media only screen and (max-width: 768px) {
     font-size: 1.5rem;

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

@@ -3,10 +3,10 @@ import { useRouter } from "next/router";
 import { Button } from "src/components/Button";
 import { Modal, ModalProps } from "src/components/Modal";
 import { deleteJson } from "src/services/db/json";
-import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
 
 export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useGraph(state => state.setJson);
+  const setJson = useJson(state => state.setJson);
   const { query, replace } = useRouter();
 
   const handleClear = () => {

+ 65 - 48
src/containers/Modals/CloudModal/index.tsx

@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
 import { useQuery } from "@tanstack/react-query";
 import dayjs from "dayjs";
 import relativeTime from "dayjs/plugin/relativeTime";
+import toast from "react-hot-toast";
 import {
   AiOutlineEdit,
   AiOutlineLock,
@@ -10,37 +11,29 @@ import {
   AiOutlineUnlock,
 } from "react-icons/ai";
 import { Modal, ModalProps } from "src/components/Modal";
+import { Spinner } from "src/components/Spinner";
 import { getAllJson, updateJson } from "src/services/db/json";
-import useUser from "src/store/useUser";
 import styled from "styled-components";
 
 dayjs.extend(relativeTime);
 
 const StyledModalContent = styled.div`
   display: flex;
-  flex-wrap: wrap;
+  flex-direction: column;
   gap: 14px;
   overflow: auto;
 `;
 
-const StyledJsonCard = styled.a`
+const StyledJsonCard = styled.a<{ active?: boolean }>`
   display: block;
   background: ${({ theme }) => theme.BLACK_SECONDARY};
-  border: 2px solid ${({ theme }) => theme.BLACK_SECONDARY};
+  border: 2px solid ${({ theme, active }) => active ? theme.SEAGREEN : theme.BLACK_SECONDARY};
   border-radius: 5px;
   overflow: hidden;
-  min-width: 200px;
-  max-width: 250px;
   flex: 1;
   height: 160px;
 `;
 
-const StyledImg = styled.img`
-  width: 100%;
-  height: 100px;
-  object-fit: cover;
-`;
-
 const StyledInfo = styled.div`
   padding: 4px 6px;
 `;
@@ -51,9 +44,10 @@ const StyledTitle = styled.div`
   gap: 4px;
   font-size: 14px;
   font-weight: 500;
+  width: fit-content;
+  cursor: pointer;
 
   span {
-    max-width: 100%;
     overflow: hidden;
     text-overflow: ellipsis;
   }
@@ -72,36 +66,63 @@ const StyledModal = styled(Modal)`
   }
 `;
 
-interface GraphCardProsp {
-  id?: string;
-  title: string;
-  preview: string;
-  details: string;
-}
+const StyledCreateWrapper = styled.div`
+  display: flex;
+  height: 100%;
+  gap: 6px;
+  align-items: center;
+  justify-content: center;
+  opacity: 0.6;
+  height: 45px;
+  font-size: 14px;
+  cursor: pointer;
+`;
 
-const GraphCard: React.FC<{ data: any }> = ({ data, ...props }) => {
+const StyledNameInput = styled.input`
+  background: transparent;
+  border: none;
+  outline: none;
+  width: 90%;
+  color: ${({ theme }) => theme.SEAGREEN};
+  font-weight: 600;
+`;
+
+const GraphCard: React.FC<{ data: any; refetch: () => void, active: boolean }> = ({
+  data,
+  refetch,
+  active,
+  ...props
+}) => {
   const [editMode, setEditMode] = React.useState(false);
   const [name, setName] = React.useState(data.name);
 
   const onSubmit = () => {
-    updateJson(data._id, { name });
+    toast
+      .promise(updateJson(data._id, { name }), {
+        loading: "Updating document...",
+        error: "Error occured while updating document!",
+        success: `Renamed document to ${name}`,
+      })
+      .then(refetch);
+
     setEditMode(false);
   };
 
   return (
-    <StyledJsonCard href={`?json=${data._id}`} {...props}>
-      <StyledImg
-        width="200"
-        height="100"
-        src="https://blog.shevarezo.fr/uploads/posts/bulk/FNj3yQLp_visualiser-donnees-json-diagramme-json-crack_rotate3.png"
-      />
+    <StyledJsonCard
+      href={`?json=${data._id}`}
+      as={editMode ? "div" : "a"}
+      active={active}
+      {...props}
+    >
       <StyledInfo>
         {editMode ? (
           <form onSubmit={onSubmit}>
-            <input
+            <StyledNameInput
               value={name}
               onChange={e => setName(e.currentTarget.value)}
               onClick={e => e.preventDefault()}
+              autoFocus
             />
             <input type="submit" hidden />
           </form>
@@ -121,50 +142,46 @@ const GraphCard: React.FC<{ data: any }> = ({ data, ...props }) => {
           {data.private ? <AiOutlineLock /> : <AiOutlineUnlock />}
           Last modified {dayjs(data.updatedAt).fromNow()}
         </StyledDetils>
-        <StyledDetils></StyledDetils>
       </StyledInfo>
     </StyledJsonCard>
   );
 };
 
-const StyledCreateWrapper = styled.div`
-  display: flex;
-  height: 100%;
-  align-items: center;
-  justify-content: center;
-  opacity: 0.6;
-  cursor: pointer;
-`;
-
 const CreateCard: React.FC = () => (
   <StyledJsonCard href="/editor">
     <StyledCreateWrapper>
-      <AiOutlinePlus size="30" />
+      <AiOutlinePlus size="24" />
+      Create New JSON
     </StyledCreateWrapper>
   </StyledJsonCard>
 );
 
 export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   const { isReady, query } = useRouter();
-  const user = useUser(state => state.user);
-  const { data, isLoading } = useQuery(
-    ["allJson", query, user],
+
+  const { data, isFetching, refetch } = useQuery(
+    ["allJson", query],
     () => getAllJson(),
     {
       enabled: isReady,
     }
   );
 
-  if (isLoading) return <div>loading</div>;
   return (
-    <StyledModal size="lg" visible={visible} setVisible={setVisible}>
+    <StyledModal visible={visible} setVisible={setVisible}>
       <Modal.Header>Saved On The Cloud</Modal.Header>
       <Modal.Content>
         <StyledModalContent>
-          {data?.data?.result?.map(json => (
-            <GraphCard data={json} key={json._id} />
-          ))}
-          <CreateCard />
+          {isFetching ? (
+            <Spinner />
+          ) : (
+            <>
+              <CreateCard />
+              {data?.data?.result?.map(json => (
+                <GraphCard data={json} key={json._id} refetch={refetch} active={query.json === json._id} />
+              ))}
+            </>
+          )}
         </StyledModalContent>
       </Modal.Content>
       <Modal.Controls setVisible={setVisible}></Modal.Controls>

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

@@ -4,7 +4,7 @@ import { AiOutlineUpload } from "react-icons/ai";
 import { Button } from "src/components/Button";
 import { Input } from "src/components/Input";
 import { Modal, ModalProps } from "src/components/Modal";
-import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
 import styled from "styled-components";
 
 const StyledModalContent = styled(Modal.Content)`
@@ -43,7 +43,7 @@ const StyledUploadMessage = styled.h3`
 `;
 
 export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useGraph(state => state.setJson);
+  const setJson = useJson(state => state.setJson);
   const [url, setURL] = React.useState("");
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
 
@@ -59,7 +59,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
       return fetch(url)
         .then(res => res.json())
         .then(json => {
-          setJson(JSON.stringify(json));
+          setJson(JSON.stringify(json, null, 2));
           setVisible(false);
         })
         .catch(() => toast.error("Failed to fetch JSON!"))

+ 0 - 35
src/hooks/useFetchedJson.tsx

@@ -1,35 +0,0 @@
-import React from "react";
-import { useRouter } from "next/router";
-import { useQuery } from "@tanstack/react-query";
-import { decompressFromBase64 } from "lz-string";
-import { defaultJson } from "src/constants/data";
-import { getJson } from "src/services/db/json";
-import useGraph from "src/store/useGraph";
-
-export function useJson() {
-  const { query, isReady } = useRouter();
-  const setLoading = useGraph(state => state.setLoading);
-  const setJson = useGraph(state => state.setJson);
-
-  const { data, isLoading, status } = useQuery(
-    ["dbJson", query.json],
-    () => getJson(query.json as string),
-    {
-      enabled: isReady && !!query.json,
-    }
-  );
-
-  React.useEffect(() => {
-    if (isReady) {
-      if (query.json) {
-        if (isLoading) return setLoading(true);
-        if (status || !data) setJson(defaultJson);
-
-        if (data?.data) setJson(decompressFromBase64(data.data.json) as string);
-        setLoading(false);
-      } else setJson(defaultJson);
-    }
-  }, [data, isLoading, isReady, query.json, setJson, setLoading, status]);
-
-  return { data };
-}

+ 15 - 4
src/pages/editor.tsx

@@ -1,9 +1,11 @@
 import React from "react";
 import Head from "next/head";
+import { useRouter } from "next/router";
+import { Loading } from "src/components/Loading";
 import { Sidebar } from "src/components/Sidebar";
 import { BottomBar } from "src/containers/Editor/BottomBar";
 import Panes from "src/containers/Editor/Panes";
-import { useJson } from "src/hooks/useFetchedJson";
+import useJson from "src/store/useJson";
 import useUser from "src/store/useUser";
 import styled from "styled-components";
 
@@ -27,12 +29,21 @@ export const StyledEditorWrapper = styled.div`
 `;
 
 const EditorPage: React.FC = () => {
+  const { isReady, query } = useRouter();
   const checkSession = useUser(state => state.checkSession);
-  useJson();
+  const fetchJson = useJson(state => state.fetchJson);
+  const loading = useJson(state => state.loading);
 
   React.useEffect(() => {
-    checkSession();
-  }, [checkSession]);
+    // Fetch JSON by query
+    // Check Session User
+    if (isReady) {
+      checkSession();
+      fetchJson(query.json);
+    }
+  }, [checkSession, fetchJson, isReady, query.json]);
+
+  if (loading) return <Loading message="Fetching JSON from cloud..." />;
 
   return (
     <StyledEditorWrapper>

+ 1 - 1
src/services/db/json.tsx

@@ -28,7 +28,7 @@ const getAllJson = async (): Promise<{ data: JSON[] }> =>
   await altogic.endpoint.get(`json`);
 
 const getJson = async (id: string): Promise<{ data: JSON }> =>
-  await altogic.endpoint.get(`json/${id}`);
+  await altogic.endpoint.get(`json/${id}`)
 
 const updateJson = async (id: string, data: object) =>
   await altogic.endpoint.put(`json/${id}`, {

+ 0 - 2
src/store/useConfig.tsx

@@ -16,8 +16,6 @@ const initialStates = {
   fullscreen: false,
   performanceMode: true,
   zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
-  hasChanges: false,
-  hasError: false,
 };
 
 export type Config = typeof initialStates;

+ 6 - 5
src/store/useGraph.tsx

@@ -22,7 +22,7 @@ const initialStates = {
 export type Graph = typeof initialStates;
 
 interface GraphActions {
-  setJson: (json: string) => void;
+  setGraph: (json: string) => void;
   getJson: () => string;
   setNodeEdges: (nodes: NodeData[], edges: EdgeData[]) => void;
   setLoading: (loading: boolean) => void;
@@ -36,9 +36,11 @@ interface GraphActions {
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
   getJson: () => get().json,
-  setJson: (json: string) => {
-    const { nodes, edges } = parser(json, useConfig.getState().foldNodes);
-    set({ json: JSON.stringify(parse(json), null, 2) });
+  setGraph: (data: string) => {
+    const { nodes, edges } = parser(data, useConfig.getState().foldNodes);
+    const json = JSON.stringify(parse(data), null, 2);
+
+    set({ json, loading: true });
     get().setNodeEdges(nodes, edges);
   },
   setDirection: direction => set({ direction }),
@@ -50,7 +52,6 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
       collapsedNodes: [],
       collapsedEdges: [],
       graphCollapsed: false,
-      loading: true,
     }),
   setLoading: loading => set({ loading }),
   expandNodes: nodeId => {

+ 59 - 0
src/store/useJson.tsx

@@ -0,0 +1,59 @@
+import { decompressFromBase64 } from "lz-string";
+import { altogic } from "src/api/altogic";
+import { defaultJson } from "src/constants/data";
+import useGraph from "src/store/useGraph";
+import create from "zustand";
+
+interface Json {
+  _id: string;
+  createdAt: string;
+  updatedAt: string;
+  json: string;
+  name: string;
+  private: false;
+}
+
+interface JsonActions {
+  setJson: (json: string) => void;
+  fetchJson: (jsonId: string | string[] | undefined) => void;
+  setError: (hasError: boolean) => void;
+  setHasChanges: (hasChanges: boolean) => void;
+}
+
+const initialStates = {
+  data: null as Json | null,
+  json: "",
+  loading: true,
+  hasChanges: false,
+  hasError: false,
+};
+
+export type JsonStates = typeof initialStates;
+
+const useJson = create<JsonStates & JsonActions>()(set => ({
+  ...initialStates,
+  fetchJson: async jsonId => {
+    if (jsonId) {
+      const { data } = await altogic.endpoint.get(`json/${jsonId}`);
+
+      if (data) {
+        const decompressedData = decompressFromBase64(data.json);
+        if (decompressedData) {
+          useGraph.getState().setGraph(decompressedData);
+          return set({ data, json: decompressedData ?? undefined, loading: false });
+        }
+      }
+    }
+
+    useGraph.getState().setGraph(defaultJson);
+    set({ json: defaultJson, loading: false });
+  },
+  setJson: json => {
+    useGraph.getState().setGraph(json);
+    set({ json, hasChanges: true });
+  },
+  setError: (hasError: boolean) => set({ hasError }),
+  setHasChanges: (hasChanges: boolean) => set({ hasChanges }),
+}));
+
+export default useJson;