Parcourir la source

revamp json editor

Aykut Saraç il y a 3 ans
Parent
commit
f1450c7953
6 fichiers modifiés avec 242 ajouts et 193 suppressions
  1. 1 0
      package.json
  2. 92 96
      src/components/Sidebar/index.tsx
  3. 42 0
      src/constants/data.ts
  4. 70 93
      src/containers/JsonEditor/index.tsx
  5. 11 4
      src/pages/editor/index.tsx
  6. 26 0
      yarn.lock

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "next": "12.0.9",
     "next-transpile-modules": "^9.0.0",
     "react": "17.0.2",
+    "react-ace": "^9.5.0",
     "react-dom": "17.0.2",
     "react-hot-toast": "^2.2.0",
     "react-icons": "^4.3.1",

+ 92 - 96
src/components/Sidebar/index.tsx

@@ -22,7 +22,7 @@ import {
 import { getNextLayout } from "src/containers/LiveEditor/helpers";
 import { StorageConfig } from "src/typings/global";
 import { CanvasDirection } from "reaflow";
-import { useLoading } from "src/hooks/useLoading";
+import { defaultConfig } from "src/constants/data";
 
 const StyledSidebar = styled.div`
   display: flex;
@@ -105,17 +105,14 @@ function getLayoutIcon(layout: CanvasDirection) {
   return <CgArrowLongUpE />;
 }
 
-export const Sidebar: React.FC<{
+const Sidebar: React.FC<{
   setJson: React.Dispatch<React.SetStateAction<string>>;
 }> = ({ setJson }) => {
-  const pageLoaded = useLoading();
-
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
-  const [config, setConfig] = useLocalStorage<StorageConfig>("config", {
-    layout: "LEFT",
-    expand: true,
-    controls: true,
-  });
+  const [config, setConfig] = useLocalStorage<StorageConfig>(
+    "config",
+    defaultConfig
+  );
 
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files) setJsonFile(e.target.files?.item(0));
@@ -139,92 +136,91 @@ export const Sidebar: React.FC<{
     }
   }, [jsonFile, setJson]);
 
-  if (pageLoaded)
-    return (
-      <StyledSidebar>
-        <StyledTopWrapper>
-          <Link passHref href="/">
-            <StyledElement as="a">
-              <StyledLogo>
-                <StyledText>J</StyledText>
-                <StyledText secondary>V</StyledText>
-              </StyledLogo>
-            </StyledElement>
-          </Link>
-          <StyledElement title="Home">
-            <Link href="/">
-              <a>
-                <AiFillHome />
-              </a>
-            </Link>
-          </StyledElement>
-          <StyledElement
-            as="a"
-            onClick={() => {
-              setJson("[]");
-              localStorage.removeItem("json");
-            }}
-            title="Clear JSON"
-          >
-            <AiOutlineClear />
-          </StyledElement>
-          <StyledElement
-            as="a"
-            onClick={() =>
-              setConfig((c) => ({
-                ...c,
-                layout: getNextLayout(c.layout),
-              }))
-            }
-            title="Change Layout"
-          >
-            {getLayoutIcon(config.layout)}
-          </StyledElement>
-          <StyledElement
-            title="Toggle Controls"
-            as="a"
-            onClick={() => toggle("controls")}
-          >
-            {config.controls ? <AiFillControl /> : <AiOutlineControl />}
-          </StyledElement>
-
-          <StyledElement
-            as="a"
-            title="Toggle Expand/Collapse"
-            onClick={() => toggle("expand")}
-          >
-            {config.expand ? <MdUnfoldMore /> : <MdUnfoldLess />}
-          </StyledElement>
-          <StyledElement as="a" title="Import JSON File">
-            <StyledImportFile>
-              <input
-                key={jsonFile?.name}
-                onChange={handleFileChange}
-                type="file"
-                accept="application/JSON"
-              />
-              <FaFileImport />
-            </StyledImportFile>
+  return (
+    <StyledSidebar>
+      <StyledTopWrapper>
+        <Link passHref href="/">
+          <StyledElement as="a">
+            <StyledLogo>
+              <StyledText>J</StyledText>
+              <StyledText secondary>V</StyledText>
+            </StyledLogo>
           </StyledElement>
-        </StyledTopWrapper>
-        <StyledBottomWrapper>
-          <StyledElement>
-            <Link href="https://twitter.com/aykutsarach">
-              <a rel="me" target="_blank">
-                <AiOutlineTwitter />
-              </a>
-            </Link>
-          </StyledElement>
-          <StyledElement>
-            <Link href="https://github.com/AykutSarac/jsonvisio.com">
-              <a rel="me" target="_blank">
-                <AiFillGithub />
-              </a>
-            </Link>
-          </StyledElement>
-        </StyledBottomWrapper>
-      </StyledSidebar>
-    );
-
-  return null;
+        </Link>
+        <StyledElement title="Home">
+          <Link href="/">
+            <a>
+              <AiFillHome />
+            </a>
+          </Link>
+        </StyledElement>
+        <StyledElement
+          as="a"
+          onClick={() => {
+            setJson("[]");
+            localStorage.removeItem("json");
+          }}
+          title="Clear JSON"
+        >
+          <AiOutlineClear />
+        </StyledElement>
+        <StyledElement
+          as="a"
+          onClick={() =>
+            setConfig((c) => ({
+              ...c,
+              layout: getNextLayout(c.layout),
+            }))
+          }
+          title="Change Layout"
+        >
+          {getLayoutIcon(config.layout)}
+        </StyledElement>
+        <StyledElement
+          title="Toggle Controls"
+          as="a"
+          onClick={() => toggle("controls")}
+        >
+          {config.controls ? <AiFillControl /> : <AiOutlineControl />}
+        </StyledElement>
+
+        <StyledElement
+          as="a"
+          title="Toggle Expand/Collapse"
+          onClick={() => toggle("expand")}
+        >
+          {config.expand ? <MdUnfoldMore /> : <MdUnfoldLess />}
+        </StyledElement>
+        <StyledElement as="a" title="Import JSON File">
+          <StyledImportFile>
+            <input
+              key={jsonFile?.name}
+              onChange={handleFileChange}
+              type="file"
+              accept="application/JSON"
+            />
+            <FaFileImport />
+          </StyledImportFile>
+        </StyledElement>
+      </StyledTopWrapper>
+      <StyledBottomWrapper>
+        <StyledElement>
+          <Link href="https://twitter.com/aykutsarach">
+            <a rel="me" target="_blank">
+              <AiOutlineTwitter />
+            </a>
+          </Link>
+        </StyledElement>
+        <StyledElement>
+          <Link href="https://github.com/AykutSarac/jsonvisio.com">
+            <a rel="me" target="_blank">
+              <AiFillGithub />
+            </a>
+          </Link>
+        </StyledElement>
+      </StyledBottomWrapper>
+    </StyledSidebar>
+  );
 };
+
+export default Sidebar;

+ 42 - 0
src/constants/data.ts

@@ -0,0 +1,42 @@
+import { StorageConfig } from "src/typings/global";
+
+export const defaultJson = [
+  {
+    Author: "J. K. Rowling.",
+    Genre: "Fantasy",
+    Characters: ["Hermione Granger", "Harry Potter", "Lord Voldemort", "MORE"],
+    Books: [
+      { title: "Philosopher's Stone", date: "1997" },
+      {
+        title: "Chamber of Secrets",
+        date: "1998",
+      },
+      {
+        title: "Prisoner of Azkaban",
+        date: "1999",
+      },
+      {
+        title: "Goblet of Fire",
+        date: "1999",
+      },
+      {
+        title: "Order of the Phoenix",
+        date: "2003",
+      },
+      {
+        title: "Half-Blood Prince",
+        date: "2005",
+      },
+      {
+        title: "Deathly Hallows",
+        date: "2007",
+      },
+    ],
+  },
+];
+
+export const defaultConfig: StorageConfig = {
+  layout: "LEFT",
+  expand: true,
+  controls: true,
+};

+ 70 - 93
src/containers/JsonEditor/index.tsx

@@ -1,93 +1,70 @@
-import React from "react";
-import styled from "styled-components";
-import JSONInput from "react-json-editor-ajrm";
-import locale from "react-json-editor-ajrm/locale/en";
-
-interface JsonData {
-  plainText: string;
-  markupText: string;
-  json: string;
-  jsObject?: object;
-  lines: number;
-  error: boolean;
-}
-
-const StyledJSONInput = styled(JSONInput)`
-  margin-top: 10px;
-  padding: 5px;
-`;
-
-export const defaultValue = [
-  {
-    Author: "J. K. Rowling.",
-    Genre: "Fantasy",
-    Characters: ["Hermione Granger", "Harry Potter", "Lord Voldemort", "MORE"],
-    Books: [
-      { title: "Philosopher's Stone", date: "1997" },
-      {
-        title: "Chamber of Secrets",
-        date: "1998",
-      },
-      {
-        title: "Prisoner of Azkaban",
-        date: "1999",
-      },
-      {
-        title: "Goblet of Fire",
-        date: "1999",
-      },
-      {
-        title: "Order of the Phoenix",
-        date: "2003",
-      },
-      {
-        title: "Half-Blood Prince",
-        date: "2005",
-      },
-      {
-        title: "Deathly Hallows",
-        date: "2007",
-      },
-    ],
-  },
-];
-
-export const JsonEditor: React.FC<{
-  json: string;
-  setJson: React.Dispatch<React.SetStateAction<string>>;
-}> = ({ json, setJson }) => {
-  const [initialJson, setInitialJson] = React.useState(json);
-
-  React.useEffect(() => {
-    const jsonStored = localStorage.getItem("json");
-    if (jsonStored) setInitialJson(jsonStored);
-
-    const element = document.querySelector(
-      '[name="outer-box"] > div'
-    ) as HTMLDivElement;
-    if (element) {
-      element.style.transform = "translate(-75%, 25%)";
-    }
-  }, []);
-
-  React.useEffect(() => {
-    if (json === "[]") setInitialJson(json);
-  }, [json]);
-
-  const handleChange = (data: JsonData) => {
-    if (!data.error) {
-      if (data.json === "") return setJson("[]");
-      setJson(data.json);
-    }
-  };
-
-  return (
-    <StyledJSONInput
-      placeholder={JSON.parse(initialJson)}
-      onChange={handleChange}
-      locale={locale}
-      height="100%"
-      width="auto"
-    />
-  );
-};
+import dynamic from "next/dynamic";
+import React, { ComponentType } from "react";
+import { IAceEditorProps } from "react-ace";
+import toast from "react-hot-toast";
+
+function isJson(value: string) {
+  value = typeof value !== "string" ? JSON.stringify(value) : value;
+
+  try {
+    value = JSON.parse(value);
+  } catch (e: any) {
+    toast.error(e.message);
+    return false;
+  }
+
+  if (typeof value === "object" && value !== null) {
+    return true;
+  }
+
+  return false;
+}
+
+const AceEditor: ComponentType<IAceEditorProps> = dynamic(
+  async () => {
+    const Ace = require("react-ace").default;
+    require("ace-builds/src-noconflict/mode-json");
+    require("ace-builds/src-noconflict/theme-tomorrow_night");
+    return Ace;
+  },
+  {
+    ssr: false,
+  }
+);
+
+const JsonEditor: React.FC<{
+  json: string;
+  setJson: React.Dispatch<React.SetStateAction<string>>;
+}> = ({ json, setJson }) => {
+  const [value, setValue] = React.useState(
+    JSON.stringify(JSON.parse(json), null, 2)
+  );
+
+  React.useEffect(() => {
+    setValue(JSON.stringify(JSON.parse(json), null, 2));
+  }, [json]);
+
+  React.useEffect(() => {
+    const formatTimer = setTimeout(() => {
+      if (!isJson(value)) return;
+      setValue(JSON.stringify(JSON.parse(value), null, 2));
+      setJson(value);
+    }, 2000);
+
+    return () => clearTimeout(formatTimer);
+  }, [value]);
+
+  return (
+    <AceEditor
+      value={value}
+      onChange={setValue}
+      mode="json"
+      theme="tomorrow_night"
+      width="auto"
+      height="100%"
+      fontSize={14}
+    />
+  );
+};
+
+export default JsonEditor;

+ 11 - 4
src/pages/editor/index.tsx

@@ -5,10 +5,18 @@ import styled from "styled-components";
 import SplitPane from "react-split-pane";
 
 import { Button } from "src/components/Button";
-import { Sidebar } from "src/components/Sidebar";
-import { defaultValue, JsonEditor } from "src/containers/JsonEditor";
+import { defaultJson } from "src/constants/data";
+import dynamic from "next/dynamic";
 import { LiveEditor } from "src/containers/LiveEditor";
 
+const JsonEditor = dynamic(() => import("src/containers/JsonEditor"), {
+  ssr: false,
+});
+
+const Sidebar = dynamic(() => import("src/components/Sidebar"), {
+  ssr: false,
+});
+
 const StyledPageWrapper = styled.div`
   display: flex;
 `;
@@ -23,7 +31,6 @@ const StyledIncompatible = styled.div`
     display: flex;
     flex-direction: column;
     background: ${({ theme }) => theme.BLACK_LIGHT};
-    content: "This app is not compatible with your device!";
     color: ${({ theme }) => theme.SILVER};
     width: 100%;
     height: 100vh;
@@ -117,7 +124,7 @@ const StyledEditor = styled(SplitPane)`
 `;
 
 const Editor: React.FC = () => {
-  const [json, setJson] = React.useState<string>(JSON.stringify(defaultValue));
+  const [json, setJson] = React.useState<string>(JSON.stringify(defaultJson));
   const route = useRouter();
 
   React.useEffect(() => {

+ 26 - 0
yarn.lock

@@ -1084,6 +1084,11 @@ abab@^2.0.3, abab@^2.0.5:
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
   integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
 
+ace-builds@^1.4.13:
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.14.tgz#2c41ccbccdd09e665d3489f161a20baeb3a3c852"
+  integrity sha512-NBOQlm9+7RBqRqZwimpgquaLeTJFayqb9UEPtTkpC3TkkwDnlsT/TwsCC0svjt9kEZ6G9mH5AEOHSz6Q/HrzQQ==
+
 acorn-globals@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
@@ -1816,6 +1821,11 @@ detect-newline@^3.0.0:
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
+diff-match-patch@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
+  integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
+
 diff-sequences@^27.4.0:
   version "27.4.0"
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5"
@@ -3585,6 +3595,11 @@ lodash.flattendeep@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
   integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
 
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
 lodash.isequal@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -4156,6 +4171,17 @@ rdk@^5.1.6:
     popper.js "^1.16.1"
     react-scrolllock "^5.0.1"
 
+react-ace@^9.5.0:
+  version "9.5.0"
+  resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.5.0.tgz#b6c32b70d404dd821a7e01accc2d76da667ff1f7"
+  integrity sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg==
+  dependencies:
+    ace-builds "^1.4.13"
+    diff-match-patch "^1.0.5"
+    lodash.get "^4.4.2"
+    lodash.isequal "^4.5.0"
+    prop-types "^15.7.2"
+
 react-cool-dimensions@^2.0.7:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/react-cool-dimensions/-/react-cool-dimensions-2.0.7.tgz#2fe6657608f034cd7c89f149ed14e79cf1cb2d50"