Compare commits
14 Commits
710220f138
...
main
Author | SHA1 | Date | |
---|---|---|---|
8cead2c322 | |||
|
f61e935a5c | ||
70115fc8bb | |||
|
daffc9b76b | ||
|
3f003dc7a0 | ||
8a87c45fc3 | |||
|
a19c20b02f | ||
0d15a82389 | |||
442b6994ae | |||
|
c8475752e0 | ||
|
519bea1a98 | ||
2e3ca0b579 | |||
|
93818c3a3e | ||
|
55215cb47c |
518
package-lock.json
generated
518
package-lock.json
generated
@ -11,10 +11,24 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-defaulticon-compatibility": "^0.1.2",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"socket.io": "^4.8.0",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.79.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@ -3540,6 +3554,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-leaflet/core": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
|
||||
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
|
||||
"integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-babel": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||
@ -3655,6 +3689,12 @@
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@surma/rollup-plugin-off-main-thread": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
||||
@ -4309,6 +4349,21 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.56.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||
@ -5811,6 +5866,31 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@ -6130,6 +6210,15 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
@ -6324,6 +6413,12 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -6826,6 +6921,19 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@ -7719,6 +7827,15 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -7782,6 +7899,100 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz",
|
||||
"integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
@ -10192,6 +10403,13 @@
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
||||
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -13243,6 +13461,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@ -13258,6 +13498,27 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -13322,6 +13583,18 @@
|
||||
"shell-quote": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leaflet-defaulticon-compatibility": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz",
|
||||
"integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@ -13406,6 +13679,42 @@
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@ -13418,6 +13727,12 @@
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
@ -15829,6 +16144,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
@ -15855,6 +16176,15 @@
|
||||
"teleport": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.react": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.0.1.tgz",
|
||||
"integrity": "sha512-Lpj0tPBn561WiQ3QQWXbkx8xTtB8BZkJeMZWLJIL8iaPBCoWzW1IpCeU3gY5MDqsb0+efCvEGkl9O0naP64crA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@ -16200,12 +16530,35 @@
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-image-crop": {
|
||||
"version": "11.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.0.7.tgz",
|
||||
"integrity": "sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
|
||||
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
|
||||
"license": "Hippocratic-2.1",
|
||||
"dependencies": {
|
||||
"@react-leaflet/core": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"leaflet": "^1.9.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
@ -16215,6 +16568,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.26.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
|
||||
"integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.19.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.26.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
|
||||
"integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.19.2",
|
||||
"react-router": "6.26.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
@ -16822,6 +17207,24 @@
|
||||
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.79.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.3.tgz",
|
||||
"integrity": "sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-loader": {
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||
@ -16860,6 +17263,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass/node_modules/readdirp": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
|
||||
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@ -17228,6 +17661,83 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
|
||||
"integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
|
||||
"integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs": {
|
||||
"version": "0.3.24",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
|
||||
@ -19723,6 +20233,14 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
|
||||
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
14
package.json
14
package.json
@ -6,9 +6,20 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-defaulticon-compatibility": "^0.1.2",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"socket.io": "^4.8.0",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
@ -34,5 +45,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.79.3"
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
@ -24,7 +28,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>YANDEX ZAKLADKI</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 15 KiB |
21
server.js
Normal file
21
server.js
Normal file
@ -0,0 +1,21 @@
|
||||
const io = require("socket.io")(4000, {
|
||||
cors: {
|
||||
origin: "*", // Разрешить все источники
|
||||
},
|
||||
});
|
||||
|
||||
const devices = [
|
||||
{ id: 1, name: "Устройство 1", lat: 59.9343, lng: 30.3351, timestamp: Date.now() }, // Санкт-Петербург
|
||||
{ id: 2, name: "Устройство 2", lat: 59.9400, lng: 30.3000, timestamp: Date.now() }, // Санкт-Петербург
|
||||
];
|
||||
|
||||
setInterval(() => {
|
||||
// Имитируем обновление местоположения
|
||||
devices.forEach(device => {
|
||||
device.lat += (Math.random() - 0.5) * 0.01;
|
||||
device.lng += (Math.random() - 0.5) * 0.01;
|
||||
device.timestamp = Date.now();
|
||||
});
|
||||
|
||||
io.emit("deviceLocationUpdate", devices); // Отправляем обновленные данные клиенту
|
||||
}, 5000); // Обновляем данные каждые 5 секунд
|
38
src/App.css
38
src/App.css
@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
79
src/App.js
79
src/App.js
@ -1,25 +1,62 @@
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import DeviceGroups from "./pages/DeviceGroups";
|
||||
import Devices from "./pages/Devices";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import UserAccount from './pages/UserAccount'; // Импортируем компонент UserAccount
|
||||
import Login from './pages/Login'; // Импортируем страницу логина
|
||||
import Connections from './pages/Connections'; // Импортируем страницу подключений
|
||||
import './App.scss';
|
||||
|
||||
const App = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false); // Статус авторизации
|
||||
const [activeTab, setActiveTab] = useState('map'); // По умолчанию активная вкладка "Карта"
|
||||
|
||||
// Функция для выхода из системы
|
||||
const handleLogout = () => {
|
||||
setIsLoggedIn(false);
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
setIsLoggedIn(true); // Устанавливаем авторизацию
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/login"
|
||||
element={isLoggedIn ? <Navigate to="/app" /> : <Login onLogin={handleLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/app"
|
||||
element={isLoggedIn ? (
|
||||
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
<div style={{ display: 'flex', flex: 1 }}>
|
||||
<Sidebar
|
||||
onSelectTab={setActiveTab}
|
||||
activeTab={activeTab}
|
||||
onLogout={handleLogout} // Передаем функцию handleLogout через пропс onLogout
|
||||
/>
|
||||
<div style={{ padding: '20px', flex: 1 }}>
|
||||
{activeTab === 'map' && <Dashboard />} {/* Подключаем компонент Dashboard */}
|
||||
{activeTab === 'connection' && <Connections />} {/* Страница подключений */}
|
||||
{activeTab === 'account' && <UserAccount />} {/* Подключаем компонент UserAccount */}
|
||||
{activeTab === 'groups' && <DeviceGroups />} {/* Группы устройств */}
|
||||
{activeTab === 'devices' && <Devices />} {/* Устройства */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Navigate to="/login" />
|
||||
)}
|
||||
/>
|
||||
{/* Перенаправляем на /login по умолчанию */}
|
||||
<Route path="*" element={<Navigate to="/login" />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
41
src/App.scss
Normal file
41
src/App.scss
Normal file
@ -0,0 +1,41 @@
|
||||
.app-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
width: 250px;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
&.closed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.open {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-toggle-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
}
|
34
src/Connections.scss
Normal file
34
src/Connections.scss
Normal file
@ -0,0 +1,34 @@
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
form {
|
||||
input {
|
||||
margin-right: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
background-color: #2980b9;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #3498db;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
48
src/DeviceGroups.css
Normal file
48
src/DeviceGroups.css
Normal file
@ -0,0 +1,48 @@
|
||||
.device-groups {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.device-groups h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.group-creation {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.group-creation input {
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.group-creation button {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
li div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
li button {
|
||||
margin-right: 10px;
|
||||
}
|
38
src/Devices.css
Normal file
38
src/Devices.css
Normal file
@ -0,0 +1,38 @@
|
||||
.devices {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.devices h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.devices table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.devices table, .devices th, .devices td {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.devices th, .devices td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.devices th {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.devices tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.buttons button {
|
||||
margin-right: 10px;
|
||||
}
|
70
src/Login.scss
Normal file
70
src/Login.scss
Normal file
@ -0,0 +1,70 @@
|
||||
// styles/Login.scss
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
.login-box {
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column; // Выровнять элементы по колонке
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
src/Sidebar.js
Normal file
58
src/Sidebar.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import './Sidebar.scss'; // Keep your original styling file, if necessary
|
||||
|
||||
const Sidebar = ({ onSelectTab, activeTab, onLogout }) => {
|
||||
const [isOpen, setIsOpen] = useState(true); // Состояние для видимости боковой панели
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
|
||||
|
||||
const toggleSubMenu = () => {
|
||||
setIsSubMenuOpen(!isSubMenuOpen);
|
||||
// Переключение на вкладку "Аккаунт пользователя"
|
||||
if (!isSubMenuOpen) {
|
||||
onSelectTab('account');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className="toggle-button" onClick={toggleSidebar}>
|
||||
{isOpen ? 'Скрыть панель' : 'Показать панель'}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="sidebar">
|
||||
<h2>Меню</h2>
|
||||
<ul>
|
||||
<li className={activeTab === 'map' ? 'active' : ''} onClick={() => onSelectTab('map')}>
|
||||
Карта в реальном времени
|
||||
</li>
|
||||
<li className={activeTab === 'connection' ? 'active' : ''} onClick={() => onSelectTab('connection')}>
|
||||
Подключение
|
||||
</li>
|
||||
<li className={activeTab === 'account' ? 'active' : ''} onClick={toggleSubMenu}>
|
||||
Аккаунт пользователя
|
||||
</li>
|
||||
{isSubMenuOpen && (
|
||||
<>
|
||||
<li className={activeTab === 'groups' ? 'active' : ''} onClick={() => onSelectTab('groups')}>
|
||||
Группы устройств
|
||||
</li>
|
||||
<li className={activeTab === 'devices' ? 'active' : ''} onClick={() => onSelectTab('devices')}>
|
||||
Устройства
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
<button className="logout-button" onClick={onLogout}>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
48
src/Sidebar.scss
Normal file
48
src/Sidebar.scss
Normal file
@ -0,0 +1,48 @@
|
||||
.sidebar {
|
||||
width: 250px; // Ширина боковой панели
|
||||
background-color: #f0f0f0; // Цвет фона
|
||||
padding: 20px; // Отступы
|
||||
position: fixed; // Фиксированное положение
|
||||
top: 0; // Положение сверху
|
||||
left: 0; // Положение слева
|
||||
height: 100%; // Высота панели
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); // Тень
|
||||
transition: transform 0.3s ease; // Анимация при открытии/закрытии
|
||||
z-index: 1000; // Слой над другими элементами
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
position: fixed; // Фиксированное положение
|
||||
top: 10px; // Отступ сверху
|
||||
left: 10px; // Отступ слева
|
||||
padding: 10px 15px; // Отступы кнопки
|
||||
background-color: #007bff; // Цвет фона кнопки
|
||||
color: white; // Цвет текста
|
||||
border: none; // Убрать границы
|
||||
border-radius: 5px; // Закругление углов
|
||||
cursor: pointer; // Курсор при наведении
|
||||
z-index: 1001; // Слой над боковой панелью
|
||||
|
||||
&:hover {
|
||||
background-color: #0056b3; // Цвет фона при наведении
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
font-weight: bold; // Жирный текст для активного элемента
|
||||
color: #007bff; // Цвет текста для активного элемента
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
margin-top: 20px; // Отступ сверху
|
||||
padding: 10px 15px; // Отступы кнопки
|
||||
background-color: #dc3545; // Цвет фона кнопки "Выйти"
|
||||
color: white; // Цвет текста
|
||||
border: none; // Убрать границы
|
||||
border-radius: 5px; // Закругление углов
|
||||
cursor: pointer; // Курсор при наведении
|
||||
|
||||
&:hover {
|
||||
background-color: #c82333; // Цвет фона при наведении
|
||||
}
|
||||
}
|
140
src/UserAccount.scss
Normal file
140
src/UserAccount.scss
Normal file
@ -0,0 +1,140 @@
|
||||
.user-account {
|
||||
padding: 40px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 60px auto;
|
||||
font-family: 'Arial', sans-serif;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-info, .edit-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
p, input {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
outline: none;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
|
||||
input[type="text"], input[type="email"], input[type="tel"], input[type="file"] {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
margin-top: 10px;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
button {
|
||||
padding: 14px 30px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #2980b9;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-color: #e74c3c;
|
||||
|
||||
&:hover {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
margin-top: 30px;
|
||||
padding: 14px 35px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #27ae60;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #1e8449;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
63
src/pages/Connections.js
Normal file
63
src/pages/Connections.js
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../Connections.scss';
|
||||
|
||||
const Connections = () => {
|
||||
const [webhooks, setWebhooks] = useState([]);
|
||||
const [websocketUrl, setWebsocketUrl] = useState('');
|
||||
|
||||
const addWebhook = (newWebhook) => {
|
||||
setWebhooks([...webhooks, newWebhook]);
|
||||
};
|
||||
|
||||
const handleWebhookSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const newWebhook = {
|
||||
url: e.target.webhookUrl.value,
|
||||
groups: e.target.groups.value.split(','),
|
||||
};
|
||||
addWebhook(newWebhook);
|
||||
e.target.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Подключения</h2>
|
||||
|
||||
{/* Webhook подключение */}
|
||||
<div className="section">
|
||||
<h3>Подключения Webhooks</h3>
|
||||
<form onSubmit={handleWebhookSubmit}>
|
||||
<input type="text" name="webhookUrl" placeholder="URL Webhook" required />
|
||||
<input type="text" name="groups" placeholder="Группы (через запятую)" required />
|
||||
<button type="submit">Добавить Webhook</button>
|
||||
</form>
|
||||
|
||||
<ul>
|
||||
{webhooks.map((webhook, index) => (
|
||||
<li key={index}>
|
||||
Webhook: {webhook.url} (Группы: {webhook.groups.join(', ')})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* WebSocket подключение */}
|
||||
<div className="section">
|
||||
<h3>Подключения WebSockets</h3>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setWebsocketUrl(e.target.websocketUrl.value);
|
||||
}}
|
||||
>
|
||||
<input type="text" name="websocketUrl" placeholder="URL WebSocket" required />
|
||||
<button type="submit">Подключиться</button>
|
||||
</form>
|
||||
|
||||
{websocketUrl && <p>Подключено по WebSocket: {websocketUrl}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Connections;
|
78
src/pages/Dashboard.js
Normal file
78
src/pages/Dashboard.js
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "leaflet-defaulticon-compatibility";
|
||||
import io from "socket.io-client";
|
||||
|
||||
// Инициализация WebSocket соединения
|
||||
const socket = io("ws://localhost:4000");
|
||||
|
||||
const Dashboard = () => {
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [userLocation, setUserLocation] = useState([59.9343, 30.3351]); // Центр по умолчанию на Санкт-Петербург
|
||||
const [locationLoaded, setLocationLoaded] = useState(false); // Флаг загрузки местоположения
|
||||
|
||||
useEffect(() => {
|
||||
// Получаем местоположение пользователя
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude } = position.coords;
|
||||
setUserLocation([latitude, longitude]); // Устанавливаем координаты пользователя
|
||||
setLocationLoaded(true); // Флаг, что местоположение загружено
|
||||
},
|
||||
(error) => {
|
||||
console.error("Ошибка при получении геолокации:", error);
|
||||
setLocationLoaded(true); // Продолжаем даже при ошибке
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.error("Геолокация не поддерживается вашим браузером");
|
||||
setLocationLoaded(true); // Продолжаем даже если геолокация недоступна
|
||||
}
|
||||
|
||||
// Получаем данные устройств в реальном времени
|
||||
socket.on("deviceLocationUpdate", (data) => {
|
||||
setDevices(data);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("deviceLocationUpdate");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1 }}>
|
||||
<h1>Панель мониторинга - Карта устройств</h1>
|
||||
{locationLoaded ? (
|
||||
<MapContainer
|
||||
center={userLocation} // Центрируем карту на координатах пользователя
|
||||
zoom={12} // Начальный зум
|
||||
style={{ height: "90vh" }}
|
||||
>
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
/>
|
||||
{devices.map((device, index) => (
|
||||
<Marker key={index} position={[device.lat, device.lng]}>
|
||||
<Popup>
|
||||
<b>{device.name}</b>
|
||||
<br />
|
||||
Последнее обновление: {new Date(device.timestamp).toLocaleString("ru-RU")}
|
||||
</Popup>
|
||||
</Marker>
|
||||
))}
|
||||
{/* Маркер текущего местоположения пользователя */}
|
||||
<Marker position={userLocation}>
|
||||
<Popup>Вы здесь</Popup>
|
||||
</Marker>
|
||||
</MapContainer>
|
||||
) : (
|
||||
<p>Загрузка карты...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
87
src/pages/DeviceGroups.js
Normal file
87
src/pages/DeviceGroups.js
Normal file
@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../DeviceGroups.css';
|
||||
|
||||
const DeviceGroups = () => {
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
|
||||
const createGroup = () => {
|
||||
if (newGroupName) {
|
||||
const newGroup = {
|
||||
name: newGroupName,
|
||||
devices: [],
|
||||
active: true,
|
||||
};
|
||||
setGroups([...groups, newGroup]);
|
||||
setNewGroupName('');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteGroup = (groupName) => {
|
||||
setGroups(groups.filter(group => group.name !== groupName));
|
||||
};
|
||||
|
||||
const toggleGroupStatus = (groupName) => {
|
||||
setGroups(groups.map(group =>
|
||||
group.name === groupName ? { ...group, active: !group.active } : group
|
||||
));
|
||||
};
|
||||
|
||||
const addDeviceToGroup = (groupName, device) => {
|
||||
setGroups(groups.map(group =>
|
||||
group.name === groupName ? { ...group, devices: [...group.devices, device] } : group
|
||||
));
|
||||
};
|
||||
|
||||
const removeDeviceFromGroup = (groupName, device) => {
|
||||
setGroups(groups.map(group =>
|
||||
group.name === groupName ? { ...group, devices: group.devices.filter(d => d !== device) } : group
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="device-groups">
|
||||
<h2>Группы устройств</h2>
|
||||
|
||||
<div className="group-creation">
|
||||
<input
|
||||
type="text"
|
||||
value={newGroupName}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
placeholder="Название новой группы"
|
||||
/>
|
||||
<button onClick={createGroup}>Создать группу</button>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{groups.map(group => (
|
||||
<li key={group.name} className={group.active ? 'active' : 'inactive'}>
|
||||
<div>
|
||||
<span>{group.name}</span>
|
||||
<button onClick={() => deleteGroup(group.name)}>Удалить</button>
|
||||
<button onClick={() => toggleGroupStatus(group.name)}>
|
||||
{group.active ? 'Деактивировать' : 'Активировать'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Устройства в группе:</h4>
|
||||
<ul>
|
||||
{group.devices.map((device, index) => (
|
||||
<li key={index}>
|
||||
{device} <button onClick={() => removeDeviceFromGroup(group.name, device)}>Удалить</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={() => addDeviceToGroup(group.name, 'Новое устройство')}>
|
||||
Добавить устройство
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceGroups;
|
146
src/pages/Devices.js
Normal file
146
src/pages/Devices.js
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import axios from 'axios';
|
||||
import '../Devices.css';
|
||||
|
||||
const Devices = () => {
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState(null);
|
||||
const [deviceName, setDeviceName] = useState('');
|
||||
const [deviceStatus, setDeviceStatus] = useState('');
|
||||
const [isRegenerating, setIsRegenerating] = useState(false); // отображение генерации токена
|
||||
const [showFullToken, setShowFullToken] = useState({}); // управление отображением токена
|
||||
|
||||
useEffect(() => {
|
||||
// получение устройств
|
||||
const fetchDevices = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/devices'); // потом нужно будет заменить на нашу апишку
|
||||
setDevices(response.data.devices);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке устройств:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDevices();
|
||||
}, []);
|
||||
|
||||
const addDevice = () => {
|
||||
if (deviceName) {
|
||||
const newDevice = { name: deviceName, status: deviceStatus || 'Неактивен', id: Date.now(), access_key: '' };
|
||||
setDevices([...devices, newDevice]);
|
||||
setDeviceName('');
|
||||
setDeviceStatus('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeDevice = () => {
|
||||
if (selectedDevice) {
|
||||
setDevices(devices.filter(device => device.id !== selectedDevice.id));
|
||||
setSelectedDevice(null);
|
||||
}
|
||||
};
|
||||
|
||||
const editDevice = () => {
|
||||
if (selectedDevice) {
|
||||
const updatedDevices = devices.map(device =>
|
||||
device.id === selectedDevice.id
|
||||
? { ...device, name: deviceName, status: deviceStatus }
|
||||
: device
|
||||
);
|
||||
setDevices(updatedDevices);
|
||||
setSelectedDevice(null);
|
||||
setDeviceName('');
|
||||
setDeviceStatus('');
|
||||
}
|
||||
};
|
||||
|
||||
// генерация токена
|
||||
const handleShowFullToken = (deviceId) => {
|
||||
setShowFullToken(prevState => ({ ...prevState, [deviceId]: !prevState[deviceId] }));
|
||||
};
|
||||
|
||||
// пересоздание токена
|
||||
const handleRegenerateToken = async (deviceId) => {
|
||||
setIsRegenerating(true);
|
||||
try {
|
||||
const response = await axios.post(`/api/devices/${deviceId}/regenerate-token`); // тоже заменить на апишку
|
||||
setDevices(devices.map(device =>
|
||||
device.id === deviceId ? { ...device, access_key: response.data.access_key } : device
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('Ошибка при пересоздании токена:', error);
|
||||
}
|
||||
setIsRegenerating(false);
|
||||
};
|
||||
|
||||
const generateQR = (device) => {
|
||||
return (
|
||||
<QRCodeCanvas value={JSON.stringify(device)} size={128} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="devices">
|
||||
<h2>Устройства</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Название устройства"
|
||||
value={deviceName}
|
||||
onChange={(e) => setDeviceName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Статус устройства"
|
||||
value={deviceStatus}
|
||||
onChange={(e) => setDeviceStatus(e.target.value)}
|
||||
/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Статус</th>
|
||||
<th>QR Код</th>
|
||||
<th>Токен устройства</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{devices.map((device) => (
|
||||
<tr key={device.id} onClick={() => setSelectedDevice(device)}>
|
||||
<td>{device.name}</td>
|
||||
<td>{device.status}</td>
|
||||
<td>{generateQR(device)}</td>
|
||||
<td>
|
||||
{showFullToken[device.id] ? (
|
||||
device.access_key
|
||||
) : (
|
||||
<span>{device.access_key.slice(0, 6)}...</span>
|
||||
)}
|
||||
<button onClick={() => handleShowFullToken(device.id)}>
|
||||
{showFullToken[device.id] ? 'Скрыть' : 'Показать'}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={editDevice}>Редактировать</button>
|
||||
<button
|
||||
onClick={() => handleRegenerateToken(device.id)}
|
||||
disabled={isRegenerating}
|
||||
>
|
||||
{isRegenerating ? 'Пересоздание...' : 'Пересоздать токен'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="buttons">
|
||||
<button onClick={addDevice}>Добавить</button>
|
||||
<button onClick={removeDevice} disabled={!selectedDevice}>Удалить выбранное</button>
|
||||
<button onClick={editDevice} disabled={!selectedDevice}>Сохранить изменения</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Devices;
|
52
src/pages/Login.js
Normal file
52
src/pages/Login.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../Login.scss';
|
||||
|
||||
const Login = ({ onLogin }) => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
// Simple login/password check
|
||||
if (username === 'admin' && password === 'admin') {
|
||||
onLogin(); // Call function on successful login
|
||||
} else {
|
||||
setError('Неверный логин или пароль');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<div className="login-box"> {/* Added a wrapper for styling */}
|
||||
<h2>Вход</h2>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="input-group">
|
||||
<label htmlFor="username">Логин</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Логин"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="password">Пароль</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
115
src/pages/UserAccount.js
Normal file
115
src/pages/UserAccount.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import '../UserAccount.scss';
|
||||
|
||||
const UserAccount = () => {
|
||||
const [userData, setUserData] = useState({
|
||||
username: 'Мамут Рахал',
|
||||
email: 'yatupoidayn@mail.ru',
|
||||
phone: '+666',
|
||||
avatar: '', // Новое поле для аватара
|
||||
});
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(userData);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Функция для загрузки аватара
|
||||
const handleAvatarChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setFormData((prevData) => ({
|
||||
...prevData,
|
||||
avatar: reader.result,
|
||||
}));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setUserData(formData);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormData(userData);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="user-account">
|
||||
<h1>Данные</h1>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="edit-form">
|
||||
<label>
|
||||
Имя:
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Email:
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Телефон:
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Загрузить аватар:
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
/>
|
||||
</label>
|
||||
<div className="buttons">
|
||||
<button onClick={handleSave}>Сохранить</button>
|
||||
<button onClick={handleCancel}>Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="user-info">
|
||||
<div className="avatar-container">
|
||||
{userData.avatar ? (
|
||||
<img src={userData.avatar} alt="User Avatar" className="avatar" />
|
||||
) : (
|
||||
<div className="avatar-placeholder">A</div>
|
||||
)}
|
||||
</div>
|
||||
<p><strong>Имя:</strong> {userData.username}</p>
|
||||
<p><strong>Email:</strong> {userData.email}</p>
|
||||
<p><strong>Телефон:</strong> {userData.phone}</p>
|
||||
<button className="edit-button" onClick={() => setIsEditing(true)}>
|
||||
Редактировать
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserAccount;
|
Loading…
Reference in New Issue
Block a user