From f3129f7fa386781cbe27f7ef832830fb07d70e45 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 9 May 2025 22:16:13 +0100 Subject: [PATCH] Added more functionality --- .env | 3 +- package-lock.json | 1208 ++++++++++++++++- package.json | 14 +- public/logo.svg | 37 +- src/App.jsx | 156 ++- src/App.test.js | 8 - src/assets/icons/fillamenticon.afdesign | Bin 81398 -> 0 bytes src/assets/icons/fillamenticon.svg | 8 - src/components/Auth/AuthContext.js | 419 +++--- src/components/Auth/AuthLayout.jsx | 39 +- src/components/Auth/AuthParticles.jsx | 92 +- src/components/Auth/LoginUser.jsx | 192 +-- src/components/Auth/RegisterPasskey.jsx | 69 - .../Dashboard/Fillaments/EditFillament.jsx | 205 --- .../Dashboard/Fillaments/Fillaments.jsx | 170 --- .../Dashboard/Fillaments/NewFillament.jsx | 331 ----- .../Dashboard/GCodeFiles/EditGCodeFile.jsx | 205 --- .../Dashboard/GCodeFiles/GCodeFiles.jsx | 170 --- .../Dashboard/GCodeFiles/NewGCodeFile.jsx | 295 ---- src/components/Dashboard/Overview.jsx | 44 - .../Dashboard/PrintJobs/NewPrintJob.jsx | 56 - .../Dashboard/PrintJobs/PrintJobs.jsx | 285 ---- .../Dashboard/Printers/ControlPrinter.jsx | 232 ---- .../Dashboard/Printers/EditPrinter.jsx | 129 -- .../Dashboard/Printers/Printers.jsx | 270 ---- src/components/Dashboard/Profile.jsx | 108 -- src/components/Dashboard/common/Dashboard.jsx | 14 +- .../Dashboard/common/DashboardBreadcrumb.jsx | 95 +- .../Dashboard/common/DashboardLayout.jsx | 84 +- .../common/DashboardMovementPanel.jsx | 199 --- .../Dashboard/common/DashboardNavigation.jsx | 213 ++- .../Dashboard/common/DashboardPrintStatus.jsx | 237 ---- .../Dashboard/common/DashboardSidebar.jsx | 67 +- .../common/DashboardTemperaturePanel.jsx | 237 ---- .../Dashboard/common/FillamentSelect.jsx | 150 -- .../Dashboard/common/GCodePreview.jsx | 109 +- .../Dashboard/context/SocketContext.js | 113 +- src/components/Icons/FillamentIcon.jsx | 9 - src/components/Icons/GCodeFileIcon.jsx | 12 +- src/components/Icons/PassKeysIcon.jsx | 12 +- src/components/PrivateRoute.jsx | 30 +- src/components/PublicRoute.jsx | 25 +- src/index.css | 4 + src/index.js | 16 +- src/reportWebVitals.js | 18 +- src/setupTests.js | 2 +- 46 files changed, 2203 insertions(+), 4188 deletions(-) delete mode 100644 src/App.test.js delete mode 100644 src/assets/icons/fillamenticon.afdesign delete mode 100644 src/assets/icons/fillamenticon.svg delete mode 100644 src/components/Auth/RegisterPasskey.jsx delete mode 100644 src/components/Dashboard/Fillaments/EditFillament.jsx delete mode 100644 src/components/Dashboard/Fillaments/Fillaments.jsx delete mode 100644 src/components/Dashboard/Fillaments/NewFillament.jsx delete mode 100644 src/components/Dashboard/GCodeFiles/EditGCodeFile.jsx delete mode 100644 src/components/Dashboard/GCodeFiles/GCodeFiles.jsx delete mode 100644 src/components/Dashboard/GCodeFiles/NewGCodeFile.jsx delete mode 100644 src/components/Dashboard/Overview.jsx delete mode 100644 src/components/Dashboard/PrintJobs/NewPrintJob.jsx delete mode 100644 src/components/Dashboard/PrintJobs/PrintJobs.jsx delete mode 100644 src/components/Dashboard/Printers/ControlPrinter.jsx delete mode 100644 src/components/Dashboard/Printers/EditPrinter.jsx delete mode 100644 src/components/Dashboard/Printers/Printers.jsx delete mode 100644 src/components/Dashboard/Profile.jsx delete mode 100644 src/components/Dashboard/common/DashboardMovementPanel.jsx delete mode 100644 src/components/Dashboard/common/DashboardPrintStatus.jsx delete mode 100644 src/components/Dashboard/common/DashboardTemperaturePanel.jsx delete mode 100644 src/components/Dashboard/common/FillamentSelect.jsx delete mode 100644 src/components/Icons/FillamentIcon.jsx diff --git a/.env b/.env index 3ad6561..0cfcc91 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -HTTPS=false \ No newline at end of file +HTTPS=false +ENVIRONMENT=development diff --git a/package-lock.json b/package-lock.json index 2393baa..4ff677d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,18 @@ "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.5.0", "antd": "^5.19.2", + "antd-style": "^3.7.1", "axios": "*", + "dotenv": "^16.5.0", "gcode-preview": "^2.17.0", + "keycloak-js": "^26.1.5", "moment": "*", + "prop-types": "^15.8.1", "react": "*", "react-dom": "*", "react-router-dom": "*", "react-scripts": "*", + "react-stl-viewer": "^2.5.0", "socket.io-client": "*", "styled-components": "*", "three": "^0.166.1", @@ -26,6 +31,13 @@ "web-vitals": "*" }, "devDependencies": { + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^4.6.2", + "prettier": "^3.3.3", + "prettier-eslint": "^16.3.0", "standard": "^17.1.0" } }, @@ -64,9 +76,9 @@ } }, "node_modules/@ant-design/cssinjs": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.21.0.tgz", - "integrity": "sha512-gIilraPl+9EoKdYxnupxjHB/Q6IHNRjEXszKbDxZdsgv4sAZ9pjkCq8yanDWNvyfjp4leir2OVAJm0vxwKK8YA==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", + "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", @@ -75,13 +87,19 @@ "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", - "stylis": "^4.0.13" + "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, + "node_modules/@ant-design/cssinjs/node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/@ant-design/icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.4.0.tgz", @@ -186,9 +204,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.8.tgz", - "integrity": "sha512-nYAikI4XTGokU2QX7Jx+v4rxZKhKivaQaREZjuW3mrJrbdWJ5yUfohnoUULge+zEEaKjPYNxhoRgUKktjXtbwA==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz", + "integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==", "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -2247,6 +2265,39 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "license": "Apache-2.0" + }, "node_modules/@csstools/normalize.css": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", @@ -2542,6 +2593,108 @@ "node": ">=10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -2563,12 +2716,94 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", "license": "MIT" }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", "license": "MIT" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3604,6 +3839,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -3801,6 +4049,64 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@react-three/fiber": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", + "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=18 <19", + "react-dom": ">=18 <19", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@remix-run/router": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", @@ -3890,9 +4196,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "license": "MIT" }, "node_modules/@simplewebauthn/browser": { @@ -4931,6 +5237,12 @@ "@types/node": "*" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4961,6 +5273,24 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -5039,6 +5369,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.22", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz", + "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.11", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", @@ -5444,6 +5780,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/glslang": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@webgpu/glslang/-/glslang-0.0.15.tgz", + "integrity": "sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q==", + "license": "glslang/LICENSE.txt" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5759,6 +6101,26 @@ "react-dom": ">=16.9.0" } }, + "node_modules/antd-style": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/antd-style/-/antd-style-3.7.1.tgz", + "integrity": "sha512-CQOfddVp4aOvBfCepa+Kj2e7ap+2XBINg1Kn2osdE3oQvrD7KJu/K0sfnLcFLkgCJygbxmuazYdWLKb+drPDYA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.24.1", + "@emotion/cache": "^11.11.0", + "@emotion/css": "^11.11.2", + "@emotion/react": "^11.11.4", + "@emotion/serialize": "^1.1.3", + "@emotion/utils": "^1.2.1", + "use-merge-value": "^1.2.0" + }, + "peerDependencies": { + "antd": ">=5.8.1", + "react": ">=18" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -6080,9 +6442,9 @@ } }, "node_modules/axe-core": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz", - "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -6415,6 +6777,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6609,6 +6991,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6784,6 +7190,20 @@ "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", "license": "MIT" }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -8022,12 +8442,15 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -8036,6 +8459,12 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "license": "BSD-2-Clause" }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8494,6 +8923,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -8828,6 +9270,37 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-promise": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", @@ -9460,6 +9933,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -9536,6 +10016,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9687,6 +10173,12 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -10347,6 +10839,29 @@ "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", "license": "(Apache-2.0 OR MPL-1.1)" }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -10437,6 +10952,21 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -10744,6 +11274,26 @@ "node": ">=4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -10816,6 +11366,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11504,6 +12064,27 @@ "set-function-name": "^2.0.1" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -13844,6 +14425,12 @@ "node": ">=4.0" } }, + "node_modules/keycloak-js": { + "version": "26.1.5", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.1.5.tgz", + "integrity": "sha512-5m8DQceKgBD+iVyy8GYeOiH3Hu12q6azAxhBRabVnZANt5BEi9r/qEXyOeye9yzrjqPF6RlH48TTTYNd1iXZMQ==", + "license": "Apache-2.0" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -13880,6 +14467,12 @@ "node": ">= 8" } }, + "node_modules/ktx-parse": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.4.5.tgz", + "integrity": "sha512-MK3FOody4TXbFf8Yqv7EBbySw7aPvEcPX++Ipt6Sox+/YMFvR5xaTyhfNSk1AEmMy+RYIw81ctN4IMxCB8OAlg==", + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -14067,6 +14660,91 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -14319,6 +14997,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mmd-parser": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mmd-parser/-/mmd-parser-1.0.4.tgz", + "integrity": "sha512-Qi0VCU46t2IwfGv5KF0+D/t9cizcDug7qnNoy9Ggk7aucp0tssV8IwTMkBlDbm+VqAf3cdQHTCARKSsuS2MYFg==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -14719,6 +15403,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opentype.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", + "integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==", + "license": "MIT", + "dependencies": { + "string.prototype.codepointat": "^0.2.1", + "tiny-inflate": "^1.0.3" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -16395,6 +17095,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16404,6 +17110,267 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-eslint": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", + "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "prettier-plugin-svelte": "^3.0.0", + "svelte-eslint-parser": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prettier-eslint/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/prettier-eslint/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -17514,6 +18481,31 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -17628,6 +18620,45 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/react-stl-viewer": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-stl-viewer/-/react-stl-viewer-2.5.0.tgz", + "integrity": "sha512-jlYId05N0P9rKVEdOfLO1bPsS9SYfMRxjnKvhks6T/c4HU0BxnNcZpRr4gfpJU0nFL6HHgmVKhKTh1LCHNcZuA==", + "license": "MIT", + "dependencies": { + "@react-three/fiber": "^8.15.5", + "three-stdlib": "2.17.2" + }, + "peerDependencies": { + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.154" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17744,6 +18775,12 @@ "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "license": "MIT" }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -17852,6 +18889,13 @@ "node": ">=0.10.0" } }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true, + "license": "MIT" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -19039,6 +20083,12 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", @@ -19451,6 +20501,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -19550,6 +20609,23 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tailwindcss": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", @@ -19774,6 +20850,28 @@ "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==", "license": "MIT" }, + "node_modules/three-stdlib": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.17.2.tgz", + "integrity": "sha512-7ZLCJJogtn1D1MlUi7q0iLUbrxj7K++YxjHIIz5AZ4wX4E137BgiiTmhH4XhAuvXGRk9ph3ZtoHTfJBXhqDX3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "@types/offscreencanvas": "^2019.6.4", + "@webgpu/glslang": "^0.0.15", + "chevrotain": "^10.1.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "ktx-parse": "^0.4.5", + "mmd-parser": "^1.0.4", + "opentype.js": "^1.3.3", + "potpack": "^1.0.1", + "zstddec": "^0.0.2" + }, + "peerDependencies": { + "three": ">=0.122.0" + } + }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -19795,6 +20893,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19879,6 +20983,19 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -20284,6 +21401,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-merge-value": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-merge-value/-/use-merge-value-1.2.0.tgz", + "integrity": "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.x" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -20368,6 +21494,31 @@ "node": ">=0.10.48" } }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -21401,6 +22552,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zstddec": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.0.2.tgz", + "integrity": "sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==", + "license": "MIT AND BSD-3-Clause" + }, + "node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 749cd68..f4c34b5 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,18 @@ "@tsparticles/react": "^3.0.0", "@tsparticles/slim": "^3.5.0", "antd": "^5.19.2", + "antd-style": "^3.7.1", "axios": "*", + "dotenv": "^16.5.0", "gcode-preview": "^2.17.0", + "keycloak-js": "^26.1.5", "moment": "*", + "prop-types": "^15.8.1", "react": "*", "react-dom": "*", "react-router-dom": "*", "react-scripts": "*", + "react-stl-viewer": "^2.5.0", "socket.io-client": "*", "styled-components": "*", "three": "^0.166.1", @@ -21,7 +26,7 @@ "web-vitals": "*" }, "scripts": { - "start": "react-scripts start", + "dev": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" @@ -45,6 +50,13 @@ ] }, "devDependencies": { + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^4.6.2", + "prettier": "^3.3.3", + "prettier-eslint": "^16.3.0", "standard": "^17.1.0" } } diff --git a/public/logo.svg b/public/logo.svg index 1a06d54..8004a88 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,18 +1,33 @@ - - - - - - Farm Control - - - - + + + + + + + + + + + + + + + + + + + - + + + + + + + diff --git a/src/App.jsx b/src/App.jsx index 5455e5a..db04ebf 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,89 +1,131 @@ -import React, { useContext, useState } from "react"; +import React from 'react' import { BrowserRouter as Router, Routes, Route, - Navigate, -} from "react-router-dom"; -import { App, ConfigProvider, theme } from "antd"; -import LoginUser from "./components/Auth/LoginUser.jsx"; -import RegisterPasskey from "./components/Auth/RegisterPasskey.jsx"; -import Profile from "./components/Dashboard/Profile.jsx"; -import Overview from "./components/Dashboard/Overview"; + Navigate +} from 'react-router-dom' +import { App, ConfigProvider, theme } from 'antd' +import AuthLayout from './components/Auth/AuthLayout.jsx' +import ProductionOverview from './components/Dashboard/Production/Overview' -import Printers from "./components/Dashboard/Printers/Printers"; -import EditPrinter from "./components/Dashboard/Printers/EditPrinter.jsx"; -import ControlPrinter from "./components/Dashboard/Printers/ControlPrinter.jsx"; +import Printers from './components/Dashboard/Production/Printers' +import ControlPrinter from './components/Dashboard/Production/Printers/ControlPrinter.jsx' +import PrinterInfo from './components/Dashboard/Production/Printers/PrinterInfo.jsx' -import PrintJobs from "./components/Dashboard/PrintJobs/PrintJobs.jsx"; +import PrintJobs from './components/Dashboard/Production/PrintJobs.jsx' +import PrintJobInfo from './components/Dashboard/Production/PrintJobs/PrintJobInfo.jsx' -import Fillaments from "./components/Dashboard/Fillaments/Fillaments.jsx"; +import Spools from './components/Dashboard/Inventory/Spools' -import GCodeFiles from "./components/Dashboard/GCodeFiles/GCodeFiles.jsx"; +import Filaments from './components/Dashboard/Management/Filaments' +import FilamentInfo from './components/Dashboard/Management/Filaments/FilamentInfo.jsx' -import Dashboard from "./components/Dashboard/common/Dashboard"; -import PrivateRoute from "./components/PrivateRoute"; -import PublicRoute from "./components/PublicRoute.jsx"; -import "./App.css"; -import { SocketProvider } from "./components/Dashboard/context/SocketContext.js"; -import { AuthProvider } from "./components/Auth/AuthContext.js"; +import GCodeFiles from './components/Dashboard/Production/GCodeFiles' +import GCodeFileInfo from './components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx' + +import Parts from './components/Dashboard/Management/Parts.jsx' +import PartInfo from './components/Dashboard/Management/Parts/PartInfo.jsx' + +import Products from './components/Dashboard/Management/Products.jsx' +import ProductInfo from './components/Dashboard/Management/Products/ProductInfo.jsx' + +import Dashboard from './components/Dashboard/common/Dashboard' +import PrivateRoute from './components/PrivateRoute' +import PublicRoute from './components/PublicRoute.jsx' +import './App.css' +import { SocketProvider } from './components/Dashboard/context/SocketContext.js' +import { AuthProvider } from './components/Auth/AuthContext.js' +import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.js' +import Vendors from './components/Dashboard/Management/Vendors' +import VendorInfo from './components/Dashboard/Management/Vendors/VendorInfo' const FarmControlApp = () => { return ( - - - } />} - /> + + + + ( + + )} + /> + } + /> + } />} + /> - } /> - } - /> + } />} + > + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + - } />} - > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + } />} + > + } /> + + + } />} + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + - ); -}; + ) +} -export default FarmControlApp; +export default FarmControlApp diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/assets/icons/fillamenticon.afdesign b/src/assets/icons/fillamenticon.afdesign deleted file mode 100644 index 3e5d04a0b41374ba457d83ba8c21fe1266c7db1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81398 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 aG6f;;)Q;cNBRV?)fU1&~Vy(Po - - - - - - - diff --git a/src/components/Auth/AuthContext.js b/src/components/Auth/AuthContext.js index 902c5d3..b38bdef 100644 --- a/src/components/Auth/AuthContext.js +++ b/src/components/Auth/AuthContext.js @@ -1,223 +1,266 @@ // src/contexts/AuthContext.js -import React, { createContext, useState, useEffect, useCallback } from "react"; -import axios from "axios"; -import { message } from "antd"; +import React, { createContext, useState, useCallback, useEffect } from 'react' +import axios from 'axios' +import { message, Modal, notification, Progress, Button, Space } from 'antd' +import PropTypes from 'prop-types' import { - startAuthentication, - startRegistration, -} from "@simplewebauthn/browser"; + ExclamationCircleOutlined, + InfoCircleOutlined +} from '@ant-design/icons' -const AuthContext = createContext(); +const AuthContext = createContext() const AuthProvider = ({ children }) => { - const [messageApi, contextHolder] = message.useMessage(); - const [token, setToken] = useState( - localStorage.getItem("access_token") || null - ); - const [loading, setLoading] = useState(false); + const [messageApi, contextHolder] = message.useMessage() + const [notificationApi, notificationContextHolder] = + notification.useNotification() + const [authenticated, setAuthenticated] = useState(false) + const [loading, setLoading] = useState(false) + const [token, setToken] = useState(null) + const [expiresAt, setExpiresAt] = useState(null) + const [userProfile, setUserProfile] = useState(null) + const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false) + const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false) - const validateToken = useCallback(async (token) => { - if (!token) { - return false; - } - setLoading(true); - try { - const reponse = await axios.post( - "http://localhost:8080/auth/validate-token", - { - token, - } - ); - if (reponse.data.status === "OK") { - setLoading(false); - return true; - } - } catch (error) { - console.error("Invalid token", error); - messageApi.error("Session invalid."); - setToken(null); - localStorage.removeItem("access_token"); - } - setLoading(false); - return false; - }, [messageApi]); + const logout = useCallback((redirectUri = '/login') => { + setAuthenticated(false) + setToken(null) + setExpiresAt(null) + setUserProfile(null) + window.location.href = `http://localhost:8080/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}` + }, []) - const getAuthMode = useCallback(async (email) => { - if (!email) { - return { successful: false }; - } - setLoading(true); + // Login using query parameters + const loginWithSSO = useCallback( + (redirectUri = window.location.pathname + window.location.search) => { + messageApi.info('Logging in with tombutcher.work') + window.location.href = `http://localhost:8080/auth/login?redirect_uri=${encodeURIComponent(redirectUri)}` + }, + [messageApi] + ) + // Function to check if the user is logged in + const checkAuthStatus = useCallback(async () => { + setLoading(true) try { - const response = await axios.post("http://localhost:8080/auth/modes", { - email, - }); - const { authModes } = response.data; - setLoading(false); - return { successful: true, authModes }; - } catch (error) { - if (error.response === undefined) { - messageApi.error( - "An error occoured obtaining the auth mode: " + error.message - ); + // Make a call to your backend to check auth status + const response = await axios.get('http://localhost:8080/auth/user', { + withCredentials: true // Important for including cookies + }) + + if (response.status === 200 && response.data) { + console.log('User is authenticated!') + setAuthenticated(true) + setToken(response.data.access_token) + setExpiresAt(response.data.expires_at) + setUserProfile(response.data) } else { - if (error.response.status === 400) { - messageApi.error(error.response.data.error); - } else { - messageApi.error( - "An unexpected error occoured obtaining the auth mode. (" + - error.response.status + - ")" - ); - } + setAuthenticated(false) } - } - setLoading(false); - return { successful: false }; - }, [messageApi]); - - const handleLoginFinished = (user, access_token) => { - setToken(access_token); - localStorage.setItem("access_token", access_token); - messageApi.info("Welcome, " + user.name + "."); - return { successful: true, hasPasskey: user.hasPasskey }; - }; - - const loginWithPassword = useCallback(async (email, password) => { - if (!email || !password) { - return { successful: false }; - } - setLoading(true); - try { - const response = await axios.post("http://localhost:8080/auth/login", { - email, - password, - }); - const { user, access_token } = response.data; - return handleLoginFinished(user, access_token); } catch (error) { - if (error.response === undefined) { - messageApi.error("An error occoured: " + error.message); + console.log('Auth check failed', error) + if (error.response?.status === 401) { + setShowUnauthorizedModal(true) + } + setAuthenticated(false) + } finally { + setLoading(false) + } + }, []) + + const refreshToken = useCallback(async () => { + try { + const response = await axios.get('http://localhost:8080/auth/refresh', { + withCredentials: true + }) + if (response.status === 200 && response.data) { + setToken(response.data.access_token) + setExpiresAt(response.data.expires_at) + } + } catch (error) { + console.error('Token refresh failed', error) + } + }, []) + + const showTokenExpirationMessage = useCallback( + (expiresAt) => { + const now = new Date() + const expirationDate = new Date(expiresAt) + const timeRemaining = expirationDate - now + + if (timeRemaining <= 0) { + if (authenticated) { + setShowSessionExpiredModal(true) + setAuthenticated(false) + notificationApi.destroy('token-expiration') + } } else { - if (error.response.status === 400) { - messageApi.error(error.response.data.error); - } else { - messageApi.error( - "An unexpected error occoured. (" + error.response.status + ")" - ); + const minutes = Math.floor(timeRemaining / 60000) + const seconds = Math.floor((timeRemaining % 60000) / 1000) + + // Only show notification in the final minute + if (minutes === 0) { + const totalSeconds = 60 + const remainingSeconds = totalSeconds - seconds + const progress = (remainingSeconds / totalSeconds) * 100 + + notificationApi.info({ + message: 'Session Expiring Soon', + description: ( +
+
+ Your session will expire in {seconds} seconds +
+ +
+ ), + duration: 0, + key: 'token-expiration', + icon: null, + placement: 'bottomRight', + style: { + width: 360 + }, + className: 'token-expiration-notification', + closeIcon: null, + onClose: () => {}, + btn: ( + + ) + }) + } else if (minutes === 1) { + // Clear any existing notification when we enter the final minute + notificationApi.destroy('token-expiration') } } + }, + [authenticated, notificationApi] + ) + + const handleSessionExpiredModalOk = () => { + setShowSessionExpiredModal(false) + loginWithSSO() + } + + // Initialize on component mount + useEffect(() => { + let intervalId + + const tokenRefreshInterval = () => { + if (expiresAt) { + showTokenExpirationMessage(expiresAt) + } } - setLoading(false); - return { successful: false }; - }, [messageApi]); - const loginWithPasskey = useCallback(async (email) => { - if (!email) { - return { successful: false }; + if (authenticated) { + intervalId = setInterval(tokenRefreshInterval, 1000) } - setLoading(true); - try { - const loginOptionsResponse = await axios.post( - "http://localhost:8080/auth/passkey/login", - { email }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - const loginOptions = loginOptionsResponse.data; - console.log(loginOptions); - const attestationResponse = await startAuthentication(loginOptions); - - const loginResponse = await axios.post( - "http://localhost:8080/auth/passkey/login", { email, attestationResponse }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - const { user, access_token } = loginResponse.data; - return handleLoginFinished(user, access_token); - } catch (error) { - console.log(error); - messageApi.error("An error occoured: " + error.name); + return () => { + if (intervalId) { + clearInterval(intervalId) + } } - setLoading(false); - return { successful: false }; - }, [messageApi]); - - const logout = useCallback(() => { - setToken(null); - localStorage.removeItem("access_token"); - messageApi.info("Sucessfully logged out."); - }, [messageApi]); - - const registerPasskey = useCallback(async () => { - if (!token) { - return { successful: false }; - } - setLoading(true); - try { - const registerOptionsResponse = await axios.post( - "http://localhost:8080/auth/passkey/register", - { token }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - - const registerOptions = registerOptionsResponse.data; - console.log(registerOptions); - const attestationResponse = await startRegistration(registerOptions); - - await axios.post( - "http://localhost:8080/auth/passkey/register", - attestationResponse, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - messageApi.success("Passkey registered!"); - setLoading(false); - return { successful: true }; - } catch (error) { - console.log(error); - messageApi.error("An error occoured: " + error.name); - } - setLoading(false); - return { successful: false }; - }, [messageApi]); + }, [expiresAt, authenticated, showTokenExpirationMessage]) useEffect(() => { - console.log("Token changed!" + token) - validateToken(token); - }, [token]); // if token changes, validate it. + checkAuthStatus() + }, [checkAuthStatus]) return ( <> {contextHolder} + {notificationContextHolder} {children} + + + Session Expired + + } + open={showSessionExpiredModal} + onOk={handleSessionExpiredModalOk} + okText='Log In' + style={{ maxWidth: 430 }} + closable={false} + centered + maskClosable={false} + footer={[ + + ]} + > + Your session has expired. Please log in again to continue. + + + + Please log in to continue + + } + open={showUnauthorizedModal} + onOk={() => { + setShowUnauthorizedModal(false) + loginWithSSO() + }} + okText='Log In' + style={{ maxWidth: 430 }} + closable={false} + centered + maskClosable={false} + footer={[ + + ]} + > + You need to be logged in to access FarmControl. Please log in with + tombutcher.work to continue. + - ); -}; + ) +} -export { AuthContext, AuthProvider }; +AuthProvider.propTypes = { + children: PropTypes.node.isRequired +} + +export { AuthContext, AuthProvider } diff --git a/src/components/Auth/AuthLayout.jsx b/src/components/Auth/AuthLayout.jsx index 20adf16..18150b3 100644 --- a/src/components/Auth/AuthLayout.jsx +++ b/src/components/Auth/AuthLayout.jsx @@ -1,29 +1,38 @@ -import React, { useContext } from "react"; -import { Spin, Flex, Card } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; -import { AuthContext } from "./AuthContext"; -import AuthParticles from "./AuthParticles"; -import "./Auth.css"; +import PropTypes from 'prop-types' +import React, { useContext } from 'react' +import { Spin, Flex, Card } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import { AuthContext } from './AuthContext' +import AuthParticles from './AuthParticles' +import './Auth.css' const AuthLayout = ({ children }) => { - const { loading } = useContext(AuthContext); + const { loading } = useContext(AuthContext) return ( <> - } size="large"> + } + size='large' + > {children} - ); -}; + ) +} -export default AuthLayout; +AuthLayout.propTypes = { + children: PropTypes.node.isRequired +} + +export default AuthLayout diff --git a/src/components/Auth/AuthParticles.jsx b/src/components/Auth/AuthParticles.jsx index c147f02..e8f01fd 100644 --- a/src/components/Auth/AuthParticles.jsx +++ b/src/components/Auth/AuthParticles.jsx @@ -1,105 +1,108 @@ -import React, { useState, useEffect, useMemo, useCallback } from "react"; -import Particles, { initParticlesEngine } from "@tsparticles/react"; -import { loadSlim } from "@tsparticles/slim"; +import PropTypes from 'prop-types' +import React, { useState, useEffect, useMemo, useCallback } from 'react' +import Particles, { initParticlesEngine } from '@tsparticles/react' +import { loadSlim } from '@tsparticles/slim' -import "./Auth.css"; +import './Auth.css' const ParticlesComponent = React.memo(({ options, particlesLoaded }) => { return ( - ); -}); + ) +}) + +ParticlesComponent.displayName = 'ParticlesComponent' const AuthParticles = () => { - const [init, setInit] = useState(false); + const [init, setInit] = useState(false) // this should be run only once per application lifetime useEffect(() => { initParticlesEngine(async (engine) => { - await loadSlim(engine); + await loadSlim(engine) }).then(() => { - setInit(true); - }); - }, []); + setInit(true) + }) + }, []) - const particlesLoaded = useCallback((container) => { - console.log(container); - }, []); + const particlesLoaded = useCallback(() => { + console.log('Particles Loaded!') + }, []) const options = useMemo( () => ({ background: { color: { - value: "#141414", - }, + value: '#141414' + } }, fpsLimit: 120, interactivity: { events: { onClick: { enable: true, - mode: "push", + mode: 'push' }, onHover: { enable: true, - mode: "repulse", - }, + mode: 'repulse' + } }, modes: { push: { - quantity: 4, + quantity: 4 }, repulse: { distance: 200, - duration: 0.4, - }, - }, + duration: 0.4 + } + } }, particles: { color: { - value: "#ffffff", + value: '#ffffff' }, links: { - color: "#ffffff", + color: '#ffffff', distance: 150, enable: true, opacity: 0.5, - width: 1, + width: 1 }, move: { - direction: "none", + direction: 'none', enable: true, outModes: { - default: "bounce", + default: 'bounce' }, random: false, speed: 1, - straight: false, + straight: false }, number: { density: { - enable: true, + enable: true }, - value: 160, + value: 160 }, opacity: { - value: 0.5, + value: 0.5 }, shape: { - type: "circle", + type: 'circle' }, size: { - value: { min: 1, max: 5 }, - }, + value: { min: 1, max: 5 } + } }, - detectRetina: true, + detectRetina: true }), [] - ); + ) return ( <> {init && ( @@ -109,7 +112,12 @@ const AuthParticles = () => { /> )} - ); -}; + ) +} -export default AuthParticles; +ParticlesComponent.propTypes = { + options: PropTypes.object.isRequired, + particlesLoaded: PropTypes.func.isRequired +} + +export default AuthParticles diff --git a/src/components/Auth/LoginUser.jsx b/src/components/Auth/LoginUser.jsx index fcdc6f4..ac0a382 100644 --- a/src/components/Auth/LoginUser.jsx +++ b/src/components/Auth/LoginUser.jsx @@ -1,178 +1,52 @@ -import React, { useState, useContext, useEffect, useMemo, useRef } from "react"; -import axios from "axios"; -import { useNavigate } from "react-router-dom"; -import { - Form, - Input, - Button, - Checkbox, - Spin, - Divider, - Typography, - Flex, - Card, - Space, - message, -} from "antd"; -import { - UserOutlined, - LockOutlined, - LoginOutlined, - UserAddOutlined, - ArrowRightOutlined, -} from "@ant-design/icons"; -import Particles, { initParticlesEngine } from "@tsparticles/react"; -import { loadSlim } from "@tsparticles/slim"; -import { AuthContext } from "./AuthContext"; -import AuthLayout from "./AuthLayout"; +import React, { useContext } from 'react' +//import { useNavigate } from 'react-router-dom' +import { Form, Button, Divider, Typography, Flex } from 'antd' +import { UserAddOutlined } from '@ant-design/icons' +import { AuthContext } from './AuthContext' +import AuthLayout from './AuthLayout' -import PassKeysIcon from "../Icons/PassKeysIcon"; // Adjust the path if necessary +import './Auth.css' -import "./Auth.css"; - -const { Title, Text } = Typography; +const { Text } = Typography const LoginUser = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [authModes, setAuthModes] = useState([]); - const [error, setError] = useState(""); - const navigate = useNavigate(); - const { loginWithPassword, loginWithPasskey, getAuthMode } = - useContext(AuthContext); - - const handleLogin = async (e) => { - if (email === "") { - return; - } - if (authModes.length === 0) { - const result = await getAuthMode(email); - if (result.successful === true) { - setAuthModes(result.authModes); - } - return; - } - var result; - if (password.length > 0) { - result = await loginWithPassword(email, password); - } else { - result = await loginWithPasskey(email); - } - if (result.successful === true) { - if (authModes.includes("passkey")) { - setTimeout(() => { - navigate("/dashboard/overview"); - }, 200); - } else { - setTimeout(() => { - navigate("/login/register-passkey"); - }, 200); - } - } - }; + //const [error] = useState('') + //const navigate = useNavigate() + const { loginWithSSO } = useContext(AuthContext) + const handleLogin = async () => { + loginWithSSO('/production/overview') + } return ( - + Farm Control Logo

Farm Control

- - Please sign in using your credentials below. - + Please sign in below.
-
{ - handleLogin(e); - }} - > - - - } // Use UserOutlined icon - type="email" - value={email} - onChange={(e) => setEmail(e.target.value)} - placeholder="Email" - disabled={authModes.length > 0 ? true : false} - /> -
- {authModes.includes("password") ? ( -
{ - handleLogin(e); - }} - > - - } // Use LockOutlined icon - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - placeholder="Password" - /> - - - - {authModes.includes("passkey") ? ( - - ) : ( - <> - )} - - - {error &&

{error}

} -
-
- ) : ( - <> - )} -
- ); -}; + ) +} -export default LoginUser; +export default LoginUser diff --git a/src/components/Auth/RegisterPasskey.jsx b/src/components/Auth/RegisterPasskey.jsx deleted file mode 100644 index 5c97a7c..0000000 --- a/src/components/Auth/RegisterPasskey.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState, useContext, useEffect, useMemo } from "react"; -import axios from "axios"; -import { useNavigate } from "react-router-dom"; -import { - Form, - Input, - Button, - Checkbox, - Spin, - Divider, - Typography, - Flex, - Card, - Space, -} from "antd"; -import { LockOutlined } from "@ant-design/icons"; -import Particles, { initParticlesEngine } from "@tsparticles/react"; -import { loadSlim } from "@tsparticles/slim"; -import { AuthContext } from "./AuthContext"; - -import PassKeysIcon from "../Icons/PassKeysIcon"; // Adjust the path if necessary - -import "./Auth.css"; -import AuthLayout from "./AuthLayout"; - -const { Title, Text } = Typography; - -const RegisterPasskey = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - const navigate = useNavigate(); - const { registerPasskey } = useContext(AuthContext); - const [init, setInit] = useState(false); - - const handleRegisterPasskey = async (e) => { - const result = await registerPasskey(email, password); - if (result.successful === true) { - setTimeout(() => { - navigate("/dashboard/overview"); - }, 500); - } else {} - }; - - return ( - - - -

Register a Passkey

- - Please setup a passkey in order to continue. The passkey may use - another device for encryption. - -
- -
- ); -}; - -export default RegisterPasskey; diff --git a/src/components/Dashboard/Fillaments/EditFillament.jsx b/src/components/Dashboard/Fillaments/EditFillament.jsx deleted file mode 100644 index 924d408..0000000 --- a/src/components/Dashboard/Fillaments/EditFillament.jsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Skeleton, ColorPicker, Upload, Descriptions, Badge, Popconfirm } from "antd"; -import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; - -const { Title, Text } = Typography; - -const EditFillament = ({ id, onOk }) => { - const [messageApi, contextHolder] = message.useMessage(); - - const [dataLoading, setDataLoading] = useState(false); - const [editFillamentLoading, setEditFillamentLoading] = useState(false); - const [deleteFillamentLoading, setDeleteFillamentLoading] = useState(false); - - const [currentStep, setCurrentStep] = useState(0); - const [fillament, setFillament] = useState(null); - - const [imageList, setImageList] = useState([]); - const [image, setImage] = useState(""); - - const [editFillamentForm] = Form.useForm(); - const [editFillamentFormValues, setEditFillamentFormValues] = useState({}); - - const { token } = useContext(AuthContext); - - useEffect(() => { - // Fetch printer details when the component mounts - const fetchFillamentDetails = async () => { - if (id) { - try { - setDataLoading(true); - const response = await axios.get(`http://localhost:8080/fillaments/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setDataLoading(false); - editFillamentForm.setFieldsValue(response.data); // Set form values with fetched data - setEditFillamentFormValues(response.data); - } catch (error) { - messageApi.error('Error fetching printer details:' + error.message); - } - } - }; - fetchFillamentDetails(); - }, [id, editFillamentForm, token]); - - const handleEditFillament = async () => { - setEditFillamentLoading(true); - // Exclude the 'online' field from the submission - try { - await axios.put(`http://localhost:8080/fillaments/${id}`, editFillamentFormValues, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Fillament details updated successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error updating fillament details: ' + error.message); - } finally { - setEditFillamentLoading(false); - } - }; - - const handleDeleteFillament = async () => { - setDeleteFillamentLoading(true); - try { - await axios.delete(`http://localhost:8080/fillaments/${id}`, "", { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Fillament deleted successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error updating fillament details: ' + error.message); - } finally { - setDeleteFillamentLoading(false); - } - }; - - const handleImageUpload = ({ file, onSuccess }) => { - const reader = new FileReader(); - reader.onload = (e) => { - console.log("Setting image buffer", e.target.result); - //setImage(e.target.result); - onSuccess("ok"); - }; - reader.readAsDataURL(file); - }; - return ( - <> - {contextHolder} - } size="large"> -
setEditFillamentFormValues((prevValues) => ({ - ...prevValues, - ...changedValues, - }))} - > - - - - - - - - - - - { - if (!value) return '£'; - return `£${value}`; - }} step={0.01} style={{ width: "100%" }} addonAfter="per kg" /> - - - { - return "#" + color.toHex(); - }} - > - - - - - - - - { setImageList(fileList) }} - > - - - - - } - /> - - - } - /> - - - - - - - - - - -
- -
- - ); -}; - -export default EditFillament; diff --git a/src/components/Dashboard/Fillaments/Fillaments.jsx b/src/components/Dashboard/Fillaments/Fillaments.jsx deleted file mode 100644 index 891d808..0000000 --- a/src/components/Dashboard/Fillaments/Fillaments.jsx +++ /dev/null @@ -1,170 +0,0 @@ -// src/fillaments.js - -import React, { useEffect, useState, useReducer, useContext } from "react"; -import axios from "axios"; -import moment from "moment"; -import { useNavigate, useOutletContext } from "react-router-dom"; -import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, Drawer, message } from "antd"; -import { InfoCircleOutlined, EditOutlined, LoadingOutlined, ControlOutlined, PlusOutlined, CopyOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - -import NewFillament from "./NewFillament"; -import EditFillament from "./EditFillament"; - - -const { Title } = Typography; - -const Fillaments = () => { - const [messageApi, contextHolder] = message.useMessage(); - const initialState = { - error: null, - }; - - const [fillamentsData, setFillamentsData] = useState([]); - - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - const [newFillamentOpen, setNewFillamentOpen] = useState(false); - const [newFillament, setNewFillament] = useState(null); - - const [loading, setLoading] = useState(true); - - const [editFillamentOpen, setEditFillamentOpen] = useState(false); - const [editFillament, setEditFillament] = useState(null); - - const { token, logout } = useContext(AuthContext); - const { socket } = useContext(SocketContext); - - const navigate = useNavigate(); - - const fetchFillamentsData = async () => { - try { - const response = await axios.get("http://localhost:8080/fillaments", { - params: { - page: 1, - limit: 25, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setFillamentsData(response.data); - setLoading(false); - //setPagination({ ...pagination, total: response.data.totalItems }); // Update total count - } catch (err) { - console.error(err); - } - }; - - useEffect(() => { - // Fetch initial data - fetchFillamentsData(); - - }, [token]); - - // Column definitions - const columns = [ - { - title: "Name", - dataIndex: "name", - key: "name", - - }, - { - title: "Brand", - dataIndex: "brand", - key: "brand", - - }, - { - title: "Material", - dataIndex: "type", - key: "type", - - }, - { - title: "Price", - dataIndex: "price", - key: "type", - render: (price) => { - return "£" + price + " per kg"; - }, - }, - { - title: "Colour", - dataIndex: "color", - key: "color", - render: (color) => { - return ; - }, - }, - { - title: "Updated At", - dataIndex: "updated_at", - key: "updated_at", - render: (updated_at) => { - if (updated_at !== null) { - const formattedDate = moment(updated_at.$date).format( - "YYYY-MM-DD HH:mm:ss" - ); - return {formattedDate}; - } else { - return "n/a"; - } - }, - }, - { - title: "Actions", - key: "operation", - fixed: "right", - width: 100, - render: (text, record) => { - return ( - - - - }} - /> - - { setNewFillamentOpen(false); }}> - { setNewFillamentOpen(false); fetchFillamentsData(); }} reset={newFillamentOpen}/> - - { setEditFillamentOpen(false); }}> - {editFillament} - - - ); -}; - -export default Fillaments; diff --git a/src/components/Dashboard/Fillaments/NewFillament.jsx b/src/components/Dashboard/Fillaments/NewFillament.jsx deleted file mode 100644 index e4c8d56..0000000 --- a/src/components/Dashboard/Fillaments/NewFillament.jsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Divider, ColorPicker, Upload, Descriptions, Badge } from "antd"; -import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; - -const { Title, Text } = Typography; - -const initialNewFillamentForm = { - name: "", - brand: "", - type: "", - price: 0, - color: "#FFFFFF", - diameter: "1.75", - image: null, - url: "", - barcode: "", -}; - -const NewFillament = ({ onOk, reset }) => { - const [messageApi, contextHolder] = message.useMessage(); - - const [newFillamentLoading, setNewFillamentLoading] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - const [nextEnabled, setNextEnabled] = useState(false); - - const [newFillamentForm] = Form.useForm(); - const [newFillamentFormValues, setNewFillamentFormValues] = useState(initialNewFillamentForm); - - const [imageList, setImageList] = useState([]); - - const newFillamentFormUpdateValues = Form.useWatch([], newFillamentForm); - - const { token } = useContext(AuthContext); - - React.useEffect(() => { - newFillamentForm - .validateFields({ - validateOnly: true, - }) - .then(() => setNextEnabled(true)) - .catch(() => setNextEnabled(false)); - }, [newFillamentForm, newFillamentFormUpdateValues]); - - const summaryItems = [ - { - key: 'name', - label: 'Name', - children: newFillamentFormValues.name, - }, - { - key: 'brand', - label: 'Brand', - children: newFillamentFormValues.brand, - }, - { - key: 'type', - label: 'Material', - children: newFillamentFormValues.type, - }, - { - key: 'price', - label: 'Price', - children: "£" + newFillamentFormValues.price + " per kg", - }, - { - key: 'color', - label: 'Colour', - children: () - }, - { - key: 'diameter', - label: 'Diameter', - children: newFillamentFormValues.diameter + "mm", - }, - { - key: 'image', - label: 'Image', - children: (), - }, - { - key: 'url', - label: 'URL', - children: newFillamentFormValues.url, - }, - { - key: 'barcode', - label: 'Barcode', - children: newFillamentFormValues.barcode, - }, - ]; - - React.useEffect(() => { - console.log("reset changed") - if (reset) { - console.log("resetting") - newFillamentForm.resetFields(); - } - }, [reset, newFillamentForm]) - - - const handleNewFillament = async () => { - setNewFillamentLoading(true); - try { - await axios.post(`http://localhost:8080/fillaments`, newFillamentFormValues, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('New fillament created successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error creating new fillament: ' + error.message); - } finally { - setNewFillamentLoading(false); - } - }; - - const getBase64 = (file) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - }); - }; - - const handleImageUpload = async ({ file, fileList }) => { - console.log(fileList); - if (fileList.length == 0) { - setImageList(fileList) - newFillamentForm.setFieldsValue({ image: "" }); - return; - } - const base64 = await getBase64(file); - setNewFillamentFormValues((prevValues) => ({ - ...prevValues, - image: base64, - })); - fileList[0].name = "Fillament Image" - setImageList(fileList) - newFillamentForm.setFieldsValue({ image: base64 }); - }; - - const steps = [ - { - title: 'Required', - key: 'required', - content: ( - <> - - Required information: - - - - - - - - - - - - - { - if (!value) return '£'; - return `£${value}`; - }} step={0.01} style={{ width: "100%" }} addonAfter="per kg" /> - - - ), - }, - { - title: 'Optional', - key: 'optional', - content: ( - <> - - Optional information: - - - { - return "#" + color.toHex(); - }} - > - - - - - - - (Array.isArray(e) ? e : e && e.fileList)}> - false} // Prevent automatic upload - onChange={handleImageUpload} - > - - - - - } - /> - - - } - /> - - - ), - }, - { - title: 'Summary', - key: 'done', - content: ( - - - - ), - }, - ]; - - return ( - - {contextHolder} - - - - - - - - - New Fillament -
setNewFillamentFormValues((prevValues) => ({ - ...prevValues, - ...changedValues, - }))} - initialValues={initialNewFillamentForm} - > - {steps[currentStep].content} - - - - {currentStep < steps.length - 1 && ( - - )} - {currentStep === steps.length - 1 && ( - - )} - - -
- - - - - ); -}; - -export default NewFillament; diff --git a/src/components/Dashboard/GCodeFiles/EditGCodeFile.jsx b/src/components/Dashboard/GCodeFiles/EditGCodeFile.jsx deleted file mode 100644 index f283b7a..0000000 --- a/src/components/Dashboard/GCodeFiles/EditGCodeFile.jsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Skeleton, ColorPicker, Upload, Descriptions, Badge, Popconfirm } from "antd"; -import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; - -const { Title, Text } = Typography; - -const EditFillament = ({ id, onOk }) => { - const [messageApi, contextHolder] = message.useMessage(); - - const [dataLoading, setDataLoading] = useState(false); - const [editFillamentLoading, setEditFillamentLoading] = useState(false); - const [deleteFillamentLoading, setDeleteFillamentLoading] = useState(false); - - const [currentStep, setCurrentStep] = useState(0); - const [fillament, setFillament] = useState(null); - - const [imageList, setImageList] = useState([]); - const [image, setImage] = useState(""); - - const [editFillamentForm] = Form.useForm(); - const [editFillamentFormValues, setEditFillamentFormValues] = useState({}); - - const { token } = useContext(AuthContext); - - useEffect(() => { - // Fetch printer details when the component mounts - const fetchFillamentDetails = async () => { - if (id) { - try { - setDataLoading(true); - const response = await axios.get(`http://localhost:8080/fillaments/${id}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setDataLoading(false); - editFillamentForm.setFieldsValue(response.data); // Set form values with fetched data - setEditFillamentFormValues(response.data); - } catch (error) { - messageApi.error('Error fetching printer details:' + error.message); - } - } - }; - fetchFillamentDetails(); - }, [id, editFillamentForm]); - - const handleEditFillament = async () => { - setEditFillamentLoading(true); - // Exclude the 'online' field from the submission - try { - await axios.put(`http://localhost:8080/fillaments/${id}`, editFillamentFormValues, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Fillament details updated successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error updating fillament details: ' + error.message); - } finally { - setEditFillamentLoading(false); - } - }; - - const handleDeleteFillament = async () => { - setDeleteFillamentLoading(true); - try { - await axios.delete(`http://localhost:8080/fillaments/${id}`, "", { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Fillament deleted successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error updating fillament details: ' + error.message); - } finally { - setDeleteFillamentLoading(false); - } - }; - - const handleImageUpload = ({ file, onSuccess }) => { - const reader = new FileReader(); - reader.onload = (e) => { - console.log("Setting image buffer", e.target.result); - //setImage(e.target.result); - onSuccess("ok"); - }; - reader.readAsDataURL(file); - }; - return ( - <> - {contextHolder} - } size="large"> -
setEditFillamentFormValues((prevValues) => ({ - ...prevValues, - ...changedValues, - }))} - > - - - - - - - - - - - { - if (!value) return '£'; - return `£${value}`; - }} step={0.01} style={{ width: "100%" }} addonAfter="per kg" /> - - - { - return "#" + color.toHex(); - }} - > - - - - - - - - { setImageList(fileList) }} - > - - - - - } - /> - - - } - /> - - - - - - - - - - - - -
- - ); -}; - -export default EditFillament; diff --git a/src/components/Dashboard/GCodeFiles/GCodeFiles.jsx b/src/components/Dashboard/GCodeFiles/GCodeFiles.jsx deleted file mode 100644 index 35d86cc..0000000 --- a/src/components/Dashboard/GCodeFiles/GCodeFiles.jsx +++ /dev/null @@ -1,170 +0,0 @@ -// src/gcodefiles.js - -import React, { useEffect, useState, useReducer, useContext } from "react"; -import axios from "axios"; -import moment from "moment"; -import { useNavigate, useOutletContext } from "react-router-dom"; -import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, Drawer, message } from "antd"; -import { InfoCircleOutlined, EditOutlined, LoadingOutlined, ControlOutlined, PlusOutlined, CopyOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - -import NewGCodeFile from "./NewGCodeFile"; -import EditGCodeFile from "./EditGCodeFile"; - - -const { Title } = Typography; - -const GCodeFiles = () => { - const [messageApi, contextHolder] = message.useMessage(); - const initialState = { - error: null, - }; - - const [gcodeFilesData, setGCodeFilesData] = useState([]); - - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - const [newGCodeFileOpen, setNewGCodeFileOpen] = useState(false); - const [newGCodeFile, setNewGCodeFile] = useState(null); - - const [loading, setLoading] = useState(true); - - const [editGCodeFileOpen, setEditGCodeFileOpen] = useState(false); - const [editGCodeFile, setEditGCodeFile] = useState(null); - - const { token, logout } = useContext(AuthContext); - const { socket } = useContext(SocketContext); - - const navigate = useNavigate(); - - const fetchGCodeFilesData = async () => { - try { - const response = await axios.get("http://localhost:8080/gcodefiles", { - params: { - page: 1, - limit: 25, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setGCodeFilesData(response.data); - setLoading(false); - //setPagination({ ...pagination, total: response.data.totalItems }); // Update total count - } catch (err) { - console.error(err); - } - }; - - useEffect(() => { - // Fetch initial data - fetchGCodeFilesData(); - - }, [token]); - - // Column definitions - const columns = [ - { - title: "Name", - dataIndex: "name", - key: "name", - - }, - { - title: "Brand", - dataIndex: "brand", - key: "brand", - - }, - { - title: "Material", - dataIndex: "type", - key: "type", - - }, - { - title: "Price", - dataIndex: "price", - key: "type", - render: (price) => { - return "£" + price + " per kg"; - }, - }, - { - title: "Colour", - dataIndex: "color", - key: "color", - render: (color) => { - return ; - }, - }, - { - title: "Updated At", - dataIndex: "updated_at", - key: "updated_at", - render: (updated_at) => { - if (updated_at !== null) { - const formattedDate = moment(updated_at.$date).format( - "YYYY-MM-DD HH:mm:ss" - ); - return {formattedDate}; - } else { - return "n/a"; - } - }, - }, - { - title: "Actions", - key: "operation", - fixed: "right", - width: 100, - render: (text, record) => { - return ( - - - -
}} - /> - - { setNewGCodeFileOpen(false); }}> - { setNewGCodeFileOpen(false); fetchGCodeFilesData(); }} reset={newGCodeFileOpen}/> - - { setEditGCodeFileOpen(false); }}> - {editGCodeFile} - - - ); -}; - -export default GCodeFiles; diff --git a/src/components/Dashboard/GCodeFiles/NewGCodeFile.jsx b/src/components/Dashboard/GCodeFiles/NewGCodeFile.jsx deleted file mode 100644 index 52e9183..0000000 --- a/src/components/Dashboard/GCodeFiles/NewGCodeFile.jsx +++ /dev/null @@ -1,295 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, InputNumber, Button, message, Spin, Typography, Select, Flex, Steps, Col, Row, Divider, ColorPicker, Upload, Descriptions, Badge, } from "antd"; -import { LoadingOutlined, UploadOutlined, BarcodeOutlined, LinkOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; - -import GCodeFileIcon from '../../Icons/GCodeFileIcon'; - -import FillamentSelect from '../common/FillamentSelect'; - -const { Dragger } = Upload; - - -const { Title, Text } = Typography; - -const initialNewGCodeFileForm = { - name: "", - brand: "", - type: "", - price: 0, - color: "#FFFFFF", - diameter: "1.75", - image: null, - url: "", - barcode: "", -}; - -const NewGCodeFile = ({ onOk, reset }) => { - const [messageApi, contextHolder] = message.useMessage(); - - const [newGCodeFileLoading, setNewGCodeFileLoading] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - const [nextEnabled, setNextEnabled] = useState(false); - - const [newGCodeFileForm] = Form.useForm(); - const [newGCodeFileFormValues, setNewGCodeFileFormValues] = useState(initialNewGCodeFileForm); - - const [imageList, setImageList] = useState([]); - - const [gcode, setGCode] = useState(""); - - const newGCodeFileFormUpdateValues = Form.useWatch([], newGCodeFileForm); - - const { token } = useContext(AuthContext); - - React.useEffect(() => { - newGCodeFileForm - .validateFields({ - validateOnly: true, - }) - .then(() => setNextEnabled(true)) - .catch(() => setNextEnabled(false)); - }, [newGCodeFileForm, newGCodeFileFormUpdateValues]); - - const summaryItems = [ - { - key: 'name', - label: 'Name', - children: newGCodeFileFormValues.name, - }, - { - key: 'brand', - label: 'Brand', - children: newGCodeFileFormValues.brand, - }, - { - key: 'type', - label: 'Material', - children: newGCodeFileFormValues.type, - }, - { - key: 'price', - label: 'Price', - children: "£" + newGCodeFileFormValues.price + " per kg", - }, - { - key: 'color', - label: 'Colour', - children: () - }, - { - key: 'diameter', - label: 'Diameter', - children: newGCodeFileFormValues.diameter + "mm", - }, - { - key: 'image', - label: 'Image', - children: (), - }, - { - key: 'url', - label: 'URL', - children: newGCodeFileFormValues.url, - }, - { - key: 'barcode', - label: 'Barcode', - children: newGCodeFileFormValues.barcode, - }, - ]; - - React.useEffect(() => { - console.log("reset changed") - if (reset) { - console.log("resetting") - newGCodeFileForm.resetFields(); - } - }, [reset, newGCodeFileForm]) - - - const handleNewGCodeFile = async () => { - setNewGCodeFileLoading(true); - try { - await axios.post(`http://localhost:8080/gcodefiles`, newGCodeFileFormValues, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('New G Code file created successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error creating new gcode file: ' + error.message); - } finally { - setNewGCodeFileLoading(false); - } - }; - - const getBase64 = (file) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result); - reader.onerror = (error) => reject(error); - }); - }; - - const handleImageUpload = async ({ file, fileList }) => { - console.log(fileList); - if (fileList.length == 0) { - setImageList(fileList) - newGCodeFileForm.setFieldsValue({ image: "" }); - return; - } - const base64 = await getBase64(file); - setNewGCodeFileFormValues((prevValues) => ({ - ...prevValues, - image: base64, - })); - fileList[0].name = "GCodeFile Image" - setImageList(fileList) - newGCodeFileForm.setFieldsValue({ image: base64 }); - }; - - const steps = [ - { - title: 'Details', - key: 'details', - content: ( - <> - - Please provide the following information: - - - - - - - - - - ), - }, - { - title: 'Upload', - key: 'upload', - content: ( - <> - (Array.isArray(e) ? e : e && e.fileList)}> - -

- -

-

Click or drag .gcode or .g file here.

-

- Support for a single or bulk upload. Strictly prohibited from uploading company data or other - banned files. -

- -
-
- - ), - }, - { - title: 'Preview', - key: 'preview', - content: ( - <> - - - ), - }, - { - title: 'Summary', - key: 'done', - content: ( - - - - ), - }, - ]; - - return ( - - {contextHolder} -
- - - - - - - - New G Code File -
setNewGCodeFileFormValues((prevValues) => ({ - ...prevValues, - ...changedValues, - }))} - initialValues={initialNewGCodeFileForm} - > - {steps[currentStep].content} - - - - {currentStep < steps.length - 1 && ( - - )} - {currentStep === steps.length - 1 && ( - - )} - - -
- - - - - ); -}; - -export default NewGCodeFile; diff --git a/src/components/Dashboard/Overview.jsx b/src/components/Dashboard/Overview.jsx deleted file mode 100644 index d0f6e3e..0000000 --- a/src/components/Dashboard/Overview.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import DashboardLayout from './common/DashboardLayout'; -import axios from 'axios'; -import { useNavigate } from 'react-router-dom'; - -const Overview = ({ setToken }) => { - const [user, setUser] = useState(null); - const navigate = useNavigate(); - - useEffect(() => { - const fetchUserData = async () => { - const access_token = localStorage.getItem('access_token'); - if (access_token) { - try { - const response = await axios.get('http://localhost:8080/overview', { - headers: { - Authorization: `Bearer ${access_token}` - } - }); - //setUser(response.data); - } catch (err) { - console.error(err); - } - } - }; - fetchUserData(); - }, [setToken, navigate]); - - return ( -
-

Overview

- {user ? ( -
-

Welcome, {user.username}!

- -
- ) : ( -

Loading...

- )} -
- ); -}; - -export default Overview; diff --git a/src/components/Dashboard/PrintJobs/NewPrintJob.jsx b/src/components/Dashboard/PrintJobs/NewPrintJob.jsx deleted file mode 100644 index 3dcf272..0000000 --- a/src/components/Dashboard/PrintJobs/NewPrintJob.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, Button, message, Spin, Typography, Tag, Flex, Steps } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; - -const { Title } = Typography; - -const NewPrintJob = () => { - const navigate = useNavigate(); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [currentStep, setCurrentStep] = useState(false); - - - const { token } = useContext(AuthContext); - const handleFormSubmit = async (values) => { - setLoading(true); - // Exclude the 'online' field from the submission - const { online, remoteAddress, hostId, ...rest } = values; - try { - await axios.put(`http://localhost:8080/printers/${remoteAddress}`, rest, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - message.success('Printer details updated successfully'); - - } catch (error) { - message.error('Error updating printer details'); - } finally { - setLoading(false); - } - }; - - return ( -
- Select G-Code - -
- ); -}; - -export default NewPrintJob; diff --git a/src/components/Dashboard/PrintJobs/PrintJobs.jsx b/src/components/Dashboard/PrintJobs/PrintJobs.jsx deleted file mode 100644 index 0ca9bdc..0000000 --- a/src/components/Dashboard/PrintJobs/PrintJobs.jsx +++ /dev/null @@ -1,285 +0,0 @@ -// src/PrintJobs.js - -import React, { useEffect, useState, useReducer, useContext } from "react"; -import axios from "axios"; -import moment from "moment"; -import { useNavigate, useOutletContext } from "react-router-dom"; -import { Table, Typography, Badge, Button, Flex, Progress, Space, Tooltip, Modal, message } from "antd"; -import { InfoCircleOutlined, EditOutlined, ControlOutlined, PlusOutlined, CopyOutlined, LoadingOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - -import NewPrintJob from "./NewPrintJob"; - - -const { Title } = Typography; - -// Action types for reducer -const actionTypes = { - UPDATE_PRINTER_DATA: 'UPDATE_PRINTER_DATA', - FETCH_DATA_SUCCESS: 'FETCH_DATA_SUCCESS', - FETCH_DATA_FAILURE: 'FETCH_DATA_FAILURE', -}; - -// Reducer function to manage state updates -const reducer = (state, action) => { - switch (action.type) { - case actionTypes.UPDATE_PRINTER_DATA: - return { - ...state, - printerData: updatePrinterData(state.printerData, action.payload), - }; - case actionTypes.FETCH_DATA_SUCCESS: - return { - ...state, - printJobsData: action.payload, - error: null, - }; - case actionTypes.FETCH_DATA_FAILURE: - return { - ...state, - error: action.payload, - }; - default: - return state; - } -}; - -// Helper function to update printerData based on wsData -const updatePrinterData = (printerData, newData) => { - const updatedData = [...printerData]; // Copy current state - const existingIndex = updatedData.findIndex(printer => printer.remoteAddress === newData.remoteAddress); - - if (existingIndex !== -1) { - // Update existing entry - const existingEntry = updatedData[existingIndex]; - const updatedEntry = { ...existingEntry }; - - // Update only the parameters that exist in newData - for (const param in newData) { - if (newData.hasOwnProperty(param)) { - updatedEntry[param] = newData[param]; - } - } - - updatedData[existingIndex] = updatedEntry; - } else { - // Add new entry - updatedData.push(newData); - } - - return updatedData; -}; - -const PrintJobs = () => { - const [messageApi, contextHolder] = message.useMessage(); - const initialState = { - printJobsData: [], - error: null, - }; - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - const [state, dispatch] = useReducer(reducer, initialState); - const [newPrintJobOpen, setNewPrintJobOpen] = useState(false); - - const [loading, setLoading] = useState(true); - - const { token, logout } = useContext(AuthContext); - const { socket } = useContext(SocketContext); - - const navigate = useNavigate(); - - useEffect(() => { - // Fetch initial data - const fetchData = async () => { - try { - const response = await axios.get("http://localhost:8080/printjobs", { - params: { - page: 1, - limit: 25, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setLoading(false); - dispatch({ type: actionTypes.FETCH_DATA_SUCCESS, payload: response.data }); - //setPagination({ ...pagination, total: response.data.totalItems }); // Update total count - } catch (err) { - console.error(err); - } - }; - - fetchData(); - - }, [token]); - - useEffect(() => { - if (socket) { - socket.on("status", (statusUpdate) => { - console.log("Received status:", statusUpdate); - dispatch({ type: "UPDATE_PRINTER_DATA", payload: statusUpdate }); - }); - - return () => { - socket.off("status"); - }; - } - }, [socket]); - - - const handleTableChange = (pagination, filters, sorter) => { - setPagination(pagination); // Update pagination state on table change - }; - - // Column definitions - const columns = [ - { - title: "ID", - dataIndex: "id", - key: "id", - render: (text) => ( - - {text.slice(-8)} - - - -
}} - /> - - - - - - ); -}; - -export default PrintJobs; diff --git a/src/components/Dashboard/Printers/ControlPrinter.jsx b/src/components/Dashboard/Printers/ControlPrinter.jsx deleted file mode 100644 index fba1695..0000000 --- a/src/components/Dashboard/Printers/ControlPrinter.jsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { - useEffect, - useState, - useContext, - useReducer, - useRef, -} from "react"; -import axios from "axios"; -import { useLocation, useNavigate } from "react-router-dom"; -import { - Form, - Input, - Button, - message, - Spin, - Typography, - Tag, - Flex, - Col, - Row, - Dropdown, - Space, - Card, - Upload, -} from "antd"; -import { LoadingOutlined, UploadOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - -import DashboardTemperaturePanel from "../common/DashboardTemperaturePanel"; -import DashboardMovementPanel from "../common/DashboardMovementPanel"; - -const { Title } = Typography; - -// Action types for reducer -const actionTypes = { - UPDATE_PRINTER_DATA: "UPDATE_PRINTER_DATA", - FETCH_DATA_SUCCESS: "FETCH_DATA_SUCCESS", - FETCH_DATA_FAILURE: "FETCH_DATA_FAILURE", -}; - -// Reducer function to manage state updates -const reducer = (state, action) => { - switch (action.type) { - case actionTypes.UPDATE_PRINTER_DATA: - return { - ...state, - printerData: updatePrinterData(state.printerData, action.payload), - }; - case actionTypes.FETCH_DATA_SUCCESS: - return { - ...state, - printerData: action.payload, - error: null, - }; - case actionTypes.FETCH_DATA_FAILURE: - return { - ...state, - error: action.payload, - }; - default: - return state; - } -}; - -// Helper function to update printerData based on wsData -const updatePrinterData = (printerData, newData) => { - const updatedData = [...printerData]; // Copy current state - const existingIndex = updatedData.findIndex( - (printer) => printer.remoteAddress === newData.remoteAddress - ); - - if (existingIndex !== -1) { - // Update existing entry - updatedData[existingIndex] = { ...updatedData[existingIndex], ...newData }; - } else { - // Add new entry - updatedData.push(newData); - } - - return updatedData; -}; - -// Helper function to parse query parameters -const useQuery = () => { - return new URLSearchParams(useLocation().search); -}; - -const ControlPrinter = () => { - const initialState = { - printerData: [], - error: null, - }; - const query = useQuery(); - const remoteAddress = query.get("remoteAddress"); - const navigate = useNavigate(); - const [printer, setPrinter] = useState(null); - - const { token, logout } = useContext(AuthContext); - const { socket } = useContext(SocketContext); - - const [state, dispatch] = useReducer(reducer, initialState); - - useEffect(() => { - // Fetch printer details when the component mounts - const fetchPrinterDetails = async () => { - if (remoteAddress) { - try { - const response = await axios.get( - `http://localhost:8080/printers/${remoteAddress}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); - setPrinter(response.data); - } catch (error) { - message.error("Error fetching printer details"); - } - } - }; - fetchPrinterDetails(); - }, [token, logout, remoteAddress]); - - useEffect(() => { - const joinPrinterRoom = () => { - if (socket) { - socket.on("status", (statusUpdate) => { - console.log("Received status:", statusUpdate); - dispatch({ type: "UPDATE_WS_DATA", payload: statusUpdate }); - }); - - socket.emit("join", { remoteAddress }); - - return () => { - socket.off("status"); - socket.emit("leave", { remoteAddress }); - }; - } - }; - joinPrinterRoom(); - }, [socket, remoteAddress]); - - const sendCommand = (type, data) => { - const commandData = { - remoteAddress, - type, - data, - }; - socket.emit("command", commandData); - }; - - const handleUpload = (file) => { - const reader = new FileReader(); - reader.onload = () => { - sendCommand("writeToSD", { - filename: "test.g", - gcode: reader.result, - }); - message.success("File uploaded successfully"); - }; - reader.readAsText(file); - }; - - const handleUploadFileButtonClick = () => {}; - - const uploadProps = { - beforeUpload: (file) => { - const isGCODE = file.name.endsWith(".gcode"); - if (!isGCODE) { - message.error(`${file.name} is not a gcode file`); - } - return isGCODE || Upload.LIST_IGNORE; - }, - onChange: (info) => { - }, - }; - - return ( - - - { - handleUpload(file); - setTimeout(() => { - onSuccess("ok"); - }, 0); - }} - > - - - - - {printer ? ( - - - - - - - - - - - - - - Card content - - - - ) : ( - } size="large" /> - )} - - ); -}; - -export default ControlPrinter; diff --git a/src/components/Dashboard/Printers/EditPrinter.jsx b/src/components/Dashboard/Printers/EditPrinter.jsx deleted file mode 100644 index d03e85e..0000000 --- a/src/components/Dashboard/Printers/EditPrinter.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import DashboardLayout from '../common/DashboardLayout'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, Button, message, Spin, Typography, Tag, Flex, Popconfirm, Skeleton } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - - -const { Title } = Typography; - -const EditPrinter = ({ remoteAddress, onOk }) => { - const [messageApi, contextHolder] = message.useMessage(); - const navigate = useNavigate(); - const [editPrinterForm] = Form.useForm(); - const [editLoading, setEditLoading] = useState(false); - const [deleteLoading, setDeleteLoading] = useState(false); - const [printer, setPrinter] = useState(null); - - const { token, logout } = useContext(AuthContext); - - useEffect(() => { - // Fetch printer details when the component mounts - const fetchPrinterDetails = async () => { - if (remoteAddress) { - try { - const response = await axios.get(`http://localhost:8080/printers/${remoteAddress}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setPrinter(response.data); - editPrinterForm.setFieldsValue(response.data); // Set form values with fetched data - } catch (error) { - messageApi.error('Error fetching printer details:' + error.message); - } - } - }; - - fetchPrinterDetails(); - }, [remoteAddress, editPrinterForm]); - - const handleEdit = async (values) => { - setEditLoading(true); - // Exclude the 'online' field from the submission - const { online, remoteAddress, hostId, ...rest } = values; - try { - await axios.put(`http://localhost:8080/printers/${remoteAddress}`, rest, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Printer details updated successfully.'); - onOk(); - } catch (error) { - messageApi.error('Error updating printer details: ' + error.message); - } finally { - setEditLoading(false); - } - }; - - const handleDelete = async () => { - setDeleteLoading(true); - try { - await axios.delete(`http://localhost:8080/printers/${remoteAddress}`, "", { - headers: { - Authorization: `Bearer ${token}`, - } - }); - messageApi.success('Printer details updated successfully.'); - - } catch (error) { - messageApi.error('Error updating printer details: ' + error.message); - } finally { - setDeleteLoading(false); - } - }; - - return ( - <> - {contextHolder} - -
- - - - - - - - - - - - - - - - - - - -
- - ); -}; - -export default EditPrinter; diff --git a/src/components/Dashboard/Printers/Printers.jsx b/src/components/Dashboard/Printers/Printers.jsx deleted file mode 100644 index a13f0c6..0000000 --- a/src/components/Dashboard/Printers/Printers.jsx +++ /dev/null @@ -1,270 +0,0 @@ -// src/Printers.js - -import React, { useEffect, useState, useReducer, useContext } from "react"; -import axios from "axios"; -import moment from "moment"; -import { useNavigate, useOutletContext } from "react-router-dom"; -import { Table, Typography, Badge, Button, Flex, Progress, Space, Drawer } from "antd"; -import { InfoCircleOutlined, EditOutlined, ControlOutlined, LoadingOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../../Auth/AuthContext"; -import { SocketContext } from "../context/SocketContext"; - -import EditPrinter from "./EditPrinter" - -const { Title } = Typography; - -// Action types for reducer -const actionTypes = { - UPDATE_PRINTER_DATA: 'UPDATE_PRINTER_DATA', - FETCH_DATA_SUCCESS: 'FETCH_DATA_SUCCESS', - FETCH_DATA_FAILURE: 'FETCH_DATA_FAILURE', -}; - -// Reducer function to manage state updates -const reducer = (state, action) => { - switch (action.type) { - case actionTypes.UPDATE_PRINTER_DATA: - return { - ...state, - printerData: updatePrinterData(state.printerData, action.payload), - }; - case actionTypes.FETCH_DATA_SUCCESS: - return { - ...state, - printerData: action.payload, - error: null, - }; - case actionTypes.FETCH_DATA_FAILURE: - return { - ...state, - error: action.payload, - }; - default: - return state; - } -}; - -// Helper function to update printerData based on wsData -const updatePrinterData = (printerData, newData) => { - const updatedData = [...printerData]; // Copy current state - const existingIndex = updatedData.findIndex(printer => printer.remoteAddress === newData.remoteAddress); - - if (existingIndex !== -1) { - // Update existing entry - const existingEntry = updatedData[existingIndex]; - const updatedEntry = { ...existingEntry }; - - // Update only the parameters that exist in newData - for (const param in newData) { - if (newData.hasOwnProperty(param)) { - updatedEntry[param] = newData[param]; - } - } - - updatedData[existingIndex] = updatedEntry; - } else { - // Add new entry - updatedData.push(newData); - } - - return updatedData; -}; - -const Printers = () => { - const initialState = { - printerData: [], - error: null, - }; - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, - }); - - const [state, dispatch] = useReducer(reducer, initialState); - - const [loading, setLoading] = useState(true); - - const [editPrinterOpen, setEditPrinterOpen] = useState(false); - const [editPrinter, setEditPrinter] = useState(null); - - - const { token, logout } = useContext(AuthContext); - const { socket } = useContext(SocketContext); - - const navigate = useNavigate(); - - const fetchPrintersData = async () => { - try { - const response = await axios.get("http://localhost:8080/printers", { - params: { - page: 1, - limit: 25, - }, - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setLoading(false); - dispatch({ type: actionTypes.FETCH_DATA_SUCCESS, payload: response.data }); - //setPagination({ ...pagination, total: response.data.totalItems }); // Update total count - } catch (err) { - console.error(err); - } - }; - useEffect(() => { - // Fetch initial data - fetchPrintersData(); - }, [token]); - - useEffect(() => { - if (socket) { - socket.on("status", (statusUpdate) => { - console.log("Received status:", statusUpdate); - dispatch({ type: "UPDATE_PRINTER_DATA", payload: statusUpdate }); - }); - - return () => { - socket.off("status"); - }; - } - }, [socket]); - - - const handleTableChange = (pagination, filters, sorter) => { - setPagination(pagination); // Update pagination state on table change - }; - - const handleEdit = (remoteAddress) => { - setEditPrinter( { setEditPrinterOpen(false); fetchPrintersData(); }} />); - setEditPrinterOpen(true); - }; - - // Column definitions - const columns = [ - { - title: "Name", - dataIndex: "friendlyName", - key: "friendlyName", - }, - { - title: "Remote Addresss", - dataIndex: "remoteAddress", - key: "remoteAddress", - }, - { - title: "Host", - dataIndex: "hostId", - key: "hostId", - }, - { - title: 'Status', - key: 'status', - dataIndex: 'status', - render: (status) => { - let badgeStatus; - let badgeText; - - switch (status.type) { - case 'Online': - badgeStatus = 'success'; - badgeText = 'Online'; - break; - case 'Offline': - badgeStatus = 'default'; - badgeText = 'Offline'; - break; - case 'Initializing': - badgeStatus = 'warning'; - badgeText = 'Initializing'; - break; - case 'Printing': - badgeStatus = 'processing'; - badgeText = 'Printing'; - break; - case 'Processing': - badgeStatus = 'processing'; - badgeText = 'Processing'; - break; - case 'Idle': - badgeStatus = 'success'; - badgeText = 'Idle'; - break; - case 'Error': - badgeStatus = 'error'; - badgeText = 'Error'; - break; - default: - badgeStatus = 'default'; - badgeText = 'Unknown'; - } - - return ( - - ); - }, - }, - { - title: "Print Job", - dataIndex: "status", - key: "printJob", - width: "15%", - render: (status) => { - if (status.type == "Printing") { - return ( - - ); - } - }, - }, - { - title: "Connected At", - dataIndex: "connectedAt", - key: "connectedAt", - render: (connectedAt) => { - if (connectedAt !== null) { - const formattedDate = moment(connectedAt.$date).format( - "YYYY-MM-DD HH:mm:ss" - ); - return {formattedDate}; - } else { - return "n/a"; - } - }, - }, - { - title: "Actions", - key: "operation", - fixed: "right", - width: 100, - render: (text, record) => { - return ( - -
}} - /> - { setEditPrinterOpen(false); }}> - {editPrinter} - - - ); -}; - -export default Printers; diff --git a/src/components/Dashboard/Profile.jsx b/src/components/Dashboard/Profile.jsx deleted file mode 100644 index 88a42ae..0000000 --- a/src/components/Dashboard/Profile.jsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useEffect, useState, useContext } from 'react'; -import DashboardLayout from './common/DashboardLayout'; -import axios from 'axios'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Form, Input, Button, message, Spin, Typography, Tag, Flex } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; - -import { AuthContext } from "../Auth/AuthContext"; - -const { Title } = Typography; - -const Profile = () => { - - const navigate = useNavigate(); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [printer, setPrinter] = useState(null); - - const { token } = useContext(AuthContext); - - useEffect(() => { - // Fetch printer details when the component mounts - const fetchPrinterDetails = async () => { - if (token) { - try { - const response = await axios.get(`http://localhost:8080/profile`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - setPrinter(response.data); - form.setFieldsValue(response.data); // Set form values with fetched data - } catch (error) { - message.error('Error fetching printer details'); - } - } - }; - - fetchPrinterDetails(); - }, [form]); - - const handleFormSubmit = async (values) => { - setLoading(true); - // Exclude the 'online' field from the submission - const { online, remoteAddress, hostId, ...rest } = values; - try { - await axios.put(`http://localhost:8080/profile`, rest, { - headers: { - Authorization: `Bearer ${token}`, - } - }); - message.success('Profile updated successfully.'); - } catch (error) { - message.error('Error updating profile: ' + error.message); - } finally { - setLoading(false); - } - }; - - return ( -
- Edit Printer - {printer ? ( -
- - - - - - - - - - - {printer.online ? Online : Offline} - - - - - - - - - ) : ( - } size="large" /> - )} -
- ); -}; - -export default Profile; diff --git a/src/components/Dashboard/common/Dashboard.jsx b/src/components/Dashboard/common/Dashboard.jsx index 2f5f822..4dfe03f 100644 --- a/src/components/Dashboard/common/Dashboard.jsx +++ b/src/components/Dashboard/common/Dashboard.jsx @@ -1,14 +1,14 @@ // Dashboard.js -import React, { useEffect, useState } from 'react'; -import DashboardLayout from './DashboardLayout'; -import { useNavigate, Outlet } from 'react-router-dom'; +import React from 'react' +import DashboardLayout from './DashboardLayout' +import { Outlet } from 'react-router-dom' const Dashboard = () => { return ( - + - ); -}; + ) +} -export default Dashboard; +export default Dashboard diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index 6cf770b..326857d 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -1,37 +1,68 @@ // DashboardBreadcrumb.js -import React from 'react'; -import { Breadcrumb } from 'antd'; -import { Link, useLocation } from 'react-router-dom'; +import React from 'react' +import { Breadcrumb, Button, Flex, Space } from 'antd' +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons' const breadcrumbNameMap = { - '/dashboard': 'Dashboard', - '/dashboard/overview': 'Overview', - '/dashboard/printers': 'Printers', - '/dashboard/printers/control': 'Control Printer', - '/dashboard/printers/edit': 'Edit Printer', - '/dashboard/printjobs': 'Print Jobs', - '/dashboard/fillaments': 'Fillaments', - '/dashboard/gcodefiles': 'G Code Files', -}; + '/production': 'Production', + '/management': 'Management', + '/production/overview': 'Overview', + '/production/printers': 'Printers', + '/production/printers/control': 'Control', + '/production/printers/info': 'Info', + '/production/printjobs': 'Print Jobs', + '/production/printjobs/info': 'Info', + '/production/gcodefiles': 'G Code Files', + '/production/gcodefiles/info': 'Info', + '/management/filaments': 'Filaments', + '/management/filaments/info': 'Info', + '/management/parts': 'Parts', + '/management/parts/info': 'Info', + '/management/products': 'Products', + '/management/products/info': 'Info', + '/management/vendors': 'Vendors', + '/management/vendors/info': 'Info' +} const DashboardBreadcrumb = () => { - const location = useLocation(); - const pathSnippets = location.pathname.split('/').filter(i => i); - - const breadcrumbItems = pathSnippets.map((_, index) => { - const url = `/${pathSnippets.slice(0, index + 1).join('/')}`; - return ( - - {breadcrumbNameMap[url]} - - ); - }); - - return ( - - {breadcrumbItems} - - ); - }; - - export default DashboardBreadcrumb; \ No newline at end of file + const location = useLocation() + const navigate = useNavigate() + const pathSnippets = location.pathname.split('/').filter((i) => i) + + const breadcrumbItems = pathSnippets.map((_, index) => { + const url = `/${pathSnippets.slice(0, index + 1).join('/')}` + return { + title: ( + + {breadcrumbNameMap[url]} + + ), + key: url + } + }) + + return ( + + + + - - - - - - - - - - - - - - - - - - 0.1 - 1 - 10 - 100 - - - `${value} mm`} - parser={(value) => value?.replace(" mm", "")} - onChange={handlePosInputChange} - placeholder="10 mm" - name="posInput" - /> - `${value} mm/s`} - parser={(value) => value?.replace(" mm/s", "")} - onChange={handleRateInputChange} - placeholder="100 mm/s" - name="rateInput" - /> - - - - - ); -}; - -export default DashboardMovementPanel; diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index ab139fa..d262d1c 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -1,48 +1,193 @@ // DashboardNavigation.js -import React, { useContext, useEffect, useState } from "react"; -import { Layout, Menu, message } from "antd"; -import { UserOutlined, LogoutOutlined } from "@ant-design/icons"; -import { AuthContext } from "../../Auth/AuthContext"; -import { useNavigate } from "react-router-dom"; -import Title from "antd/es/skeleton/Title"; +import React, { useContext, useEffect, useState } from 'react' +import { + Menu, + Flex, + Tag, + Space, + Dropdown, + Button, + Tooltip, + Typography +} from 'antd' +import { + UserOutlined, + LogoutOutlined, + PrinterOutlined, + SettingOutlined, + ProductOutlined, + ShoppingCartOutlined, + PoundOutlined, + MailOutlined, + SearchOutlined, + BellOutlined, + DisconnectOutlined, + MenuOutlined +} from '@ant-design/icons' +import { AuthContext } from '../../Auth/AuthContext' +import { SocketContext } from '../context/SocketContext' +import { SpotlightContext } from '../context/SpotlightContext' +import { useNavigate, useLocation } from 'react-router-dom' +import { Header } from 'antd/es/layout/layout' -const { Header } = Layout; +const { Text } = Typography const DashboardNavigation = () => { - const { logout } = useContext(AuthContext); - const navigate = useNavigate(); + const { logout, userProfile } = useContext(AuthContext) + const { showSpotlight } = useContext(SpotlightContext) + const { socket } = useContext(SocketContext) + const [socketConnected, setSocketConnected] = useState(false) + const navigate = useNavigate() + const location = useLocation() + const [selectedKey, setSelectedKey] = useState('production') - const handleLogout = async (e) => { - logout(); - setTimeout(() => { - navigate('/login') - }, 500) - }; + useEffect(() => { + const pathParts = location.pathname.split('/').filter(Boolean) + if (pathParts.length > 1) { + setSelectedKey(pathParts[0]) // Return the section (production/management) + } + }, [location.pathname]) - const menuItems = [ + useEffect(() => { + setSocketConnected(socket?.connected) + }, [socket?.connected]) + + const mainMenuItems = [ { - key: "1", - label: "Profile", - icon: , + key: 'production', + label: 'Production', + icon: }, { - key: "2", - label: "Logout", - icon: , - onClick: () => {handleLogout();}, + key: 'inventory', + label: 'Inventory', + icon: }, - ]; + { + key: 'shop', + label: 'Commerce', + icon: + }, + { + key: 'finance', + label: 'Finance', + icon: + }, + + { + key: 'management', + label: 'Management', + icon: + } + ] + + const userMenuItems = { + items: [ + { + key: 'username', + label: userProfile?.username, + icon: , + disabled: true + }, + { + key: 'email', + label: userProfile?.email, + icon: , + disabled: true + }, + { + key: 'logout', + label: 'Logout', + icon: + } + ], + onClick: (key) => { + if (key === 'profile') { + navigate('/profile') + } else if (key === 'logout') { + logout() + } + } + } + + const handleMainMenuClick = ({ key }) => { + if (key === 'production') { + navigate('/production/overview') + } else if (key === 'inventory') { + navigate('/inventory/spools') + } else if (key === 'management') { + navigate('/management/filaments') + } + } return ( -
- +
+ + Logo + } + /> + + ⌘ ⇧ P} arrow={false}> + + + + {!socketConnected ? ( + + } + > + Disconnected + + + ) : null} + + + Dev + + + {userProfile ? ( + + + }> + {userProfile?.name ? userProfile.name : userProfile.username} + + + + ) : null} + +
- ); -}; + ) +} -export default DashboardNavigation; +export default DashboardNavigation diff --git a/src/components/Dashboard/common/DashboardPrintStatus.jsx b/src/components/Dashboard/common/DashboardPrintStatus.jsx deleted file mode 100644 index e0784d1..0000000 --- a/src/components/Dashboard/common/DashboardPrintStatus.jsx +++ /dev/null @@ -1,237 +0,0 @@ -// DashboardTemperaturePanel.js -import React, { useContext, useEffect, useState } from "react"; -import { - Layout, - Progress, - Typography, - Spin, - Flex, - Space, - Collapse, - InputNumber, - Button, -} from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; -import { SocketContext } from "../context/SocketContext"; -import styled from "styled-components"; - -const { Text, Link } = Typography; -const { Panel } = Collapse; -const { Header } = Layout; - -const CustomCollapse = styled(Collapse)` - .ant-collapse-header { - padding: 0 !important; - } - .ant-collapse-content-box { - padding-left: 0 !important; - padding-right: 0 !important; - padding-bottom: 0 !important; - } -`; - -const DashboardTemperaturePanel = ({ - remoteAddress, - showControls = true, - showMoreInfo = true, -}) => { - const [loading, setLoading] = React.useState(false); - const [hotEndTemperature, setHotEndTemperature] = useState(0); - const [heatedBedTemperature, setHeatedBedTemperature] = useState(0); - const [temperatureData, setTemperatureData] = useState(null); - const { socket } = useContext(SocketContext); - - useEffect(() => { - if (socket) { - socket.on("temperature", (data) => { - setTemperatureData(data.temperatures); - }); - return () => { - socket.off("temperature"); - }; - } - }, [socket]); - - const sendCommand = (type, data) => { - const commandData = { - remoteAddress, - type, - data, - }; - console.log(commandData); - socket.emit("command", commandData); - }; - - const handleSetTemperatureClick = (target, value) => { - sendCommand("setTemperature", { target, value }); - }; - - const moreInfoItems = [ - { - key: "1", - label: "More Temperature Data", - children: - - {temperatureData ? ( - <> - {typeof temperatureData.hotendPower !== "undefined" && ( - - - Hot End Power:{" "} - {Math.round((temperatureData.hotendPower / 127) * 100)}% - - - - )} - - {typeof temperatureData.bedPower !== "undefined" && ( - - - Bed Power:{" "} - {Math.round((temperatureData.bedPower / 127) * 100)}% - - - - )} - - {typeof temperatureData.pindaTemp !== "undefined" && ( - - Pinda Temp: {temperatureData.pindaTemp}°C - - )} - - {typeof temperatureData.ambiantActual !== "undefined" && ( - - Ambient Actual: {temperatureData.ambiantActual}°C - - )} - - ) : ( - - } size="large" /> - - )} - - }, - ]; - - return ( -
- {temperatureData ? ( - - {temperatureData.hotEnd && ( - - - Hot End: {temperatureData.hotEnd.current}°C /{" "} - {temperatureData.hotEnd.target}°C - - - {showControls === true && ( - - - `${value}°C`} - parser={(value) => value.replace("°C", "")} - onChange={(value) => setHotEndTemperature(value)} - /> - - - - - )} - - )} - - {temperatureData.heatedBed && ( - - - Heated Bed: {temperatureData.heatedBed.current}°C /{" "} - {temperatureData.heatedBed.target}°C - - - {showControls === true && ( - - - `${value}°C`} - parser={(value) => value.replace("°C", "")} - onChange={(value) => setHeatedBedTemperature(value)} - /> - - - - - )} - - )} - {showMoreInfo === true && ( - - )} - - ) : ( - - } size="large" /> - - )} -
- ); -}; - -export default DashboardTemperaturePanel; diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx index 8396b29..7f1c4d6 100644 --- a/src/components/Dashboard/common/DashboardSidebar.jsx +++ b/src/components/Dashboard/common/DashboardSidebar.jsx @@ -1,58 +1,57 @@ // Sidebar.js -import React from "react"; -import { Link } from "react-router-dom"; -import { Layout, Menu } from "antd"; +import React from 'react' +import { Link } from 'react-router-dom' +import { Layout, Menu } from 'antd' import { DashboardOutlined, PrinterOutlined, - PlayCircleOutlined, - FileOutlined, -} from "@ant-design/icons"; + PlayCircleOutlined +} from '@ant-design/icons' -import FillamentIcon from "../../Icons/FillamentIcon" -import GCodeFileIcon from "../../Icons/GCodeFileIcon"; +import FilamentIcon from '../../Icons/FilamentIcon' +import GCodeFileIcon from '../../Icons/GCodeFileIcon' -const { Sider } = Layout; +const { Sider } = Layout const Sidebar = () => { const items = [ { - key: "overview", - label: Overview, - icon: , + key: 'overview', + label: Overview, + icon: }, { - key: "printers", - label: Printers, - icon: , + key: 'printers', + label: Printers, + icon: }, { - key: "jobs", - label: Print Jobs, - icon: , + key: 'jobs', + label: Print Jobs, + icon: }, { - key: "fillaments", - label: Fillaments, - icon: , + key: 'filaments', + label: Filaments, + icon: }, { - key: "gcodefiles", - label: G Code Files, - icon: , - }, - ]; + key: 'gcodefiles', + label: G Code Files, + icon: + } + ] return ( - + - ); -}; + ) +} -export default Sidebar; +export default Sidebar diff --git a/src/components/Dashboard/common/DashboardTemperaturePanel.jsx b/src/components/Dashboard/common/DashboardTemperaturePanel.jsx deleted file mode 100644 index e0784d1..0000000 --- a/src/components/Dashboard/common/DashboardTemperaturePanel.jsx +++ /dev/null @@ -1,237 +0,0 @@ -// DashboardTemperaturePanel.js -import React, { useContext, useEffect, useState } from "react"; -import { - Layout, - Progress, - Typography, - Spin, - Flex, - Space, - Collapse, - InputNumber, - Button, -} from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; -import { SocketContext } from "../context/SocketContext"; -import styled from "styled-components"; - -const { Text, Link } = Typography; -const { Panel } = Collapse; -const { Header } = Layout; - -const CustomCollapse = styled(Collapse)` - .ant-collapse-header { - padding: 0 !important; - } - .ant-collapse-content-box { - padding-left: 0 !important; - padding-right: 0 !important; - padding-bottom: 0 !important; - } -`; - -const DashboardTemperaturePanel = ({ - remoteAddress, - showControls = true, - showMoreInfo = true, -}) => { - const [loading, setLoading] = React.useState(false); - const [hotEndTemperature, setHotEndTemperature] = useState(0); - const [heatedBedTemperature, setHeatedBedTemperature] = useState(0); - const [temperatureData, setTemperatureData] = useState(null); - const { socket } = useContext(SocketContext); - - useEffect(() => { - if (socket) { - socket.on("temperature", (data) => { - setTemperatureData(data.temperatures); - }); - return () => { - socket.off("temperature"); - }; - } - }, [socket]); - - const sendCommand = (type, data) => { - const commandData = { - remoteAddress, - type, - data, - }; - console.log(commandData); - socket.emit("command", commandData); - }; - - const handleSetTemperatureClick = (target, value) => { - sendCommand("setTemperature", { target, value }); - }; - - const moreInfoItems = [ - { - key: "1", - label: "More Temperature Data", - children: - - {temperatureData ? ( - <> - {typeof temperatureData.hotendPower !== "undefined" && ( - - - Hot End Power:{" "} - {Math.round((temperatureData.hotendPower / 127) * 100)}% - - - - )} - - {typeof temperatureData.bedPower !== "undefined" && ( - - - Bed Power:{" "} - {Math.round((temperatureData.bedPower / 127) * 100)}% - - - - )} - - {typeof temperatureData.pindaTemp !== "undefined" && ( - - Pinda Temp: {temperatureData.pindaTemp}°C - - )} - - {typeof temperatureData.ambiantActual !== "undefined" && ( - - Ambient Actual: {temperatureData.ambiantActual}°C - - )} - - ) : ( - - } size="large" /> - - )} - - }, - ]; - - return ( -
- {temperatureData ? ( - - {temperatureData.hotEnd && ( - - - Hot End: {temperatureData.hotEnd.current}°C /{" "} - {temperatureData.hotEnd.target}°C - - - {showControls === true && ( - - - `${value}°C`} - parser={(value) => value.replace("°C", "")} - onChange={(value) => setHotEndTemperature(value)} - /> - - - - - )} - - )} - - {temperatureData.heatedBed && ( - - - Heated Bed: {temperatureData.heatedBed.current}°C /{" "} - {temperatureData.heatedBed.target}°C - - - {showControls === true && ( - - - `${value}°C`} - parser={(value) => value.replace("°C", "")} - onChange={(value) => setHeatedBedTemperature(value)} - /> - - - - - )} - - )} - {showMoreInfo === true && ( - - )} - - ) : ( - - } size="large" /> - - )} -
- ); -}; - -export default DashboardTemperaturePanel; diff --git a/src/components/Dashboard/common/FillamentSelect.jsx b/src/components/Dashboard/common/FillamentSelect.jsx deleted file mode 100644 index 1986e4b..0000000 --- a/src/components/Dashboard/common/FillamentSelect.jsx +++ /dev/null @@ -1,150 +0,0 @@ -// FillamentSelect.js -import { TreeSelect, Badge } from 'antd'; -import React, { useEffect, useState, useContext, useRef } from 'react'; -import axios from 'axios'; -import { AuthContext } from "../../Auth/AuthContext"; - -const propertyOrder = ['diameter', 'type', 'brand']; - -const FillamentSelect = ({ onChange }) => { - const [fillamentsData, setFillamentsData] = useState([]); - const [fillamentsTreeData, setFillamentsTreeData] = useState([]); - const { token, logout } = useContext(AuthContext); - const tokenRef = useRef(token); - const [loading, setLoading] = useState(true); - - const fetchFillamentsData = async (property, filter) => { - console.log("Current ref", tokenRef); - try { - const response = await axios.get("http://localhost:8080/fillaments", { - params: { - ...filter, - property, - }, - headers: { - Authorization: `Bearer ${tokenRef.current}`, - }, - }); - setLoading(false); - return response.data; - //setPagination({ ...pagination, total: response.data.totalItems }); // Update total count - } catch (err) { - console.error(err); - } - }; - - const getFilter = (node) => { - var filter = {}; - var currentId = node.id; - while (currentId != 0) { - const currentNode = fillamentsTreeData.filter(treeData => treeData['id'] == currentId)[0] - filter[propertyOrder[currentNode.propertyId]] = currentNode.value.split("-")[0]; - currentId = currentNode.pId; - } - return filter; - } - - const generateFillamentTreeNodes = async (node = null) => { - if (!node) { - return; - } - - const fillamentData = await fetchFillamentsData(null, getFilter(node)); - - let newNodeList = []; - - for (var i = 0; i < fillamentData.length; i++) { - - const fillament = fillamentData[i]; - const random = Math.random().toString(36).substring(2, 6); - - const newNode = { - id: random, - pId: node.id, - value: fillament._id, - key: fillament._id, - title: (), - isLeaf: true - } - - newNodeList.push(newNode); - } - - setFillamentsTreeData(fillamentsTreeData.concat(newNodeList)) - console.log(newNodeList); - }; - - const generateFillamentCategoryTreeNodes = async (node = null) => { - var filter = {}; - console.log("Init node: ", node); - var propertyId = 0; - - if (!node) { - node = {}; - node.id = 0; - } else { - filter = getFilter(node); - propertyId = node.propertyId + 1; - } - - - const propertyName = propertyOrder[propertyId]; - - console.log("Next Property Id", propertyId) - console.log("Filter", filter); - - const propertyData = await fetchFillamentsData(propertyName, filter) - - let newNodeList = []; - - for (var i = 0; i < propertyData.length; i++) { - - const property = propertyData[i][propertyName]; - const random = Math.random().toString(36).substring(2, 6); - - const newNode = { - id: random, - pId: node.id, - value: property + "-" + random, - key: property + "-" + random, - propertyId: propertyId, - title: property, - isLeaf: false, - selectable: false - } - - newNodeList.push(newNode); - } - - setFillamentsTreeData(fillamentsTreeData.concat(newNodeList)) - console.log(newNodeList); - }; - - const handleFillamentsTreeLoad = async (node) => { - console.log(node); - if (node) { - if (node.propertyId != propertyOrder.length - 1) { - generateFillamentCategoryTreeNodes(node); - } else { - console.log("Generating printer node..."); - generateFillamentTreeNodes(node); // End of properties - } - } else { - generateFillamentCategoryTreeNodes(null); // First property - } - }; - - useEffect(() => { - if (fillamentsTreeData.length == 0) { - handleFillamentsTreeLoad(null) - } - }, [token]); - - return ( - - - - ); -}; - -export default FillamentSelect; \ No newline at end of file diff --git a/src/components/Dashboard/common/GCodePreview.jsx b/src/components/Dashboard/common/GCodePreview.jsx index cc73c8a..da4685f 100644 --- a/src/components/Dashboard/common/GCodePreview.jsx +++ b/src/components/Dashboard/common/GCodePreview.jsx @@ -1,67 +1,72 @@ -import * as GCodePreview from 'gcode-preview'; -import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import * as THREE from 'three'; +import * as GCodePreview from 'gcode-preview' +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react' +import * as THREE from 'three' -function GCodePreviewUI(props, ref) { +function GCodePreviewUI(props, ref, initialGCode) { const { - topLayerColor = '', - lastSegmentColor = '', - startLayer, - endLayer, - lineWidth - } = props; - const canvasRef = useRef(null); - const [preview, setPreview] = useState(); + topLayerColor = '', + lastSegmentColor = '', + startLayer, + endLayer, + lineWidth + } = props + const canvasRef = useRef(null) + const [preview, setPreview] = useState() const resizePreview = () => { - preview?.resize(); - }; + preview?.resize() + } useImperativeHandle(ref, () => ({ - getLayerCount() { - return preview?.layers.length; - }, - processGCode(gcode) { - preview?.processGCode(gcode); - } - })); + getLayerCount() { + return preview?.layers.length + }, + processGCode(gcode) { + preview?.processGCode(gcode) + } + })) useEffect(() => { - setPreview( - GCodePreview.init({ - canvas: canvasRef.current, - startLayer, - endLayer, - lineWidth, - topLayerColor: new THREE.Color(topLayerColor).getHex(), - lastSegmentColor: new THREE.Color(lastSegmentColor).getHex(), - buildVolume: { x: 250, y: 220, z: 150 }, - initialCameraPosition: [0, 400, 450], - allowDragNDrop: false - }) - ); + setPreview( + GCodePreview.init({ + canvas: canvasRef.current, + startLayer, + endLayer, + lineWidth, + topLayerColor: new THREE.Color(topLayerColor).getHex(), + lastSegmentColor: new THREE.Color(lastSegmentColor).getHex(), + buildVolume: { x: 250, y: 220, z: 150 }, + initialCameraPosition: [0, 400, 450], + allowDragNDrop: false + }) + ) - window.addEventListener('resize', resizePreview); + window.addEventListener('resize', resizePreview) - return () => { - window.removeEventListener('resize', resizePreview); - }; - }, []); + return () => { + window.removeEventListener('resize', resizePreview) + } + }, []) return ( -
- +
+ -
-
topLayerColor: {topLayerColor}
-
lastSegmentColor: {lastSegmentColor}
-
startLayer: {startLayer}
-
endLayer: {endLayer}
-
lineWidth: {lineWidth}
-
-
- ); +
+
topLayerColor: {topLayerColor}
+
lastSegmentColor: {lastSegmentColor}
+
startLayer: {startLayer}
+
endLayer: {endLayer}
+
lineWidth: {lineWidth}
+
+
+ ) } -export default forwardRef(GCodePreviewUI); - +export default forwardRef(GCodePreviewUI) diff --git a/src/components/Dashboard/context/SocketContext.js b/src/components/Dashboard/context/SocketContext.js index 5ee3a57..e89a7b4 100644 --- a/src/components/Dashboard/context/SocketContext.js +++ b/src/components/Dashboard/context/SocketContext.js @@ -1,75 +1,96 @@ // src/contexts/SocketContext.js -import React, { createContext, useEffect, useState, useContext, useRef } from "react"; -import io from "socket.io-client"; -import { message } from "antd"; -import { AuthContext } from "../../Auth/AuthContext"; +import React, { + createContext, + useEffect, + useState, + useContext, + useRef +} from 'react' +import io from 'socket.io-client' +import { message, notification } from 'antd' +import PropTypes from 'prop-types' +import { AuthContext } from '../../Auth/AuthContext' -const SocketContext = createContext(); +const SocketContext = createContext() const SocketProvider = ({ children }) => { - const { token } = useContext(AuthContext); - const socketRef = useRef(null); - const [connecting, setConnecting] = useState(false); - const [error, setError] = useState(null); - const [messageApi, contextHolder] = message.useMessage(); + const { token } = useContext(AuthContext) + const socketRef = useRef(null) + const [connecting, setConnecting] = useState(false) + const [error, setError] = useState(null) + const [messageApi, contextHolder] = message.useMessage() + const [notificationApi] = notification.useNotification() useEffect(() => { if (token) { - console.log("Token is available, connecting to web socket server..."); + console.log('Token is available, connecting to web socket server...') - const newSocket = io("http://localhost:5050", { + const newSocket = io('http://localhost:8081', { reconnectionAttempts: 3, timeout: 3000, - query: { token }, - }); + auth: { token: token } + }) - setConnecting(true); + setConnecting(true) - newSocket.on("connect", () => { - console.log("Socket connected"); - setConnecting(false); - setError(null); - }); + newSocket.on('connect', () => { + console.log('Socket connected') + setConnecting(false) + setError(null) + }) - newSocket.on("disconnect", () => { - console.log("Socket disconnected"); - setError("Socket disconnected"); - }); + newSocket.on('disconnect', () => { + console.log('Socket disconnected') + setError('Socket disconnected') + }) - newSocket.on("connect_error", (err) => { - console.error("Socket connection error:", err); - messageApi.error("Socket connection error: " + err.message); - setError("Socket connection error"); - }); + newSocket.on('connect_error', (err) => { + console.error('Socket connection error:', err) + messageApi.error('Socket connection error: ' + err.message) + setError('Socket connection error') + }) - newSocket.on("error", (err) => { - console.error("Socket error:", err); - setError("Socket error"); - }); + newSocket.on('bridge.notification', (data) => { + notificationApi[data.type]({ + title: data.title, + message: data.message + }) + }) - socketRef.current = newSocket; + newSocket.on('error', (err) => { + console.error('Socket error:', err) + setError('Socket error') + }) + + socketRef.current = newSocket // Clean up function return () => { if (socketRef.current) { - console.log("Cleaning up socket connection..."); - socketRef.current.disconnect(); - socketRef.current = null; + console.log('Cleaning up socket connection...') + socketRef.current.disconnect() + socketRef.current = null } - }; + } } else if (!token && socketRef.current) { - console.log("Token not available, disconnecting socket..."); - socketRef.current.disconnect(); - socketRef.current = null; + console.log('Token not available, disconnecting socket...') + socketRef.current.disconnect() + socketRef.current = null } - }, [token, messageApi]); + }, [token, messageApi]) return ( - + {contextHolder} {children} - ); -}; + ) +} -export { SocketContext, SocketProvider }; \ No newline at end of file +SocketProvider.propTypes = { + children: PropTypes.node.isRequired +} + +export { SocketContext, SocketProvider } diff --git a/src/components/Icons/FillamentIcon.jsx b/src/components/Icons/FillamentIcon.jsx deleted file mode 100644 index 3b70687..0000000 --- a/src/components/Icons/FillamentIcon.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import Icon from '@ant-design/icons'; -import { ReactComponent as CustomIconSvg } from '../../assets/icons/fillamenticon.svg'; - -const FillamentIcon = (props) => ( - -); - -export default FillamentIcon; \ No newline at end of file diff --git a/src/components/Icons/GCodeFileIcon.jsx b/src/components/Icons/GCodeFileIcon.jsx index e3484e9..f686279 100644 --- a/src/components/Icons/GCodeFileIcon.jsx +++ b/src/components/Icons/GCodeFileIcon.jsx @@ -1,9 +1,7 @@ -import React from 'react'; -import Icon from '@ant-design/icons'; -import { ReactComponent as CustomIconSvg } from '../../assets/icons/gcodefileicon.svg'; +import React from 'react' +import Icon from '@ant-design/icons' +import { ReactComponent as CustomIconSvg } from '../../assets/icons/gcodefileicon.svg' -const GCodeFileIcon = (props) => ( - -); +const GCodeFileIcon = (props) => -export default GCodeFileIcon; \ No newline at end of file +export default GCodeFileIcon diff --git a/src/components/Icons/PassKeysIcon.jsx b/src/components/Icons/PassKeysIcon.jsx index bff5c84..cfdc38e 100644 --- a/src/components/Icons/PassKeysIcon.jsx +++ b/src/components/Icons/PassKeysIcon.jsx @@ -1,9 +1,7 @@ -import React from 'react'; -import Icon from '@ant-design/icons'; -import { ReactComponent as CustomIconSvg } from '../../assets/icons/passkeysicon.svg'; +import React from 'react' +import Icon from '@ant-design/icons' +import { ReactComponent as CustomIconSvg } from '../../assets/icons/passkeysicon.svg' -const PassKeysIcon = (props) => ( - -); +const PassKeysIcon = (props) => -export default PassKeysIcon; \ No newline at end of file +export default PassKeysIcon diff --git a/src/components/PrivateRoute.jsx b/src/components/PrivateRoute.jsx index e1492c0..660837a 100644 --- a/src/components/PrivateRoute.jsx +++ b/src/components/PrivateRoute.jsx @@ -1,8 +1,28 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; +// PrivateRoute.js +import PropTypes from 'prop-types' +import React, { useContext } from 'react' +//import { Navigate } from 'react-router-dom' +import { AuthContext } from './Auth/AuthContext' const PrivateRoute = ({ component: Component }) => { - return localStorage.getItem('access_token') ? : ; -}; + const { authenticated, loading, showSessionExpiredModal } = + useContext(AuthContext) -export default PrivateRoute; + // Show loading state while auth state is being determined + if (loading) { + return
Loading...
+ } + + // Redirect to login if not authenticated + return authenticated || showSessionExpiredModal ? ( + + ) : ( + + ) +} + +PrivateRoute.propTypes = { + component: PropTypes.func.isRequired +} + +export default PrivateRoute diff --git a/src/components/PublicRoute.jsx b/src/components/PublicRoute.jsx index 8877e35..ef019ff 100644 --- a/src/components/PublicRoute.jsx +++ b/src/components/PublicRoute.jsx @@ -1,8 +1,23 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; +// PublicRoute.js +import PropTypes from 'prop-types' +import React, { useContext } from 'react' +import { Navigate } from 'react-router-dom' +import { AuthContext } from './Auth/AuthContext' const PublicRoute = ({ component: Component }) => { - return !localStorage.getItem('access_token') ? : ; -}; + const { authenticated, loading } = useContext(AuthContext) -export default PublicRoute; + // Show loading state while auth state is being determined + if (loading) { + return
Loading...
+ } + + // Redirect to login if not authenticated + return !authenticated ? : +} + +PublicRoute.propTypes = { + component: PropTypes.func.isRequired +} + +export default PublicRoute diff --git a/src/index.css b/src/index.css index ec2585e..826eea2 100644 --- a/src/index.css +++ b/src/index.css @@ -11,3 +11,7 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +.ant-modal-mask { + backdrop-filter: blur(3px); +} diff --git a/src/index.js b/src/index.js index 1ee10af..f49c8e5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,17 @@ -import reportWebVitals from "./reportWebVitals"; -import React from "react"; -import ReactDOM from "react-dom/client"; -import FarmControlApp from "./App"; -import "./index.css"; +import reportWebVitals from './reportWebVitals' +import React from 'react' +import ReactDOM from 'react-dom/client' +import FarmControlApp from './App' +import './index.css' -const root = ReactDOM.createRoot(document.getElementById("root")); +const root = ReactDOM.createRoot(document.getElementById('root')) root.render( -); +) // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +reportWebVitals() diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js index 5253d3a..dc6ff07 100644 --- a/src/reportWebVitals.js +++ b/src/reportWebVitals.js @@ -1,13 +1,13 @@ -const reportWebVitals = onPerfEntry => { +const reportWebVitals = (onPerfEntry) => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); + getCLS(onPerfEntry) + getFID(onPerfEntry) + getFCP(onPerfEntry) + getLCP(onPerfEntry) + getTTFB(onPerfEntry) + }) } -}; +} -export default reportWebVitals; +export default reportWebVitals diff --git a/src/setupTests.js b/src/setupTests.js index 8f2609b..52aaef1 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import '@testing-library/jest-dom'