Просмотр исходного кода

Merge pull request #318 from AykutSarac/feat/mantine-ui

Feat/mantine UI
Aykut Saraç 2 лет назад
Родитель
Сommit
25a7d22c9b
50 измененных файлов с 1181 добавлено и 1551 удалено
  1. 24 19
      package.json
  2. BIN
      public/assets/rocket_ship.webp
  3. 0 99
      src/components/Button/index.tsx
  4. 2 2
      src/components/CustomNode/ObjectNode.tsx
  5. 2 2
      src/components/CustomNode/TextNode.tsx
  6. 2 2
      src/components/CustomNode/index.tsx
  7. 1 1
      src/components/CustomNode/styles.tsx
  8. 10 2
      src/components/Graph/PremiumView.tsx
  9. 85 52
      src/components/Graph/index.tsx
  10. 0 19
      src/components/Input/index.tsx
  11. 24 24
      src/components/Loading/index.tsx
  12. 0 76
      src/components/Modal/index.tsx
  13. 0 69
      src/components/Modal/styles.tsx
  14. 1 1
      src/components/SearchInput/index.tsx
  15. 2 2
      src/components/Sidebar/index.tsx
  16. 0 28
      src/components/Spinner/index.tsx
  17. 12 5
      src/components/Sponsors/index.tsx
  18. 0 65
      src/components/Toggle/index.tsx
  19. 0 62
      src/components/Tooltip/index.tsx
  20. 14 1
      src/constants/globalStyle.ts
  21. 1 24
      src/containers/Editor/LiveEditor/GraphCanvas.tsx
  22. 5 5
      src/containers/Editor/LiveEditor/Tools.tsx
  23. 33 34
      src/containers/Home/index.tsx
  24. 0 36
      src/containers/Home/styles.tsx
  25. 8 8
      src/containers/ModalController/index.tsx
  26. 58 79
      src/containers/Modals/AccountModal/index.tsx
  27. 11 10
      src/containers/Modals/ClearModal/index.tsx
  28. 34 59
      src/containers/Modals/CloudModal/index.tsx
  29. 69 108
      src/containers/Modals/DownloadModal/index.tsx
  30. 13 23
      src/containers/Modals/ImportModal/index.tsx
  31. 10 16
      src/containers/Modals/LoginModal/index.tsx
  32. 40 87
      src/containers/Modals/NodeModal/index.tsx
  33. 35 34
      src/containers/Modals/SettingsModal/index.tsx
  34. 40 61
      src/containers/Modals/ShareModal/index.tsx
  35. 20 11
      src/containers/PricingCards/index.tsx
  36. 0 33
      src/hooks/useHideNodes.tsx
  37. 0 25
      src/hooks/useKeyPress.tsx
  38. 51 0
      src/pages/404.tsx
  39. 34 14
      src/pages/_app.tsx
  40. 5 0
      src/pages/_document.tsx
  41. 1 1
      src/pages/_error.tsx
  42. 2 2
      src/pages/docs.tsx
  43. 3 2
      src/pages/editor.tsx
  44. 2 2
      src/pages/pricing.tsx
  45. 24 20
      src/pages/sign-in.tsx
  46. 3 3
      src/store/useGraph.tsx
  47. 1 0
      src/typings/types.d.ts
  48. 6 0
      src/utils/core/jsonParser.ts
  49. 11 0
      src/utils/dataToString.ts
  50. 482 323
      yarn.lock

+ 24 - 19
package.json

