create graph embed

This commit is contained in:
AykutSarac 2022-07-23 13:20:41 +03:00
parent d1529c50d8
commit fa74a3a997
6 changed files with 208 additions and 81 deletions

View File

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

View File

@ -1,17 +1,60 @@
import React from "react"; 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 { CustomNode } from "src/components/CustomNode";
import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers"; import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
import useConfig from "src/hooks/store/useConfig"; import useConfig from "src/hooks/store/useConfig";
import styled from "styled-components";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
export const Graph: React.FC = () => { interface GraphProps {
const json = useConfig((state) => state.json); 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( const [expand, layout] = useConfig(
(state) => [state.settings.expand, state.settings.layout], (state) => [state.settings.expand, state.settings.layout],
shallow shallow
); );
const onInit = (ref: ReactZoomPanPinchRef) => {
updateSetting("zoomPanPinch", ref);
};
const [nodes, setNodes] = React.useState<NodeData[]>([]); const [nodes, setNodes] = React.useState<NodeData[]>([]);
const [edges, setEdges] = React.useState<EdgeData[]>([]); const [edges, setEdges] = React.useState<EdgeData[]>([]);
const [size, setSize] = React.useState({ const [size, setSize] = React.useState({
@ -37,18 +80,38 @@ export const Graph: React.FC = () => {
}; };
return ( return (
<Canvas <StyledEditorWrapper isWidget={isWidget}>
nodes={nodes} <TransformWrapper
edges={edges} maxScale={1.8}
maxWidth={size.width} minScale={0.4}
maxHeight={size.height} initialScale={0.7}
direction={layout} wheel={wheelOptions}
key={layout} onInit={onInit}
onCanvasClick={onCanvasClick} centerOnInit
onLayoutChange={onLayoutChange} >
node={CustomNode} <TransformComponent
zoomable={false} wrapperStyle={{
readonly 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>
); );
}; };

View File

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

View File

@ -1,11 +1,5 @@
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import {
TransformWrapper,
TransformComponent,
ReactZoomPanPinchRef,
} from "react-zoom-pan-pinch";
import { Tools } from "src/containers/Editor/Tools"; import { Tools } from "src/containers/Editor/Tools";
import { Graph } from "src/components/Graph"; import { Graph } from "src/components/Graph";
import useConfig from "src/hooks/store/useConfig"; import useConfig from "src/hooks/store/useConfig";
@ -14,53 +8,13 @@ const StyledLiveEditor = styled.div`
position: relative; 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 LiveEditor: React.FC = () => {
const updateSetting = useConfig((state) => state.updateSetting); const json = useConfig((state) => state.json);
const onInit = (ref: ReactZoomPanPinchRef) => {
updateSetting("zoomPanPinch", ref);
};
return ( return (
<StyledLiveEditor> <StyledLiveEditor>
<Tools /> <Tools />
<StyledEditorWrapper> <Graph json={json} />
<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>
</StyledLiveEditor> </StyledLiveEditor>
); );
}; };

View File

@ -20,22 +20,46 @@ const StyledErrorWrapper = styled.div`
font-weight: 600; 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 }) => { export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
const json = useConfig((state) => state.json); const json = useConfig((state) => state.json);
const [url, setURL] = React.useState(""); const [encodedJson, setEncodedJson] = React.useState("");
const [_, copy] = useCopyToClipboard(); 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(() => { React.useEffect(() => {
const jsonEncode = compress(JSON.parse(json)); const jsonEncode = compress(JSON.parse(json));
const jsonString = JSON.stringify(jsonEncode); const jsonString = JSON.stringify(jsonEncode);
setURL( setEncodedJson(encodeURIComponent(jsonString));
`https://jsonvisio.com/editor?json=${encodeURIComponent(jsonString)}`
);
}, [json]); }, [json]);
const handleShare = () => { const handleShare = (value: string) => {
copy(url); copy(value);
toast.success(`Link copied to clipboard.`); toast.success(`Link copied to clipboard.`);
setVisible(false); setVisible(false);
}; };
@ -44,7 +68,7 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
<Modal visible={visible} setVisible={setVisible}> <Modal visible={visible} setVisible={setVisible}>
<Modal.Header>Create a Share Link</Modal.Header> <Modal.Header>Create a Share Link</Modal.Header>
<Modal.Content> <Modal.Content>
{url.length > 5000 ? ( {encodedJson.length > 5000 ? (
<StyledErrorWrapper> <StyledErrorWrapper>
<BiErrorAlt size={60} /> <BiErrorAlt size={60} />
<StyledWarning> <StyledWarning>
@ -53,16 +77,35 @@ export const ShareModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
</StyledWarning> </StyledWarning>
</StyledErrorWrapper> </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.Content>
<Modal.Controls setVisible={setVisible}> <Modal.Controls setVisible={setVisible}></Modal.Controls>
{url.length < 5000 && (
<Button status="SECONDARY" onClick={handleShare}>
Copy
</Button>
)}
</Modal.Controls>
</Modal> </Modal>
); );
}; };

View File

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