Ver Fonte

feat: implement prsim & modal ui

AykutSarac há 2 anos atrás
pai
commit
7c2f319f23
40 ficheiros alterados com 336 adições e 789 exclusões
  1. 1 0
      package.json
  2. BIN
      public/assets/rocket_ship.webp
  3. 0 99
      src/components/Button/index.tsx
  4. 2 2
      src/components/Graph/PremiumView.tsx
  5. 1 1
      src/components/Graph/index.tsx
  6. 0 19
      src/components/Input/index.tsx
  7. 27 21
      src/components/Loading/index.tsx
  8. 0 76
      src/components/Modal/index.tsx
  9. 0 69
      src/components/Modal/styles.tsx
  10. 0 28
      src/components/Spinner/index.tsx
  11. 12 5
      src/components/Sponsors/index.tsx
  12. 0 65
      src/components/Toggle/index.tsx
  13. 0 62
      src/components/Tooltip/index.tsx
  14. 1 1
      src/containers/Editor/LiveEditor/GraphCanvas.tsx
  15. 15 11
      src/containers/Home/index.tsx
  16. 0 36
      src/containers/Home/styles.tsx
  17. 8 8
      src/containers/ModalController/index.tsx
  18. 4 5
      src/containers/Modals/AccountModal/index.tsx
  19. 4 5
      src/containers/Modals/ClearModal/index.tsx
  20. 4 4
      src/containers/Modals/CloudModal/index.tsx
  21. 46 61
      src/containers/Modals/DownloadModal/index.tsx
  22. 6 6
      src/containers/Modals/ImportModal/index.tsx
  23. 3 4
      src/containers/Modals/LoginModal/index.tsx
  24. 24 78
      src/containers/Modals/NodeModal/index.tsx
  25. 7 17
      src/containers/Modals/SettingsModal/index.tsx
  26. 3 3
      src/containers/Modals/ShareModal/index.tsx
  27. 20 11
      src/containers/PricingCards/index.tsx
  28. 0 33
      src/hooks/useHideNodes.tsx
  29. 0 25
      src/hooks/useKeyPress.tsx
  30. 51 0
      src/pages/404.tsx
  31. 33 6
      src/pages/_app.tsx
  32. 1 1
      src/pages/_error.tsx
  33. 3 2
      src/pages/editor.tsx
  34. 2 2
      src/pages/pricing.tsx
  35. 24 20
      src/pages/sign-in.tsx
  36. 3 3
      src/store/useGraph.tsx
  37. 1 0
      src/typings/types.d.ts
  38. 6 0
      src/utils/core/jsonParser.ts
  39. 11 0
      src/utils/dataToString.ts
  40. 13 0
      yarn.lock

+ 1 - 0
package.json

@@ -18,6 +18,7 @@
     "@mantine/core": "^6.0.0",
     "@mantine/hooks": "^6.0.0",
     "@mantine/next": "^6.0.0",
+    "@mantine/prism": "^6.0.0",
     "@monaco-editor/react": "^4.4.6",
     "@next/font": "^13.1.6",
     "@sentry/nextjs": "^7.36.0",

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/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,7 @@ 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 component="a" href="https://www.patreon.com/jsoncrack" target="_blank">
       DO IT!
     </Button>
     <img src="/assets/undraw_to_the_stars_re_wq2x.svg" width="300" height="300" alt="oops" />

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

@@ -68,7 +68,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]

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

