Procházet zdrojové kódy

Merge pull request #35 from HyunseungLee-Travis/add-fetch-json-from-url

Add Fetch JSON from URL
Aykut Saraç před 3 roky
rodič
revize
473a96e24d

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

@@ -25,6 +25,11 @@ const StyledButton = styled.button<{ status: keyof typeof ButtonType }>`
   padding: 8px 16px;
   padding: 8px 16px;
   min-width: 60px;
   min-width: 60px;
 
 
+  :disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+  }
+
   @media only screen and (max-width: 768px) {
   @media only screen and (max-width: 768px) {
     font-size: 18px;
     font-size: 18px;
   }
   }

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

@@ -43,7 +43,7 @@ export const StyledKey = styled.span<{
   objectKey?: boolean;
   objectKey?: boolean;
   parent?: boolean;
   parent?: boolean;
 }>`
 }>`
-  font-weight: bold;
+  font-weight: 500;
   color: ${({ theme, objectKey, parent }) =>
   color: ${({ theme, objectKey, parent }) =>
     parent ? theme.NODE_KEY : objectKey ? "#5c87ff" : theme.TEXT_POSITIVE};
     parent ? theme.NODE_KEY : objectKey ? "#5c87ff" : theme.TEXT_POSITIVE};
 `;
 `;

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

@@ -19,7 +19,7 @@ const StyledLoading = styled.div`
 `;
 `;
 
 
 const StyledLogo = styled.h2`
 const StyledLogo = styled.h2`
-  font-weight: 700;
+  font-weight: 600;
   font-size: 56px;
   font-size: 56px;
   pointer-events: none;
   pointer-events: none;
   margin-bottom: 10px;
   margin-bottom: 10px;

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

@@ -0,0 +1,64 @@
+import React from "react";
+import { ReactComponent } from "src/typings/global";
+import { Button } from "src/components/Button";
+import * as Styled from "./styles";
+
+type ControlProps = React.PropsWithChildren<{
+  setVisible: (status: boolean) => void;
+}>;
+
+type ModalTypes = {
+  Header: ReactComponent;
+  Content: ReactComponent;
+  Controls: React.FC<ControlProps>;
+};
+
+interface ModalProps {
+  visible: boolean;
+  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+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 }) => {
+  return (
+    <Styled.ControlsWrapper>
+      <Button onClick={() => setVisible(false)}>Close</Button>
+      {children}
+    </Styled.ControlsWrapper>
+  );
+};
+
+const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
+  children,
+  visible,
+  setVisible,
+}) => {
+  const onClick = (e: React.SyntheticEvent<HTMLDivElement>) => {
+    if (e.currentTarget === e.target) {
+      setVisible((v) => !v);
+    }
+  };
+
+  return (
+    <Styled.ModalWrapper visible={visible} onClick={onClick}>
+      <Styled.ModalInnerWrapper>{children}</Styled.ModalInnerWrapper>
+    </Styled.ModalWrapper>
+  );
+};
+
+Modal.Header = Header;
+Modal.Content = Content;
+Modal.Controls = Controls;
+
+export { Modal };

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

@@ -0,0 +1,50 @@
+import styled from "styled-components";
+
+export const ModalWrapper = styled.div<{ visible: boolean }>`
+  position: fixed;
+  top: 0;
+  left: 0;
+  height: 100vh;
+  width: 100%;
+  display: ${({ visible }) => (visible ? "flex" : "none")};
+  justify-content: center;
+  align-items: center;
+  background: rgba(0, 0, 0, 0.85);
+  z-index: 6;
+
+  * {
+    box-sizing: border-box;
+  }
+`;
+
+export const ModalInnerWrapper = styled.div`
+  min-width: 40vw;
+`;
+
+export const Title = styled.h2`
+  color: ${({ theme }) => theme.TEXT_NORMAL};
+  font-size: 20px !important;
+  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`
+  color: ${({ theme }) => theme.TEXT_NORMAL};
+  background: ${({ theme }) => theme.MODAL_BACKGROUND};
+  padding: 16px;
+  overflow: hidden scroll;
+`;
+
+export const ControlsWrapper = styled.div`
+  display: flex;
+  flex-direction: row-reverse;
+  background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
+  padding: 16px;
+  border-radius: 0 0 5px 5px;
+  gap: 10px;
+`;

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

@@ -22,7 +22,7 @@ const StyledLogo = styled.div`
   font-weight: 600;
   font-weight: 600;
   font-size: 20px;
   font-size: 20px;
   cursor: pointer;
   cursor: pointer;
