├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── CodeGenerator └── CodeDisplay.js ├── Config.js ├── Editor ├── Canvas.js ├── DiagramAdapter.js ├── Sidebar.js ├── nodes.css └── sidebar.css ├── PropertyPanel ├── AssignPropertyEditor.js ├── IfPropertyEditor.js └── LogPropertyEditor.js ├── ReactFlow └── Nodes │ ├── AssignNode.js │ ├── ConsoleNode.js │ ├── EndNode.js │ ├── IfNode.js │ └── StartNode.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speed Build: A Low-Code App Builder Made With React 2 | 3 | Welcome to the first [DevPinch](https://devpinch.online) learning project! 4 | 5 | The goal is to create a collection of close to real-world projects that teach you about software and web development. 6 | 7 | ## Project Details 8 | - Description: Build a low-code app builder 9 | - Tech Stack: 10 | - [React](https://reactjs.org/) 11 | - [ReactFlow](https://reactflow.dev/) 12 | 13 | ## Tutorials 14 | To view all the relevant tutorials involving this project, check the project's [DevPinch page](https://devpinch.online/speed-build.html). 15 | 16 | More concepts will be added on this list once they've been implemented. So, stay tuned! 17 | 18 | ## Testing it out 19 | Feel free to test out the project and let me know if you have any comments or suggestions! 20 | 21 | Just execute the ff. command: 22 | 23 | ``` 24 | npm start 25 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-done-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "react": "^17.0.2", 10 | "react-dom": "^17.0.2", 11 | "react-flow-renderer": "^9.6.8", 12 | "react-scripts": "4.0.3", 13 | "react-select": "^5.1.0", 14 | "web-vitals": "^1.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "react-syntax-highlighter": "^15.4.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectkenneth/react-low-code-app-builder/666ca92d6fc6fe64f3097e2a2ac7212ca87f918d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 29 | React App 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectkenneth/react-low-code-app-builder/666ca92d6fc6fe64f3097e2a2ac7212ca87f918d/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectkenneth/react-low-code-app-builder/666ca92d6fc6fe64f3097e2a2ac7212ca87f918d/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import Canvas from "./Editor/Canvas"; 2 | 3 | function App() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/CodeGenerator/CodeDisplay.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { isNode, isEdge } from "react-flow-renderer"; 3 | 4 | import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; 5 | import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 6 | 7 | function check(traverseElement, needleId) { 8 | if (traverseElement.id === needleId) { 9 | return true; 10 | } else if (traverseElement.connections.length > 0) { 11 | return check(traverseElement.connections[0], needleId); 12 | } 13 | 14 | return false; 15 | } 16 | 17 | function stopAt(traverseElement, needleId, parentElement) { 18 | if (traverseElement.id === needleId) { 19 | parentElement.connections = []; 20 | } else if (traverseElement.connections.length > 0) { 21 | return stopAt(traverseElement.connections[0], needleId, traverseElement); 22 | } 23 | 24 | return parentElement; 25 | } 26 | 27 | function populateChildren(traverseElement) { 28 | const conns = traverseElement.connections; 29 | 30 | if (traverseElement.type === "if" && conns.length === 2) { 31 | const firstConn = conns[0]; 32 | const secondConn = conns[1]; 33 | 34 | let curConn = firstConn; 35 | do { 36 | if (check(secondConn, curConn.id, traverseElement)) { 37 | traverseElement.connections = [curConn]; 38 | traverseElement.children = [stopAt(firstConn, curConn.id, { type: "empty" }), stopAt(secondConn, curConn.id, { type: "empty" })]; 39 | // traverseElement.children = [firstConn, secondConn]; 40 | } 41 | 42 | curConn = curConn.connections[0]; 43 | } while (curConn && curConn.connections.length > 0); 44 | } 45 | 46 | return traverseElement; 47 | } 48 | 49 | function traverse(elements, item) { 50 | if (item) { 51 | const temp = { 52 | id: item.id, 53 | type: item.type, 54 | data: item.data, 55 | connections: elements 56 | .filter((el) => (isEdge(el) && el.source === item.id)) 57 | .map((edge) => { 58 | const edgeTarget = elements.filter((el) => (isNode(el) && el.id === edge.target)); 59 | 60 | if (edgeTarget.length > 0) { 61 | const subStruct = traverse(elements, edgeTarget[0]); 62 | subStruct.edge = edge; 63 | return subStruct; 64 | } 65 | 66 | return null; 67 | }) 68 | }; 69 | 70 | return populateChildren(temp); 71 | } else { 72 | const matches = elements.filter((el) => (isNode(el) && el.type === "start")); 73 | 74 | if (matches.length > 0) { 75 | return traverse(elements, matches[0]); 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | function generateScript(traverseItem, codeData, tabs = "") { 83 | let output = ""; 84 | 85 | let data = codeData[traverseItem.id].data; 86 | 87 | if (traverseItem.type === "assign") { 88 | output = tabs + "let " + data.variable + " = " + data.value + ";\r\n"; 89 | } else if (traverseItem.type === "if" && traverseItem.children && traverseItem.children.length === 2) { 90 | output = tabs + "if (" + data.left + " " + data.condition + " " + data.right + ") {\r\n"; 91 | 92 | output += generateScript(traverseItem.children[0], codeData, tabs + " "); 93 | 94 | if (traverseItem.children.length > 1 && traverseItem.children[1].type !== "empty") { 95 | output += tabs + "} else {\r\n" 96 | output += generateScript(traverseItem.children[1], codeData, tabs + " "); 97 | } 98 | 99 | output += tabs + "}\r\n\r\n"; 100 | } else if (traverseItem.type === "log") { 101 | output = tabs + "console.log(" + data.message + ");\r\n"; 102 | } 103 | 104 | traverseItem.connections.forEach((curTraverseItem) => { 105 | output += generateScript(curTraverseItem, codeData, tabs); 106 | }); 107 | 108 | return output; 109 | } 110 | 111 | const CodeDisplay = ({ nodes, codeData }) => { 112 | const [output, setOutput] = useState(""); 113 | 114 | const onDisplayCode = () => { 115 | const rootTraverseItem = traverse(nodes); 116 | 117 | let tempOutput = generateScript(rootTraverseItem, codeData, " "); 118 | 119 | tempOutput = "function generatedFunction() {\r\n" + tempOutput; 120 | tempOutput += "}"; 121 | 122 | setOutput(tempOutput); 123 | } 124 | 125 | const RenderFormattedCode = () => { 126 | if (output.length > 0) { 127 | return
128 |
129 | 130 | {output} 131 | 132 |
133 |
134 | } 135 | 136 | return null; 137 | } 138 | 139 | return ( 140 | <> 141 |
142 |
143 |
144 | {RenderFormattedCode()} 145 | 146 | ); 147 | } 148 | 149 | export default CodeDisplay; -------------------------------------------------------------------------------- /src/Config.js: -------------------------------------------------------------------------------- 1 | import IfPropertyEditor from "./PropertyPanel/IfPropertyEditor"; 2 | import AssignPropertyEditor from "./PropertyPanel/AssignPropertyEditor"; 3 | import LogPropertyEditor from "./PropertyPanel/LogPropertyEditor"; 4 | 5 | const Config = { 6 | if: { 7 | propertyEditorComponent: IfPropertyEditor 8 | }, 9 | assign: { 10 | propertyEditorComponent: AssignPropertyEditor 11 | }, 12 | log: { 13 | propertyEditorComponent: LogPropertyEditor 14 | } 15 | } 16 | 17 | export default Config; -------------------------------------------------------------------------------- /src/Editor/Canvas.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import "./nodes.css"; 4 | 5 | import Config from "../Config"; 6 | 7 | import Sidebar from "./Sidebar"; 8 | import CodeDisplay from "../CodeGenerator/CodeDisplay"; 9 | 10 | import DiagramAdapter from "./DiagramAdapter"; 11 | 12 | const Canvas = () => { 13 | const [nodes, setNodes] = useState([ 14 | { 15 | id: "node_0", 16 | type: "start", 17 | position: { x: 150, y: 25 }, 18 | }, 19 | { 20 | id: "node_1", 21 | type: "end", 22 | position: { x: 150, y: 225 }, 23 | }, 24 | { 25 | id: "node_0-node_1", type: "step", source: "node_0", target: "node_1", arrowHeadType: "arrowclosed", style: { 26 | strokeWidth: "3px" 27 | } 28 | } 29 | ]); 30 | 31 | const [codeData, setCodeData] = useState({ 32 | "node_0": { 33 | id: "node_0", 34 | type: "start", 35 | data: {} 36 | }, 37 | "node_1": { 38 | id: "node_1", 39 | type: "end", 40 | data: {} 41 | } 42 | }); 43 | 44 | const [activeCodeData, setActiveCodeData] = useState({ id: null, type: "" }); 45 | 46 | const onActivateNode = (activeNodeId) => setActiveCodeData(codeData[activeNodeId]); 47 | 48 | const onDeactivateAll = () => { 49 | setActiveCodeData({ id: null, type: "" }); 50 | }; 51 | 52 | const onUpdateCodeData = (data) => { 53 | activeCodeData.data = data; 54 | codeData[data.id] = activeCodeData; 55 | 56 | setActiveCodeData(activeCodeData); 57 | setCodeData(codeData); 58 | }; 59 | 60 | const onAddNode = (newNodeId, newNodeType) => { 61 | codeData[newNodeId] = { 62 | id: newNodeId, 63 | type: newNodeType, 64 | data: {} 65 | }; 66 | 67 | setCodeData(codeData); 68 | }; 69 | 70 | const renderPropertyEditor = () => { 71 | if (activeCodeData.id !== null && Config[activeCodeData.type]) { 72 | const PropertyEditor = Config[activeCodeData.type].propertyEditorComponent; 73 | return (); 74 | } else { 75 | return (Select an element from the canvas to update.); 76 | } 77 | } 78 | 79 | return ( 80 |
81 |
82 |
83 |

Tools

84 | 85 |
86 |
87 |

Canvas

88 | 89 |
90 |
91 |

Property Panel

92 | {renderPropertyEditor()} 93 |



94 |

Code Generator

95 | 96 |
97 |
98 |
99 | ); 100 | } 101 | 102 | export default Canvas; -------------------------------------------------------------------------------- /src/Editor/DiagramAdapter.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from "react"; 2 | import ReactFlow, { addEdge, removeElements, isNode } from "react-flow-renderer"; 3 | 4 | import IfNode from "../ReactFlow/Nodes/IfNode"; 5 | import StartNode from "../ReactFlow/Nodes/StartNode"; 6 | import EndNode from "../ReactFlow/Nodes/EndNode"; 7 | import AssignNode from "../ReactFlow/Nodes/AssignNode"; 8 | import ConsoleCustomNode from "../ReactFlow/Nodes/ConsoleNode"; 9 | 10 | const nodeTypes = { 11 | if: IfNode, 12 | start: StartNode, 13 | end: EndNode, 14 | assign: AssignNode, 15 | log: ConsoleCustomNode 16 | }; 17 | 18 | let id = 2; 19 | const getId = () => `node_${id++}`; 20 | 21 | const DiagramAdapter = ({ nodes, setNodes, onAddNode, onActivateNode, onDeactivateAll }) => { 22 | const reactFlowWrapper = useRef(null); 23 | const [reactFlowInstance, setReactFlowInstance] = useState(null); 24 | 25 | const onLoad = (_reactFlowInstance) => 26 | setReactFlowInstance(_reactFlowInstance); 27 | 28 | const onConnect = (params) => setNodes((els) => { 29 | params.type = "step"; 30 | params.arrowHeadType = "arrowclosed"; 31 | 32 | if (params.sourceHandle === "true") { 33 | params.label = "TRUE" 34 | } else if (params.sourceHandle === "false") { 35 | params.label = "FALSE" 36 | } 37 | 38 | params.style = { 39 | strokeWidth: "3px" 40 | } 41 | 42 | return addEdge(params, els); 43 | }); 44 | 45 | const onElementsRemove = (elementsToRemove) => { 46 | setNodes((els) => removeElements(elementsToRemove, els)); 47 | }; 48 | 49 | const onSelectionChange = (elements) => { 50 | if (elements) { 51 | const selectedNodes = elements.filter((els) => isNode(els)); 52 | 53 | if (selectedNodes.length > 0) { 54 | onActivateNode(selectedNodes[0].id); 55 | } 56 | } 57 | }; 58 | 59 | const onPaneClick = () => onDeactivateAll(); 60 | 61 | const onDragOver = (event) => { 62 | event.preventDefault(); 63 | event.dataTransfer.dropEffect = "move"; 64 | }; 65 | 66 | const onDrop = (event) => { 67 | event.preventDefault(); 68 | 69 | const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); 70 | const type = event.dataTransfer.getData("application/reactflow"); 71 | const position = reactFlowInstance.project({ 72 | x: event.clientX - reactFlowBounds.left, 73 | y: event.clientY - reactFlowBounds.top, 74 | }); 75 | const nodeId = getId(); 76 | 77 | const newNode = { 78 | id: nodeId, 79 | type, 80 | position, 81 | data: {} 82 | }; 83 | 84 | setNodes(nodes.concat(newNode)); 85 | onAddNode(nodeId, type); 86 | }; 87 | 88 | return ( 89 |
90 | 104 |
105 | ); 106 | } 107 | 108 | export default DiagramAdapter; -------------------------------------------------------------------------------- /src/Editor/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./sidebar.css" 4 | 5 | const Sidebar = () => { 6 | const onDragStart = (event, nodeType) => { 7 | event.dataTransfer.setData("application/reactflow", nodeType); 8 | event.dataTransfer.effectAllowed = "move"; 9 | }; 10 | 11 | return ( 12 | 29 | ); 30 | }; 31 | 32 | export default Sidebar; -------------------------------------------------------------------------------- /src/Editor/nodes.css: -------------------------------------------------------------------------------- 1 | .node { 2 | padding: 10px; 3 | border: 3pt solid #bbb; 4 | background-color: #fff; 5 | } 6 | 7 | .node.node-start { 8 | color: green; 9 | } 10 | 11 | .node.node-end { 12 | color: red; 13 | } 14 | 15 | .node.node-if { 16 | color: blue; 17 | } 18 | 19 | .node.node-log { 20 | background-color: #ddd; 21 | } 22 | 23 | .node.node-assign, 24 | .node.node-log { 25 | color: black; 26 | } 27 | 28 | .selected .node { 29 | box-shadow: 0px 0px 19px 12px #DBDBDB; 30 | border-color: #000; 31 | } 32 | 33 | .container .react-flow__edge-path { 34 | stroke: #ccc; 35 | stroke-width: 3pt; 36 | } 37 | 38 | .container .selected .react-flow__edge-path { 39 | stroke: #000; 40 | } 41 | 42 | .container .selected .react-flow__edge-path .react-flow__arrowhead { 43 | stroke: #000; 44 | } 45 | 46 | .container .node input { 47 | width: 120px; 48 | } -------------------------------------------------------------------------------- /src/Editor/sidebar.css: -------------------------------------------------------------------------------- 1 | aside .node { 2 | margin-bottom: 5px; 3 | border-radius: 3px; 4 | } -------------------------------------------------------------------------------- /src/PropertyPanel/AssignPropertyEditor.js: -------------------------------------------------------------------------------- 1 | const AssignPropertyEditor = ({ codeData, updateData }) => { 2 | const type = codeData.type; 3 | const localData = codeData.data; 4 | 5 | if (type === "assign") { 6 | const onVariableChange = (event) => { 7 | localData.variable = event.target.value; 8 | 9 | updateData(localData); 10 | }; 11 | 12 | const onValueChange = (event) => { 13 | localData.value = event.target.value; 14 | 15 | updateData(localData); 16 | }; 17 | 18 | return ( 19 |
20 | Assign:
21 | 22 |  =  23 | 24 |
25 | ); 26 | } 27 | 28 | return null; 29 | }; 30 | 31 | export default AssignPropertyEditor; -------------------------------------------------------------------------------- /src/PropertyPanel/IfPropertyEditor.js: -------------------------------------------------------------------------------- 1 | const options = [ 2 | { value: "=", label: "=" }, 3 | { value: "<=", label: "<=" }, 4 | { value: "<", label: "<" }, 5 | { value: "=>", label: "=>" }, 6 | { value: ">", label: ">" }, 7 | { value: "!=", label: "!=" }, 8 | ]; 9 | 10 | const IfPropertyEditor = ({ codeData, updateData }) => { 11 | const type = codeData.type; 12 | const localData = codeData.data; 13 | 14 | if (type === "if") { 15 | const onLeftChange = (event) => { 16 | localData.left = event.target.value; 17 | 18 | updateData(localData); 19 | }; 20 | 21 | const onConditionChange = (event) => { 22 | localData.condition = event.target.value; 23 | 24 | updateData(localData); 25 | }; 26 | 27 | const onRightChange = (event) => { 28 | localData.right = event.target.value; 29 | 30 | updateData(localData); 31 | }; 32 | 33 | const renderOptions = () => 34 | options.map(opt => 35 | 36 | ); 37 | 38 | return ( 39 |
40 | If:
41 | 42 |    45 | 46 |
47 | ); 48 | } 49 | 50 | return null; 51 | }; 52 | 53 | export default IfPropertyEditor; -------------------------------------------------------------------------------- /src/PropertyPanel/LogPropertyEditor.js: -------------------------------------------------------------------------------- 1 | const LogPropertyEditor = ({ codeData, updateData }) => { 2 | const type = codeData.type; 3 | const localData = codeData.data; 4 | 5 | if (type === "log") { 6 | const onMessageChange = (event) => { 7 | localData.message = event.target.value; 8 | 9 | updateData(localData); 10 | }; 11 | 12 | return ( 13 |
14 | Log:
15 | 16 |
17 | ); 18 | } 19 | 20 | return null; 21 | }; 22 | 23 | export default LogPropertyEditor; -------------------------------------------------------------------------------- /src/ReactFlow/Nodes/AssignNode.js: -------------------------------------------------------------------------------- 1 | import { Handle } from "react-flow-renderer"; 2 | 3 | const AssignNode = ({ data, isConnectable }) => { 4 | return ( 5 |
6 | 12 |
13 | ASSIGN 14 |
15 | 21 |
22 | ); 23 | }; 24 | 25 | export default AssignNode; -------------------------------------------------------------------------------- /src/ReactFlow/Nodes/ConsoleNode.js: -------------------------------------------------------------------------------- 1 | import { Handle } from "react-flow-renderer"; 2 | 3 | const ConsoleCustomNode = ({ data, isConnectable }) => { 4 | return ( 5 |
6 | 12 |
13 | LOG 14 |
15 | 21 |
22 | ); 23 | }; 24 | 25 | export default ConsoleCustomNode; -------------------------------------------------------------------------------- /src/ReactFlow/Nodes/EndNode.js: -------------------------------------------------------------------------------- 1 | import { Handle } from "react-flow-renderer"; 2 | 3 | const StartCustomNode = ({ data, isConnectable }) => { 4 | return ( 5 |
6 |
7 | END 8 |
9 | 15 |
16 | ); 17 | }; 18 | 19 | export default StartCustomNode; -------------------------------------------------------------------------------- /src/ReactFlow/Nodes/IfNode.js: -------------------------------------------------------------------------------- 1 | import { Handle } from "react-flow-renderer"; 2 | 3 | const IfCustomNode = ({ data, isConnectable }) => { 4 | return ( 5 |
6 | 12 |
13 | IF 14 |
15 | 21 | 27 |
28 | ); 29 | }; 30 | 31 | export default IfCustomNode; -------------------------------------------------------------------------------- /src/ReactFlow/Nodes/StartNode.js: -------------------------------------------------------------------------------- 1 | import { Handle } from "react-flow-renderer"; 2 | 3 | const StartNode = ({ data, isConnectable }) => { 4 | return ( 5 |
6 |
7 | START 8 |
9 | 15 |
16 | ); 17 | }; 18 | 19 | export default StartNode; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); --------------------------------------------------------------------------------