Browse Source

Merge branch 'main' into fix/align-object-child

AykutSarac 2 years ago
parent
commit
08afad8fe0
78 changed files with 2808 additions and 1462 deletions
  1. 3 1
      .env.development
  2. 3 1
      .env.production
  3. 1 1
      .prettierrc
  4. 18 32
      README.md
  5. 1 7
      next.config.js
  6. 23 16
      package.json
  7. 0 0
      public/assets/404.svg
  8. BIN
      public/assets/Mona-Sans.woff2
  9. BIN
      public/assets/icon.png
  10. BIN
      public/assets/jsoncrack-screenshot.webp
  11. BIN
      public/assets/jsoncrack.png
  12. 13 0
      src/api/altogic.ts
  13. 11 7
      src/components/Button/index.tsx
  14. 5 20
      src/components/CustomNode/ObjectNode.tsx
  15. 45 37
      src/components/CustomNode/TextNode.tsx
  16. 1 6
      src/components/CustomNode/index.tsx
  17. 2 8
      src/components/CustomNode/styles.tsx
  18. 4 1
      src/components/ErrorContainer/index.tsx
  19. 84 0
      src/components/Footer/index.tsx
  20. 4 9
      src/components/Graph/ErrorView.tsx
  21. 19 55
      src/components/Graph/index.tsx
  22. 14 11
      src/components/Loading/index.tsx
  23. 5 3
      src/components/Modal/index.tsx
  24. 8 4
      src/components/Modal/styles.tsx
  25. 55 42
      src/components/MonacoEditor/index.tsx
  26. 1 5
      src/components/SearchInput/index.tsx
  27. 115 124
      src/components/Sidebar/index.tsx
  28. 28 0
      src/components/Spinner/index.tsx
  29. 3 10
      src/components/Sponsors/index.tsx
  30. 4 9
      src/components/SupportButton/index.tsx
  31. 22 30
      src/components/Tooltip/index.tsx
  32. 27 12
      src/constants/globalStyle.ts
  33. 1 0
      src/constants/theme.ts
  34. 189 0
      src/containers/Editor/BottomBar.tsx
  35. 2 4
      src/containers/Editor/JsonEditor/index.tsx
  36. 12 17
      src/containers/Editor/LiveEditor/GraphCanvas.tsx
  37. 9 9
      src/containers/Editor/Panes.tsx
  38. 10 24
      src/containers/Editor/Tools.tsx
  39. 82 95
      src/containers/Home/index.tsx
  40. 37 50
      src/containers/Home/styles.tsx
  41. 51 0
      src/containers/ModalController/index.tsx
  42. 143 0
      src/containers/Modals/AccountModal/index.tsx
  43. 12 6
      src/containers/Modals/ClearModal/index.tsx
  44. 270 0
      src/containers/Modals/CloudModal/index.tsx
  45. 6 6
      src/containers/Modals/DownloadModal/index.tsx
  46. 0 76
      src/containers/Modals/GoalsModal/index.tsx
  47. 5 8
      src/containers/Modals/ImportModal/index.tsx
  48. 26 0
      src/containers/Modals/LoginModal/index.tsx
  49. 1 3
      src/containers/Modals/NodeModal/index.tsx
  50. 27 22
      src/containers/Modals/SettingsModal/index.tsx
  51. 24 63
      src/containers/Modals/ShareModal/index.tsx
  52. 128 0
      src/containers/PricingCards/index.tsx
  53. 11 15
      src/hooks/useFocusNode.tsx
  54. 33 0
      src/hooks/useHideNodes.tsx
  55. 0 35
      src/pages/Embed/index.tsx
  56. 0 135
      src/pages/Widget/index.tsx
  57. 22 33
      src/pages/_app.tsx
  58. 3 6
      src/pages/_document.tsx
  59. 1 1
      src/pages/_error.tsx
  60. 187 0
      src/pages/docs.tsx
  61. 24 5
      src/pages/editor.tsx
  62. 35 0
      src/pages/pricing.tsx
  63. 92 0
      src/pages/widget.tsx
  64. 35 0
      src/services/db/json.tsx
  65. 0 58
      src/store/useConfig.tsx
  66. 73 27
      src/store/useGraph.tsx
  67. 118 0
      src/store/useJson.tsx
  68. 38 0
      src/store/useModal.tsx
  69. 21 17
      src/store/useStored.tsx
  70. 59 0
      src/store/useUser.tsx
  71. 56 0
      src/typings/altogic.ts
  72. 0 6
      src/typings/types.d.ts
  73. 2 6
      src/utils/getChildrenEdges.ts
  74. 2 3
      src/utils/getOutgoers.ts
  75. 0 10
      src/utils/isValidJson.ts
  76. 45 50
      src/utils/jsonParser.ts
  77. 1 4
      src/utils/search.ts
  78. 396 217
      yarn.lock

+ 3 - 1
.env.development

@@ -1 +1,3 @@
-NEXT_PUBLIC_BASE_URL=http://localhost:3000
+NEXT_PUBLIC_BASE_URL=http://localhost:3000
+NEXT_PUBLIC_ALTOGIC_ENV_URL=https://jsoncrack.c5-na.altogic.com
+NEXT_PUBLIC_ALTOGIC_CLIENT_KEY=f1e92022789f4ccf91273a345ab2bdf8

+ 3 - 1
.env.production

@@ -1 +1,3 @@
-NEXT_PUBLIC_BASE_URL=https://jsoncrack.com
+NEXT_PUBLIC_BASE_URL=https://jsoncrack.com
+NEXT_PUBLIC_ALTOGIC_ENV_URL=https://jsoncrack.c5-na.altogic.com
+NEXT_PUBLIC_ALTOGIC_CLIENT_KEY=f1e92022789f4ccf91273a345ab2bdf8

+ 1 - 1
.prettierrc

@@ -2,7 +2,7 @@
   "trailingComma": "es5",
   "singleQuote": false,
   "semi": true,