-  font-weight: 700;
+  font-weight: 600;
   color: ${({ theme }) => theme.FULL_WHITE};
   color: ${({ theme }) => theme.FULL_WHITE};
 `;
 `;
 
 

+ 17 - 28
src/components/Sidebar/index.tsx

@@ -4,8 +4,6 @@ import Link from "next/link";
 import styled from "styled-components";
 import styled from "styled-components";
 import { CanvasDirection } from "reaflow";
 import { CanvasDirection } from "reaflow";
 import { TiFlowMerge } from "react-icons/ti";
 import { TiFlowMerge } from "react-icons/ti";
-import { BsList } from "react-icons/bs";
-import { MdUploadFile } from "react-icons/md";
 import { RiPatreonFill } from "react-icons/ri";
 import { RiPatreonFill } from "react-icons/ri";
 import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
 import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
 import {
 import {
@@ -20,15 +18,16 @@ import { Tooltip } from "src/components/Tooltip";
 import { ConfigActionType } from "src/reducer/reducer";
 import { ConfigActionType } from "src/reducer/reducer";
 import { useConfig } from "src/hocs/config";
 import { useConfig } from "src/hocs/config";
 import { useRouter } from "next/router";
 import { useRouter } from "next/router";
+import { ImportModal } from "src/containers/ImportModal";
 
 
 const StyledSidebar = styled.div`
 const StyledSidebar = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
-  width: 36px;
+  width: fit-content;
   background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
   background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
-  padding: 8px;
+  padding: 4px;
   border-right: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
   border-right: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
 `;
 `;
 
 
@@ -37,19 +36,23 @@ const StyledElement = styled.div`
   justify-content: center;
   justify-content: center;
   text-align: center;
   text-align: center;
   font-size: 28px;
   font-size: 28px;
-  font-weight: 700;
+  font-weight: 600;
   width: 100%;
   width: 100%;
   color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
   color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
   cursor: pointer;
   cursor: pointer;
 
 
-  &:hover :is(a, svg) {
-    color: ${({ theme }) => theme.INTERACTIVE_HOVER};
-  }
-
   svg {
   svg {
-    padding: 8px 0;
+    padding: 8px;
     vertical-align: middle;
     vertical-align: middle;
   }
   }
+
+  a {
+    display: flex;
+  }
+
+  &:hover :is(a, svg) {
+    color: ${({ theme }) => theme.INTERACTIVE_HOVER};
+  }
 `;
 `;
 
 
 const StyledText = styled.span<{ secondary?: boolean }>`
 const StyledText = styled.span<{ secondary?: boolean }>`
@@ -90,14 +93,6 @@ const StyledLogo = styled.div`
   color: ${({ theme }) => theme.FULL_WHITE};
   color: ${({ theme }) => theme.FULL_WHITE};
 `;
 `;
 
 
-const StyledImportFile = styled.label`
-  cursor: pointer;
-
-  input[type="file"] {
-    display: none;
-  }
-`;
-
 function rotateLayout(layout: CanvasDirection) {
 function rotateLayout(layout: CanvasDirection) {
   if (layout === "LEFT") return 90;
   if (layout === "LEFT") return 90;
   if (layout === "UP") return 180;
   if (layout === "UP") return 180;
@@ -109,6 +104,7 @@ export const Sidebar: React.FC = () => {
   const { json, settings, dispatch } = useConfig();
   const { json, settings, dispatch } = useConfig();
   const router = useRouter();
   const router = useRouter();
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
   const [jsonFile, setJsonFile] = React.useState<File | null>(null);
+  const [modalVisible, setModalVisible] = React.useState(false);
 
 
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files) setJsonFile(e.target.files?.item(0));
     if (e.target.files) setJsonFile(e.target.files?.item(0));
@@ -147,6 +143,7 @@ export const Sidebar: React.FC = () => {
   return (
   return (
     <StyledSidebar>
     <StyledSidebar>
       <StyledTopWrapper>
       <StyledTopWrapper>
+        <ImportModal visible={modalVisible} setVisible={setModalVisible} />
         <Link passHref href="/">
         <Link passHref href="/">
           <StyledElement onClick={() => router.push("/")}>
           <StyledElement onClick={() => router.push("/")}>
             <StyledLogo>
             <StyledLogo>
@@ -156,16 +153,8 @@ export const Sidebar: React.FC = () => {
           </StyledElement>
           </StyledElement>
         </Link>
         </Link>
         <Tooltip title="Import File">
         <Tooltip title="Import File">
-          <StyledElement>
-            <StyledImportFile>
-              <input
-                key={jsonFile?.name}
-                onChange={handleFileChange}
-                type="file"
-                accept="application/JSON"
-              />
-              <AiOutlineFileAdd />
-            </StyledImportFile>
+          <StyledElement onClick={() => setModalVisible(true)}>
+            <AiOutlineFileAdd />
           </StyledElement>
           </StyledElement>
         </Tooltip>
         </Tooltip>
         <Tooltip title="Rotate Layout">
         <Tooltip title="Rotate Layout">

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

@@ -25,7 +25,7 @@ const StyledTooltip = styled.div<{ visible: boolean }>`
   white-space: nowrap;
   white-space: nowrap;
   font-size: 16px;
   font-size: 16px;
   user-select: none;
   user-select: none;
-  font-weight: 600;
+  font-weight: 500;
   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.07),
   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 4px 8px rgba(0, 0, 0, 0.07), 0 8px 16px rgba(0, 0, 0, 0.07),
     0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);
     0 16px 32px rgba(0, 0, 0, 0.07), 0 32px 64px rgba(0, 0, 0, 0.07);