+ 27 - 21
src/components/Loading/index.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import { Center, Image, Stack } from "@mantine/core";
 import styled, { keyframes } from "styled-components";
 
 interface LoadingProps {
@@ -15,27 +16,31 @@ 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;
+
+  img {
+    transform: rotate(45deg) translate(125px, -50px);
+  }
 `;
 
 const StyledLogo = styled.h2`
   font-weight: 800;
-  font-size: 56px;
-  pointer-events: none;
+  font-size: 70px;
   margin-bottom: 10px;
 `;
 
@@ -45,19 +50,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>
-    </StyledLoading>
-  );
-};
+export const Loading: React.FC<LoadingProps> = ({ loading = true, message }) => (
+  <StyledLoading visible={loading}>
+    <Center>
+      <Stack>
+        <Image mah={225} maw={225} src="./assets/rocket_ship.webp" alt="loading image" />
+        <StyledLogo>
+          <StyledText>JSON</StyledText> Crack
+        </StyledLogo>
+        <StyledMessage>{message ?? "Preparing the environment for you..."}</StyledMessage>
+      </Stack>
+    </Center>
+  </StyledLoading>
+);

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

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

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

@@ -32,7 +32,7 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
   return (
     <>
       <Graph openNodeModal={openNodeModal} isWidget={isWidget} />
-      <NodeModal visible={isNodeModalVisible} closeModal={() => setNodeModalVisible(false)} />
+      <NodeModal opened={isNodeModalVisible} onClose={() => setNodeModalVisible(false)} />
     </>
   );
 };

+ 15 - 11
src/containers/Home/index.tsx

@@ -4,7 +4,7 @@ import Head from "next/head";
 import Link from "next/link";
 import Script from "next/script";
 import { Button } from "@mantine/core";
-import { AiOutlineRight } from "react-icons/ai";
+import { AiOutlineRight, AiTwotoneStar } from "react-icons/ai";
 import {
   HiCursorClick,
   HiLightningBolt,
@@ -197,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>
 );
@@ -223,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
@@ -260,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)} />
     </>
   );
 };

+ 4 - 5
src/containers/Modals/AccountModal/index.tsx

@@ -1,8 +1,7 @@
 import React from "react";
 import Link from "next/link";
-import { Modal, Group, Button, Badge, Avatar, Grid, Divider } from "@mantine/core";
+import { Modal, Group, Button, Badge, Avatar, Grid, Divider, ModalProps } from "@mantine/core";
 import { IoRocketSharp } from "react-icons/io5";
-import { ModalProps } from "src/components/Modal";
 import useUser from "src/store/useUser";
 import styled from "styled-components";
 
@@ -43,7 +42,7 @@ const StyledContainer = styled.div`
   }
 `;
 
-export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
+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);
@@ -53,7 +52,7 @@ export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
   };
 
   return (
-    <Modal title="Account" opened={visible} onClose={() => setVisible(false)} centered>
+    <Modal title="Account" opened={opened} onClose={onClose} centered>
       <StyledTitle>Hello, {user?.name}!</StyledTitle>
       <Group py="sm">
         <Grid gutter="xs">
@@ -112,7 +111,7 @@ export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
           color="red"
           onClick={() => {
             logout();
-            setVisible(false);
+            onClose();
           }}
         >
           Log Out

+ 4 - 5
src/containers/Modals/ClearModal/index.tsx

@@ -1,17 +1,16 @@
 import React from "react";
 import { useRouter } from "next/router";
-import { Modal, Group, Button, Text, Divider } from "@mantine/core";
-import { 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,7 +19,7 @@ export const ClearModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   };
 
   return (
-    <Modal title="Delete JSON" opened={visible} onClose={() => setVisible(false)} centered>
+    <Modal title="Delete JSON" opened={opened} onClose={onClose} centered>
       <Group py="sm">
         <Text>Are you sure you want to delete JSON?</Text>
       </Group>

+ 4 - 4
src/containers/Modals/CloudModal/index.tsx

@@ -10,6 +10,7 @@ import {
   Center,
   Divider,
   ScrollArea,
+  ModalProps,
 } from "@mantine/core";
 import { useQuery } from "@tanstack/react-query";
 import dayjs from "dayjs";
@@ -18,7 +19,6 @@ import toast from "react-hot-toast";
 import { AiOutlineEdit, AiOutlineLock, AiOutlinePlus, AiOutlineUnlock } from "react-icons/ai";
 import { FaTrash } from "react-icons/fa";
 import { IoRocketSharp } from "react-icons/io5";
-import { ModalProps } from "src/components/Modal";
 import { deleteJson, getAllJson, saveJson, updateJson } from "src/services/db/json";
 import useJson from "src/store/useJson";
 import useUser from "src/store/useUser";
@@ -204,15 +204,15 @@ 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 (
-    <Modal title="Saved On The Cloud" opened={visible} onClose={() => setVisible(false)} centered>
+    <Modal title="Saved On The Cloud" opened={opened} onClose={onClose} centered>
       <ScrollArea h={360}>
         <Stack py="sm">
           {isFetching ? (

+ 46 - 61
src/containers/Modals/DownloadModal/index.tsx

@@ -8,12 +8,18 @@ import {
   Button,
   Divider,
   Grid,
+  ModalProps,
+  ColorInput,
+  Stack,
 } from "@mantine/core";
 import { toBlob, toPng, toSvg } from "html-to-image";
 import toast from "react-hot-toast";
 import { FiCopy, FiDownload } from "react-icons/fi";
-import { ModalProps } from "src/components/Modal";
-import styled from "styled-components";
+
+enum Extensions {
+  SVG = "svg",
+  PNG = "png",
+}
 
 const swatches = [
   "#B80000",
@@ -44,29 +50,7 @@ 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;
-  }
-`;
-
-enum Extensions {
-  SVG = "svg",
-  PNG = "png",
-}
-export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const DownloadModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const [extension, setExtension] = React.useState(Extensions.PNG);
   const [fileDetails, setFileDetails] = React.useState({
     filename: "jsoncrack.com",
@@ -98,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();
     }
   };
 
