diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0e16d72..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}" - } - ] -} diff --git a/admin-client/src/components/Tile.tsx b/admin-client/src/components/Tile.tsx index 1f57d56..9735d46 100644 --- a/admin-client/src/components/Tile.tsx +++ b/admin-client/src/components/Tile.tsx @@ -3,15 +3,15 @@ import React from 'react'; type TileProps = { category: string; name: string; - url: string; + src: string; onClickFunc: () => void; }; -const Tile: React.FC = ({ category, name, url, onClickFunc }) => { +const Tile: React.FC = ({ category, name, src, onClickFunc }) => { return (
- {name} + {name}
{name}
diff --git a/backend/.gitignore b/backend/.gitignore index 16472a6..c9003c6 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -12,7 +12,6 @@ /build #ignore uploads folder -/uploads # misc .env .DS_Store diff --git a/backend/babel.config.json b/backend/babel.config.json new file mode 100644 index 0000000..bfef096 --- /dev/null +++ b/backend/babel.config.json @@ -0,0 +1,17 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "edge": "17", + "firefox": "60", + "chrome": "67", + "safari": "11.1" + }, + "useBuiltIns": "usage", + "corejs": "3.6.5" + } + ] + ] +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 480f2a4..e14bbcd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@babel/generator": "^7.21.1", "@types/multer": "^1.4.7", "@types/node": "^18.11.9", "@types/socket.io": "^3.0.2", + "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", @@ -21,6 +23,7 @@ "socket.io": "^4.5.1" }, "devDependencies": { + "@types/babel__generator": "^7.6.4", "@types/express": "^4.17.13", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", @@ -1095,6 +1098,58 @@ "node": ">=14.0.0" } }, + "node_modules/@babel/generator": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", + "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", + "dependencies": { + "@babel/types": "^7.21.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", + "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1217,11 +1272,31 @@ "dev": true, "peer": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "engines": { "node": ">=6.0.0" } @@ -1229,14 +1304,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1326,6 +1399,15 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1663,12 +1745,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1676,7 +1758,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1945,9 +2027,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -2703,6 +2785,43 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3600,6 +3719,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4322,9 +4452,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -4904,6 +5034,14 @@ "globrex": "^0.1.2" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6199,6 +6337,48 @@ "tslib": "^2.3.1" } }, + "@babel/generator": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", + "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", + "requires": { + "@babel/types": "^7.21.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/types": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", + "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -6290,23 +6470,35 @@ "dev": true, "peer": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -6381,6 +6573,15 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -6649,12 +6850,12 @@ "dev": true }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -6662,7 +6863,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } @@ -6867,9 +7068,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { "version": "0.5.0", @@ -7453,6 +7654,38 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + } } }, "fast-deep-equal": { @@ -8089,6 +8322,11 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -8611,9 +8849,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -9028,6 +9266,11 @@ "globrex": "^0.1.2" } }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 8f0e742..6d2f76a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,11 @@ "author": "", "license": "ISC", "dependencies": { + "@babel/generator": "^7.21.1", "@types/multer": "^1.4.7", "@types/node": "^18.11.9", "@types/socket.io": "^3.0.2", + "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", @@ -24,6 +26,7 @@ "socket.io": "^4.5.1" }, "devDependencies": { + "@types/babel__generator": "^7.6.4", "@types/express": "^4.17.13", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", diff --git a/backend/src/Controller/astController.ts b/backend/src/Controller/astController.ts new file mode 100644 index 0000000..5e92a63 --- /dev/null +++ b/backend/src/Controller/astController.ts @@ -0,0 +1,35 @@ +import generate from "@babel/generator"; +import generatePyCode from "../../utils/generatePyCode"; +import { Request, Response } from "express"; + +export const generateJsCode = async (req: Request, res: Response) => { + /** + * generate code from the ast + * if no error, send response with the generated code + * else send error + */ + try { + const generated = await generate(req.body); + res.status(200).json({ code: generated.code }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const generatePythonCode = async (req: Request, res: Response) => { + /** + * generate code from the ast + * if no error, send response with the generated code + * else send error + */ + const generated = generatePyCode(req.body); + try { + console.log(req.body); + res.status(500).json({ code: generated }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + + + diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index bd5eb6d..0d6ca10 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -1,4 +1,6 @@ import { Socket, Server } from "socket.io"; +import { findConnections } from "./../../utils/tileConnections"; +import { TileConnection } from "./../../types/socket.types.d"; import { DefaultEventsMap } from "socket.io/dist/typed-events"; import { RoomData, @@ -72,11 +74,20 @@ export const tileDrop = ( if (room) { room.tiles.push({ tile: { - id: data.tile.id, - category: data.tile.category, - src: data.tile.src, x: data.tile.x, y: data.tile.y, + id: data.tile.id, + _id: data.tile._id, + src: data.tile.src, + name: data.tile.name, + width: data.tile.width, + color: data.tile.color, + height: data.tile.height, + points: data.tile.points, + astNode: data.tile.astNode, + anchors: data.tile.anchors, + category: data.tile.category, + textPosition: data.tile.textPosition, }, }); io.to(data.roomId).emit("room-data", room); @@ -159,6 +170,26 @@ export const cursorMove = ( } }; +export const deleteLine = ( + data: SocketDeleteData, + state: RoomData[], + io: Server, +) => { + /** + * check if the room exists + * if it does,then check if the tile exists + * if it does, delete the tile from the room + * update the room state + * emit the room data to the every user in the room + */ + + const room = state.find((room) => room.roomId === data.roomId); + if (room) { + room.tileConnections = findConnections(data.id, room.tileConnections); + io.to(data.roomId).emit("room-data", room); + } +}; + export const deleteTile = ( data: SocketDeleteData, state: RoomData[], @@ -182,6 +213,7 @@ export const deleteTile = ( export const disconnect = ( state: RoomData[], socket: Socket, + io: Server, ) => { /** * before updating the state check if the user is the host @@ -197,8 +229,25 @@ export const disconnect = ( state.splice(state.indexOf(room), 1); } else { room.users.splice(room.users.indexOf(user), 1); + io.emit("room-data", room); } } }); }); }; + +export const tileConnect = ( + data: TileConnection, + state: RoomData[], + io: Server, +) => { + const room = state.find((room) => room.roomId === data.roomId); + if (room) { + if (room.tileConnections?.length > 0) { + room.tileConnections?.push(data); + } else { + room.tileConnections = [data]; + } + } + io.to(data.roomId).emit("room-data", room); +}; diff --git a/backend/src/Controller/tileController.ts b/backend/src/Controller/tileController.ts index 6beffd4..ee96dab 100644 --- a/backend/src/Controller/tileController.ts +++ b/backend/src/Controller/tileController.ts @@ -35,7 +35,13 @@ export const createTile = async (req: Request, res: Response) => { const tile = new Tile({ category: formData.category, name: formData.name, - url: filePath, + src: filePath, + points: formData.points, + color: formData.color, + textPosition: { + x: formData.textPositionX, + y: formData.textPositionY, + }, }); const newTile = await tile.save(); res.status(200).json(newTile); @@ -80,10 +86,15 @@ export const updateTile = async (req: Request, res: Response) => { if (process.env.NODE_ENV === "production") { backendUrl = process.env.PROD_BACKEND_URL; } + tile.textPosition = req.body.textPosition + ? req.body.textPosition + : tile.textPosition; + tile.color = req.body.color ? req.body.color : tile.color; + tile.points = req.body.points ? req.body.points : tile.points; + tile.src = tile.name = req.body.name ? req.body.name : tile.name; + tile.anchors = req.body.anchors ? req.body.anchors : tile.anchors; + req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.src; tile.category = req.body.category ? req.body.category : tile.category; - tile.name = req.body.name ? req.body.name : tile.name; - tile.url = - req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.url; const updatedTile = await tile.save(); res.status(200).json(updatedTile); } else { @@ -104,8 +115,8 @@ export const deleteTile = async (req: Request, res: Response) => { try { const tile = await Tile.findByIdAndDelete(req.params.id); if (tile) { - const url = tile.url.split("/"); - const fileName = url[url.length - 1]; + const src = tile.src.split("/"); + const fileName = src[src.length - 1]; fs.unlink(`./uploads/${fileName}`, (err) => { if (err) { console.log(err); diff --git a/backend/src/Model/tileModel.ts b/backend/src/Model/tileModel.ts index 3dd1ebd..27d0c64 100644 --- a/backend/src/Model/tileModel.ts +++ b/backend/src/Model/tileModel.ts @@ -1,5 +1,14 @@ import { Schema, model } from "mongoose"; - +const textPositionSchema = new Schema({ + x: { + type: Number, + required: true, + }, + y: { + type: Number, + required: true, + }, +}); //Schema for the Tile model const TileSchema = new Schema({ @@ -11,10 +20,37 @@ const TileSchema = new Schema({ type: String, required: true, }, - url: { + src: { type: String, required: true, }, + points: { + type: [Number], + required: true, + }, + color: { + type: String, + required: true, + }, + anchors: { + type: [ + { + type: String, + x: Number, + y: Number, + }, + ], + required: true, + }, + width: { + type: Number, + required: true, + }, + height: { + type: Number, + required: true, + }, + textPosition: textPositionSchema, }); export const Tile = model("Tile", TileSchema); diff --git a/backend/src/Routes/ApiRoutes.ts b/backend/src/Routes/ApiRoutes.ts index 64b03c4..c426fa2 100644 --- a/backend/src/Routes/ApiRoutes.ts +++ b/backend/src/Routes/ApiRoutes.ts @@ -1,5 +1,10 @@ import { Router } from "express"; import { uploadFiles } from "../middleware/upload"; +import { + generateJsCode, + generatePythonCode, +} from "../Controller/astController"; +import bodyParser from "body-parser"; import { findTile, getAllTiles, @@ -9,15 +14,18 @@ import { } from "../Controller/tileController"; /* -* Api routes for the tiles -* uses the upload middleware to upload files -*/ + * Api routes for the tiles + * uses the upload middleware to upload files + */ const router = Router(); +const jsonParser = bodyParser.json(); router.get("/", getAllTiles); router.get("/:id", findTile); router.post("/", uploadFiles, createTile); +router.post("/ast/js", jsonParser, generateJsCode); +router.post("/ast/py", jsonParser, generatePythonCode); router.put("/:id", uploadFiles, updateTile); router.delete("/:id", deleteTile); diff --git a/backend/src/index.ts b/backend/src/index.ts index f0b7219..59ecd79 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,16 +4,19 @@ import { SocketCursorData, SocketDeleteData, TabFocusData, + TileConnection, } from "./../types/socket.types.d"; import { - createRoom, - cursorMove, joinRoom, tabFocus, tileDrag, tileDrop, + createRoom, deleteTile, + deleteLine, disconnect, + cursorMove, + tileConnect, errorHandling, } from "./Controller/socketController"; import cors from "cors"; @@ -77,9 +80,6 @@ app.use(cors()); app.use("/uploads", express.static("uploads")); app.use(express.urlencoded({ extended: true })); - - - io.on("connection", (socket) => { socket.on("room-create", (data: UserData) => createRoom(data, state, socket)); socket.on("join-room", (data: UserData) => joinRoom(data, state, socket, io)); @@ -94,8 +94,14 @@ io.on("connection", (socket) => { socket.on("tile-delete", (data: SocketDeleteData) => deleteTile(data, state, io), ); - socket.on("disconnect", () => disconnect(state, socket)); + socket.on("line-delete", (data: SocketDeleteData) => + deleteLine(data, state, io), + ); + socket.on("disconnect", () => disconnect(state, socket, io)); socket.on("error", (err: Error) => errorHandling(err)); + socket.on("tile-connection", (data: TileConnection) => + tileConnect(data, state, io), + ); }); db.on("error", (error) => console.error(error)); diff --git a/backend/types/ast.types.d.ts b/backend/types/ast.types.d.ts new file mode 100644 index 0000000..1ea02ff --- /dev/null +++ b/backend/types/ast.types.d.ts @@ -0,0 +1,70 @@ +export type ASTType = { + type: string; + errors: any[]; + program: Program; +}; + +export type Program = { + type: string; + body: Body[]; +}; + +export type Body = { + type: string; + test: Test; + consequent: Consequent; +}; + +export type Test = { + type: string; + left: Left; + right: Right; + operator: string; +}; + +export type Left = { + type: string; + name: string; +}; + +export type Right = { + type: string; + value: string; +}; + +export type Consequent = { + type: string; + body: ConsequentBody[]; +}; + +export type ConsequentBody = { + type: string; + expression: Expression; +}; + +export type Expression = { + type: string; + callee: Callee; + arguments: Argument[]; +}; + +export type Callee = { + type: string; + object: Object; + property: Property; +}; + +export type Object = { + type: string; + name: string; +}; + +export type Property = { + type: string; + name: string; +}; + +export type Argument = { + type: string; + value: string; +}; diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 2a84554..902c959 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -15,12 +15,21 @@ export type SocketDeleteData = { id: string; }; -export type NewNode = { - id: string; - category: string; - src: string; +export type NewTile = { x: number; y: number; + id: string; + _id: string; + src: string; + name: string; + width: number; + color: string; + height: number; + category: string; + points: number[]; + astNode?: { javaScript: any; python: any; MQTTtopic?: string }; + textPosition: { x: number; y: number }; + anchors: { type: string; x: number; y: number }[]; }; export type TabFocusData = { @@ -31,7 +40,7 @@ export type TabFocusData = { export type SocketDragTile = { remoteUser: string; - tile: NewNode; + tile: NewTile; roomId: string; remoteUserColor: string; }; @@ -45,11 +54,18 @@ export type SocketCursorData = { //state object for each room export type TileData = { - tile: NewNode; + tile: NewTile; +}; + +export type TileConnection = { + to: string; + from: string; + roomId: string; }; export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; + tileConnections?: TileConnection[]; }; diff --git a/backend/uploads/1674420527774.png b/backend/uploads/1674420527774.png new file mode 100644 index 0000000..92856a5 Binary files /dev/null and b/backend/uploads/1674420527774.png differ diff --git a/backend/uploads/1674422039501.png b/backend/uploads/1674422039501.png new file mode 100644 index 0000000..39b3b4a Binary files /dev/null and b/backend/uploads/1674422039501.png differ diff --git a/backend/uploads/1674423645286.png b/backend/uploads/1674423645286.png new file mode 100644 index 0000000..065abc4 Binary files /dev/null and b/backend/uploads/1674423645286.png differ diff --git a/backend/uploads/1674424309409.png b/backend/uploads/1674424309409.png new file mode 100644 index 0000000..6a7c7f0 Binary files /dev/null and b/backend/uploads/1674424309409.png differ diff --git a/backend/uploads/1674425020844.png b/backend/uploads/1674425020844.png new file mode 100644 index 0000000..a54e7b1 Binary files /dev/null and b/backend/uploads/1674425020844.png differ diff --git a/backend/uploads/1674425372221.png b/backend/uploads/1674425372221.png new file mode 100644 index 0000000..8885a13 Binary files /dev/null and b/backend/uploads/1674425372221.png differ diff --git a/backend/uploads/1674425372223.png b/backend/uploads/1674425372223.png new file mode 100644 index 0000000..7f4d5e3 Binary files /dev/null and b/backend/uploads/1674425372223.png differ diff --git a/backend/uploads/1674425372224.png b/backend/uploads/1674425372224.png new file mode 100644 index 0000000..0054c11 Binary files /dev/null and b/backend/uploads/1674425372224.png differ diff --git a/backend/uploads/1674425372225.png b/backend/uploads/1674425372225.png new file mode 100644 index 0000000..69b5b15 Binary files /dev/null and b/backend/uploads/1674425372225.png differ diff --git a/backend/uploads/1674425372226.png b/backend/uploads/1674425372226.png new file mode 100644 index 0000000..5bd2e75 Binary files /dev/null and b/backend/uploads/1674425372226.png differ diff --git a/backend/uploads/1674425372227.png b/backend/uploads/1674425372227.png new file mode 100644 index 0000000..832ab12 Binary files /dev/null and b/backend/uploads/1674425372227.png differ diff --git a/backend/uploads/1674425372228.png b/backend/uploads/1674425372228.png new file mode 100644 index 0000000..d6f86f2 Binary files /dev/null and b/backend/uploads/1674425372228.png differ diff --git a/backend/uploads/1674425372229.png b/backend/uploads/1674425372229.png new file mode 100644 index 0000000..25c8c30 Binary files /dev/null and b/backend/uploads/1674425372229.png differ diff --git a/backend/uploads/1674425372230.png b/backend/uploads/1674425372230.png new file mode 100644 index 0000000..0ae41e4 Binary files /dev/null and b/backend/uploads/1674425372230.png differ diff --git a/backend/uploads/1674425372231.png b/backend/uploads/1674425372231.png new file mode 100644 index 0000000..247cad3 Binary files /dev/null and b/backend/uploads/1674425372231.png differ diff --git a/backend/uploads/1674425372232.png b/backend/uploads/1674425372232.png new file mode 100644 index 0000000..3b12dfc Binary files /dev/null and b/backend/uploads/1674425372232.png differ diff --git a/backend/uploads/Readme.md b/backend/uploads/Readme.md new file mode 100644 index 0000000..62652df --- /dev/null +++ b/backend/uploads/Readme.md @@ -0,0 +1,3 @@ +# Upload Ordner + +Dieser Ordner enthält alle hochgeladenen Images. Für Testzwecke wurde der Ordner commited. Sollte aber in einer echten Nutzung nicht commited werden. \ No newline at end of file diff --git a/backend/utils/generatePyCode.ts b/backend/utils/generatePyCode.ts new file mode 100644 index 0000000..bf34925 --- /dev/null +++ b/backend/utils/generatePyCode.ts @@ -0,0 +1,74 @@ +import { ASTType, Test } from "./../types/ast.types.d"; + +const getPythonLogicalOperator = (operatorString: string) => { + //get python equivalent of logical Operator + switch (operatorString) { + case "||": + return "or"; + case "&&": + return "and"; + case "!==": + return "not"; + default: + return " "; + } +}; + +const getPythonIdentityOperator = (operatorString: string) => { + //get python equivalent of Identity Operator + switch (operatorString) { + case "===": + return "is"; + case "!==": + return "is not"; + default: + return " "; + } +}; + +const generatePyCode = (ast: ASTType) => { + const body = ast.program.body; + let pyCode = ""; + + body.forEach((body) => { + const left = body.test.left; + const right = body.test.right; + const operator = getPythonIdentityOperator(body.test.operator); + const consequent = body.consequent; + if (body.type === "IfStatement" && body.test.type === "BinaryExpression") { + pyCode += `if ${left.name} ${operator} ${right.value}:`; + } + + if (body.type === "IfStatement" && body.test.type === "LogicalExpression") { + const logicalLeft = body.test.left as any; + const logicalRight = body.test.right as any; + pyCode += `if ${logicalLeft.left.name} ${getPythonLogicalOperator( + logicalLeft.operator, + )} ${logicalLeft.right.value} ${getPythonLogicalOperator( + body.test.operator, + )} ${logicalRight.left.name} ${getPythonLogicalOperator( + logicalRight.operator, + )} ${logicalRight.right.value} :`; + } + + if (consequent.type === "BlockStatement") { + consequent.body.forEach((expression) => { + if (expression.type === "ExpressionStatement") { + const callee = expression.expression.callee; + const args = expression.expression.arguments; + const argList = args.map((arg) => `"${arg.value}"`).join(", "); + if ( + expression.type === "ExpressionStatement" && + expression.expression.type === "CallExpression" + ) { + pyCode += `\n\t${callee.object.name}.${callee.property.name}(${argList})`; + } + } + }); + } + }); + + return pyCode; +}; + +export default generatePyCode; diff --git a/backend/utils/tileConnections.ts b/backend/utils/tileConnections.ts new file mode 100644 index 0000000..197f733 --- /dev/null +++ b/backend/utils/tileConnections.ts @@ -0,0 +1,16 @@ +import { TileConnection } from "./../types/socket.types"; + +const splitIds = (id: string) => { + const [fromId, toId] = id.split("."); + return { fromId, toId }; +}; + +const findConnections = (value: string, connections: TileConnection[]) => { + const { fromId, toId } = splitIds(value); + console.log(fromId, toId); + return connections.filter( + (connection) => connection.from !== fromId && connection.to !== toId, + ); +}; + +export { splitIds, findConnections }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bf2069b..e8c83f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,12 +19,14 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "konva": "^8.3.11", + "prismjs": "^1.29.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-draggable": "^4.4.5", "react-konva": "^18.2.1", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-simple-code-editor": "^0.13.1", "react-toastify": "^9.1.1", "simple-zustand-devtools": "^1.1.0", "socket.io-client": "^4.5.1", @@ -34,6 +36,7 @@ "zustand": "^4.1.1" }, "devDependencies": { + "@types/prismjs": "^1.26.0", "@types/socket.io-client": "^3.0.0", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.28.0", @@ -3498,6 +3501,12 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==" }, + "node_modules/@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -12250,6 +12259,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12696,6 +12713,15 @@ } } }, + "node_modules/react-simple-code-editor": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", @@ -17960,6 +17986,12 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==" }, + "@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==", + "dev": true + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -24136,6 +24168,11 @@ } } }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -24459,6 +24496,12 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-simple-code-editor": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "requires": {} + }, "react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9cd8be6..47fbad5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,12 +14,14 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "konva": "^8.3.11", + "prismjs": "^1.29.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-draggable": "^4.4.5", "react-konva": "^18.2.1", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-simple-code-editor": "^0.13.1", "react-toastify": "^9.1.1", "simple-zustand-devtools": "^1.1.0", "socket.io-client": "^4.5.1", @@ -55,6 +57,7 @@ ] }, "devDependencies": { + "@types/prismjs": "^1.26.0", "@types/socket.io-client": "^3.0.0", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.28.0", diff --git a/frontend/src/astTypes.d.ts b/frontend/src/astTypes.d.ts new file mode 100644 index 0000000..8b4d6c4 --- /dev/null +++ b/frontend/src/astTypes.d.ts @@ -0,0 +1,148 @@ +/** + * This file contains the AST for the JS code generator + * The AST is based on the ESTree standard + * Basic building blocks are: + * Program + * StringLiterals + * Identifiers + * Expression Statements + * Statements + * + * These should be used to create an AST for the generator like Babel can + * use to create a JS file + */ + +/** + * Operators used for LogicalExpressions + */ +export interface ASTType { + type: 'File'; + errors: []; + program: { + type: 'Program'; + body: IfStatement[]; + }; +} + +export enum Operator { + equals = '===', + unequals = '!==', + lessThan = '<', + greaterThan = '>', + lessOrEquals = '<=', + greaterOrEquals = '>=', + and = '&&', + or = '||', + true = 'true', + false = 'false', +} +/** + * Program + * The Startnode of the AST + * */ + +export interface Program { + type: 'Program'; + body: IfStatement[]; + sourceType: 'module'; +} + +/** + * StringLiterals + * all types of strings, numbers, booleans + */ +export interface StringLiteral { + type: 'StringLiteral'; + value: string; +} + +/** + * Identifiers + * all types of variables, functions, classes + */ +export interface Identifier { + type: 'Identifier'; + name: string; +} +export interface NumericLiteral { + type: 'NumericLiteral'; + name: number; +} +/** + * Expression Statements + * basic expressions + */ + +// MemeberExpression can be used for objects and its properties +export interface MemberExpression { + type: 'MemberExpression'; + object: Identifier; + property: Identifier; +} +export interface BinaryExpression { + type: 'BinaryExpression'; + left: MemberExpression | Identifier | StringLiteral | null; + right: MemberExpression | Identifier | StringLiteral | NumericLiteral | null; + operator: Operator | null; +} + +// LogicalExpression can be used for if statements +export interface LogicalExpression { + type: 'LogicalExpression'; + left: BinaryExpression | MemberExpression | StringLiteral | null; + right: BinaryExpression | MemberExpression | StringLiteral | null; + operator: Operator; +} +/** + * CallExpression can be used for functions. + * They also can take in other functions or Expressions as arguments + **/ +export interface CallExpression { + type: 'CallExpression'; + callee: MemberExpression; + object: Identifier; + property: Identifier; + arguments: + | MemberExpression[] + | StringLiteral[] + | LogicalExpression[] + | IfStatement[] + | Identifier[]; +} + +/** + * Statements + */ + +/** + * IfStatement can be used for if statements + * Since most of the proccesses can be convered by if statements, + * we wont be needing other types of statements at first + * */ +export interface ExpressionStatement { + type: 'ExpressionStatement'; + expression: + | MemberExpression + | BinaryExpression + | LogicalExpression + | CallExpression + | IfStatement; +} + +export interface IfStatement { + type: 'IfStatement'; + test: BinaryExpression | LogicalExpression; + consequent: { + type: 'BlockStatement'; + body: ExpressionStatement[]; + }; +} + +/** + * Blockstatement are the body of a program + * or the body of a function + */ +export interface BlockStatement { + type: 'BlockStatement'; + body: ExpressionStatement[]; +} diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 0ff56ad..285df66 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -1,14 +1,17 @@ import Tile from '../Tiles/Tile'; import React, { useEffect } from 'react'; -import { Stage, Layer } from 'react-konva'; import { useGrid } from '../../hooks/useGrid'; import { useMouse } from '../../hooks/useMouse'; +import { Stage, Layer, Line } from 'react-konva'; import { Stage as StageType } from 'konva/lib/Stage'; import { Layer as LayerType } from 'konva/lib/Layer'; +import { SocketDragTile, RoomData } from '../../types'; import { useBoardState } from '../../state/BoardState'; -import useWindowDimensions from '../../hooks/useWindowDimensions'; +import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; -import { SocketDragTile, RoomData } from '../../types'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; +import { useContextMenuState } from '../../state/ContextMenuState'; +import { useConnectedTilesState } from '../../state/SyntaxTreeState'; // Main Stage Component that holds the Canvas. Scales based on the window size. @@ -18,17 +21,54 @@ const Board = () => { const { height, width } = useWindowDimensions(); const { gridComponents } = useGrid({ stageRef, gridLayer }); - const { handleDragOver, handleDrop, handleWheel, handleMouseMove, toggleCategory } = useMouse(); + const { + handleDragOver, + handleDrop, + handleWheel, + toggleCategory, + handleMouseMove, + handleBoardDrag, + } = useMouse(); + const { handleContextMenu } = useContextMenu(); + const room = useWebSocketState((state) => state.room); const addTile = useBoardState((state) => state.addTile); - const updateTile = useBoardState((state) => state.updateTile); - const deleteTile = useBoardState((state) => state.removeTile); - const setStageReference = useBoardState((state) => state.setStageReference); const socket = useWebSocketState((state) => state.socket); const setRoom = useWebSocketState((state) => state.setRoom); - const room = useWebSocketState((state) => state.room); + const deleteTile = useBoardState((state) => state.removeTile); + const updateTile = useBoardState((state) => state.updateTile); + const setStageReference = useBoardState((state) => state.setStageReference); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); + const connectionPreview = useConnectedTilesState((state) => state.connectionPreview); + + const setLineContextMenuOpen = useContextMenuState((state) => state.setLineContextMenuOpen); setStageReference(stageRef); + const connectionLines = room?.tileConnections?.map((connection) => { + const [toId, toAnchorType] = connection.to.split('_'); + const [fromId, fromAnchorType] = connection.from.split('_'); + const toTile = room?.tiles?.find((tile) => tile.tile.id === toId); + const fromTile = room?.tiles?.find((tile) => tile.tile.id === fromId); + const toAnchor = toTile?.tile.anchors.find((anchor) => anchor.type === toAnchorType); + const fromAnchor = fromTile?.tile.anchors.find((anchor) => anchor.type === fromAnchorType); + + if (fromTile && toTile && fromAnchor && toAnchor) { + return ( + handleContextMenu(e, setLineContextMenuOpen)} + id={`${fromId}_${fromAnchorType}.${toId}_${toAnchorType}`} + key={`${fromId}_${fromAnchorType}.${toId}_${toAnchorType}`} + points={[ + fromTile.tile.x + fromAnchor.x, + fromTile.tile.y + fromAnchor.y, + toTile.tile.x + toAnchor.x, + toTile.tile.y + toAnchor.y, + ]} + stroke='black' + strokeWidth={6} + /> + ); + } + }); useEffect(() => { if (socket) { socket?.on('tile-drop', (data: SocketDragTile) => { @@ -59,22 +99,35 @@ const Board = () => { handleWheel(e)} > {gridComponents} + {connectionPreview} + {connectionLines} {room?.tiles?.map((tileObject) => ( ))} @@ -84,4 +137,4 @@ const Board = () => { ); }; -export default Board; +export default Board; \ No newline at end of file diff --git a/frontend/src/components/ContextMenus/LineRightClickMenu.tsx b/frontend/src/components/ContextMenus/LineRightClickMenu.tsx new file mode 100644 index 0000000..e8d3488 --- /dev/null +++ b/frontend/src/components/ContextMenus/LineRightClickMenu.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import RemoveLine from './MenuOptions/RemoveLine'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { useContextMenu } from '../../hooks/useContextMenu'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useContextMenuState } from '../../state/ContextMenuState'; + +const LineRightClickMenu = () => { + const { contextMenuAnchorPoint, handleRemoveLine } = useContextMenu(); + const setLineContextMenuOpen = useContextMenuState((state) => state.setLineContextMenuOpen); + + return ( +
+
+ setLineContextMenuOpen(false)} + icon={faXmark} + /> +
+
    + +