@@ -1,7 +1,7 @@
 {
   "name": "json-crack",
   "private": true,
-  "version": "v2.6.0",
+  "version": "v2.8.0",
   "author": "https://github.com/AykutSarac",
   "homepage": "https://jsoncrack.com",
   "scripts": {
@@ -13,44 +13,49 @@
     "deploy": "gh-pages -d out -t true"
   },
   "dependencies": {
+    "@emotion/react": "^11.10.6",
+    "@emotion/server": "^11.10.0",
+    "@mantine/core": "^6.0.1",
+    "@mantine/hooks": "^6.0.1",
+    "@mantine/next": "^6.0.1",
+    "@mantine/prism": "^6.0.1",
     "@monaco-editor/react": "^4.4.6",
-    "@next/font": "^13.1.6",
-    "@sentry/nextjs": "^7.36.0",
-    "@tanstack/react-query": "^4.24.4",
-    "allotment": "^1.18.0",
+    "@sentry/nextjs": "^7.42.0",
+    "@tanstack/react-query": "^4.26.1",
+    "allotment": "^1.18.1",
     "altogic": "^2.3.9",
-    "axios": "^1.3.2",
+    "axios": "^1.3.4",
     "dayjs": "^1.11.7",
     "html-to-image": "^1.11.11",
     "jsonc-parser": "^3.2.0",
     "lodash.debounce": "^4.0.8",
-    "lz-string": "^1.4.4",
-    "next": "^13.1.6",
+    "lz-string": "^1.5.0",
+    "next": "^13.2.3",
     "react": "^18.2.0",
     "react-color": "^2.19.3",
     "react-dom": "^18.2.0",
     "react-hot-toast": "^2.4.0",
-    "react-icons": "^4.7.1",
+    "react-icons": "^4.8.0",
     "react-linkify-it": "^1.0.7",
     "react-syntax-highlighter": "^15.5.0",
-    "react-zoom-pan-pinch": "^2.5.0",
+    "react-zoom-pan-pinch": "^3.0.2",
     "reaflow": "^5.1.2",
-    "styled-components": "^5.3.6",
-    "zustand": "^4.3.2"
+    "styled-components": "^5.3.8",
+    "zustand": "^4.3.6"
   },
   "devDependencies": {
-    "@next/bundle-analyzer": "^13.1.6",
-    "@testing-library/react": "^13.3.0",
-    "@trivago/prettier-plugin-sort-imports": "^4.0.0",
+    "@next/bundle-analyzer": "^13.2.3",
+    "@testing-library/react": "^14.0.0",
+    "@trivago/prettier-plugin-sort-imports": "^4.1.1",
     "@types/lodash.debounce": "^4.0.7",
     "@types/lz-string": "^1.3.34",
-    "@types/node": "^18.13.0",
-    "@types/react": "18.0.27",
+    "@types/node": "^18.15.0",
+    "@types/react": "18.0.28",
     "@types/react-color": "^3.0.6",
     "@types/react-syntax-highlighter": "^15.5.6",
     "@types/styled-components": "^5.1.26",
-    "eslint": "8.33.0",
-    "eslint-config-next": "13.1.6",
+    "eslint": "8.35.0",
+    "eslint-config-next": "13.2.3",
     "eslint-plugin-unused-imports": "^2.0.0",
     "prettier": "^2.8.4",
     "ts-node": "^10.9.1",

BIN
public/assets/rocket_ship.webp


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

@@ -1,99 +0,0 @@
-import React from "react";
-import styled, { DefaultTheme } from "styled-components";
-
-enum ButtonType {
-  PRIMARY = "PRIMARY",
-  SECONDARY = "BLURPLE",
-  TERTIARY = "PURPLE",
-  DANGER = "DANGER",
-  SUCCESS = "SEAGREEN",
-  WARNING = "ORANGE",
-}
-
-interface ButtonProps {
-  status?: keyof typeof ButtonType;
-  block?: boolean;
-}
-
-type ConditionalProps =
-  | ({
-      link: boolean;
-    } & React.ComponentPropsWithoutRef<"a">)
-  | ({
-      link?: never;
-    } & React.ComponentPropsWithoutRef<"button">);
-
-function getButtonStatus(status: keyof typeof ButtonType, theme: DefaultTheme) {
-  return theme[ButtonType[status]];
-}
-
-const StyledButton = styled.button<{
-  status: keyof typeof ButtonType;
-  block: boolean;
-  link: boolean;
-}>`
-  display: inline-flex;
-  align-items: center;
-  background: ${({ status, theme }) => getButtonStatus(status, theme)};
-  color: #ffffff;
-  padding: ${({ link }) => (link ? "2px 16px" : "8px 16px")};
-  min-width: 70px;
-  min-height: 32px;
-  border-radius: 3px;
-  font-size: 14px;
-  font-weight: 500;
-  width: ${({ block }) => (block ? "-webkit-fill-available" : "fit-content")};
-  height: 40px;
-  background-image: none;
-
-  :disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-  }
-
-  div {
-    white-space: normal;
-    margin: 0 auto;
-    text-overflow: ellipsis;
-    overflow: hidden;
-  }
-
-  &:hover {
-    background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0 0);
-  }
-
-  @media only screen and (max-width: 768px) {
-    font-size: 14px;
-  }
-`;
-
-const StyledButtonContent = styled.div`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  gap: 8px;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  font-weight: 600;
-`;
-
-export const Button: React.FC<ButtonProps & ConditionalProps> = ({
-  children,
-  status,
-  block = false,
-  link = false,
-  ...props
-}) => {
-  return (
-    <StyledButton
-      type="button"
-      as={link ? "a" : "button"}
-      status={status ?? "PRIMARY"}
-      block={block}
-      link={link}
-      {...props}
-    >
-      <StyledButtonContent>{children}</StyledButtonContent>
-    </StyledButton>
-  );
-};

+ 2 - 2
src/components/CustomNode/ObjectNode.tsx

@@ -2,7 +2,7 @@ import React from "react";
 import { CustomNodeProps } from "src/components/CustomNode";
 import * as Styled from "./styles";
 
-const ObjectNode: React.FC<CustomNodeProps> = ({ node, x, y }) => {
+const Node: React.FC<CustomNodeProps> = ({ node, x, y }) => {
   const { text, width, height, data } = node;
   const ref = React.useRef(null);
 
@@ -34,4 +34,4 @@ function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) {
   return String(prev.node.text) === String(next.node.text) && prev.node.width === next.node.width;
 }
 
-export default React.memo(ObjectNode, propsAreEqual);
+export const ObjectNode = React.memo(Node, propsAreEqual);

+ 2 - 2
src/components/CustomNode/TextNode.tsx

@@ -40,7 +40,7 @@ const StyledImage = styled.img`
   background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
 `;
 
-const TextNode: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) => {
+const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) => {
   const {
     id,
     text,
@@ -110,4 +110,4 @@ function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) {
   return prev.node.text === next.node.text && prev.node.width === next.node.width;
 }
 
-export default React.memo(TextNode, propsAreEqual);
+export const TextNode = React.memo(Node, propsAreEqual);

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

@@ -1,7 +1,7 @@
 import React from "react";
 import { Node, NodeProps } from "reaflow";
-import ObjectNode from "./ObjectNode";
-import TextNode from "./TextNode";
+import { ObjectNode } from "./ObjectNode";
+import { TextNode } from "./TextNode";
 
 export interface CustomNodeProps {
   node: NodeData;

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

@@ -1,4 +1,4 @@
-import { Roboto_Mono } from "@next/font/google";
+import { Roboto_Mono } from "next/font/google";
 import { LinkItUrl } from "react-linkify-it";
 import styled, { DefaultTheme } from "styled-components";
 

+ 10 - 2
src/components/Graph/PremiumView.tsx

@@ -1,6 +1,6 @@
 import React from "react";
+import { Button } from "@mantine/core";
 import styled from "styled-components";
-import { Button } from "../Button";
 
 const StyledPremiumView = styled.div`
   display: flex;
@@ -38,7 +38,15 @@ export const PremiumView = () => (
     <StyledInfo>
       Upgrade JSON Crack to premium and explore & unlock full potantial of your data!
     </StyledInfo>
-    <Button status="TERTIARY" href="https://www.patreon.com/jsoncrack" link target="_blank">
+    <Button
+      size="lg"
+      component="a"
+      variant="gradient"
+      gradient={{ from: "purple", to: "pink" }}
+      href="https://www.patreon.com/jsoncrack"
+      target="_blank"
+      style={{ border: "2px solid black" }}
+    >
       DO IT!
     </Button>
     <img src="/assets/undraw_to_the_stars_re_wq2x.svg" width="300" height="300" alt="oops" />

+ 85 - 52
src/components/Graph/index.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
-import { Canvas, Edge, ElkRoot } from "reaflow";
+import { Canvas, Edge, EdgeProps, ElkRoot, NodeProps } from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
 import useGraph from "src/store/useGraph";
 import useUser from "src/store/useUser";
@@ -48,7 +48,7 @@ const StyledEditorWrapper = styled.div<{ widget: boolean }>`
   }
 `;
 
-const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
+export const Graph = ({ isWidget = false, openNodeModal }: GraphProps) => {
   const isPremium = useUser(state => state.isPremium);
   const setLoading = useGraph(state => state.setLoading);
   const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
@@ -60,6 +60,27 @@ const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
   const nodes = useGraph(state => state.nodes);
   const edges = useGraph(state => state.edges);
 
+  const collapsedNodes = useGraph(state => state.collapsedNodes);
+  const collapsedEdges = useGraph(state => state.collapsedEdges);
+
+  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(","));
+      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]);
+
   const [size, setSize] = React.useState({
     width: 1,
     height: 1,
@@ -68,7 +89,7 @@ const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
   const handleNodeClick = React.useCallback(
     (_: React.MouseEvent<SVGElement>, data: NodeData) => {
       if (setSelectedNode)
-        setSelectedNode({ node: data.text, path: getNodePath(nodes, edges, data.id) });
+        setSelectedNode({ nodeData: data, path: getNodePath(nodes, edges, data.id) });
       if (openNodeModal) openNodeModal();
     },
     [edges, nodes, openNodeModal, setSelectedNode]
@@ -92,12 +113,10 @@ const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
           height: (layout.height as number) + 400,
         });
 
-        requestAnimationFrame(() => {
-          setTimeout(() => {
-            setLoading(false);
-            setTimeout(() => {
-              if (changeRatio > 70 || isWidget) centerView();
-            });
+        setTimeout(() => {
+          setLoading(false);
+          window.requestAnimationFrame(() => {
+            if (changeRatio > 70 || isWidget) centerView();
           });
         });
       }
@@ -110,6 +129,20 @@ const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
     if (input) input.blur();
   }, []);
 
+  const memoizedNode = React.useCallback(
+    (props: JSX.IntrinsicAttributes & NodeProps<any>) => (
+      <CustomNode {...props} onClick={handleNodeClick} animated={false} />
+    ),
+    [handleNodeClick]
+  );
+
+  const memoizedEdge = React.useCallback(
+    (props: JSX.IntrinsicAttributes & Partial<EdgeProps>) => (
+      <Edge {...props} containerClassName={`edge-${props.id}`} />
+    ),
+    []
+  );
+
   if (nodes.length > 8_000) return <ErrorView />;
 
   if (nodes.length > 1_000 && !isWidget) {
@@ -117,50 +150,50 @@ const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
   }
 
   return (
-    <StyledEditorWrapper onContextMenu={e => e.preventDefault()} widget={isWidget}>
+    <>
       <Loading message="Painting graph..." loading={loading} />
-      <TransformWrapper
-        maxScale={2}
-        minScale={0.05}
-        initialScale={0.4}
-        wheel={{ step: 0.08 }}
-        zoomAnimation={{ animationType: "linear" }}
-        doubleClick={{ disabled: true }}
-        onInit={onInit}
-        onPanning={ref => ref.instance.wrapperComponent?.classList.add("dragging")}
-        onPanningStop={ref => ref.instance.wrapperComponent?.classList.remove("dragging")}
-      >
-        <TransformComponent
-          wrapperStyle={{
-            width: "100%",
-            height: "100%",
-            overflow: "hidden",
-            display: loading ? "none" : "block",
-          }}
+      <StyledEditorWrapper onContextMenu={e => e.preventDefault()} widget={isWidget}>
+        <TransformWrapper
+          maxScale={2}
+          minScale={0.05}
+          initialScale={0.4}
+          wheel={{ step: 0.08 }}
+          zoomAnimation={{ animationType: "linear" }}
+          doubleClick={{ disabled: true }}
+          onInit={onInit}
+          onPanning={ref => ref.instance.wrapperComponent?.classList.add("dragging")}
+          onPanningStop={ref => ref.instance.wrapperComponent?.classList.remove("dragging")}
         >
-          <Canvas
-            className="jsoncrack-canvas"
-            nodes={nodes}
-            edges={edges}
-            maxWidth={size.width}
-            maxHeight={size.height}
-            direction={direction}
-            onLayoutChange={onLayoutChange}
-            onCanvasClick={onCanvasClick}
-            zoomable={false}
-            animated={false}
-            readonly={true}
-            dragEdge={null}
-            dragNode={null}
-            fit={true}
-            key={direction}
-            node={props => <CustomNode {...props} onClick={handleNodeClick} />}
-            edge={props => <Edge {...props} containerClassName={`edge-${props.id}`} />}
-          />
-        </TransformComponent>
-      </TransformWrapper>
-    </StyledEditorWrapper>
+          <TransformComponent
+            wrapperStyle={{
+              width: "100%",
+              height: "100%",
+              overflow: "hidden",
+              display: loading ? "none" : "block",
+            }}
+          >
+            <Canvas
+              className="jsoncrack-canvas"
+              nodes={nodes}
+              edges={edges}
+              maxWidth={size.width}
+              maxHeight={size.height}
+              direction={direction}
+              onLayoutChange={onLayoutChange}
+              onCanvasClick={onCanvasClick}
+              node={memoizedNode}
+              edge={memoizedEdge}
+              key={direction}
+              zoomable={false}
+              animated={false}
+              readonly={true}
+              dragEdge={null}
+              dragNode={null}
+              fit={true}
+            />
+          </TransformComponent>
+        </TransformWrapper>
+      </StyledEditorWrapper>
+    </>
   );
 };
-
-export const Graph = React.memo(GraphComponent);

+ 0 - 19
src/components/Input/index.tsx

@@ -1,19 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-const StyledInput = styled.input`
-  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-  outline: none;
-  border: none;
-  border-radius: 4px;
-  line-height: 32px;
-  padding: 10px;
-  width: 100%;
-  margin-bottom: 10px;
-  height: 40px;
-`;
-
-type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
-
-export const Input: React.FC<InputProps> = props => <StyledInput {...props} />;

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

@@ -1,4 +1,5 @@
 import React from "react";
+import { Center, Image, Stack, Title } from "@mantine/core";
 import styled, { keyframes } from "styled-components";
 
 interface LoadingProps {
@@ -15,28 +16,26 @@ const fadeIn = keyframes`
   }
 `;
 
-const StyledLoading = styled.div`
+const StyledLoading = styled.div<{ visible: boolean }>`
+  display: ${({ visible }) => (visible ? "grid" : "none")};
   position: fixed;
   top: 0;
   left: 0;
-  display: grid;
   place-content: center;
   width: 100%;
   height: 100vh;
   text-align: center;
-  background: ${({ theme }) => theme.BLACK_DARK};
-  z-index: 36;
-  pointer-events: none;
+  background: rgba(30, 30, 30, 0.8);
+  z-index: 100;
+  pointer-events: visiblePainted;
+  cursor: wait;
   animation: 0.2s ${fadeIn};
   animation-fill-mode: forwards;
   visibility: hidden;
-`;
 
-const StyledLogo = styled.h2`
-  font-weight: 800;
-  font-size: 56px;
-  pointer-events: none;
-  margin-bottom: 10px;
+  img {
+    transform: rotate(45deg) translate(100px, -70px);
+  }
 `;
 
 const StyledText = styled.span`
@@ -45,19 +44,20 @@ const StyledText = styled.span`
 
 const StyledMessage = styled.div`
   color: #b9bbbe;
-  font-size: 24px;
-  font-weight: 500;
+  font-size: 32px;
+  font-weight: 600;
 `;
 
-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>
+export const Loading: React.FC<LoadingProps> = ({ loading = true, message }) => (
+  <Center mx="auto">
+    <StyledLoading visible={loading}>
+      <Stack>
+        <Image maw={150} src="./assets/rocket_ship.webp" alt="loading image" />
+        <Title size="4rem">
+          <StyledText>JSON</StyledText> Crack
+        </Title>
+        <StyledMessage>{message ?? "Preparing the environment for you..."}</StyledMessage>
+      </Stack>
     </StyledLoading>
-  );
-};
+  </Center>
+);

+ 0 - 76
src/components/Modal/index.tsx