@@ -120,7 +104,7 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
       toast.error("Failed to download image!");
     } finally {
       toast.dismiss("toastDownload");
-      setVisible(false);
+      onClose();
     }
   };
 
@@ -128,42 +112,43 @@ export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
     setFileDetails({ ...fileDetails, [key]: value });
 
   return (
-    <Modal opened={visible} onClose={() => setVisible(false)} title="Download Image" centered>
-      <Group align="flex-end" py="sm" grow>
-        <Grid 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>
-      </Group>
-      <Group py="sm" grow>
-        <StyledContainer>
-          Background Color
-          <ColorPicker
-            format="rgba"
-            value={fileDetails.backgroundColor}
-            onChange={color => updateDetails("backgroundColor", color)}
-            swatches={swatches}
+    <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
           />
-        </StyledContainer>
-      </Group>
-
+        </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}>

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

@@ -1,8 +1,7 @@
 import React from "react";
-import { Modal, Group, Button, TextInput, Stack, Divider } from "@mantine/core";
+import { Modal, Group, Button, TextInput, Stack, Divider, ModalProps } from "@mantine/core";
 import toast from "react-hot-toast";
 import { AiOutlineUpload } from "react-icons/ai";
-import { ModalProps } from "src/components/Modal";
 import useJson from "src/store/useJson";
 import styled from "styled-components";
 
@@ -33,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);
@@ -63,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"));
@@ -75,19 +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 title="Import JSON" opened={visible} onClose={() => setVisible(false)} centered>
+    <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"
+          data-autofocus
         />
         <StyledUploadWrapper onDrop={handleFileDrag} onDragOver={handleFileDrag}>
           <input

+ 3 - 4
src/containers/Modals/LoginModal/index.tsx

@@ -1,10 +1,9 @@
 import React from "react";
-import { Modal, Stack, Button, Text, Title } from "@mantine/core";
-import { 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 title="Sign In" opened={visible} onClose={() => setVisible(false)} centered>
+    <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>

+ 24 - 78
src/containers/Modals/NodeModal/index.tsx

@@ -1,78 +1,42 @@
 import React from "react";
-import dynamic from "next/dynamic";
-import { Modal, Group, Button, Stack, Divider, Text, Menu } from "@mantine/core";
-import toast from "react-hot-toast";
-import { AiOutlineMenu } from "react-icons/ai";
-import { FiCopy } from "react-icons/fi";
-import coldarkCold from "react-syntax-highlighter/dist/cjs/styles/prism/coldark-cold";
-import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
+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";
-
-const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter/dist/cjs/prism-async"), {
-  ssr: false,
-});
-
-interface NodeModalProps {
-  visible: boolean;
-  closeModal: () => void;
-}
-
-const CodeBlock: React.FC<{ children: string | string[] }> = ({ children }) => {
-  const lightmode = useStored(state => state.lightmode);
+import { dataToString } from "src/utils/dataToString";
+import { shallow } from "zustand/shallow";
 
+const CodeBlock: React.FC<{ children: any }> = ({ children }) => {
   return (
-    <SyntaxHighlighter
-      language="json"
-      style={lightmode ? coldarkCold : oneDark}
-      customStyle={{
-        borderRadius: "4px",
-        fontSize: "14px",
-        margin: "0",
-        maxHeight: "250px",
-        maxWidth: "600px",
-        minWidth: "350px"
-      }}
-      showLineNumbers
-    >
-      {children}
-    </SyntaxHighlighter>
+    <ScrollArea>
+      <Prism
+        maw={600}
+        miw={350}
+        mah={250}
+        language="json"
+        copyLabel="Copy to clipboard"
+        copiedLabel="Copied to clipboard"
+        withLineNumbers
+      >
+        {children}
+      </Prism>
+    </ScrollArea>
   );
 };
 
-export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
-  const path = useGraph(state => state.path);
-  const nodeData = useGraph(state =>
-    Array.isArray(state.selectedNode) ? Object.fromEntries(state.selectedNode) : state.selectedNode
+export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
+  const [nodeData, path] = useGraph(
+    state => [dataToString(state.selectedNode.text), state.selectedNode.path],
+    shallow
   );
 
-  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.");
-    }
-  };
-
   return (
-    <Modal title="Node Content" size="auto" opened={visible} onClose={closeModal} centered>
+    <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>
-            {JSON.stringify(
-              nodeData,
-              (_, v) => {
-                if (typeof v === "string") return v.replaceAll('"', "");
-                return v;
-              },
-              2
-            )}
-          </CodeBlock>
+          <CodeBlock>{nodeData}</CodeBlock>
         </Stack>
         <Stack spacing="xs">
           <Text fz="sm" fw={700}>
@@ -81,24 +45,6 @@ export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
           <CodeBlock>{path}</CodeBlock>
         </Stack>
       </Stack>
-      <Divider py="xs" />
-      <Group position="right">
-        <Menu trigger="hover" openDelay={100} closeDelay={400}>
-          <Menu.Target>
-            <Button color="gray" leftIcon={<AiOutlineMenu />}>
-              Actions
-            </Button>
-          </Menu.Target>
-          <Menu.Dropdown>
-            <Menu.Item icon={<FiCopy />} onClick={() => handleClipboard(JSON.stringify(nodeData))}>
-              Clipboard JSON
-            </Menu.Item>
-            <Menu.Item icon={<FiCopy />} onClick={() => handleClipboard(path)}>
-              Clipboard Path to Node
-            </Menu.Item>
-          </Menu.Dropdown>
-        </Menu>
-      </Group>
     </Modal>
   );
 };