+ 1 - 1
src/constants/globalStyle.ts

@@ -11,7 +11,7 @@ const GlobalStyle = createGlobalStyle`
     font-weight: 400;
     font-weight: 400;
     font-size: 16px;
     font-size: 16px;
   }
   }
-
+  
   a {
   a {
     text-decoration: none;
     text-decoration: none;
     color: unset;
     color: unset;

+ 2 - 0
src/constants/theme.ts

@@ -32,6 +32,7 @@ export const darkTheme: DefaultTheme = {
   BACKGROUND_SECONDARY: "#2f3136",
   BACKGROUND_SECONDARY: "#2f3136",
   BACKGROUND_PRIMARY: "#36393f",
   BACKGROUND_PRIMARY: "#36393f",
   BACKGROUND_MODIFIER_ACCENT: "rgba(79,84,92,0.48)",
   BACKGROUND_MODIFIER_ACCENT: "rgba(79,84,92,0.48)",
+  MODAL_BACKGROUND: "#36393E",
   TEXT_NORMAL: "#dcddde",
   TEXT_NORMAL: "#dcddde",
   TEXT_POSITIVE: "hsl(139,calc(var(--saturation-factor, 1)*51.6%),52.2%)",
   TEXT_POSITIVE: "hsl(139,calc(var(--saturation-factor, 1)*51.6%),52.2%)",
 } as const;
 } as const;
@@ -50,6 +51,7 @@ export const lightTheme: DefaultTheme = {
   BACKGROUND_SECONDARY: "#f2f3f5",
   BACKGROUND_SECONDARY: "#f2f3f5",
   BACKGROUND_PRIMARY: "#FFFFFF",
   BACKGROUND_PRIMARY: "#FFFFFF",
   BACKGROUND_MODIFIER_ACCENT: "rgba(106,116,128,0.24)",
   BACKGROUND_MODIFIER_ACCENT: "rgba(106,116,128,0.24)",
+  MODAL_BACKGROUND: "#FFFFFF",
   TEXT_NORMAL: "#2e3338",
   TEXT_NORMAL: "#2e3338",
   TEXT_POSITIVE: "#008736",
   TEXT_POSITIVE: "#008736",
 } as const;
 } as const;

+ 2 - 2
src/containers/Editor/Tools.tsx

@@ -8,7 +8,7 @@ import {
 import { FiDownload } from "react-icons/fi";
 import { FiDownload } from "react-icons/fi";
 import { HiOutlineSun, HiOutlineMoon } from "react-icons/hi";
 import { HiOutlineSun, HiOutlineMoon } from "react-icons/hi";
 import { MdCenterFocusWeak } from "react-icons/md";
 import { MdCenterFocusWeak } from "react-icons/md";
-import { Input } from "src/components/Input";
+import { SearchInput } from "src/containers/SearchInput";
 import { useConfig } from "src/hocs/config";
 import { useConfig } from "src/hocs/config";
 import { ConfigActionType } from "src/reducer/reducer";
 import { ConfigActionType } from "src/reducer/reducer";
 import styled from "styled-components";
 import styled from "styled-components";
@@ -68,7 +68,7 @@ export const Tools: React.FC = () => {
       <StyledToolElement aria-label="switch theme" onClick={toggleTheme}>
       <StyledToolElement aria-label="switch theme" onClick={toggleTheme}>
         {settings.lightmode ? <HiOutlineMoon /> : <HiOutlineSun />}
         {settings.lightmode ? <HiOutlineMoon /> : <HiOutlineSun />}
       </StyledToolElement>
       </StyledToolElement>
-      <Input />
+      <SearchInput />
       <StyledToolElement aria-label="save" onClick={exportAsImage}>
       <StyledToolElement aria-label="save" onClick={exportAsImage}>
         <FiDownload />
         <FiDownload />
       </StyledToolElement>
       </StyledToolElement>

+ 134 - 0
src/containers/ImportModal/index.tsx

@@ -0,0 +1,134 @@
+import React from "react";
+import styled from "styled-components";
+import toast from "react-hot-toast";
+
+import { useConfig } from "src/hocs/config";
+import { ConfigActionType } from "src/reducer/reducer";
+import { Modal } from "src/components/Modal";
+import { Button } from "src/components/Button";
+import { AiOutlineUpload } from "react-icons/ai";
+
+const StyledInput = styled.input`
+  background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+  outline: none;
+  border: none;
+  border-radius: 5px;
+  padding: 6px 8px;
+  width: 100%;
+  margin-bottom: 10px;
+`;
+
+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;
+  justify-content: center;
+  align-items: center;
+  background: ${({ theme }) => theme.BACKGROUND_SECONDARY};
+  border: 2px dashed ${({ theme }) => theme.BACKGROUND_TERTIARY};
+  border-radius: 5px;
+  width: 100%;
+  padding: 16px;
+  cursor: pointer;
+
+  input[type="file"] {
+    display: none;
+  }
+`;
+
+const StyledFileName = styled.span`
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+`;
+
+const StyledUploadMessage = styled.h3`
+  color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
+  margin-bottom: 0;
+`;
+
+export const ImportModal = ({ visible, setVisible }) => {
+  const { dispatch } = useConfig();
+  const [url, setURL] = React.useState("");
+  const [jsonFile, setJsonFile] = React.useState<File | null>(null);
+
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files) setJsonFile(e.target.files?.item(0));
+  };
+
+  const handleImportFile = () => {
+    if (url) {
+      setJsonFile(null);
+
+      return fetch(url)
+        .then((res) => res.json())
+        .then((json) => {
+          dispatch({
+            type: ConfigActionType.SET_JSON,
+            payload: JSON.stringify(json),
+          });
+
+          setVisible(false);
+        })
+        .catch(() => toast.error("Failed to fetch JSON!"));
+    }
+
+    if (jsonFile) {
+      const reader = new FileReader();
+
+      reader.readAsText(jsonFile, "UTF-8");
+      reader.onload = function (data) {
+        dispatch({
+          type: ConfigActionType.SET_JSON,
+          payload: data.target?.result as string,
+        });
+      };
+    }
+  };
+
+  React.useEffect(() => {
+    return () => {
+      setJsonFile(null);
+      setURL("");
+    };
+  }, [visible]);
+
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Select An Import Method</Modal.Header>
+      <StyledModalContent>
+        <StyledInput
+          value={url}
+          onChange={(e) => setURL(e.target.value)}
+          type="url"
+          placeholder="URL of JSON to fetch"
+        />
+        <StyledUploadWrapper>
+          <input
+            key={jsonFile?.name}
+            onChange={handleFileChange}
+            type="file"
+            accept="application/JSON"
+          />
+          <AiOutlineUpload size={48} />
+          <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)}
+        >
+          Import
+        </Button>
+      </Modal.Controls>
+    </Modal>
+  );
+};

