Kaynağa Gözat

Merge pull request #280 from AykutSarac/mona-sans

Cloud updates
Aykut Saraç 2 yıl önce
ebeveyn
işleme
1940987759
74 değiştirilmiş dosya ile 2607 ekleme ve 1138 silme
  1. 3 1
      .env.development
  2. 3 1
      .env.production
  3. 1 1
      .prettierrc
  4. 1 1
      next.config.js
  5. 12 2
      package.json
  6. 0 0
      public/assets/404.svg
  7. BIN
      public/assets/Mona-Sans.woff2
  8. BIN
      public/assets/icon.png
  9. 13 0
      src/api/altogic.ts
  10. 11 7
      src/components/Button/index.tsx
  11. 5 20
      src/components/CustomNode/ObjectNode.tsx
  12. 45 37
      src/components/CustomNode/TextNode.tsx
  13. 1 6
      src/components/CustomNode/index.tsx
  14. 2 8
      src/components/CustomNode/styles.tsx
  15. 4 1
      src/components/ErrorContainer/index.tsx
  16. 84 0
      src/components/Footer/index.tsx
  17. 4 9
      src/components/Graph/ErrorView.tsx
  18. 19 55
      src/components/Graph/index.tsx
  19. 14 11
      src/components/Loading/index.tsx
  20. 5 3
      src/components/Modal/index.tsx
  21. 8 4
      src/components/Modal/styles.tsx
  22. 55 42
      src/components/MonacoEditor/index.tsx
  23. 1 5
      src/components/SearchInput/index.tsx
  24. 115 124
      src/components/Sidebar/index.tsx
  25. 28 0
      src/components/Spinner/index.tsx
  26. 3 10
      src/components/Sponsors/index.tsx
  27. 4 9
      src/components/SupportButton/index.tsx
  28. 22 30
      src/components/Tooltip/index.tsx
  29. 27 12
      src/constants/globalStyle.ts
  30. 1 0
      src/constants/theme.ts
  31. 184 0
      src/containers/Editor/BottomBar.tsx
  32. 2 4
      src/containers/Editor/JsonEditor/index.tsx
  33. 12 17
      src/containers/Editor/LiveEditor/GraphCanvas.tsx
  34. 9 9
      src/containers/Editor/Panes.tsx
  35. 10 24
      src/containers/Editor/Tools.tsx
  36. 67 93
      src/containers/Home/index.tsx
  37. 37 50
      src/containers/Home/styles.tsx
  38. 51 0
      src/containers/ModalController/index.tsx
  39. 143 0
      src/containers/Modals/AccountModal/index.tsx
  40. 12 6
      src/containers/Modals/ClearModal/index.tsx
  41. 270 0
      src/containers/Modals/CloudModal/index.tsx
  42. 6 6
      src/containers/Modals/DownloadModal/index.tsx
  43. 0 76
      src/containers/Modals/GoalsModal/index.tsx
  44. 5 8
      src/containers/Modals/ImportModal/index.tsx
  45. 26 0
      src/containers/Modals/LoginModal/index.tsx
  46. 1 3
      src/containers/Modals/NodeModal/index.tsx
  47. 27 22
      src/containers/Modals/SettingsModal/index.tsx
  48. 24 63
      src/containers/Modals/ShareModal/index.tsx
  49. 128 0
      src/containers/PricingCards/index.tsx
  50. 11 15
      src/hooks/useFocusNode.tsx
  51. 33 0
      src/hooks/useHideNodes.tsx
  52. 22 33
      src/pages/_app.tsx
  53. 2 6
      src/pages/_document.tsx
  54. 1 1
      src/pages/_error.tsx
  55. 186 0
      src/pages/docs.tsx
  56. 24 5
      src/pages/editor.tsx
  57. 0 35
      src/pages/embed.tsx
  58. 35 0
      src/pages/pricing.tsx
  59. 20 76
      src/pages/widget.tsx
  60. 35 0
      src/services/db/json.tsx
  61. 0 58
      src/store/useConfig.tsx
  62. 73 27
      src/store/useGraph.tsx
  63. 105 0
      src/store/useJson.tsx
  64. 38 0
      src/store/useModal.tsx
  65. 21 17
      src/store/useStored.tsx
  66. 59 0
      src/store/useUser.tsx
  67. 56 0
      src/typings/altogic.ts
  68. 0 6
      src/typings/types.d.ts
  69. 2 6
      src/utils/getChildrenEdges.ts
  70. 2 3
      src/utils/getOutgoers.ts
  71. 0 10
      src/utils/isValidJson.ts
  72. 43 48
      src/utils/jsonParser.ts
  73. 1 4
      src/utils/search.ts
  74. 333 8
      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$)",