+
+ ); +}; + +export default LineRightClickMenu; diff --git a/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx b/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx new file mode 100644 index 0000000..4d8bbca --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx @@ -0,0 +1,18 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; + +type Props = { + onclick: () => void; +}; + +const RemoveTile: React.FC = ({ onclick }) => { + return ( +
  • +

    Verbindung aufheben

    + +
  • + ); +}; + +export default RemoveTile; diff --git a/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx b/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx new file mode 100644 index 0000000..afb226b --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx @@ -0,0 +1,18 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; + +type Props = { + onclick: () => void; +}; + +const RemoveTile: React.FC = ({ onclick }) => { + return ( +
  • +

    Baustein entfernen

    + +
  • + ); +}; + +export default RemoveTile; diff --git a/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx new file mode 100644 index 0000000..8bc8c8c --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { faLightbulb } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +type Props = { + onclick: () => void; +}; + +const SetLamp: React.FC = ({ onclick }) => { + + + return ( +
  • +

    Lampe auswählen

    + +
  • + ); +}; + +export default SetLamp; diff --git a/frontend/src/components/ContextMenus/RightClickMenu.tsx b/frontend/src/components/ContextMenus/RightClickMenu.tsx deleted file mode 100644 index 4a25a7b..0000000 --- a/frontend/src/components/ContextMenus/RightClickMenu.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { useBoardState } from '../../state/BoardState'; -import { useContextMenu } from '../../hooks/useContextMenu'; -import { faTrashCan } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; - -const RightClickMenu = () => { - const { contextMenuAnchorPoint, handleClick } = useContextMenu(); - const removeTile = useBoardState((state) => state.removeTile); - - const handleRemoveTile = () => { - removeTile(contextMenuAnchorPoint.id); - handleClick(); - }; - - return ( -
    -
      -
    • -

      Remove Tile

      - -
    • -
    -
    - ); -}; - -export default RightClickMenu; diff --git a/frontend/src/components/ContextMenus/TileRightClickMenu.tsx b/frontend/src/components/ContextMenus/TileRightClickMenu.tsx new file mode 100644 index 0000000..ded8cf0 --- /dev/null +++ b/frontend/src/components/ContextMenus/TileRightClickMenu.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import SetLamp from './MenuOptions/SetLamp'; +import RemoveTile from './MenuOptions/RemoveTile'; +import { useBoardState } from '../../state/BoardState'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { useContextMenu } from '../../hooks/useContextMenu'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useContextMenuState } from '../../state/ContextMenuState'; + +const TileRightClickMenu = () => { + const removeTile = useBoardState((state) => state.removeTile); + const { contextMenuAnchorPoint, handleClick } = useContextMenu(); + const setPanelOpen = useContextMenuState((state) => state.setPanelOpen); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); + + const handleRemoveTile = () => { + removeTile(contextMenuAnchorPoint.id); + handleClick(); + }; + + const handleSelectLamp = () => { + setPanelOpen(true); + setContextMenuOpen(false); + }; + + return ( +
    +
    + setContextMenuOpen(false)} + icon={faXmark} + /> +
    +
      + + +
    +
    + ); +}; + +export default TileRightClickMenu; diff --git a/frontend/src/components/Forms/AddTileForm.tsx b/frontend/src/components/Forms/AddTileForm.tsx deleted file mode 100644 index 1e4be77..0000000 --- a/frontend/src/components/Forms/AddTileForm.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { InnerObject } from '../../types'; -import { useToggle } from '../../hooks/useToggle'; -import React, { useEffect, useState } from 'react'; -import { useBoardState } from '../../state/BoardState'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faAngleDown, faAngleUp, faX } from '@fortawesome/free-solid-svg-icons'; -import { getTileType } from '../../hooks/useCategory'; - -type AddTileFormProps = { - closeForm: () => void; -}; - -const AddTileForm: React.FC = ({ closeForm }) => { - const { toggleForm } = useToggle(); - const [isOpen, setIsOpen] = useState(false); - const allTiles = useBoardState((state) => state.allTiles); - const setAllTiles = useBoardState((state) => state.setAllTiles); - const [categories, setCategories] = useState>([]); - const [selectedCategory, setSelectedCategory] = useState(allTiles[0].category); - const [selectedName, setSelectedName] = useState(); - - const setCategory = (category: string) => { - setSelectedCategory(category); - setIsOpen(false); - }; - - // TODO: Add a database to save the new Tiles to. - const handleSubmit = () => { - if (selectedName && selectedCategory) { - const svgData = getTileType(selectedCategory); - const newNode: InnerObject = { - category: selectedCategory, - name: selectedName, - svgPath: svgData.svgPath, - fill: svgData.fill, - svgRotate: svgData.rotation, - url: '', - }; - setAllTiles([...allTiles, newNode]); - } - closeForm(); - setIsOpen(false); - }; - - const handleChange = (event: React.FormEvent) => { - setSelectedName(event.currentTarget.value); - }; - - useEffect(() => { - const categoryArray: Array = []; - const categorySet: Set = new Set(); - allTiles.forEach((item) => { - categorySet.add(item.category); - }); - categorySet.forEach((item) => categoryArray.push(item)); - setCategories(categoryArray); - }, []); - - return ( -
    -
    -
    - toggleForm()} - className='mb-2 relative text-green-800' - /> -
    -
    - - -
    -
    - - - {isOpen && ( - - )} -
    -
    - -
    -
    -
    - ); -}; - -export default AddTileForm; diff --git a/frontend/src/components/Forms/InfoComponent.tsx b/frontend/src/components/Forms/InfoComponent.tsx index ea960e1..4e9702c 100644 --- a/frontend/src/components/Forms/InfoComponent.tsx +++ b/frontend/src/components/Forms/InfoComponent.tsx @@ -4,10 +4,12 @@ import { useWebSocketState } from '../../state/WebSocketState'; import Default from '../Buttons/Default'; import RoomCodeInput from './Inputs/RoomCodeInput'; import UserDisplay from '../UserDisplay/UserDisplay'; +import { useCodeGeneration } from '../../hooks/useCodeGeneration'; const InfoComponent = () => { - const socket = useWebSocketState((state) => state.socket); const navigate = useNavigate(); + const { generateCode } = useCodeGeneration(); + const socket = useWebSocketState((state) => state.socket); const roomId = useWebSocketState((state) => state.room?.roomId); const users = useWebSocketState((state) => state.room?.users); @@ -18,13 +20,14 @@ const InfoComponent = () => { }; // Component that displays the connected users and Disconnect Button return ( -
    +
    {users?.map((user) => ( ))}
    {roomId && } +
    ); diff --git a/frontend/src/components/Forms/SelectLampForm.tsx b/frontend/src/components/Forms/SelectLampForm.tsx new file mode 100644 index 0000000..e41a08c --- /dev/null +++ b/frontend/src/components/Forms/SelectLampForm.tsx @@ -0,0 +1,72 @@ +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; +import { useContextMenuState } from '../../state/ContextMenuState'; +import Tile from '../Tiles/Tile'; + +const SelectLampForm = () => { + const [selectedLamp, setSelectedLamp] = React.useState(''); + const lamps = ['Floor Lamp', 'Table Lamp', 'Desk Lamp', 'Ceiling Lamp', 'Bedside Lamp']; + const panelOpen = useContextMenuState((state) => state.panelOpen); + const setPanelOpen = useContextMenuState((state) => state.setPanelOpen); + + const closePanel = () => { + if (panelOpen) setPanelOpen(false); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + }; + + return ( + <> + {panelOpen === true && ( +
    + +
    +
    +

    Wähle eine Lampe aus

    +
    + +
    + + + +
    +
    +
    + +
    +
    +
    +
    + )} + + ); +}; + +export default SelectLampForm; diff --git a/frontend/src/components/Sidebar/Category.tsx b/frontend/src/components/Sidebar/Category.tsx index 038c617..6a37d59 100644 --- a/frontend/src/components/Sidebar/Category.tsx +++ b/frontend/src/components/Sidebar/Category.tsx @@ -1,5 +1,5 @@ import MenuTile from '../Tiles/MenuTile'; -import { InnerObject } from '../../types'; +import { Tile } from '../../types'; import { useMouse } from '../../hooks/useMouse'; import React, { useEffect, useState } from 'react'; import { useBoardState } from '../../state/BoardState'; @@ -8,14 +8,13 @@ type CategoryProps = { category: string; }; - const Category: React.FC = ({ category }) => { - const [stateItems, setStateItems] = useState([]); + const [stateItems, setStateItems] = useState([]); const allTiles = useBoardState((state) => state.allTiles); const { handleDragStart } = useMouse(); useEffect(() => { - const InnerObjectArray: InnerObject[] = []; + const InnerObjectArray: Tile[] = []; allTiles.forEach((object) => { object.category === category && InnerObjectArray.push(object); }); diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 30a09e5..2a8f6ac 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -1,5 +1,6 @@ import Tab from './Tab'; import Category from './Category'; +import kacheln from '../../json/blocks.json'; import { useToast } from '../../hooks/useToast'; import React, { useState, useEffect } from 'react'; import { useBoardState } from '../../state/BoardState'; @@ -21,17 +22,19 @@ const Sidebar: React.FC = () => { }; useEffect(() => { - (async () => { - try { - const response = await fetch(`${backendUrl}`, { - method: 'GET', - }); - const data = await response.json(); - setAllTiles(data); - } catch (error) { - notify('error', 'There was an error fetching the tiles, please try again later', false); - } - })(); + // (async () => { + // try { + // const response = await fetch(`${backendUrl}`, { + // method: 'GET', + // }); + // const data = await response.json(); + // setAllTiles(data); + // } catch (error) { + // notify('error', 'There was an error fetching the tiles, please try again later', false); + // } + // })(); + + setAllTiles(kacheln as any); }, []); useEffect(() => { diff --git a/frontend/src/components/Tiles/MenuTile.tsx b/frontend/src/components/Tiles/MenuTile.tsx index 8701828..aa5befa 100644 --- a/frontend/src/components/Tiles/MenuTile.tsx +++ b/frontend/src/components/Tiles/MenuTile.tsx @@ -3,19 +3,16 @@ import React from 'react'; type MenuProps = { name: string; category: string; - svgPath: string; - fill: string; - svgRotate: number; - url: string; + src: string; dragFunction: (event: React.DragEvent) => void; }; -const MenuTile: React.FC = ({ url, name, category, dragFunction }) => { +const MenuTile: React.FC = ({ src, name, category, dragFunction }) => { return ( = ({ src, x, y, category, id }) => { +const Tile: React.FC = ({ + x, + y, + id, + _id, + src, + name, + color, + width, + height, + points, + anchors, + astNode, + category, + textPosition, +}) => { const tileRef = React.useRef(null); + const { handleContextMenu } = useContextMenu(); + const { fromShapeId } = useConnectedTilesState((state) => state); + const { updateTilePosition, setActiveDragElement, handleClick } = useMouse(); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); - const { handleContextMenu } = useContextMenu(); - const [image] = useImage(src); - const remoteDragColor = useBoardState((state) => state.remoteDragColor); - - // add border to image if active return ( <> - {userColor && ( - setActiveDragElement(tileRef, event)} - onDragEnd={updateTilePosition} - onMouseOver={(e) => handleMouseEnL(e, 4)} - onMouseLeave={(e) => handleMouseEnL(e, 0)} - > - - + {userColor && id && ( + <> + {anchors.map((point, index) => ( + + ))} + + handleContextMenu(e, setContextMenuOpen)} + data-points={JSON.stringify(points)} + data-anchors={JSON.stringify(anchors)} + data-textPosition={JSON.stringify(textPosition)} + onDragMove={(event) => setActiveDragElement(tileRef, event)} + > + + + + )} ); diff --git a/frontend/src/components/Tiles/TileBorder.tsx b/frontend/src/components/Tiles/TileBorder.tsx new file mode 100644 index 0000000..6065f46 --- /dev/null +++ b/frontend/src/components/Tiles/TileBorder.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Line } from 'react-konva'; + +type Props = { + x: number; + y: number; + points?: number[]; + id: string; +}; + +const TileBorder: React.FC = ({ x, y, id, points }) => { + const defaultPoints = [0, 0, 100, 0, 100, 100, 0, 100, 0, 0]; + + return ( + + ); +}; + +export default TileBorder; diff --git a/frontend/src/components/Tiles/TileBorderAnchor.tsx b/frontend/src/components/Tiles/TileBorderAnchor.tsx new file mode 100644 index 0000000..231a84c --- /dev/null +++ b/frontend/src/components/Tiles/TileBorderAnchor.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Line } from 'react-konva'; +import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; +import { KonvaEventObject } from 'konva/lib/Node'; +import Category from '../Sidebar/Category'; + +type Props = { + x: number; + y: number; + id: string; + fill: string; + type: string; + category?: string; + onClick: (event: KonvaEventObject) => void; +}; + +const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type, category }) => { + const dragBounds = (ref: React.RefObject) => { + if (ref.current !== null) { + return ref.current.getAbsolutePosition(); + } + return { + x: 0, + y: 0, + }; + }; + + const directionalArrows = [ + [-10, 0, 25, 0, 50, 15, 25, 30, 0, 30, -10, 0], + [20, 0, -50, 0, -25, 15, -50, 30, 35, 30, 30, 0], + ]; + + const anchor = React.useRef(null); + return ( + <> + {type === 'L' || type === 'BL' || type === 'TL' ? ( + dragBounds(anchor)} + stroke={'black'} + /> + ) : ( + dragBounds(anchor)} + stroke={'black'} + /> + )} + + ); +}; + +export default TileBorderAnchors; + diff --git a/frontend/src/hooks/useAnchor.tsx b/frontend/src/hooks/useAnchor.tsx new file mode 100644 index 0000000..c1846c2 --- /dev/null +++ b/frontend/src/hooks/useAnchor.tsx @@ -0,0 +1,118 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React from 'react'; +import { Line } from 'react-konva'; + +interface Coordinates { + x: number; + y: number; +} + +const useAnchor = () => { + const getAnchorPoints = ( + x: number, + y: number, + category: string, + tileName: string, + groupSize: { width: number; height: number }, + ) => { + switch (category) { + case 'Start': + return [{ type: 'L', x: x + groupSize.width / 2 + 50, y: y + 150 }]; + break; + case 'Ende': + return [{ type: 'R', x: x - groupSize.width / 6 + 80, y: y + 150 }]; + break; + case 'Objekte': + return [ + { type: 'L', x: x - groupSize.width / 5 + 10, y: y + 50 }, + { type: 'R', x: x + groupSize.width - 130, y: y + 50 }, + ]; + break; + case 'Zustand': + return [ + { type: 'L', x: x - groupSize.width / 6 + 80, y: y + 150 }, + { type: 'R', x: x + groupSize.width + 50, y: y + 150 }, + ]; + case 'Konditionen': + if (tileName === 'Dann') { + return [ + { type: 'L', x: x - groupSize.width / 2 + 100, y: y + 50 }, + { type: 'R', x: x + groupSize.width / 2 + 100, y: y + 50 }, + ]; + } else { + if (tileName === 'Und') { + return [ + { type: 'TL', x: x + groupSize.width - 440, y: y + 50 }, + { type: 'TR', x: x + groupSize.width / 2 + 50, y: y + 50 }, + { type: 'BL', x: x + groupSize.width / 2 + 40, y: y + 350 }, + { type: 'BR', x: x + groupSize.width - 440, y: y + 350 }, + ]; + } + } + break; + default: + console.warn(`Please provide a valid category. Given: ${category}`); + break; + } + }; + + const handleAnchorDragStart = ( + e: KonvaEventObject, + callback: (value: JSX.Element | null) => void, + createConnectionPoints: ( + fromPosition: Coordinates, + toPosition: Coordinates, + ) => [fx: number, fy: number, tx: number, ty: number], + ) => { + const position = e.target.position(); + callback( + , + ); + }; + + const handleAnchorDragMove = ( + e: KonvaEventObject, + callback: (value: JSX.Element) => void, + createConnectionPoints: ( + fromPosition: Coordinates, + toPosition: Coordinates, + ) => [fx: number, fy: number, tx: number, ty: number], + ) => { + const position = e.target.position(); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const mousePos = { + x: pointerPosition.x - position.x, + y: pointerPosition.y - position.y, + }; + callback( + , + ); + } + }; + + const handleAnchorDragEnd = ( + e: KonvaEventObject, + id: string, + callback: (value: JSX.Element | null) => void, + ) => { + callback(null); + }; + + return { getAnchorPoints, handleAnchorDragStart, handleAnchorDragMove, handleAnchorDragEnd }; +}; + +export default useAnchor; diff --git a/frontend/src/hooks/useCategory.tsx b/frontend/src/hooks/useCategory.tsx index 76522a0..53e582f 100644 --- a/frontend/src/hooks/useCategory.tsx +++ b/frontend/src/hooks/useCategory.tsx @@ -12,7 +12,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { switch (categoryName) { case 'Start': return { - points: [0, -100, 100, -50, 0, 0, 0, 100, -90, 50], + points: [0, -200, 200, -200, 100, -50, 200, 100, 0, 100], textPosition: { x: -60, y: 30 }, fill: '#f9b43d', rotation: -60, @@ -20,7 +20,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'End': return { - points: [-200, -50, 0, -50, 0, 50, -200, 50, -100, 0], + points: [-200, -250, 0, -250, 0, 50, -200, 50, -100, -100], textPosition: { x: -125, y: -5 }, fill: '#f9b43d', rotation: 0, @@ -29,7 +29,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { case 'Objects': return { // points for a top pointed hexagon - points: [0, -100, 100, -50, 100, 50, 0, 100, -100, 50, -100, -50], + points: [0, -100, 200, -100, 300, 50, 200, 200, 0, 200, -100, 50], textPosition: { x: -60, y: 0 }, fill: '#eb555b', rotation: 0, @@ -37,7 +37,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'Actions': return { - points: [0, -50, -100, 0, 0, 50, 100, 0], + points: [-200, -250, 0, -250, 100, -100, 0, 50, -200, 50, -100, -100], textPosition: { x: -60, y: -5 }, fill: '#f4aece', rotation: 0, @@ -61,7 +61,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'Union': return { - points: [-50, -100, -50, 100, 50, 50, 50, -50], + points: [-100, -50, 0, -200, 100, -200, 200, -50, 200, 50, 100, 200, 0, 200, -100, 50], textPosition: { x: -55, y: 0 }, fill: '#bababa', rotation: 0, diff --git a/frontend/src/hooks/useCodeGeneration.tsx b/frontend/src/hooks/useCodeGeneration.tsx new file mode 100644 index 0000000..c7f3b74 --- /dev/null +++ b/frontend/src/hooks/useCodeGeneration.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; +import { useBoardState } from '../state/BoardState'; +import { generateAst } from '../utils/generateAst'; + +const allowedConnections = { + Start: ['Objekte'], + Objekte: ['Zustand'], + Zustand: ['Konditionen', 'Ende'], + Konditionen: ['Objekte'], + End: [], +}; + +export const useCodeGeneration = () => { + const { connections, ast, setAst, generatedCode, setGeneratedCode } = useConnectedTilesState( + (state) => state, + ); + const { tilesOnBoard } = useBoardState((state) => state); + const generateCode = () => { + connections.forEach((connection) => { + const { from, to } = connection; + const fromSplit = from.split('_'); + const toSplit = to.split('_'); + const fromTileObject = tilesOnBoard.find((tile) => tile.id === fromSplit[0]); + const toTileObject = tilesOnBoard.find((tile) => tile.id === toSplit[0]); + const fromTile = { + id: fromSplit[0], + anchorPosition: fromSplit[1], + tileName: fromTileObject?.name, + tileCategory: fromTileObject?.category, + astNode: fromTileObject?.astNode, + }; + const toTile = { + id: toSplit[0], + anchorPosition: toSplit[1], + tileName: toTileObject?.name, + tileCategory: toTileObject?.category, + astNode: toTileObject?.astNode, + }; + if (!fromTile.tileName || !toTile.tileName) return; + // special Condition for Connection 2 Conditions + if ( + fromTile.tileCategory === 'Konditionen' && + fromTile.tileName === 'Und' && + toTile.tileCategory === 'Konditionen' && + toTile.tileName === 'Dann' + ) { + generateAst(fromTile, toTile, ast, setAst, connections, generatedCode, setGeneratedCode); + } else { + if ( + !allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections].includes( + toTile.tileCategory as never, + ) + ) { + alert( + `ungültige Verbindung: ${fromTile.tileCategory} -> ${toTile.tileCategory}. ${ + fromTile.tileCategory + } dürfen nur mit ${ + allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections] + } verbunden werden.`, + ); + } else { + generateAst(fromTile, toTile, ast, setAst, connections, generatedCode, setGeneratedCode); + } + } + }); + }; + + return { + generateCode, + }; +}; diff --git a/frontend/src/hooks/useContextMenu.tsx b/frontend/src/hooks/useContextMenu.tsx index 0671280..a41790c 100644 --- a/frontend/src/hooks/useContextMenu.tsx +++ b/frontend/src/hooks/useContextMenu.tsx @@ -2,25 +2,32 @@ import { useCallback } from 'react'; import { KonvaEventObject } from 'konva/lib/Node'; import { useWebSocketState } from '../state/WebSocketState'; import { useContextMenuState } from '../state/ContextMenuState'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; export const useContextMenu = () => { - const room = useWebSocketState((state) => state.room); - const socket = useWebSocketState((state) => state.socket); + const { room, socket } = useWebSocketState((state) => state); const contextMenu = useContextMenuState((state) => state.contextMenuOpen); - const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); - const contextMenuAnchorPoint = useContextMenuState((state) => state.contextMenuAnchorPoint); - const setContextMenuAnchorPoint = useContextMenuState((state) => state.setContextMenuAnchorPoint); + const { + contextMenuAnchorPoint, + setContextMenuOpen, + setLineContextMenuOpen, + setContextMenuAnchorPoint, + } = useContextMenuState((state) => state); + const removeConnection = useConnectedTilesState((state) => state.removeConnection); - const handleContextMenu = (event: KonvaEventObject) => { + const handleContextMenu = ( + event: KonvaEventObject, + stateFunc: (value: boolean) => void, + ) => { event.evt.preventDefault(); - setContextMenuOpen(true); - if (event.target.parent?.id()) { - setContextMenuAnchorPoint({ - x: event.evt.pageX, - y: event.evt.pageY, - id: event.target.parent.id(), - }); - } + stateFunc(true); + + setContextMenuAnchorPoint({ + x: event.evt.pageX, + y: event.evt.pageY, + // id of parent is for TIles, id of target is for Lines + id: event.target.parent?.id() ? event.target.parent.id() : event.target.attrs.id, + }); }; const handleClick = useCallback(() => { @@ -28,9 +35,19 @@ export const useContextMenu = () => { id: contextMenuAnchorPoint.id, roomId: room?.roomId, }; + setContextMenuOpen(false); socket && socket.emit('tile-delete', deleteData); - contextMenu && setContextMenuOpen(false); }, []); - return { contextMenu, handleContextMenu, handleClick, contextMenuAnchorPoint }; + const handleRemoveLine = () => { + removeConnection(contextMenuAnchorPoint.id); + const deleteData = { + id: contextMenuAnchorPoint.id, + roomId: room?.roomId, + }; + setLineContextMenuOpen(false); + socket && socket.emit('line-delete', deleteData); + }; + + return { contextMenu, handleContextMenu, handleClick, handleRemoveLine, contextMenuAnchorPoint }; }; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 49fcbc3..985bee7 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -1,28 +1,32 @@ import React from 'react'; -import { NewNode, SocketDragTile } from '../types'; +import { SocketDragTile, Tile } from '../types'; import { v4 as uuidv4 } from 'uuid'; import { Group } from 'konva/lib/Group'; import { KonvaEventObject } from 'konva/lib/Node'; import { useBoardState } from '../state/BoardState'; import { useWebSocketState } from '../state/WebSocketState'; +import { useContextMenuState } from '../state/ContextMenuState'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; export const useMouse = () => { const setTiles = useBoardState((state) => state.addTile); + const socket = useWebSocketState((state) => state.socket); + const allTiles = useBoardState((state) => state.allTiles); const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); + const roomId = useWebSocketState((state) => state.room?.roomId); const setActiveDragTile = useBoardState((state) => state.setActiveDragTile); - const socket = useWebSocketState((state) => state.socket); - const categoriesOpen = useBoardState((state) => state.categoriesOpen); const setCategoriesOpen = useBoardState((state) => state.setCategoriesOpen); - const roomId = useWebSocketState((state) => state.room?.roomId); + const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); const userColor = useWebSocketState( (state) => state.room?.users.find((user) => user.userId === socket?.id)?.color, ); - + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); + const { fromShapeId, setFromShapeId, connections, setConnections } = useConnectedTilesState( + (state) => state, + ); const toggleCategory = () => { - if (categoriesOpen) { - setCategoriesOpen(false); - } + setCategoriesOpen(false); }; const handleMouseMove = () => { @@ -34,8 +38,8 @@ export const useMouse = () => { const cursorPos = { x: x, y: y, - remoteUser: socket.id, roomId: roomId, + remoteUser: socket.id, }; if (socket !== null) { socket?.emit('cursor', cursorPos); @@ -44,78 +48,59 @@ export const useMouse = () => { } }; - const setActiveDragElement = ( - activeTileReference: React.RefObject, - event: KonvaEventObject, + const handleMouseEnL = ( + event: KonvaEventObject, + isClicked: boolean, + strokeWidth: number, ) => { - setActiveDragTile(activeTileReference); - - if (stageRef.current) { - const { 'data-src': url } = event.target.attrs; - const stage = stageRef.current; - const pos = stage.getRelativePointerPosition(); - const updatedTile: NewNode = { - id: event.target.attrs.id, - category: event.target.attrs.name, - x: event.target.x(), - y: event.target.y(), - src: url, - }; - if (socket !== null && roomId && userColor) { - const socketDragTile: SocketDragTile = { - remoteUser: socket.id, - tile: updatedTile, - roomId: roomId, - remoteUserColor: userColor, - }; - const cursorPos = { - x: pos?.x, - y: pos?.y, - remoteUser: socket.id, - roomId: roomId, - }; - socket?.emit('tile-drag', socketDragTile); - socket?.emit('cursor', cursorPos); - } - } - }; - - const handleMouseEnL = (event: KonvaEventObject, strokeWidth: number) => { // for mouse Enter and Leave // function to set the stroke width when user hovers over a Tile - event.target.setAttr('strokeWidth', strokeWidth); + if (isClicked === true) { + event.target.setAttr('strokeWidth', 0); + } else { + event.target.setAttr('strokeWidth', strokeWidth); + } }; const handleDragOver = (event: React.DragEvent) => event.preventDefault(); const handleDragStart = (event: React.DragEvent) => { // use HTML DnD API to send Tile Information - const dragPayload = JSON.stringify({ - nodeClass: event.currentTarget.getAttribute('data-class'), - offsetX: event.nativeEvent.offsetX, - offsetY: event.nativeEvent.offsetY, - clientWidth: event.currentTarget.clientWidth, - clientHeight: event.currentTarget.clientHeight, - url: event.currentTarget.getAttribute('src'), - }); - event.dataTransfer.setData('dragStart/Tile', dragPayload); - }; + const tile = allTiles.find( + (tile) => tile.name === event.currentTarget.getAttribute('data-name'), + ); - const updateTilePosition = (event: KonvaEventObject) => { - /** - * updates the Tile position in the state - * also clears the active Drag Element - */ - const { 'data-src': url } = event.target.attrs; - if (stageRef.current) { - const updatedTile: NewNode = { - id: event.target.attrs.id, - category: event.target.attrs.name, - x: event.target.x(), - y: event.target.y(), - src: url, - }; - updateTile(updatedTile); + if (tile) { + const { + _id, + src, + name, + color, + width, + height, + points, + anchors, + category, + astNode, + textPosition, + } = tile; + const dragPayload = JSON.stringify({ + id: uuidv4(), + _id: _id, + src: src, + name: name, + color: color, + width: width, + height: height, + points: points, + anchors: anchors, + astNode: astNode, + category: category, + textPosition: textPosition, + offsetX: event.nativeEvent.offsetX, + offsetY: event.nativeEvent.offsetY, + }); + event.dataTransfer.setData('dragStart/Tile', dragPayload); } }; @@ -126,22 +111,45 @@ export const useMouse = () => { if (draggedData && stageRef.current != null) { stageRef.current.setPointersPositions(event); const { x, y } = stageRef.current.getRelativePointerPosition(); - const { url, nodeClass, offsetX, offsetY, clientHeight, clientWidth } = - JSON.parse(draggedData); + const { + id, + _id, + src, + name, + color, + width, + points, + height, + anchors, + offsetX, + offsetY, + category, + astNode, + textPosition, + } = JSON.parse(draggedData); if (x && y) { - const newTile: NewNode = { - id: uuidv4(), - src: url, - category: nodeClass, - x: x - (offsetX - clientWidth / 2), - y: y - (offsetY - clientHeight / 2), + const newTile: Tile = { + id: id, + _id: _id, + src: src, + name: name, + width: width, + color: color, + height: height, + points: points, + anchors: anchors, + astNode: astNode, + category: category, + textPosition: textPosition, + x: x - (offsetX - width / 2), + y: y - (offsetY - height / 2), }; setTiles(newTile); if (socket !== null && roomId && userColor) { const socketDragTile: SocketDragTile = { - remoteUser: socket.id, tile: newTile, roomId: roomId, + remoteUser: socket.id, remoteUserColor: userColor, }; socket?.emit('tile-drop', socketDragTile); @@ -150,6 +158,45 @@ export const useMouse = () => { } }; + const updateTilePosition = (event: KonvaEventObject) => { + /** + * updates the Tile position in the state + * also clears the active Drag Element + */ + + const { + 'data-id': uid, + 'data-src': src, + 'data-fill': color, + 'data-width': width, + 'data-points': points, + 'data-height': height, + 'data-anchors': anchors, + 'data-astNode': astNode, + 'data-category': tileName, + 'data-textPosition': textPosition, + } = event.target.attrs; + if (stageRef.current) { + const updatedTile: Tile = { + _id: uid, + src: src, + width: width, + color: color, + height: height, + category: tileName, + x: event.target.x(), + y: event.target.y(), + id: event.target.attrs.id, + points: JSON.parse(points), + textPosition: textPosition, + astNode: JSON.parse(astNode), + anchors: JSON.parse(anchors), + name: event.target.attrs.name, + }; + updateTile(updatedTile); + } + }; + const handleWheel = (event: KonvaEventObject) => { // scale a stage up or down event.evt.preventDefault(); @@ -178,14 +225,104 @@ export const useMouse = () => { } }; + const setActiveDragElement = ( + activeTileReference: React.RefObject, + event: KonvaEventObject, + ) => { + setActiveDragTile(activeTileReference); + if (stageRef.current) { + const { + 'data-id': _id, + 'data-src': src, + 'data-fill': color, + 'data-width': width, + 'data-height': height, + 'data-points': points, + 'data-anchors': anchors, + 'data-astNode': astNode, + 'data-textPosition': textPosition, + } = event.target.attrs; + const stage = stageRef.current; + const pos = stage.getRelativePointerPosition(); + const updatedTile: Tile = { + _id: _id, + src: src, + color: color, + width: width, + height: height, + anchors: anchors, + x: event.target.x(), + y: event.target.y(), + id: event.target.attrs.id, + points: Array.from(points), + astNode: JSON.parse(astNode), + name: event.target.attrs.name, + category: event.target.attrs.name, + textPosition: JSON.parse(textPosition), + }; + if (socket !== null && roomId && userColor) { + const socketDragTile: SocketDragTile = { + roomId: roomId, + tile: updatedTile, + remoteUser: socket.id, + remoteUserColor: userColor, + }; + const cursorPos = { + x: pos?.x, + y: pos?.y, + roomId: roomId, + remoteUser: socket.id, + }; + socket?.emit('cursor', cursorPos); + socket?.emit('tile-drag', socketDragTile); + } + } + }; + + const handleBoardDrag = () => { + if (contextMenuOpen === true) { + setContextMenuOpen(false); + } + }; + + const handleClick = (event: KonvaEventObject) => { + const { id, 'data-type': type } = event.target.attrs; + + if (fromShapeId === `${id}_${type}`) { + setFromShapeId(null); + } + if (fromShapeId === null) { + setFromShapeId(`${id}_${type}`); + } else { + if (fromShapeId !== `${id}_${type}`) { + const newConnection = { + from: `${fromShapeId}`, + to: `${id}_${type}`, + }; + setConnections([...connections, newConnection]); + setFromShapeId(null); + + if (socket && roomId) { + const tileConnection = { + from: `${fromShapeId}`, + to: `${id}_${type}`, + roomId: roomId, + }; + socket.emit('tile-connection', tileConnection); + } + } + } + }; return { - handleMouseMove, - handleDragOver, - handleDragStart, handleDrop, + handleClick, handleWheel, toggleCategory, handleMouseEnL, + handleDragOver, + handleDragStart, + handleBoardDrag, + handleMouseMove, updateTilePosition, setActiveDragElement, }; diff --git a/frontend/src/json/blocks.json b/frontend/src/json/blocks.json new file mode 100644 index 0000000..8728a0d --- /dev/null +++ b/frontend/src/json/blocks.json @@ -0,0 +1,391 @@ +[ + { + "_id": "63cda4a235c0dc3e0c5f0455", + "category": "Start", + "name": "Wenn", + "src": "http://localhost:9001/uploads/1674420527774.png", + "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], + "color": "#f9b43d", + "width": 200, + "height": 300, + "astNode": { + "javaScript": { + "type": "IfStatement", + "test": { + "type": "BinaryExpression", + "left": { + "type": "MemberExpression", + "object": null, + "property": { + "type": "Identifier", + "name": "state" + } + }, + "right": null, + "operator": "===" + }, + "consequent": { + "type": "BlockStatement", + "body": null + } + } + }, + "anchors": [ + { + "type": "LS", + "x": 100, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 250, + "_id": "63cda4a235c0dc3e0c5f0456" + }, + "__v": 0 + }, + { + "_id": "63cdabf635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Kontakt Sensor", + "src": "http://localhost:9001/uploads/1674425372226.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "sensor" + }, + "MQTTtopic": "sensor/sensor1/set" + }, + "anchors": [ + { + "type": "L", + "x": -85, + "y": 140 + }, + { + "type": "R", + "x": 280, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 250, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdabf635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Lautsprecher", + "src": "http://localhost:9001/uploads/1674425372227.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "lautsprecher" + }, + "MQTTtopic": "lautsprecher/lautsprecher1/set" + }, + "anchors": [ + { + "type": "L", + "x": -90, + "y": 140 + }, + { + "type": "R", + "x": 280, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 250, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cda72e35c0dc3e0c5f0460", + "category": "Ende", + "name": " ", + "src": "http://localhost:9001/uploads/1674422039501.png", + "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], + "color": "#f9b43d", + "width": 200, + "height": 300, + "astNode": { + "javaScript": { + "type": "ReturnStatement", + "argument": null + } + }, + "anchors": [ + { + "type": "L", + "x": 100, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda72e35c0dc3e0c5f0461" + }, + "__v": 0 + }, + { + "_id": "63cd4bg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Timer", + "src": "http://localhost:9001/uploads/1674425372230.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "timer" + }, + "MQTTtopic": "timer/set" + }, + "anchors": [ + { + "type": "L", + "x": -90, + "y": 140 + }, + { + "type": "R", + "x": 280, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 250, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdabg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Knopf", + "src": "http://localhost:9001/uploads/1674425372225.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "knopf" + }, + "MQTTtopic": "button/button1/set" + }, + "anchors": [ + { + "type": "L", + "x": -90, + "y": 140 + }, + { + "type": "R", + "x": 280, + "y": 140 + } + ], + "textPosition": { + "x": 50, + "y": 250, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "auf", + "src": "http://localhost:9001/uploads/1674425372223.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "astNode": { + "javaScript": { + "type": "NumericLiteral", + "value": 1 + } + }, + "anchors": [ + { + "type": "L", + "x": 100, + "y": 140 + }, + { + "type": "R", + "x": 275, + "y": 140 + } + ], + "textPosition": { + "x": 125, + "y": 250, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "63cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "gib Bescheid", + "src": "http://localhost:9001/uploads/1674425372232.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "astNode": { + "javaScript": { + "type": "NumericLiteral", + "value": 1 + } + }, + "anchors": [ + { + "type": "L", + "x": 100, + "y": 140 + }, + { + "type": "R", + "x": 275, + "y": 140 + } + ], + "textPosition": { + "x": 90, + "y": 250, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "63cdaff535c0dc3e0c5f0478", + "category": "Konditionen", + "name": "Dann", + "src": "http://localhost:9001/uploads/1674424309409.png", + "points": [0, 0, 200, 0, 300, 150, -100, 150], + "color": "#F9B43D", + "width": 400, + "height": 200, + "astNode": { + "javaScript": { + "type": "ExpressionStatement", + "expression": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "object": { + "type": "Identifier", + "name": "client" + }, + "property": { + "type": "Identifier", + "name": "publish" + } + }, + "arguments": [ + { + "type": "NumericLiteral", + "value": null + } + ] + } + } + }, + "anchors": [ + { + "type": "L", + "x": -15, + "y": 0 + }, + { + "type": "R", + "x": 200, + "y": 0 + } + ], + "textPosition": { + "x": 75, + "y": 100, + "_id": "63cdaff535c0dc3e0c5f0479" + }, + "__v": 1 + }, + { + "_id": "63cdb2bc35c0dc3e0c5f047d", + "category": "Konditionen", + "name": "Und", + "src": "http://localhost:9001/uploads/1674425020844.png", + "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], + "color": "#E2E7DF", + "width": 350, + "height": 450, + "astNode": { + "javaScript": { + "type": "LogicalExpression", + "left": null, + "right": { + "right": { + "type": "BinaryExpression", + "left": null, + "right": null, + "operator": "===" + } + }, + "operator": "&&" + } + }, + "anchors": [ + { + "type": "TL", + "x": -50, + "y": 50 + }, + { + "type": "TR", + "x": 190, + "y": 50 + }, + { + "type": "BL", + "x": -50, + "y": 300 + }, + { + "type": "BR", + "x": 200, + "y": 300 + } + ], + "textPosition": { + "x": 50, + "y": 350, + "_id": "63cdb2bc35c0dc3e0c5f047e" + }, + "__v": 0 + } +] + diff --git a/frontend/src/json/blocks.old.json b/frontend/src/json/blocks.old.json new file mode 100644 index 0000000..1767814 --- /dev/null +++ b/frontend/src/json/blocks.old.json @@ -0,0 +1,402 @@ +[ + { + "_id": "63cda4a235c0dc3e0c5f0455", + "category": "Start", + "name": "Wenn", + "src": "http://localhost:9001/uploads/1674421410153.png", + "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], + "color": "#f9b43d", + "width": 200, + "height": 300, + "astNode": { + "javaScript": { + "type": "IfStatement", + "test": { + "type": "BinaryExpression", + "left": { + "type": "MemberExpression", + "object": null, + "property": { + "type": "Identifier", + "name": "state" + } + }, + "right": null, + "operator": "===" + }, + "consequent": { + "type": "BlockStatement", + "body": null + } + }, + "python": { + "type": "IfStatement", + "test": null, + "consequent": { + "type": "BlockStatement", + "body": [ + { + "type": "ExpressionStatement", + "expression": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "object": { + "type": "Identifier", + "name": "client" + }, + "property": { + "type": "Identifier", + "name": "publish" + } + }, + "arguments": [ + { + "type": "StringLiteral", + "value": "" + }, + { + "type": "StringLiteral", + "value": "" + } + ] + } + } + ] + } + } + }, + "anchors": [ + { + "type": "L", + "x": 150, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda4a235c0dc3e0c5f0456" + }, + "__v": 0 + }, + { + "_id": "63cda72e35c0dc3e0c5f0460", + "category": "Ende", + "name": " ", + "src": "http://localhost:9001/uploads/1674422062486.png", + "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], + "color": "#f9b43d", + "width": 200, + "height": 300, + "astNode": { + "javaScript": { + "type": "BlockStatement", + "body": null + }, + "python": { + "type": "BlockStatement", + "body": null + } + }, + "anchors": [ + { + "type": "L", + "x": 50, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda72e35c0dc3e0c5f0461" + }, + "__v": 0 + }, + { + "_id": "63cdabf635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Smarte Lampe", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "lampe" + }, + "MQTTtopic": "lampe/lampe1/set" + }, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdabg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Knopf", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "knopf" + } + }, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cd4bg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Timer", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "timer" + } + }, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "an", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "on" + } + }, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "83cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "drücken", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "pressed" + } + }, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "83cdad5d35c0dc4e0c5f0475", + "category": "Zustand", + "name": "speichern", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "save" + } + }, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "63cdaff535c0dc3e0c5f0478", + "category": "Konditionen", + "name": "Dann", + "src": "http://localhost:9001/uploads/1674424309409.png", + "points": [0, 0, 200, 0, 300, 150, -100, 150], + "color": "#F9B43D", + "width": 400, + "height": 200, + "anchors": [ + { + "type": "L", + "x": -100, + "y": 50 + }, + { + "type": "R", + "x": 300, + "y": 50 + } + ], + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdaff535c0dc3e0c5f0479" + }, + "__v": 1 + }, + { + "_id": "63cdb2bc35c0dc3e0c5f047d", + "category": "Konditionen", + "name": "Und", + "src": "http://localhost:9001/uploads/1674425020844.png", + "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], + "color": "#E2E7DF", + "width": 350, + "height": 450, + "astNode": { + "javaScript": { + "type": "LogicalExpression", + "left": null, + "right": { + "right": { + "type": "BinaryExpression", + "left": null, + "right": null, + "operator": "===" + } + }, + "operator": "&&" + } + }, + "anchors": [ + { + "type": "TL", + "x": -90, + "y": 50 + }, + { + "type": "TR", + "x": 225, + "y": 50 + }, + { + "type": "BL", + "x": 215, + "y": 350 + }, + { + "type": "BR", + "x": -90, + "y": 350 + } + ], + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdb2bc35c0dc3e0c5f047e" + }, + "__v": 0 + } +] diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json deleted file mode 100644 index 3a58575..0000000 --- a/frontend/src/json/kacheln.json +++ /dev/null @@ -1,90 +0,0 @@ -[ - { - "category": "Start", - "name": "Wenn", - "svgPath": "M1 186L89 237V117L194 57L97 1L1 186Z", - "svgRotate": 45, - "fill": "#f9b43d", - "url": "https://i.ibb.co/8M0jgv1/When.png" - }, - { - "category": "End", - "name": "Ende", - "svgPath": "M193.5 1H2L105 66.5L2 131.5H193.5V1Z", - "svgRotate": 0, - "fill": "#f9b43d", - "url": "https://i.ibb.co/v47Vmqq/end.png" - }, - { - "category": "Objects", - "name": "Handy", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" - }, - { - "category": "Objects", - "name": "Licht", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" - }, - { - "category": "Objects", - "name": "Lampe", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" - }, - { - "category": "Actions", - "name": "an", - "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", - "svgRotate": 0, - "fill": "#f4aece", - "url": "https://i.ibb.co/0996SHK/an.png" - }, - { - "category": "Actions", - "name": "aus", - "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", - "svgRotate": 0, - "fill": "#f4aece", - "url": "https://i.ibb.co/8MjkqL5/aus.png" - }, - { - "category": "Conditions", - "name": "Solange", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#bababa", - "url": "https://i.ibb.co/gVgx6Cf/solange.png" - }, - { - "category": "Conditions", - "name": "Sonst", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#bababa", - "url": "https://i.ibb.co/hLMHLr9/sonst.png" - }, - { - "category": "Negation", - "name": "Nicht", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "url": "https://i.ibb.co/6Xd9LyJ/Not.png" - }, - { - "category": "Union", - "name": "Und", - "svgPath": "M0 0L102.5 66.5V181L0 246.5V0Z", - "svgRotate": 0, - "fill": "#bababa", - "url": "https://i.ibb.co/4j5Mznp/Und.png" - } -] diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index d0d7103..d9f4224 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -1,33 +1,86 @@ import React from 'react'; import Board from '../components/Board/Board'; -import { useToggle } from '../hooks/useToggle'; +import Editor from 'react-simple-code-editor'; import Sidebar from '../components/Sidebar/Sidebar'; -import AddTileForm from '../components/Forms/AddTileForm'; -import Cursor from '../components/Cursor/Cursor'; -import { useWebSocketState } from '../state/WebSocketState'; -import RightClickMenu from '../components/ContextMenus/RightClickMenu'; -import { useContextMenuState } from '../state/ContextMenuState'; -import InfoComponent from '../components/Forms/InfoComponent'; import { useWindowFocus } from '../hooks/useWindowFocus'; +import { highlight, languages } from 'prismjs'; +import InfoComponent from '../components/Forms/InfoComponent'; +import SelectLampForm from '../components/Forms/SelectLampForm'; +import { useContextMenuState } from '../state/ContextMenuState'; +import TileRightClickMenu from '../components/ContextMenus/TileRightClickMenu'; +import LineRightClickMenu from '../components/ContextMenus/LineRightClickMenu'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; +import 'prismjs/components/prism-clike'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-json'; +import 'prismjs/themes/prism.css'; +import Default from '../components/Buttons/Default'; const CanvasPage = () => { - const { isOpen, toggleForm } = useToggle(); // Add Cursor here. const socket = useWebSocketState((state) => state.socket); - const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); - const room = useWebSocketState((state) => state.room); + const { contextMenuOpen, lineContextMenuOpen } = useContextMenuState((state) => state); + const { generatedCode, setGeneratedCode, ast, setAst } = useConnectedTilesState((state) => state); useWindowFocus(); return ( <> - {contextMenuOpen === true && } - {isOpen && ( -
    - toggleForm()} /> -
    - )} - + {contextMenuOpen === true && } + {lineContextMenuOpen === true && } + + +
    + +
    + AST + console.log(code)} + highlight={(code) => highlight(code, languages.json, 'json')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 12, + }} + /> +
    + setAst(null)} text={'AST löschen'} /> +
    +
    + + {generatedCode.js.length > 0 && ( +
    + JavaScript + setGeneratedCode({ ...generatedCode, js: code })} + highlight={(code) => highlight(code, languages.js, 'js')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 16, + }} + /> +
    + )} + {generatedCode.py.length > 0 && ( +
    + Python + setGeneratedCode({ ...generatedCode, py: code })} + highlight={(code) => highlight(code, languages.js, 'js')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 16, + }} + /> +
    + )} +
    ); }; diff --git a/frontend/src/state/BoardState.tsx b/frontend/src/state/BoardState.tsx index 67e74aa..6e816e4 100644 --- a/frontend/src/state/BoardState.tsx +++ b/frontend/src/state/BoardState.tsx @@ -3,60 +3,66 @@ import create from 'zustand'; import { Stage } from 'konva/lib/Stage'; import { Group } from 'konva/lib/Group'; import React, { createRef } from 'react'; -import { NewNode, InnerObject } from '../types'; +import { Tile } from '../types'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type BoardContextType = { + allTiles: Tile[]; modalOpen: boolean; + tilesOnBoard: Tile[]; categoriesOpen: boolean; - allTiles: InnerObject[]; - tilesOnBoard: NewNode[]; + selectedTile: Tile | null; remoteDragColor: string | null; - activeDragTile: React.RefObject | null; stageReference: React.RefObject; + activeDragTile: React.RefObject | null; clearActiveDragTile: () => void; - addTile: (newNode: NewNode) => void; + addTile: (newTile: Tile) => void; toggleModal: (toggle: boolean) => void; - updateTile: (updatedNode: NewNode) => void; + updateTile: (updatedNode: Tile) => void; + setAllTiles: (tilesArray: Tile[]) => void; removeTile: (nodeToRemove: string) => void; setCategoriesOpen: (toggle: boolean) => void; - setAllTiles: (tilesArray: InnerObject[]) => void; + setSelectedTile: (tile: Tile | null) => void; setRemoteDragColor: (color: string | null) => void; - setActiveDragTile: (newActiveTile: React.RefObject) => void; setStageReference: (stage: React.RefObject) => void; + setActiveDragTile: (newActiveTile: React.RefObject) => void; }; export const useBoardState = create((set) => ({ allTiles: [], - modalOpen: false, - categoriesOpen: false, tilesOnBoard: [], - activeDragTile: null, + modalOpen: false, + selectedTile: null, remoteDragColor: '', + activeDragTile: null, + categoriesOpen: false, stageReference: createRef(), - clearActiveDragTile: () => set(() => ({ activeDragTile: null })), - setActiveDragTile: (newActiveTile: React.RefObject) => - set(() => ({ activeDragTile: newActiveTile })), - setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), - toggleModal: (toggle: boolean) => set(() => ({ modalOpen: toggle })), - setAllTiles: (tilesArray: InnerObject[]) => set(() => ({ allTiles: tilesArray })), - addTile: (newTile: NewNode) => - set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), - updateTile: (updatedTile: NewNode) => + updateTile: (updatedTile: Tile) => set((state) => ({ tilesOnBoard: state.tilesOnBoard.map((tile) => tile.id === updatedTile.id ? updatedTile : tile, ), })), + setStageReference: (stageRef: React.RefObject) => + set(() => ({ stageReference: stageRef })), + setActiveDragTile: (newActiveTile: React.RefObject) => + set(() => ({ activeDragTile: newActiveTile })), + clearActiveDragTile: () => set(() => ({ activeDragTile: null })), + toggleModal: (toggle: boolean) => set(() => ({ modalOpen: toggle })), + setAllTiles: (tilesArray: Tile[]) => set(() => ({ allTiles: tilesArray })), + setSelectedTile: (tile: Tile | null) => set(() => ({ selectedTile: tile })), + setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), removeTile: (tileToRemove: string) => set((state) => ({ tilesOnBoard: state.tilesOnBoard.filter((tile) => tile.id !== tileToRemove), })), setRemoteDragColor: (color: string | null) => set(() => ({ remoteDragColor: color })), - setStageReference: (stageRef: React.RefObject) => - set(() => ({ stageReference: stageRef })), + addTile: (newTile: Tile) => set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), })); -// :TODO: Remove this before production -mountStoreDevtool('BoardStore', useBoardState); + + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('BoardStore', useBoardState); +} diff --git a/frontend/src/state/ContextMenuState.tsx b/frontend/src/state/ContextMenuState.tsx index e307709..220d1e1 100644 --- a/frontend/src/state/ContextMenuState.tsx +++ b/frontend/src/state/ContextMenuState.tsx @@ -2,20 +2,29 @@ import create from 'zustand'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type ContextMenuStateType = { + panelOpen: boolean; contextMenuOpen: boolean; - contextMenuAnchorPoint: { x: number; y: number; id: string }; - + lineContextMenuOpen: boolean; + setPanelOpen: (value: boolean) => void; setContextMenuOpen: (value: boolean) => void; + setLineContextMenuOpen: (value: boolean) => void; + contextMenuAnchorPoint: { x: number; y: number; id: string }; setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => void; }; export const useContextMenuState = create((set) => ({ + panelOpen: false, contextMenuOpen: false, + lineContextMenuOpen: false, contextMenuAnchorPoint: { x: 0, y: 0, id: '' }, - - setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), + setPanelOpen: (value: boolean) => set(() => ({ panelOpen: value })), setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => set(() => ({ contextMenuAnchorPoint: value })), + setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), + setLineContextMenuOpen: (value: boolean) => set(() => ({ lineContextMenuOpen: value })), })); -mountStoreDevtool('ContextMenuState', useContextMenuState); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('ContextMenuState', useContextMenuState); +} diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx new file mode 100644 index 0000000..8839148 --- /dev/null +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -0,0 +1,48 @@ +// state that saves Connected Tiles Globally +// also saves the connections between them + +import create from 'zustand'; +import { ASTType } from '../astTypes'; +import { mountStoreDevtool } from 'simple-zustand-devtools'; +import { findConnections } from '../utils/tileConnections'; + +export type connectionsType = { from: string; to: string }[]; +export type ConnectedTilesContextType = { + fromShapeId: string | null; + ast: ASTType | null; + generatedCode: { + js: string; + py: string; + }; + + connections: connectionsType; + connectionPreview: JSX.Element | null; + setAst: (value: ASTType | null) => void; + setGeneratedCode: (value: { js: string; py: string }) => void; + setFromShapeId: (value: string | null) => void; + setConnectionPreview: (value: JSX.Element | null) => void; + removeConnection: (value: string) => void; + setConnections: (value: { from: string; to: string }[]) => void; +}; + +export const useConnectedTilesState = create((set) => ({ + ast: null, + generatedCode: { js: '', py: '' }, + connections: [], + fromShapeId: null, + connectedTiles: {}, + connectionPreview: null, + setGeneratedCode: (value: { js: string; py: string }) => set(() => ({ generatedCode: value })), + setAst: (value: ASTType | null) => set(() => ({ ast: value })), + setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), + removeConnection: (value: string) => + set((state) => ({ + connections: findConnections(value, state.connections), + })), + setConnections: (value: { from: string; to: string }[]) => set(() => ({ connections: value })), + setConnectionPreview: (value: JSX.Element | null) => set(() => ({ connectionPreview: value })), +})); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('ConnectedTilesContext', useConnectedTilesState); +} diff --git a/frontend/src/state/WebSocketState.tsx b/frontend/src/state/WebSocketState.tsx index 1f746be..d61cb05 100644 --- a/frontend/src/state/WebSocketState.tsx +++ b/frontend/src/state/WebSocketState.tsx @@ -5,19 +5,23 @@ import { RoomData } from '../types'; // TODO: split up room and user data export type WebSocketContextType = { + clearRoom: () => void; socket: Socket | null; room: RoomData | null; - setSocket: (socket: Socket) => void; setRoom: (room: RoomData) => void; - clearRoom: () => void; + setSocket: (socket: Socket) => void; }; export const useWebSocketState = create((set) => ({ - socket: null, room: null, + socket: null, + clearRoom: () => set(() => ({ room: null })), setSocket: (socket: Socket) => set(() => ({ socket: socket })), setRoom: (roomData: RoomData) => set(() => ({ room: roomData })), - clearRoom: () => set(() => ({ room: null })), })); -mountStoreDevtool('WebSocketState', useWebSocketState); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('WebSocketState', useWebSocketState); +} + diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index a28c546..268c92e 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -12,12 +12,13 @@ export type UserData = { }; -export type NewNode = { - id: string; +export type NewTile = { category: string; - x: number; - y: number; src: string; + name: string; + points: number[]; + color: string; + textPosition: { x: number; y: number }; }; export type CursorData = { @@ -27,20 +28,20 @@ export type CursorData = { }; export type Tile = { - src: string; - id: string; x: number; y: number; - category: string; -}; - -export type MenuTileProps = { + id?: string; + _id: string; + src: string; name: string; + color: string; + astNode?: { javaScript: any; python: any; MQTTtopic?: string }; + width: number; + height: number; + points: number[]; category: string; - svgPath: string; - fill: string; - svgRotate: number; - url: string; + textPosition: { x: number; y: number }; + anchors: { type: string; x: number; y: number }[]; }; export type InnerObject = { @@ -49,28 +50,34 @@ export type InnerObject = { svgPath: string; fill: string; svgRotate: number; - url: string; + src: string; }; export type SocketDragTile = { remoteUser: string; - tile: NewNode; + tile: Tile; roomId: string; remoteUserColor: string; }; export type TileData = { - tile: { - id: string; - category: string; - src: string; - x: number; - y: number; - }; + tile: Tile; +}; + +export type TileConnection = { + to: string; + from: string; + roomId: string; }; export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; + tileConnections?: TileConnection[]; }; + +export type Coordinates = { + x: number; + y: number; +}; \ No newline at end of file diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts new file mode 100644 index 0000000..cbdcb71 --- /dev/null +++ b/frontend/src/utils/generateAst.ts @@ -0,0 +1,279 @@ +import { + CallExpression, + BinaryExpression, + LogicalExpression, + Identifier, + IfStatement, + ExpressionStatement, + ASTType, + Operator, +} from '../astTypes.d'; +import { connectionsType } from '../state/SyntaxTreeState'; + +interface NodeType { + id: string; + anchorPosition: string; + tileName: string | undefined; + tileCategory: string | undefined; + astNode: any; +} + +export const generateAst = ( + fromNode: NodeType | undefined, + toNode: NodeType | undefined, + ast: ASTType | null, + setAst: (ast: ASTType | null) => void, + connections: connectionsType, + generatedCode?: { js: string; py: string }, + setGeneratedCode?: (value: { js: string; py: string }) => void, +) => { + console.log(fromNode?.tileCategory + '->' + toNode?.tileCategory); + if ( + fromNode?.tileCategory === undefined || + fromNode.tileName === undefined || + toNode?.tileName === undefined || + toNode?.tileCategory === undefined + ) + return; + + if (fromNode.tileCategory === 'Start' && toNode.tileCategory === 'Objekte' && ast === null) { + const binary = fromNode.astNode.javaScript.test as BinaryExpression; + const startNode = fromNode.astNode.javaScript as IfStatement; + binary.left = toNode.astNode.javaScript as Identifier; + startNode.test = binary; + setAst({ + type: 'File', + errors: [], + program: { + type: 'Program', + body: [startNode], + }, + }); + } + if ( + fromNode.tileCategory === 'Objekte' && + toNode.tileCategory === 'Zustand' && + ast !== null && + ast.program?.body[0].type === 'IfStatement' + ) { + if (ast.program?.body[0].test?.type === 'BinaryExpression') { + ast.program.body[0].test.right = toNode.astNode.javaScript as Identifier; + setAst(ast); + } + console.log('from Obj->Zustand:', ast.program.body[0].test?.type === 'LogicalExpression'); + if (ast.program.body[0].test?.type === 'LogicalExpression') { + console.log(toNode.id); + const findOtherConnection = connections.filter((a) => a.from === `${toNode.id}_R`); + console.log(findOtherConnection); + if (findOtherConnection.length === 1) { + const direction = findOtherConnection[0].to.split('_')[1]; + console.log('direction:', direction); + if (direction === 'TL') { + ast.program.body[0].test.left = { + type: 'BinaryExpression', + left: null, + right: null, + operator: Operator.equals, + }; + ast.program.body[0].test.left.left = fromNode.astNode.javaScript; + ast.program.body[0].test.left.right = toNode.astNode.javaScript; + } else if (direction === 'BL') { + ast.program.body[0].test.right = { + type: 'BinaryExpression', + left: null, + right: null, + operator: Operator.equals, + }; + ast.program.body[0].test.right.left = fromNode.astNode.javaScript; + ast.program.body[0].test.right.right = toNode.astNode.javaScript; + } + } + } + } + + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Dann' && + ast !== null && + ast.program?.body[0].type === 'IfStatement' + ) { + ast.program.body[0].consequent.body = []; + setAst(ast); + } + if ( + fromNode.tileCategory === 'Konditionen' && + fromNode.tileName === 'Dann' && + toNode.tileCategory === 'Objekte' && + ast !== null && + ast.program?.body[0].consequent.type === 'BlockStatement' + ) { + ast.program.body[0].consequent.body = [fromNode.astNode.javaScript as ExpressionStatement]; + const callExpression = ast.program.body[0].consequent.body[0].expression as CallExpression; + callExpression.arguments = [ + { + type: 'StringLiteral', + value: toNode.astNode.MQTTtopic, + }, + ]; + setAst(ast); + } + + if ( + fromNode.tileCategory === 'Objekte' && + toNode.tileCategory === 'Zustand' && + ast !== null && + ast.program?.body[0].consequent.body !== null && + ast.program?.body[0].consequent.type === 'BlockStatement' && + ast.program?.body[0].consequent.body.length !== 0 && + ast?.program?.body[0].consequent.body[0].expression.type === 'CallExpression' && + ast?.program?.body[0].consequent.body[0].expression.arguments.length !== 0 + ) { + ast?.program?.body[0].consequent.body[0].expression.arguments.push( + toNode.astNode.javaScript as any, + ); + + setAst(ast); + } + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Und' && + toNode.anchorPosition === 'TL' && + ast !== null + ) { + // save the current elements in the if Condition + console.log(ast.program.body[0].test.type); + if (ast.program.body[0].test.type !== 'LogicalExpression') { + const leftBinaryExpression = ast.program.body[0].test; + const logicNode = { + type: 'IfStatement', + test: { + type: 'LogicalExpression', + left: null, + right: null, + operator: '&&', + } as LogicalExpression, + consequent: { + type: 'BlockStatement', + body: null, + }, + } as unknown as IfStatement; + + const newAst = { + type: 'File', + errors: [], + program: { + type: 'Program', + body: [logicNode], + }, + } as ASTType; + logicNode.test.left = leftBinaryExpression as BinaryExpression; + console.log('added logical Op with TL'); + setAst(newAst); + } + } + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Und' && + toNode.anchorPosition === 'BL' && + ast !== null + ) { + // save the current elements in the if Condition + console.log(ast.program.body[0].test.type); + if (ast.program.body[0].test.type !== 'LogicalExpression') { + const leftBinaryExpression = ast.program.body[0].test; + const logicNode = { + type: 'IfStatement', + test: { + type: 'LogicalExpression', + left: null, + right: null, + operator: '&&', + } as LogicalExpression, + consequent: { + type: 'BlockStatement', + body: null, + }, + } as unknown as IfStatement; + + const newAst = { + type: 'File', + errors: [], + program: { + type: 'Program', + body: [logicNode], + }, + } as ASTType; + logicNode.test.right = leftBinaryExpression as BinaryExpression; + console.log('added logical Op with BL'); + setAst(newAst); + } + } + // if ( + // fromNode.tileCategory === 'Zustand' && + // toNode.tileCategory === 'Konditionen' && + // toNode.tileName === 'Und' && + // toNode.anchorPosition === 'BL' && + // ast !== null && + // ast.program.body[0].test.type === 'LogicalExpression' + // ) { + // ast.program.body[0].test.right = { + // type: 'BinaryExpression', + // left: null, + // right: null, + // operator: '===', + // } as BinaryExpression; + // ast.program.body[0].test.right.right = toNode.astNode.javaScript as Identifier; + // } + // if ( + // fromNode.tileCategory === 'Zustand' && + // toNode.tileCategory === 'Konditionen' && + // toNode.tileName === 'Und' && + // toNode.anchorPosition === 'TL' && + // ast !== null && + // ast.program.body[0].test.type === 'LogicalExpression' + // ) { + // ast.program.body[0].test.left = { + // type: 'BinaryExpression', + // left: null, + // right: null, + // operator: '===', + // } as BinaryExpression; + // ast.program.body[0].test.left.right = toNode.astNode.javaScript as Identifier; + // } + + if (fromNode.tileCategory === 'Zustand' && toNode.tileCategory === 'Ende' && ast !== null) { + // send ast to backend + const backendUrl = process.env.REACT_APP_BACKEND_URL; + console.log('fetch ast to backend: ', JSON.stringify(ast)); + const data = Promise.all([ + fetch(`${backendUrl}/ast/js`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), + }).then((value) => value.json()), + fetch(`${backendUrl}/ast/py`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), + }).then((value) => value.json()), + ]); + + (async () => { + try { + const resolvedData = await data; + setGeneratedCode && + setGeneratedCode({ js: resolvedData[0].code, py: resolvedData[1].code }); + setAst(null); + } catch (error) { + alert(error); + } + })(); + } +}; diff --git a/frontend/src/utils/tileConnections.ts b/frontend/src/utils/tileConnections.ts new file mode 100644 index 0000000..705157a --- /dev/null +++ b/frontend/src/utils/tileConnections.ts @@ -0,0 +1,11 @@ +const splitIds = (id: string) => { + const [fromId, toId] = id.split('.'); + return { fromId, toId }; +}; + +const findConnections = (value: string, connections: { from: string; to: string }[]) => { + const { fromId, toId } = splitIds(value); + return connections.filter((connection) => connection.from !== fromId && connection.to !== toId); +}; + +export { splitIds, findConnections };