@@ -1,76 +0,0 @@
-import React from "react";
-import { Button } from "src/components/Button";
-import useKeyPress from "src/hooks/useKeyPress";
-import * as Styled from "./styles";
-
-type ControlProps = React.PropsWithChildren<{
-  setVisible: (status: boolean) => void;
-}>;
-
-export type ReactComponent = React.FC<React.PropsWithChildren<{}>>;
-
-type ModalTypes = {
-  Header: ReactComponent;
-  Content: ReactComponent;
-  Controls: React.FC<ControlProps>;
-};
-
-export interface ModalProps {
-  visible: boolean;
-  setVisible: React.Dispatch<React.SetStateAction<boolean>> | ((visible: boolean) => void);
-  size?: "sm" | "md" | "lg";
-}
-
-const Header: ReactComponent = ({ children }) => {
-  return (
-    <Styled.HeaderWrapper>
-      <Styled.Title>{children}</Styled.Title>
-    </Styled.HeaderWrapper>
-  );
-};
-
-const Content: ReactComponent = ({ children }) => {
-  return <Styled.ContentWrapper>{children}</Styled.ContentWrapper>;
-};
-
-const Controls: React.FC<ControlProps> = ({ children, setVisible }) => {
-  const handleEspacePress = useKeyPress("Escape");
-
-  React.useEffect(() => {
-    if (handleEspacePress) setVisible(false);
-  }, [handleEspacePress, setVisible]);
-
-  return (
-    <Styled.ControlsWrapper>
-      <Button onClick={() => setVisible(false)}>Close</Button>
-      {children}
-    </Styled.ControlsWrapper>
-  );
-};
-
-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(false);
-    }
-  };
-
-  if (!visible) return null;
-
-  return (
-    <Styled.ModalWrapper onClick={onClick}>
-      <Styled.ModalInnerWrapper size={size}>{children}</Styled.ModalInnerWrapper>
-    </Styled.ModalWrapper>
-  );
-};
-
-Modal.Header = Header;
-Modal.Content = Content;
-Modal.Controls = Controls;
-
-export { Modal };

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

@@ -1,69 +0,0 @@
-import styled, { keyframes } from "styled-components";
-
-const appearAnimation = keyframes`
-  from { transform: scale(0.6); opacity: 0; }
-  to { transform: scale(1); opacity: 1; };
-`;
-
-export const ModalWrapper = styled.div`
-  position: fixed;
-  top: 0;
-  left: 0;
-  height: 100vh;
-  width: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: rgba(0, 0, 0, 0.85);
-  z-index: 36;
-
-  * {
-    box-sizing: border-box;
-  }
-`;
-
-export const ModalInnerWrapper = styled.div<{ size: "sm" | "md" | "lg" }>`
-  min-width: 440px;
-  max-width: ${({ size }) => (size === "sm" ? "490px" : size === "md" ? "50%" : "80%")};
-  width: fit-content;
-  animation: ${appearAnimation} 220ms ease-in-out;
-  line-height: 20px;
-
-  @media only screen and (max-width: 768px) {
-    min-width: 90%;
-    max-width: 90%;
-  }
-`;
-
-export const Title = styled.h2`
-  display: flex;
-  align-items: center;
-  gap: 5px;
-  color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
-  font-size: 20px;
-  margin: 0;
-`;
-
-export const HeaderWrapper = styled.div`
-  background: ${({ theme }) => theme.MODAL_BACKGROUND};
-  padding: 16px;
-  border-radius: 5px 5px 0 0;
-`;
-
-export const ContentWrapper = styled.div`
-  background: ${({ theme }) => theme.MODAL_BACKGROUND};
-  color: ${({ theme }) => theme.TEXT_NORMAL};
-  padding: 16px;
-  overflow: hidden auto;
-  height: fit-content;
-  max-height: calc(100vh - 156px);
-`;
-
-export const ControlsWrapper = styled.div`
-  display: flex;
-  flex-direction: row-reverse;
-  background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
-  padding: 12px;
-  border-radius: 0 0 5px 5px;
-  gap: 10px;
-`;

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

@@ -77,7 +77,7 @@ export const SearchInput: React.FC = () => {
     <StyledInputWrapper>
       <StyledForm onSubmit={onSubmit}>
         <StyledInput
-          type="text"
+          type="search"
           value={content.value}
           onChange={e => setContent(val => ({ ...val, value: e.target.value }))}
           placeholder="Search Node"

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

@@ -1,4 +1,5 @@
 import React from "react";
+import { Tooltip } from "@mantine/core";
 import toast from "react-hot-toast";
 import { AiOutlineDelete, AiOutlineSave, AiOutlineFileAdd, AiOutlineEdit } from "react-icons/ai";
 import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
@@ -10,7 +11,6 @@ import {
   VscExpandAll,
   VscSettingsGear,
 } from "react-icons/vsc";
-import { Tooltip } from "src/components/Tooltip";
 import useGraph from "src/store/useGraph";
 import useJson from "src/store/useJson";
 import useModal from "src/store/useModal";
@@ -136,7 +136,7 @@ const SidebarButton: React.FC<{
   component: React.ReactNode;
 }> = ({ onClick, deviceDisplay, title, component }) => {
   return (
-    <Tooltip className={deviceDisplay} title={title}>
+    <Tooltip className={deviceDisplay} label={title} color="gray" position="right" withArrow>
       <StyledElement onClick={onClick}>{component}</StyledElement>
     </Tooltip>
   );

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

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

+ 12 - 5
src/components/Sponsors/index.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import { Avatar, Tooltip, UnstyledButton } from "@mantine/core";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
 
@@ -83,11 +84,17 @@ export const Sponsors = () => {
   return (
     <StyledSponsorsWrapper>
       {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" />
-          </a>
-        </StyledSponsor>
+        <Tooltip label={user.handle} key={user.handle}>
+          <UnstyledButton
+            component="a"
+            href={user.profile}
+            variant="subtle"
+            target="_blank"
+            rel="noreferrer"
+          >
+            <Avatar radius="md" src={user.avatar} alt={user.handle} />
+          </UnstyledButton>
+        </Tooltip>
       ))}
     </StyledSponsorsWrapper>
   );

+ 0 - 65
src/components/Toggle/index.tsx

@@ -1,65 +0,0 @@
-import React from "react";
-import { IoIosCheckmarkCircle, IoMdCloseCircle } from "react-icons/io";
-import styled from "styled-components";
-
-interface ToggleProps {
-  checked?: boolean;
-  children?: React.ReactNode;
-  onChange?: (value: boolean) => void;
-}
-
-const StyledToggleWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  user-select: none;
-  width: 100%;
-  gap: 6px;
-`;
-
-const StyledLabel = styled.label`
-  color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
-  font-weight: 500;
-  cursor: pointer;
-`;
-
-const StyledToggle = styled.div<{ active: boolean }>`
-  position: relative;
-  display: flex;
-  justify-content: ${({ active }) => (active ? "right" : "left")};
-  align-items: center;
-  width: 40px;
-  height: 24px;
-  padding: 2px;
-  border-radius: 14px;
-  background: ${({ active }) => (active ? "#3AA55D" : "#72767c")};
-  cursor: pointer;
-
-  input {
-    display: none;
-  }
-`;
-
-const Toggle: React.FC<ToggleProps> = ({ children, checked = false, onChange }) => {
-  const [isChecked, setIsChecked] = React.useState(checked);
-
-  const handleClick = () => {
-    setIsChecked(!isChecked);
-    if (onChange) onChange(!isChecked);
-  };
-
-  return (
-    <StyledToggleWrapper>
-      <StyledToggle active={isChecked} onClick={handleClick}>
-        {isChecked ? (
-          <IoIosCheckmarkCircle size={22} color="white" />
-        ) : (
-          <IoMdCloseCircle size={22} color="white" />
-        )}
-        <input type="checkbox" checked={isChecked} onChange={handleClick} />
-      </StyledToggle>
-      <StyledLabel onClick={handleClick}>{children}</StyledLabel>
-    </StyledToggleWrapper>
-  );
-};
-
-export default Toggle;

+ 0 - 62
src/components/Tooltip/index.tsx

@@ -1,62 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-
-interface TooltipProps extends React.ComponentPropsWithoutRef<"div"> {
-  title?: string;
-}
-
-const StyledTooltip = styled.div`
-  position: absolute;
-  display: none;
-  top: 0;
-  right: 0;
-  transform: translate(calc(100% + 15px), 25%);
-  z-index: 2;
-  background: ${({ theme }) => theme.BACKGROUND_PRIMARY};
-  color: ${({ theme }) => theme.TEXT_NORMAL};
-  border-radius: 5px;
-  padding: 6px 8px;
-  white-space: nowrap;
-  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);
-
-  &::after {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    transform: translate(-90%, 50%);
-    border-width: 8px;
-    border-style: solid;
-    border-color: transparent ${({ theme }) => theme.BACKGROUND_PRIMARY} transparent transparent;
-  }
-
-  @media only screen and (max-width: 768px) {
-    display: none;
-  }
-`;
-
-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
-}) => (
-  <StyledTooltipWrapper {...props}>
-    {title && <StyledTooltip>{title}</StyledTooltip>}
-    <div>{children}</div>
-  </StyledTooltipWrapper>
-);

+ 14 - 1
src/constants/globalStyle.ts

@@ -1,5 +1,13 @@
+import localFont from "next/font/local";
 import { createGlobalStyle } from "styled-components";
 
+const monaSans = localFont({
+  src: "../pages/Mona-Sans.woff2",
+  variable: "--mona-sans",
+  display: "swap",
+  fallback: ["Arial, Helvetica, sans-serif", "Tahoma, Verdana, sans-serif"],
+});
+
 const GlobalStyle = createGlobalStyle`
   html, body {
     margin: 0;
@@ -14,6 +22,7 @@ const GlobalStyle = createGlobalStyle`
     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;
+    font-family: ${monaSans.style.fontFamily};
 
     @media only screen and (max-width: 768px) {
       background-position: right;
@@ -29,8 +38,12 @@ const GlobalStyle = createGlobalStyle`
     display: none;
   }
 
+  .mantine-Modal-inner {
+    padding: 0;
+  }
+
   svg {
-    vertical-align: top;
+    vertical-align: text-top;
   }
 
   

+ 1 - 24
src/containers/Editor/LiveEditor/GraphCanvas.tsx