+ 1 - 1
next.config.js

@@ -9,7 +9,7 @@ const withPWA = require("next-pwa")({
  * @type {import('next').NextConfig}
  */
 const nextConfig = {
-  reactStrictMode: true,
+  reactStrictMode: false,
 };
 
 module.exports = withPWA(nextConfig);

+ 12 - 2
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,11 +14,17 @@
   },
   "dependencies": {
     "@monaco-editor/react": "^4.4.6",
+    "@react-oauth/google": "^0.4.0",
     "@sentry/nextjs": "^7.16.0",
+    "@tanstack/react-query": "^4.19.1",
     "allotment": "^1.17.0",
-    "compress-json": "^2.1.2",
+    "altogic": "^2.3.8",
+    "axios": "^1.1.3",
+    "dayjs": "^1.11.6",
     "html-to-image": "^1.10.8",
     "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",
@@ -28,6 +34,7 @@
     "react-icons": "^4.6.0",
     "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",
     "styled-components": "^5.3.6",
@@ -36,9 +43,12 @@
   "devDependencies": {
     "@testing-library/react": "^13.3.0",
     "@trivago/prettier-plugin-sort-imports": "^3.3.0",
+    "@types/lodash.debounce": "^4.0.7",
+    "@types/lz-string": "^1.3.34",
     "@types/node": "^18.7.21",
     "@types/react": "18.0.21",
     "@types/react-color": "^3.0.6",
+    "@types/react-syntax-highlighter": "^15.5.5",
     "@types/styled-components": "^5.1.26",
     "eslint": "8.24.0",
     "eslint-config-next": "12.3.1",

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
public/assets/404.svg


BIN
public/assets/Mona-Sans.woff2


BIN
public/assets/icon.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",

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

@@ -0,0 +1,184 @@
+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;
+`;
+
+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"
+              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} />
     </>
   );
 };

+ 67 - 93
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,23 +48,22 @@ 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>
         <Styles.StyledSponsorButton
           href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode"
@@ -69,7 +73,6 @@ const HeroSection = () => {
           GET IT ON VS CODE
           <SiVisualstudiocode />
         </Styles.StyledSponsorButton>
-        <GoalsModal visible={isModalVisible} setVisible={setModalVisible} />
       </Styles.StyledButtonWrapper>
     </Styles.StyledHeroSection>
   );
@@ -96,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>
 
@@ -108,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>
 
@@ -120,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>
 
@@ -132,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>
@@ -143,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"
@@ -194,30 +194,38 @@ 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="https://jsoncrack.com/embed"
-        status="SECONDARY"
-        link
-      >
+      <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,
+                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",
-                  direction: "DOWN"
-                }
+                },
               },
               "*"
             );
@@ -263,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>
@@ -311,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.TEXT_NORMAL};
+  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;

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

+ 2 - 6
src/pages/_document.tsx

@@ -15,13 +15,9 @@ 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"
           />

+ 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;
 `;
 

+ 186 - 0
src/pages/docs.tsx

@@ -0,0 +1,186 @@
+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: 100%;
+  height: 400px;
+  flex: 1;
+`;
+
+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-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.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>
   );
 };

+ 0 - 35
src/pages/embed.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;

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

+ 20 - 76
src/pages/widget.tsx

@@ -4,14 +4,16 @@ 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 { NodeModal } from "src/containers/Modals/NodeModal";
 import useGraph from "src/store/useGraph";
-import { parser } from "src/utils/jsonParser";
+import useJson from "src/store/useJson";
 import styled, { ThemeProvider } from "styled-components";
 
-const Graph = dynamic<any>(() => import("src/components/Graph").then(c => c.Graph), {
-  ssr: false,
-});
+const GraphCanvas = dynamic(
+  () => import("src/containers/Editor/LiveEditor/GraphCanvas").then(c => c.GraphCanvas),
+  {
+    ssr: false,
+  }
+);
 
 const StyledAttribute = styled.a`
   position: fixed;
@@ -45,70 +47,28 @@ interface EmbedMessage {
   };
 }
 
-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 { query, push, isReady } = useRouter();
   const [theme, setTheme] = React.useState("dark");
-
-  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), []);
+  const fetchJson = useJson(state => state.fetchJson);
+  const setGraph = useGraph(state => state.setGraph);
 
   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 (isReady) {
+      fetchJson(query.json);
+      if (!inIframe()) push("/");
     }
-
-    if (!inIframe()) push("/");
-  }, [collapsedNodes, collapsedEdges, loading, 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);
+        }
 
