瀏覽代碼

feat: mantine ui migration

AykutSarac 2 年之前
父節點
當前提交
ddc2a0dc53

+ 5 - 0
package.json

@@ -13,6 +13,11 @@
     "deploy": "gh-pages -d out -t true"
   },
   "dependencies": {
+    "@emotion/react": "^11.10.6",
+    "@emotion/server": "^11.10.0",
+    "@mantine/core": "^6.0.0",
+    "@mantine/hooks": "^6.0.0",
+    "@mantine/next": "^6.0.0",
     "@monaco-editor/react": "^4.4.6",
     "@next/font": "^13.1.6",
     "@sentry/nextjs": "^7.36.0",

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

+ 10 - 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;
@@ -30,7 +39,7 @@ const GlobalStyle = createGlobalStyle`
   }
 
   svg {
-    vertical-align: top;
+    vertical-align: text-top;
   }
 
   

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

+ 18 - 23
src/containers/Home/index.tsx

@@ -1,7 +1,9 @@
 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 { Button } from "@mantine/core";
 import { AiOutlineRight } from "react-icons/ai";
 import {
   HiCursorClick,
@@ -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>
 );

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

@@ -1,18 +1,17 @@
 import React from "react";
+import Link from "next/link";
+import { Modal, Group, Button, Badge, Avatar, Grid, Divider } 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 { 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 +26,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 +43,7 @@ const StyledContainer = styled.div`
   }
 `;
 
-const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) => {
+export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
   const user = useUser(state => state.user);
   const isPremium = useUser(state => state.isPremium());
   const logout = useUser(state => state.logout);
@@ -71,57 +53,63 @@ 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={visible} onClose={() => setVisible(false)} 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);
@@ -129,15 +117,7 @@ const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) =
         >
           Log Out
         </Button>
-      </Modal.Controls>
-    </>
-  );
-};
-
-export const AccountModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
-  return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <AccountView setVisible={setVisible} />
+      </Group>
     </Modal>
   );
 };

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

@@ -1,7 +1,7 @@
 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 } from "@mantine/core";
+import { ModalProps } from "src/components/Modal";
 import { deleteJson } from "src/services/db/json";
 import useJson from "src/store/useJson";
 
@@ -20,14 +20,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={visible} onClose={() => setVisible(false)} 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>
   );
 };

+ 32 - 57
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,
+} 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 { 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";
@@ -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>
   );
 };
@@ -238,12 +212,13 @@ export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   });
 
   return (
-    <StyledModal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Saved On The Cloud</Modal.Header>
-      <Modal.Content>
-        <StyledModalContent>
+    <Modal title="Saved On The Cloud" opened={visible} onClose={() => setVisible(false)} 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>
   );
 };

+ 54 - 78
src/containers/Modals/DownloadModal/index.tsx

@@ -1,35 +1,21 @@
 import React from "react";
+import {
+  ColorPicker,
+  TextInput,
+  SegmentedControl,
+  Group,
+  Modal,
+  Button,
+  Divider,
+  Grid,
+} 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 { 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 = [
+const swatches = [
   "#B80000",
   "#DB3E00",
   "#FCCB00",
@@ -76,30 +62,15 @@ const StyledContainer = styled.div`
   }
 `;
 
-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,
+  SVG = "svg",
+  PNG = "png",
 }
 export const DownloadModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const [extension, setExtension] = React.useState(Extensions.png);
+  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,
   });
 
@@ -137,14 +108,14 @@ 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 {
@@ -157,46 +128,51 @@ 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
+    <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)}
-              setExtension={setExtension}
-              activeExtension={extension}
-              extensions={Object.keys(Extensions).filter(v => isNaN(Number(v)))}
             />
-          </StyledColorWrapper>
-        </StyledContainer>
+          </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
-          <StyledColorWrapper>
-            <TwitterPicker
-              triangle="hide"
-              colors={defaultColors}
-              color={fileDetails.backgroundColor}
-              onChange={color => updateDetails("backgroundColor", color.hex)}
-              styles={{
-                default: ColorPickerStyles,
-              }}
-            />
-            <StyledColorIndicator color={fileDetails.backgroundColor} />
-          </StyledColorWrapper>
+          <ColorPicker
+            format="rgba"
+            value={fileDetails.backgroundColor}
+            onChange={color => updateDetails("backgroundColor", color)}
+            swatches={swatches}
+            fullWidth
+          />
         </StyledContainer>
-      </Modal.Content>
-      <Modal.Controls setVisible={setVisible}>
-        <Button status="SECONDARY" onClick={clipboardImage}>
-          <FiCopy size={18} /> Clipboard
+      </Group>
+
+      <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>
   );
 };

+ 10 - 20
src/containers/Modals/ImportModal/index.tsx

@@ -1,19 +1,11 @@
 import React from "react";
+import { Modal, Group, Button, TextInput, Stack, Divider } 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 { 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 +14,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;
@@ -90,15 +81,13 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   };
 
   return (
-    <Modal visible={visible} setVisible={setVisible}>
-      <Modal.Header>Import JSON</Modal.Header>
-      <StyledModalContent>
-        <Input
+    <Modal title="Import JSON" opened={visible} onClose={() => setVisible(false)} centered>
+      <Stack py="sm">
+        <TextInput
           value={url}
           onChange={e => setURL(e.target.value)}
           type="url"
           placeholder="URL of JSON to fetch"
-          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 - 15
src/containers/Modals/LoginModal/index.tsx

@@ -1,22 +1,17 @@
 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 } from "@mantine/core";
+import { ModalProps } from "src/components/Modal";
 
 export const LoginModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
   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={visible} onClose={() => setVisible(false)} 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>
   );
 };

+ 66 - 59
src/containers/Modals/NodeModal/index.tsx

@@ -1,14 +1,13 @@
 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 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 coldarkCold from "react-syntax-highlighter/dist/cjs/styles/prism/coldark-cold";
+import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
 import useGraph from "src/store/useGraph";
 import useStored from "src/store/useStored";
-import styled from "styled-components";
 
 const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter/dist/cjs/prism-async"), {
   ssr: false,
@@ -19,19 +18,29 @@ interface NodeModalProps {
   closeModal: () => void;
 }
 
-const StyledTitle = styled.div`
-  line-height: 16px;
-  font-size: 12px;
-  font-weight: 600;
-  padding: 16px 0;
+const CodeBlock: React.FC<{ children: string | string[] }> = ({ children }) => {
+  const lightmode = useStored(state => state.lightmode);
 
-  &:first-of-type {
-    padding-top: 0;
-  }
-`;
+  return (
+    <SyntaxHighlighter
+      language="json"
+      style={lightmode ? coldarkCold : oneDark}
+      customStyle={{
+        borderRadius: "4px",
+        fontSize: "14px",
+        margin: "0",
+        maxHeight: "250px",
+        maxWidth: "600px",
+        minWidth: "350px"
+      }}
+      showLineNumbers
+    >
+      {children}
+    </SyntaxHighlighter>
+  );
+};
 
 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
@@ -48,50 +57,48 @@ export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
   };
 
   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={visible} onClose={closeModal} 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>
+        </Stack>
+        <Stack spacing="xs">
+          <Text fz="sm" fw={700}>
+            Node Path
+          </Text>
+          <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>
   );
 };

+ 31 - 20
src/containers/Modals/SettingsModal/index.tsx

@@ -1,5 +1,6 @@
 import React from "react";
-import { Modal, ModalProps } from "src/components/Modal";
+import { Modal, Group, Switch, Stack } from "@mantine/core";
+import { ModalProps } from "src/components/Modal";
 import Toggle from "src/components/Toggle";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
@@ -40,25 +41,35 @@ 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={visible} onClose={() => setVisible(false)} centered>
+      <Group py="sm">
+        <Stack>
+          <Switch
+            label="Live Image Preview"
+            size="md"
+            onChange={e => toggleImagePreview(e.currentTarget.checked)}
+            checked={imagePreview}
+          />
+          <Switch
+            label="Display Collapse/Expand Button"
+            size="md"
+            onChange={e => toggleHideCollapse(e.currentTarget.checked)}
+            checked={hideCollapse}
+          />
+          <Switch
+            label="Display Children Count"
+            size="md"
+            onChange={e => toggleChildrenCount(e.currentTarget.checked)}
+            checked={childrenCount}
+          />
+          <Switch
+            label="Light Theme"
+            size="md"
+            onChange={e => setLightTheme(e.currentTarget.checked)}
+            checked={lightmode}
+          />
+        </Stack>
+      </Group>
     </Modal>
   );
 };

+ 39 - 60
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";
-
-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;
-  }
-`;
+import {
+  TextInput,
+  Stack,
+  Modal,
+  Button,
+  CopyButton,
+  Tooltip,
+  ActionIcon,
+  Text,
+} from "@mantine/core";
+import { MdCheck, MdCopyAll } from "react-icons/md";
+import { ModalProps } from "src/components/Modal";
 
 export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
-  const { push, query } = useRouter();
+  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={visible} onClose={() => setVisible(false)} 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>
   );
 };

+ 3 - 10
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";
@@ -11,13 +11,6 @@ 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"],
-});
-
 if (process.env.NODE_ENV !== "development") {
   init({
     dsn: "https://[email protected]/6495191",
@@ -49,7 +42,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
         <GoogleAnalytics />
         <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
           <GlobalStyle />
-          <main className={monaSans.className}>
+          <MantineProvider theme={{ colorScheme: lightmode ? "light" : "dark" }}>
             <Component {...pageProps} />
             <ModalController />
             <Toaster
@@ -66,7 +59,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
                 },
               }}
             />
-          </main>
+          </MantineProvider>
         </ThemeProvider>
       </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 { SeoTags } from "src/components/SeoTags";
+import { createGetInitialProps } from '@mantine/next';
+
+const getInitialProps = createGetInitialProps();
 
 class MyDocument extends Document {
+  static getInitialProps = getInitialProps;
+
   render() {
     return (
       <Html lang="en">

File diff suppressed because it is too large
+ 631 - 8
yarn.lock


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