+ 7 - 17
src/containers/Modals/SettingsModal/index.tsx

@@ -1,23 +1,9 @@
 import React from "react";
-import { Modal, Group, Switch, Stack } from "@mantine/core";
-import { 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);
 
@@ -41,30 +27,34 @@ export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) =>
   );
 
   return (
-    <Modal title="Settings" opened={visible} onClose={() => setVisible(false)} centered>
+    <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}
           />

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

@@ -9,16 +9,16 @@ import {
   Tooltip,
   ActionIcon,
   Text,
+  ModalProps,
 } from "@mantine/core";
 import { MdCheck, MdCopyAll } from "react-icons/md";
-import { ModalProps } from "src/components/Modal";
 
-export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
+export const ShareModal: React.FC<ModalProps> = ({ opened, onClose }) => {
   const { query } = useRouter();
   const shareURL = `https://jsoncrack.com/editor?json=${query.json}`;
 
   return (
-    <Modal title="Create a Share Link" opened={visible} onClose={() => setVisible(false)} centered>
+    <Modal title="Create a Share Link" opened={opened} onClose={onClose} centered>
       <Stack py="sm">
         <Text fz="sm" fw={700}>
           Share Link

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

+ 33 - 6
src/pages/_app.tsx

@@ -9,7 +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";
+import { ThemeProvider, useTheme } from "styled-components";
 
 if (process.env.NODE_ENV !== "development") {
   init({
@@ -29,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);
 
@@ -40,9 +41,35 @@ function JsonCrack({ Component, pageProps }: AppProps) {
     return (
       <QueryClientProvider client={queryClient}>
         <GoogleAnalytics />
-        <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
-          <GlobalStyle />
-          <MantineProvider theme={{ colorScheme: lightmode ? "light" : "dark" }}>
+        <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
@@ -59,8 +86,8 @@ function JsonCrack({ Component, pageProps }: AppProps) {
                 },
               }}
             />
-          </MantineProvider>
-        </ThemeProvider>
+          </ThemeProvider>
+        </MantineProvider>
       </QueryClientProvider>
     );
 }

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

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

+ 13 - 0
yarn.lock

@@ -542,6 +542,14 @@
     "@mantine/ssr" "6.0.0"
     "@mantine/styles" "6.0.0"
 
+"@mantine/prism@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@mantine/prism/-/prism-6.0.0.tgz#dfe3a44704133ca5bec29d805a9e5efa569efbac"
+  integrity sha512-lYHgpQOF2w92lJpSgS6igkzp1e7yHMpLUq4Pg/mtjXZH1mP+bniBl8rqNlGV5fj5TTJ1Jivp6XaAvFwJEVyeHw==
+  dependencies:
+    "@mantine/utils" "6.0.0"
+    prism-react-renderer "^1.2.1"
+
 "@mantine/[email protected]":
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/@mantine/ssr/-/ssr-6.0.0.tgz#ceda528faf205083c53adc6533a15a0578f85a03"
@@ -3579,6 +3587,11 @@ pretty-format@^27.0.2:
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
 
+prism-react-renderer@^1.2.1:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085"
+  integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==
+
 prismjs@^1.27.0:
   version "1.29.0"
   resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"