-        const { nodes, edges } = parser(event.data.json);
-
-        const options = {
-          direction: "RIGHT",
-          theme,
-          ...event.data.options,
-        };
-
-        setGraphValue("direction", options.direction);
-        if (options.theme === "light" || options.theme === "dark")
-          setTheme(options.theme);
-
-        setGraphValue("nodes", nodes);
-        setGraphValue("edges", edges);
+        setGraph(event.data.json, event.data.options);
       } catch (error) {
         console.error(error);
         toast.error("Invalid JSON!");
@@ -117,27 +77,11 @@ const WidgetPage = () => {
 
     window.addEventListener("message", handler);
     return () => window.removeEventListener("message", handler);
-  }, [setGraphValue, theme]);
-
-  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>
-    );
+  }, [setGraph, theme]);
 
   return (
     <ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
-      <Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget />
-      <NodeModal
-        selectedNode={selectedNode}
-        visible={isModalVisible}
-        closeModal={() => setModalVisible(false)}
-      />
+      <GraphCanvas isWidget />
       <StyledAttribute href={`${baseURL}/editor`} target="_blank" rel="noreferrer">
         jsoncrack.com
       </StyledAttribute>

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

+ 105 - 0
src/store/useJson.tsx

@@ -0,0 +1,105 @@
+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,
+};
+
+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,
+          });
+        }
+      }
+    }
+
+    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;
-};

+ 43 - 48
src/utils/jsonParser.ts

@@ -1,38 +1,49 @@
 import { Node, parseTree } from "jsonc-parser";
+import useGraph from "src/store/useGraph";
+import useStored from "src/store/useStored";
 
-const calculateSize = (
-  text: string | [string, string][],
-  isParent = false,
-  isFolded: boolean
-) => {
-  let value = "";
+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") value = text;
-  else value = text.map(([k, v]) => `${k}: ${v}`).join("\n");
+  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 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[] = [];
@@ -98,11 +109,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
@@ -111,7 +118,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);
@@ -134,28 +141,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 = [];
 
@@ -177,7 +178,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 = "";
@@ -232,28 +233,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 = [];
 
@@ -309,7 +304,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);
@@ -320,11 +315,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");
   });

+ 333 - 8
yarn.lock

@@ -921,6 +921,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.3.1":
+  version "7.20.7"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
+  integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
 "@babel/template@^7.16.7", "@babel/template@^7.18.10":
   version "7.18.10"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -1291,6 +1298,11 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@react-oauth/google@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.4.0.tgz#ae4fe2724040bd11facdc53aad43a21e9f34b2c9"
+  integrity sha512-2QxxrKbXXH8bwHSefB56sBgsKs7Bq3Pvv8tVmGJuINGefECsssIUKidTDm5P55T4CV99sCX/GUfxs3l2Ntxo8Q==
+
 "@rollup/plugin-babel@^5.2.0":
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@@ -1463,6 +1475,11 @@
   dependencies:
     "@sentry/cli" "^1.74.4"
 
+"@socket.io/component-emitter@~3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+  integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
 "@surma/rollup-plugin-off-main-thread@^2.2.3":
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -1480,6 +1497,19 @@
   dependencies:
     tslib "^2.4.0"
 
+"@tanstack/[email protected]":
+  version "4.19.1"
+  resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.19.1.tgz#2e92d9e8a50884eb231c5beb4386e131ebe34306"
+  integrity sha512-Zp0aIose5C8skBzqbVFGk9HJsPtUhRVDVNWIqVzFbGQQgYSeLZMd3Sdb4+EnA5wl1J7X+bre2PJGnQg9x/zHOA==
+
+"@tanstack/react-query@^4.19.1":
+  version "4.19.1"
+  resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.19.1.tgz#43356dd537127e76d75f5a2769eb23dafd9a3690"
+  integrity sha512-5dvHvmc0vrWI03AJugzvKfirxCyCLe+qawrWFCXdu8t7dklIhJ7D5ZhgTypv7mMtIpdHPcECtCiT/+V74wCn2A==
+  dependencies:
+    "@tanstack/query-core" "4.19.1"
+    use-sync-external-store "^1.2.0"
+
 "@testing-library/dom@^8.5.0":
   version "8.18.1"
   resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.18.1.tgz#80f91be02bc171fe5a3a7003f88207be31ac2cf3"
@@ -1554,6 +1584,13 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
+"@types/hast@^2.0.0":
+  version "2.3.4"
+  resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
+  integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/hoist-non-react-statics@*":
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@@ -1572,6 +1609,23 @@
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
 
+"@types/lodash.debounce@^4.0.7":
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
+  integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*":
+  version "4.14.191"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
+  integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+
+"@types/lz-string@^1.3.34":
+  version "1.3.34"
+  resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5"
+  integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==
+
 "@types/minimatch@*":
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
@@ -1602,6 +1656,13 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-syntax-highlighter@^15.5.5":
+  version "15.5.5"
+  resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.5.tgz#4d3b51f8956195f1f63360ff03f8822c5d74c516"
+  integrity sha512-QH3JZQXa2usAvJvSsdSUJ4Yu4j8ReuZpgRrEW+XP+Rmosbn425YshW9iGEb/pAARm8496axHhHUPRH3UmTiB6A==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react@*", "@types/[email protected]":
   version "18.0.21"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67"
@@ -1644,6 +1705,11 @@
   resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
   integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
 
+"@types/unist@*":
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
+  integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
+
 "@typescript-eslint/parser@^5.21.0":
   version "5.38.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.0.tgz#5a59a1ff41a7b43aacd1bb2db54f6bf1c02b2ff8"
@@ -1759,6 +1825,14 @@ allotment@^1.17.0:
     lodash.isequal "^4.5.0"
     use-resize-observer "^9.0.0"
 
+altogic@^2.3.8:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/altogic/-/altogic-2.3.8.tgz#475ca083faec97660d30b838bbddaeeb83da9072"
+  integrity sha512-KTRSPH/g490sJiIe0qfuJaMsidGFkYSAnPR93Hn9VQ9GyZ+0/KmMIPSV6ctRaCvk/fw06w56IgYQNZDf6VJyxg==
+  dependencies:
+    cross-fetch "^3.1.4"
+    socket.io-client "^4.5.1"
+
 ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -1887,6 +1961,11 @@ async@^3.2.3:
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
   integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
 
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
 at-least-node@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@@ -1897,6 +1976,15 @@ axe-core@^4.4.3:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
   integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
 
+axios@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
+  integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 axobject-query@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -2064,6 +2152,21 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+character-entities-legacy@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
+  integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+
+character-entities@^1.0.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
+  integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+
+character-reference-invalid@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
+  integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+
 classnames@^2.3.0, classnames@^2.3.1:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
@@ -2105,6 +2208,18 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+comma-separated-tokens@^1.0.0:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
+  integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -2125,11 +2240,6 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
 
-compress-json@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/compress-json/-/compress-json-2.1.2.tgz#37e0e7c7480c572fad9ad387fca5a2f36fee6f83"
-  integrity sha512-91247RD8bKQXzRmXUS4zGT250mhw86+J9X8w2L2SGtRE7g0CvzjOETFaFmsDdaXPWv8T7L9iiM7kdcnnH3BH7w==
-
 [email protected]:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2174,6 +2284,13 @@ create-require@^1.1.0:
   resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
   integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
 
+cross-fetch@^3.1.4:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+  dependencies:
+    node-fetch "2.6.7"
+
 cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -2224,7 +2341,12 @@ damerau-levenshtein@^1.0.8:
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
   integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
 
-debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
+dayjs@^1.11.6:
+  version "1.11.6"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb"
+  integrity sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==
+
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
   integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2305,6 +2427,11 @@ del@^4.1.1:
     pify "^4.0.1"
     rimraf "^2.6.3"
 
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2387,6 +2514,22 @@ emojis-list@^3.0.0:
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
   integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
 
+engine.io-client@~6.2.3:
+  version "6.2.3"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.3.tgz#a8cbdab003162529db85e9de31575097f6d29458"
+  integrity sha512-aXPtgF1JS3RuuKcpSrBtimSjYvrbhKW9froICH4s0F3XQWLxsKNxqzG39nnvQZQnva4CMvUK63T7shevxRyYHw==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+    engine.io-parser "~5.0.3"
+    ws "~8.2.3"
+    xmlhttprequest-ssl "~2.0.0"
+
+engine.io-parser@~5.0.3:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
+  integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
+
 enhanced-resolve@^5.10.0:
   version "5.10.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
@@ -2753,6 +2896,13 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
+fault@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
+  integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+  dependencies:
+    format "^0.2.0"
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -2812,6 +2962,11 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
   integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 for-each@~0.3.3:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -2819,6 +2974,20 @@ for-each@~0.3.3:
   dependencies:
     is-callable "^1.1.3"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+format@^0.2.0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+  integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
+
 framer-motion@^6.2.8:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7"
@@ -3067,11 +3236,32 @@ has@^1.0.3, has@~1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+hast-util-parse-selector@^2.0.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
+  integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
+
+hastscript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
+  integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    comma-separated-tokens "^1.0.0"
+    hast-util-parse-selector "^2.0.0"
+    property-information "^5.0.0"
+    space-separated-tokens "^1.0.0"
+
 hey-listen@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
   integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
 