@@ -1,38 +1,15 @@
 import React from "react";
 import { Graph } from "src/components/Graph";
 import { NodeModal } from "src/containers/Modals/NodeModal";
-import useGraph from "src/store/useGraph";
 
 export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
   const [isNodeModalVisible, setNodeModalVisible] = React.useState(false);
-
-  const collapsedNodes = useGraph(state => state.collapsedNodes);
-  const collapsedEdges = useGraph(state => state.collapsedEdges);
-
   const openNodeModal = React.useCallback(() => setNodeModalVisible(true), []);
 
-  React.useEffect(() => {
-    const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
-    const edgeList = collapsedEdges.map(id => `[class$="edge-${id}"]`);
-
-    const hiddenItems = document.querySelectorAll(".hide");
-    hiddenItems.forEach(item => item.classList.remove("hide"));
-
-    if (nodeList.length) {
-      const selectedNodes = document.querySelectorAll(nodeList.join(","));
-      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]);
-
   return (
     <>
       <Graph openNodeModal={openNodeModal} isWidget={isWidget} />
-      <NodeModal visible={isNodeModalVisible} closeModal={() => setNodeModalVisible(false)} />
+      <NodeModal opened={isNodeModalVisible} onClose={() => setNodeModalVisible(false)} />
     </>
   );
 };

+ 5 - 5
src/containers/Editor/LiveEditor/Tools.tsx

@@ -58,20 +58,20 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
 
   return (
     <StyledTools>
-      <StyledToolElement aria-label="fullscreen" hide={isWidget} onClick={toggleEditor}>
+      <StyledToolElement title="fullscreen" hide={isWidget} onClick={toggleEditor}>
         <AiOutlineFullscreen />
       </StyledToolElement>
       <SearchInput />
-      <StyledToolElement aria-label="save" onClick={() => setVisible("download")(true)}>
+      <StyledToolElement title="save" onClick={() => setVisible("download")(true)}>
         <FiDownload />
       </StyledToolElement>
-      <StyledToolElement aria-label="center canvas" onClick={centerView}>
+      <StyledToolElement title="center canvas" onClick={centerView}>
         <MdCenterFocusWeak />
       </StyledToolElement>
-      <StyledToolElement aria-label="zoom out" onClick={zoomOut}>
+      <StyledToolElement title="zoom out" onClick={zoomOut}>
         <AiOutlineMinus />
       </StyledToolElement>
-      <StyledToolElement aria-label="zoom in" onClick={zoomIn}>
+      <StyledToolElement title="zoom in" onClick={zoomIn}>
         <AiOutlinePlus />
       </StyledToolElement>
     </StyledTools>

+ 33 - 34
src/containers/Home/index.tsx

@@ -1,8 +1,10 @@
 import React from "react";
 import dynamic from "next/dynamic";
 import Head from "next/head";
+import Link from "next/link";
 import Script from "next/script";
-import { AiOutlineRight } from "react-icons/ai";
+import { Button } from "@mantine/core";
+import { AiOutlineRight, AiTwotoneStar } from "react-icons/ai";
 import {
   HiCursorClick,
   HiLightningBolt,
@@ -36,30 +38,23 @@ const HeroSection = () => (
       Seamlessly visualize your JSON data{" "}
       <Styles.StyledHighlightedText>instantly</Styles.StyledHighlightedText> into graphs.
     </Styles.StyledSubTitle>
-
-    <Styles.StyledButton href="/editor" link>
-      GO TO EDITOR
-      <AiOutlineRight strokeWidth="80" />
-    </Styles.StyledButton>
-
+    <Link href="/editor">
+      <Button color="grape" size="lg">
+        GO TO EDITOR
+        <AiOutlineRight strokeWidth="80" />
+      </Button>
+    </Link>
     <Styles.StyledButtonWrapper>
-      <Styles.StyledSponsorButton
-        href="https://github.com/sponsors/AykutSarac"
-        target="_blank"
-        rel="noreferrer"
-        link
-      >
-        SPONSOR US
-        <IoHeart />
-      </Styles.StyledSponsorButton>
-      <Styles.StyledSponsorButton
-        href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode"
-        link
-        isBlue
-      >
-        GET IT ON VS CODE
-        <SiVisualstudiocode />
-      </Styles.StyledSponsorButton>
+      <Link href="https://github.com/sponsors/AykutSarac" target="_blank" rel="noreferrer">
+        <Button color="red" size="md" variant="outline" rightIcon={<IoHeart />}>
+          SPONSOR US
+        </Button>
+      </Link>
+      <Link href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode">
+        <Button color="blue" size="md" variant="outline" rightIcon={<SiVisualstudiocode />}>
+          GET IT ON VS CODE
+        </Button>
+      </Link>
     </Styles.StyledButtonWrapper>
   </Styles.StyledHeroSection>
 );
@@ -202,13 +197,16 @@ const GitHubSection = () => (
         <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
+      <Button
+        w={200}
+        color="grape"
+        size="md"
+        component="a"
         href="https://github.com/AykutSarac/jsoncrack.com"
-        status="SECONDARY"
-        link
+        leftIcon={<AiTwotoneStar />}
       >
         STAR ON GITHUB
-      </Styles.StyledButton>
+      </Button>
     </Styles.StyledSectionArea>
   </Styles.StyledSection>
 );
@@ -228,9 +226,9 @@ const EmbedSection = () => (
         intuitive interface makes it easy to navigate and understand even complex JSON data, making
         it a valuable tool for anyone working with JSON.
       </Styles.StyledMinorTitle>
-      <Styles.StyledButton href="/docs" status="SECONDARY" link>
+      <Button w={200} size="md" component="a" href="/docs">
         LEARN TO EMBED
-      </Styles.StyledButton>
+      </Button>
     </Styles.StyledSectionArea>
     <div>
       <Styles.StyledFrame
@@ -265,14 +263,15 @@ const SponsorSection = () => (
     <Styles.StyledMinorTitle>
       Your supports make JSON Crack possible to continue and accessible for everyone!
     </Styles.StyledMinorTitle>
-    <Styles.StyledButton
+    <Button
+      size="md"
+      component="a"
+      color="green"
       href="https://github.com/sponsors/AykutSarac"
       rel="external"
-      status="SUCCESS"
-      link
     >
       Become A Sponsor!
-    </Styles.StyledButton>
+    </Button>
     <Sponsors />
   </Styles.StyledSponsorSection>
 );

+ 0 - 36
src/containers/Home/styles.tsx

@@ -1,5 +1,4 @@
 import Link from "next/link";
-import { Button } from "src/components/Button";
 import styled from "styled-components";
 
 export const StyledButtonWrapper = styled.div`
@@ -157,41 +156,6 @@ export const StyledMinorTitle = styled.p`
   }
 `;
 
-export const StyledButton = styled(Button)`
-  background: ${({ status }) => !status && "#a13cc2"};
-  padding: 12px 24px;
-  height: 46px;
-
-  div {
-    font-weight: 700;
-    font-size: 1rem;
-  }
-`;
-
-export const StyledSponsorButton = styled(Button)<{ isBlue?: boolean }>`
-  background: transparent;
-  border: 1px solid ${({ isBlue }) => (isBlue ? "#1F9CF0" : "#ee3d48")};
-  transition: all 200ms;
-  padding: 12px 24px;
-
-  div {
-    font-weight: 700;
-    font-size: 16px;
-  }
-
-  svg {
-    color: ${({ isBlue }) => (isBlue ? "#1F9CF0" : "#ee3d48")};
-  }
-
-  &:hover {
-    background: ${({ isBlue }) => (isBlue ? "#1F9CF0" : "#ee3d48")};
-
-    svg {
-      color: white;
-    }
-  }
-`;
-
 export const StyledFeaturesSection = styled.section`
   display: grid;
   margin: 0 auto;

+ 8 - 8
src/containers/ModalController/index.tsx

@@ -38,14 +38,14 @@ export const ModalController = () => {
 
   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")} />
+      <ImportModal opened={importModal} onClose={() => setVisible("import")(false)} />
+      <ClearModal opened={clearModal} onClose={() => setVisible("clear")(false)} />
+      <DownloadModal opened={downloadModal} onClose={() => setVisible("download")(false)} />
+      <SettingsModal opened={settingsModal} onClose={() => setVisible("settings")(false)} />
+      <CloudModal opened={cloudModal} onClose={() => setVisible("cloud")(false)} />
+      <AccountModal opened={accountModal} onClose={() => setVisible("account")(false)} />
+      <LoginModal opened={loginModal} onClose={() => setVisible("login")(false)} />
+      <ShareModal opened={shareModal} onClose={() => setVisible("share")(false)} />
     </>
   );
 };

+ 58 - 79
src/containers/Modals/AccountModal/index.tsx

@@ -1,18 +1,16 @@
 import React from "react";
+import Link from "next/link";
+import { Modal, Group, Button, Badge, Avatar, Grid, Divider, ModalProps } from "@mantine/core";
 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`
+const StyledTitle = styled.div`
   display: flex;
   align-items: center;
   color: ${({ theme }) => theme.TEXT_POSITIVE};
   flex: 1;
   font-weight: 700;
-  margin-top: 0;
 
   &::after {
     background: ${({ theme }) => theme.TEXT_POSITIVE};
@@ -27,23 +25,6 @@ const StyledTitle = styled.p`
   }
 `;
 
-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;
@@ -61,7 +42,7 @@ const StyledContainer = styled.div`
   }
 `;
 
-const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) => {
+export const AccountModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const user = useUser(state => state.user);
   const isPremium = useUser(state => state.isPremium());
   const logout = useUser(state => state.logout);
@@ -71,73 +52,71 @@ const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) =
   };
 
   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 ? (
+    <Modal title="Account" opened={opened} onClose={onClose} centered>
+      <StyledTitle>Hello, {user?.name}!</StyledTitle>
+      <Group py="sm">
+        <Grid gutter="xs">
+          <Grid.Col span={2}>
+            <Avatar size="lg" radius="lg" src={user?.profilePicture} alt={user?.name} />
+          </Grid.Col>
+          <Grid.Col span={4}>
+            <StyledContainer>
+              USERNAME
+              <div>{user?.name}</div>
+            </StyledContainer>
+          </Grid.Col>
+          <Grid.Col span={6}>
+            <StyledContainer>
+              ACCOUNT STATUS
+              <div>{isPremium ? <Badge>Premium</Badge> : <Badge color="gray">Free</Badge>}</div>
+            </StyledContainer>
+          </Grid.Col>
+          <Grid.Col span={6}>
+            <StyledContainer>
+              EMAIL
+              <div>{user?.email}</div>
+            </StyledContainer>
+          </Grid.Col>
+          <Grid.Col span={4}>
+            <StyledContainer>
+              REGISTRATION
+              <div>{user?.signUpAt && new Date(user.signUpAt).toDateString()}</div>
+            </StyledContainer>
+          </Grid.Col>
+        </Grid>
+      </Group>
+      <Divider py="xs" />
+      <Group position="right">
+        {isPremium ? (
+          <Button
+            variant="light"
+            color="red"
+            onClick={() => window.open("https://patreon.com/jsoncrack", "_blank")}
+            leftIcon={<IoRocketSharp />}
+          >
+            Cancel Subscription
+          </Button>
+        ) : (
+          <Link href="/pricing" target="_blank" rel="noreferrer">
             <Button
-              status="DANGER"
-              block
-              onClick={() => window.open("https://patreon.com/jsoncrack", "_blank")}
+              variant="gradient"
+              gradient={{ from: "teal", to: "lime", deg: 105 }}
+              leftIcon={<IoRocketSharp />}
             >
-              <IoRocketSharp />
-              Cancel Subscription
-            </Button>
-          ) : (
-            <Button href="/pricing" status="TERTIARY" block link>
-              <IoRocketSharp />
               UPGRADE TO PREMIUM!
             </Button>
-          )}
-        </StyledAccountWrapper>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible}>
+          </Link>
+        )}
         <Button
