AykutSarac 2 years ago
parent
commit
42bc27aa97

+ 5 - 0
.prettierignore

@@ -0,0 +1,5 @@
+.github
+.next
+node_modules/
+out
+public

+ 9 - 0
.prettierrc

@@ -0,0 +1,9 @@
+{
+  "trailingComma": "es5",
+  "singleQuote": false,
+  "semi": true,
+  "printWidth": 80,
+  "importOrder": ["^[~]", "^[./]"],
+  "importOrderSortSpecifiers": true,
+  "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"]
+}

+ 22 - 22
.travis.yml

@@ -1,22 +1,22 @@
-language: node_js
-node_js:
-  - "14"
-branches:
-  only:
-  - main
-cache:
-  directories:
-  - node_modules
-script:
-  - npm run test
-  - npm run lint
-  - npm run build
-deploy:
-  provider: pages
-  skip_cleanup: true
-  local_dir: out
-  token: $GITHUB_TOKEN
-  target_branch: gh-pages
-  keep_history: true
-  on:
-    branch: main
+language: node_js
+node_js:
+  - "14"
+branches:
+  only:
+    - main
+cache:
+  directories:
+    - node_modules
+script:
+  - npm run test
+  - npm run lint
+  - npm run build
+deploy:
+  provider: pages
+  skip_cleanup: true
+  local_dir: out
+  token: $GITHUB_TOKEN
+  target_branch: gh-pages
+  keep_history: true
+  on:
+    branch: main

+ 10 - 10
README.md

@@ -28,8 +28,8 @@
       <img width="800" src="/public/jsonvisio-screenshot.webp" alt="preview 1" />
   </p>
 
-
 # JSON Visio (jsonvisio.com)
+
 JSON Visio is a tool that generates graph diagrams from JSON objects. These diagrams are much easier to navigate than the textual format and to make it even more convenient, the tool also allows you to search the nodes. Additionally, the generated diagrams can also be downloaded or clipboard as image.
 
 You can use the web version at [jsonvisio.com](https://jsonvisio.com) or also run it locally as [Docker container](https://github.com/AykutSarac/jsonvisio.com#-docker).
@@ -37,14 +37,14 @@ You can use the web version at [jsonvisio.com](https://jsonvisio.com) or also ru
 > <b><a href="https://jsonvisio.com">JSON Visio - Directly onto graphs</a></b>
 
 ## ⚡️ Features
-* Search Nodes
-* Share links & Create Embed Widgets
-* Download/Clipboard as image
-* Upload JSON locally or fetch from URL
-* Great UI/UX
-* Light/Dark Mode
-* Advanced Error Message
 
+- Search Nodes
+- Share links & Create Embed Widgets
+- Download/Clipboard as image
+- Upload JSON locally or fetch from URL
+- Great UI/UX
+- Light/Dark Mode
+- Advanced Error Messages
 
 ## 🛠 Development Setup
 
@@ -52,13 +52,13 @@ You can use the web version at [jsonvisio.com](https://jsonvisio.com) or also ru
   npm install --legacy-peer-deps
   npm run dev
 ```
-  
+
 ## 🐳 Docker
 
 ```
 A Docker file is provided in the root of the repository.
 If you want to run JSON Visio locally:
-  
+
 * Build Docker image with `docker build -t jsonvisio .`
 * Run locally with `docker run -p 8888:8080 jsonvisio`
 * Go to [http://localhost:8888]

+ 17 - 17
jest.config.ts

@@ -1,17 +1,17 @@
-import nextJest from "next/jest";
-
-const createJestConfig = nextJest({
-  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
-  dir: "./",
-});
-
-// Add any custom config to be passed to Jest
-const customJestConfig = {
-  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
-  testEnvironment: "jsdom",
-  moduleDirectories: ["node_modules", "src"],
-  modulePaths: ["<rootDir>"]
-};
-
-// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
-module.exports = createJestConfig(customJestConfig);
+import nextJest from "next/jest";
+
+const createJestConfig = nextJest({
+  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
+  dir: "./",
+});
+
+// Add any custom config to be passed to Jest
+const customJestConfig = {
+  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
+  testEnvironment: "jsdom",
+  moduleDirectories: ["node_modules", "src"],
+  modulePaths: ["<rootDir>"],
+};
+
+// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
+module.exports = createJestConfig(customJestConfig);

+ 6 - 6
jest.setup.ts

@@ -1,6 +1,6 @@
-// Optional: configure or set up a testing framework before each test.
-// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
-
-// Used for __tests__/testing-library.js
-// Learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect'
+// Optional: configure or set up a testing framework before each test.
+// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
+
+// Used for __tests__/testing-library.js
+// Learn more: https://github.com/testing-library/jest-dom
+import "@testing-library/jest-dom/extend-expect";

+ 69 - 69
src/components/Button/index.tsx

@@ -1,69 +1,69 @@
-import React from "react";
-import styled, { DefaultTheme } from "styled-components";
-
-enum ButtonType {
-  PRIMARY = "PRIMARY",
-  SECONDARY = "BLURPLE",
-  DANGER = "DANGER",
-  SUCCESS = "SEAGREEN",
-  WARNING = "ORANGE",
-}
-
-export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
-  status?: keyof typeof ButtonType;
-  block?: boolean;
-}
-
-function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
-  return theme[ButtonType[status]];
-}
-
-const StyledButton = styled.button<{
-  status: keyof typeof ButtonType;
-  block: boolean;
-}>`
-  display: flex;
-  align-items: center;
-  background: ${({ status, theme }) => getButtonStatus(status, theme)};
-  color: #ffffff;
-  padding: 8px 16px;
-  min-width: 60px;
-  width: ${({ block }) => (block ? "100%" : "fit-content")};
-  height: 40px;
-
-  :disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-  }
-
-  @media only screen and (max-width: 768px) {
-    font-size: 18px;
-  }
-`;
-
-const StyledButtonContent = styled.div`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 8px;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-`;
-
-export const Button: React.FC<ButtonProps> = ({
-  children,
-  status,
-  block = false,
-  ...props
-}) => {
-  return (
-    <StyledButton
-      type="button"
-      status={status ?? "PRIMARY"}
-      block={block}
-      {...props}
-    >
-      <StyledButtonContent>{children}</StyledButtonContent>
-    </StyledButton>
-  );
-};
+import React from "react";
+import styled, { DefaultTheme } from "styled-components";
+
+enum ButtonType {
+  PRIMARY = "PRIMARY",
+  SECONDARY = "BLURPLE",
+  DANGER = "DANGER",
+  SUCCESS = "SEAGREEN",
+  WARNING = "ORANGE",
+}
+
+export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
+  status?: keyof typeof ButtonType;
+  block?: boolean;
+}
+
+function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
+  return theme[ButtonType[status]];
+}
+
+const StyledButton = styled.button<{
+  status: keyof typeof ButtonType;
+  block: boolean;
+}>`
+  display: flex;
+  align-items: center;
+  background: ${({ status, theme }) => getButtonStatus(status, theme)};
+  color: #ffffff;
+  padding: 8px 16px;
+  min-width: 60px;
+  width: ${({ block }) => (block ? "100%" : "fit-content")};
+  height: 40px;
+
+  :disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+  }
+
+  @media only screen and (max-width: 768px) {
+    font-size: 18px;
+  }
+`;
+
+const StyledButtonContent = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 8px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+export const Button: React.FC<ButtonProps> = ({
+  children,
+  status,
+  block = false,
+  ...props
+}) => {
+  return (
+    <StyledButton
+      type="button"
+      status={status ?? "PRIMARY"}
+      block={block}
+      {...props}
+    >
+      <StyledButtonContent>{children}</StyledButtonContent>
+    </StyledButton>
+  );
+};

