├── .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 |
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 |
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 | );
--------------------------------------------------------------------------------