+highlight.js@^10.4.1, highlight.js@~10.7.0:
+  version "10.7.3"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
+  integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
 hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -3142,6 +3332,19 @@ internal-slot@^1.0.3:
     has "^1.0.3"
     side-channel "^1.0.4"
 
+is-alphabetical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
+  integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+
+is-alphanumerical@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
+  integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+  dependencies:
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+
 is-arguments@^1.0.4:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@@ -3184,6 +3387,11 @@ is-date-object@^1.0.1:
   dependencies:
     has-tostringtag "^1.0.0"
 
+is-decimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
+  integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+
 is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -3208,6 +3416,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
   dependencies:
     is-extglob "^2.1.1"
 
+is-hexadecimal@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
+  integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+
 is-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -3562,6 +3775,14 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lowlight@^1.17.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+  integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+  dependencies:
+    fault "^1.0.0"
+    highlight.js "~10.7.0"
+
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -3621,6 +3842,18 @@ micromatch@^4.0.4:
     braces "^3.0.2"
     picomatch "^2.3.1"
 
[email protected]:
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
 minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -3732,7 +3965,7 @@ next@^12.3.1:
     "@next/swc-win32-ia32-msvc" "12.3.1"
     "@next/swc-win32-x64-msvc" "12.3.1"
 
-node-fetch@^2.6.7:
+[email protected], node-fetch@^2.6.7:
   version "2.6.7"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
   integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@@ -3896,6 +4129,18 @@ parent-module@^1.0.0:
   dependencies:
     callsites "^3.0.0"
 
+parse-entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+  integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -4023,6 +4268,16 @@ pretty-format@^27.0.2:
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
 
+prismjs@^1.27.0:
+  version "1.29.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
+  integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
+
+prismjs@~1.27.0:
+  version "1.27.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
+  integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -4042,6 +4297,13 @@ prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1:
     object-assign "^4.1.1"
     react-is "^16.13.1"
 
+property-information@^5.0.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
+  integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
+  dependencies:
+    xtend "^4.0.0"
+
 proxy-from-env@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@@ -4145,6 +4407,17 @@ react-scrolllock@^5.0.1:
   dependencies:
     exenv "^1.2.2"
 
+react-syntax-highlighter@^15.5.0:
+  version "15.5.0"
+  resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
+  integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+    highlight.js "^10.4.1"
+    lowlight "^1.17.0"
+    prismjs "^1.27.0"
+    refractor "^3.6.0"
+
 react-use-gesture@^8.0.1:
   version "8.0.1"
   resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-8.0.1.tgz#4360c0f7c9e26baf9fbe58f63fc9de7ef699c17f"
@@ -4210,6 +4483,15 @@ reakeys@^1.2.6:
   dependencies:
     mousetrap "^1.6.5"
 
+refractor@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
+  integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
+  dependencies:
+    hastscript "^6.0.0"
+    parse-entities "^2.0.0"
+    prismjs "~1.27.0"
+
 regenerate-unicode-properties@^10.1.0:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
@@ -4222,6 +4504,11 @@ regenerate@^1.4.2:
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
   integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
+regenerator-runtime@^0.13.11:
+  version "0.13.11"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
 regenerator-runtime@^0.13.4:
   version "0.13.9"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
@@ -4468,6 +4755,24 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
+socket.io-client@^4.5.1:
+  version "4.5.4"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.4.tgz#d3cde8a06a6250041ba7390f08d2468ccebc5ac9"
+  integrity sha512-ZpKteoA06RzkD32IbqILZ+Cnst4xewU7ZYK12aS1mzHftFFjpoMz69IuhP/nL25pJfao/amoPI527KnuhFm01g==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.2"
+    engine.io-client "~6.2.3"
+    socket.io-parser "~4.2.1"
+
+socket.io-parser@~4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
+  integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+
 source-list-map@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -4508,6 +4813,11 @@ sourcemap-codec@^1.4.8:
   resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
   integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
 
+space-separated-tokens@^1.0.0:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
+  integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
+
 state-local@^1.0.6:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
@@ -4952,7 +5262,7 @@ use-resize-observer@^9.0.0:
   dependencies:
     "@juggle/resize-observer" "^3.3.1"
 
[email protected]:
[email protected], use-sync-external-store@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
@@ -5206,6 +5516,21 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+ws@~8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+xmlhttprequest-ssl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
+  integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
+xtend@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor