Bläddra i källkod

create graph embed

AykutSarac 2 år sedan
förälder
incheckning
fa74a3a997

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

@@ -29,6 +29,7 @@ const StyledButton = styled.button<{
   padding: 8px 16px;
   min-width: 60px;
   width: ${({ block }) => (block ? "100%" : "fit-content")};
+  height: 40px;
 
   :disabled {
     cursor: not-allowed;
@@ -45,6 +46,8 @@ const StyledButtonContent = styled.div`
   justify-content: center;
   align-items: center;
   gap: 8px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 `;
 
 export const Button: React.FC<ButtonProps> = ({

+ 79 - 16
src/components/Graph/index.tsx

@@ -1,17 +1,60 @@
 import React from "react";
-import { Canvas, EdgeData, ElkRoot, NodeData } from "reaflow";
+import {
+  ReactZoomPanPinchRef,
+  TransformComponent,
+  TransformWrapper,
+} from "react-zoom-pan-pinch";
+import {
+  Canvas,
+  CanvasContainerProps,
+  EdgeData,
+  ElkRoot,
+  NodeData,
+} from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
 import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
 import useConfig from "src/hooks/store/useConfig";
+import styled from "styled-components";
 import shallow from "zustand/shallow";
 
-export const Graph: React.FC = () => {
-  const json = useConfig((state) => state.json);
+interface GraphProps {
+  json: string;
+  isWidget?: boolean;
+}
+
+const wheelOptions = {
+  step: 0.05,
+};
+
+const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
+  position: absolute;
+  width: 100%;
+  height: ${({ isWidget }) => (isWidget ? "100vh" : "calc(100vh - 36px)")};
+
+  :active {
+    cursor: move;
+  }
+
+  rect {
+    fill: ${({ theme }) => theme.BACKGROUND_NODE};
+  }
+`;
+
+export const Graph: React.FC<GraphProps & CanvasContainerProps> = ({
+  json,
+  isWidget = false,
+  ...props
+}) => {
+  const updateSetting = useConfig((state) => state.updateSetting);
   const [expand, layout] = useConfig(
     (state) => [state.settings.expand, state.settings.layout],
     shallow
   );
 
+  const onInit = (ref: ReactZoomPanPinchRef) => {
+    updateSetting("zoomPanPinch", ref);
+  };
+
   const [nodes, setNodes] = React.useState<NodeData[]>([]);
   const [edges, setEdges] = React.useState<EdgeData[]>([]);
   const [size, setSize] = React.useState({
@@ -37,18 +80,38 @@ export const Graph: React.FC = () => {
   };
 
   return (
-    <Canvas
-      nodes={nodes}
-      edges={edges}
-      maxWidth={size.width}
-      maxHeight={size.height}
-      direction={layout}
-      key={layout}
-      onCanvasClick={onCanvasClick}
-      onLayoutChange={onLayoutChange}
-      node={CustomNode}
-      zoomable={false}
-      readonly
-    />
+    <StyledEditorWrapper isWidget={isWidget}>
+      <TransformWrapper
+        maxScale={1.8}
+        minScale={0.4}
+        initialScale={0.7}
+        wheel={wheelOptions}
+        onInit={onInit}
+        centerOnInit
+      >
+        <TransformComponent
+          wrapperStyle={{
+            width: "100%",
+            height: "100%",
+            overflow: "hidden",
+          }}
+        >
+          <Canvas
+            nodes={nodes}
+            edges={edges}
+            maxWidth={size.width}
+            maxHeight={size.height}
+            direction={layout}
+            key={layout}
+            onCanvasClick={onCanvasClick}
+            onLayoutChange={onLayoutChange}
+            node={CustomNode}
+            zoomable={false}
+            readonly
+            {...props}
+          />
+        </TransformComponent>
+      </TransformWrapper>
+    </StyledEditorWrapper>
   );
 };

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

@@ -8,10 +8,10 @@ const StyledInput = styled.input`
   border: none;
   border-radius: 4px;
   line-height: 32px;
-  padding: 12px 8px;
+  padding: 10px;
   width: 100%;
   margin-bottom: 10px;
-  height: 30px;
+  height: 40px;
 `;
 
 type InputProps = React.InputHTMLAttributes<HTMLInputElement>;

+ 2 - 48
src/containers/Editor/LiveEditor/index.tsx

@@ -1,11 +1,5 @@
 import React from "react";
 import styled from "styled-components";
-import {
-  TransformWrapper,
-  TransformComponent,
-  ReactZoomPanPinchRef,
-} from "react-zoom-pan-pinch";
-
 import { Tools } from "src/containers/Editor/Tools";
 import { Graph } from "src/components/Graph";
 import useConfig from "src/hooks/store/useConfig";
@@ -14,53 +8,13 @@ const StyledLiveEditor = styled.div`
   position: relative;
 `;
 
-const StyledEditorWrapper = styled.div`
-  position: absolute;
-  width: 100%;
-  height: calc(100vh - 36px);
-
-  :active {
-    cursor: move;
-  }
-
-  rect {
-    fill: ${({ theme }) => theme.BACKGROUND_NODE};
-  }
-`;
-
-const wheelOptions = {
-  step: 0.05,
-};
-
 const LiveEditor: React.FC = () => {
-  const updateSetting = useConfig((state) => state.updateSetting);
-
-  const onInit = (ref: ReactZoomPanPinchRef) => {
-    updateSetting("zoomPanPinch", ref);
-  };
+  const json = useConfig((state) => state.json);
 
   return (
     <StyledLiveEditor>
       <Tools />
-      <StyledEditorWrapper>
-        <TransformWrapper
-          maxScale={1.8}
-          minScale={0.4}
-          initialScale={0.9}
-          wheel={wheelOptions}
-          onInit={onInit}
-        >
-          <TransformComponent
-            wrapperStyle={{
-              width: "100%",
-              height: "100%",
-              overflow: "hidden",
-            }}
-          >
-            <Graph />
-          </TransformComponent>
-        </TransformWrapper>
-      </StyledEditorWrapper>
+      <Graph json={json} />
     </StyledLiveEditor>
   );
 };

+ 58 - 15
src/containers/Modals/ShareModal/index.tsx

@@ -20,22 +20,46 @@ const StyledErrorWrapper = styled.div`
   font-weight: 600;
 `;
 
+const StyledFlex = styled.div`
+  display: flex;
+  gap: 12px;
+`;
+
+const StyledContainer = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  padding: 12px 0;
+  border-top: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+  font-size: 12px;
+  line-height: 16px;
+  font-weight: 600;
+  text-transform: uppercase;
+  color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
+
+  &:first-of-type {
+    padding-top: 0;
+    border: none;
+  }
+`;
+
 export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   const json = useConfig((state) => state.json);
-  const [url, setURL] = React.useState("");
+  const [encodedJson, setEncodedJson] = React.useState("");
   const [_, copy] = useCopyToClipboard();
 
+  const embedText = `<iframe src="https://jsonvisio.com/widget/${encodedJson}" width="512" height="384"></iframe>`;
+  const shareURL = `https://jsonvisio.com/editor?json=${encodedJson}`;
+
   React.useEffect(() => {
     const jsonEncode = compress(JSON.parse(json));
     const jsonString = JSON.stringify(jsonEncode);
 
-    setURL(
-      `https://jsonvisio.com/editor?json=${encodeURIComponent(jsonString)}`
-    );
+    setEncodedJson(encodeURIComponent(jsonString));
   }, [json]);
 
-  const handleShare = () => {
-    copy(url);
+  const handleShare = (value: string) => {
+    copy(value);
     toast.success(`Link copied to clipboard.`);
     setVisible(false);
   };
@@ -44,7 +68,7 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
     <Modal visible={visible} setVisible={setVisible}>
       <Modal.Header>Create a Share Link</Modal.Header>
       <Modal.Content>
-        {url.length > 5000 ? (
+        {encodedJson.length > 5000 ? (
           <StyledErrorWrapper>
             <BiErrorAlt size={60} />
             <StyledWarning>
@@ -53,16 +77,35 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
             </StyledWarning>
           </StyledErrorWrapper>
         ) : (
-          <Input value={url} type="url" readOnly />
+          <>
+            <StyledContainer>
+              Share Link
+              <StyledFlex>
+                <Input value={shareURL} type="url" readOnly />
+                <Button
+                  status="SECONDARY"
+                  onClick={() => handleShare(shareURL)}
+                >
+                  Copy
+                </Button>
+              </StyledFlex>
+            </StyledContainer>
+            <StyledContainer>
+              Embed into your website
+              <StyledFlex>
+                <Input value={embedText} type="url" readOnly />
+                <Button
+                  status="SECONDARY"
+                  onClick={() => handleShare(embedText)}
+                >
+                  Copy
+                </Button>
+              </StyledFlex>
+            </StyledContainer>
+          </>
         )}
       </Modal.Content>
-      <Modal.Controls setVisible={setVisible}>
-        {url.length < 5000 && (
-          <Button status="SECONDARY" onClick={handleShare}>
-            Copy
-          </Button>
-        )}
-      </Modal.Controls>
+      <Modal.Controls setVisible={setVisible}></Modal.Controls>
     </Modal>
   );
 };

+ 64 - 0
src/pages/widget/[json].tsx

@@ -0,0 +1,64 @@
+import { decompress } from "compress-json";
+import dynamic from "next/dynamic";
+import { useRouter } from "next/router";
+import React from "react";
+import { isValidJson } from "src/utils/isValidJson";
+import styled from "styled-components";
+
+const Graph = dynamic<any>(
+  () => import("src/components/Graph").then((c) => c.Graph),
+  { ssr: false }
+);
+
+const StyledAttribute = styled.a`
+  position: fixed;
+  bottom: 0;
+  right: 0;
+  background: rgba(255, 255, 255, 0.3);
+  padding: 4px 8px;
+  font-size: 14px;
+  font-weight: 500;
+`;
+
+function inIframe() {
+  try {
+    return window.self !== window.top;
+  } catch (e) {
+    return true;
+  }
+}
+
+const Widget = () => {
+  const { query, push } = useRouter();
+  const [json, setJson] = React.useState("");
+
+  React.useEffect(() => {
+    const isJsonValid =
+      typeof query.json === "string" &&
+      isValidJson(decodeURIComponent(query.json));
+
+    if (isJsonValid) {
+      const jsonDecoded = decompress(JSON.parse(isJsonValid));
+      const jsonString = JSON.stringify(jsonDecoded);
+
+      setJson(jsonString);
+    }
+
+    if (!inIframe()) push("/");
+  }, [query.json]);
+
+  return (
+    <div>
+      <Graph json={json} isWidget />
+      <StyledAttribute
+        href={`https://jsonvisio.com/editor?json=${query.json}`}
+        target="_blank"
+        rel="noreferrer"
+      >
+        jsonvisio.com
+      </StyledAttribute>
+    </div>
+  );
+};
+
+export default Widget;