-          status="DANGER"
+          color="red"
           onClick={() => {
             logout();
-            setVisible(false);
+            onClose();
           }}
         >
           Log Out
         </Button>
-      </Modal.Controls>
-    </>
-  );
-};
-
-export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
-  return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <AccountView setVisible={setVisible} />
+      </Group>
     </Modal>
   );
 };

+ 11 - 10
src/containers/Modals/ClearModal/index.tsx

@@ -1,17 +1,16 @@
 import React from "react";
 import { useRouter } from "next/router";
-import { Button } from "src/components/Button";
-import { Modal, ModalProps } from "src/components/Modal";
+import { Modal, Group, Button, Text, Divider, ModalProps } from "@mantine/core";
 import { deleteJson } from "src/services/db/json";
 import useJson from "src/store/useJson";
 
-export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const ClearModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const setJson = useJson(state => state.setJson);
   const { query, replace } = useRouter();
 
   const handleClear = () => {
     setJson("{}");
-    setVisible(false);
+    onClose();
 
     if (typeof query.json === "string") {
       deleteJson(query.json);
@@ -20,14 +19,16 @@ export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   };
 
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <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}>
+    <Modal title="Delete JSON" opened={opened} onClose={onClose} centered>
+      <Group py="sm">
+        <Text>Are you sure you want to delete JSON?</Text>
+      </Group>
+      <Divider py="xs" />
+      <Group position="right">
+        <Button color="red" onClick={handleClear}>
           Confirm
         </Button>
-      </Modal.Controls>
+      </Group>
     </Modal>
   );
 };

+ 34 - 59
src/containers/Modals/CloudModal/index.tsx

@@ -1,21 +1,24 @@
 import React from "react";
 import { useRouter } from "next/router";
+import {
+  Modal,
+  Group,
+  Button,
+  Text,
+  Stack,
+  Loader,
+  Center,
+  Divider,
+  ScrollArea,
+  ModalProps,
+} from "@mantine/core";
 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 { AiOutlineEdit, 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";
@@ -24,18 +27,13 @@ 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};
+  line-height: 20px;
+  padding: 6px;
   border: 2px solid ${({ theme, active }) => (active ? theme.SEAGREEN : theme.BLACK_SECONDARY)};
   border-radius: 5px;
   overflow: hidden;
@@ -43,9 +41,7 @@ const StyledJsonCard = styled.a<{ active?: boolean; create?: boolean }>`
   height: 160px;
 `;
 
-const StyledInfo = styled.div`
-  padding: 4px 6px;
-`;
+const StyledInfo = styled.div``;
 
 const StyledTitle = styled.div`
   display: flex;
@@ -69,18 +65,6 @@ const StyledDetils = styled.div`
   gap: 4px;
 `;
 
-const StyledModal = styled(Modal)`
-  #modal-view {
-    display: none;
-  }
-`;
-
-const StyledDeleteButton = styled(Button)`
-  background: transparent;
-  color: ${({ theme }) => theme.CRIMSON};
-  opacity: 0.8;
-`;
-
 const StyledCreateWrapper = styled.div`
   display: flex;
   height: 100%;
@@ -102,16 +86,6 @@ const StyledNameInput = styled.input`
   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,
@@ -179,9 +153,9 @@ const GraphCard: React.FC<{ data: Json; refetch: () => void; active: boolean }>
           Last modified {dayjs(data.updatedAt).fromNow()}
         </StyledDetils>
       </StyledInfo>
-      <StyledDeleteButton onClick={onDeleteClick}>
+      <Button variant="subtle" color="red" onClick={onDeleteClick}>
         <FaTrash />
-      </StyledDeleteButton>
+      </Button>
     </StyledJsonCard>
   );
 };
@@ -230,20 +204,21 @@ const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => {
   );
 };
 
-export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const CloudModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const { isReady, query } = useRouter();
 
   const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), {
-    enabled: isReady && visible,
+    enabled: isReady && opened,
   });
 
   return (
-    <StyledModal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Saved On The Cloud</Modal.Header>
-      <Modal.Content>
-        <StyledModalContent>
+    <Modal title="Saved On The Cloud" opened={opened} onClose={onClose} centered>
+      <ScrollArea h={360}>
+        <Stack py="sm">
           {isFetching ? (
-            <Spinner />
+            <Center>
+              <Loader />
+            </Center>
           ) : (
             <>
               <CreateCard reachedLimit={data ? data?.data.result.length > 15 : false} />
@@ -257,16 +232,16 @@ export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
               ))}
             </>
           )}
-        </StyledModalContent>
-      </Modal.Content>
+        </Stack>
+      </ScrollArea>
 
-      <Modal.Controls setVisible={setVisible}>
-        <StyledInfoText>
-          <AiOutlineInfoCircle />
+      <Divider py="xs" />
+      <Group position="right">
+        <Text fz="xs">
           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>
+        </Text>
+      </Group>
+    </Modal>
   );
 };

+ 69 - 108
src/containers/Modals/DownloadModal/index.tsx

@@ -1,35 +1,27 @@
 import React from "react";
+import {
+  ColorPicker,
+  TextInput,
+  SegmentedControl,
+  Group,
+  Modal,
+  Button,
+  Divider,
+  Grid,
+  ModalProps,
+  ColorInput,
+  Stack,
+} from "@mantine/core";
 import { toBlob, toPng, toSvg } from "html-to-image";
-import { TwitterPicker } from "react-color";
-import { TwitterPickerStylesProps } from "react-color/lib/components/twitter/Twitter";
 import toast from "react-hot-toast";
 import { FiCopy, FiDownload } from "react-icons/fi";
-import { Button } from "src/components/Button";
-import { FileInput } from "src/components/FileInput";
-import { Modal, ModalProps } from "src/components/Modal";
-import styled from "styled-components";
-
-const ColorPickerStyles: Partial<TwitterPickerStylesProps> = {
-  card: {
-    background: "transparent",
-    boxShadow: "none",
-  },
-  body: {
-    padding: 0,
-  },
-  input: {
-    background: "rgba(0, 0, 0, 0.2)",
-    boxShadow: "none",
-    textTransform: "none",
-    whiteSpace: "nowrap",
-    textOverflow: "ellipsis",
-  },
-  hash: {
-    background: "rgba(180, 180, 180, 0.3)",
-  },
-};
 
-const defaultColors = [
+enum Extensions {
+  SVG = "svg",
+  PNG = "png",
+}
+
+const swatches = [
   "#B80000",
   "#DB3E00",
   "#FCCB00",
@@ -58,48 +50,11 @@ function downloadURI(uri: string, name: string) {
   document.body.removeChild(link);
 }
 
-const StyledContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  padding: 12px 0;
-  border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
-  font-size: 12px;
-  line-height: 16px;
-  font-weight: 600;
-  text-transform: uppercase;
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-
-  &:first-of-type {
-    padding-top: 0;
-    border: none;
-  }
-`;
-
-const StyledColorWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-`;
-
-const StyledColorIndicator = styled.div<{ color: string }>`
-  flex: 1;
-  width: 100%;
-  height: auto;
-  border-radius: 6px;
-  background: ${({ color }) => color};
-  border: 1px solid;
-  border-color: rgba(0, 0, 0, 0.1);
-`;
-
-enum Extensions {
-  svg,
-  png,
-}
-export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const [extension, setExtension] = React.useState(Extensions.png);
+export const DownloadModal: React.FC<ModalProps> = ({ opened, onClose }) => {
+  const [extension, setExtension] = React.useState(Extensions.PNG);
   const [fileDetails, setFileDetails] = React.useState({
     filename: "jsoncrack.com",
-    backgroundColor: "transparent",
+    backgroundColor: "rgba(255, 255, 255, 1)",
     quality: 1,
   });
 
@@ -127,7 +82,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
       toast.error("Failed to copy to clipboard");
     } finally {
       toast.dismiss("toastClipboard");
-      setVisible(false);
+      onClose();
     }
   };
 
@@ -137,19 +92,19 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
 
       const imageElement = document.querySelector("svg[id*='ref']") as HTMLElement;
 
-      let exportImage = extension === Extensions.svg ? toSvg : toPng;
+      let exportImage = extension === Extensions.SVG ? toSvg : toPng;
 
       const dataURI = await exportImage(imageElement, {
         quality: fileDetails.quality,
         backgroundColor: fileDetails.backgroundColor,
       });
 
-      downloadURI(dataURI, `${fileDetails.filename}.${Extensions[extension]}`);
+      downloadURI(dataURI, `${fileDetails.filename}.${extension}`);
     } catch (error) {
       toast.error("Failed to download image!");
     } finally {
       toast.dismiss("toastDownload");
-      setVisible(false);
+      onClose();
     }
   };
 
@@ -157,46 +112,52 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
     setFileDetails({ ...fileDetails, [key]: value });
 
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Download Image</Modal.Header>
-      <Modal.Content>
-        <StyledContainer>
-          File Name
-          <StyledColorWrapper>
-            <FileInput
-              value={fileDetails.filename}
-              onChange={e => updateDetails("filename", e.target.value)}
-              setExtension={setExtension}
-              activeExtension={extension}
-              extensions={Object.keys(Extensions).filter(v => isNaN(Number(v)))}
-            />
-          </StyledColorWrapper>
-        </StyledContainer>
-        <StyledContainer>
-          Background Color
-          <StyledColorWrapper>
-            <TwitterPicker
-              triangle="hide"
-              colors={defaultColors}
-              color={fileDetails.backgroundColor}
-              onChange={color => updateDetails("backgroundColor", color.hex)}
-              styles={{
-                default: ColorPickerStyles,
-              }}
-            />
-            <StyledColorIndicator color={fileDetails.backgroundColor} />
-          </StyledColorWrapper>
-        </StyledContainer>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible}>
-        <Button status="SECONDARY" onClick={clipboardImage}>
-          <FiCopy size={18} /> Clipboard
+    <Modal opened={opened} onClose={onClose} title="Download Image" centered>
+      <Grid py="sm" align="end" grow>
+        <Grid.Col span={8}>
+          <TextInput
+            label="File Name"
+            value={fileDetails.filename}
+            onChange={e => updateDetails("filename", e.target.value)}
+          />
+        </Grid.Col>
+        <Grid.Col span={4}>
+          <SegmentedControl
+            value={extension}
+            onChange={e => setExtension(e as Extensions)}
+            data={[
+              { label: "SVG", value: Extensions.SVG },
+              { label: "PNG", value: Extensions.PNG },
+            ]}
+            fullWidth
+          />
+        </Grid.Col>
+      </Grid>
+      <Stack py="sm">
+        <ColorInput
+          label="Background Color"
+          value={fileDetails.backgroundColor}
+          onChange={color => updateDetails("backgroundColor", color)}
+          withEyeDropper={false}
+        />
+        <ColorPicker
+          format="rgba"
+          value={fileDetails.backgroundColor}
+          onChange={color => updateDetails("backgroundColor", color)}
+          swatches={swatches}
+          withPicker={false}
+          fullWidth
+        />
+      </Stack>
+      <Divider py="xs" />
+      <Group position="right">
+        <Button leftIcon={<FiCopy />} onClick={clipboardImage}>
+          Clipboard
         </Button>
-        <Button status="SUCCESS" onClick={exportAsImage}>
-          <FiDownload size={18} />
+        <Button color="green" leftIcon={<FiDownload />} onClick={exportAsImage}>
           Download
         </Button>
-      </Modal.Controls>
+      </Group>
     </Modal>
   );
 };

+ 13 - 23
src/containers/Modals/ImportModal/index.tsx

@@ -1,19 +1,10 @@
 import React from "react";
+import { Modal, Group, Button, TextInput, Stack, Divider, ModalProps } from "@mantine/core";
 import toast from "react-hot-toast";
 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 useJson from "src/store/useJson";
 import styled from "styled-components";
 
-const StyledModalContent = styled(Modal.Content)`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  flex-direction: column;
-`;
-
 const StyledUploadWrapper = styled.label`
   display: flex;
   flex-direction: column;
@@ -22,7 +13,6 @@ const StyledUploadWrapper = styled.label`
   background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
   border: 2px dashed ${({ theme }) => theme.BACKGROUND_TERTIARY};
   border-radius: 5px;
-  width: 100%;
   min-height: 200px;
   padding: 16px;
   cursor: pointer;
@@ -42,7 +32,7 @@ const StyledUploadMessage = styled.h3`
   margin-bottom: 0;
 `;
 
-export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const ImportModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const setJson = useJson(state => state.setJson);
   const [url, setURL] = React.useState("");
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
@@ -72,7 +62,7 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
         .then(res => res.json())
         .then(json => {
           setJson(JSON.stringify(json, null, 2));
-          setVisible(false);
+          onClose();
         })
         .catch(() => toast.error("Failed to fetch JSON!"))
         .finally(() => toast.dismiss("toastFetch"));
@@ -84,21 +74,20 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
       reader.readAsText(jsonFile, "UTF-8");
       reader.onload = function (data) {
         setJson(data.target?.result as string);
-        setVisible(false);
+        onClose();
       };
     }
   };
 
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Import JSON</Modal.Header>
-      <StyledModalContent>
-        <Input
+    <Modal title="Import JSON" opened={opened} onClose={onClose} centered>
+      <Stack py="sm">
+        <TextInput
           value={url}
           onChange={e => setURL(e.target.value)}
           type="url"
           placeholder="URL of JSON to fetch"
-          autoFocus
+          data-autofocus
         />
         <StyledUploadWrapper onDrop={handleFileDrag} onDragOver={handleFileDrag}>
           <input
@@ -111,12 +100,13 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
           <StyledUploadMessage>Click Here to Upload JSON</StyledUploadMessage>
           <StyledFileName>{jsonFile?.name ?? "None"}</StyledFileName>
         </StyledUploadWrapper>
-      </StyledModalContent>
-      <Modal.Controls setVisible={setVisible}>
-        <Button status="SECONDARY" onClick={handleImportFile} disabled={!(jsonFile || url)}>
+      </Stack>
+      <Divider py="xs" />
+      <Group position="right">
+        <Button onClick={handleImportFile} disabled={!(jsonFile || url)}>
           Import
         </Button>
-      </Modal.Controls>
+      </Group>
     </Modal>
   );
 };

+ 10 - 16
src/containers/Modals/LoginModal/index.tsx

@@ -1,22 +1,16 @@
 import React from "react";
-import Link from "next/link";
-import { Button } from "src/components/Button";
-import { Modal, ModalProps } from "src/components/Modal";
+import { Modal, Stack, Button, Text, Title, ModalProps } from "@mantine/core";
 
-export const LoginModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
+export const LoginModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   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>
-        <Link href="/sign-in">
-          <Button onClick={() => setVisible(false)} status="SECONDARY" block>
-            Sign In
-          </Button>
-        </Link>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible} />
+    <Modal title="Sign In" opened={opened} onClose={onClose} centered>
+      <Stack py="sm">
+        <Title order={2}>Welcome Back!</Title>
+        <Text>Login to unlock full potential of JSON Crack!</Text>
+        <Button component="a" href="/sign-in" size="md" fullWidth>
+          Sign In
+        </Button>
+      </Stack>
     </Modal>
   );
 };

+ 40 - 87
src/containers/Modals/NodeModal/index.tsx

@@ -1,97 +1,50 @@
 import React from "react";
-import dynamic from "next/dynamic";
-import toast from "react-hot-toast";
-import { FiCopy } from "react-icons/fi";
-import vs from "react-syntax-highlighter/dist/cjs/styles/prism/vs";
-import vscDarkPlus from "react-syntax-highlighter/dist/cjs/styles/prism/vsc-dark-plus";
-import { Button } from "src/components/Button";
-import { Modal } from "src/components/Modal";
+import { Modal, Stack, Text, ScrollArea, ModalProps } from "@mantine/core";
+import { Prism } from "@mantine/prism";
 import useGraph from "src/store/useGraph";
-import useStored from "src/store/useStored";
-import styled from "styled-components";
+import { dataToString } from "src/utils/dataToString";
+import { shallow } from "zustand/shallow";
 
-const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter/dist/cjs/prism-async"), {
-  ssr: false,
-});
-
-interface NodeModalProps {
-  visible: boolean;
-  closeModal: () => void;
-}
-
-const StyledTitle = styled.div`
-  line-height: 16px;
-  font-size: 12px;
-  font-weight: 600;
-  padding: 16px 0;
-
-  &:first-of-type {
-    padding-top: 0;
-  }
-`;
-
-export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
-  const lightmode = useStored(state => state.lightmode);
-  const path = useGraph(state => state.path);
-  const nodeData = useGraph(state =>
-    Array.isArray(state.selectedNode) ? Object.fromEntries(state.selectedNode) : state.selectedNode
+const CodeBlock: React.FC<{ children: any }> = ({ children }) => {
+  return (
+    <ScrollArea>
+      <Prism
+        maw={600}
+        miw={350}
+        mah={250}
+        language="json"
+        copyLabel="Copy to clipboard"
+        copiedLabel="Copied to clipboard"
+        withLineNumbers
+      >
+        {children}
+      </Prism>
+    </ScrollArea>
   );
+};
 
-  const handleClipboard = (content: string) => {
-    try {
-      navigator.clipboard?.writeText(content);
-      toast.success("Content copied to clipboard!");
-      closeModal();
-    } catch (error) {
-      toast.error("Failed to save to clipboard.");
-    }
-  };
+export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
+  const [nodeData, path] = useGraph(
+    state => [dataToString(state.selectedNode.text), state.selectedNode.path],
+    shallow
+  );
 
   return (
-    <Modal visible={visible} setVisible={closeModal} size="lg">
-      <Modal.Header>Node Content</Modal.Header>
-      <Modal.Content>
-        <StyledTitle>Content</StyledTitle>
-        <SyntaxHighlighter
-          style={lightmode ? vs : vscDarkPlus}
-          customStyle={{
-            borderRadius: "4px",
-            fontSize: "14px",
-            margin: "0",
-          }}
-          language="json"
-          showLineNumbers
-        >
-          {JSON.stringify(
-            nodeData,
-            (_, v) => {
-              if (typeof v === "string") return v.replaceAll('"', "");
-              return v;
-            },
-            2
-          )}
-        </SyntaxHighlighter>
-        <StyledTitle>Node Path</StyledTitle>
-        <SyntaxHighlighter
-          style={lightmode ? vs : vscDarkPlus}
-          customStyle={{
-            borderRadius: "4px",
-            fontSize: "14px",
-            margin: "0",
-          }}
-          language="javascript"
-        >
-          {path}
-        </SyntaxHighlighter>
-      </Modal.Content>
-      <Modal.Controls setVisible={closeModal}>
-        <Button status="SECONDARY" onClick={() => handleClipboard(JSON.stringify(nodeData))}>
-          <FiCopy size={18} /> Clipboard
-        </Button>
-        <Button status="SECONDARY" onClick={() => handleClipboard(path)}>
-          <FiCopy size={18} /> Copy Path
-        </Button>
-      </Modal.Controls>
+    <Modal title="Node Content" size="auto" opened={opened} onClose={onClose} centered>
+      <Stack py="sm" spacing="sm">
+        <Stack spacing="xs">
+          <Text fz="sm" fw={700}>
+            Content
+          </Text>
+          <CodeBlock>{nodeData}</CodeBlock>
+        </Stack>
+        <Stack spacing="xs">
+          <Text fz="sm" fw={700}>
+            Node Path
+          </Text>
+          <CodeBlock>{path}</CodeBlock>
+        </Stack>
+      </Stack>
     </Modal>
   );
 };

+ 35 - 34
src/containers/Modals/SettingsModal/index.tsx

@@ -1,22 +1,9 @@
 import React from "react";
-import { Modal, ModalProps } from "src/components/Modal";
-import Toggle from "src/components/Toggle";
+import { Modal, Group, Switch, Stack, ModalProps } from "@mantine/core";
 import useStored from "src/store/useStored";
-import styled from "styled-components";
 import { shallow } from "zustand/shallow";
 
-const StyledToggle = styled(Toggle)`
-  flex-flow: row-reverse;
-  background: black;
-`;
-
-const StyledModalWrapper = styled.div`
-  display: flex;
-  flex-direction: column;
-  gap: 20px;
-`;
-
-export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const SettingsModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const lightmode = useStored(state => state.lightmode);
   const setLightTheme = useStored(state => state.setLightTheme);
 
@@ -40,25 +27,39 @@ export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
   );
 
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Settings</Modal.Header>
-      <Modal.Content>
-        <StyledModalWrapper>
-          <StyledToggle onChange={toggleImagePreview} checked={imagePreview}>
-            Live Image Preview
-          </StyledToggle>
-          <StyledToggle onChange={toggleHideCollapse} checked={hideCollapse}>
-            Display Collapse/Expand Button
-          </StyledToggle>
-          <StyledToggle onChange={toggleChildrenCount} checked={childrenCount}>
-            Display Children Count
-          </StyledToggle>
-          <StyledToggle onChange={() => setLightTheme(!lightmode)} checked={lightmode}>
-            Light Theme
-          </StyledToggle>
-        </StyledModalWrapper>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible} />
+    <Modal title="Settings" opened={opened} onClose={onClose} centered>
+      <Group py="sm">
+        <Stack>
+          <Switch
+            label="Live Image Preview"
+            size="md"
+            color="teal"
+            onChange={e => toggleImagePreview(e.currentTarget.checked)}
+            checked={imagePreview}
+          />
+          <Switch
+            label="Display Collapse/Expand Button"
+            size="md"
+            color="teal"
+            onChange={e => toggleHideCollapse(e.currentTarget.checked)}
+            checked={hideCollapse}
+          />
+          <Switch
+            label="Display Children Count"
+            size="md"
+            color="teal"
+            onChange={e => toggleChildrenCount(e.currentTarget.checked)}
+            checked={childrenCount}
+          />
+          <Switch
+            label="Light Theme"
+            size="md"
+            color="teal"
+            onChange={e => setLightTheme(e.currentTarget.checked)}
+            checked={lightmode}
+          />
+        </Stack>
+      </Group>
     </Modal>
   );
 };

+ 40 - 61
src/containers/Modals/ShareModal/index.tsx

@@ -1,72 +1,51 @@
 import React from "react";
 import { useRouter } from "next/router";
-import toast from "react-hot-toast";
-import { Button } from "src/components/Button";
-import { Input } from "src/components/Input";
-import { Modal, ModalProps } from "src/components/Modal";
-import styled from "styled-components";
+import {
+  TextInput,
+  Stack,
+  Modal,
+  Button,
+  CopyButton,
+  Tooltip,
+  ActionIcon,
+  Text,
+  ModalProps,
+} from "@mantine/core";
+import { MdCheck, MdCopyAll } from "react-icons/md";
 
-const StyledFlex = styled.div`
-  display: flex;
-  gap: 12px;
-`;
-
-const StyledContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-  padding: 12px 0;
-  border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
-  font-size: 12px;
-  line-height: 16px;
-  font-weight: 600;
-  text-transform: uppercase;
-  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
-
-  &:first-of-type {
-    padding-top: 0;
-    border: none;
-  }
-`;
-
-export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const { push, query } = useRouter();
+export const ShareModal: React.FC<ModalProps> = ({ opened, onClose }) => {
+  const { query } = useRouter();
   const shareURL = `https://jsoncrack.com/editor?json=${query.json}`;
 
-  const handleShare = (value: string) => {
-    navigator.clipboard?.writeText(value);
-    toast.success(`Link copied to clipboard.`);
-    setVisible(false);
-  };
-
-  const onEmbedClick = () => {
-    push("/docs");
-    setVisible(false);
-  };
-
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Create a Share Link</Modal.Header>
-      <Modal.Content>
-        <StyledContainer>
+    <Modal title="Create a Share Link" opened={opened} onClose={onClose} centered>
+      <Stack py="sm">
+        <Text fz="sm" fw={700}>
           Share Link
-          <StyledFlex>
-            <Input value={shareURL} type="url" readOnly />
-            <Button status="SECONDARY" onClick={() => handleShare(shareURL)}>
-              Copy
-            </Button>
-          </StyledFlex>
-        </StyledContainer>
-        <StyledContainer>
+        </Text>
+        <TextInput
+          value={shareURL}
+          type="url"
+          readOnly
+          rightSection={
+            <CopyButton value={shareURL} timeout={2000}>
+              {({ copied, copy }) => (
+                <Tooltip label={copied ? "Copied" : "Copy"} withArrow position="right">
+                  <ActionIcon color={copied ? "teal" : "gray"} onClick={copy}>
+                    {copied ? <MdCheck size="1rem" /> : <MdCopyAll size="1rem" />}
+                  </ActionIcon>
+                </Tooltip>
+              )}
+            </CopyButton>
+          }
+        />
+        <Text fz="sm" fw={700}>
           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>
+        </Text>
+        <Button component="a" color="green" target="_blank" href="/docs" fullWidth>
+          Learn How to Embed
+        </Button>
+      </Stack>
     </Modal>
   );
 };