+ 2 - 1
src/components/CustomNode/styles.tsx

@@ -36,7 +36,8 @@ export const StyledText = styled.div<{
   height: ${({ height }) => height};
   min-height: 50;
   color: ${({ theme }) => theme.TEXT_NORMAL};
-  padding-right: ${({ parent, hideCollapse }) => parent && !hideCollapse && "20px"};
+  padding-right: ${({ parent, hideCollapse }) =>
+    parent && !hideCollapse && "20px"};
 `;
 
 export const StyledForeignObject = styled.foreignObject`

+ 49 - 49
src/components/Loading/index.tsx

@@ -1,49 +1,49 @@
-import React from "react";
-import styled from "styled-components";
-
-interface LoadingProps {
-  message?: string;
-}
-
-const StyledLoading = styled.div`
-  position: fixed;
-  top: 0;
-  left: 0;
-  display: grid;
-  place-content: center;
-  width: 100%;
-  height: 100vh;
-  text-align: center;
-  background: ${({ theme }) => theme.BLACK_DARK};
-  z-index: 10;
-`;
-
-const StyledLogo = styled.h2`
-  font-weight: 600;
-  font-size: 56px;
-  pointer-events: none;
-  margin-bottom: 10px;
-`;
-
-const StyledText = styled.span`
-  color: #faa81a;
-`;
-
-const StyledMessage = styled.div`
-  color: #b9bbbe;
-  font-size: 24px;
-  font-weight: 500;
-`;
-
-export const Loading: React.FC<LoadingProps> = ({ message }) => {
-  return (
-    <StyledLoading>
-      <StyledLogo>
-        <StyledText>JSON</StyledText> Visio
-      </StyledLogo>
-      <StyledMessage>
-        {message ?? "Preparing the environment for you..."}
-      </StyledMessage>
-    </StyledLoading>
-  );
-};
+import React from "react";
+import styled from "styled-components";
+
+interface LoadingProps {
+  message?: string;
+}
+
+const StyledLoading = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  display: grid;
+  place-content: center;
+  width: 100%;
+  height: 100vh;
+  text-align: center;
+  background: ${({ theme }) => theme.BLACK_DARK};
+  z-index: 10;
+`;
+
+const StyledLogo = styled.h2`
+  font-weight: 600;
+  font-size: 56px;
+  pointer-events: none;
+  margin-bottom: 10px;
+`;
+
+const StyledText = styled.span`
+  color: #faa81a;
+`;
+
+const StyledMessage = styled.div`
+  color: #b9bbbe;
+  font-size: 24px;
+  font-weight: 500;
+`;
+
+export const Loading: React.FC<LoadingProps> = ({ message }) => {
+  return (
+    <StyledLoading>
+      <StyledLogo>
+        <StyledText>JSON</StyledText> Visio
+      </StyledLogo>
+      <StyledMessage>
+        {message ?? "Preparing the environment for you..."}
+      </StyledMessage>
+    </StyledLoading>
+  );
+};

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

@@ -1,214 +1,214 @@
-import React from "react";
-import toast from "react-hot-toast";
-import Link from "next/link";
-import styled from "styled-components";
-import { CanvasDirection } from "reaflow";
-import { TiFlowMerge } from "react-icons/ti";
-import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
-import {
-  AiOutlineDelete,
-  AiFillGithub,
-  AiOutlineTwitter,
-  AiOutlineSave,
-  AiOutlineFileAdd,
-  AiOutlineLink,
-} from "react-icons/ai";
-
-import { Tooltip } from "src/components/Tooltip";
-import { useRouter } from "next/router";
-import { ImportModal } from "src/containers/Modals/ImportModal";
-import { ClearModal } from "src/containers/Modals/ClearModal";
-import { ShareModal } from "src/containers/Modals/ShareModal";
-import useConfig from "src/hooks/store/useConfig";
-import { getNextLayout } from "src/containers/Editor/LiveEditor/helpers";
-import { HiHeart } from "react-icons/hi";
-import shallow from "zustand/shallow";
-import { IoAlertCircleSharp } from "react-icons/io5";
-
-const StyledSidebar = styled.div`
-  display: flex;
-  justify-content: space-between;
-  flex-direction: column;
-  align-items: center;
-  width: fit-content;
-  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
-  padding: 4px;
-  border-right: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
-`;
-
-const StyledElement = styled.div<{ beta?: boolean }>`
-  position: relative;
-  display: flex;
-  justify-content: center;
-  text-align: center;
-  font-size: 26px;
-  font-weight: 600;
-  width: 100%;
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-  cursor: pointer;
-
-  svg {
-    padding: 8px;
-    vertical-align: middle;
-  }
-
-  a {
-    display: flex;
-  }
-
-  &:hover :is(a, svg) {
-    color: ${({ theme }) => theme.INTERACTIVE_HOVER};
-  }
-`;
-
-const StyledText = styled.span<{ secondary?: boolean }>`
-  color: ${({ theme, secondary }) =>
-    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;
-  flex-direction: column;
-  align-items: center;
-  width: 100%;
-
-  & > div:nth-child(n + 1) {
-    border-bottom: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
-  }
-`;
-
-const StyledBottomWrapper = styled.nav`
-  display: flex;
-  justify-content: space-between;
-  flex-direction: column;
-  align-items: center;
-  width: 100%;
-
-  & > div,
-  a:nth-child(0) {
-    border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
-  }
-`;
-
-const StyledLogo = styled.div`
-  color: ${({ theme }) => theme.FULL_WHITE};
-`;
-
-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 getJson = useConfig((state) => state.getJson);
-  const setConfig = useConfig((state) => state.setConfig);
-  const [uploadVisible, setUploadVisible] = React.useState(false);
-  const [clearVisible, setClearVisible] = React.useState(false);
-  const [shareVisible, setShareVisible] = React.useState(false);
-  const { push } = useRouter();
-
-  const [expand, layout] = useConfig(
-    (state) => [state.expand, state.layout],
-    shallow
-  );
-
-  const handleSave = () => {
-    const a = document.createElement("a");
-    const file = new Blob([getJson()], { type: "text/plain" });
-
-    a.href = window.URL.createObjectURL(file);
-    a.download = "jsonvisio.json";
-    a.click();
-  };
-
-  const toggleExpandCollapse = () => {
-    setConfig("expand", !expand);
-    toast(`${expand ? "Collapsed" : "Expanded"} nodes.`);
-  };
-
-  const toggleLayout = () => {
-    const nextLayout = getNextLayout(layout);
-    setConfig("layout", nextLayout);
-  };
-
-  return (
-    <StyledSidebar>
-      <StyledTopWrapper>
-        <Link passHref href="/">
-          <StyledElement onClick={() => push("/")}>
-            <StyledLogo>
-              <StyledText>J</StyledText>
-              <StyledText secondary>V</StyledText>
-            </StyledLogo>
-          </StyledElement>
-        </Link>
-        <Tooltip title="Import File">
-          <StyledElement onClick={() => setUploadVisible(true)}>
-            <AiOutlineFileAdd />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Rotate Layout">
-          <StyledElement onClick={toggleLayout}>
-            <StyledFlowIcon rotate={rotateLayout(layout)} />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title={expand ? "Shrink Nodes" : "Expand Nodes"}>
-          <StyledElement
-            title="Toggle Expand/Collapse"
-            onClick={toggleExpandCollapse}
-          >
-            {expand ? <CgArrowsMergeAltH /> : <CgArrowsShrinkH />}
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Save JSON">
-          <StyledElement onClick={handleSave}>
-            <AiOutlineSave />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Clear JSON">
-          <StyledElement onClick={() => setClearVisible(true)}>
-            <AiOutlineDelete />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Share">
-          <StyledElement onClick={() => setShareVisible(true)}>
-            <AiOutlineLink />
-          </StyledElement>
-        </Tooltip>
-      </StyledTopWrapper>
-      <StyledBottomWrapper>
-        <StyledElement>
-          <Link href="https://twitter.com/aykutsarach">
-            <a aria-label="Twitter" rel="me" target="_blank">
-              <AiOutlineTwitter />
-            </a>
-          </Link>
-        </StyledElement>
-        <StyledElement>
-          <Link href="https://github.com/AykutSarac/jsonvisio.com">
-            <a aria-label="GitHub" rel="me" target="_blank">
-              <AiFillGithub />
-            </a>
-          </Link>
-        </StyledElement>
-        <StyledElement>
-          <Link href="https://github.com/sponsors/AykutSarac">
-            <a aria-label="GitHub Sponsors" rel="me" target="_blank">
-              <HiHeart />
-            </a>
-          </Link>
-        </StyledElement>
-      </StyledBottomWrapper>
-      <ImportModal visible={uploadVisible} setVisible={setUploadVisible} />
-      <ClearModal visible={clearVisible} setVisible={setClearVisible} />
-      <ShareModal visible={shareVisible} setVisible={setShareVisible} />
-    </StyledSidebar>
-  );
-};
+import React from "react";
+import toast from "react-hot-toast";
+import Link from "next/link";
+import styled from "styled-components";
+import { CanvasDirection } from "reaflow";
+import { TiFlowMerge } from "react-icons/ti";
+import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
+import {
+  AiOutlineDelete,
+  AiFillGithub,
+  AiOutlineTwitter,
+  AiOutlineSave,
+  AiOutlineFileAdd,
+  AiOutlineLink,
+} from "react-icons/ai";
+
+import { Tooltip } from "src/components/Tooltip";
+import { useRouter } from "next/router";
+import { ImportModal } from "src/containers/Modals/ImportModal";
+import { ClearModal } from "src/containers/Modals/ClearModal";
+import { ShareModal } from "src/containers/Modals/ShareModal";
+import useConfig from "src/hooks/store/useConfig";
+import { getNextLayout } from "src/containers/Editor/LiveEditor/helpers";
+import { HiHeart } from "react-icons/hi";
+import shallow from "zustand/shallow";
+import { IoAlertCircleSharp } from "react-icons/io5";
+
+const StyledSidebar = styled.div`
+  display: flex;
+  justify-content: space-between;
+  flex-direction: column;
+  align-items: center;
+  width: fit-content;
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  padding: 4px;
+  border-right: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+`;
+
+const StyledElement = styled.div<{ beta?: boolean }>`
+  position: relative;
+  display: flex;
+  justify-content: center;
+  text-align: center;
+  font-size: 26px;
+  font-weight: 600;
+  width: 100%;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+  cursor: pointer;
+
+  svg {
+    padding: 8px;
+    vertical-align: middle;
+  }
+
+  a {
+    display: flex;
+  }
+
+  &:hover :is(a, svg) {
+    color: ${({ theme }) => theme.INTERACTIVE_HOVER};
+  }
+`;
+
+const StyledText = styled.span<{ secondary?: boolean }>`
+  color: ${({ theme, secondary }) =>
+    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;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+
+  & > div:nth-child(n + 1) {
+    border-bottom: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  }
+`;
+
+const StyledBottomWrapper = styled.nav`
+  display: flex;
+  justify-content: space-between;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+
+  & > div,
+  a:nth-child(0) {
+    border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  }
+`;
+
+const StyledLogo = styled.div`
+  color: ${({ theme }) => theme.FULL_WHITE};
+`;
+
+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 getJson = useConfig((state) => state.getJson);
+  const setConfig = useConfig((state) => state.setConfig);
+  const [uploadVisible, setUploadVisible] = React.useState(false);
+  const [clearVisible, setClearVisible] = React.useState(false);
+  const [shareVisible, setShareVisible] = React.useState(false);
+  const { push } = useRouter();
+
+  const [expand, layout] = useConfig(
+    (state) => [state.expand, state.layout],
+    shallow
+  );
+
+  const handleSave = () => {
+    const a = document.createElement("a");
+    const file = new Blob([getJson()], { type: "text/plain" });
+
+    a.href = window.URL.createObjectURL(file);
+    a.download = "jsonvisio.json";
+    a.click();
+  };
+
+  const toggleExpandCollapse = () => {
+    setConfig("expand", !expand);
+    toast(`${expand ? "Collapsed" : "Expanded"} nodes.`);
+  };
+
+  const toggleLayout = () => {
+    const nextLayout = getNextLayout(layout);
+    setConfig("layout", nextLayout);
+  };
+
+  return (
+    <StyledSidebar>
+      <StyledTopWrapper>
+        <Link passHref href="/">
+          <StyledElement onClick={() => push("/")}>
+            <StyledLogo>
+              <StyledText>J</StyledText>
+              <StyledText secondary>V</StyledText>
+            </StyledLogo>
+          </StyledElement>
+        </Link>
+        <Tooltip title="Import File">
+          <StyledElement onClick={() => setUploadVisible(true)}>
+            <AiOutlineFileAdd />
+          </StyledElement>
+        </Tooltip>
+        <Tooltip title="Rotate Layout">
+          <StyledElement onClick={toggleLayout}>
+            <StyledFlowIcon rotate={rotateLayout(layout)} />
+          </StyledElement>
+        </Tooltip>
+        <Tooltip title={expand ? "Shrink Nodes" : "Expand Nodes"}>
+          <StyledElement
+            title="Toggle Expand/Collapse"
+            onClick={toggleExpandCollapse}
+          >
+            {expand ? <CgArrowsMergeAltH /> : <CgArrowsShrinkH />}
+          </StyledElement>
+        </Tooltip>
+        <Tooltip title="Save JSON">
+          <StyledElement onClick={handleSave}>
+            <AiOutlineSave />
+          </StyledElement>
+        </Tooltip>
+        <Tooltip title="Clear JSON">
+          <StyledElement onClick={() => setClearVisible(true)}>
+            <AiOutlineDelete />
+          </StyledElement>
+        </Tooltip>
+        <Tooltip title="Share">
+          <StyledElement onClick={() => setShareVisible(true)}>
+            <AiOutlineLink />
+          </StyledElement>
+        </Tooltip>
+      </StyledTopWrapper>
+      <StyledBottomWrapper>
+        <StyledElement>
+          <Link href="https://twitter.com/aykutsarach">
+            <a aria-label="Twitter" rel="me" target="_blank">
+              <AiOutlineTwitter />
+            </a>
+          </Link>
+        </StyledElement>
+        <StyledElement>
+          <Link href="https://github.com/AykutSarac/jsonvisio.com">
+            <a aria-label="GitHub" rel="me" target="_blank">
+              <AiFillGithub />
+            </a>
+          </Link>
+        </StyledElement>
+        <StyledElement>
+          <Link href="https://github.com/sponsors/AykutSarac">
+            <a aria-label="GitHub Sponsors" rel="me" target="_blank">
+              <HiHeart />
+            </a>
+          </Link>
+        </StyledElement>
+      </StyledBottomWrapper>
+      <ImportModal visible={uploadVisible} setVisible={setUploadVisible} />
+      <ClearModal visible={clearVisible} setVisible={setClearVisible} />
+      <ShareModal visible={shareVisible} setVisible={setShareVisible} />
+    </StyledSidebar>
+  );
+};

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

@@ -53,8 +53,8 @@ export const Tooltip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
 
   return (
     <StyledTooltipWrapper>
-      { title &&  <StyledTooltip visible={visible}>{title}</StyledTooltip>}
-      
+      {title && <StyledTooltip visible={visible}>{title}</StyledTooltip>}
+
       <StyledChildren
         onMouseEnter={() => setVisible(true)}
         onMouseLeave={() => setVisible(false)}

+ 12 - 10
src/components/__tests__/Button.test.tsx

@@ -1,10 +1,12 @@
-import React from "react";
-import { Button } from "src/components/Button";
-import { screen, render } from '@testing-library/react';
-
-describe("Button", () => {
-  it("should render Button component", () => {
-    render(<Button>Click Me!</Button>);
-    expect(screen.getByRole('button', { name: /Click Me/ })).toBeInTheDocument();
-  });
-});
+import React from "react";
+import { Button } from "src/components/Button";
+import { screen, render } from "@testing-library/react";
+
+describe("Button", () => {
+  it("should render Button component", () => {
+    render(<Button>Click Me!</Button>);
+    expect(
+      screen.getByRole("button", { name: /Click Me/ })
+    ).toBeInTheDocument();
+  });
+});

+ 87 - 87
src/containers/Editor/LiveEditor/helpers.ts

@@ -1,87 +1,87 @@
-import { CanvasDirection, NodeData, EdgeData } from "reaflow";
-import { parser } from "src/utils/json-editor-parser";
-
-export function getEdgeNodes(
-  graph: string,
-  isExpanded: boolean = true
-): {
-  nodes: NodeData[];
-  edges: EdgeData[];
-} {
-  const elements = parser(JSON.parse(graph));
-
-  let nodes: NodeData[] = [],
-    edges: EdgeData[] = [];
-
-  for (let i = 0; i < elements.length; i++) {
-    const el = elements[i];
-
-    if (isNode(el)) {
-      const text = renderText(el.text);
-      const lines = text.split("\n");
-      const lineLengths = lines
-        .map((line) => line.length)
-        .sort((a, b) => a - b);
-      const longestLine = lineLengths.reverse()[0];
-
-      const height = lines.length * 17.8 < 30 ? 40 : lines.length * 17.8;
-
-      nodes.push({
-        id: el.id,
-        text: el.text,
-        data: {
-          isParent: el.parent,
-        },
-        width: isExpanded ? 35 + longestLine * 8 + (el.parent && 60) : 180,
-        height,
-      });
-    } else {
-      edges.push(el);
-    }
-  }
-
-  return {
-    nodes,
-    edges,
-  };
-}
-
-export function getNextLayout(layout: CanvasDirection) {
-  switch (layout) {
-    case "RIGHT":
-      return "DOWN";
-
-    case "DOWN":
-      return "LEFT";
-
-    case "LEFT":
-      return "UP";
-
-    default:
-      return "RIGHT";
-  }
-}
-
-function renderText(value: string | object) {
-  if (value instanceof Object) {
-    let temp = "";
-    const entries = Object.entries(value);
-
-    if (Object.keys(value).every((val) => !isNaN(+val))) {
-      return Object.values(value).join("");
-    }
-
-    entries.forEach((entry) => {
-      temp += `${entry[0]}: ${String(entry[1])}\n`;
-    });
-
-    return temp;
-  }
-
-  return value;
-}
-
-function isNode(element: NodeData | EdgeData) {
-  if ("text" in element) return true;
-  return false;
-}
+import { CanvasDirection, NodeData, EdgeData } from "reaflow";
+import { parser } from "src/utils/json-editor-parser";
+
+export function getEdgeNodes(
+  graph: string,
+  isExpanded: boolean = true
+): {
+  nodes: NodeData[];
+  edges: EdgeData[];
+} {
+  const elements = parser(JSON.parse(graph));
+
+  let nodes: NodeData[] = [],
+    edges: EdgeData[] = [];
+
+  for (let i = 0; i < elements.length; i++) {
+    const el = elements[i];
+
+    if (isNode(el)) {
+      const text = renderText(el.text);
+      const lines = text.split("\n");
+      const lineLengths = lines
+        .map((line) => line.length)
+        .sort((a, b) => a - b);
+      const longestLine = lineLengths.reverse()[0];
+
+      const height = lines.length * 17.8 < 30 ? 40 : lines.length * 17.8;
+
+      nodes.push({
+        id: el.id,
+        text: el.text,
+        data: {
+          isParent: el.parent,
+        },
+        width: isExpanded ? 35 + longestLine * 8 + (el.parent && 60) : 180,
+        height,
+      });
+    } else {
+      edges.push(el);
+    }
+  }
+
+  return {
+    nodes,
+    edges,
+  };
+}
+
+export function getNextLayout(layout: CanvasDirection) {
+  switch (layout) {
+    case "RIGHT":
+      return "DOWN";
+
+    case "DOWN":
+      return "LEFT";
+
+    case "LEFT":
+      return "UP";
+
+    default:
+      return "RIGHT";
+  }
+}
+
+function renderText(value: string | object) {
+  if (value instanceof Object) {
+    let temp = "";
+    const entries = Object.entries(value);
+
+    if (Object.keys(value).every((val) => !isNaN(+val))) {
+      return Object.values(value).join("");
+    }
+
+    entries.forEach((entry) => {
+      temp += `${entry[0]}: ${String(entry[1])}\n`;
+    });
+
+    return temp;
+  }
+
+  return value;
+}
+
+function isNode(element: NodeData | EdgeData) {
+  if ("text" in element) return true;
+  return false;
+}

+ 19 - 19
src/containers/Editor/LiveEditor/index.tsx

@@ -1,19 +1,19 @@
-import React from "react";
-import styled from "styled-components";
-import { Tools } from "src/containers/Editor/Tools";
-import { Graph } from "src/components/Graph";
-
-const StyledLiveEditor = styled.div`
-  position: relative;
-`;
-
-const LiveEditor: React.FC = () => {
-  return (
-    <StyledLiveEditor>
-      <Tools />
-      <Graph />
-    </StyledLiveEditor>
-  );
-};
-
-export default LiveEditor;
+import React from "react";
+import styled from "styled-components";
+import { Tools } from "src/containers/Editor/Tools";
+import { Graph } from "src/components/Graph";
+
+const StyledLiveEditor = styled.div`
+  position: relative;
+`;
+
+const LiveEditor: React.FC = () => {
+  return (
+    <StyledLiveEditor>
+      <Tools />
+      <Graph />
+    </StyledLiveEditor>
+  );
+};
+
+export default LiveEditor;

+ 43 - 43
src/pages/Editor/index.tsx

@@ -1,43 +1,43 @@
-import React from "react";
-import Head from "next/head";
-import styled from "styled-components";
-import Panes from "src/containers/Editor/Panes";
-import { Sidebar } from "src/components/Sidebar";
-import { Incompatible } from "src/containers/Incompatible";
-
-export const StyledPageWrapper = styled.div`
-  display: flex;
-  height: 100vh;
-`;
-
-export const StyledEditorWrapper = styled.div`
-  width: 100%;
-  overflow: hidden;
-
-  @media only screen and (max-width: 568px) {
-    display: none;
-  }
-`;
-
-const EditorPage: React.FC = () => {
-  return (
-    <StyledEditorWrapper>
-      <Head>
-        <title>Editor | JSON Visio</title>
-        <meta
-          name="description"
-          content="View your JSON data in graphs instantly."
-        />
-      </Head>
-      <StyledPageWrapper>
-        <Sidebar />
-        <StyledEditorWrapper>
-          <Panes />
-        </StyledEditorWrapper>
-        <Incompatible />
-      </StyledPageWrapper>
-    </StyledEditorWrapper>
-  );
-};
-
-export default EditorPage;
+import React from "react";
+import Head from "next/head";
+import styled from "styled-components";
+import Panes from "src/containers/Editor/Panes";
+import { Sidebar } from "src/components/Sidebar";
+import { Incompatible } from "src/containers/Incompatible";
+
+export const StyledPageWrapper = styled.div`
+  display: flex;
+  height: 100vh;
+`;
+
+export const StyledEditorWrapper = styled.div`
+  width: 100%;
+  overflow: hidden;
+
+  @media only screen and (max-width: 568px) {
+    display: none;
+  }
+`;
+
+const EditorPage: React.FC = () => {
+  return (
+    <StyledEditorWrapper>
+      <Head>
+        <title>Editor | JSON Visio</title>
+        <meta
+          name="description"
+          content="View your JSON data in graphs instantly."
+        />
+      </Head>
+      <StyledPageWrapper>
+        <Sidebar />
+        <StyledEditorWrapper>
+          <Panes />
+        </StyledEditorWrapper>
+        <Incompatible />
+      </StyledPageWrapper>
+    </StyledEditorWrapper>
+  );
+};
+
+export default EditorPage;

+ 81 - 81
src/pages/_app.tsx

@@ -1,81 +1,81 @@
-import React from "react";
-import type { AppProps } from "next/app";
-import { Toaster } from "react-hot-toast";
-import { ThemeProvider } from "styled-components";
-import { init } from "@sentry/nextjs";
-
-import GlobalStyle from "src/constants/globalStyle";
-import { darkTheme, lightTheme } from "src/constants/theme";
-import { GoogleAnalytics } from "src/components/GoogleAnalytics";
-import useConfig from "src/hooks/store/useConfig";
-import { decompress } from "compress-json";
-import { useRouter } from "next/router";
-import { isValidJson } from "src/utils/isValidJson";
-import useStored from "src/hooks/store/useStored";
-
-if (process.env.NODE_ENV !== "development") {
-  init({
-    dsn: "https://[email protected]/6495191",
-    tracesSampleRate: 0.5,
-  });
-}
-
-function JsonVisio({ Component, pageProps }: AppProps) {
-  const { query } = useRouter();
-  const lightmode = useStored((state) => state.lightmode);
-  const setJson = useConfig((state) => state.setJson);
-  const [isRendered, setRendered] = React.useState(false);
-
-  React.useEffect(() => {
-    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);
-    }
-  }, [query.json, setJson]);
-
-  React.useEffect(() => {
-    if (!window.matchMedia("(display-mode: standalone)").matches) {
-      navigator.serviceWorker
-        ?.getRegistrations()
-        .then(function (registrations) {
-          for (let registration of registrations) {
-            registration.unregister();
-          }
-        })
-        .catch(function (err) {
-          console.error("Service Worker registration failed: ", err);
-        });
-    }
-
-    setRendered(true);
-  }, []);
-
-  if (!isRendered) return null;
-
-  return (
-    <>
-      <GoogleAnalytics />
-      <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
-        <GlobalStyle />
-        <Component {...pageProps} />
-        <Toaster
-          position="bottom-right"
-          toastOptions={{
-            style: {
-              background: "#4D4D4D",
-              color: "#B9BBBE",
-            },
-          }}
-        />
-      </ThemeProvider>
-    </>
-  );
-}
-
-export default JsonVisio;
+import React from "react";
+import type { AppProps } from "next/app";
+import { Toaster } from "react-hot-toast";
+import { ThemeProvider } from "styled-components";
+import { init } from "@sentry/nextjs";
+
+import GlobalStyle from "src/constants/globalStyle";
+import { darkTheme, lightTheme } from "src/constants/theme";
+import { GoogleAnalytics } from "src/components/GoogleAnalytics";
+import useConfig from "src/hooks/store/useConfig";
+import { decompress } from "compress-json";
+import { useRouter } from "next/router";
+import { isValidJson } from "src/utils/isValidJson";
+import useStored from "src/hooks/store/useStored";
+
+if (process.env.NODE_ENV !== "development") {
+  init({
+    dsn: "https://[email protected]/6495191",
+    tracesSampleRate: 0.5,
+  });
+}
+
+function JsonVisio({ Component, pageProps }: AppProps) {
+  const { query } = useRouter();
+  const lightmode = useStored((state) => state.lightmode);
+  const setJson = useConfig((state) => state.setJson);
+  const [isRendered, setRendered] = React.useState(false);
+
+  React.useEffect(() => {
+    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);
+    }
+  }, [query.json, setJson]);
+
+  React.useEffect(() => {
+    if (!window.matchMedia("(display-mode: standalone)").matches) {
+      navigator.serviceWorker
+        ?.getRegistrations()
+        .then(function (registrations) {
+          for (let registration of registrations) {
+            registration.unregister();
+          }
+        })
+        .catch(function (err) {
+          console.error("Service Worker registration failed: ", err);
+        });
+    }
+
+    setRendered(true);
+  }, []);
+
+  if (!isRendered) return null;
+
+  return (
+    <>
+      <GoogleAnalytics />
+      <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
+        <GlobalStyle />
+        <Component {...pageProps} />
+        <Toaster
+          position="bottom-right"
+          toastOptions={{
+            style: {
+              background: "#4D4D4D",
+              color: "#B9BBBE",
+            },
+          }}
+        />
+      </ThemeProvider>
+    </>
+  );
+}
+
+export default JsonVisio;

+ 69 - 69
src/pages/_document.tsx

@@ -1,69 +1,69 @@
-import Document, {
-  Html,
-  Head,
-  Main,
-  NextScript,
-  DocumentContext,
-  DocumentInitialProps,
-} from "next/document";
-import { SeoTags } from "src/components/SeoTags";
-import { ServerStyleSheet } from "styled-components";
-
-class MyDocument extends Document {
-  static async getInitialProps(
-    ctx: DocumentContext
-  ): Promise<DocumentInitialProps> {
-    const sheet = new ServerStyleSheet();
-    const originalRenderPage = ctx.renderPage;
-
-    try {
-      ctx.renderPage = () =>
-        originalRenderPage({
-          enhanceApp: (App) => (props) =>
-            sheet.collectStyles(<App {...props} />),
-        });
-
-      const initialProps = await Document.getInitialProps(ctx);
-      return {
-        ...initialProps,
-        styles: [initialProps.styles, sheet.getStyleElement()],
-      };
-    } finally {
-      sheet.seal();
-    }
-  }
-
-  render() {
-    return (
-      <Html lang="en">
-        <Head>
-          <SeoTags
-            description="Simple visualization tool for your JSON data. No forced structure, paste your JSON and view it instantly."
-            title="JSON Visio - Directly onto graphs"
-            image="https://jsonvisio.com/jsonvisio.png"
-          />
-          <meta name="theme-color" content="#36393E" />
-          <link rel="manifest" href="/manifest.json" />
-          <link rel="icon" href="/favicon.ico" />
-          <link rel="preconnect" href="https://fonts.googleapis.com" />
-          <link
-            rel="preconnect"
-            href="https://fonts.gstatic.com"
-            crossOrigin="anonymous"
-          />
-          <link
-            href="https://fonts.googleapis.com/css2?family=Catamaran:wght@400;500;700&family=Roboto+Mono:wght@500&family=Roboto:wght@400;500;700&display=swap"
-            rel="stylesheet"
-            crossOrigin="anonymous"
-          />
-        </Head>
-        <body>
-          <Main />
-          <NextScript />
-        </body>
-      </Html>
-    );
-  }
-}
-
-export default MyDocument;
+import Document, {
+  Html,
+  Head,
+  Main,
+  NextScript,
+  DocumentContext,
+  DocumentInitialProps,
+} from "next/document";
+import { SeoTags } from "src/components/SeoTags";
+import { ServerStyleSheet } from "styled-components";
+
+class MyDocument extends Document {
+  static async getInitialProps(
+    ctx: DocumentContext
+  ): Promise<DocumentInitialProps> {
+    const sheet = new ServerStyleSheet();
+    const originalRenderPage = ctx.renderPage;
+
+    try {
+      ctx.renderPage = () =>
+        originalRenderPage({
+          enhanceApp: (App) => (props) =>
+            sheet.collectStyles(<App {...props} />),
+        });
+
+      const initialProps = await Document.getInitialProps(ctx);
+      return {
+        ...initialProps,
+        styles: [initialProps.styles, sheet.getStyleElement()],
+      };
+    } finally {
+      sheet.seal();
+    }
+  }
+
+  render() {
+    return (
+      <Html lang="en">
+        <Head>
+          <SeoTags
+            description="Simple visualization tool for your JSON data. No forced structure, paste your JSON and view it instantly."
+            title="JSON Visio - Directly onto graphs"
+            image="https://jsonvisio.com/jsonvisio.png"
+          />
+          <meta name="theme-color" content="#36393E" />
+          <link rel="manifest" href="/manifest.json" />
+          <link rel="icon" href="/favicon.ico" />
+          <link rel="preconnect" href="https://fonts.googleapis.com" />
+          <link
+            rel="preconnect"
+            href="https://fonts.gstatic.com"
+            crossOrigin="anonymous"
+          />
+          <link
+            href="https://fonts.googleapis.com/css2?family=Catamaran:wght@400;500;700&family=Roboto+Mono:wght@500&family=Roboto:wght@400;500;700&display=swap"
+            rel="stylesheet"
+            crossOrigin="anonymous"
+          />
+        </Head>
+        <body>
+          <Main />
+          <NextScript />
+        </body>
+      </Html>
+    );
+  }
+}
+
+export default MyDocument;

+ 105 - 105
src/utils/json-editor-parser.ts

@@ -1,105 +1,105 @@
-/**
- * Copyright (C) 2022 Aykut Saraç - All Rights Reserved
- */
-import toast from "react-hot-toast";
-
-const filterChild = ([_, v]) => {
-  const isNull = v === null;
-  const isArray = Array.isArray(v) && v.length;
-  const isObject = v instanceof Object;
-
-  return !isNull && (isArray || isObject);
-};
-
-const filterValues = ([k, v]) => {
-  if (Array.isArray(v) || v instanceof Object) return false;
-
-  return true;
-};
-
-function generateChildren(object: Object, nextId: () => string) {
-  if (!(object instanceof Object)) object = [object];
-
-  return Object.entries(object)
-    .filter(filterChild)
-    .flatMap(([k, v]) => {
-      // const isObject = v instanceof Object && !Array.isArray(v);
-
-      // if (isObject) {
-      //   return [
-      //     {
-      //       id: nextId(),
-      //       text: k,
-      //       parent: true,
-      //       children: generateChildren(v, nextId),
-      //     },
-      //   ];
-      // }
-
-      return [
-        {
-          id: nextId(),
-          text: k,
-          parent: true,
-          children: extract(v, nextId),
-        },
-      ];
-    });
-}
-
-function generateNodeData(object: Object | number) {
-  const isObject = object instanceof Object;
-
-  if (isObject) {
-    const entries = Object.entries(object).filter(filterValues);
-    return Object.fromEntries(entries);
-  }
-
-  return String(object);
-}
-
-const extract = (
-  os: string[] | object[] | null,
-  nextId = (
-    (id) => () =>
-      String(++id)
-  )(0)
-) => {
-  if (!os) return [];
-
-  return [os].flat().map((o) => ({
-    id: nextId(),
-    text: generateNodeData(o),
-    children: generateChildren(o, nextId),
-    parent: false,
-  }));
-};
-
-const flatten = (xs: { id: string; children: never[] }[]) =>
-  xs.flatMap(({ children, ...rest }) => [rest, ...flatten(children)]);
-
-const relationships = (xs: { id: string; children: never[] }[]) => {
-  return xs.flatMap(({ id: from, children = [] }) => [
-    ...children.map(({ id: to }) => ({
-      id: `e${from}-${to}`,
-      from,
-      to,
-    })),
-    ...relationships(children),
-  ]);
-};
-
-export const parser = (input: string | string[]) => {
-  try {
-    if (!Array.isArray(input)) input = [input];
-
-    const mappedElements = extract(input);
-    const res = [...flatten(mappedElements), ...relationships(mappedElements)];
-
-    return res;
-  } catch (error) {
-    console.error(error);
-    toast.error("An error occured while parsing JSON data!");
-    return [];
-  }
-};
+/**
+ * Copyright (C) 2022 Aykut Saraç - All Rights Reserved
+ */
+import toast from "react-hot-toast";
+
+const filterChild = ([_, v]) => {
+  const isNull = v === null;
+  const isArray = Array.isArray(v) && v.length;
+  const isObject = v instanceof Object;
+
+  return !isNull && (isArray || isObject);
+};
+
+const filterValues = ([k, v]) => {
+  if (Array.isArray(v) || v instanceof Object) return false;
+
+  return true;
+};
+
+function generateChildren(object: Object, nextId: () => string) {
+  if (!(object instanceof Object)) object = [object];
+
+  return Object.entries(object)
+    .filter(filterChild)
+    .flatMap(([k, v]) => {
+      // const isObject = v instanceof Object && !Array.isArray(v);
+
+      // if (isObject) {
+      //   return [
+      //     {
+      //       id: nextId(),
+      //       text: k,
+      //       parent: true,
+      //       children: generateChildren(v, nextId),
+      //     },
+      //   ];
+      // }
+
+      return [
+        {
+          id: nextId(),
+          text: k,
+          parent: true,
+          children: extract(v, nextId),
+        },
+      ];
+    });
+}
+
+function generateNodeData(object: Object | number) {
+  const isObject = object instanceof Object;
+
+  if (isObject) {
+    const entries = Object.entries(object).filter(filterValues);
+    return Object.fromEntries(entries);
+  }
+
+  return String(object);
+}
+
+const extract = (
+  os: string[] | object[] | null,
+  nextId = (
+    (id) => () =>
+      String(++id)
+  )(0)
+) => {
+  if (!os) return [];
+
+  return [os].flat().map((o) => ({
+    id: nextId(),
+    text: generateNodeData(o),
+    children: generateChildren(o, nextId),
+    parent: false,
+  }));
+};
+
+const flatten = (xs: { id: string; children: never[] }[]) =>
+  xs.flatMap(({ children, ...rest }) => [rest, ...flatten(children)]);
+
+const relationships = (xs: { id: string; children: never[] }[]) => {
+  return xs.flatMap(({ id: from, children = [] }) => [
+    ...children.map(({ id: to }) => ({
+      id: `e${from}-${to}`,
+      from,
+      to,
+    })),
+    ...relationships(children),
+  ]);
+};
+
+export const parser = (input: string | string[]) => {
+  try {
+    if (!Array.isArray(input)) input = [input];
+
+    const mappedElements = extract(input);
+    const res = [...flatten(mappedElements), ...relationships(mappedElements)];
+
+    return res;
+  } catch (error) {
+    console.error(error);
+    toast.error("An error occured while parsing JSON data!");
+    return [];
+  }
+};