-  "printWidth": 85,
+  "printWidth": 100,
   "arrowParens": "avoid",
   "importOrder": [
     "^(react/(.*)$)|^(react$)",

+ 18 - 32
README.md

@@ -1,33 +1,15 @@
-<center>
+<div align="center" style="display:flex;flex-direction:column;">
   <a href="https://jsoncrack.com">
-    <img width="1080" alt="jsoncrack" src="https://user-images.githubusercontent.com/47941171/187418000-8edea92b-b3ac-4b07-9c4c-e42f6763817d.png">
+    <img width="700" alt="jsoncrack" src="https://user-images.githubusercontent.com/47941171/206401172-74c21f7f-0a32-4532-96cc-4cf6b493c837.png">
   </a>
-</center>
-
-<p>
-    <p align="center">
-      <a href="https://discord.gg/yVyTtCRueq">
-        <img alt="github sponsors" src="https://dcbadge.vercel.app/api/server/yVyTtCRueq?style=flat-square" />
-      </a>
-      <a href="https://app.travis-ci.com/github/AykutSarac/jsoncrack.com">
-        <img alt="travis ci badge" src="https://img.shields.io/travis/com/AykutSarac/jsoncrack.com/main?style=flat-square" />
-      </a>
-      <a href="https://github.com/AykutSarac/jsoncrack.com/blob/main/LICENSE">
-        <img alt="license badge" src="https://img.shields.io/github/license/AykutSarac/jsoncrack.com?style=flat-square" />
-      </a>
-      <a href="https://github.com/AykutSarac/jsoncrack.com/releases">
-        <img alt="version badge" src="https://img.shields.io/github/package-json/v/AykutSarac/jsoncrack.com?color=brightgreen&style=flat-square" />
-      </a>
-      <a href="https://github.com/sponsors/AykutSarac">
-        <img alt="github sponsors" src="https://img.shields.io/github/sponsors/AykutSarac?style=flat-square" />
-      </a>
-  </p>
+  <h3>“Explore, analyze and understand even most complex JSON structures.”</br>Unlock the full potentiel of your data.</h3>
   <p align="center">
-    <i>Simple json visualization tool for your data.</i>
-    <p align="center">
-    <a href="https://www.producthunt.com/posts/json-crack?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-json&#0045;crack" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=332281&theme=light" alt="JSON&#0032;Crack - Simple&#0032;visualization&#0032;tool&#0032;for&#0032;your&#0032;JSON&#0032;data&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
-    </p>
+  <a href="https://www.producthunt.com/posts/json-crack?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-json&#0045;crack" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=332281&theme=light" alt="JSON&#0032;Crack - Simple&#0032;visualization&#0032;tool&#0032;for&#0032;your&#0032;JSON&#0032;data&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
+    <a href="https://discord.gg/yVyTtCRueq" target="_blank"><img src="https://user-images.githubusercontent.com/47941171/206397224-94da03a4-59d0-48cd-aafc-512624a768d6.png" "style=" height: 54px;" height="54" /></a>
+    </br>
+    <a href="https://github.com/sponsors/AykutSarac" target="_blank"><img src="https://user-images.githubusercontent.com/47941171/206397875-a4e73f02-5d8f-4db0-902b-9a4bc2b22d90.png" "style=" height: 54px;" height="54" /></a>
   </p>
+</div>
 
   <p align="center">
       <img width="800" src="./public/assets/jsoncrack-screenshot.webp" alt="preview 1" />
@@ -35,21 +17,25 @@
 
 # JSON Crack (jsoncrack.com)
 
-JSON Crack 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 [jsoncrack.com](https://jsoncrack.com) or also run it locally as [Docker container](https://github.com/AykutSarac/jsoncrack.com#-docker).
+Introducing JSON Crack – the open-source, free JSON visualization app that will revolutionize the way you work with data. With its intuitive and user-friendly interface, JSON Crack makes it easy to explore, analyze, and understand even the most complex JSON structures. Whether you're a developer working on a large-scale project or a data enthusiast looking to uncover hidden insights, JSON Crack has the tools and features you need to unlock the full potential of your data. Best of all, because JSON Crack is open-source and free, you can use it without breaking the bank. Try JSON Crack today and experience the power of data visualization like never before.
 
 > <b><a href="https://jsoncrack.com">JSON Crack - Crack your data into pieces</a></b>
 
-## ⚡️ Features
+## ⚡️ Key Features
 
 - Search Nodes
 - Share links & Create Embed Widgets
 - Download/Clipboard as image
 - Upload JSON locally or fetch from URL
-- Great UI/UX
+- User-friendly Interface
 - Light/Dark Mode
-- Advanced Error Messages
+
+## ⭐️ Embedding Into Your Website
+
+You can use the JSON Crack to visualize your JSON **at your products or websites**, see our very simple guide: https://jsoncrack.com/embed
+You can choose to **[partner us](https://github.com/sponsors/AykutSarac)** to remove attribute for your commercial products.
+
+<img width="291" alt="Screenshot_2022-12-08_at_11 46 02-removebg-preview" src="https://user-images.githubusercontent.com/47941171/206400503-150f60b6-f4b3-4649-854d-be4a7b826275.png">
 
 ## 🛠 Development Setup
 

+ 1 - 7
next.config.js

@@ -9,13 +9,7 @@ const withPWA = require("next-pwa")({
  * @type {import('next').NextConfig}
  */
 const nextConfig = {
-  reactStrictMode: true,
-  exportPathMap: async () => ({
-    "/": { page: "/" },
-    "/editor": { page: "/Editor" },
-    "/widget": { page: "/Widget" },
-    "/embed": { page: "/Embed" },
-  }),
+  reactStrictMode: false,
 };
 
 module.exports = withPWA(nextConfig);

+ 23 - 16
package.json

@@ -1,7 +1,7 @@
 {
   "name": "json-crack",
   "private": true,
-  "version": "v2.2.0",
+  "version": "v2.5.0",
   "author": "https://github.com/AykutSarac",
   "homepage": "https://jsoncrack.com",
   "scripts": {
@@ -14,39 +14,46 @@
   },
   "dependencies": {
     "@monaco-editor/react": "^4.4.6",
-    "@sentry/nextjs": "^7.16.0",
-    "allotment": "^1.17.0",
-    "compress-json": "^2.1.2",
-    "html-to-image": "^1.10.8",
+    "@sentry/nextjs": "^7.28.1",
+    "@tanstack/react-query": "^4.20.4",
+    "allotment": "^1.17.1",
+    "altogic": "^2.3.9",
+    "axios": "^1.2.2",
+    "dayjs": "^1.11.7",
+    "html-to-image": "^1.11.3",
     "jsonc-parser": "^3.2.0",
+    "lodash.debounce": "^4.0.8",
+    "lz-string": "^1.4.4",
     "next": "^12.3.1",
-    "next-transpile-modules": "^9.1.0",
     "react": "^18.2.0",
     "react-color": "^2.19.3",
     "react-dom": "^18.2.0",
     "react-hot-toast": "^2.4.0",
-    "react-icons": "^4.6.0",
+    "react-icons": "^4.7.1",
     "react-in-viewport": "^1.0.0-alpha.28",
     "react-linkify-it": "^1.0.7",
+    "react-syntax-highlighter": "^15.5.0",
     "react-zoom-pan-pinch": "^2.1.3",
-    "reaflow": "^5.0.7",
+    "reaflow": "^5.1.1",
     "styled-components": "^5.3.6",
-    "zustand": "^4.1.3"
+    "zustand": "^4.1.5"
   },
   "devDependencies": {
     "@testing-library/react": "^13.3.0",
-    "@trivago/prettier-plugin-sort-imports": "^3.3.0",
-    "@types/node": "^18.7.21",
-    "@types/react": "18.0.21",
+    "@trivago/prettier-plugin-sort-imports": "^4.0.0",
+    "@types/lodash.debounce": "^4.0.7",
+    "@types/lz-string": "^1.3.34",
+    "@types/node": "^18.11.18",
+    "@types/react": "18.0.26",
     "@types/react-color": "^3.0.6",
+    "@types/react-syntax-highlighter": "^15.5.5",
     "@types/styled-components": "^5.1.26",
-    "eslint": "8.24.0",
+    "eslint": "8.31.0",
     "eslint-config-next": "12.3.1",
-    "eslint-plugin-testing-library": "^5.7.0",
     "eslint-plugin-unused-imports": "^2.0.0",
     "next-pwa": "5.6.0",
-    "prettier": "^2.7.1",
+    "prettier": "^2.8.1",
     "ts-node": "^10.9.1",
-    "typescript": "4.8.3"
+    "typescript": "4.9.4"
   }
 }

File diff suppressed because it is too large
+ 0 - 0
public/assets/404.svg


BIN
public/assets/Mona-Sans.woff2


BIN
public/assets/icon.png


BIN
public/assets/jsoncrack-screenshot.webp


BIN
public/assets/jsoncrack.png


+ 13 - 0
src/api/altogic.ts

@@ -0,0 +1,13 @@
+import { APIError, createClient } from "altogic";
+
+let envUrl = process.env.NEXT_PUBLIC_ALTOGIC_ENV_URL as string;
+let clientKey = process.env.NEXT_PUBLIC_ALTOGIC_CLIENT_KEY as string;
+
+const altogic = createClient(envUrl, clientKey);
+
+export interface AltogicResponse<T> {
+  data: T;
+  errors: APIError | null;
+}
+
+export { altogic };

+ 11 - 7
src/components/Button/index.tsx

@@ -4,6 +4,7 @@ import styled, { DefaultTheme } from "styled-components";
 enum ButtonType {
   PRIMARY = "PRIMARY",
   SECONDARY = "BLURPLE",
+  TERTIARY = "PURPLE",
   DANGER = "DANGER",
   SUCCESS = "SEAGREEN",
   WARNING = "ORANGE",
@@ -16,7 +17,7 @@ interface ButtonProps {
 
 type ConditionalProps =
   | ({
-      link?: boolean;
+      link: boolean;
     } & React.ComponentPropsWithoutRef<"a">)
   | ({
       link?: never;
@@ -29,19 +30,20 @@ function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
 const StyledButton = styled.button<{
   status: keyof typeof ButtonType;
   block: boolean;
+  link: boolean;
 }>`
-  display: flex;
+  display: inline-flex;
   align-items: center;
   background: ${({ status, theme }) => getButtonStatus(status, theme)};
   color: #ffffff;
-  padding: 8px 16px;
-  min-width: 60px;
+  padding: ${({ link }) => (link ? "2px 16px" : "8px 16px")};
+  min-width: 70px;
   min-height: 32px;
   border-radius: 3px;
+  font-family: "Mona Sans";
   font-size: 14px;
   font-weight: 500;
-  font-family: "Catamaran", sans-serif;
-  width: ${({ block }) => (block ? "100%" : "fit-content")};
+  width: ${({ block }) => (block ? "-webkit-fill-available" : "fit-content")};
   height: 40px;
   background-image: none;
 
@@ -73,6 +75,7 @@ const StyledButtonContent = styled.div`
   gap: 8px;
   white-space: nowrap;
   text-overflow: ellipsis;
+  font-weight: 600;
 `;
 
 export const Button: React.FC<ButtonProps & ConditionalProps> = ({
@@ -84,10 +87,11 @@ export const Button: React.FC<ButtonProps & ConditionalProps> = ({
 }) => {
   return (
     <StyledButton
-      as={link ? "a" : "button"}
       type="button"
+      as={link ? "a" : "button"}
       status={status ?? "PRIMARY"}
       block={block}
+      link={link}
       {...props}
     >
       <StyledButtonContent>{children}</StyledButtonContent>

+ 5 - 20
src/components/CustomNode/ObjectNode.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 // import { useInViewport } from "react-in-viewport";
 import { CustomNodeProps } from "src/components/CustomNode";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import * as Styled from "./styles";
 
 const inViewport = true;
@@ -9,28 +9,16 @@ const inViewport = true;
 const ObjectNode: React.FC<CustomNodeProps> = ({ node, x, y }) => {
   const { text, width, height, data } = node;
   const ref = React.useRef(null);
-  const performanceMode = useConfig(state => state.performanceMode);
+  const performanceMode = useGraph(state => state.performanceMode);
   // const { inViewport } = useInViewport(ref);
 
   if (data.isEmpty) return null;
 
   return (
-    <Styled.StyledForeignObject
-      width={width}
-      height={height}
-      x={0}
-      y={0}
-      ref={ref}
-      isObject
-    >
+    <Styled.StyledForeignObject width={width} height={height} x={0} y={0} ref={ref} isObject>
       {(!performanceMode || inViewport) &&
         text.map((val, idx) => (
-          <Styled.StyledRow
-            data-key={JSON.stringify(val[1])}
-            data-x={x}
-            data-y={y}
-            key={idx}
-          >
+          <Styled.StyledRow data-key={JSON.stringify(val[1])} data-x={x} data-y={y} key={idx}>
             <Styled.StyledKey objectKey>
               {JSON.stringify(val[0]).replaceAll('"', "")}:{" "}
             </Styled.StyledKey>
@@ -42,10 +30,7 @@ const ObjectNode: React.FC<CustomNodeProps> = ({ node, x, y }) => {
 };
 
 function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) {
-  return (
-    String(prev.node.text) === String(next.node.text) &&
-    prev.node.width === next.node.width
-  );
+  return String(prev.node.text) === String(next.node.text) && prev.node.width === next.node.width;
 }
 
 export default React.memo(ObjectNode, propsAreEqual);

+ 45 - 37
src/components/CustomNode/TextNode.tsx

@@ -2,7 +2,6 @@ import React from "react";
 import { MdLink, MdLinkOff } from "react-icons/md";
 // import { useInViewport } from "react-in-viewport";
 import { CustomNodeProps } from "src/components/CustomNode";
-import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
@@ -28,27 +27,34 @@ const StyledExpand = styled.button`
 
 const StyledTextNodeWrapper = styled.div<{ hasCollapse: boolean }>`
   display: flex;
-  justify-content: ${({ hasCollapse }) =>
-    hasCollapse ? "space-between" : "center"};
+  justify-content: ${({ hasCollapse }) => (hasCollapse ? "space-between" : "center")};
   align-items: center;
   height: 100%;
   width: 100%;
 `;
 
-const TextNode: React.FC<CustomNodeProps> = ({
-  node,
-  x,
-  y,
-  hasCollapse = false,
-}) => {
+const StyledImageWrapper = styled.div`
+  padding: 5px;
+`;
+
+const StyledImage = styled.img`
+  border-radius: 2px;
+  object-fit: contain;
+  background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+`;
+
+const TextNode: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) => {
   const { id, text, width, height, data } = node;
   const ref = React.useRef(null);
   const hideCollapse = useStored(state => state.hideCollapse);
-  const hideChildrenCount = useStored(state => state.hideChildrenCount);
+  const childrenCount = useStored(state => state.childrenCount);
+  const imagePreview = useStored(state => state.imagePreview);
   const expandNodes = useGraph(state => state.expandNodes);
   const collapseNodes = useGraph(state => state.collapseNodes);
   const isExpanded = useGraph(state => state.collapsedParents.includes(id));
-  const performanceMode = useConfig(state => state.performanceMode);
+  const performanceMode = useGraph(state => state.performanceMode);
+  const isImage =
+    !Array.isArray(text) && /(https?:\/\/.*\.(?:png|jpg|gif))/i.test(text) && imagePreview;
   // const { inViewport } = useInViewport(ref);
 
   const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -64,36 +70,38 @@ const TextNode: React.FC<CustomNodeProps> = ({
       height={height}
       x={0}
       y={0}
-      hideCollapse={hideCollapse}
       hasCollapse={data.parent && hasCollapse}
       ref={ref}
     >
-      <StyledTextNodeWrapper hasCollapse={data.parent && !hideCollapse}>
-        {(!performanceMode || inViewport) && (
-          <Styled.StyledKey
-            data-x={x}
-            data-y={y}
-            data-key={JSON.stringify(text)}
-            parent={data.parent}
-          >
-            <Styled.StyledLinkItUrl>
-              {JSON.stringify(text).replaceAll('"', "")}
-            </Styled.StyledLinkItUrl>
-          </Styled.StyledKey>
-        )}
-
-        {data.parent && data.childrenCount > 0 && !hideChildrenCount && (
-          <Styled.StyledChildrenCount>
-            ({data.childrenCount})
-          </Styled.StyledChildrenCount>
-        )}
+      {isImage ? (
+        <StyledImageWrapper>
+          <StyledImage src={text} width="70" height="70" />
+        </StyledImageWrapper>
+      ) : (
+        <StyledTextNodeWrapper hasCollapse={data.parent && hideCollapse}>
+          {(!performanceMode || inViewport) && (
+            <Styled.StyledKey
+              data-x={x}
+              data-y={y}
+              data-key={JSON.stringify(text)}
+              parent={data.parent}
+            >
+              <Styled.StyledLinkItUrl>
+                {JSON.stringify(text).replaceAll('"', "")}
+              </Styled.StyledLinkItUrl>
+            </Styled.StyledKey>
+          )}
+          {data.parent && data.childrenCount > 0 && childrenCount && (
+            <Styled.StyledChildrenCount>({data.childrenCount})</Styled.StyledChildrenCount>
+          )}
 
-        {inViewport && data.parent && hasCollapse && !hideCollapse && (
-          <StyledExpand onClick={handleExpand}>
-            {isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
-          </StyledExpand>
-        )}
-      </StyledTextNodeWrapper>
+          {inViewport && data.parent && hasCollapse && hideCollapse && (
+            <StyledExpand onClick={handleExpand}>
+              {isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
+            </StyledExpand>
+          )}
+        </StyledTextNodeWrapper>
+      )}
     </Styled.StyledForeignObject>
   );
 };

+ 1 - 6
src/components/CustomNode/index.tsx

@@ -28,12 +28,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
         }
 
         return (
-          <TextNode
-            node={node as NodeData}
-            hasCollapse={data.childrenCount > 0}
-            x={x}
-            y={y}
-          />
+          <TextNode node={node as NodeData} hasCollapse={data.childrenCount > 0} x={x} y={y} />
         );
       }}
     </Node>

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

@@ -16,7 +16,6 @@ export const StyledLinkItUrl = styled(LinkItUrl)`
 
 export const StyledForeignObject = styled.foreignObject<{
   hasCollapse?: boolean;
-  hideCollapse?: boolean;
   isObject?: boolean;
 }>`
   text-align: ${({ isObject }) => !isObject && "center"};
@@ -53,11 +52,7 @@ export const StyledForeignObject = styled.foreignObject<{
   }
 `;
 
-function getKeyColor(
-  theme: DefaultTheme,
-  parent: "array" | "object" | false,
-  objectKey: boolean
-) {
+function getKeyColor(theme: DefaultTheme, parent: "array" | "object" | false, objectKey: boolean) {
   if (parent) {
     if (parent === "array") return theme.NODE_COLORS.PARENT_ARR;
     return theme.NODE_COLORS.PARENT_OBJ;
@@ -74,8 +69,7 @@ export const StyledKey = styled.span<{
   display: inline;
   flex: 1;
   font-weight: 500;
-  color: ${({ theme, objectKey = false, parent = false }) =>
-    getKeyColor(theme, parent, objectKey)};
+  color: ${({ theme, objectKey = false, parent = false }) => getKeyColor(theme, parent, objectKey)};
   font-size: ${({ parent }) => parent && "14px"};
   overflow: hidden;
   text-overflow: ellipsis;

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

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

+ 84 - 0
src/components/Footer/index.tsx

@@ -0,0 +1,84 @@
+import Link from "next/link";
+import { FaGithub, FaLinkedin, FaTwitter } from "react-icons/fa";
+import styled from "styled-components";
+import pkg from "../../../package.json";
+
+export const StyledFooter = styled.footer`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  width: 80%;
+  margin: 0 auto;
+  padding: 30px 3%;
+  border-top: 1px solid #b4b4b4;
+  opacity: 0.7;
+`;
+
+export const StyledFooterText = styled.p`
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  color: #b4b4b4;
+`;
+
+export const StyledNavLink = styled.a`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 1rem;
+  cursor: pointer;
+  transition: color 0.2s;
+
+  &:hover {
+    font-weight: 500;
+    color: ${({ theme }) => theme.ORANGE};
+  }
+`;
+
+export const StyledIconLinks = styled.div`
+  display: flex;
+  gap: 20px;
+`;
+
+export const Footer = () => (
+  <StyledFooter>
+    <StyledFooterText>
+      <Link href="/">
+        <a>
+          <img width="120" src="assets/icon.png" alt="icon" loading="lazy" />
+        </a>
+      </Link>
+      <span>
+        © {new Date().getFullYear()} JSON Crack - {pkg.version}
+      </span>
+    </StyledFooterText>
+    <StyledIconLinks>
+      <StyledNavLink
+        href="https://github.com/AykutSarac/jsoncrack.com"
+        rel="external"
+        target="_blank"
+        aria-label="github"
+      >
+        <FaGithub size={26} />
+      </StyledNavLink>
+
+      <StyledNavLink
+        href="https://www.linkedin.com/in/aykutsarac/"
+        rel="me"
+        target="_blank"
+        aria-label="linkedin"
+      >
+        <FaLinkedin size={26} />
+      </StyledNavLink>
+
+      <StyledNavLink
+        href="https://twitter.com/jsoncrack"
+        rel="me"
+        target="_blank"
+        aria-label="twitter"
+      >
+        <FaTwitter size={26} />
+      </StyledNavLink>
+    </StyledIconLinks>
+  </StyledFooter>
+);

+ 4 - 9
src/components/Graph/ErrorView.tsx

@@ -26,17 +26,12 @@ const StyledInfo = styled.p`
 
 export const ErrorView = () => (
   <StyledErrorView>
-    <img
-      src="/assets/undraw_qa_engineers_dg-5-p.svg"
-      width="200"
-      height="200"
-      alt="oops"
-    />
+    <img src="/assets/undraw_qa_engineers_dg-5-p.svg" width="200" height="200" alt="oops" />
     <StyledTitle>JSON Crack is unable to handle this file!</StyledTitle>
     <StyledInfo>
-      We apologize for the problem you encountered. We are doing our best as an Open
-      Source community to improve our service. Unfortunately, JSON Crack is currently
-      unable to handle such a large file.
+      We apologize for the problem you encountered. We are doing our best as an Open Source
+      community to improve our service. Unfortunately, JSON Crack is currently unable to handle such
+      a large file.
     </StyledInfo>
   </StyledErrorView>
 );

+ 19 - 55
src/components/Graph/index.tsx

@@ -1,12 +1,7 @@
 import React from "react";
-import {
-  ReactZoomPanPinchRef,
-  TransformComponent,
-  TransformWrapper,
-} from "react-zoom-pan-pinch";
+import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
 import { Canvas, Edge, ElkRoot } from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
-import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
 import styled from "styled-components";
 import { Loading } from "../Loading";
@@ -41,14 +36,10 @@ const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
   }
 `;
 
-const GraphComponent = ({
-  isWidget = false,
-  openModal,
-  setSelectedNode,
-}: GraphProps) => {
+const GraphComponent = ({ isWidget = false, openModal, setSelectedNode }: GraphProps) => {
   const setLoading = useGraph(state => state.setLoading);
-  const setConfig = useConfig(state => state.setConfig);
-  const centerView = useConfig(state => state.centerView);
+  const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
+  const centerView = useGraph(state => state.centerView);
 
   const loading = useGraph(state => state.loading);
   const direction = useGraph(state => state.direction);
@@ -70,58 +61,35 @@ const GraphComponent = ({
 
   const onInit = React.useCallback(
     (ref: ReactZoomPanPinchRef) => {
-      setConfig("zoomPanPinch", ref);
+      setZoomPanPinch(ref);
     },
-    [setConfig]
+    [setZoomPanPinch]
   );
 
   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 changeRatio = Math.abs((areaSize * 100) / (size.width * size.height) - 100);
 
-        setSize({ width: layout.width + 400, height: layout.height + 400 });
+        setSize({
+          width: (layout.width as number) + 400,
+          height: (layout.height as number) + 400,
+        });
 
         requestAnimationFrame(() => {
           setTimeout(() => {
             setLoading(false);
-            setTimeout(() => (changeRatio > 75 || isWidget) && centerView(), 0);
-          }, 0);
+            setTimeout(() => {
+              if (changeRatio > 70 || isWidget) centerView();
+            });
+          });
         });
       }
     },
-    [size.width, size.height, setLoading, isWidget, centerView]
+    [centerView, isWidget, setLoading, size.height, size.width]
   );
 
-  // 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 input = document.querySelector("input:focus") as HTMLInputElement;
     if (input) input.blur();
@@ -131,7 +99,7 @@ const GraphComponent = ({
 
   return (
     <StyledEditorWrapper isWidget={isWidget} onContextMenu={e => e.preventDefault()}>
-      {loading && <Loading message="Painting graph..." />}
+      <Loading message="Painting graph..." loading={loading} />
       <TransformWrapper
         maxScale={2}
         minScale={0.05}
@@ -141,9 +109,7 @@ const GraphComponent = ({
         doubleClick={{ disabled: true }}
         onInit={onInit}
         onPanning={ref => ref.instance.wrapperComponent?.classList.add("dragging")}
-        onPanningStop={ref =>
-          ref.instance.wrapperComponent?.classList.remove("dragging")
-        }
+        onPanningStop={ref => ref.instance.wrapperComponent?.classList.remove("dragging")}
       >
         <TransformComponent
           wrapperStyle={{
@@ -170,9 +136,7 @@ const GraphComponent = ({
             fit={true}
             key={direction}
             node={props => <CustomNode {...props} onClick={handleNodeClick} />}
-            edge={props => (
-              <Edge {...props} containerClassName={`edge-${props.id}`} />
-            )}
+            edge={props => <Edge {...props} containerClassName={`edge-${props.id}`} />}
           />
         </TransformComponent>
       </TransformWrapper>

+ 14 - 11
src/components/Loading/index.tsx

@@ -2,6 +2,7 @@ import React from "react";
 import styled, { keyframes } from "styled-components";
 
 interface LoadingProps {
+  loading?: boolean;
   message?: string;
 }
 
@@ -32,7 +33,7 @@ const StyledLoading = styled.div`
 `;
 
 const StyledLogo = styled.h2`
-  font-weight: 600;
+  font-weight: 800;
   font-size: 56px;
   pointer-events: none;
   margin-bottom: 10px;
@@ -48,13 +49,15 @@ const StyledMessage = styled.div`
   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> = ({ loading = true, message }) => {
+  if (!loading) return null;
+
+  return (
+    <StyledLoading>
+      <StyledLogo>
+        <StyledText>JSON</StyledText> Crack
+      </StyledLogo>
+      <StyledMessage>{message ?? "Preparing the environment for you..."}</StyledMessage>
+    </StyledLoading>
+  );
+};

+ 5 - 3
src/components/Modal/index.tsx

@@ -17,7 +17,8 @@ type ModalTypes = {
 
 export interface ModalProps {
   visible: boolean;
-  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
+  setVisible: React.Dispatch<React.SetStateAction<boolean>> | ((visible: boolean) => void);
+  size?: "sm" | "md" | "lg";
 }
 
 const Header: ReactComponent = ({ children }) => {
@@ -51,10 +52,11 @@ const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
   children,
   visible,
   setVisible,
+  size = "sm",
 }) => {
   const onClick = (e: React.SyntheticEvent<HTMLDivElement>) => {
     if (e.currentTarget === e.target) {
-      setVisible(v => !v);
+      setVisible(false);
     }
   };
 
@@ -62,7 +64,7 @@ const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
 
   return (
     <Styled.ModalWrapper onClick={onClick}>
-      <Styled.ModalInnerWrapper>{children}</Styled.ModalInnerWrapper>
+      <Styled.ModalInnerWrapper size={size}>{children}</Styled.ModalInnerWrapper>
     </Styled.ModalWrapper>
   );
 };

+ 8 - 4
src/components/Modal/styles.tsx

@@ -22,9 +22,9 @@ export const ModalWrapper = styled.div`
   }
 `;
 
-export const ModalInnerWrapper = styled.div`
+export const ModalInnerWrapper = styled.div<{ size: "sm" | "md" | "lg" }>`
   min-width: 440px;
-  max-width: 490px;
+  max-width: ${({ size }) => (size === "sm" ? "490px" : size === "md" ? "50%" : "90%")};
   width: fit-content;
   animation: ${appearAnimation} 220ms ease-in-out;
   line-height: 20px;
@@ -36,9 +36,12 @@ export const ModalInnerWrapper = styled.div`
 `;
 
 export const Title = styled.h2`
+  display: flex;
+  align-items: center;
+  gap: 5px;
   color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
   font-size: 20px !important;
-  margin: 0;
+  margin: 0 !important;
 `;
 
 export const HeaderWrapper = styled.div`
@@ -52,13 +55,14 @@ export const ContentWrapper = styled.div`
   background: ${({ theme }) => theme.MODAL_BACKGROUND};
   padding: 16px;
   overflow: hidden auto;
+  max-height: 500px;
 `;
 
 export const ControlsWrapper = styled.div`
   display: flex;
   flex-direction: row-reverse;
   background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
-  padding: 16px;
+  padding: 12px;
   border-radius: 0 0 5px 5px;
   gap: 10px;
 `;

+ 55 - 42
src/components/MonacoEditor/index.tsx

@@ -1,11 +1,9 @@
 import React from "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 useConfig from "src/store/useConfig";
-import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
 import useStored from "src/store/useStored";
-import { parser } from "src/utils/jsonParser";
 import styled from "styled-components";
 
 loader.config({
@@ -28,63 +26,78 @@ const StyledWrapper = styled.div`
   grid-template-rows: minmax(0, 1fr);
 `;
 
-function handleEditorWillMount(monaco: Monaco) {
-  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
-    allowComments: true,
-    comments: "ignore",
-  });
-}
+export const MonacoEditor = () => {
+  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);
 
-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 = useJson(state => state.hasError);
+  const getHasChanges = useJson(state => state.getHasChanges);
   const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
 
+  const handleEditorWillMount = React.useCallback(
+    (monaco: Monaco) => {
+      monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
+        allowComments: true,
+        comments: "ignore",
+      });
+
+      monaco.editor.onDidChangeMarkers(([uri]) => {
+        const markers = monaco.editor.getModelMarkers({ resource: uri });
+        setError(!!markers.length);
+      });
+    },
+    [setError]
+  );
+
+  const debouncedSetJson = React.useMemo(
+    () =>
+      debounce(value => {
+        if (hasError) return;
+        setJson(value || "[]");
+      }, 1200),
+    [hasError, setJson]
+  );
+
   React.useEffect(() => {
-    const { nodes, edges } = parser(json, foldNodes);
+    if ((value || !hasError) && loaded) debouncedSetJson(value);
+    setLoaded(true);
 
-    setGraphValue("nodes", nodes);
-    setGraphValue("edges", edges);
-    setValue(json);
-  }, [foldNodes, json, setGraphValue]);
+    return () => debouncedSetJson.cancel();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [debouncedSetJson, hasError, value]);
 
   React.useEffect(() => {
-    const formatTimer = setTimeout(() => {
-      if (!value) {
-        setHasError(false);
-        return setJson("{}");
-      }
+    const beforeunload = (e: BeforeUnloadEvent) => {
+      if (getHasChanges()) {
+        const confirmationMessage =
+          "Unsaved changes, if you leave before saving  your changes will be lost";
 
-      const errors = [];
-      const parsedJSON = JSON.stringify(parse(value, errors), null, 2);
-      if (errors.length) return setHasError(true);
+        (e || window.event).returnValue = confirmationMessage; //Gecko + IE
+        return confirmationMessage;
+      }
+    };
 
-      setJson(parsedJSON);
-      setHasError(false);
-    }, 1200);
+    window.addEventListener("beforeunload", beforeunload);
 
-    return () => clearTimeout(formatTimer);
-  }, [value, setJson, setHasError]);
+    return () => {
+      window.removeEventListener("beforeunload", beforeunload);
+    };
+  }, [getHasChanges]);
 
   return (
     <StyledWrapper>
       <Editor
-        height="100%"
-        defaultLanguage="json"
-        value={value}
+        value={json}
         theme={lightmode}
         options={editorOptions}
         onChange={setValue}
         loading={<Loading message="Loading Editor..." />}
         beforeMount={handleEditorWillMount}
+        defaultLanguage="json"
+        height="100%"
       />
     </StyledWrapper>
   );

+ 1 - 5
src/components/SearchInput/index.tsx

@@ -76,11 +76,7 @@ export const SearchInput: React.FC = () => {
           placeholder="Search Node"
         />
         <StyledSearchButton type="reset" aria-label="search" onClick={handleClear}>
-          {content.value ? (
-            <IoCloseSharp size={18} />
-          ) : (
-            <AiOutlineSearch size={18} />
-          )}
+          {content.value ? <IoCloseSharp size={18} /> : <AiOutlineSearch size={18} />}
         </StyledSearchButton>
       </StyledForm>
     </StyledInputWrapper>

+ 115 - 124
src/components/Sidebar/index.tsx

@@ -1,31 +1,23 @@
 import React from "react";
-import Link from "next/link";
 import toast from "react-hot-toast";
-import {
-  AiOutlineDelete,
-  AiFillGithub,
-  AiOutlineTwitter,
-  AiOutlineSave,
-  AiOutlineFileAdd,
-  AiOutlineLink,
-  AiOutlineEdit,
-} from "react-icons/ai";
+import { AiOutlineDelete, AiOutlineSave, AiOutlineFileAdd, AiOutlineEdit } from "react-icons/ai";
 import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
 import { FiDownload } from "react-icons/fi";
-import { HiHeart } from "react-icons/hi";
 import { MdCenterFocusWeak } from "react-icons/md";
 import { TiFlowMerge } from "react-icons/ti";
-import { VscCollapseAll, VscExpandAll } from "react-icons/vsc";
+import {
+  VscAccount,
+  VscCloud,
+  VscCollapseAll,
+  VscExpandAll,
+  VscSettingsGear,
+} from "react-icons/vsc";
 import { Tooltip } from "src/components/Tooltip";
-import { ClearModal } from "src/containers/Modals/ClearModal";
-import { DownloadModal } from "src/containers/Modals/DownloadModal";
-import { ImportModal } from "src/containers/Modals/ImportModal";
-import { ShareModal } from "src/containers/Modals/ShareModal";
-import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
+import useModal from "src/store/useModal";
 import { getNextDirection } from "src/utils/getNextDirection";
 import styled from "styled-components";
-import shallow from "zustand/shallow";
 
 const StyledSidebar = styled.div`
   display: flex;
@@ -48,7 +40,7 @@ const StyledElement = styled.button`
   display: flex;
   justify-content: center;
   text-align: center;
-  font-size: 26px;
+  font-size: 24px;
   font-weight: 600;
   width: fit-content;
   color: ${({ theme }) => theme.SIDEBAR_ICONS};
@@ -78,8 +70,7 @@ const StyledElement = styled.button`
 `;
 
 const StyledText = styled.span<{ secondary?: boolean }>`
-  color: ${({ theme, secondary }) =>
-    secondary ? theme.INTERACTIVE_HOVER : theme.ORANGE};
+  color: ${({ theme, secondary }) => (secondary ? theme.INTERACTIVE_HOVER : theme.ORANGE)};
 `;
 
 const StyledFlowIcon = styled(TiFlowMerge)<{ rotate: number }>`
@@ -140,28 +131,34 @@ function rotateLayout(direction: "LEFT" | "RIGHT" | "DOWN" | "UP") {
   return 360;
 }
 
-export const Sidebar: React.FC = () => {
-  const [uploadVisible, setUploadVisible] = React.useState(false);
-  const [clearVisible, setClearVisible] = React.useState(false);
-  const [shareVisible, setShareVisible] = React.useState(false);
-  const [isDownloadVisible, setDownloadVisible] = React.useState(false);
+const SidebarButton: React.FC<{
+  onClick: () => void;
+  deviceDisplay?: "desktop" | "mobile";
+  title: string;
+  component: React.ReactNode;
+}> = ({ onClick, deviceDisplay, title, component }) => {
+  return (
+    <Tooltip className={deviceDisplay} title={title}>
+      <StyledElement onClick={onClick}>{component}</StyledElement>
+    </Tooltip>
+  );
+};
 
-  const getJson = useConfig(state => state.getJson);
+export const Sidebar: React.FC = () => {
+  const setVisible = useModal(state => state.setVisible);
   const setDirection = useGraph(state => state.setDirection);
-  const setConfig = useConfig(state => state.setConfig);
-  const centerView = useConfig(state => state.centerView);
+  const getJson = useJson(state => state.getJson);
+
   const collapseGraph = useGraph(state => state.collapseGraph);
   const expandGraph = useGraph(state => state.expandGraph);
+  const centerView = useGraph(state => state.centerView);
+  const toggleFold = useGraph(state => state.toggleFold);
+  const toggleFullscreen = useGraph(state => state.toggleFullscreen);
 
-  const [graphCollapsed, direction] = useGraph(state => [
-    state.graphCollapsed,
-    state.direction,
-  ]);
-
-  const [foldNodes, hideEditor] = useConfig(
-    state => [state.foldNodes, state.hideEditor],
-    shallow
-  );
+  const direction = useGraph(state => state.direction);
+  const foldNodes = useGraph(state => state.foldNodes);
+  const fullscreen = useGraph(state => state.fullscreen);
+  const graphCollapsed = useGraph(state => state.graphCollapsed);
 
   const handleSave = () => {
     const a = document.createElement("a");
@@ -173,7 +170,7 @@ export const Sidebar: React.FC = () => {
   };
 
   const toggleFoldNodes = () => {
-    setConfig("foldNodes", !foldNodes);
+    toggleFold(!foldNodes);
     toast(`${foldNodes ? "Unfolded" : "Folded"} nodes`);
   };
 
@@ -192,96 +189,90 @@ export const Sidebar: React.FC = () => {
   return (
     <StyledSidebar>
       <StyledTopWrapper>
-        <Link passHref href="/">
-          <StyledElement as={StyledLogo}>
-            <StyledText>J</StyledText>
-            <StyledText secondary>C</StyledText>
-          </StyledElement>
-        </Link>
-        <Tooltip className="mobile" title="Edit JSON">
-          <StyledElement onClick={() => setConfig("hideEditor", !hideEditor)}>
-            <AiOutlineEdit />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Import File">
-          <StyledElement onClick={() => setUploadVisible(true)}>
-            <AiOutlineFileAdd />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Rotate Layout">
-          <StyledElement onClick={toggleDirection}>
-            <StyledFlowIcon rotate={rotateLayout(direction)} />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip className="mobile" title="Center View">
-          <StyledElement onClick={centerView}>
-            <MdCenterFocusWeak />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip
-          className="desktop"
+        <StyledElement href="/" as={StyledLogo}>
+          <StyledText>J</StyledText>
+          <StyledText secondary>C</StyledText>
+        </StyledElement>
+
+        <SidebarButton
+          title="Edit JSON"
+          deviceDisplay="mobile"
+          onClick={() => toggleFullscreen(!fullscreen)}
+          component={<AiOutlineEdit />}
+        />
+
+        <SidebarButton
+          title="Import File"
+          onClick={() => setVisible("import")(true)}
+          component={<AiOutlineFileAdd />}
+        />
+
+        <SidebarButton
+          title="Rotate Layout"
+          onClick={toggleDirection}
+          component={<StyledFlowIcon rotate={rotateLayout(direction)} />}
+        />
+
+        <SidebarButton
+          title="Center View"
+          deviceDisplay="mobile"
+          onClick={centerView}
+          component={<MdCenterFocusWeak />}
+        />
+
+        <SidebarButton
           title={foldNodes ? "Unfold Nodes" : "Fold Nodes"}
-        >
-          <StyledElement onClick={toggleFoldNodes}>
-            {foldNodes ? <CgArrowsShrinkH /> : <CgArrowsMergeAltH />}
-          </StyledElement>
-        </Tooltip>
-        <Tooltip
-          className="desktop"
+          deviceDisplay="desktop"
+          onClick={toggleFoldNodes}
+          component={foldNodes ? <CgArrowsShrinkH /> : <CgArrowsMergeAltH />}
+        />
+
+        <SidebarButton
           title={graphCollapsed ? "Expand Graph" : "Collapse Graph"}
-        >
-          <StyledElement onClick={toggleExpandCollapseGraph}>
-            {graphCollapsed ? <VscExpandAll /> : <VscCollapseAll />}
-          </StyledElement>
-        </Tooltip>
-        <Tooltip className="desktop" title="Save JSON">
-          <StyledElement onClick={handleSave}>
-            <AiOutlineSave />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip className="mobile" title="Download Image">
-          <StyledElement onClick={() => setDownloadVisible(true)}>
-            <FiDownload />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip title="Clear JSON">
-          <StyledElement onClick={() => setClearVisible(true)}>
-            <AiOutlineDelete />
-          </StyledElement>
-        </Tooltip>
-        <Tooltip className="desktop" title="Share">
-          <StyledElement onClick={() => setShareVisible(true)}>
-            <AiOutlineLink />
-          </StyledElement>
-        </Tooltip>
+          deviceDisplay="desktop"
+          onClick={toggleExpandCollapseGraph}
+          component={graphCollapsed ? <VscExpandAll /> : <VscCollapseAll />}
+        />
+
+        <SidebarButton
+          title="Download JSON"
+          deviceDisplay="desktop"
+          onClick={handleSave}
+          component={<AiOutlineSave />}
+        />
+
+        <SidebarButton
+          title="Download Image"
+          deviceDisplay="mobile"
+          onClick={() => setVisible("download")(true)}
+          component={<FiDownload />}
+        />
+
+        <SidebarButton
+          title="Delete JSON"
+          onClick={() => setVisible("clear")(true)}
+          component={<AiOutlineDelete />}
+        />
+
+        <SidebarButton
+          title="View Cloud"
+          deviceDisplay="desktop"
+          onClick={() => setVisible("cloud")(true)}
+          component={<VscCloud />}
+        />
       </StyledTopWrapper>
       <StyledBottomWrapper>
-        <StyledElement>
-          <Link href="https://twitter.com/jsoncrack">
-            <a aria-label="Twitter" rel="me" target="_blank">
-              <AiOutlineTwitter />
-            </a>
-          </Link>
-        </StyledElement>
-        <StyledElement>
-          <Link href="https://github.com/AykutSarac/jsoncrack.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>
+        <SidebarButton
+          title="Account"
+          onClick={() => setVisible("account")(true)}
+          component={<VscAccount />}
+        />
+        <SidebarButton
+          title="Settings"
+          onClick={() => setVisible("settings")(true)}
+          component={<VscSettingsGear />}
+        />
       </StyledBottomWrapper>
-      <ImportModal visible={uploadVisible} setVisible={setUploadVisible} />
-      <ClearModal visible={clearVisible} setVisible={setClearVisible} />
-      <ShareModal visible={shareVisible} setVisible={setShareVisible} />
-      <DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
     </StyledSidebar>
   );
 };

+ 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>
+  );
+};

+ 3 - 10
src/components/Sponsors/index.tsx

@@ -24,7 +24,7 @@ async function getSponsors() {
 
 const StyledSponsorsWrapper = styled.ul`
   display: flex;
-  width: 100%;
+  width: 70%;
   margin: 0;
   padding: 0;
   list-style: none;
@@ -60,8 +60,7 @@ const StyledSponsor = styled.li<{ handle: string }>`
       transform: translateY(-110%);
       border-width: 5px;
       border-style: solid;
-      border-color: ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent
-        transparent transparent;
+      border-color: ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent transparent transparent;
     }
   }
 
@@ -86,13 +85,7 @@ export const Sponsors = () => {
       {sponsors.users.map(user => (
         <StyledSponsor handle={user.handle} key={user.handle}>
           <a href={user.profile} target="_blank" rel="noreferrer">
-            <img
-              src={user.avatar}
-              alt={user.handle}
-              width="40"
-              height="40"
-              loading="lazy"
-            />
+            <img src={user.avatar} alt={user.handle} width="40" height="40" loading="lazy" />
           </a>
         </StyledSponsor>
       ))}

+ 4 - 9
src/components/SupportButton/index.tsx

@@ -26,9 +26,10 @@ const StyledSupportButton = styled.a`
   transition: all 0.5s;
   overflow: hidden;
   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);
+    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);
   opacity: 0.7;
+  box-sizing: content-box !important;
 
   &:hover {
     width: 180px;
@@ -43,14 +44,8 @@ const StyledSupportButton = styled.a`
 `;
 
 export const SupportButton = () => {
-  if (location.pathname.includes("widget")) return null;
-
   return (
-    <StyledSupportButton
-      href="https://github.com/sponsors/AykutSarac"
-      target="_blank"
-      rel="me"
-    >
+    <StyledSupportButton href="https://github.com/sponsors/AykutSarac" target="_blank" rel="me">
       <StyledText>Support JSON Crack</StyledText>
       <HiHeart size={25} />
     </StyledSupportButton>

+ 22 - 30
src/components/Tooltip/index.tsx

@@ -5,14 +5,9 @@ interface TooltipProps extends React.ComponentPropsWithoutRef<"div"> {
   title?: string;
 }
 
-const StyledTooltipWrapper = styled.div`
-  position: relative;
-  width: fit-content;
-  height: 100%;
-`;
-
-const StyledTooltip = styled.div<{ visible: boolean }>`
+const StyledTooltip = styled.div`
   position: absolute;
+  display: none;
   top: 0;
   right: 0;
   transform: translate(calc(100% + 15px), 25%);
@@ -20,15 +15,15 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
   background: ${({ theme }) => theme.BACKGROUND_PRIMARY};
   color: ${({ theme }) => theme.TEXT_NORMAL};
   border-radius: 5px;
-  padding: 4px 8px;
-  display: ${({ visible }) => (visible ? "initial" : "none")};
+  padding: 6px 8px;
   white-space: nowrap;
+  font-family: "Mona Sans";
   font-size: 16px;
   user-select: none;
   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);
+    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);
 
   &::after {
     content: "";
@@ -38,8 +33,7 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
     transform: translate(-90%, 50%);
     border-width: 8px;
     border-style: solid;
-    border-color: transparent ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent
-      transparent;
+    border-color: transparent ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent transparent;
   }
 
   @media only screen and (max-width: 768px) {
@@ -47,25 +41,23 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
   }
 `;
 
-const StyledChildren = styled.div``;
+const StyledTooltipWrapper = styled.div`
+  position: relative;
+  width: fit-content;
+  height: 100%;
+
+  &:hover ${StyledTooltip} {
+    display: initial;
+  }
+`;
 
 export const Tooltip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
   children,
   title,
   ...props
-}) => {
-  const [visible, setVisible] = React.useState(false);
-
-  return (
-    <StyledTooltipWrapper {...props}>
-      {title && <StyledTooltip visible={visible}>{title}</StyledTooltip>}
-
-      <StyledChildren
-        onMouseEnter={() => setVisible(true)}
-        onMouseLeave={() => setVisible(false)}
-      >
-        {children}
-      </StyledChildren>
-    </StyledTooltipWrapper>
-  );
-};
+}) => (
+  <StyledTooltipWrapper {...props}>
+    {title && <StyledTooltip>{title}</StyledTooltip>}
+    <div>{children}</div>
+  </StyledTooltipWrapper>
+);

+ 27 - 12
src/constants/globalStyle.ts

@@ -1,32 +1,45 @@
 import { createGlobalStyle } from "styled-components";
 
 const GlobalStyle = createGlobalStyle`
+  @font-face {
+    font-family: 'Mona Sans';
+    src:
+      url('assets/Mona-Sans.woff2') format('woff2 supports variations'),
+      url('assets/Mona-Sans.woff2') format('woff2-variations');
+    font-weight: 200 900;
+    font-stretch: 75% 125%;
+  }
+
+  svg {
+    vertical-align: top;
+  }
+
+  h1, h2, h3, h4, p {
+    font-family: 'Mona Sans';
+  }
+
   html, body {
     margin: 0;
     padding: 0;
     box-sizing: border-box;
     color: ${({ theme }) => theme.FULL_WHITE};
-    font-family: 'Catamaran', sans-serif;
+    font-family: 'Mona Sans';
     font-weight: 400;
     font-size: 16px;
-    scroll-behavior: smooth;
     height: 100%;
-
     background-color: #000000;
-    opacity: 1;
-    background-image: radial-gradient(#414141 0.5px, #000000 0.5px);
-    background-size: 10px 10px;
-
-    @media only screen and (min-width: 768px) {
-      background-color: #000000;
-      opacity: 1;
-      background-image: radial-gradient(#414141 0.5px, #000000 0.5px);
-      background-size: 15px 15px;
+      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 800 800'%3E%3Cg fill-opacity='0.3'%3E%3Ccircle fill='%23000000' cx='400' cy='400' r='600'/%3E%3Ccircle fill='%23110718' cx='400' cy='400' r='500'/%3E%3Ccircle fill='%23220e30' cx='400' cy='400' r='400'/%3E%3Ccircle fill='%23331447' cx='400' cy='400' r='300'/%3E%3Ccircle fill='%23441b5f' cx='400' cy='400' r='200'/%3E%3Ccircle fill='%23552277' cx='400' cy='400' r='100'/%3E%3C/g%3E%3C/svg%3E");
+      background-attachment: fixed;
+      background-size: cover;
+
+    @media only screen and (max-width: 768px) {
+      background-position: right;
     }
   }
 
   * {
     -webkit-tap-highlight-color: transparent;
+    scroll-behavior: smooth;
   }
 
   .hide {
@@ -42,6 +55,7 @@ const GlobalStyle = createGlobalStyle`
   }
 
   button {
+    font-family: 'Mona Sans';
     border: none;
     outline: none;
     background: transparent;
@@ -49,6 +63,7 @@ const GlobalStyle = createGlobalStyle`
     margin: 0;
     padding: 0;
     cursor: pointer;
+    font-weight: 800;
   }
 
   #carbonads * {

+ 1 - 0
src/constants/theme.ts

@@ -1,6 +1,7 @@
 const fixedColors = {
   CRIMSON: "#DC143C",
   BLURPLE: "#5865F2",
+  PURPLE: "#9036AF",
   FULL_WHITE: "#FFFFFF",
   BLACK: "#202225",
   BLACK_DARK: "#2C2F33",

+ 189 - 0
src/containers/Editor/BottomBar.tsx

@@ -0,0 +1,189 @@
+import React from "react";
+import { useRouter } from "next/router";
+import toast from "react-hot-toast";
+import {
+  AiOutlineCloudSync,
+  AiOutlineCloudUpload,
+  AiOutlineLink,
+  AiOutlineLock,
+  AiOutlineUnlock,
+} from "react-icons/ai";
+import { VscAccount } from "react-icons/vsc";
+import { saveJson, updateJson } from "src/services/db/json";
+import useJson from "src/store/useJson";
+import useModal from "src/store/useModal";
+import useStored from "src/store/useStored";
+import useUser from "src/store/useUser";
+import styled from "styled-components";
+
+const StyledBottomBar = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  max-height: 28px;
+  height: 28px;
+  padding: 0 6px;
+
+  @media only screen and (max-width: 768px) {
+    display: none;
+  }
+`;
+
+const StyledLeft = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: left;
+  gap: 4px;
+`;
+
+const StyledRight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: right;
+  gap: 4px;
+`;
+
+const StyledBottomBarItem = styled.button`
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  width: fit-content;
+  margin: 0;
+  height: 28px;
+  padding: 4px;
+  font-size: 12px;
+  font-weight: 400;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+
+  &:hover:not(&:disabled) {
+    background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0 0);
+    color: ${({ theme }) => theme.INTERACTIVE_HOVER};
+  }
+
+  &:disabled {
+    opacity: 0.4;
+    cursor: progress;
+  }
+`;
+
+const StyledImg = styled.img<{ light: boolean }>`
+  filter: ${({ light }) => light && "invert(100%)"};
+`;
+
+export const BottomBar = () => {
+  const { replace, query } = useRouter();
+  const data = useJson(state => state.data);
+  const user = useUser(state => state.user);
+  const lightmode = useStored(state => state.lightmode);
+  const hasChanges = useJson(state => state.hasChanges);
+
+  const getJson = useJson(state => state.getJson);
+  const setVisible = useModal(state => state.setVisible);
+  const setHasChanges = useJson(state => state.setHasChanges);
+  const [isPrivate, setIsPrivate] = React.useState(false);
+  const [isUpdating, setIsUpdating] = React.useState(false);
+
+  React.useEffect(() => {
+    setIsPrivate(data?.private ?? false);
+  }, [data]);
+
+  const handleSaveJson = React.useCallback(async () => {
+    if (!user) return setVisible("login")(true);
+
+    if (hasChanges) {
+      try {
+        setIsUpdating(true);
+        toast.loading("Saving JSON...", { id: "jsonSave" });
+        const res = await saveJson({ id: query.json as string, data: getJson() });
+
+        if (res.errors && res.errors.items.length > 0) throw res.errors;
+        if (res.data._id) replace({ query: { json: res.data._id } });
+
+        toast.success("JSON saved to cloud", { id: "jsonSave" });
+        setHasChanges(false);
+      } catch (error: any) {
+        if (error?.items?.length > 0) {
+          return toast.error(error.items[0].message, { id: "jsonSave", duration: 5000 });
+        }
+
+        toast.error("Failed to save JSON!", { id: "jsonSave" });
+      } finally {
+        setIsUpdating(false);
+      }
+    }
+  }, [getJson, hasChanges, query.json, replace, setHasChanges, setVisible, user]);
+
+  const handleLoginClick = () => {
+    if (user) return setVisible("account")(true);
+    else setVisible("login")(true);
+  };
+
+  const setPrivate = async () => {
+    try {
+      if (!query.json) return handleSaveJson();
+      if (!isPrivate && user?.type === 0) {
+        return window.open("https://jsoncrack.com/pricing", "_blank");
+      }
+
+      setIsUpdating(true);
+      const res = await updateJson(query.json as string, { private: !isPrivate });
+      if (!res.errors?.items.length) {
+        setIsPrivate(res.data.private);
+        toast.success(`Document set to ${isPrivate ? "public" : "private"}.`);
+      } else throw res.errors;
+    } catch (error) {
+      toast.error("An error occured while updating document!");
+    } finally {
+      setIsUpdating(false);
+    }
+  };
+
+  return (
+    <StyledBottomBar>
+      <StyledLeft>
+        <StyledBottomBarItem onClick={handleLoginClick}>
+          <VscAccount />
+          {user ? user.name : "Login"}
+        </StyledBottomBarItem>
+        <StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating}>
+          {hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
+          {hasChanges ? "Unsaved Changes" : "Saved"}
+        </StyledBottomBarItem>
+        {data && (
+          <>
+            {typeof data.private !== "undefined" && (
+              <StyledBottomBarItem onClick={setPrivate} disabled={isUpdating}>
+                {isPrivate ? <AiOutlineLock /> : <AiOutlineUnlock />}
+                {isPrivate ? "Private" : "Public"}
+              </StyledBottomBarItem>
+            )}
+            <StyledBottomBarItem onClick={() => setVisible("share")(true)}>
+              <AiOutlineLink />
+              Share
+            </StyledBottomBarItem>
+          </>
+        )}
+      </StyledLeft>
+      <StyledRight>
+        <a
+          href="https://www.altogic.com/?utm_source=jsoncrack&utm_medium=referral&utm_campaign=sponsorship"
+          rel="sponsored noreferrer"
+          target="_blank"
+        >
+          <StyledBottomBarItem>
+            Powered by
+            <StyledImg
+              height="20"
+              width="54"
+              src="https://www.altogic.com/img/logo_dark.svg"
+              alt="powered by altogic"
+              light={lightmode}
+            />
+          </StyledBottomBarItem>
+        </a>
+      </StyledRight>
+    </StyledBottomBar>
+  );
+};

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

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

+ 12 - 17
src/containers/Editor/LiveEditor/GraphCanvas.tsx

@@ -7,11 +7,10 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
   const [isModalVisible, setModalVisible] = React.useState(false);
   const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
 
-  const openModal = React.useCallback(() => setModalVisible(true), []);
-
   const collapsedNodes = useGraph(state => state.collapsedNodes);
   const collapsedEdges = useGraph(state => state.collapsedEdges);
-  const loading = useGraph(state => state.loading);
+
+  const openModal = React.useCallback(() => setModalVisible(true), []);
 
   React.useEffect(() => {
     const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
@@ -22,27 +21,23 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
 
     if (nodeList.length) {
       const selectedNodes = document.querySelectorAll(nodeList.join(","));
-      const selectedEdges = document.querySelectorAll(edgeList.join(","));
-
       selectedNodes.forEach(node => node.classList.add("hide"));
+    }
+
+    if (edgeList.length) {
+      const selectedEdges = document.querySelectorAll(edgeList.join(","));
       selectedEdges.forEach(edge => edge.classList.add("hide"));
     }
-  }, [collapsedNodes, collapsedEdges, loading]);
+  }, [collapsedNodes, collapsedEdges]);
 
   return (
     <>
-      <Graph
-        openModal={openModal}
-        setSelectedNode={setSelectedNode}
-        isWidget={isWidget}
+      <Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget={isWidget} />
+      <NodeModal
+        selectedNode={selectedNode}
+        visible={isModalVisible}
+        closeModal={() => setModalVisible(false)}
       />
-      {!isWidget && (
-        <NodeModal
-          selectedNode={selectedNode}
-          visible={isModalVisible}
-          closeModal={() => setModalVisible(false)}
-        />
-      )}
     </>
   );
 };

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

@@ -3,7 +3,7 @@ import dynamic from "next/dynamic";
 import { Allotment } from "allotment";
 import "allotment/dist/style.css";
 import { JsonEditor } from "src/containers/Editor/JsonEditor";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import styled from "styled-components";
 
 export const StyledEditor = styled(Allotment)`
@@ -17,25 +17,25 @@ const LiveEditor = dynamic(() => import("src/containers/Editor/LiveEditor"), {
 });
 
 const Panes: React.FC = () => {
-  const hideEditor = useConfig(state => state.hideEditor);
-  const setConfig = useConfig(state => state.setConfig);
-  const isMobile = window.innerWidth <= 768;
+  const fullscreen = useGraph(state => state.fullscreen);
+  const toggleFullscreen = useGraph(state => state.toggleFullscreen);
+  const isMobile = React.useMemo(() => window.innerWidth <= 768, []);
 
   React.useEffect(() => {
-    if (isMobile) setConfig("hideEditor", true);
-  }, [isMobile, setConfig]);
+    if (isMobile) toggleFullscreen(true);
+  }, [isMobile, toggleFullscreen]);
 
   return (
     <StyledEditor proportionalLayout={false} vertical={isMobile}>
       <Allotment.Pane
         preferredSize={isMobile ? "100%" : 400}
-        minSize={hideEditor ? 0 : 300}
+        minSize={fullscreen ? 0 : 300}
         maxSize={isMobile ? Infinity : 800}
-        visible={!hideEditor}
+        visible={!fullscreen}
       >
         <JsonEditor />
       </Allotment.Pane>
-      <Allotment.Pane minSize={0} maxSize={isMobile && !hideEditor ? 0 : Infinity}>
+      <Allotment.Pane minSize={0} maxSize={isMobile && !fullscreen ? 0 : Infinity}>
         <LiveEditor />
       </Allotment.Pane>
     </StyledEditor>

+ 10 - 24
src/containers/Editor/Tools.tsx

@@ -2,12 +2,10 @@ import React from "react";
 import { AiOutlineFullscreen, AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
 import { FiDownload } from "react-icons/fi";
 import { MdCenterFocusWeak } from "react-icons/md";
-import { TbSettings } from "react-icons/tb";
 import { SearchInput } from "src/components/SearchInput";
-import { SettingsModal } from "src/containers/Modals/SettingsModal";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
+import useModal from "src/store/useModal";
 import styled from "styled-components";
-import { DownloadModal } from "../Modals/DownloadModal";
 
 export const StyledTools = styled.div`
   position: relative;
@@ -48,16 +46,15 @@ const StyledToolElement = styled.button`
 `;
 
 export const Tools: React.FC = () => {
-  const [settingsVisible, setSettingsVisible] = React.useState(false);
-  const [isDownloadVisible, setDownloadVisible] = React.useState(false);
+  const setVisible = useModal(state => state.setVisible);
 
-  const hideEditor = useConfig(state => state.hideEditor);
-  const setConfig = useConfig(state => state.setConfig);
+  const fullscreen = useGraph(state => state.fullscreen);
+  const toggleFullscreen = useGraph(state => state.toggleFullscreen);
 
-  const zoomIn = useConfig(state => state.zoomIn);
-  const zoomOut = useConfig(state => state.zoomOut);
-  const centerView = useConfig(state => state.centerView);
-  const toggleEditor = () => setConfig("hideEditor", !hideEditor);
+  const zoomIn = useGraph(state => state.zoomIn);
+  const zoomOut = useGraph(state => state.zoomOut);
+  const centerView = useGraph(state => state.centerView);
+  const toggleEditor = () => toggleFullscreen(!fullscreen);
 
   return (
     <>
@@ -65,17 +62,8 @@ export const Tools: React.FC = () => {
         <StyledToolElement aria-label="fullscreen" onClick={toggleEditor}>
           <AiOutlineFullscreen />
         </StyledToolElement>
-        <StyledToolElement
-          aria-label="settings"
-          onClick={() => setSettingsVisible(true)}
-        >
-          <TbSettings />
-        </StyledToolElement>
         <SearchInput />
-        <StyledToolElement
-          aria-label="save"
-          onClick={() => setDownloadVisible(true)}
-        >
+        <StyledToolElement aria-label="save" onClick={() => setVisible("download")(true)}>
           <FiDownload />
         </StyledToolElement>
         <StyledToolElement aria-label="center canvas" onClick={centerView}>
@@ -88,8 +76,6 @@ export const Tools: React.FC = () => {
           <AiOutlinePlus />
         </StyledToolElement>
       </StyledTools>
-      <DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
-      <SettingsModal visible={settingsVisible} setVisible={setSettingsVisible} />
     </>
   );
 };

+ 82 - 95
src/containers/Home/index.tsx

@@ -2,20 +2,22 @@ import React from "react";
 import Head from "next/head";
 import Link from "next/link";
 import Script from "next/script";
-import { FaGithub, FaHeart, FaLinkedin, FaTwitter } from "react-icons/fa";
+import { AiOutlineRight } from "react-icons/ai";
 import {
   HiCursorClick,
   HiLightningBolt,
   HiOutlineDownload,
   HiOutlineSearchCircle,
 } from "react-icons/hi";
+import { IoRocketSharp } from "react-icons/io5";
 import { SiVisualstudiocode } from "react-icons/si";
 import { CarbonAds } from "src/components/CarbonAds";
+import { Footer } from "src/components/Footer";
 import { Producthunt } from "src/components/Producthunt";
 import { Sponsors } from "src/components/Sponsors";
-import { defaultJson } from "src/constants/data";
-import { GoalsModal } from "src/containers/Modals/GoalsModal";
-import pkg from "../../../package.json";
+import { SupportButton } from "src/components/SupportButton";
+import { baseURL } from "src/constants/data";
+import { PricingCards } from "../PricingCards";
 import * as Styles from "./styles";
 
 const Navbar = () => (
@@ -34,6 +36,9 @@ const Navbar = () => (
     >
       GitHub
     </Styles.StyledNavLink>
+    <Link href="docs" passHref>
+      <Styles.StyledNavLink>Documentation</Styles.StyledNavLink>
+    </Link>
   </Styles.StyledNavbar>
 );
 
@@ -43,34 +48,31 @@ const HeroSection = () => {
   return (
     <Styles.StyledHeroSection id="main">
       <Styles.StyledTitle>
-        <Styles.StyledGradientText>JSON</Styles.StyledGradientText> Crack
+        <Styles.StyledGradientText>JSON</Styles.StyledGradientText> CRACK
       </Styles.StyledTitle>
       <Styles.StyledSubTitle>
         Seamlessly visualize your JSON data{" "}
-        <Styles.StyledHighlightedText>instantly</Styles.StyledHighlightedText> into
-        graphs.
+        <Styles.StyledHighlightedText>instantly</Styles.StyledHighlightedText> into graphs.
       </Styles.StyledSubTitle>
-      <Styles.StyledMinorTitle>Paste - Import - Fetch!</Styles.StyledMinorTitle>
 
-      <Styles.StyledButton rel="prefetch" href="/editor" link>
+      <Styles.StyledButton href="/editor" link>
         GO TO EDITOR
+        <AiOutlineRight strokeWidth="80" />
       </Styles.StyledButton>
 
       <Styles.StyledButtonWrapper>
-        <Styles.StyledSponsorButton onClick={() => setModalVisible(true)}>
-          Help JSON Crack&apos;s Goals
-          <FaHeart />
+        <Styles.StyledSponsorButton href="/pricing" link>
+          GET PREMIUM
+          <IoRocketSharp />
         </Styles.StyledSponsorButton>
-        <Link
+        <Styles.StyledSponsorButton
           href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode"
-          passHref
+          link
+          isBlue
         >
-          <Styles.StyledSponsorButton isBlue>
-            GET IT ON VS CODE
-            <SiVisualstudiocode />
-          </Styles.StyledSponsorButton>
-        </Link>
-        <GoalsModal visible={isModalVisible} setVisible={setModalVisible} />
+          GET IT ON VS CODE
+          <SiVisualstudiocode />
+        </Styles.StyledSponsorButton>
       </Styles.StyledButtonWrapper>
     </Styles.StyledHeroSection>
   );
@@ -97,9 +99,9 @@ const FeaturesSection = () => (
       </Styles.StyledCardIcon>
       <Styles.StyledCardTitle>EASY-TO-USE</Styles.StyledCardTitle>
       <Styles.StyledCardDescription>
-        Don&apos;t even bother to update your schema to view your JSON into graphs;
-        directly paste, import or fetch! JSON Crack helps you to visualize without
-        any additional values and save your time.
+        We believe that powerful software doesn&apos;t have to be difficult to use. That&apos;s why
+        we&apos;ve designed our app to be as intuitive and easy-to-use as possible. You can quickly
+        and easily load your JSON data and start exploring and analyzing it right away!
       </Styles.StyledCardDescription>
     </Styles.StyledSectionCard>
 
@@ -109,9 +111,9 @@ const FeaturesSection = () => (
       </Styles.StyledCardIcon>
       <Styles.StyledCardTitle>SEARCH</Styles.StyledCardTitle>
       <Styles.StyledCardDescription>
-        Have a huge file of values, keys or arrays? Worry no more, type in the
-        keyword you are looking for into search input and it will take you to each
-        node with matching result highlighting the line to understand better!
+        Have a huge file of values, keys or arrays? Worry no more, type in the keyword you are
+        looking for into search input and it will take you to each node with matching result
+        highlighting the line to understand better!
       </Styles.StyledCardDescription>
     </Styles.StyledSectionCard>
 
@@ -121,9 +123,9 @@ const FeaturesSection = () => (
       </Styles.StyledCardIcon>
       <Styles.StyledCardTitle>DOWNLOAD</Styles.StyledCardTitle>
       <Styles.StyledCardDescription>
-        Download the graph to your local machine and use it wherever you want, to
-        your blogs, website or make it a poster and paste to the wall. Who
-        wouldn&apos;t want to see a JSON Crack graph onto their wall, eh?
+        Download the graph to your local machine and use it wherever you want, to your blogs,
+        website or make it a poster and paste to the wall. Who wouldn&apos;t want to see a JSON
+        Crack graph onto their wall, eh?
       </Styles.StyledCardDescription>
     </Styles.StyledSectionCard>
 
@@ -133,9 +135,9 @@ const FeaturesSection = () => (
       </Styles.StyledCardIcon>
       <Styles.StyledCardTitle>LIVE</Styles.StyledCardTitle>
       <Styles.StyledCardDescription>
-        With Microsoft&apos;s Monaco Editor which is also used by VS Code, easily
-        edit your JSON and directly view through the graphs. Also there&apos;s a JSON
-        validator above of it to make sure there is no type error.
+        With Microsoft&apos;s Monaco Editor which is also used by VS Code, easily edit your JSON and
+        directly view through the graphs. Also there&apos;s a JSON validator above of it to make
+        sure there is no type error.
       </Styles.StyledCardDescription>
     </Styles.StyledSectionCard>
   </Styles.StyledFeaturesSection>
@@ -144,40 +146,37 @@ const FeaturesSection = () => (
 const GitHubSection = () => (
   <Styles.StyledSection id="github" reverse>
     <Styles.StyledTwitterQuote>
-      <blockquote
-        className="twitter-tweet"
-        data-lang="en"
-        data-dnt="true"
-        data-theme="light"
-      >
+      <blockquote className="twitter-tweet" data-lang="en" data-dnt="true" data-theme="light">
         <p lang="en" dir="ltr">
-          Looking to understand or explore some JSON? Just paste or upload to
-          visualize it as a graph with{" "}
-          <a href="https://t.co/HlKSrhKryJ">https://t.co/HlKSrhKryJ</a> 😍 <br />
+          Looking to understand or explore some JSON? Just paste or upload to visualize it as a
+          graph with <a href="https://t.co/HlKSrhKryJ">https://t.co/HlKSrhKryJ</a> 😍 <br />
           <br />
-          Thanks to{" "}
-          <a href="https://twitter.com/aykutsarach?ref_src=twsrc%5Etfw">
+          Thanks to <a href="https://twitter.com/aykutsarach?ref_src=twsrc%5Etfw">
             @aykutsarach
-          </a>
-          ! <a href="https://t.co/0LyPUL8Ezz">pic.twitter.com/0LyPUL8Ezz</a>
+          </a>! <a href="https://t.co/0LyPUL8Ezz">pic.twitter.com/0LyPUL8Ezz</a>
         </p>
         &mdash; GitHub (@github){" "}
         <a href="https://twitter.com/github/status/1519363257794015233?ref_src=twsrc%5Etfw">
           April 27, 2022
         </a>
       </blockquote>{" "}
-      <Script
-        async
-        src="https://platform.twitter.com/widgets.js"
-        charSet="utf-8"
-      ></Script>
+      <Script async src="https://platform.twitter.com/widgets.js" charSet="utf-8"></Script>
     </Styles.StyledTwitterQuote>
     <Styles.StyledSectionArea>
       <Styles.StyledSubTitle>Open Source Community</Styles.StyledSubTitle>
       <Styles.StyledMinorTitle>
-        Join the Open Source Community by suggesting new ideas, support by
-        contributing; implementing new features, fixing bugs and doing changes minor
-        to major!
+        At JSON Crack, we believe in the power of open source software and the communities that
+        support it. That&apos;s why we&apos;re proud to be part of the open source community and to
+        contribute to the development and growth of open source tools and technologies.
+        <br />
+        <br /> As part of our commitment to the open source community, we&apos;ve made our app
+        freely available to anyone who wants to use it, and we welcome contributions from anyone
+        who&apos;s interested in helping to improve it. Whether you&apos;re a developer, a data
+        scientist, or just someone who&apos;s passionate about open source, we&apos;d love to have
+        you join our community and help us make JSON Crack the best it can be.
+        <br />
+        <br /> So why not join us and become part of the JSON Crack open source community today? We
+        can&apos;t wait to see what we can accomplish together!
       </Styles.StyledMinorTitle>
       <Styles.StyledButton
         href="https://github.com/AykutSarac/jsoncrack.com"
@@ -195,19 +194,41 @@ const EmbedSection = () => (
     <Styles.StyledSectionArea>
       <Styles.StyledSubTitle>Embed Into Your Website</Styles.StyledSubTitle>
       <Styles.StyledMinorTitle>
-        Easily embed the JSON Crack graph into your website to showcase your
-        visitors, blog readers or anybody else!
+        JSON Crack provides users with the necessary code to embed the app into a website easily
+        using an iframe. This code can be easily copied and pasted into the desired location on the
+        website, allowing users to quickly and easily integrate JSON Crack into existing workflows.
+        <br />
+        <br /> Once the app is embedded, users can use it to view and analyze JSON data directly on
+        the website. This can be useful for a variety of purposes, such as quickly checking the
+        structure of a JSON file or verifying the data contained within it. JSON Crack&apos;s
+        intuitive interface makes it easy to navigate and understand even complex JSON data, making
+        it a valuable tool for anyone working with JSON.
       </Styles.StyledMinorTitle>
+      <Styles.StyledButton href="/docs" status="SECONDARY" link>
+        LEARN TO EMBED
+      </Styles.StyledButton>
     </Styles.StyledSectionArea>
     <div>
       <Styles.StyledIframge
-        src="https://jsoncrack.com/widget"
+        src={`${baseURL}/widget`}
         onLoad={e => {
           const frame = e.currentTarget.contentWindow;
           setTimeout(() => {
-            frame?.postMessage({
-              json: defaultJson,
-            });
+            frame?.postMessage(
+              {
+                json: JSON.stringify({
+                  "random images": [
+                    "https://random.imagecdn.app/50/50?.png",
+                    "https://random.imagecdn.app/80/80?.png",
+                    "https://random.imagecdn.app/100/100?.png",
+                  ],
+                }),
+                options: {
+                  theme: "dark",
+                },
+              },
+              "*"
+            );
           }, 500);
         }}
       ></Styles.StyledIframge>
@@ -250,42 +271,6 @@ const SponsorSection = () => (
   </Styles.StyledSponsorSection>
 );
 
-const Footer = () => (
-  <Styles.StyledFooter>
-    <Styles.StyledFooterText>
-      © 2022 JSON Crack - {pkg.version}
-    </Styles.StyledFooterText>
-    <Styles.StyledIconLinks>
-      <Styles.StyledNavLink
-        href="https://github.com/AykutSarac/jsoncrack.com"
-        rel="external"
-        target="_blank"
-        aria-label="github"
-      >
-        <FaGithub size={26} />
-      </Styles.StyledNavLink>
-
-      <Styles.StyledNavLink
-        href="https://www.linkedin.com/in/aykutsarac/"
-        rel="me"
-        target="_blank"
-        aria-label="linkedin"
-      >
-        <FaLinkedin size={26} />
-      </Styles.StyledNavLink>
-
-      <Styles.StyledNavLink
-        href="https://twitter.com/jsoncrack"
-        rel="me"
-        target="_blank"
-        aria-label="twitter"
-      >
-        <FaTwitter size={26} />
-      </Styles.StyledNavLink>
-    </Styles.StyledIconLinks>
-  </Styles.StyledFooter>
-);
-
 const Home: React.FC = () => {
   return (
     <Styles.StyledHome>
@@ -298,8 +283,10 @@ const Home: React.FC = () => {
       <FeaturesSection />
       <GitHubSection />
       <EmbedSection />
+      <PricingCards />
       <SupportSection />
       <SponsorSection />
+      <SupportButton />
       <Footer />
     </Styles.StyledHome>
   );

+ 37 - 50
src/containers/Home/styles.tsx

@@ -4,6 +4,10 @@ import styled from "styled-components";
 export const StyledButtonWrapper = styled.div`
   display: flex;
   gap: 18px;
+
+  @media only screen and (max-width: 768px) {
+    display: none;
+  }
 `;
 
 export const StyledTwitterQuote = styled.div`
@@ -70,13 +74,7 @@ export const StyledHome = styled.div`
 
 export const StyledGradientText = styled.span`
   background: #ffb76b;
-  background: linear-gradient(
-    to right,
-    #ffb76b 0%,
-    #ffa73d 30%,
-    #ff7c00 60%,
-    #ff7f04 100%
-  );
+  background: linear-gradient(to right, #ffb76b 0%, #ffa73d 30%, #ff7c00 60%, #ff7f04 100%);
   background-clip: text;
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;
@@ -105,6 +103,10 @@ export const StyledHeroSection = styled.section`
   gap: 1.5em;
   min-height: 40vh;
   padding: 0 3%;
+
+  h2 {
+    margin-bottom: 25px;
+  }
 `;
 
 export const StyledNavLink = styled.a`
@@ -122,12 +124,12 @@ export const StyledNavLink = styled.a`
 `;
 
 export const StyledTitle = styled.h1`
-  font-size: 5rem;
   font-weight: 900;
   margin: 0;
+  font-size: min(10vw, 64px);
 
   @media only screen and (max-width: 768px) {
-    font-size: 3rem;
+    font-size: 2.5rem;
   }
 `;
 
@@ -139,14 +141,14 @@ export const StyledSubTitle = styled.h2`
   margin: 0;
 
   @media only screen and (max-width: 768px) {
-    font-size: 1.75rem;
+    font-size: 1.5rem;
   }
 `;
 
 export const StyledMinorTitle = styled.p`
   color: #b4b4b4;
   text-align: center;
-  font-size: 1.25rem;
+  font-size: 1rem;
   margin: 0;
   letter-spacing: 1.2px;
 
@@ -158,11 +160,12 @@ export const StyledMinorTitle = styled.p`
 export const StyledButton = styled(Button)`
   background: ${({ status }) => !status && "#a13cc2"};
   padding: 12px 24px;
+  height: 46px;
 
   div {
-    font-family: "Roboto", sans-serif;
+    font-family: "Mona Sans";
     font-weight: 700;
-    font-size: 16px;
+    font-size: 1rem;
   }
 `;
 
@@ -189,28 +192,38 @@ export const StyledSponsorButton = styled(Button)<{ isBlue?: boolean }>`
       color: white;
     }
   }
-
-  @media only screen and (max-width: 768px) {
-    display: ${({ isBlue }) => isBlue && "none"};
-  }
 `;
 
 export const StyledFeaturesSection = styled.section`
-  display: flex;
-  max-width: 85%;
+  display: grid;
   margin: 0 auto;
-  gap: 2rem;
-  padding: 50px 3%;
+  max-width: 60%;
+  justify-content: center;
+  grid-template-columns: repeat(2, 40%);
+  grid-template-rows: repeat(2, 1fr);
+  grid-column-gap: 60px;
+  grid-row-gap: 60px;
 
   @media only screen and (max-width: 768px) {
     flex-direction: column;
     max-width: 80%;
+    display: flex;
   }
 `;
 
 export const StyledSectionCard = styled.div`
   text-align: center;
   flex: 1;
+  border: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  background: rgb(48, 0, 65);
+  background: linear-gradient(
+    138deg,
+    rgba(48, 0, 65, 0.8870141806722689) 0%,
+    rgba(72, 12, 84, 0.40802258403361347) 33%,
+    rgba(65, 8, 92, 0.6012998949579832) 100%
+  );
+  border-radius: 6px;
+  padding: 16px;
 `;
 
 export const StyledCardTitle = styled.div`
@@ -255,7 +268,7 @@ export const StyledSection = styled.section<{ reverse?: boolean }>`
     width: 100%;
   }
 
-  @media only screen and (max-width: 768px) {
+  @media only screen and (max-width: 1200px) {
     flex-direction: ${({ reverse }) => (reverse ? "column-reverse" : "column")};
     max-width: 80%;
   }
@@ -278,8 +291,7 @@ export const StyledSectionArea = styled.div`
     width: 100%;
     align-items: center;
 
-    h2,
-    p {
+    h2 {
       text-align: center;
     }
   }
@@ -297,7 +309,7 @@ export const StyledSponsorSection = styled.section`
   padding: 50px 3%;
 
   @media only screen and (max-width: 768px) {
-    max-width: 80%;
+    max-width: 90%;
   }
 `;
 
@@ -327,7 +339,6 @@ export const StyledPreviewSection = styled.section`
 
   @media only screen and (max-width: 768px) {
     display: none;
-    max-width: 80%;
   }
 `;
 
@@ -338,30 +349,6 @@ export const StyledImage = styled.img`
   filter: drop-shadow(0px 0px 12px rgba(255, 255, 255, 0.6));
 `;
 
-export const StyledFooter = styled.footer`
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  width: 80%;
-  margin: 0 auto;
-  padding: 30px 3%;
-  border-top: 1px solid #b4b4b4;
-  opacity: 0.7;
-`;
-
-export const StyledFooterText = styled.p`
-  color: #b4b4b4;
-`;
-
-export const StyledIconLinks = styled.div`
-  display: flex;
-  gap: 20px;
-
-  ${StyledNavLink} {
-    color: unset;
-  }
-`;
-
 export const StyledHighlightedText = styled.span`
   text-decoration: underline;
   text-decoration-style: dashed;

+ 51 - 0
src/containers/ModalController/index.tsx

@@ -0,0 +1,51 @@
+import React from "react";
+import { AccountModal } from "src/containers/Modals/AccountModal";
+import { ClearModal } from "src/containers/Modals/ClearModal";
+import { CloudModal } from "src/containers/Modals/CloudModal";
+import { DownloadModal } from "src/containers/Modals/DownloadModal";
+import { ImportModal } from "src/containers/Modals/ImportModal";
+import { LoginModal } from "src/containers/Modals/LoginModal";
+import { SettingsModal } from "src/containers/Modals/SettingsModal";
+import { ShareModal } from "src/containers/Modals/ShareModal";
+import useModal from "src/store/useModal";
+import shallow from "zustand/shallow";
+
+export const ModalController = () => {
+  const setVisible = useModal(state => state.setVisible);
+
+  const [
+    importModal,
+    clearModal,
+    downloadModal,
+    settingsModal,
+    cloudModal,
+    accountModal,
+    loginModal,
+    shareModal,
+  ] = useModal(
+    state => [
+      state.import,
+      state.clear,
+      state.download,
+      state.settings,
+      state.cloud,
+      state.account,
+      state.login,
+      state.share,
+    ],
+    shallow
+  );
+
+  return (
+    <>
+      <ImportModal visible={importModal} setVisible={setVisible("import")} />
+      <ClearModal visible={clearModal} setVisible={setVisible("clear")} />
+      <DownloadModal visible={downloadModal} setVisible={setVisible("download")} />
+      <SettingsModal visible={settingsModal} setVisible={setVisible("settings")} />
+      <CloudModal visible={cloudModal} setVisible={setVisible("cloud")} />
+      <AccountModal visible={accountModal} setVisible={setVisible("account")} />
+      <LoginModal visible={loginModal} setVisible={setVisible("login")} />
+      <ShareModal visible={shareModal} setVisible={setVisible("share")} />
+    </>
+  );
+};

+ 143 - 0
src/containers/Modals/AccountModal/index.tsx

@@ -0,0 +1,143 @@
+import React from "react";
+import { IoRocketSharp } from "react-icons/io5";
+import { MdVerified } from "react-icons/md";
+import { Button } from "src/components/Button";
+import { Modal, ModalProps } from "src/components/Modal";
+import useUser from "src/store/useUser";
+import styled from "styled-components";
+
+const StyledTitle = styled.p`
+  display: flex;
+  align-items: center;
+  color: ${({ theme }) => theme.TEXT_POSITIVE};
+  flex: 1;
+  font-weight: 700;
+  margin-top: 0;
+
+  &::after {
+    background: ${({ theme }) => theme.TEXT_POSITIVE};
+    height: 1px;
+
+    content: "";
+    -webkit-box-flex: 1;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+    margin-left: 4px;
+    opacity: 0.6;
+  }
+`;
+
+const StyledAccountWrapper = styled.div`
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  padding: 16px;
+  border-radius: 6px;
+
+  button {
+    flex-basis: 100%;
+  }
+`;
+
+const StyledAvatar = styled.img`
+  border-radius: 100%;
+`;
+
+const StyledContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 12px 0;
+  font-size: 12px;
+  line-height: 16px;
+  font-weight: 600;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+
+  & > div {
+    font-weight: 400;
+    font-size: 14px;
+    color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
+  }
+`;
+
+const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) => {
+  const user = useUser(state => state.user);
+  const isPremium = useUser(state => state.isPremium());
+  const logout = useUser(state => state.logout);
+
+  const onImgFail = (e: React.SyntheticEvent<HTMLImageElement>) => {
+    e.currentTarget.setAttribute("src", `https://ui-avatars.com/api/?name=${user?.name}`);
+  };
+
+  return (
+    <>
+      <Modal.Header>Account</Modal.Header>
+      <Modal.Content>
+        <StyledTitle>Hello, {user?.name}!</StyledTitle>
+        <StyledAccountWrapper>
+          <StyledAvatar
+            width="60"
+            height="60"
+            src={user?.profilePicture}
+            alt={user?.name}
+            onError={onImgFail}
+          />
+          <StyledContainer>
+            USERNAME
+            <div>{user?.name}</div>
+          </StyledContainer>
+          <StyledContainer>
+            ACCOUNT STATUS
+            <div>
+              {isPremium ? "PREMIUM " : "Free"}
+              {isPremium && <MdVerified />}
+            </div>
+          </StyledContainer>
+          <StyledContainer>
+            EMAIL
+            <div>{user?.email}</div>
+          </StyledContainer>
+          <StyledContainer>
+            REGISTRATION
+            <div>{user?.signUpAt && new Date(user.signUpAt).toDateString()}</div>
+          </StyledContainer>
+          {isPremium ? (
+            <Button
+              status="DANGER"
+              block
+              onClick={() => window.open("https://patreon.com/jsoncrack", "_blank")}
+            >
+              <IoRocketSharp />
+              Cancel Subscription
+            </Button>
+          ) : (
+            <Button href="/pricing" status="TERTIARY" block link>
+              <IoRocketSharp />
+              UPGRADE TO PREMIUM!
+            </Button>
+          )}
+        </StyledAccountWrapper>
+      </Modal.Content>
+      <Modal.Controls setVisible={setVisible}>
+        <Button
+          status="DANGER"
+          onClick={() => {
+            logout();
+            setVisible(false);
+          }}
+        >
+          Log Out
+        </Button>
+      </Modal.Controls>
+    </>
+  );
+};
+
+export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <AccountView setVisible={setVisible} />
+    </Modal>
+  );
+};

+ 12 - 6
src/containers/Modals/ClearModal/index.tsx

@@ -1,22 +1,28 @@
 import React from "react";
-import toast from "react-hot-toast";
+import { useRouter } from "next/router";
 import { Button } from "src/components/Button";
 import { Modal, ModalProps } from "src/components/Modal";
-import useConfig from "src/store/useConfig";
+import { deleteJson } from "src/services/db/json";
+import useJson from "src/store/useJson";
 
 export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useConfig(state => state.setJson);
+  const setJson = useJson(state => state.setJson);
+  const { query, replace } = useRouter();
 
   const handleClear = () => {
     setJson("{}");
-    toast.success(`Cleared JSON and removed from memory.`);
     setVisible(false);
+
+    if (typeof query.json === "string") {
+      deleteJson(query.json);
+      replace("/editor");
+    }
   };
 
   return (
     <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Clear JSON</Modal.Header>
-      <Modal.Content>Are you sure you want to clear JSON?</Modal.Content>
+      <Modal.Header>Delete JSON</Modal.Header>
+      <Modal.Content>Are you sure you want to delete JSON?</Modal.Content>
       <Modal.Controls setVisible={setVisible}>
         <Button status="DANGER" onClick={handleClear}>
           Confirm

+ 270 - 0
src/containers/Modals/CloudModal/index.tsx

@@ -0,0 +1,270 @@
+import React from "react";
+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,
+  AiOutlineInfoCircle,
+  AiOutlineLock,
+  AiOutlinePlus,
+  AiOutlineUnlock,
+} from "react-icons/ai";
+import { FaTrash } from "react-icons/fa";
+import { IoRocketSharp } from "react-icons/io5";
+import { Button } from "src/components/Button";
+import { Modal, ModalProps } from "src/components/Modal";
+import { Spinner } from "src/components/Spinner";
+import { deleteJson, getAllJson, saveJson, updateJson } from "src/services/db/json";
+import useJson from "src/store/useJson";
+import useUser from "src/store/useUser";
+import { Json } from "src/typings/altogic";
+import styled from "styled-components";
+
+dayjs.extend(relativeTime);
+
+const StyledModalContent = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  overflow: auto;
+`;
+
+const StyledJsonCard = styled.a<{ active?: boolean; create?: boolean }>`
+  display: ${({ create }) => (create ? "block" : "flex")};
+  align-items: center;
+  justify-content: space-between;
+  background: ${({ theme }) => theme.BLACK_SECONDARY};
+  border: 2px solid ${({ theme, active }) => (active ? theme.SEAGREEN : theme.BLACK_SECONDARY)};
+  border-radius: 5px;
+  overflow: hidden;
+  flex: 1;
+  height: 160px;
+`;
+
+const StyledInfo = styled.div`
+  padding: 4px 6px;
+`;
+
+const StyledTitle = styled.div`
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  width: fit-content;
+  cursor: pointer;
+
+  span {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+`;
+
+const StyledDetils = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+  gap: 4px;
+`;
+
+const StyledModal = styled(Modal)`
+  #modal-view {
+    display: none;
+  }
+`;
+
+const StyledDeleteButton = styled(Button)`
+  background: transparent;
+`;
+
+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 StyledNameInput = styled.input`
+  background: transparent;
+  border: none;
+  outline: none;
+  width: 90%;
+  color: ${({ theme }) => theme.SEAGREEN};
+  font-weight: 600;
+`;
+
+const StyledInfoText = styled.span`
+  font-size: 10px;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+
+  svg {
+    vertical-align: text-top;
+    margin-right: 4px;
+  }
+`;
+
+const GraphCard: React.FC<{ data: Json; refetch: () => void; active: boolean }> = ({
+  data,
+  refetch,
+  active,
+  ...props
+}) => {
+  const [editMode, setEditMode] = React.useState(false);
+  const [name, setName] = React.useState(data.name);
+
+  const onSubmit = () => {
+    toast
+      .promise(updateJson(data._id, { name }), {
+        loading: "Updating document...",
+        error: "Error occured while updating document!",
+        success: `Renamed document to ${name}`,
+      })
+      .then(refetch);
+
+    setEditMode(false);
+  };
+
+  const onDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+    e.preventDefault();
+
+    toast
+      .promise(deleteJson(data._id), {
+        loading: "Deleting JSON file...",
+        error: "An error occured while deleting the file!",
+        success: `Deleted ${name}!`,
+      })
+      .then(refetch);
+  };
+
+  return (
+    <StyledJsonCard
+      href={`?json=${data._id}`}
+      as={editMode ? "div" : "a"}
+      active={active}
+      {...props}
+    >
+      <StyledInfo>
+        {editMode ? (
+          <form onSubmit={onSubmit}>
+            <StyledNameInput
+              value={name}
+              onChange={e => setName(e.currentTarget.value)}
+              onClick={e => e.preventDefault()}
+              autoFocus
+            />
+            <input type="submit" hidden />
+          </form>
+        ) : (
+          <StyledTitle
+            onClick={e => {
+              e.preventDefault();
+              setEditMode(true);
+            }}
+          >
+            <span>{name}</span>
+            <AiOutlineEdit />
+          </StyledTitle>
+        )}
+        <StyledDetils>
+          {data.private ? <AiOutlineLock /> : <AiOutlineUnlock />}
+          Last modified {dayjs(data.updatedAt).fromNow()}
+        </StyledDetils>
+      </StyledInfo>
+      <StyledDeleteButton onClick={onDeleteClick}>
+        <FaTrash />
+      </StyledDeleteButton>
+    </StyledJsonCard>
+  );
+};
+
+const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => {
+  const { replace } = useRouter();
+  const isPremium = useUser(state => state.isPremium());
+  const getJson = useJson(state => state.getJson);
+  const setHasChanges = useJson(state => state.setHasChanges);
+
+  const onCreate = async () => {
+    try {
+      toast.loading("Saving JSON...", { id: "jsonSave" });
+      const res = await saveJson({ data: getJson() });
+
+      if (res.errors && res.errors.items.length > 0) throw res.errors;
+
+      toast.success("JSON saved to cloud", { id: "jsonSave" });
+      setHasChanges(false);
+      replace({ query: { json: res.data._id } });
+    } catch (error: any) {
+      if (error?.items?.length > 0) {
+        return toast.error(error.items[0].message, { id: "jsonSave", duration: 7000 });
+      }
+      toast.error("Failed to save JSON!", { id: "jsonSave" });
+    }
+  };
+
+  if (!isPremium && reachedLimit)
+    return (
+      <StyledJsonCard href="/pricing" create>
+        <StyledCreateWrapper>
+          <IoRocketSharp size="18" />
+          You reached max limit, upgrade to premium for more!
+        </StyledCreateWrapper>
+      </StyledJsonCard>
+    );
+
+  return (
+    <StyledJsonCard onClick={onCreate} create>
+      <StyledCreateWrapper>
+        <AiOutlinePlus size="24" />
+        Create New JSON
+      </StyledCreateWrapper>
+    </StyledJsonCard>
+  );
+};
+
+export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+  const { isReady, query } = useRouter();
+
+  const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), {
+    enabled: isReady && visible,
+  });
+
+  return (
+    <StyledModal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Saved On The Cloud</Modal.Header>
+      <Modal.Content>
+        <StyledModalContent>
+          {isFetching ? (
+            <Spinner />
+          ) : (
+            <>
+              <CreateCard reachedLimit={data ? data?.data.result.length > 15 : false} />
+              {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}>
+        <StyledInfoText>
+          <AiOutlineInfoCircle />
+          Cloud Save feature is for ease-of-access only and not recommended to store sensitive data,
+          we don&apos;t guarantee protection of your data.
+        </StyledInfoText>
+      </Modal.Controls>
+    </StyledModal>
+  );
+};

+ 6 - 6
src/containers/Modals/DownloadModal/index.tsx

@@ -7,7 +7,7 @@ import { FiCopy, FiDownload } from "react-icons/fi";
 import { Button } from "src/components/Button";
 import { Input } from "src/components/Input";
 import { Modal, ModalProps } from "src/components/Modal";
-import useConfig from "src/store/useConfig";
+import useGraph from "src/store/useGraph";
 import styled from "styled-components";
 
 const ColorPickerStyles: Partial<TwitterPickerStylesProps> = {
@@ -93,7 +93,7 @@ const StyledColorIndicator = styled.div<{ color: string }>`
 `;
 
 export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setConfig = useConfig(state => state.setConfig);
+  const togglePerfMode = useGraph(state => state.togglePerfMode);
   const [fileDetails, setFileDetails] = React.useState({
     filename: "jsoncrack.com",
     backgroundColor: "transparent",
@@ -103,7 +103,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
   const clipboardImage = async () => {
     try {
       toast.loading("Copying to clipboard...", { id: "toastClipboard" });
-      setConfig("performanceMode", false);
+      togglePerfMode(false);
 
       const imageElement = document.querySelector("svg[id*='ref']") as HTMLElement;
 
@@ -126,14 +126,14 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
     } finally {
       toast.dismiss("toastClipboard");
       setVisible(false);
-      setConfig("performanceMode", true);
+      togglePerfMode(true);
     }
   };
 
   const exportAsImage = async () => {
     try {
       toast.loading("Downloading...", { id: "toastDownload" });
-      setConfig("performanceMode", false);
+      togglePerfMode(false);
 
       const imageElement = document.querySelector("svg[id*='ref']") as HTMLElement;
 
@@ -148,7 +148,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
     } finally {
       toast.dismiss("toastDownload");
       setVisible(false);
-      setConfig("performanceMode", true);
+      togglePerfMode(true);
     }
   };
 

+ 0 - 76
src/containers/Modals/GoalsModal/index.tsx

@@ -1,76 +0,0 @@
-import React from "react";
-import { useRouter } from "next/router";
-import { FaHeart, FaTwitter } from "react-icons/fa";
-import { Button } from "src/components/Button";
-import { Modal } from "src/components/Modal";
-import styled from "styled-components";
-
-const StyledTitle = styled.p`
-  display: flex;
-  align-items: center;
-  color: ${({ theme }) => theme.TEXT_POSITIVE};
-  flex: 1;
-  font-weight: 700;
-  font-family: "Catamaran", sans-serif;
-  margin-top: 0;
-
-  &::after {
-    background: ${({ theme }) => theme.TEXT_POSITIVE};
-    height: 1px;
-
-    content: "";
-    -webkit-box-flex: 1;
-    -ms-flex: 1 1 auto;
-    flex: 1 1 auto;
-    margin-left: 4px;
-    opacity: 0.6;
-  }
-`;
-
-const ButtonsWrapper = styled.div`
-  display: flex;
-  padding: 40px 0 0;
-  gap: 20px;
-`;
-
-export const GoalsModal = ({ visible, setVisible }) => {
-  const { push } = useRouter();
-
-  return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Help JSON Crack&apos;s Goals</Modal.Header>
-      <Modal.Content>
-        <StyledTitle>OUR GOAL</StyledTitle>
-        <b>JSON Crack&apos;s Goal</b> is to keep the service completely free and open
-        source for everyone! For the contiunity of our service and keep the new
-        updates coming we need your support to make that possible ❤️
-        <ButtonsWrapper>
-          <Button
-            href="https://github.com/sponsors/AykutSarac"
-            target="_blank"
-            rel="me"
-            status="DANGER"
-            block
-            link
-          >
-            <FaHeart />
-            Sponsor
-          </Button>
-          <Button
-            href={`https://twitter.com/intent/tweet?button=&url=${encodeURIComponent(
-              "https://jsoncrack.com"
-            )}&text=Looking+to+understand+or+explore+some+JSON?+Just+paste+or+upload+to+visualize+it+as+a+graph+with+JSON+Crack+😍&button=`}
-            rel="noreferrer"
-            status="SECONDARY"
-            block
-            link
-          >
-            <FaTwitter />
-            Share on Twitter
-          </Button>
-        </ButtonsWrapper>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible} />
-    </Modal>
-  );
-};

+ 5 - 8
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 useConfig from "src/store/useConfig";
+import useJson from "src/store/useJson";
 import styled from "styled-components";
 
 const StyledModalContent = styled(Modal.Content)`
@@ -33,6 +33,7 @@ const StyledUploadWrapper = styled.label`
 `;
 
 const StyledFileName = styled.span`
+  padding-top: 14px;
   color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
 `;
 
@@ -42,7 +43,7 @@ const StyledUploadMessage = styled.h3`
 `;
 
 export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const setJson = useConfig(state => state.setJson);
+  const setJson = useJson(state => state.setJson);
   const [url, setURL] = React.useState("");
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
 
@@ -58,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!"))
@@ -99,11 +100,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
         </StyledUploadWrapper>
       </StyledModalContent>
       <Modal.Controls setVisible={setVisible}>
-        <Button
-          status="SECONDARY"
-          onClick={handleImportFile}
-          disabled={!(jsonFile || url)}
-        >
+        <Button status="SECONDARY" onClick={handleImportFile} disabled={!(jsonFile || url)}>
           Import
         </Button>
       </Modal.Controls>

+ 26 - 0
src/containers/Modals/LoginModal/index.tsx

@@ -0,0 +1,26 @@
+import React from "react";
+import { AiOutlineGoogle } from "react-icons/ai";
+import { altogic } from "src/api/altogic";
+import { Button } from "src/components/Button";
+import { Modal, ModalProps } from "src/components/Modal";
+
+export const LoginModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
+  const handleLoginClick = () => {
+    altogic.auth.signInWithProvider("google");
+  };
+
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Login</Modal.Header>
+      <Modal.Content>
+        <h2>Welcome Back!</h2>
+        <p>Login to unlock full potential of JSON Crack!</p>
+        <Button onClick={handleLoginClick} status="SECONDARY" block>
+          <AiOutlineGoogle size={24} />
+          Login with Google
+        </Button>
+      </Modal.Content>
+      <Modal.Controls setVisible={setVisible} />
+    </Modal>
+  );
+};

+ 1 - 3
src/containers/Modals/NodeModal/index.tsx

@@ -26,9 +26,7 @@ const StyledTextarea = styled.textarea`
 `;
 
 export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps) => {
-  const nodeData = Array.isArray(selectedNode)
-    ? Object.fromEntries(selectedNode)
-    : selectedNode;
+  const nodeData = Array.isArray(selectedNode) ? Object.fromEntries(selectedNode) : selectedNode;
 
   const handleClipboard = () => {
     toast.success("Content copied to clipboard!");

+ 27 - 22
src/containers/Modals/SettingsModal/index.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { Modal } from "src/components/Modal";
+import { Modal, ModalProps } from "src/components/Modal";
 import Toggle from "src/components/Toggle";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
@@ -16,18 +16,26 @@ const StyledModalWrapper = styled.div`
   gap: 20px;
 `;
 
-export const SettingsModal: React.FC<{
-  visible: boolean;
-  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
-}> = ({ visible, setVisible }) => {
+export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   const lightmode = useStored(state => state.lightmode);
   const setLightTheme = useStored(state => state.setLightTheme);
-  const [toggleHideCollapse, hideCollapse] = useStored(
-    state => [state.toggleHideCollapse, state.hideCollapse],
-    shallow
-  );
-  const [toggleHideChildrenCount, hideChildrenCount] = useStored(
-    state => [state.toggleHideChildrenCount, state.hideChildrenCount],
+
+  const [
+    toggleHideCollapse,
+    toggleChildrenCount,
+    toggleImagePreview,
+    hideCollapse,
+    childrenCount,
+    imagePreview,
+  ] = useStored(
+    state => [
+      state.toggleHideCollapse,
+      state.toggleChildrenCount,
+      state.toggleImagePreview,
+      state.hideCollapse,
+      state.childrenCount,
+      state.imagePreview,
+    ],
     shallow
   );
 
@@ -36,20 +44,17 @@ export const SettingsModal: React.FC<{
       <Modal.Header>Settings</Modal.Header>
       <Modal.Content>
         <StyledModalWrapper>
+          <StyledToggle onChange={toggleImagePreview} checked={imagePreview}>
+            Live Image Preview
+          </StyledToggle>
           <StyledToggle onChange={toggleHideCollapse} checked={hideCollapse}>
-            Hide Collapse/Expand Button
+            Display Collapse/Expand Button
           </StyledToggle>
-          <StyledToggle
-            onChange={toggleHideChildrenCount}
-            checked={hideChildrenCount}
-          >
-            Hide Children Count
+          <StyledToggle onChange={toggleChildrenCount} checked={childrenCount}>
+            Display Children Count
           </StyledToggle>
-          <StyledToggle
-            onChange={() => setLightTheme(!lightmode)}
-            checked={lightmode}
-          >
-            Enable Light Theme
+          <StyledToggle onChange={() => setLightTheme(!lightmode)} checked={lightmode}>
+            Light Theme
           </StyledToggle>
         </StyledModalWrapper>
       </Modal.Content>

+ 24 - 63
src/containers/Modals/ShareModal/index.tsx

@@ -1,26 +1,11 @@
 import React from "react";
 import { useRouter } from "next/router";
-import { compress } from "compress-json";
 import toast from "react-hot-toast";
-import { BiErrorAlt } from "react-icons/bi";
 import { Button } from "src/components/Button";
 import { Input } from "src/components/Input";
 import { Modal, ModalProps } from "src/components/Modal";
-import { baseURL } from "src/constants/data";
-import useConfig from "src/store/useConfig";
 import styled from "styled-components";
 
-const StyledWarning = styled.p``;
-
-const StyledErrorWrapper = styled.div`
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  color: ${({ theme }) => theme.TEXT_DANGER};
-  font-weight: 600;
-`;
-
 const StyledFlex = styled.div`
   display: flex;
   gap: 12px;
@@ -45,21 +30,8 @@ const StyledContainer = styled.div`
 `;
 
 export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const json = useConfig(state => state.json);
-  const [encodedJson, setEncodedJson] = React.useState("");
-  const navigate = useRouter();
-
-  const embedText = `<iframe id="jsoncrackEmbed" src="${baseURL}/widget></iframe>`;
-  const shareURL = `${baseURL}/editor?json=${encodedJson}`;
-
-  React.useEffect(() => {
-    if (visible) {
-      const jsonEncode = compress(JSON.parse(json));
-      const jsonString = JSON.stringify(jsonEncode);
-
-      setEncodedJson(encodeURIComponent(jsonString));
-    }
-  }, [json, visible]);
+  const { push, query } = useRouter();
+  const shareURL = `https://jsoncrack.com/editor?json=${query.json}`;
 
   const handleShare = (value: string) => {
     navigator.clipboard.writeText(value);
@@ -67,43 +39,32 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
     setVisible(false);
   };
 
+  const onEmbedClick = () => {
+    push("/docs");
+    setVisible(false);
+  };
+
   return (
     <Modal visible={visible} setVisible={setVisible}>
       <Modal.Header>Create a Share Link</Modal.Header>
       <Modal.Content>
-        {encodedJson.length > 5000 ? (
-          <StyledErrorWrapper>
-            <BiErrorAlt size={60} />
-            <StyledWarning>
-              Link size exceeds 5000 characters, unable to generate link for file of
-              this size!
-            </StyledWarning>
-          </StyledErrorWrapper>
-        ) : (
-          <>
-            <StyledContainer>
-              Share Link
-              <StyledFlex>
-                <Input value={shareURL} type="url" readOnly />
-                <Button status="SECONDARY" onClick={() => handleShare(shareURL)}>
-                  Copy
-                </Button>
-              </StyledFlex>
-            </StyledContainer>
-            <StyledContainer>
-              Embed into your website
-              <StyledFlex>
-                <Button
-                  status="SUCCESS"
-                  onClick={() => navigate.push("/embed")}
-                  block
-                >
-                  Learn How to Embed
-                </Button>
-              </StyledFlex>
-            </StyledContainer>
-          </>
-        )}
+        <StyledContainer>
+          Share Link
+          <StyledFlex>
+            <Input value={shareURL} type="url" readOnly />
+            <Button status="SECONDARY" onClick={() => handleShare(shareURL)}>
+              Copy
+            </Button>
+          </StyledFlex>
+        </StyledContainer>
+        <StyledContainer>
+          Embed into your website
+          <StyledFlex>
+            <Button status="SUCCESS" onClick={onEmbedClick} block>
+              Learn How to Embed
+            </Button>
+          </StyledFlex>
+        </StyledContainer>
       </Modal.Content>
       <Modal.Controls setVisible={setVisible}></Modal.Controls>
     </Modal>

+ 128 - 0
src/containers/PricingCards/index.tsx

@@ -0,0 +1,128 @@
+import React from "react";
+import { Button } from "src/components/Button";
+import styled from "styled-components";
+
+const StyledSectionBody = styled.div`
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-template-rows: 1fr;
+  gap: 50px;
+  align-items: center;
+  justify-content: space-between;
+  background: rgba(181, 116, 214, 0.23);
+  width: 80%;
+  margin: 5% auto 0;
+  border-radius: 6px;
+  padding: 50px;
+
+  @media only screen and (max-width: 768px) {
+    grid-template-columns: 1fr;
+    grid-template-rows: 1fr 1fr;
+    padding: 20px;
+  }
+`;
+
+const StyledPricingCard = styled.div<{ premium?: boolean }>`
+  padding: 6px;
+  width: 100%;
+  height: 100%;
+
+  ${({ premium }) =>
+    premium
+      ? `
+      background: rgba(255, 5, 214, 0.19);
+border-radius: 4px;
+box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
+backdrop-filter: blur(5px);
+-webkit-backdrop-filter: blur(5px);
+border: 1px solid rgba(255, 5, 214, 0.74);`
+      : `background: rgba(255, 255, 255, 0.1);
+  border-radius: 4px;
+  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
+  backdrop-filter: blur(5px);
+  -webkit-backdrop-filter: blur(5px);
+  border: 1px solid rgba(255, 255, 255, 0.3);`};
+`;
+
+const StyledPricingCardTitle = styled.h2`
+  text-align: center;
+  font-weight: 800;
+  font-size: 24px;
+`;
+
+const StyledPricingCardPrice = styled.h3`
+  text-align: center;
+  font-weight: 600;
+  font-size: 24px;
+  color: ${({ theme }) => theme.SILVER};
+`;
+
+const StyledPricingCardDetails = styled.ul`
+  color: ${({ theme }) => theme.FULL_WHITE};
+  line-height: 2.3;
+  padding: 20px;
+`;
+
+const StyledPricingCardDetailsItem = styled.li`
+  font-weight: 500;
+
+  @media only screen and (max-width: 768px) {
+    font-size: 14px;
+  }
+`;
+
+const StyledButton = styled(Button)`
+  border: 1px solid white;
+`;
+
+const StyledPricingSection = styled.section`
+  margin: 0 auto;
+
+  h1 {
+    text-align: center;
+    padding-bottom: 25px;
+  }
+`;
+
+export const PricingCards = () => {
+  return (
+    <StyledPricingSection>
+      <h1>Unlock Full Potential of JSON Crack</h1>
+      <StyledSectionBody>
+        <StyledPricingCard>
+          <StyledPricingCardTitle>Free</StyledPricingCardTitle>
+          <StyledPricingCardDetails>
+            <StyledPricingCardDetailsItem>Store up to 15 files</StyledPricingCardDetailsItem>
+            <StyledPricingCardDetailsItem>
+              Create short-links for saved JSON files
+            </StyledPricingCardDetailsItem>
+            <StyledPricingCardDetailsItem>Embed saved JSON instantly</StyledPricingCardDetailsItem>
+          </StyledPricingCardDetails>
+        </StyledPricingCard>
+        <StyledPricingCard premium>
+          <StyledPricingCardTitle>Premium</StyledPricingCardTitle>
+          <StyledPricingCardPrice>$5/mo</StyledPricingCardPrice>
+          <StyledPricingCardDetails>
+            <StyledPricingCardDetailsItem>
+              Create and share up to 200 files
+            </StyledPricingCardDetailsItem>
+            <StyledPricingCardDetailsItem>Store private JSON</StyledPricingCardDetailsItem>
+            <StyledPricingCardDetailsItem>
+              Get access to JSON Crack API to generate JSON remotely
+            </StyledPricingCardDetailsItem>
+            <StyledPricingCardDetailsItem>Everything in previous tier</StyledPricingCardDetailsItem>
+          </StyledPricingCardDetails>
+          <StyledButton
+            href="https://www.patreon.com/jsoncrack"
+            target="_blank"
+            status="SUCCESS"
+            block
+            link
+          >
+            GET IT NOW!
+          </StyledButton>
+        </StyledPricingCard>
+      </StyledSectionBody>
+    </StyledPricingSection>
+  );
+};

+ 11 - 15
src/hooks/useFocusNode.tsx

@@ -1,14 +1,10 @@
 import React from "react";
-import {
-  searchQuery,
-  cleanupHighlight,
-  highlightMatchedNodes,
-} from "src/utils/search";
-import useConfig from "../store/useConfig";
+import useGraph from "src/store/useGraph";
+import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/utils/search";
 
 export const useFocusNode = () => {
-  const setConfig = useConfig(state => state.setConfig);
-  const zoomPanPinch = useConfig(state => state.zoomPanPinch);
+  const togglePerfMode = useGraph(state => state.togglePerfMode);
+  const zoomPanPinch = useGraph(state => state.zoomPanPinch);
   const [selectedNode, setSelectedNode] = React.useState(0);
   const [content, setContent] = React.useState({
     value: "",
@@ -18,14 +14,14 @@ export const useFocusNode = () => {
   const skip = () => setSelectedNode(current => current + 1);
 
   React.useEffect(() => {
-    setConfig("performanceMode", !content.value.length);
+    togglePerfMode(!content.value.length);
 
     const debouncer = setTimeout(() => {
       setContent(val => ({ ...val, debounced: content.value }));
     }, 800);
 
     return () => clearTimeout(debouncer);
-  }, [content.value, setConfig]);
+  }, [content.value, togglePerfMode]);
 
   React.useEffect(() => {
     if (!zoomPanPinch) return;
@@ -39,18 +35,18 @@ export const useFocusNode = () => {
     cleanupHighlight();
 
     if (ref && matchedNode && matchedNode.parentElement) {
-      const newScale = 1;
+      const newScale = 0.4;
       const x = Number(matchedNode.getAttribute("data-x"));
       const y = Number(matchedNode.getAttribute("data-y"));
 
       const newPositionX =
         (ref.offsetLeft - x) * newScale +
-        ref.clientWidth / 2 -
-        matchedNode.getBoundingClientRect().width / 2;
+        ref.clientWidth / 10 -
+        matchedNode.getBoundingClientRect().width / 10;
       const newPositionY =
         (ref.offsetLeft - y) * newScale +
-        ref.clientHeight / 2 -
-        matchedNode.getBoundingClientRect().height / 2;
+        ref.clientHeight / 10 -
+        matchedNode.getBoundingClientRect().height / 10;
 
       highlightMatchedNodes(matchedNodes, selectedNode);
 

+ 33 - 0
src/hooks/useHideNodes.tsx

@@ -0,0 +1,33 @@
+import React from "react";
+import useGraph from "src/store/useGraph";
+
+const useHideNodes = () => {
+  const collapsedNodes = useGraph(state => state.collapsedNodes);
+  const collapsedEdges = useGraph(state => state.collapsedEdges);
+
+  const nodeList = React.useMemo(
+    () => collapsedNodes.map(id => `[id$="node-${id}"]`),
+    [collapsedNodes]
+  );
+  const edgeList = React.useMemo(
+    () => collapsedEdges.map(id => `[class$="edge-${id}"]`),
+    [collapsedEdges]
+  );
+
+  const checkNodes = () => {
+    const hiddenItems = document.querySelectorAll(".hide");
+    hiddenItems.forEach(item => item.classList.remove("hide"));
+
+    if (nodeList.length > 1) {
+      const selectedNodes = document.querySelectorAll(nodeList.join(","));
+      const selectedEdges = document.querySelectorAll(edgeList.join(","));
+
+      selectedNodes.forEach(node => node.classList.add("hide"));
+      selectedEdges.forEach(edge => edge.classList.add("hide"));
+    }
+  };
+
+  return { checkNodes };
+};
+
+export default useHideNodes;

+ 0 - 35
src/pages/Embed/index.tsx

@@ -1,35 +0,0 @@
-import React from "react";
-import Head from "next/head";
-import styled from "styled-components";
-
-const StyledPageWrapper = styled.iframe`
-  height: 100vh;
-  width: 100%;
-  border: none;
-`;
-
-const Embed = () => {
-  return (
-    <>
-      <Head>
-        <title>Creating JSON Crack Embed | JSON Crack</title>
-        <meta
-          name="description"
-          content="Embedding JSON Crack tutorial into your websites."
-        />
-      </Head>
-      <StyledPageWrapper
-        scrolling="no"
-        title="Untitled"
-        src="https://codepen.io/AykutSarac/embed/PoawZYo?default-tab=html%2Cresult"
-        loading="lazy"
-      >
-        See the Pen <a href="https://codepen.io/AykutSarac/pen/PoawZYo">Untitled</a>{" "}
-        by Aykut Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
-        <a href="https://codepen.io">CodePen</a>.
-      </StyledPageWrapper>
-    </>
-  );
-};
-
-export default Embed;

+ 0 - 135
src/pages/Widget/index.tsx

@@ -1,135 +0,0 @@
-import React from "react";
-import dynamic from "next/dynamic";
-import { useRouter } from "next/router";
-import toast from "react-hot-toast";
-import { baseURL } from "src/constants/data";
-import { NodeModal } from "src/containers/Modals/NodeModal";
-import useGraph from "src/store/useGraph";
-import { parser } from "src/utils/jsonParser";
-import styled from "styled-components";
-
-const Graph = dynamic<any>(() => import("src/components/Graph").then(c => c.Graph), {
-  ssr: false,
-});
-
-const StyledAttribute = styled.a`
-  position: fixed;
-  bottom: 0;
-  right: 0;
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-  background: ${({ theme }) => theme.SILVER_DARK};
-  padding: 4px 8px;
-  font-size: 14px;
-  font-weight: 500;
-  border-radius: 3px 0 0 0;
-  opacity: 0.8;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 12px;
-  }
-`;
-
-function inIframe() {
-  try {
-    return window.self !== window.top;
-  } catch (e) {
-    return true;
-  }
-}
-
-interface EmbedMessage {
-  data: {
-    json?: string;
-    options?: any;
-  };
-}
-
-const StyledDeprecated = styled.div`
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  width: 100%;
-  height: 100vh;
-
-  a {
-    text-decoration: underline;
-  }
-`;
-
-const WidgetPage = () => {
-  const { query, push } = useRouter();
-
-  const [isModalVisible, setModalVisible] = React.useState(false);
-  const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
-
-  const collapsedNodes = useGraph(state => state.collapsedNodes);
-  const collapsedEdges = useGraph(state => state.collapsedEdges);
-  const loading = useGraph(state => state.loading);
-  const setGraphValue = useGraph(state => state.setGraphValue);
-
-  const openModal = React.useCallback(() => setModalVisible(true), []);
-
-  React.useEffect(() => {
-    const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
-    const edgeList = collapsedEdges.map(id => `[class$="edge-${id}"]`);
-
-    const hiddenItems = document.querySelectorAll(".hide");
-    hiddenItems.forEach(item => item.classList.remove("hide"));
-
-    if (nodeList.length) {
-      const selectedNodes = document.querySelectorAll(nodeList.join(","));
-      const selectedEdges = document.querySelectorAll(edgeList.join(","));
-
-      selectedNodes.forEach(node => node.classList.add("hide"));
-      selectedEdges.forEach(edge => edge.classList.add("hide"));
-    }
-
-    if (!inIframe()) push("/");
-  }, [collapsedNodes, collapsedEdges, loading, push]);
-
-  React.useEffect(() => {
-    const handler = (event: EmbedMessage) => {
-      try {
-        if (!event.data?.json) return;
-        const { nodes, edges } = parser(event.data.json);
-
-        setGraphValue("nodes", nodes);
-        setGraphValue("edges", edges);
-      } catch (error) {
-        console.error(error);
-        toast.error("Invalid JSON!");
-      }
-    };
-
-    window.addEventListener("message", handler);
-    return () => window.removeEventListener("message", handler);
-  }, [setGraphValue]);
-
-  if (query.json)
-    return (
-      <StyledDeprecated>
-        <h1>⚠️ Deprecated ⚠️</h1>
-        <br />
-        <a href="https://jsoncrack.com/embed" target="_blank" rel="noreferrer">
-          https://jsoncrack.com/embed
-        </a>
-      </StyledDeprecated>
-    );
-
-  return (
-    <>
-      <Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget />
-      <NodeModal
-        selectedNode={selectedNode}
-        visible={isModalVisible}
-        closeModal={() => setModalVisible(false)}
-      />
-      <StyledAttribute href={`${baseURL}/editor`} target="_blank" rel="noreferrer">
-        jsoncrack.com
-      </StyledAttribute>
-    </>
-  );
-};
-
-export default WidgetPage;

+ 22 - 33
src/pages/_app.tsx

@@ -1,63 +1,52 @@
 import React from "react";
 import type { AppProps } from "next/app";
-import { useRouter } from "next/router";
 import { init } from "@sentry/nextjs";
-import { decompress } from "compress-json";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { Toaster } from "react-hot-toast";
 import { GoogleAnalytics } from "src/components/GoogleAnalytics";
-import { SupportButton } from "src/components/SupportButton";
 import GlobalStyle from "src/constants/globalStyle";
 import { darkTheme, lightTheme } from "src/constants/theme";
-import useConfig from "src/store/useConfig";
+import { ModalController } from "src/containers/ModalController";
 import useStored from "src/store/useStored";
-import { isValidJson } from "src/utils/isValidJson";
 import { ThemeProvider } from "styled-components";
 
 if (process.env.NODE_ENV !== "development") {
   init({
     dsn: "https://[email protected]/6495191",
-    tracesSampleRate: 0.5,
+    tracesSampleRate: 0.25,
   });
 }
 
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      refetchOnWindowFocus: false,
+      retry: false,
+    },
+  },
+});
+
 function JsonCrack({ Component, pageProps }: AppProps) {
-  const { query, pathname } = useRouter();
+  const [isReady, setReady] = React.useState(false);
   const lightmode = useStored(state => state.lightmode);
-  const setJson = useConfig(state => state.setJson);
-  const [isRendered, setRendered] = React.useState(false);
-
-  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(() => {
-    setRendered(true);
+    setReady(true);
   }, []);
 
-  if (isRendered)
+  if (isReady)
     return (
-      <>
+      <QueryClientProvider client={queryClient}>
         <GoogleAnalytics />
         <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
           <GlobalStyle />
           <Component {...pageProps} />
           <Toaster
-            position="bottom-right"
+            position="top-right"
             containerStyle={{
-              right: 60,
+              top: 40,
+              right: 6,
+              fontSize: 14,
             }}
             toastOptions={{
               style: {
@@ -66,9 +55,9 @@ function JsonCrack({ Component, pageProps }: AppProps) {
               },
             }}
           />
-          <SupportButton />
+          <ModalController />
         </ThemeProvider>
-      </>
+      </QueryClientProvider>
     );
 }
 

+ 3 - 6
src/pages/_document.tsx

@@ -15,16 +15,13 @@ class MyDocument extends Document {
           <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
-            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"
+            href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500&family=Roboto:wght@400;500;700&display=swap"
             rel="stylesheet"
             crossOrigin="anonymous"
           />
+          <link rel="preload" href="assets/Mona-Sans.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
         </Head>
         <body>
           <Main />

+ 1 - 1
src/pages/_error.tsx

@@ -15,7 +15,7 @@ const StyledNotFound = styled.div`
 const StyledMessage = styled.h4`
   color: ${({ theme }) => theme.FULL_WHITE};
   font-size: 25px;
-  font-weight: 600;
+  font-weight: 800;
   margin: 10px 0;
 `;
 

+ 187 - 0
src/pages/docs.tsx

@@ -0,0 +1,187 @@
+import React from "react";
+import dynamic from "next/dynamic";
+import Head from "next/head";
+import { materialDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
+import { Button } from "src/components/Button";
+import { Footer } from "src/components/Footer";
+import styled from "styled-components";
+
+const SyntaxHighlighter = dynamic(
+  () => import("react-syntax-highlighter").then(c => c.PrismAsync),
+  {
+    ssr: false,
+  }
+);
+
+const StyledFrame = styled.iframe`
+  border: none;
+  width: 80%;
+  flex: 500px;
+  margin: 3% auto;
+`;
+
+const StyledPage = styled.div`
+  padding: 5%;
+`;
+
+const StyledContent = styled.section`
+  margin-top: 20px;
+  background: rgba(181, 116, 214, 0.23);
+  padding: 16px;
+  border-radius: 6px;
+`;
+
+const StyledDescription = styled.div``;
+
+const StyledContentBody = styled.div`
+  display: flex;
+  flex-direction: column;
+  flex-wrap: wrap;
+  gap: 15px;
+  line-height: 1.8;
+
+  ${StyledDescription} {
+    flex: 1;
+  }
+`;
+
+const StyledHighlight = styled.span<{ link?: boolean; alert?: boolean }>`
+  text-align: left;
+  white-space: nowrap;
+  color: ${({ theme, link, alert }) =>
+    alert ? theme.DANGER : link ? theme.BLURPLE : theme.TEXT_POSITIVE};
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  border-radius: 4px;
+  font-weight: 500;
+  padding: 4px;
+  font-size: 14px;
+  margin: ${({ alert }) => alert && "8px 0"};
+`;
+
+const Docs = () => {
+  return (
+    <>
+      <Head>
+        <title>Creating JSON Crack Embed | JSON Crack</title>
+        <meta name="description" content="Embedding JSON Crack tutorial into your websites." />
+      </Head>
+      <StyledPage>
+        <Button href="/" link status="SECONDARY">
+          &lt; Go Back
+        </Button>
+        <h1>Documentation</h1>
+        <StyledContent>
+          <h2># Fetching from URL</h2>
+          <StyledContentBody>
+            <StyledDescription>
+              By adding <StyledHighlight>?json=https://catfact.ninja/fact</StyledHighlight> query at
+              the end of iframe src you will be able to fetch from URL at widgets without additional
+              scripts. This applies to editor page as well, the following link will fetch the url at
+              the editor:{" "}
+              <StyledHighlight
+                as="a"
+                href="https://jsoncrack.com/editor?json=https://catfact.ninja/fact"
+                link
+              >
+                https://jsoncrack.com/editor?json=https://catfact.ninja/fact
+              </StyledHighlight>
+            </StyledDescription>
+
+            <StyledFrame
+              scrolling="no"
+              title="Untitled"
+              src="https://codepen.io/AykutSarac/embed/KKBpWVR?default-tab=html%2Cresult"
+              loading="eager"
+            >
+              See the Pen <a href="https://codepen.io/AykutSarac/pen/KKBpWVR">Untitled</a> by Aykut
+              Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
+              <a href="https://codepen.io">CodePen</a>.
+            </StyledFrame>
+          </StyledContentBody>
+        </StyledContent>
+        <StyledContent>
+          <h2># Embed Saved JSON</h2>
+          <StyledContentBody>
+            <StyledDescription>
+              Just like fetching from URL above, you can embed saved public json by adding the json
+              id to &quot;json&quot; query{" "}
+              <StyledHighlight>?json=639b65c5a82efc29a24b2de2</StyledHighlight>
+            </StyledDescription>
+            <StyledFrame
+              scrolling="no"
+              title="Untitled"
+              src="https://codepen.io/AykutSarac/embed/vYaORgM?default-tab=html%2Cresult"
+              loading="lazy"
+            >
+              See the Pen <a href="https://codepen.io/AykutSarac/pen/vYaORgM">Untitled</a> by Aykut
+              Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
+              <a href="https://codepen.io">CodePen</a>.
+            </StyledFrame>
+          </StyledContentBody>
+        </StyledContent>
+        <StyledContent>
+          <h2># Communicating with API</h2>
+          <h3>◼︎ Post Message to Embed</h3>
+          <StyledContentBody>
+            <StyledDescription>
+              Communicating with the embed is possible with{" "}
+              <StyledHighlight
+                as="a"
+                href="https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage"
+                link
+              >
+                MessagePort
+              </StyledHighlight>
+              , you should pass an object consist of &quot;json&quot; and &quot;options&quot; key
+              where json is a string and options is an object that may contain the following:
+              <SyntaxHighlighter language="markdown" style={materialDark} showLineNumbers={true}>
+                {`{\n\ttheme: "light" | "dark",\n\tdirection: "TOP" | "RIGHT" | "DOWN" | "LEFT"\n}`}
+              </SyntaxHighlighter>
+            </StyledDescription>
+
+            <StyledFrame
+              scrolling="no"
+              title="Untitled"
+              src="https://codepen.io/AykutSarac/embed/rNrVyWP?default-tab=html%2Cresult"
+              loading="lazy"
+            >
+              See the Pen <a href="https://codepen.io/AykutSarac/pen/rNrVyWP">Untitled</a> by Aykut
+              Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
+              <a href="https://codepen.io">CodePen</a>.
+            </StyledFrame>
+          </StyledContentBody>
+        </StyledContent>
+        <StyledContent>
+          <h3>◼︎ On Page Load</h3>
+          <StyledContentBody>
+            <StyledDescription>
+              <StyledHighlight as="div" alert>
+                ⚠️ <b>Important!</b> - iframe should be defined before the script tag
+              </StyledHighlight>
+              <StyledHighlight as="div" alert>
+                ⚠️ <b>Note</b> - postMessage should be delayed using setTimeout
+              </StyledHighlight>
+              To display JSON on load event, you should post json into iframe using it&apos;s onload
+              event like in the example. Make sure to use{" "}
+              <StyledHighlight>setTimeout</StyledHighlight> when loading data and set a time around
+              500ms otherwise it won&apos;t work.
+            </StyledDescription>
+            <StyledFrame
+              scrolling="no"
+              title="Untitled"
+              src="https://codepen.io/AykutSarac/embed/QWBbpqx?default-tab=html%2Cresult"
+              loading="lazy"
+            >
+              See the Pen <a href="https://codepen.io/AykutSarac/pen/QWBbpqx">Untitled</a> by Aykut
+              Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
+              <a href="https://codepen.io">CodePen</a>.
+            </StyledFrame>
+          </StyledContentBody>
+        </StyledContent>
+      </StyledPage>
+      <Footer />
+    </>
+  );
+};
+
+export default Docs;

+ 24 - 5
src/pages/Editor/index.tsx → src/pages/editor.tsx

@@ -1,13 +1,18 @@
 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/store/useJson";
+import useUser from "src/store/useUser";
 import styled from "styled-components";
 
 export const StyledPageWrapper = styled.div`
   display: flex;
   flex-direction: row;
-  height: 100vh;
+  height: calc(100vh - 28px);
   width: 100%;
 
   @media only screen and (max-width: 768px) {
@@ -24,14 +29,27 @@ export const StyledEditorWrapper = styled.div`
 `;
 
 const EditorPage: React.FC = () => {
+  const { isReady, query } = useRouter();
+  const checkSession = useUser(state => state.checkSession);
+  const fetchJson = useJson(state => state.fetchJson);
+  const loading = useJson(state => state.loading);
+
+  React.useEffect(() => {
+    // 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>
       <Head>
         <title>Editor | JSON Crack</title>
-        <meta
-          name="description"
-          content="View your JSON data in graphs instantly."
-        />
+        <meta name="description" content="View your JSON data in graphs instantly." />
       </Head>
       <StyledPageWrapper>
         <Sidebar />
@@ -39,6 +57,7 @@ const EditorPage: React.FC = () => {
           <Panes />
         </StyledEditorWrapper>
       </StyledPageWrapper>
+      <BottomBar />
     </StyledEditorWrapper>
   );
 };

+ 35 - 0
src/pages/pricing.tsx

@@ -0,0 +1,35 @@
+import React from "react";
+import { Button } from "src/components/Button";
+import { Footer } from "src/components/Footer";
+import { PricingCards } from "src/containers/PricingCards";
+import styled from "styled-components";
+
+const StyledPageWrapper = styled.div`
+  padding: 5%;
+`;
+
+const StyledHeroSection = styled.section`
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  align-items: center;
+`;
+const Pricing = () => {
+  return (
+    <>
+      <StyledPageWrapper>
+        <Button href="/" link>
+          &lt; Go Back
+        </Button>
+        <StyledHeroSection>
+          <img src="assets/icon.png" alt="json crack" width="400" />
+          <h1>Premium</h1>
+        </StyledHeroSection>
+        <PricingCards />
+      </StyledPageWrapper>
+      <Footer />
+    </>
+  );
+};
+
+export default Pricing;

+ 92 - 0
src/pages/widget.tsx

@@ -0,0 +1,92 @@
+import React from "react";
+import dynamic from "next/dynamic";
+import { useRouter } from "next/router";
+import toast from "react-hot-toast";
+import { baseURL } from "src/constants/data";
+import { darkTheme, lightTheme } from "src/constants/theme";
+import useGraph from "src/store/useGraph";
+import useJson from "src/store/useJson";
+import styled, { ThemeProvider } from "styled-components";
+
+const GraphCanvas = dynamic(
+  () => import("src/containers/Editor/LiveEditor/GraphCanvas").then(c => c.GraphCanvas),
+  {
+    ssr: false,
+  }
+);
+
+const StyledAttribute = styled.a`
+  position: fixed;
+  bottom: 0;
+  right: 0;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+  background: ${({ theme }) => theme.SILVER_DARK};
+  padding: 4px 8px;
+  font-size: 14px;
+  font-weight: 500;
+  border-radius: 3px 0 0 0;
+  opacity: 0.8;
+
+  @media only screen and (max-width: 768px) {
+    font-size: 12px;
+  }
+`;
+
+function inIframe() {
+  try {
+    return window.self !== window.top;
+  } catch (e) {
+    return true;
+  }
+}
+
+interface EmbedMessage {
+  data: {
+    json?: string;
+    options?: any;
+  };
+}
+
+const WidgetPage = () => {
+  const { query, push, isReady } = useRouter();
+  const [theme, setTheme] = React.useState("dark");
+  const fetchJson = useJson(state => state.fetchJson);
+  const setGraph = useGraph(state => state.setGraph);
+
+  React.useEffect(() => {
+    if (isReady) {
+      fetchJson(query.json);
+      if (!inIframe()) push("/");
+    }
+  }, [fetchJson, isReady, push, query.json]);
+
+  React.useEffect(() => {
+    const handler = (event: EmbedMessage) => {
+      try {
+        if (!event.data?.json) return;
+        if (event.data?.options?.theme === "light" || event.data?.options?.theme === "dark") {
+          setTheme(event.data.options.theme);
+        }
+
+        setGraph(event.data.json, event.data.options);
+      } catch (error) {
+        console.error(error);
+        toast.error("Invalid JSON!");
+      }
+    };
+
+    window.addEventListener("message", handler);
+    return () => window.removeEventListener("message", handler);
+  }, [setGraph, theme]);
+
+  return (
+    <ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
+      <GraphCanvas isWidget />
+      <StyledAttribute href={`${baseURL}/editor`} target="_blank" rel="noreferrer">
+        jsoncrack.com
+      </StyledAttribute>
+    </ThemeProvider>
+  );
+};
+
+export default WidgetPage;

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

@@ -0,0 +1,35 @@
+import { compressToBase64 } from "lz-string";
+import { altogic, AltogicResponse } from "src/api/altogic";
+import { Json } from "src/typings/altogic";
+
+const saveJson = async ({
+  id,
+  data,
+}: {
+  id?: string | null;
+  data: string;
+}): Promise<AltogicResponse<{ _id: string }>> => {
+  const compressedData = compressToBase64(data);
+
+  if (id) {
+    return await altogic.endpoint.put(`json/${id}`, {
+      json: compressedData,
+    });
+  }
+
+  return await altogic.endpoint.post("json", {
+    json: compressedData,
+  });
+};
+
+const getAllJson = async (): Promise<AltogicResponse<{ result: Json[] }>> =>
+  await altogic.endpoint.get(`json`);
+
+const updateJson = async (id: string, data: object) =>
+  await altogic.endpoint.put(`json/${id}`, {
+    ...data,
+  });
+
+const deleteJson = async (id: string) => await altogic.endpoint.delete(`json/${id}`);
+
+export { saveJson, getAllJson, updateJson, deleteJson };

+ 0 - 58
src/store/useConfig.tsx

@@ -1,58 +0,0 @@
-import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
-import { defaultJson } from "src/constants/data";
-import create from "zustand";
-
-interface ConfigActions {
-  setJson: (json: string) => void;
-  setConfig: (key: keyof Config, value: unknown) => void;
-  getJson: () => string;
-  zoomIn: () => void;
-  zoomOut: () => void;
-  centerView: () => void;
-}
-
-const initialStates = {
-  json: defaultJson,
-  cursorMode: "move" as "move" | "navigation",
-  foldNodes: false,
-  hideEditor: false,
-  performanceMode: true,
-  disableLoading: false,
-  zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
-};
-
-export type Config = typeof initialStates;
-
-const useConfig = create<Config & ConfigActions>()((set, get) => ({
-  ...initialStates,
-  getJson: () => get().json,
-  setJson: (json: string) => set({ json }),
-  zoomIn: () => {
-    const zoomPanPinch = get().zoomPanPinch;
-    if (zoomPanPinch) {
-      zoomPanPinch.setTransform(
-        zoomPanPinch?.state.positionX,
-        zoomPanPinch?.state.positionY,
-        zoomPanPinch?.state.scale + 0.4
-      );
-    }
-  },
-  zoomOut: () => {
-    const zoomPanPinch = get().zoomPanPinch;
-    if (zoomPanPinch) {
-      zoomPanPinch.setTransform(
-        zoomPanPinch?.state.positionX,
-        zoomPanPinch?.state.positionY,
-        zoomPanPinch?.state.scale - 0.4
-      );
-    }
-  },
-  centerView: () => {
-    const zoomPanPinch = get().zoomPanPinch;
-    const canvas = document.querySelector(".jsoncrack-canvas") as HTMLElement;
-    if (zoomPanPinch && canvas) zoomPanPinch.zoomToElement(canvas);
-  },
-  setConfig: (setting: keyof Config, value: unknown) => set({ [setting]: value }),
-}));
-
-export default useConfig;

+ 73 - 27
src/store/useGraph.tsx

@@ -1,13 +1,20 @@
+import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
 import { CanvasDirection } from "reaflow";
 import { Graph } from "src/components/Graph";
 import { getChildrenEdges } from "src/utils/getChildrenEdges";
 import { getOutgoers } from "src/utils/getOutgoers";
+import { parser } from "src/utils/jsonParser";
 import create from "zustand";
+import useJson from "./useJson";
 
 const initialStates = {
-  loading: false,
+  zoomPanPinch: undefined as ReactZoomPanPinchRef | undefined,
   direction: "RIGHT" as CanvasDirection,
+  loading: true,
   graphCollapsed: false,
+  foldNodes: false,
+  fullscreen: false,
+  performanceMode: true,
   nodes: [] as NodeData[],
   edges: [] as EdgeData[],
   collapsedNodes: [] as string[],
@@ -18,27 +25,39 @@ const initialStates = {
 export type Graph = typeof initialStates;
 
 interface GraphActions {
-  setGraphValue: (key: keyof Graph, value: any) => void;
+  setGraph: (json?: string, options?: Partial<Graph>[]) => void;
   setLoading: (loading: boolean) => void;
   setDirection: (direction: CanvasDirection) => void;
+  setZoomPanPinch: (ref: ReactZoomPanPinchRef) => void;
   expandNodes: (nodeId: string) => void;
   collapseNodes: (nodeId: string) => void;
   collapseGraph: () => void;
   expandGraph: () => void;
+  toggleFold: (value: boolean) => void;
+  toggleFullscreen: (value: boolean) => void;
+  togglePerfMode: (value: boolean) => void;
+  zoomIn: () => void;
+  zoomOut: () => void;
+  centerView: () => void;
 }
 
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
-  setDirection: direction => set({ direction }),
-  setGraphValue: (key, value) =>
+  setGraph: (data, options) => {
+    const { nodes, edges } = parser(data ?? useJson.getState().json);
+
     set({
+      nodes,
+      edges,
       collapsedParents: [],
       collapsedNodes: [],
       collapsedEdges: [],
       graphCollapsed: false,
       loading: true,
-      [key]: value,
-    }),
+      ...options,
+    });
+  },
+  setDirection: direction => set({ direction }),
   setLoading: loading => set({ loading }),
   expandNodes: nodeId => {
     const [childrenNodes, matchingNodes] = getOutgoers(
@@ -57,18 +76,12 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
     const matchingNodesConnectedToParent = matchingNodes.filter(node =>
       nodesConnectedToParent.includes(node)
     );
-    const nodeIds = childrenNodes
-      .map(node => node.id)
-      .concat(matchingNodesConnectedToParent);
+    const nodeIds = childrenNodes.map(node => node.id).concat(matchingNodesConnectedToParent);
     const edgeIds = childrenEdges.map(edge => edge.id);
 
     const collapsedParents = get().collapsedParents.filter(cp => cp !== nodeId);
-    const collapsedNodes = get().collapsedNodes.filter(
-      nodeId => !nodeIds.includes(nodeId)
-    );
-    const collapsedEdges = get().collapsedEdges.filter(
-      edgeId => !edgeIds.includes(edgeId)
-    );
+    const collapsedNodes = get().collapsedNodes.filter(nodeId => !nodeIds.includes(nodeId));
+    const collapsedEdges = get().collapsedEdges.filter(edgeId => !edgeIds.includes(edgeId));
 
     set({
       collapsedParents,
@@ -100,19 +113,19 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
       .filter(edge => parentNodesIds.includes(edge.from))
       .map(edge => edge.to);
 
+    const collapsedParents = get()
+      .nodes.filter(node => !parentNodesIds.includes(node.id) && node.data.parent)
+      .map(node => node.id);
+
+    const collapsedNodes = get()
+      .nodes.filter(
+        node => !parentNodesIds.includes(node.id) && !secondDegreeNodesIds.includes(node.id)
+      )
+      .map(node => node.id);
+
     set({
-      collapsedParents: get()
-        .nodes.filter(
-          node => !parentNodesIds.includes(node.id) && node.data.isParent
-        )
-        .map(node => node.id),
-      collapsedNodes: get()
-        .nodes.filter(
-          node =>
-            !parentNodesIds.includes(node.id) &&
-            !secondDegreeNodesIds.includes(node.id)
-        )
-        .map(node => node.id),
+      collapsedParents,
+      collapsedNodes,
       collapsedEdges: get()
         .edges.filter(edge => !parentNodesIds.includes(edge.from))
         .map(edge => edge.id),
@@ -127,6 +140,39 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
       graphCollapsed: false,
     });
   },
+
+  zoomIn: () => {
+    const zoomPanPinch = get().zoomPanPinch;
+    if (zoomPanPinch) {
+      zoomPanPinch.setTransform(
+        zoomPanPinch?.state.positionX,
+        zoomPanPinch?.state.positionY,
+        zoomPanPinch?.state.scale + 0.4
+      );
+    }
+  },
+  zoomOut: () => {
+    const zoomPanPinch = get().zoomPanPinch;
+    if (zoomPanPinch) {
+      zoomPanPinch.setTransform(
+        zoomPanPinch?.state.positionX,
+        zoomPanPinch?.state.positionY,
+        zoomPanPinch?.state.scale - 0.4
+      );
+    }
+  },
+  centerView: () => {
+    const zoomPanPinch = get().zoomPanPinch;
+    const canvas = document.querySelector(".jsoncrack-canvas") as HTMLElement;
+    if (zoomPanPinch && canvas) zoomPanPinch.zoomToElement(canvas);
+  },
+  toggleFold: foldNodes => {
+    set({ foldNodes });
+    get().setGraph();
+  },
+  togglePerfMode: performanceMode => set({ performanceMode }),
+  toggleFullscreen: fullscreen => set({ fullscreen }),
+  setZoomPanPinch: zoomPanPinch => set({ zoomPanPinch }),
 }));
 
 export default useGraph;

+ 118 - 0
src/store/useJson.tsx

@@ -0,0 +1,118 @@
+import { decompressFromBase64 } from "lz-string";
+import toast from "react-hot-toast";
+import { altogic } from "src/api/altogic";
+import { defaultJson } from "src/constants/data";
+import { saveJson as saveJsonDB } from "src/services/db/json";
+import useGraph from "src/store/useGraph";
+import { Json } from "src/typings/altogic";
+import create from "zustand";
+
+interface JsonActions {
+  setJson: (json: string) => void;
+  getJson: () => string;
+  getHasChanges: () => boolean;
+  fetchJson: (jsonId: string | string[] | undefined) => void;
+  setError: (hasError: boolean) => void;
+  setHasChanges: (hasChanges: boolean) => void;
+  saveJson: (isNew?: boolean) => Promise<string | undefined>;
+}
+
+const initialStates = {
+  data: null as Json | null,
+  json: "",
+  loading: true,
+  hasChanges: false,
+  hasError: false,
+};
+
+function inIframe() {
+  try {
+    return window.self !== window.top;
+  } catch (e) {
+    return true;
+  }
+}
+
+export type JsonStates = typeof initialStates;
+
+const useJson = create<JsonStates & JsonActions>()((set, get) => ({
+  ...initialStates,
+  getJson: () => get().json,
+  getHasChanges: () => get().hasChanges,
+  fetchJson: async jsonId => {
+    const isURL = new RegExp(
+      /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
+    );
+
+    if (typeof jsonId === "string" && isURL.test(jsonId)) {
+      try {
+        const res = await fetch(jsonId);
+        const json = await res.json();
+        const jsonStr = JSON.stringify(json, null, 2);
+
+        useGraph.getState().setGraph(jsonStr);
+        return set({ json: jsonStr, loading: false });
+      } catch (error) {
+        useGraph.getState().setGraph(defaultJson);
+        set({ json: defaultJson, loading: false });
+        toast.error("Failed to fetch JSON from URL!");
+      }
+    } else if (jsonId) {
+      const { data, errors } = await altogic.endpoint.get(`json/${jsonId}`, undefined, {
+        userid: altogic.auth.getUser()?._id,
+      });
+
+      if (!errors) {
+        const decompressedData = decompressFromBase64(data.json);
+        if (decompressedData) {
+          useGraph.getState().setGraph(decompressedData);
+          return set({
+            data,
+            json: decompressedData ?? undefined,
+            loading: false,
+          });
+        }
+      }
+    }
+
+    if (inIframe()) {
+      useGraph.getState().setGraph("[]");
+      return set({ json: "[]", loading: false });
+    } else {
+      useGraph.getState().setGraph(defaultJson);
+      set({ json: defaultJson, loading: false });
+    }
+  },
+  setJson: json => {
+    useGraph.getState().setGraph(json);
+    set({ json, hasChanges: true });
+  },
+  saveJson: async (isNew = true) => {
+    try {
+      const url = new URL(window.location.href);
+      const params = new URLSearchParams(url.search);
+      const jsonQuery = params.get("json");
+
+      toast.loading("Saving JSON...", { id: "jsonSave" });
+      const res = await saveJsonDB({ id: isNew ? undefined : jsonQuery, data: get().json });
+
+      if (res.errors && res.errors.items.length > 0) throw res.errors;
+
+      toast.success("JSON saved to cloud", { id: "jsonSave" });
+      set({ hasChanges: false });
+      return res.data._id;
+    } catch (error: any) {
+      if (error?.items?.length > 0) {
+        toast.error(error.items[0].message, { id: "jsonSave", duration: 5000 });
+        return undefined;
+      }
+
+      toast.error("Failed to save JSON!", { id: "jsonSave" });
+      return undefined;
+    }
+  },
+  setError: (hasError: boolean) => set({ hasError }),
+  setHasChanges: (hasChanges: boolean) => set({ hasChanges }),
+}));
+
+export default useJson;

+ 38 - 0
src/store/useModal.tsx

@@ -0,0 +1,38 @@
+import create from "zustand";
+import useUser from "./useUser";
+
+interface ModalActions {
+  setVisible: (modal: keyof typeof initialStates) => (visible: boolean) => void;
+}
+
+const initialStates = {
+  clear: false,
+  cloud: false,
+  download: false,
+  goals: false,
+  import: false,
+  account: false,
+  node: false,
+  settings: false,
+  share: false,
+  login: false,
+};
+
+type ModalType = keyof typeof initialStates;
+
+const authModals: ModalType[] = ["cloud", "share", "account"];
+
+export type ModalStates = typeof initialStates;
+
+const useModal = create<ModalStates & ModalActions>()(set => ({
+  ...initialStates,
+  setVisible: modal => visible => {
+    if (authModals.includes(modal) && !useUser.getState().isAuthenticated) {
+      return set({ login: true });
+    }
+
+    set({ [modal]: visible });
+  },
+}));
+
+export default useModal;

+ 21 - 17
src/store/useStored.tsx

@@ -1,5 +1,6 @@
 import create from "zustand";
 import { persist } from "zustand/middleware";
+import useGraph from "./useGraph";
 
 type Sponsor = {
   handle: string;
@@ -15,30 +16,29 @@ function getTomorrow() {
   return new Date(tomorrow).getTime();
 }
 
-export interface Config {
-  lightmode: boolean;
-  hideCollapse: boolean;
-  hideChildrenCount: boolean;
+const initialStates = {
+  lightmode: false,
+  hideCollapse: true,
+  childrenCount: true,
+  imagePreview: true,
   sponsors: {
-    users: Sponsor[];
-    nextDate: number;
-  };
+    users: [] as Sponsor[],
+    nextDate: Date.now(),
+  },
+};
+
+export interface ConfigActions {
   setSponsors: (sponsors: Sponsor[]) => void;
   setLightTheme: (theme: boolean) => void;
   toggleHideCollapse: (value: boolean) => void;
-  toggleHideChildrenCount: (value: boolean) => void;
+  toggleChildrenCount: (value: boolean) => void;
+  toggleImagePreview: (value: boolean) => void;
 }
 
 const useStored = create(
-  persist<Config>(
+  persist<typeof initialStates & ConfigActions>(
     set => ({
-      lightmode: false,
-      hideCollapse: false,
-      hideChildrenCount: true,
-      sponsors: {
-        users: [],
-        nextDate: Date.now(),
-      },
+      ...initialStates,
       setLightTheme: (value: boolean) =>
         set({
           lightmode: value,
@@ -51,7 +51,11 @@ const useStored = create(
           },
         }),
       toggleHideCollapse: (value: boolean) => set({ hideCollapse: value }),
-      toggleHideChildrenCount: (value: boolean) => set({ hideChildrenCount: value }),
+      toggleChildrenCount: (value: boolean) => set({ childrenCount: value }),
+      toggleImagePreview: (value: boolean) => {
+        set({ imagePreview: value });
+        useGraph.getState().setGraph();
+      },
     }),
     {
       name: "config",

+ 59 - 0
src/store/useUser.tsx

@@ -0,0 +1,59 @@
+import toast from "react-hot-toast";
+import { altogic } from "src/api/altogic";
+import { AltogicAuth, User } from "src/typings/altogic";
+import create from "zustand";
+import useModal from "./useModal";
+
+interface UserActions {
+  login: (response: AltogicAuth) => void;
+  logout: () => void;
+  setUser: (key: keyof typeof initialStates, value: any) => void;
+  checkSession: () => void;
+  isPremium: () => boolean;
+}
+
+const initialStates = {
+  isAuthenticated: false,
+  user: null as User | null,
+};
+
+export type UserStates = typeof initialStates;
+
+const useUser = create<UserStates & UserActions>()((set, get) => ({
+  ...initialStates,
+  setUser: (key, value) => set({ [key]: value }),
+  isPremium: () => {
+    const user = get().user;
+
+    if (user) return user.type > 0;
+    return false;
+  },
+  logout: () => {
+    altogic.auth.signOut();
+    toast.success("Logged out.");
+    useModal.setState({ account: false });
+    set(initialStates);
+  },
+  login: response => {
+    set({ user: response.user as any, isAuthenticated: true });
+  },
+  checkSession: async () => {
+    const currentSession = altogic.auth.getSession();
+
+    if (currentSession) {
+      const dbUser = await altogic.auth.getUserFromDB();
+
+      altogic.auth.setSession(currentSession);
+      set({ user: dbUser.user as any, isAuthenticated: true });
+    } else {
+      if (!new URLSearchParams(window.location.search).get("access_token")) return;
+
+      const data = await altogic.auth.getAuthGrant();
+      if (!data.errors?.items.length) {
+        set({ user: data.user as any, isAuthenticated: true });
+      }
+    }
+  },
+}));
+
+export default useUser;

+ 56 - 0
src/typings/altogic.ts

@@ -0,0 +1,56 @@
+export interface User {
+  _id: string;
+  provider: string;
+  providerUserId: string;
+  email: string;
+  name: string;
+  profilePicture: string;
+  signUpAt: Date;
+  lastLoginAt: Date;
+  type: 0 | 1;
+}
+
+export interface Json {
+  _id: string;
+  createdAt: string;
+  updatedAt: string;
+  json: string;
+  name: string;
+  private: false;
+}
+
+interface Device {
+  family: string;
+  major: string;
+  minor: string;
+  patch: string;
+}
+
+interface Os {
+  family: string;
+  major: string;
+  minor: string;
+  patch: string;
+}
+
+interface UserAgent {
+  family: string;
+  major: string;
+  minor: string;
+  patch: string;
+  device: Device;
+  os: Os;
+}
+
+interface Session {
+  userId: string;
+  token: string;
+  creationDtm: Date;
+  userAgent: UserAgent;
+  accessGroupKeys: any[];
+}
+
+export interface AltogicAuth {
+  user: User;
+  session: Session;
+}

+ 0 - 6
src/typings/types.d.ts

@@ -1,11 +1,5 @@
 type CanvasDirection = "LEFT" | "RIGHT" | "DOWN" | "UP";
 
-interface CustomNodeData {
-  isParent: true;
-  childrenCount: children.length;
-  children: NodeData[];
-}
-
 interface NodeData<T = any> {
   id: string;
   disabled?: boolean;

+ 2 - 6
src/utils/getChildrenEdges.ts

@@ -1,11 +1,7 @@
-export const getChildrenEdges = (
-  nodes: NodeData[],
-  edges: EdgeData[]
-): EdgeData[] => {
+export const getChildrenEdges = (nodes: NodeData[], edges: EdgeData[]): EdgeData[] => {
   const nodeIds = nodes.map(node => node.id);
 
   return edges.filter(
-    edge =>
-      nodeIds.includes(edge.from as string) || nodeIds.includes(edge.to as string)
+    edge => nodeIds.includes(edge.from as string) || nodeIds.includes(edge.to as string)
   );
 };

+ 2 - 3
src/utils/getOutgoers.ts

@@ -15,11 +15,10 @@ export const getOutgoers = (
   const runner = (nodeId: string) => {
     const outgoerIds = edges.filter(e => e.from === nodeId).map(e => e.to);
     const nodeList = nodes.filter(n => {
-      if (parent.includes(n.id) && !matchingNodes.includes(n.id))
-        matchingNodes.push(n.id);
+      if (parent.includes(n.id) && !matchingNodes.includes(n.id)) matchingNodes.push(n.id);
       return outgoerIds.includes(n.id) && !parent.includes(n.id);
     });
-    
+
     outgoerNodes.push(...nodeList);
     nodeList.forEach(node => runner(node.id));
   };

+ 0 - 10
src/utils/isValidJson.ts

@@ -1,10 +0,0 @@
-import { parse } from "jsonc-parser";
-
-export const isValidJson = (str: string) => {
-  try {
-    parse(str);
-  } catch (e) {
-    return false;
-  }
-  return str;
-};

+ 45 - 50
src/utils/jsonParser.ts

@@ -1,38 +1,49 @@
 import { Node, NodeType, parseTree } from "jsonc-parser";
+import useGraph from "src/store/useGraph";
+import useStored from "src/store/useStored";
+
+const calculateSize = (text: string | [string, string][], isParent = false) => {
+  const isFolded = useGraph.getState().foldNodes;
+  const isImagePreview = useStored.getState().imagePreview;
+  let lineCounts = 1;
+  let lineLengths: number[] = [];
+
+  if (typeof text === "string") {
+    lineLengths.push(text.length);
+  } else {
+    lineCounts = text.map(([k, v]) => {
+      const length = `${k}: ${v}`.length;
+      const line = length > 150 ? 150 : length;
+      lineLengths.push(line);
+      return `${k}: ${v}`;
+    }).length;
+  }
 
-const calculateSize = (
-  text: string | [string, string][],
-  isParent = false,
-  isFolded: boolean
-) => {
-  let value = "";
-
-  if (typeof text === "string") value = text;
-  else value = text.map(([k, v]) => `${k}: ${v}`).join("\n");
-
-  const lineCount = value.split("\n");
-  const lineLengths = lineCount.map(line => line.length);
-  const longestLine = lineLengths.sort((a, b) => b - a)[0];
+  const longestLine = Math.max(...lineLengths);
 
   const getWidth = () => {
+    if (text.length === 0) return 35;
     if (Array.isArray(text) && !text.length) return 40;
-    if (!isFolded) return 35 + longestLine * 8 + (isParent ? 60 : 0);
-    if (isParent) return 170;
+    if (!isFolded) return 35 + longestLine * 7.8 + (isParent ? 60 : 0);
+    if (isParent && isFolded) return 170;
     return 200;
   };
 
   const getHeight = () => {
-    if (lineCount.length * 17.8 < 30) return 40;
-    return (lineCount.length + 1) * 18;
+    if (lineCounts * 17.8 < 30) return 40;
+    return (lineCounts + 1) * 18;
   };
 
+  const isImage =
+    !Array.isArray(text) && /(https?:\/\/.*\.(?:png|jpg|gif))/i.test(text) && isImagePreview;
+
   return {
-    width: getWidth(),
-    height: getHeight(),
+    width: isImage ? 80 : getWidth(),
+    height: isImage ? 80 : getHeight(),
   };
 };
 
-export const parser = (jsonStr: string, isFolded = false) => {
+export const parser = (jsonStr: string) => {
   try {
     let json = parseTree(jsonStr);
     let nodes: NodeData[] = [];
@@ -117,11 +128,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
 
       if (!children) {
         if (value !== undefined) {
-          if (
-            parentType === "property" &&
-            nextType !== "object" &&
-            nextType !== "array"
-          ) {
+          if (parentType === "property" && nextType !== "object" && nextType !== "array") {
             brothersParentId = myParentId;
             if (nextType === undefined) {
               // add key and value to brothers node
@@ -130,7 +137,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
               brotherKey = value;
             }
           } else if (parentType === "array") {
-            const { width, height } = calculateSize(String(value), false, isFolded);
+            const { width, height } = calculateSize(String(value), false);
             const nodeFromArrayId = addNodes(String(value), width, height, false);
             if (myParentId) {
               addEdges(myParentId, nodeFromArrayId);
@@ -153,28 +160,22 @@ export const parser = (jsonStr: string, isFolded = false) => {
             let findBrothersNode = brothersNodeProps.find(
               e =>
                 e.parentId === brothersParentId &&
-                e.objectsFromArrayId ===
-                  objectsFromArray[objectsFromArray.length - 1]
+                e.objectsFromArrayId === objectsFromArray[objectsFromArray.length - 1]
             );
             if (findBrothersNode) {
               let ModifyNodes = [...nodes];
               let findNode = nodes.findIndex(e => e.id === findBrothersNode?.id);
 
               if (ModifyNodes[findNode]) {
-                ModifyNodes[findNode].text =
-                  ModifyNodes[findNode].text.concat(brothersNode);
-                const { width, height } = calculateSize(
-                  ModifyNodes[findNode].text,
-                  false,
-                  isFolded
-                );
+                ModifyNodes[findNode].text = ModifyNodes[findNode].text.concat(brothersNode);
+                const { width, height } = calculateSize(ModifyNodes[findNode].text, false);
                 ModifyNodes[findNode].width = width;
                 ModifyNodes[findNode].height = height;
                 nodes = [...ModifyNodes];
                 brothersNode = [];
               }
             } else {
-              const { width, height } = calculateSize(brothersNode, false, isFolded);
+              const { width, height } = calculateSize(brothersNode, false);
               const brothersNodeId = addNodes(brothersNode, width, height, false);
               brothersNode = [];
 
@@ -196,7 +197,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
           }
 
           // add parent node
-          const { width, height } = calculateSize(parentName, true, isFolded);
+          const { width, height } = calculateSize(parentName, true);
           parentId = addNodes(parentName, width, height, type);
           bracketOpen = [...bracketOpen, { id: parentId, type: type }];
           parentName = "";
@@ -253,28 +254,22 @@ export const parser = (jsonStr: string, isFolded = false) => {
             let findBrothersNode = brothersNodeProps.find(
               e =>
                 e.parentId === brothersParentId &&
-                e.objectsFromArrayId ===
-                  objectsFromArray[objectsFromArray.length - 1]
+                e.objectsFromArrayId === objectsFromArray[objectsFromArray.length - 1]
             );
             if (findBrothersNode) {
               let ModifyNodes = [...nodes];
               let findNode = nodes.findIndex(e => e.id === findBrothersNode?.id);
 
               if (ModifyNodes[findNode]) {
-                ModifyNodes[findNode].text =
-                  ModifyNodes[findNode].text.concat(brothersNode);
-                const { width, height } = calculateSize(
-                  ModifyNodes[findNode].text,
-                  false,
-                  isFolded
-                );
+                ModifyNodes[findNode].text = ModifyNodes[findNode].text.concat(brothersNode);
+                const { width, height } = calculateSize(ModifyNodes[findNode].text, false);
                 ModifyNodes[findNode].width = width;
                 ModifyNodes[findNode].height = height;
                 nodes = [...ModifyNodes];
                 brothersNode = [];
               }
             } else {
-              const { width, height } = calculateSize(brothersNode, false, isFolded);
+              const { width, height } = calculateSize(brothersNode, false);
               const brothersNodeId = addNodes(brothersNode, width, height, false);
               brothersNode = [];
 
@@ -330,7 +325,7 @@ export const parser = (jsonStr: string, isFolded = false) => {
       if (notHaveParent.length > 1) {
         if (json.type !== "array") {
           const text = "";
-          const { width, height } = calculateSize(text, false, isFolded);
+          const { width, height } = calculateSize(text, false);
           const emptyId = addNodes(text, width, height, false, true);
           notHaveParent.forEach(children => {
             addEdges(emptyId, children);
@@ -341,11 +336,11 @@ export const parser = (jsonStr: string, isFolded = false) => {
       if (nodes.length === 0) {
         if (json.type === "array") {
           const text = "[]";
-          const { width, height } = calculateSize(text, false, isFolded);
+          const { width, height } = calculateSize(text, false);
           addNodes(text, width, height, false);
         } else {
           const text = "{}";
-          const { width, height } = calculateSize(text, false, isFolded);
+          const { width, height } = calculateSize(text, false);
           addNodes(text, width, height, false);
         }
       }

+ 1 - 4
src/utils/search.ts

@@ -11,10 +11,7 @@ export const cleanupHighlight = () => {
   });
 };
 
-export const highlightMatchedNodes = (
-  nodes: NodeListOf<Element>,
-  selectedNode: number
-) => {
+export const highlightMatchedNodes = (nodes: NodeListOf<Element>, selectedNode: number) => {
   nodes?.forEach(node => {
     node.parentElement?.closest("foreignObject")?.classList.add("searched");
   });

File diff suppressed because it is too large
+ 396 - 217
yarn.lock


Some files were not shown because too many files changed in this diff