+ 20 - 11
src/containers/PricingCards/index.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { Button } from "src/components/Button";
+import { Button } from "@mantine/core";
 import styled from "styled-components";
 
 const StyledSectionBody = styled.div`
@@ -45,9 +45,17 @@ border: 1px solid rgba(255, 5, 214, 0.74);`
 `;
 
 const StyledPricingCardTitle = styled.h2`
+  display: flex;
+  align-items: center;
+  justify-content: center;
   text-align: center;
   font-weight: 800;
   font-size: 24px;
+  gap: 6px;
+
+  img {
+    transform: rotate(45deg);
+  }
 `;
 
 const StyledPricingCardPrice = styled.h3`
@@ -71,10 +79,6 @@ const StyledPricingCardDetailsItem = styled.li`
   }
 `;
 
-const StyledButton = styled(Button)`
-  border: 1px solid white;
-`;
-
 const StyledPricingSection = styled.section`
   margin: 0 auto;
 
@@ -111,15 +115,20 @@ export const PricingCards = () => {
             </StyledPricingCardDetailsItem>
             <StyledPricingCardDetailsItem>Everything in previous tier</StyledPricingCardDetailsItem>
           </StyledPricingCardDetails>
-          <StyledButton
+          <Button
+            size="md"
+            variant="gradient"
+            gradient={{ from: "pink", to: "red", deg: 105 }}
+            component="a"
             href="https://www.patreon.com/jsoncrack"
             target="_blank"
-            status="SUCCESS"
-            block
-            link
+            fullWidth
+            style={{
+              border: "2px solid black",
+            }}
           >
-            GET IT NOW!
-          </StyledButton>
+            GET PREMIUM
+          </Button>
         </StyledPricingCard>
       </StyledSectionBody>
     </StyledPricingSection>

+ 0 - 33
src/hooks/useHideNodes.tsx

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

+ 0 - 25
src/hooks/useKeyPress.tsx

@@ -1,25 +0,0 @@
-import React from "react";
-
-const useKeyPress = (targetKey: string) => {
-  const [keyPressed, setKeyPressed] = React.useState(false);
-
-  React.useEffect(() => {
-    function downHandler({ key }) {
-      if (key === targetKey) setKeyPressed(true);
-    }
-    const upHandler = ({ key }) => {
-      if (key === targetKey) setKeyPressed(false);
-    };
-    window.addEventListener("keydown", downHandler);
-    window.addEventListener("keyup", upHandler);
-
-    return () => {
-      window.removeEventListener("keydown", downHandler);
-      window.removeEventListener("keyup", upHandler);
-    };
-  }, [targetKey]);
-
-  return keyPressed;
-};
-
-export default useKeyPress;

+ 51 - 0
src/pages/404.tsx

@@ -0,0 +1,51 @@
+import React from "react";
+import { useRouter } from "next/router";
+import { Button } from "@mantine/core";
+import styled from "styled-components";
+
+const StyledNotFound = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  margin-top: 0 40px;
+  text-align: center;
+`;
+
+const StyledMessage = styled.h4`
+  color: ${({ theme }) => theme.FULL_WHITE};
+  font-size: 25px;
+  font-weight: 800;
+  margin: 10px 0;
+`;
+
+const StyledSubMessage = styled.div`
+  width: 50%;
+  color: ${({ theme }) => theme.SILVER};
+  margin-bottom: 25px;
+`;
+
+const StyledImageWrapper = styled.div`
+  width: 300px;
+`;
+
+const NotFound: React.FC = () => {
+  const router = useRouter();
+
+  return (
+    <StyledNotFound>
+      <StyledImageWrapper>
+        <img src="/assets/404.svg" alt="not found" width={300} height={400} />
+      </StyledImageWrapper>
+      <StyledMessage>WIZARDS BEHIND CURTAINS?</StyledMessage>
+      <StyledSubMessage>
+        Looks like you&apos;re lost, let&apos;s head back to the home!
+      </StyledSubMessage>
+      <Button type="button" onClick={() => router.push("/")}>
+        Go Home
+      </Button>
+    </StyledNotFound>
+  );
+};
+
+export default NotFound;