+ 1 - 1
src/containers/Incompatible/index.tsx

@@ -25,7 +25,7 @@ export const StyledIncompatible = styled.div`
 
 
     &::before {
     &::before {
       content: "Uh, oh!";
       content: "Uh, oh!";
-      font-weight: 700;
+      font-weight: 600;
       font-size: 60px;
       font-size: 60px;
       opacity: 0.6;
       opacity: 0.6;
     }
     }

+ 1 - 1
src/components/Input/index.tsx → src/containers/SearchInput/index.tsx

@@ -51,7 +51,7 @@ const StyledSearchButton = styled.button`
   }
   }
 `;
 `;
 
 
-export const Input: React.FC = () => {
+export const SearchInput: React.FC = () => {
   const [content, setContent, skip] = useFocusNode();
   const [content, setContent, skip] = useFocusNode();
 
 
   const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
   const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {

+ 1 - 1
src/pages/404.tsx

@@ -17,7 +17,7 @@ const StyledNotFound = styled.div`
 const StyledMessage = styled.h4`
 const StyledMessage = styled.h4`
   color: ${({ theme }) => theme.FULL_WHITE};
   color: ${({ theme }) => theme.FULL_WHITE};
   font-size: 25px;
   font-size: 25px;
-  font-weight: 700;
+  font-weight: 600;
   margin: 10px 0;
   margin: 10px 0;
 `;
 `;
 
 

+ 1 - 1
src/pages/_document.tsx

@@ -52,7 +52,7 @@ class MyDocument extends Document {
             crossOrigin="anonymous"
             crossOrigin="anonymous"
           />
           />
           <link
           <link
-            href="https://fonts.googleapis.com/css2?family=Catamaran:wght@300;400;500;600;700&family=PT+Sans:wght@400;500;700&family=Roboto+Mono:wght@500&display=swap"
+            href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&family=Roboto+Mono:wght@500&family=Catamaran:wght@400;500;600&display=swap"
             rel="stylesheet"
             rel="stylesheet"
           />
           />
         </Head>
         </Head>

+ 1 - 1
src/pages/index.tsx

@@ -3,12 +3,12 @@ import { Button } from "src/components/Button";
 import { Container } from "src/components/Container";
 import { Container } from "src/components/Container";
 import { Navbar } from "src/components/Navbar";
 import { Navbar } from "src/components/Navbar";
 import { Image } from "src/components/Image";
 import { Image } from "src/components/Image";
-import styled from "styled-components";
 import { AiFillGithub } from "react-icons/ai";
 import { AiFillGithub } from "react-icons/ai";
 import { Footer } from "src/components/Footer";
 import { Footer } from "src/components/Footer";
 import Head from "next/head";
 import Head from "next/head";
 import { Producthunt } from "src/components/Producthunt";
 import { Producthunt } from "src/components/Producthunt";
 import { useRouter } from "next/router";
 import { useRouter } from "next/router";
+import styled from "styled-components";
 
 
 const StyledHome = styled.div`
 const StyledHome = styled.div`
   padding: 24px;
   padding: 24px;

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

@@ -35,6 +35,7 @@ declare module "styled-components" {
     BACKGROUND_SECONDARY: string;
     BACKGROUND_SECONDARY: string;
     BACKGROUND_PRIMARY: string;
     BACKGROUND_PRIMARY: string;
     BACKGROUND_MODIFIER_ACCENT: string;
     BACKGROUND_MODIFIER_ACCENT: string;
+    MODAL_BACKGROUND: string;
     TEXT_NORMAL: string;
     TEXT_NORMAL: string;
     TEXT_POSITIVE: string;
     TEXT_POSITIVE: string;
     TEXT_DANGER: string;
     TEXT_DANGER: string;