diff --git a/app/package-lock.json b/app/package-lock.json index f45a024..91c994b 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -47,6 +47,7 @@ "react-router-dom": "^7.4.0", "react-scripts": "5.0.1", "react-toastify": "^10.0.5", + "recharts": "^3.2.1", "sass": "^1.78.0", "socket.io-client": "^4.8.1", "three": "^0.168.0", @@ -3856,6 +3857,42 @@ "integrity": "sha512-1P/8pqXtwJSVseLBtO0AOdqQe1XnhPFTVvG9FMORFVFunYqkKXxULgni8x3fsutkQisyY+X7YQoIMLvZR4lKFA==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3966,6 +4003,18 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@stitches/react": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", @@ -6458,6 +6507,69 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/d3-voronoi": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", @@ -6810,6 +6922,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/webxr": { "version": "0.5.20", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz", @@ -9622,6 +9740,33 @@ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", "license": "BSD-3-Clause" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-geo": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", @@ -9631,6 +9776,112 @@ "d3-array": "1" } }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-voronoi": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", @@ -9747,6 +9998,12 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -10527,6 +10784,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -12938,6 +13205,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -18277,6 +18553,38 @@ "react": "^18.0.0" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -18471,6 +18779,49 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/recharts/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -18514,6 +18865,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -18673,6 +19039,12 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -20762,6 +21134,12 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyqueue": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", @@ -21529,6 +21907,40 @@ "extsprintf": "^1.2.0" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/app/package.json b/app/package.json index b7aa83f..ffeff66 100644 --- a/app/package.json +++ b/app/package.json @@ -42,6 +42,7 @@ "react-router-dom": "^7.4.0", "react-scripts": "5.0.1", "react-toastify": "^10.0.5", + "recharts": "^3.2.1", "sass": "^1.78.0", "socket.io-client": "^4.8.1", "three": "^0.168.0", diff --git a/app/src/SimulationDashboard/ControlPanel.tsx b/app/src/SimulationDashboard/ControlPanel.tsx new file mode 100644 index 0000000..06b92ea --- /dev/null +++ b/app/src/SimulationDashboard/ControlPanel.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +interface ControlPanelProps { + editMode: boolean; + setEditMode: (mode: boolean) => void; + addBlock: () => void; + showDataModelPanel: boolean; + setShowDataModelPanel: (show: boolean) => void; +} + +const ControlPanel: React.FC = ({ + editMode, + setEditMode, + addBlock, + showDataModelPanel, + setShowDataModelPanel, +}) => { + return ( +
+ + {editMode && ( + <> + + + + )} +
+ ); +}; + +export default ControlPanel; diff --git a/app/src/SimulationDashboard/DashboardEditor.tsx b/app/src/SimulationDashboard/DashboardEditor.tsx new file mode 100644 index 0000000..8f579ef --- /dev/null +++ b/app/src/SimulationDashboard/DashboardEditor.tsx @@ -0,0 +1,474 @@ +import React, { useState, useRef, useEffect } from "react"; +// import "./style/style.css"; +import type { Block, DataModel, GraphTypes, Position, UIType } from "../types/simulationDashboard"; +import { dataModelManager } from "./data/dataModel"; +import ControlPanel from "./ControlPanel"; +import SwapModal from "./SwapModal"; +import DataModelPanel from "./components/models/DataModelPanel"; + +import { addBlock } from "./functions/block/addBlock"; +import { addElement } from "./functions/element/addElement"; +import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize"; +import { + updateBlockStyle, + updateBlockSize, + updateBlockPosition, + updateBlockPositionType, + updateBlockZIndex, +} from "./functions/block/updateBlock"; +import { + updateElementStyle, + updateElementSize, + updateElementPosition, + updateElementPositionType, + updateElementZIndex, + updateElementData, + updateGraphData, + updateGraphTitle, +} from "./functions/element/updateElement"; +import { swapElements } from "./functions/element/swapElements"; +import { + handleElementDragStart, + handleElementResizeStart, + handleBlockResizeStart, + handleSwapStart, + handleSwapTarget, + handleBlockClick, + handleElementClick, +} from "./functions/eventHandlers"; +import BlockGrid from "./components/block/BlockGrid"; +import ElementDropdown from "./components/element/ElementDropdown"; +import BlockEditor from "./components/block/BlockEditor"; +import ElementEditor from "./components/element/ElementEditor"; +import { handleBlockDragStart } from "./functions/block/handleBlockDragStart"; + +const DashboardEditor: React.FC = () => { + const [blocks, setBlocks] = useState([]); + console.log('blocks: ', blocks); + const [editMode, setEditMode] = useState(false); + const [selectedBlock, setSelectedBlock] = useState(null); + const [selectedElement, setSelectedElement] = useState(null); + const [draggingElement, setDraggingElement] = useState(null); + const [resizingElement, setResizingElement] = useState(null); + const [resizingBlock, setResizingBlock] = useState(null); + const [resizeStart, setResizeStart] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + const [showSwapUI, setShowSwapUI] = useState(false); + const [swapSource, setSwapSource] = useState(null); + const [dataModel, setDataModel] = useState(dataModelManager.getDataSnapshot()); + const [showDataModelPanel, setShowDataModelPanel] = useState(false); + const [showElementDropdown, setShowElementDropdown] = useState(null); + const [dropDownPosition, setDropDownPosition] = useState({ top: 0, left: 0 }); + const [draggingBlock, setDraggingBlock] = useState(null); + const [elementDragOffset, setElementDragOffset] = useState({ x: 0, y: 0 }); + const [blockDragOffset, setBlockDragOffset] = useState({ x: 0, y: 0 }); + + const editorRef = useRef(null); + const blockEditorRef = useRef(null); + const elementEditorRef = useRef(null); + const dropdownRef = useRef(null); + const blockRef = useRef(null); + + const currentBlock = blocks.find((b) => b.id === selectedBlock); + const currentElement = currentBlock?.elements.find((el) => el.id === selectedElement); + + // Subscribe to data model changes + useEffect(() => { + const handleDataChange = (): void => { + setDataModel(dataModelManager.getDataSnapshot()); + }; + + const keys = dataModelManager.getAvailableKeys(); + const subscriptions: Array<[string, () => void]> = []; + + keys.forEach((key) => { + const callback = () => handleDataChange(); + dataModelManager.subscribe(key, callback); + subscriptions.push([key, callback]); + }); + + const interval = setInterval(() => { + const currentKeys = dataModelManager.getAvailableKeys(); + const newKeys = currentKeys.filter((key) => !keys.includes(key)); + + newKeys.forEach((key) => { + const callback = () => handleDataChange(); + dataModelManager.subscribe(key, callback); + subscriptions.push([key, callback]); + }); + }, 1000); + + return () => { + subscriptions.forEach(([key, callback]) => { + dataModelManager.unsubscribe(key, callback); + }); + clearInterval(interval); + }; + }, []); + + // Click outside handler + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + const target = event.target as Node; + + const isInsideBlockEditor = blockEditorRef.current?.contains(target); + const isInsideElementEditor = elementEditorRef.current?.contains(target); + const isInsideDropdown = dropdownRef.current?.contains(target); + const isInsideEditor = editorRef.current?.contains(target); + + if (!isInsideEditor) { + setSelectedBlock(null); + setSelectedElement(null); + setShowSwapUI(false); + return; + } + + if ( + isInsideEditor && + !isInsideBlockEditor && + !isInsideElementEditor && + !isInsideDropdown + ) { + const clickedElement = event.target as HTMLElement; + const isBlock = clickedElement.closest("[data-block-id]"); + const isElement = clickedElement.closest("[data-element-id]"); + const isButton = clickedElement.closest("button"); + const isResizeHandle = clickedElement.closest(".resize-handle"); + + if (!isBlock && !isElement && !isButton && !isResizeHandle) { + setSelectedBlock(null); + setSelectedElement(null); + setShowSwapUI(false); + setShowElementDropdown(null); + } + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [selectedBlock, selectedElement]); + + // Drag and drop handler + useEffect(() => { + const handleMouseMove = (e: MouseEvent): void => { + // Element dragging - use elementDragOffset + if (draggingElement && selectedBlock && currentElement?.positionType === "absolute") { + const blockElement = document.querySelector( + `[data-block-id="${selectedBlock}"]` + ) as HTMLElement; + if (blockElement) { + const blockRect = blockElement.getBoundingClientRect(); + const newX = e.clientX - blockRect.left - elementDragOffset.x; // Use elementDragOffset + const newY = e.clientY - blockRect.top - elementDragOffset.y; // Use elementDragOffset + + updateElementPosition( + selectedBlock, + draggingElement, + { + x: Math.max(0, Math.min(blockRect.width - 50, newX)), + y: Math.max(0, Math.min(blockRect.height - 30, newY)), + }, + setBlocks, + blocks + ); + } + } + + // Block dragging - use blockDragOffset + if ( + draggingBlock && + currentBlock?.positionType && + (currentBlock.positionType === "absolute" || currentBlock.positionType === "fixed") + ) { + const editorElement = editorRef.current; + if (editorElement) { + const editorRect = editorElement.getBoundingClientRect(); + const newX = e.clientX - editorRect.left - blockDragOffset.x; // Use blockDragOffset + const newY = e.clientY - editorRect.top - blockDragOffset.y; // Use blockDragOffset + + updateBlockPosition( + draggingBlock, + { + x: Math.max( + 0, + Math.min(editorRect.width - (currentBlock.size?.width || 400), newX) + ), + y: Math.max( + 0, + Math.min( + editorRect.height - (currentBlock.size?.height || 300), + newY + ) + ), + }, + setBlocks, + blocks + ); + } + } + + if ((resizingElement || resizingBlock) && resizeStart) { + if (resizingElement && selectedBlock) { + const deltaX = e.clientX - resizeStart.x; + const deltaY = e.clientY - resizeStart.y; + + const newWidth = Math.max(100, resizeStart.width + deltaX); + const newHeight = Math.max(50, resizeStart.height + deltaY); + + updateElementSize( + selectedBlock, + resizingElement, + { + width: newWidth, + height: newHeight, + }, + setBlocks, + blocks + ); + } else if (resizingBlock) { + const deltaX = e.clientX - resizeStart.x; + const deltaY = e.clientY - resizeStart.y; + + const currentBlock = blocks.find((b) => b.id === resizingBlock); + const minSize = currentBlock + ? calculateMinBlockSize(currentBlock) + : { width: 300, height: 200 }; + + const newWidth = Math.max(minSize.width, resizeStart.width + deltaX); + const newHeight = Math.max(minSize.height, resizeStart.height + deltaY); + + updateBlockSize( + resizingBlock, + { + width: newWidth, + height: newHeight, + }, + setBlocks, + blocks + ); + } + } + }; + + const handleMouseUp = (): void => { + setDraggingElement(null); + setResizingElement(null); + setDraggingBlock(null); + setResizingBlock(null); + setResizeStart(null); + }; + + if (draggingElement || draggingBlock || resizingElement || resizingBlock) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [ + draggingElement, + resizingElement, + draggingBlock, + resizingBlock, + elementDragOffset, + blockDragOffset, + selectedBlock, + currentElement, + resizeStart, + currentBlock, + blocks, + ]); + + // Update dropdown position when showElementDropdown changes + useEffect(() => { + if (showElementDropdown && blockRef.current) { + const rect = blockRef.current.getBoundingClientRect(); + setDropDownPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + }); + } + }, [showElementDropdown]); + + return ( +
+ addBlock(setBlocks, blocks)} + showDataModelPanel={showDataModelPanel} + setShowDataModelPanel={setShowDataModelPanel} + /> + + + handleBlockClick( + blockId, + event, + editMode, + setSelectedBlock, + setSelectedElement, + setShowSwapUI + ) + } + handleElementClick={(blockId, elementId, event) => + handleElementClick( + blockId, + elementId, + event, + editMode, + setSelectedElement, + setSelectedBlock, + setShowSwapUI, + setShowElementDropdown + ) + } + handleElementDragStart={(elementId, event) => + handleElementDragStart( + elementId, + event, + currentElement, + setDraggingElement, + setElementDragOffset + ) + } + handleElementResizeStart={(elementId, event) => + handleElementResizeStart(elementId, event, setResizingElement, setResizeStart) + } + handleBlockResizeStart={(blockId, event) => + handleBlockResizeStart(blockId, event, setResizingBlock, setResizeStart) + } + handleSwapStart={(elementId, event) => + handleSwapStart(elementId, event, setSwapSource, setShowSwapUI) + } + handleSwapTarget={(elementId, event) => + handleSwapTarget( + elementId, + event, + swapSource, + selectedBlock, + swapElements, + setBlocks, + blocks, + setShowSwapUI, + setSwapSource + ) + } + handleBlockDragStart={(blockId, event) => + handleBlockDragStart(blockId, event, setDraggingBlock, setBlockDragOffset) + } + setShowElementDropdown={setShowElementDropdown} + showElementDropdown={showElementDropdown} + blockRef={blockRef} + /> + + + addElement( + blockId, + type as UIType, + graphType as GraphTypes, + setBlocks, + blocks, + setShowElementDropdown + ) + } + dropdownRef={dropdownRef} + /> + + {showDataModelPanel && editMode && ( + + )} + + {selectedBlock && editMode && !selectedElement && currentBlock && ( + + updateBlockStyle(blockId, style, setBlocks, blocks) + } + updateBlockSize={(blockId, size) => + updateBlockSize(blockId, size, setBlocks, blocks) + } + updateBlockPosition={(blockId, position) => + updateBlockPosition(blockId, position, setBlocks, blocks) + } + updateBlockPositionType={(blockId, positionType) => + updateBlockPositionType(blockId, positionType, setBlocks, blocks) + } + updateBlockZIndex={(blockId, zIndex) => + updateBlockZIndex(blockId, zIndex, setBlocks, blocks) + } + /> + )} + + {selectedElement && editMode && selectedBlock && currentElement && ( + + updateElementStyle(blockId, elementId, style, setBlocks, blocks) + } + updateElementSize={(blockId, elementId, size) => + updateElementSize(blockId, elementId, size, setBlocks, blocks) + } + updateElementPosition={(blockId, elementId, position) => + updateElementPosition(blockId, elementId, position, setBlocks, blocks) + } + updateElementPositionType={(blockId, elementId, positionType) => + updateElementPositionType( + blockId, + elementId, + positionType, + setBlocks, + blocks + ) + } + updateElementZIndex={(blockId, elementId, zIndex) => + updateElementZIndex(blockId, elementId, zIndex, setBlocks, blocks) + } + updateElementData={(blockId, elementId, updates) => + updateElementData(blockId, elementId, updates, setBlocks, blocks) + } + updateGraphData={(blockId, elementId, newData) => + updateGraphData(blockId, elementId, newData, setBlocks, blocks) + } + updateGraphTitle={(blockId, elementId, title) => + updateGraphTitle(blockId, elementId, title, setBlocks, blocks) + } + setSwapSource={setSwapSource} + setShowSwapUI={setShowSwapUI} + dataModelManager={dataModelManager} + /> + )} + + {showSwapUI && ( + + )} +
+ ); +}; + +export default DashboardEditor; diff --git a/app/src/SimulationDashboard/SwapModal.tsx b/app/src/SimulationDashboard/SwapModal.tsx new file mode 100644 index 0000000..879b765 --- /dev/null +++ b/app/src/SimulationDashboard/SwapModal.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +interface SwapModalProps { + setShowSwapUI: (show: boolean) => void; + setSwapSource: (source: string | null) => void; +} + +const SwapModal: React.FC = ({ setShowSwapUI, setSwapSource }) => { + return ( +
+

Swap Elements

+

Click on another element to swap positions with the selected element

+ +
+ ); +}; + +export default SwapModal; diff --git a/app/src/SimulationDashboard/components/block/BlockComponent.tsx b/app/src/SimulationDashboard/components/block/BlockComponent.tsx new file mode 100644 index 0000000..b6498a7 --- /dev/null +++ b/app/src/SimulationDashboard/components/block/BlockComponent.tsx @@ -0,0 +1,154 @@ +import type { RefObject } from "react"; +import type { Block } from "../../../types/simulationDashboard"; +import ElementComponent from "../element/ElementComponent"; + +interface BlockComponentProps { + block: Block; + editMode: boolean; + selectedBlock: string | null; + selectedElement: string | null; + showSwapUI: boolean; + swapSource: string | null; + calculateMinBlockSize: (block: Block) => { width: number; height: number }; + handleBlockClick: (blockId: string, event: React.MouseEvent) => void; + handleElementClick: (blockId: string, elementId: string, event: React.MouseEvent) => void; + handleElementDragStart: (elementId: string, event: React.MouseEvent) => void; + handleElementResizeStart: (elementId: string, event: React.MouseEvent) => void; + handleBlockResizeStart: (blockId: string, event: React.MouseEvent) => void; + handleSwapStart: (elementId: string, event: React.MouseEvent) => void; + handleSwapTarget: (elementId: string, event: React.MouseEvent) => void; + setShowElementDropdown: (blockId: string | null) => void; + showElementDropdown: string | null; + blockRef: RefObject; + handleBlockDragStart: (blockId: string, event: React.MouseEvent) => void; +} + +const BlockComponent: React.FC = ({ + block, + editMode, + selectedBlock, + selectedElement, + showSwapUI, + swapSource, + calculateMinBlockSize, + handleBlockClick, + handleElementClick, + handleElementDragStart, + handleElementResizeStart, + handleBlockResizeStart, + handleSwapStart, + handleSwapTarget, + setShowElementDropdown, + showElementDropdown, + blockRef, + handleBlockDragStart, +}) => { + const minSize = calculateMinBlockSize(block); + const isSelected = selectedBlock === block.id; + const isDraggable = + editMode && (block.positionType === "absolute" || block.positionType === "fixed"); + + const handleMouseDown = (event: React.MouseEvent) => { + if (isDraggable) { + handleBlockDragStart(block.id, event); + } + handleBlockClick(block.id, event); + }; + + return ( +
+ {/* Add Element Button */} + {editMode && isSelected && ( + + )} + + {/* Elements */} + {block.elements.map((el) => ( + + ))} + + {/* Block resize handle */} + {editMode && isSelected && ( +
handleBlockResizeStart(block.id, e)} + /> + )} + + {/* Drag handle indicator for absolute/fixed blocks */} + {isDraggable && editMode && ( +
+ ⤣ +
+ )} +
+ ); +}; + +export default BlockComponent; diff --git a/app/src/SimulationDashboard/components/block/BlockEditor.tsx b/app/src/SimulationDashboard/components/block/BlockEditor.tsx new file mode 100644 index 0000000..12ab90b --- /dev/null +++ b/app/src/SimulationDashboard/components/block/BlockEditor.tsx @@ -0,0 +1,242 @@ +import type { RefObject } from "react"; +import type { Block } from "../../../types/simulationDashboard"; +import { getAlphaFromRgba, rgbaToHex } from "../../functions/helpers/colorHandlers"; +import { getCurrentBlockStyleValue } from "../../functions/helpers/getCurrentBlockStyleValue"; +import { handleBackgroundColorChange } from "../../functions/helpers/handleBackgroundColorChange"; +import { handleBackgroundAlphaChange } from "../../functions/helpers/handleBackgroundAlphaChange"; +import { handleBlurAmountChange } from "../../functions/helpers/handleBlurAmountChange"; + +interface BlockEditorProps { + blockEditorRef: RefObject; + currentBlock: Block; + selectedBlock: string; + updateBlockStyle: (blockId: string, style: React.CSSProperties) => void; + updateBlockSize: (blockId: string, size: { width: number; height: number }) => void; + updateBlockPosition: (blockId: string, position: { x: number; y: number }) => void; + updateBlockPositionType: ( + blockId: string, + positionType: "relative" | "absolute" | "fixed" + ) => void; + updateBlockZIndex: (blockId: string, zIndex: number) => void; +} + +const BlockEditor: React.FC = ({ + blockEditorRef, + currentBlock, + selectedBlock, + updateBlockStyle, + updateBlockSize, + updateBlockPosition, + updateBlockPositionType, + updateBlockZIndex, +}) => { + return ( +
+

Block Style

+ +
+ + +
+ + {currentBlock.positionType === "absolute" && ( + <> +
+ + + updateBlockPosition(selectedBlock, { + ...currentBlock.position!, + x: Number(e.target.value), + }) + } + className="form-input" + /> +
+
+ + + updateBlockPosition(selectedBlock, { + ...currentBlock.position!, + y: Number(e.target.value), + }) + } + className="form-input" + /> +
+ + )} + +
+ + + handleBackgroundColorChange( + currentBlock, + selectedBlock, + updateBlockStyle, + e.target.value + ) + } + className="color-input" + /> +
+ +
+ + + handleBackgroundAlphaChange( + currentBlock, + selectedBlock, + updateBlockStyle, + Number(e.target.value) + ) + } + className="range-input" + /> +
+ +
+ + + handleBlurAmountChange( + selectedBlock, + updateBlockStyle, + Number(e.target.value) + ) + } + className="range-input" + /> +
+ +
+ + + updateBlockSize(selectedBlock, { + ...currentBlock.size!, + width: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + + updateBlockSize(selectedBlock, { + ...currentBlock.size!, + height: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + updateBlockZIndex(selectedBlock, Number(e.target.value))} + className="form-input" + /> +
+ +
+ + + updateBlockStyle(selectedBlock, { padding: Number(e.target.value) }) + } + className="form-input" + /> +
+ +
+ + + updateBlockStyle(selectedBlock, { + borderRadius: Number(e.target.value), + }) + } + className="form-input" + /> +
+
+ ); +}; + +export default BlockEditor; diff --git a/app/src/SimulationDashboard/components/block/BlockGrid.tsx b/app/src/SimulationDashboard/components/block/BlockGrid.tsx new file mode 100644 index 0000000..3b3b4be --- /dev/null +++ b/app/src/SimulationDashboard/components/block/BlockGrid.tsx @@ -0,0 +1,78 @@ +import React, { type RefObject, useRef } from "react"; +import type { Block } from "../../../types/simulationDashboard"; +import BlockComponent from "./BlockComponent"; + +// components/BlockGrid.tsx - Updated props +interface BlockGridProps { + blocks: Block[]; + editMode: boolean; + selectedBlock: string | null; + selectedElement: string | null; + showSwapUI: boolean; + swapSource: string | null; + calculateMinBlockSize: (block: Block) => { width: number; height: number }; + handleBlockClick: (blockId: string, event: React.MouseEvent) => void; + handleElementClick: (blockId: string, elementId: string, event: React.MouseEvent) => void; + handleElementDragStart: (elementId: string, event: React.MouseEvent) => void; + handleElementResizeStart: (elementId: string, event: React.MouseEvent) => void; + handleBlockResizeStart: (blockId: string, event: React.MouseEvent) => void; + handleSwapStart: (elementId: string, event: React.MouseEvent) => void; + handleSwapTarget: (elementId: string, event: React.MouseEvent) => void; + handleBlockDragStart: (blockId: string, event: React.MouseEvent) => void; + setShowElementDropdown: (blockId: string | null) => void; + showElementDropdown: string | null; + blockRef: RefObject; +} + +const BlockGrid: React.FC = ({ + blocks, + editMode, + selectedBlock, + selectedElement, + showSwapUI, + swapSource, + calculateMinBlockSize, + handleBlockClick, + handleElementClick, + handleElementDragStart, + handleElementResizeStart, + handleBlockResizeStart, + handleSwapStart, + handleSwapTarget, + setShowElementDropdown, + handleBlockDragStart, + showElementDropdown, + blockRef, +}) => { + const noopRef = useRef(null); + + return ( +
+ {blocks.map((block) => ( + + ))} +
+ ); +}; + +export default BlockGrid; diff --git a/app/src/SimulationDashboard/components/element/ElementComponent.tsx b/app/src/SimulationDashboard/components/element/ElementComponent.tsx new file mode 100644 index 0000000..b90c9f1 --- /dev/null +++ b/app/src/SimulationDashboard/components/element/ElementComponent.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import type { UIElement } from "../../../types/simulationDashboard"; +import { resolveElementValue } from "../../functions/helpers/resolveElementValue"; +import ElementContent from "./ElementContent"; + +interface ElementComponentProps { + element: UIElement; + blockId: string; + editMode: boolean; + selectedElement: string | null; + showSwapUI: boolean; + swapSource: string | null; + handleElementClick: (blockId: string, elementId: string, event: React.MouseEvent) => void; + handleElementDragStart: (elementId: string, event: React.MouseEvent) => void; + handleElementResizeStart: (elementId: string, event: React.MouseEvent) => void; + handleSwapStart: (elementId: string, event: React.MouseEvent) => void; + handleSwapTarget: (elementId: string, event: React.MouseEvent) => void; +} + +const ElementComponent: React.FC = ({ + element, + blockId, + editMode, + selectedElement, + showSwapUI, + swapSource, + handleElementClick, + handleElementDragStart, + handleElementResizeStart, + handleSwapStart, + handleSwapTarget, +}) => { + const isSelected = selectedElement === element.id; + const isSwapSource = swapSource === element.id; + + const elementClasses = [ + "element", + element.positionType === "absolute" ? "absolute" : "", + element.type === "graph" ? "graph" : "", + editMode ? "edit-mode" : "", + isSelected ? "selected" : "", + isSwapSource ? "swap-source" : "", + showSwapUI && !isSwapSource ? "swap-target" : "", + ] + .filter(Boolean) + .join(" "); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + if (showSwapUI && swapSource !== element.id) { + handleSwapTarget(element.id, e); + } else { + handleElementClick(blockId, element.id, e); + } + }} + onMouseDown={(e) => handleElementDragStart(element.id, e)} + > + + + {editMode && ( + <> + {element.positionType === "relative" && ( + + )} +
handleElementResizeStart(element.id, e)} + /> + + )} +
+ ); +}; + +export default ElementComponent; diff --git a/app/src/SimulationDashboard/components/element/ElementContent.tsx b/app/src/SimulationDashboard/components/element/ElementContent.tsx new file mode 100644 index 0000000..feb66ca --- /dev/null +++ b/app/src/SimulationDashboard/components/element/ElementContent.tsx @@ -0,0 +1,203 @@ +import { + LineChart, + Line, + BarChart, + Bar, + AreaChart, + Area, + PieChart, + Pie, + RadarChart, + Radar, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; +import type { ResolvedElementValue, UIElement } from "../../../types/simulationDashboard"; + +interface ElementContentProps { + element: UIElement; + resolvedData: ResolvedElementValue; +} + +const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042", "#8884D8", "#82CA9D"]; + +const ElementContent: React.FC = ({ element, resolvedData }) => { + const chartData = resolvedData.graphData || + element.graphData || [ + { name: "Jan", value: 400 }, + { name: "Feb", value: 300 }, + { name: "Mar", value: 600 }, + { name: "Apr", value: 800 }, + { name: "May", value: 500 }, + { name: "Jun", value: 900 }, + ]; + + const tooltipStyle = { + contentStyle: { + backgroundColor: "rgba(50, 50, 50, 0.9)", + border: "1px solid rgba(255,255,255,0.2)", + borderRadius: "4px", + color: "#fff", + }, + }; + + switch (element.type) { + case "label-value": + return ( +
+ + {resolvedData.label || "Label"} + + + {resolvedData.value?.toString() || "Value"} + +
+ ); + + case "text": + return
{resolvedData.value?.toString() || "Text"}
; + + case "icon": + return
🔔
; + + case "graph": + return ( +
+
{element.graphTitle || "Chart"}
+
+ + {element.graphType === "bar" ? ( + + + + + + + + ) : element.graphType === "area" ? ( + + + + + + + + ) : element.graphType === "pie" ? ( + + + `${name} ${((percent as number) * 100).toFixed(0)}%` + } + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {chartData.map((_, index: number) => ( + + ))} + + + + ) : element.graphType === "radar" ? ( + + + + + + + + ) : ( + + + + + + + + )} + +
+
+ ); + + default: + return Unknown Element; + } +}; + +export default ElementContent; diff --git a/app/src/SimulationDashboard/components/element/ElementDropdown.tsx b/app/src/SimulationDashboard/components/element/ElementDropdown.tsx new file mode 100644 index 0000000..e641f98 --- /dev/null +++ b/app/src/SimulationDashboard/components/element/ElementDropdown.tsx @@ -0,0 +1,55 @@ +import React, { type RefObject } from "react"; +import { createPortal } from "react-dom"; + +interface ElementDropdownProps { + showElementDropdown: string | null; + dropDownPosition: { top: number; left: number }; + addElement: (blockId: string, type: string, graphType?: string | undefined) => void; + dropdownRef: RefObject; +} + +const ElementDropdown: React.FC = ({ + showElementDropdown, + dropDownPosition, + addElement, + dropdownRef, +}) => { + if (!showElementDropdown) return null; + + const elementTypes = [ + { label: "Label-Value", type: "label-value" }, + { label: "Text", type: "text" }, + { label: "Icon", type: "icon" }, + { label: "Line Chart", type: "graph", graphType: "line" }, + { label: "Bar Chart", type: "graph", graphType: "bar" }, + { label: "Area Chart", type: "graph", graphType: "area" }, + { label: "Pie Chart", type: "graph", graphType: "pie" }, + { label: "Radar Chart", type: "graph", graphType: "radar" }, + ]; + + return createPortal( +
+ {elementTypes.map((elementType) => ( + + ))} +
, + document.body + ); +}; + +export default ElementDropdown; diff --git a/app/src/SimulationDashboard/components/element/ElementEditor.tsx b/app/src/SimulationDashboard/components/element/ElementEditor.tsx new file mode 100644 index 0000000..ea77081 --- /dev/null +++ b/app/src/SimulationDashboard/components/element/ElementEditor.tsx @@ -0,0 +1,617 @@ +import type { RefObject } from "react"; +import type { + Block, + DataBinding, + DataSourceType, + ExtendedCSSProperties, + GraphDataPoint, + UIElement, +} from "../../../types/simulationDashboard"; +import { getCurrentElementStyleValue } from "../../functions/helpers/getCurrentElementStyleValue"; +import type { DataModelManager } from "../../data/dataModel"; + +interface ElementEditorProps { + elementEditorRef: RefObject; + currentElement: UIElement; + selectedBlock: string; + selectedElement: string; + blocks: Block[]; + setBlocks: (blocks: Block[]) => void; + updateElementStyle: (blockId: string, elementId: string, style: ExtendedCSSProperties) => void; + updateElementSize: ( + blockId: string, + elementId: string, + size: { width: number; height: number } + ) => void; + updateElementPosition: ( + blockId: string, + elementId: string, + position: { x: number; y: number } + ) => void; + updateElementPositionType: ( + blockId: string, + elementId: string, + positionType: "relative" | "absolute" | "fixed" + ) => void; + updateElementZIndex: (blockId: string, elementId: string, zIndex: number) => void; + updateElementData: (blockId: string, elementId: string, updates: Partial) => void; + updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void; + updateGraphTitle: (blockId: string, elementId: string, title: string) => void; + setSwapSource: (source: string | null) => void; + setShowSwapUI: (show: boolean) => void; + dataModelManager: DataModelManager; +} + +const ElementEditor: React.FC = ({ + elementEditorRef, + currentElement, + selectedBlock, + selectedElement, + blocks, + setBlocks, + updateElementStyle, + updateElementSize, + updateElementPosition, + updateElementPositionType, + updateElementZIndex, + updateElementData, + updateGraphData, + updateGraphTitle, + setSwapSource, + setShowSwapUI, + dataModelManager, +}) => { + const defaultGraphData = [ + { name: "Jan", value: 400 }, + { name: "Feb", value: 300 }, + { name: "Mar", value: 600 }, + { name: "Apr", value: 800 }, + { name: "May", value: 500 }, + { name: "Jun", value: 900 }, + ]; + + return ( +
+

Element Style

+ + {currentElement.type === "label-value" && ( + <> +
+ + + updateElementStyle(selectedBlock, selectedElement, { + labelColor: e.target.value, + }) + } + className="color-input" + /> +
+ +
+ + + updateElementStyle(selectedBlock, selectedElement, { + valueColor: e.target.value, + }) + } + className="color-input" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + updateElementStyle(selectedBlock, selectedElement, { + gap: `${e.target.value}px`, + }) + } + className="form-input" + /> +
+ + )} + + {currentElement.type !== "label-value" && ( +
+ + + updateElementStyle(selectedBlock, selectedElement, { + color: e.target.value, + }) + } + className="color-input" + /> +
+ )} + +
+ + +
+ +
+ + + updateElementStyle(selectedBlock, selectedElement, { + fontSize: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + + updateElementSize(selectedBlock, selectedElement, { + ...currentElement.size!, + width: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + + updateElementSize(selectedBlock, selectedElement, { + ...currentElement.size!, + height: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + + updateElementStyle(selectedBlock, selectedElement, { + padding: Number(e.target.value), + }) + } + className="form-input" + /> +
+ +
+ + +
+ + {currentElement.positionType === "absolute" && ( + <> +
+ + + updateElementPosition(selectedBlock, selectedElement, { + ...currentElement.position!, + x: Number(e.target.value), + }) + } + className="form-input" + /> +
+
+ + + updateElementPosition(selectedBlock, selectedElement, { + ...currentElement.position!, + y: Number(e.target.value), + }) + } + className="form-input" + /> +
+ + )} + + {currentElement.positionType === "fixed" && ( + <> +
+ + + updateElementPosition(selectedBlock, selectedElement, { + ...currentElement.position!, + x: Number(e.target.value), + }) + } + className="form-input" + /> +
+
+ + + updateElementPosition(selectedBlock, selectedElement, { + ...currentElement.position!, + y: Number(e.target.value), + }) + } + className="form-input" + /> +
+ + )} + +
+ + + updateElementZIndex(selectedBlock, selectedElement, Number(e.target.value)) + } + className="form-input" + /> +
+ + {currentElement.type === "graph" && ( + <> +
+ + +
+ +
+ + + updateGraphTitle(selectedBlock, selectedElement, e.target.value) + } + className="form-input" + /> +
+ + )} + +
+ +
+ + {/* Data Binding Section */} +

+ Data Binding +

+ +
+ + +
+ + {currentElement.data?.dataSource === "static" && ( + <> + {currentElement.type === "label-value" && ( +
+ + + updateElementData(selectedBlock, selectedElement, { + label: e.target.value, + }) + } + className="form-input" + /> +
+ )} +
+ + + updateElementData(selectedBlock, selectedElement, { + staticValue: e.target.value, + }) + } + className="form-input" + /> +
+ + )} + + {currentElement.data?.dataSource === "dynamic" && ( + <> + {currentElement.type === "label-value" && ( +
+ + + updateElementData(selectedBlock, selectedElement, { + label: e.target.value, + }) + } + className="form-input" + /> +
+ )} +
+ + +
+ + )} + + {currentElement.data?.dataSource === "formula" && ( + <> + {currentElement.type === "label-value" && ( +
+ + + updateElementData(selectedBlock, selectedElement, { + label: e.target.value, + }) + } + className="form-input" + /> +
+ )} +
+ + + updateElementData(selectedBlock, selectedElement, { + formula: e.target.value, + }) + } + className="form-input" + /> + + Use {"{key}"} to reference data values. Example:{" "} + {"{totalUsers} * {growthRate}"} + +
+ + )} + + {currentElement.type === "graph" && ( +
+ +