+ 34 - 14
src/pages/_app.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import type { AppProps } from "next/app";
-import localFont from "@next/font/local";
+import { MantineProvider } from "@mantine/core";
 import { init } from "@sentry/nextjs";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { Toaster } from "react-hot-toast";
@@ -9,14 +9,7 @@ import GlobalStyle from "src/constants/globalStyle";
 import { darkTheme, lightTheme } from "src/constants/theme";
 import { ModalController } from "src/containers/ModalController";
 import useStored from "src/store/useStored";
-import { ThemeProvider } from "styled-components";
-
-const monaSans = localFont({
-  src: "./Mona-Sans.woff2",
-  variable: "--mona-sans",
-  display: "swap",
-  fallback: ["Arial, Helvetica, sans-serif", "Tahoma, Verdana, sans-serif"],
-});
+import { ThemeProvider, useTheme } from "styled-components";
 
 if (process.env.NODE_ENV !== "development") {
   init({
@@ -36,6 +29,7 @@ const queryClient = new QueryClient({
 });
 
 function JsonCrack({ Component, pageProps }: AppProps) {
+  const theme = useTheme();
   const [isReady, setReady] = React.useState(false);
   const lightmode = useStored(state => state.lightmode);
 
@@ -47,9 +41,35 @@ function JsonCrack({ Component, pageProps }: AppProps) {
     return (
       <QueryClientProvider client={queryClient}>
         <GoogleAnalytics />
-        <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
-          <GlobalStyle />
-          <main className={monaSans.className}>
+        <MantineProvider
+          theme={{
+            colorScheme: lightmode ? "light" : "dark",
+            components: {
+              Divider: {
+                styles: () => ({
+                  root: {
+                    borderTopColor: "#4D4D4D !important",
+                  },
+                }),
+              },
+              Modal: {
+                styles: theme => ({
+                  title: {
+                    fontWeight: 700,
+                  },
+                  header: {
+                    backgroundColor: theme.colorScheme === "dark" ? "#36393E" : "#FFFFFF",
+                  },
+                  body: {
+                    backgroundColor: theme.colorScheme === "dark" ? "#36393E" : "#FFFFFF",
+                  },
+                }),
+              },
+            },
+          }}
+        >
+          <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
+            <GlobalStyle />
             <Component {...pageProps} />
             <ModalController />
             <Toaster
@@ -66,8 +86,8 @@ function JsonCrack({ Component, pageProps }: AppProps) {
                 },
               }}
             />
-          </main>
-        </ThemeProvider>
+          </ThemeProvider>
+        </MantineProvider>
       </QueryClientProvider>
     );
 }

+ 5 - 0
src/pages/_document.tsx

@@ -1,8 +1,13 @@
 import Document, { Html, Head, Main, NextScript } from "next/document";
 import Script from "next/script";
+import { createGetInitialProps } from "@mantine/next";
 import { SeoTags } from "src/components/SeoTags";
 
+const getInitialProps = createGetInitialProps();
+
 class MyDocument extends Document {
+  static getInitialProps = getInitialProps;
+
   render() {
     return (
       <Html lang="en">

+ 1 - 1
src/pages/_error.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { useRouter } from "next/router";
-import { Button } from "src/components/Button";
+import { Button } from "@mantine/core";
 import styled from "styled-components";
 
 const StyledNotFound = styled.div`

+ 2 - 2
src/pages/docs.tsx

@@ -1,8 +1,8 @@
 import React from "react";
 import dynamic from "next/dynamic";
 import Head from "next/head";
+import { Button } from "@mantine/core";
 import materialDark from "react-syntax-highlighter/dist/cjs/styles/prism/material-dark";
-import { Button } from "src/components/Button";
 import { Footer } from "src/components/Footer";
 import styled from "styled-components";
 
@@ -63,7 +63,7 @@ const Docs = () => {
         <meta name="description" content="Embedding JSON Crack tutorial into your websites." />
       </Head>
       <StyledPage>
-        <Button href="/" link status="SECONDARY">
+        <Button component="a" href="/">
           &lt; Go Back
         </Button>
         <h1>Documentation</h1>

+ 3 - 2
src/pages/editor.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import Head from "next/head";
 import { useRouter } from "next/router";
-import { AdTest } from "src/components/AdTest";
 import { Loading } from "src/components/Loading";
 import { Sidebar } from "src/components/Sidebar";
 import { BottomBar } from "src/containers/Editor/BottomBar";
@@ -10,6 +9,8 @@ import useJson from "src/store/useJson";
 import useUser from "src/store/useUser";
 import styled from "styled-components";
 
+// import { AdTest } from "src/components/AdTest";
+
 export const StyledPageWrapper = styled.div`
   display: flex;
   flex-direction: row;
@@ -59,7 +60,7 @@ const EditorPage: React.FC = () => {
         </StyledEditorWrapper>
       </StyledPageWrapper>
       <BottomBar />
-      <AdTest />
+      {/* <AdTest /> */}
     </StyledEditorWrapper>
   );
 };

+ 2 - 2
src/pages/pricing.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { Button } from "src/components/Button";
+import { Button } from "@mantine/core";
 import { Footer } from "src/components/Footer";
 import { PricingCards } from "src/containers/PricingCards";
 import styled from "styled-components";
@@ -18,7 +18,7 @@ const Pricing = () => {
   return (
     <>
       <StyledPageWrapper>
-        <Button href="/" link>
+        <Button component="a" href="/">
           &lt; Go Back
         </Button>
         <StyledHeroSection>

+ 24 - 20
src/pages/sign-in.tsx

@@ -2,9 +2,9 @@ import React from "react";
 import Head from "next/head";
 import Link from "next/link";
 import { useRouter } from "next/router";
+import { Button, Center, Container, Stack } from "@mantine/core";
 import { AiOutlineGithub, AiOutlineGoogle } from "react-icons/ai";
 import { altogic } from "src/api/altogic";
-import { Button } from "src/components/Button";
 import { Footer } from "src/components/Footer";
 import { Navbar } from "src/components/Navbar";
 import useUser from "src/store/useUser";
@@ -21,15 +21,6 @@ const StyledHeroSection = styled.section`
   align-items: center;
 `;
 
-const StyledLoginButtons = styled.div`
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  margin-top: 60px;
-  gap: 24px;
-`;
-
 const SignIn = () => {
   const { isReady, replace } = useRouter();
   const checkSession = useUser(state => state.checkSession);
@@ -57,16 +48,29 @@ const SignIn = () => {
           </Link>
           <h1>Sign In</h1>
         </StyledHeroSection>
-        <StyledLoginButtons>
-          <Button status="DANGER" onClick={() => handleLoginClick("google")}>
-            <AiOutlineGoogle size={24} />
-            Sign In with Google
-          </Button>
-          <Button status="TERTIARY" onClick={() => handleLoginClick("github")}>
-            <AiOutlineGithub size={24} />
-            Sign In with GitHub
-          </Button>
-        </StyledLoginButtons>
+        <Container>
+          <Center>
+            <Stack my={60} w={250} spacing="xl">
+              <Button
+                size="md"
+                color="red"
+                onClick={() => handleLoginClick("google")}
+                leftIcon={<AiOutlineGoogle size={24} />}
+              >
+                Sign In with Google
+              </Button>
+              <Button
+                size="md"
+                variant="white"
+                color="gray"
+                onClick={() => handleLoginClick("github")}
+                leftIcon={<AiOutlineGithub size={24} />}
+              >
+                Sign In with GitHub
+              </Button>
+            </Stack>
+          </Center>
+        </Container>
       </StyledPageWrapper>
       <Footer />
     </>

+ 3 - 3
src/store/useGraph.tsx

@@ -18,7 +18,7 @@ const initialStates = {
   collapsedNodes: [] as string[],
   collapsedEdges: [] as string[],
   collapsedParents: [] as string[],
-  selectedNode: [] as string | object,
+  selectedNode: {} as NodeData,
   path: "",
 };
 
@@ -38,12 +38,12 @@ interface GraphActions {
   zoomIn: () => void;
   zoomOut: () => void;
   centerView: () => void;
-  setSelectedNode: ({ node, path }: { node: string | string[]; path: string }) => void;
+  setSelectedNode: ({ nodeData, path }: { nodeData: NodeData; path: string }) => void;
 }
 
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
-  setSelectedNode: ({ node, path }) => set({ selectedNode: node, path }),
+  setSelectedNode: ({ nodeData, path }) => set({ selectedNode: nodeData, path }),
   setGraph: (data, options) => {
     const { nodes, edges } = parser(data ?? useJson.getState().json);
 

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

@@ -4,6 +4,7 @@ interface NodeData<T = any> {
   id: string;
   disabled?: boolean;
   text?: any;
+  path?: string;
   height?: number;
   width?: number;
   isParent?: string;

+ 6 - 0
src/utils/core/jsonParser.ts

@@ -1,4 +1,5 @@
 import { parseTree } from "jsonc-parser";
+import { getNodePath } from "../getNodePath";
 import { addEdgeToGraph } from "./addEdgeToGraph";
 import { addNodeToGraph } from "./addNodeToGraph";
 import { traverse } from "./traverse";
@@ -65,6 +66,11 @@ export const parser = (jsonStr: string) => {
       else addNodeToGraph({ graph: states.graph, text: "{}" });
     }
 
+    states.graph.nodes = states.graph.nodes.map(node => ({
+      ...node,
+      path: getNodePath(states.graph.nodes, states.graph.edges, node.id),
+    }));
+
     return states.graph;
   } catch (error) {
     console.error(error);

+ 11 - 0
src/utils/dataToString.ts

@@ -0,0 +1,11 @@
+export const dataToString = (data: any) => {
+  const text = Array.isArray(data) ? Object.fromEntries(data) : data;
+  return JSON.stringify(
+    text,
+    (_, v) => {
+      if (typeof v === "string") return v.replaceAll('"', "");
+      return v;
+    },
+    2
+  );
+};

Разница между файлами не показана из-за своего большого размера
+ 482 - 323
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов