Update dependencies, refactor authentication, and enhance UI components
- Upgraded @reduxjs/toolkit to version 2.9.0 and added new dependencies including @techniker-me/pcast-api and moment. - Refactored authentication logic and added middleware for improved request handling. - Introduced new UI components such as buttons, loaders, and forms, along with a theme system following SOLID principles. - Updated routing to include protected routes and improved the login form with better error handling. - Removed unused CSS and organized the project structure for better maintainability.
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
# Phenix Web Control Center
|
# Phenix Web Control Center
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```
|
```
|
||||||
nvm use
|
nvm use
|
||||||
npm i
|
npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
@@ -14,11 +16,13 @@ npm run start
|
|||||||
## Build
|
## Build
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build:prod
|
npm run build:prod
|
||||||
```
|
```
|
||||||
72
bun.lock
72
bun.lock
@@ -5,12 +5,16 @@
|
|||||||
"name": "webcontrolcenter-zinn-2",
|
"name": "webcontrolcenter-zinn-2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phenixrts/sdk": "2025.2.2",
|
"@phenixrts/sdk": "2025.2.2",
|
||||||
"@reduxjs/toolkit": "2.8.2",
|
"@reduxjs/toolkit": "2.9.0",
|
||||||
|
"@techniker-me/pcast-api": "2025.1.5",
|
||||||
"@techniker-me/tools": "2025.0.16",
|
"@techniker-me/tools": "2025.0.16",
|
||||||
|
"moment": "2.30.1",
|
||||||
"phenix-web-proto": "2020.0.3",
|
"phenix-web-proto": "2020.0.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-redux": "9.2.0",
|
"react-redux": "9.2.0",
|
||||||
|
"react-router-dom": "7.8.2",
|
||||||
|
"styled-components": "6.1.19",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.34.0",
|
"@eslint/js": "9.34.0",
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
"@types/react": "19.1.12",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@vitejs/plugin-react-swc": "4.0.1",
|
"@vitejs/plugin-react-swc": "4.0.1",
|
||||||
|
"babel-plugin-styled-components": "2.1.4",
|
||||||
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||||
"eslint": "9.34.0",
|
"eslint": "9.34.0",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
@@ -25,8 +30,9 @@
|
|||||||
"eslint-plugin-react-refresh": "0.4.20",
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.42.0",
|
"typescript-eslint": "8.42.0",
|
||||||
|
"typescript-plugin-styled-components": "3.0.0",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"vite-plugin-babel": "1.3.2",
|
"vite-plugin-babel": "1.3.2",
|
||||||
"vite-plugin-commonjs": "0.10.4",
|
"vite-plugin-commonjs": "0.10.4",
|
||||||
@@ -44,6 +50,8 @@
|
|||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
||||||
|
|
||||||
|
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
@@ -52,6 +60,8 @@
|
|||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||||
@@ -62,12 +72,20 @@
|
|||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
"@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
|
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.2.2", "", { "dependencies": { "@emotion/memoize": "^0.8.1" } }, "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw=="],
|
||||||
|
|
||||||
|
"@emotion/memoize": ["@emotion/memoize@0.8.1", "", {}, "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="],
|
||||||
|
|
||||||
|
"@emotion/unitless": ["@emotion/unitless@0.8.1", "", {}, "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
|
||||||
@@ -162,7 +180,7 @@
|
|||||||
|
|
||||||
"@phenixrts/sdk": ["@phenixrts/sdk@2025.2.2", "", {}, "sha512-thxg7IE3a8qE/hk1KM6zMZcZXww54/Hy8UdR4C4J43itp4r+JfD3jKLMqrHajPazbz7IaqX8ihiMDgrmRBGI7w=="],
|
"@phenixrts/sdk": ["@phenixrts/sdk@2025.2.2", "", {}, "sha512-thxg7IE3a8qE/hk1KM6zMZcZXww54/Hy8UdR4C4J43itp4r+JfD3jKLMqrHajPazbz7IaqX8ihiMDgrmRBGI7w=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.8.2", "", { "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" }, "optionalPeers": ["react", "react-redux"] }, "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A=="],
|
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.9.0", "", { "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" }, "optionalPeers": ["react", "react-redux"] }, "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.32", "", {}, "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.32", "", {}, "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g=="],
|
||||||
|
|
||||||
@@ -238,6 +256,8 @@
|
|||||||
|
|
||||||
"@swc/types": ["@swc/types@0.1.24", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng=="],
|
"@swc/types": ["@swc/types@0.1.24", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng=="],
|
||||||
|
|
||||||
|
"@techniker-me/pcast-api": ["@techniker-me/pcast-api@2025.1.5", "https://registry-node.techniker.me/@techniker-me/pcast-api/-/pcast-api-2025.1.5.tgz", { "dependencies": { "phenix-edge-auth": "1.2.7" } }, "sha512-2e/ufy6rUx4fm9g8RMmzYXLUd+Tq8fQvpTCUQbgf7612u/tAZtmWujs3OUK4QzdIPF1W2GuPPWI3NzObb0paog=="],
|
||||||
|
|
||||||
"@techniker-me/tools": ["@techniker-me/tools@2025.0.16", "https://registry-node.techniker.me/@techniker-me/tools/-/tools-2025.0.16.tgz", {}, "sha512-Ul2yj1vd4lCO8g7IW2pHkAsdeRVEUMqGpiIvSedCc1joVXEWPbh4GESW83kMHtisjFjjlZIzb3EVlCE0BCiBWQ=="],
|
"@techniker-me/tools": ["@techniker-me/tools@2025.0.16", "https://registry-node.techniker.me/@techniker-me/tools/-/tools-2025.0.16.tgz", {}, "sha512-Ul2yj1vd4lCO8g7IW2pHkAsdeRVEUMqGpiIvSedCc1joVXEWPbh4GESW83kMHtisjFjjlZIzb3EVlCE0BCiBWQ=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
@@ -250,6 +270,8 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
|
||||||
|
|
||||||
|
"@types/stylis": ["@types/stylis@4.2.5", "", {}, "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="],
|
||||||
|
|
||||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.42.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.42.0", "@typescript-eslint/type-utils": "8.42.0", "@typescript-eslint/utils": "8.42.0", "@typescript-eslint/visitor-keys": "8.42.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.42.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ=="],
|
||||||
@@ -302,6 +324,8 @@
|
|||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
|
"babel-plugin-styled-components": ["babel-plugin-styled-components@2.1.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5", "lodash": "^4.17.21", "picomatch": "^2.3.1" }, "peerDependencies": { "styled-components": ">= 2" } }, "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g=="],
|
||||||
|
|
||||||
"babel-plugin-transform-amd-to-commonjs": ["babel-plugin-transform-amd-to-commonjs@1.6.0", "", { "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-Dwvn+0BM6hdLMA5sfD9QzMICo8NnqqyqCyiNeKPruAuEZDdDVWbPkPh26ckJqfL/hYIkzAvK3Zj2H/7pBzIpig=="],
|
"babel-plugin-transform-amd-to-commonjs": ["babel-plugin-transform-amd-to-commonjs@1.6.0", "", { "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-Dwvn+0BM6hdLMA5sfD9QzMICo8NnqqyqCyiNeKPruAuEZDdDVWbPkPh26ckJqfL/hYIkzAvK3Zj2H/7pBzIpig=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
@@ -320,6 +344,8 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001739", "", {}, "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001739", "", {}, "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
@@ -332,8 +358,14 @@
|
|||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
|
||||||
|
|
||||||
|
"css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
@@ -556,6 +588,8 @@
|
|||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
@@ -574,6 +608,8 @@
|
|||||||
|
|
||||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
@@ -614,6 +650,8 @@
|
|||||||
|
|
||||||
"pbf": ["pbf@3.1.0", "", { "dependencies": { "ieee754": "^1.1.6", "resolve-protobuf-schema": "^2.0.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-/hYJmIsTmh7fMkHAWWXJ5b8IKLWdjdlAFb3IHkRBn1XUhIYBChVGfVwmHEAV3UfXTxsP/AKfYTXTS/dCPxJd5w=="],
|
"pbf": ["pbf@3.1.0", "", { "dependencies": { "ieee754": "^1.1.6", "resolve-protobuf-schema": "^2.0.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-/hYJmIsTmh7fMkHAWWXJ5b8IKLWdjdlAFb3IHkRBn1XUhIYBChVGfVwmHEAV3UfXTxsP/AKfYTXTS/dCPxJd5w=="],
|
||||||
|
|
||||||
|
"phenix-edge-auth": ["phenix-edge-auth@1.2.7", "", {}, "sha512-hmIA4iKrR6Pf+EoIu/k7kxKYXkiD7I8PQL7iZb+9NkC676hCeGPZK+OsRc9uNW+fDZgZlN1qoQjBxDiT5JRe+A=="],
|
||||||
|
|
||||||
"phenix-web-assert": ["phenix-web-assert@2020.0.2", "", { "dependencies": { "phenix-web-lodash-light": "^2020.0.2" } }, "sha512-WRgWqXsL1Du/ty/dq/vkooOd3e2BhLCw24vVLWmWZjM/o5TjeOhxQSMSklrDuVRamVGiEkvuQpjL8lIeP/W4TQ=="],
|
"phenix-web-assert": ["phenix-web-assert@2020.0.2", "", { "dependencies": { "phenix-web-lodash-light": "^2020.0.2" } }, "sha512-WRgWqXsL1Du/ty/dq/vkooOd3e2BhLCw24vVLWmWZjM/o5TjeOhxQSMSklrDuVRamVGiEkvuQpjL8lIeP/W4TQ=="],
|
||||||
|
|
||||||
"phenix-web-batch-http": ["phenix-web-batch-http@2020.0.2", "", { "dependencies": { "phenix-web-assert": "^2020.0.2", "phenix-web-disposable": "^2020.0.2", "phenix-web-event": "^2020.0.2", "phenix-web-global": "^2020.0.2", "phenix-web-http": "^2020.0.2", "phenix-web-lodash-light": "^2020.0.2", "phenix-web-network-connection-monitor": "^2020.0.2", "phenix-web-observable": "^2020.0.2" } }, "sha512-SG9/9RerkYcXIEH9Vk6DRosWF2229DXyeUa0hiU7jawj8xrOnAE7CMVdKcbaK4GGbZWMrT4HetIAMkqxSaHGFA=="],
|
"phenix-web-batch-http": ["phenix-web-batch-http@2020.0.2", "", { "dependencies": { "phenix-web-assert": "^2020.0.2", "phenix-web-disposable": "^2020.0.2", "phenix-web-event": "^2020.0.2", "phenix-web-global": "^2020.0.2", "phenix-web-http": "^2020.0.2", "phenix-web-lodash-light": "^2020.0.2", "phenix-web-network-connection-monitor": "^2020.0.2", "phenix-web-observable": "^2020.0.2" } }, "sha512-SG9/9RerkYcXIEH9Vk6DRosWF2229DXyeUa0hiU7jawj8xrOnAE7CMVdKcbaK4GGbZWMrT4HetIAMkqxSaHGFA=="],
|
||||||
@@ -638,12 +676,14 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
@@ -664,6 +704,10 @@
|
|||||||
|
|
||||||
"react-redux": ["react-redux@9.2.0", "", { "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" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
"react-redux": ["react-redux@9.2.0", "", { "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" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||||
|
|
||||||
|
"react-router": ["react-router@7.8.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ=="],
|
||||||
|
|
||||||
|
"react-router-dom": ["react-router-dom@7.8.2", "", { "dependencies": { "react-router": "7.8.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow=="],
|
||||||
|
|
||||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||||
|
|
||||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||||
@@ -696,12 +740,16 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
|
|
||||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||||
|
|
||||||
|
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||||
|
|
||||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
@@ -730,6 +778,10 @@
|
|||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"styled-components": ["styled-components@6.1.19", "", { "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" } }, "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA=="],
|
||||||
|
|
||||||
|
"stylis": ["stylis@4.3.2", "", {}, "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
@@ -740,6 +792,8 @@
|
|||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||||
@@ -754,6 +808,8 @@
|
|||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.42.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/parser": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg=="],
|
"typescript-eslint": ["typescript-eslint@8.42.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/parser": "8.42.0", "@typescript-eslint/typescript-estree": "8.42.0", "@typescript-eslint/utils": "8.42.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg=="],
|
||||||
|
|
||||||
|
"typescript-plugin-styled-components": ["typescript-plugin-styled-components@3.0.0", "", { "peerDependencies": { "typescript": "~4.8 || 5" } }, "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A=="],
|
||||||
|
|
||||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||||
@@ -802,7 +858,13 @@
|
|||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"fdir/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"styled-components/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
||||||
|
|
||||||
|
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
}
|
}
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -14,12 +14,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phenixrts/sdk": "2025.2.2",
|
"@phenixrts/sdk": "2025.2.2",
|
||||||
"@reduxjs/toolkit": "2.8.2",
|
"@reduxjs/toolkit": "2.9.0",
|
||||||
|
"@techniker-me/pcast-api": "2025.1.5",
|
||||||
"@techniker-me/tools": "2025.0.16",
|
"@techniker-me/tools": "2025.0.16",
|
||||||
|
"moment": "2.30.1",
|
||||||
"phenix-web-proto": "2020.0.3",
|
"phenix-web-proto": "2020.0.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-redux": "9.2.0"
|
"react-redux": "9.2.0",
|
||||||
|
"react-router-dom": "7.8.2",
|
||||||
|
"styled-components": "6.1.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.34.0",
|
"@eslint/js": "9.34.0",
|
||||||
@@ -27,6 +31,7 @@
|
|||||||
"@types/react": "19.1.12",
|
"@types/react": "19.1.12",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@vitejs/plugin-react-swc": "4.0.1",
|
"@vitejs/plugin-react-swc": "4.0.1",
|
||||||
|
"babel-plugin-styled-components": "2.1.4",
|
||||||
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||||
"eslint": "9.34.0",
|
"eslint": "9.34.0",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
@@ -34,8 +39,9 @@
|
|||||||
"eslint-plugin-react-refresh": "0.4.20",
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.42.0",
|
"typescript-eslint": "8.42.0",
|
||||||
|
"typescript-plugin-styled-components": "3.0.0",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"vite-plugin-babel": "1.3.2",
|
"vite-plugin-babel": "1.3.2",
|
||||||
"vite-plugin-commonjs": "0.10.4"
|
"vite-plugin-commonjs": "0.10.4"
|
||||||
|
|||||||
28
src/App.tsx
28
src/App.tsx
@@ -1,26 +1,12 @@
|
|||||||
import {JSX, useState} from 'react';
|
import {JSX} from 'react';
|
||||||
import {useAppDispatch} from './store';
|
import Router from './routers';
|
||||||
import {authenticateCredentialsThunk} from './store/slices/Authentication.slice';
|
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [applicationId, setApplicationId] = useState<string>('phenixrts.com-alex.zinn');
|
|
||||||
const [secret, setSecret] = useState<string>('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg');
|
|
||||||
|
|
||||||
const handleAuthenticate = async () => {
|
|
||||||
const response = await dispatch(authenticateCredentialsThunk({applicationId, secret}));
|
|
||||||
console.log(`${new Date().toISOString()} AuthenticationResponse [%o]`, response.payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const App = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Hello World</h1>
|
<Router />
|
||||||
<div>
|
|
||||||
<input type="text" value={applicationId} onChange={e => setApplicationId(e.target.value)} />
|
|
||||||
<br />
|
|
||||||
<input type="text" value={secret} onChange={e => setSecret(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<button onClick={handleAuthenticate}>Authenticate</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
15
src/components/ProtectedRoute.tsx
Normal file
15
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {Navigate, useLocation} from 'react-router-dom';
|
||||||
|
import {useAppSelector} from 'store';
|
||||||
|
import {selectIsAuthenticated} from 'store/slices/Authentication.slice';
|
||||||
|
|
||||||
|
export function ProtectedRoute({component}: {component: JSX.Element}): JSX.Element {
|
||||||
|
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" state={{from: location}} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
32
src/components/buttons/copy-icon-button/index.tsx
Normal file
32
src/components/buttons/copy-icon-button/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {useState} from 'react';
|
||||||
|
import {faCopy, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import IconButton from 'components/buttons/icon-button';
|
||||||
|
|
||||||
|
import {CopyButtonContainer} from './styles';
|
||||||
|
|
||||||
|
const iconChangeTimeout = 2000;
|
||||||
|
|
||||||
|
export const CopyIconButton = (props: {text: string; quoted?: boolean; displayText?: boolean; className?: string}): JSX.Element => {
|
||||||
|
const {text, quoted = false, displayText = true, className} = props;
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const copyToClipboard = (): void => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
setTimeout(() => setCopied(false), iconChangeTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CopyButtonContainer className={className}>
|
||||||
|
{displayText && (quoted ? `"${text}"` : text)}
|
||||||
|
<IconButton
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
tooltipText="Copy"
|
||||||
|
icon={copied ? faCheck : faCopy}
|
||||||
|
/>
|
||||||
|
</CopyButtonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
src/components/buttons/copy-icon-button/styles.ts
Normal file
9
src/components/buttons/copy-icon-button/styles.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
|
||||||
|
export const CopyButtonContainer = styled.default.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
36
src/components/buttons/export-file-button/index.tsx
Normal file
36
src/components/buttons/export-file-button/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {theme} from 'components/shared/theme';
|
||||||
|
import {Button} from 'components/buttons';
|
||||||
|
|
||||||
|
const {colors} = theme;
|
||||||
|
|
||||||
|
interface IExportFileButton {
|
||||||
|
label?: string;
|
||||||
|
file: string;
|
||||||
|
fileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExportFileButton = ({label = 'Export File', file, fileName = 'file'}: IExportFileButton): JSX.Element => {
|
||||||
|
const handleExport = () => {
|
||||||
|
const downloadUrl = URL.createObjectURL(new Blob([file]));
|
||||||
|
const linkTag = document.createElement('a');
|
||||||
|
|
||||||
|
linkTag.href = downloadUrl;
|
||||||
|
linkTag.setAttribute('target', '_blank');
|
||||||
|
linkTag.setAttribute('download', fileName);
|
||||||
|
linkTag.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="testId-exportFile"
|
||||||
|
backgroundColor={colors.red}
|
||||||
|
borderColor={colors.red}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
src/components/buttons/icon-button/icon-button.tsx
Normal file
32
src/components/buttons/icon-button/icon-button.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {IconProp} from '@fortawesome/fontawesome-svg-core';
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import {Position, Tooltip} from 'components/tooltip';
|
||||||
|
|
||||||
|
import {IconButtonContainer} from './styles';
|
||||||
|
|
||||||
|
interface IIconButton {
|
||||||
|
onClick: () => void;
|
||||||
|
tooltipText: string;
|
||||||
|
icon: IconProp;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconButton = ({
|
||||||
|
onClick,
|
||||||
|
tooltipText,
|
||||||
|
icon,
|
||||||
|
className
|
||||||
|
}: IIconButton) => (
|
||||||
|
<Tooltip position={Position.Bottom} message={tooltipText}>
|
||||||
|
<IconButtonContainer className={`icon-button ${className}`} role="link" tabIndex={-11} onKeyDown={null} onClick={onClick}>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
</IconButtonContainer>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default IconButton;
|
||||||
5
src/components/buttons/icon-button/index.tsx
Normal file
5
src/components/buttons/icon-button/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {default} from './icon-button';
|
||||||
30
src/components/buttons/icon-button/styles.ts
Normal file
30
src/components/buttons/icon-button/styles.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
|
||||||
|
export const IconButtonContainer = styled.default.div`
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
&.icon-button {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
`;
|
||||||
16
src/components/buttons/icon-buttons/add-button.tsx
Normal file
16
src/components/buttons/icon-buttons/add-button.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {IconButton} from './style';
|
||||||
|
import addIcon from 'assets/images/icon/hash-plus.svg';
|
||||||
|
|
||||||
|
interface IAddButton {
|
||||||
|
onClick: () => void;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddButton = ({onClick, className}: IAddButton): JSX.Element => (
|
||||||
|
<IconButton onClick={onClick} className={className}>
|
||||||
|
<img src={addIcon} alt={'Add'} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
5
src/components/buttons/icon-buttons/index.ts
Normal file
5
src/components/buttons/icon-buttons/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export * from './refresh-button';
|
||||||
|
export * from './add-button';
|
||||||
17
src/components/buttons/icon-buttons/refresh-button.tsx
Normal file
17
src/components/buttons/icon-buttons/refresh-button.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {IconButton} from './style';
|
||||||
|
|
||||||
|
import refreshIcon from 'assets/images/icon/refresh.svg';
|
||||||
|
|
||||||
|
interface IRefreshButton {
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshButton = ({onClick, disabled = false}: IRefreshButton): JSX.Element => (
|
||||||
|
<IconButton onClick={onClick} disabled={disabled}>
|
||||||
|
<img src={refreshIcon} alt={'Refresh'} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
17
src/components/buttons/icon-buttons/style.ts
Normal file
17
src/components/buttons/icon-buttons/style.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import {theme} from 'components/shared/theme';
|
||||||
|
|
||||||
|
const {fontSizeL, colors} = theme;
|
||||||
|
|
||||||
|
export const IconButton = styled.default.button`
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: ${fontSizeL};
|
||||||
|
color: ${colors.white};
|
||||||
|
opacity: ${({disabled}) => disabled ? 0.3 : 1};
|
||||||
|
cursor: ${({disabled}) => disabled ? 'not-allowed' : 'pointer'};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
57
src/components/buttons/index.tsx
Normal file
57
src/components/buttons/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import Theme from 'theme';
|
||||||
|
|
||||||
|
export const Button = styled.default.button<{
|
||||||
|
backgroundColor?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>`
|
||||||
|
${({backgroundColor, textColor, borderColor}) => styled.css`
|
||||||
|
color: ${textColor || Theme.colors.white};
|
||||||
|
background-color: ${backgroundColor || Theme.colors.white};
|
||||||
|
border-color: ${borderColor || backgroundColor || Theme.colors.lightRed};
|
||||||
|
`}
|
||||||
|
${({disabled}) => styled.css`
|
||||||
|
opacity: ${disabled ? 0.8 : 1};
|
||||||
|
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
||||||
|
`}
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
outline: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: ${Theme.primaryBorderRadius};
|
||||||
|
padding: ${Theme.spacing.small} ${Theme.spacing.medium};
|
||||||
|
font-size: ${Theme.typography.primaryFontSize};
|
||||||
|
transition: color .15s ease-in-out, background-color .15s ease-in-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FilterButton = styled.default(Button)`
|
||||||
|
font-weight: bolder;
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0 ${Theme.spacing.xSmall};
|
||||||
|
padding: ${Theme.spacing.small} ${Theme.spacing.xlarge};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ConfirmButton = styled.default(Button)`
|
||||||
|
margin-right: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CancelButton = styled.default(ConfirmButton)``;
|
||||||
|
|
||||||
|
export const CustomButton = styled.default(Button)`
|
||||||
|
background-color: ${Theme.dangerColor};
|
||||||
|
border-color: ${Theme.dangerColor};
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
font-weight: bolder;
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0.75rem;
|
||||||
|
padding: ${Theme.spacing.small} ${Theme.spacing.xlarge};
|
||||||
|
overflow: visible;
|
||||||
|
`;
|
||||||
88
src/components/buttons/radio-button/index.tsx
Normal file
88
src/components/buttons/radio-button/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {Fragment} from 'react';
|
||||||
|
import {Tooltip, Position} from 'components/tooltip';
|
||||||
|
import {Label} from 'components/label';
|
||||||
|
import {
|
||||||
|
RadioGroup,
|
||||||
|
RadioWrapper,
|
||||||
|
RadioButtonContainer,
|
||||||
|
VisibleCheckBox
|
||||||
|
} from './style';
|
||||||
|
|
||||||
|
interface IRadioItems {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
tooltipMessage?: string;
|
||||||
|
tooltipPosition?: Position;
|
||||||
|
className?: string;
|
||||||
|
children?: JSX.Element;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRadioButtonGroup {
|
||||||
|
items: IRadioItems[];
|
||||||
|
handleOnChange: (value) => void;
|
||||||
|
currentValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RadioButton = (props: {currentValue: string; value: string}) => {
|
||||||
|
const {currentValue, value} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioButtonContainer>
|
||||||
|
<input type="radio" readOnly={true} value={value} checked={currentValue === value}/>
|
||||||
|
<VisibleCheckBox checked={currentValue === value}><div/></VisibleCheckBox>
|
||||||
|
</RadioButtonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RadioButtonGroup = (props: IRadioButtonGroup): JSX.Element => {
|
||||||
|
const {items, handleOnChange, currentValue} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioGroup>
|
||||||
|
{items.map((
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
tooltipPosition,
|
||||||
|
tooltipMessage,
|
||||||
|
children,
|
||||||
|
className
|
||||||
|
}: IRadioItems,
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<RadioWrapper tabIndex={-1}
|
||||||
|
onKeyPress={() => null}
|
||||||
|
disabled={disabled}
|
||||||
|
className="button-container"
|
||||||
|
role="button"
|
||||||
|
key={label + index}
|
||||||
|
onClick={() => handleOnChange(value)}>
|
||||||
|
<RadioButton value={value} currentValue={currentValue} />
|
||||||
|
<Fragment>
|
||||||
|
{tooltipMessage ? (
|
||||||
|
<Tooltip position={tooltipPosition} message={tooltipMessage}>
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
text={label}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) :
|
||||||
|
<Label
|
||||||
|
className={className}
|
||||||
|
text={label}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{children}
|
||||||
|
</Fragment>
|
||||||
|
</RadioWrapper>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RadioButtonGroup;
|
||||||
57
src/components/buttons/radio-button/style.ts
Normal file
57
src/components/buttons/radio-button/style.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import {theme, paddings} from 'components/shared/theme';
|
||||||
|
|
||||||
|
const {
|
||||||
|
spacing,
|
||||||
|
primaryFontSize,
|
||||||
|
colors
|
||||||
|
} = theme;
|
||||||
|
|
||||||
|
export const RadioGroup = styled.default.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RadioWrapper = styled.default.div<{disabled?: boolean}>`
|
||||||
|
padding: ${paddings.small};
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
${({disabled}) => disabled && styled.css`
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .5;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RadioButtonContainer = styled.default.div`
|
||||||
|
margin-right: ${spacing.xSmall};
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VisibleCheckBox = styled.default.div<{checked?: boolean}>`
|
||||||
|
border: 2px solid ${colors.gray400};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: ${primaryFontSize};
|
||||||
|
height: ${primaryFontSize};
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
${({checked}) => checked && styled.css`
|
||||||
|
border: none;
|
||||||
|
background-color: ${colors.red};
|
||||||
|
|
||||||
|
div {
|
||||||
|
background-color: ${colors.black};
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
80
src/components/buttons/scroll-buttons/index.tsx
Normal file
80
src/components/buttons/scroll-buttons/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import {theme} from 'components/shared/theme';
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
|
import {
|
||||||
|
faBackward,
|
||||||
|
faFastBackward,
|
||||||
|
faFastForward,
|
||||||
|
faForward
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
const {colors, spacing} = theme;
|
||||||
|
const ScrollButton = styled.default.button`
|
||||||
|
border: none;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
color: ${colors.white};
|
||||||
|
margin: ${spacing.xxSmall} 0;
|
||||||
|
background-color: ${colors.gray600};
|
||||||
|
cursor: pointer;
|
||||||
|
transform: rotate(90deg)
|
||||||
|
`;
|
||||||
|
const TwinButtons = styled.default.div`
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
right: 64px;
|
||||||
|
bottom: 50px;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ScrollButtons = ({current}: {current: HTMLDivElement}): JSX.Element => {
|
||||||
|
const getScrollStep = current => {
|
||||||
|
const tableViewHeight = current.offsetHeight;
|
||||||
|
|
||||||
|
return tableViewHeight / 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
if (current) {
|
||||||
|
current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollTop = () => {
|
||||||
|
if (current) {
|
||||||
|
current.scrollTop = current.scrollTop - getScrollStep(current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollBottom = () => {
|
||||||
|
if (current) {
|
||||||
|
current.scrollTop = current.scrollTop + getScrollStep(current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (current) {
|
||||||
|
current.scrollTop = current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TwinButtons>
|
||||||
|
<ScrollButton onClick={scrollToTop}>
|
||||||
|
<FontAwesomeIcon icon={faFastBackward} />
|
||||||
|
</ScrollButton>
|
||||||
|
<ScrollButton onClick={scrollTop}>
|
||||||
|
<FontAwesomeIcon icon={faBackward} />
|
||||||
|
</ScrollButton>
|
||||||
|
<ScrollButton onClick={scrollBottom}>
|
||||||
|
<FontAwesomeIcon icon={faForward} />
|
||||||
|
</ScrollButton>
|
||||||
|
<ScrollButton onClick={scrollToBottom}>
|
||||||
|
<FontAwesomeIcon icon={faFastForward} />
|
||||||
|
</ScrollButton>
|
||||||
|
</TwinButtons>
|
||||||
|
);
|
||||||
|
};
|
||||||
134
src/components/forms/Input.tsx
Normal file
134
src/components/forms/Input.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {ChangeEvent, forwardRef, ForwardedRef, InputHTMLAttributes} from 'react';
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import Theme from 'theme';
|
||||||
|
import {Label} from '../label';
|
||||||
|
|
||||||
|
export interface IInput extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
error?: boolean;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
imagePath?: string;
|
||||||
|
imageAltText?: string;
|
||||||
|
label?: string;
|
||||||
|
labelColor?: string;
|
||||||
|
labelIcon?: JSX.Element;
|
||||||
|
labelClassName?: string;
|
||||||
|
helperText?: string;
|
||||||
|
helperTextClassName?: string;
|
||||||
|
width?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
colors,
|
||||||
|
typography,
|
||||||
|
formFieldWidth,
|
||||||
|
formFieldMaxWidth,
|
||||||
|
primaryBorderColor,
|
||||||
|
primaryBorderRadius,
|
||||||
|
primaryInputHeight,
|
||||||
|
inputIconWidth,
|
||||||
|
spacing
|
||||||
|
} = Theme;
|
||||||
|
|
||||||
|
export const InputElement = styled.default.input<IInput>`
|
||||||
|
background-color: ${colors.white};
|
||||||
|
border: 1px solid ${({error}) => error ? colors.lightRed : primaryBorderColor};
|
||||||
|
border-radius: ${primaryBorderRadius};
|
||||||
|
display: block;
|
||||||
|
font-size: ${typography.primaryFontSize};
|
||||||
|
height: ${primaryInputHeight};
|
||||||
|
line-height: ${typography.primaryLineHeight};
|
||||||
|
outline: none;
|
||||||
|
padding: ${spacing.small} ${({icon, imagePath}) => (icon || imagePath) ? spacing.xlarge : spacing.small};
|
||||||
|
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
background-position: 1rem center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: inherit;
|
||||||
|
opacity: ${({disabled}) => disabled ? 0.8 : 1};
|
||||||
|
cursor: ${({disabled}) => disabled && 'not-allowed'};
|
||||||
|
-webkit-text-fill-color: ${({disabled}) => disabled && colors.gray800};
|
||||||
|
`;
|
||||||
|
const HelperText = styled.default.p<IInput>`
|
||||||
|
color: ${({error}) => error ? colors.lightRed : colors.gray600};
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: ${typography.fontSizeS};
|
||||||
|
margin-top: ${spacing.xxSmall};
|
||||||
|
`;
|
||||||
|
const ImageWrapper = styled.default.div`
|
||||||
|
width: ${inputIconWidth}px;
|
||||||
|
height: ${inputIconWidth}px;
|
||||||
|
z-index: 1;
|
||||||
|
top: calc(50% - ${inputIconWidth / 2}px);
|
||||||
|
left: 8px;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& img, svg {
|
||||||
|
width: ${inputIconWidth}px;
|
||||||
|
height: ${inputIconWidth}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const InputWrapper = styled.default.div`
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InputComponentWrapper = styled.default.div<IInput>`
|
||||||
|
position: relative;
|
||||||
|
width: ${({width}) => width && isNaN(+width) ? width : ((width || formFieldWidth) + 'px')};
|
||||||
|
max-width: ${formFieldMaxWidth}px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
export const InputComponent = forwardRef((
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
labelColor,
|
||||||
|
labelIcon,
|
||||||
|
labelClassName,
|
||||||
|
icon,
|
||||||
|
imagePath,
|
||||||
|
imageAltText = '',
|
||||||
|
helperText,
|
||||||
|
helperTextClassName,
|
||||||
|
error,
|
||||||
|
width,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
...props
|
||||||
|
}: IInput, ref: ForwardedRef<HTMLInputElement>): JSX.Element => {
|
||||||
|
const InputIcon = icon || (imagePath && <img src={imagePath} alt={imageAltText} />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputComponentWrapper width={width}>
|
||||||
|
{!!label && (
|
||||||
|
<Label
|
||||||
|
text={label}
|
||||||
|
color={labelColor}
|
||||||
|
icon={labelIcon}
|
||||||
|
className={labelClassName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<InputWrapper>
|
||||||
|
<ImageWrapper>
|
||||||
|
{InputIcon}
|
||||||
|
</ImageWrapper>
|
||||||
|
<InputElement
|
||||||
|
icon={icon}
|
||||||
|
name={name}
|
||||||
|
imagePath={imagePath}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</InputWrapper>
|
||||||
|
{!!helperText && <HelperText className={helperTextClassName} error={error}>{helperText}</HelperText>}
|
||||||
|
</InputComponentWrapper>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default InputComponent;
|
||||||
23
src/components/forms/label/index.tsx
Normal file
23
src/components/forms/label/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {JSX} from 'react';
|
||||||
|
import {Label as StyledLabel} from './style';
|
||||||
|
|
||||||
|
interface ILabel {
|
||||||
|
text: string;
|
||||||
|
htmlFor?: string;
|
||||||
|
color?: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Label = ({text, htmlFor, color, icon, className}: ILabel): JSX.Element => (
|
||||||
|
<StyledLabel
|
||||||
|
className={className}
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
{text} {icon}
|
||||||
|
</StyledLabel>
|
||||||
|
);
|
||||||
15
src/components/forms/label/style.ts
Normal file
15
src/components/forms/label/style.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import Theme from 'theme';
|
||||||
|
|
||||||
|
const {spacing, colors, typography} = Theme;
|
||||||
|
|
||||||
|
export const Label = styled.default.label<{color?: string}>`
|
||||||
|
font-size: ${typography.fontSizeS};
|
||||||
|
color: ${({color}) => (color || colors.gray900)};
|
||||||
|
font-weight: bold;
|
||||||
|
margin: ${spacing.xxSmall} 0;
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
4
src/components/index.ts
Normal file
4
src/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './buttons';
|
||||||
|
export * from './loaders';
|
||||||
|
export * from './ProtectedRoute';
|
||||||
|
export * from './typography';
|
||||||
54
src/components/loaders/LoadingWheel.tsx
Normal file
54
src/components/loaders/LoadingWheel.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import Theme from 'theme';
|
||||||
|
|
||||||
|
interface LoadingWheelProps {
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
color?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingWheelContainer = styled.default.div<{
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
}>`
|
||||||
|
display: inline-block;
|
||||||
|
width: ${({ size }) => size}px;
|
||||||
|
height: ${({ size }) => size}px;
|
||||||
|
border: 3px solid ${({ color }) => color}20;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: ${({ color }) => color};
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoadingWheel: React.FC<LoadingWheelProps> = ({
|
||||||
|
size = 'medium',
|
||||||
|
color = Theme.colors.white,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
const sizeMap = {
|
||||||
|
small: Theme.loaderSize.small,
|
||||||
|
medium: Theme.loaderSize.medium,
|
||||||
|
large: Theme.loaderSize.large
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingWheelContainer
|
||||||
|
size={sizeMap[size]}
|
||||||
|
color={color}
|
||||||
|
className={className}
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingWheel;
|
||||||
4
src/components/loaders/index.ts
Normal file
4
src/components/loaders/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export * from './LoadingWheel';
|
||||||
85
src/components/shared/utils.ts
Normal file
85
src/components/shared/utils.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import moment, {DurationInputArg1, unitOfTime} from 'moment';
|
||||||
|
import PlatformDetectionService from 'services/platformDetection.service';
|
||||||
|
|
||||||
|
export const truncateWord = (word = '', length = 30): string => {
|
||||||
|
if (word.length >= length) {
|
||||||
|
return `${word.substring(0, length)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return word;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isEqual = (a: any, b: any): boolean => { // eslint-disable-line
|
||||||
|
if (typeof(a) === typeof(b)) {
|
||||||
|
if (typeof(a) === 'object') {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitWord = (string = ' ', seperator = ' '): string => {
|
||||||
|
return string.split(/(?=[A-Z])/).join(seperator);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeSimilarCSVFiles = (formerCSV: string, latterCSV: string): string => {
|
||||||
|
const formerCsvArray = formerCSV.split('\n').filter(line => line.trim() !== '');
|
||||||
|
const latterCsvArray = latterCSV.split('\n').filter(line => line.trim() !== '');
|
||||||
|
|
||||||
|
if (latterCsvArray.length > 1) {
|
||||||
|
return `${formerCsvArray.join('\n')}\n${latterCsvArray.splice(1).join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formerCSV;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chunk = (array: any[], size: number = 1) => { // eslint-disable-line
|
||||||
|
const arrayChunks = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i += size) {
|
||||||
|
const arrayChunk = array.slice(i, i + size);
|
||||||
|
|
||||||
|
arrayChunks.push(arrayChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayChunks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRangeByInterval = (start: Date, end: Date, intervalAmount: DurationInputArg1 = 24, intervalUnits: unitOfTime.DurationConstructor = 'hours') => {
|
||||||
|
const datesArray = [];
|
||||||
|
let currentDate = moment.utc(start);
|
||||||
|
const stopDate = moment.utc(end);
|
||||||
|
|
||||||
|
while (currentDate.isSameOrBefore(stopDate)) {
|
||||||
|
datesArray.push(currentDate.toISOString());
|
||||||
|
currentDate = currentDate.add(intervalAmount, intervalUnits);
|
||||||
|
}
|
||||||
|
|
||||||
|
return datesArray.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBrowserLimits = (): IBrowserLimits => browserLimits[PlatformDetectionService.browserName.toLowerCase()];
|
||||||
|
|
||||||
|
export const getMaxByPropertyName = (
|
||||||
|
array: Record<string, number | string | boolean>[],
|
||||||
|
propertyName: string
|
||||||
|
): number => {
|
||||||
|
let length = array.length;
|
||||||
|
let max = -Infinity;
|
||||||
|
|
||||||
|
while (length--) {
|
||||||
|
const element = parseFloat(array[length][propertyName] as string);
|
||||||
|
|
||||||
|
if (isFinite(element) && element > max) {
|
||||||
|
max = element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
};
|
||||||
31
src/components/typography/index.tsx
Normal file
31
src/components/typography/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import Theme from 'theme';
|
||||||
|
|
||||||
|
|
||||||
|
export const H1 = styled.default.h1`
|
||||||
|
font-family: ${Theme.typography.primaryFont};
|
||||||
|
font-size: ${Theme.typography.fontSizeXxl};
|
||||||
|
font-weight: lighter;
|
||||||
|
${Theme.screenSizes.mediaPhone}{
|
||||||
|
font-size: ${Theme.typography.fontSizeXl};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const P = styled.default.p`
|
||||||
|
font-family: ${Theme.typography.primaryFont};
|
||||||
|
font-size: ${Theme.typography.primaryFontSize};
|
||||||
|
font-weight: normal;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Heading = styled.default.h2`
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
font-size: ${Theme.typography.fontSizeXl};
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WhiteText = styled.default(P)`
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
`;
|
||||||
27
src/constants/browser-limits.ts
Normal file
27
src/constants/browser-limits.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IBrowserLimits {
|
||||||
|
bytes: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const browserLimits = {
|
||||||
|
chrome: {
|
||||||
|
bytes: 563085312,
|
||||||
|
value: '537 MB'
|
||||||
|
},
|
||||||
|
firefox: {
|
||||||
|
bytes: 1073741824,
|
||||||
|
value: '1 G'
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
bytes: 524288000,
|
||||||
|
value: '500 MB'
|
||||||
|
},
|
||||||
|
safari: {
|
||||||
|
bytes: 1342177280,
|
||||||
|
value: '1.25G'
|
||||||
|
}
|
||||||
|
};
|
||||||
268
src/constants/capabilities.ts
Normal file
268
src/constants/capabilities.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import { IAdvancedSelectItem } from 'components/ui/advanced-select';
|
||||||
|
|
||||||
|
export enum VideoQuality {
|
||||||
|
Xhd = 'xhd',
|
||||||
|
Fhd = 'fhd',
|
||||||
|
Hd = 'hd',
|
||||||
|
Sd = 'sd',
|
||||||
|
Ld = 'ld',
|
||||||
|
Vld = 'vld',
|
||||||
|
Uld = 'uld',
|
||||||
|
AudioOnly = 'audio-only'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CapabilitiesSet {
|
||||||
|
TrackIsolation = 'track-isolation',
|
||||||
|
AspectRatio = 'aspect-ratio',
|
||||||
|
ResolutionLimit = 'resolution-limit',
|
||||||
|
EncodingJitterBuffer = 'encoding-jitter-buffer',
|
||||||
|
EncodingProfile = 'encoding-profile',
|
||||||
|
PlayoutBuffer = 'playout-buffer',
|
||||||
|
Ingest = 'ingest',
|
||||||
|
MultiBitrateMode = 'multi-bitrate-mode',
|
||||||
|
Quality = 'quality'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CapabilitiesType {
|
||||||
|
RemoteUriPublishing = 'remote-uri-publishing',
|
||||||
|
Publishing = 'publishing',
|
||||||
|
Viewing = 'viewing',
|
||||||
|
Forking = 'forking',
|
||||||
|
Quality = 'quality'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const capabilities: IAdvancedSelectItem[] = [
|
||||||
|
{
|
||||||
|
value: VideoQuality.Xhd,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Fhd,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Hd,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Sd,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Ld,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Vld,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VideoQuality.Uld,
|
||||||
|
type: [CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'prefer-vp8',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'prefer-vp9',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'prefer-h264',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'streaming',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Viewing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'on-demand',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Viewing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'streaming-lite',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['streaming']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'on-demand-lite',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['on-demand']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'multi-bitrate',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.MultiBitrateMode]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'multi-bitrate-contribution',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.MultiBitrateMode]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'multi-bitrate-codec=vp8',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'multi-bitrate-codec=vp9',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'multi-bitrate-codec=h264',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'aspect-ratio=16x9',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate'],
|
||||||
|
set: [CapabilitiesSet.AspectRatio]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'aspect-ratio=4x3',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate'],
|
||||||
|
set: [CapabilitiesSet.AspectRatio]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'aspect-ratio=9x16',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate'],
|
||||||
|
set: [CapabilitiesSet.AspectRatio]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'aspect-ratio=3x4',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate'],
|
||||||
|
set: [CapabilitiesSet.AspectRatio]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'resolution-limit=480',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
set: [CapabilitiesSet.ResolutionLimit]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'resolution-limit=720',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
set: [CapabilitiesSet.ResolutionLimit]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'bitrate-limit=1000000',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-jitter-buffer=PT0.5S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.EncodingJitterBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-jitter-buffer=PT1.0S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.EncodingJitterBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-jitter-buffer=PT2.0S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.EncodingJitterBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-profile=phenix-2020-1080p',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate', 'multi-bitrate-contribution'],
|
||||||
|
set: [CapabilitiesSet.EncodingProfile]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-profile=phenix-2020-720p',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate', 'multi-bitrate-contribution'],
|
||||||
|
set: [CapabilitiesSet.EncodingProfile]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'encoding-profile=phenix-2020-480p',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['multi-bitrate', 'multi-bitrate-contribution'],
|
||||||
|
set: [CapabilitiesSet.EncodingProfile]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'playout-buffer=PT0.3S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.PlayoutBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'playout-buffer=PT0.5S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.PlayoutBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'playout-buffer=PT0.8S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.PlayoutBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'playout-buffer=PT1.0S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.PlayoutBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'playout-buffer=PT2.0S',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.PlayoutBuffer]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'audio-only',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Viewing, CapabilitiesType.Forking, CapabilitiesType.Quality],
|
||||||
|
set: [CapabilitiesSet.TrackIsolation, CapabilitiesSet.Quality]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'video-only',
|
||||||
|
type: [CapabilitiesType.Viewing, CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
set: [CapabilitiesSet.TrackIsolation]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'high-fidelity',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'on-demand-archive=PT1D',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing, CapabilitiesType.Forking],
|
||||||
|
dependency: ['on-demand']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'token-auth',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
dependency: ['streaming']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'source-uri-ingest',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
set: [CapabilitiesSet.Ingest]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mpegts-unicast-ingest',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
set: [CapabilitiesSet.Ingest]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mpegts-multicast-ingest',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing],
|
||||||
|
set: [CapabilitiesSet.Ingest]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'time-shift-manifest',
|
||||||
|
type: [CapabilitiesType.RemoteUriPublishing, CapabilitiesType.Publishing]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'replay',
|
||||||
|
type: [CapabilitiesType.Viewing]
|
||||||
|
}
|
||||||
|
];
|
||||||
15
src/constants/data.ts
Normal file
15
src/constants/data.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export const legacyStreamTokens = [
|
||||||
|
'streamTokenForBroadcastStream',
|
||||||
|
'streamTokenForLiveStream',
|
||||||
|
'streamTokenForLiveStreamWithDrmOpenAccess',
|
||||||
|
'streamTokenForLiveStreamWithDrmHollywood',
|
||||||
|
'streamToken'
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum ViewContextTypes {
|
||||||
|
Channel = 'channel',
|
||||||
|
Room = 'room'
|
||||||
|
}
|
||||||
178
src/constants/error-messages.ts
Normal file
178
src/constants/error-messages.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
const commonErrorMessages = {
|
||||||
|
unauthorized: 'The streaming platform was not able to authorize the provided credentials',
|
||||||
|
capacity: 'The system is temporarily overloaded. Please try again later',
|
||||||
|
'rate-limited': 'The system is temporarily overloaded. Please try again later',
|
||||||
|
'time-exceeded': 'The request took to long to execute. Please try again later'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reportsErrorMessages = {
|
||||||
|
'excessive-report-interval': 'The interval for the report exceed 1 month',
|
||||||
|
'period-start-outside-supported-window': 'The year of the start of period is more than 1 year before the current year',
|
||||||
|
'period-end-must-be-in-past': 'The end of period is in future',
|
||||||
|
'period-end-must-be-after-start': 'The end of period is less than or equal to the start of the period',
|
||||||
|
'malformed-version-in-request-query-parameter': 'The supplied version in HTTP query parameter does not conform to the format "YYYY-MM-dd"',
|
||||||
|
'version-does-not-exist': 'The supplied version in HTTP query parameter does not exist',
|
||||||
|
unsupported: 'The parameter combination is not supported',
|
||||||
|
unauthorized: 'The streaming platform was not able to authorize the provided credentials',
|
||||||
|
'request-timeout': 'Request timed out likely due to temporary resource or network conditions. Please try again',
|
||||||
|
capacity: 'The system is temporarily overloaded. Please try again later',
|
||||||
|
default: 'Unable to generate report'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const currentActivityErrorMessages = {
|
||||||
|
'socket-not-authenticated': 'Socket not authenticated',
|
||||||
|
'unable-to-fetch-data': 'Unable to fetch active users data. Please try again',
|
||||||
|
'by-country-missing': 'Missing data sorted by country'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const summaryErrorMessages = {
|
||||||
|
'socket-not-authenticated': 'Socket not authenticated',
|
||||||
|
'unable-to-fetch-data': 'Unable to fetch usage statistics data. Please try again'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const timeToFirstFrameErrorMessages = {
|
||||||
|
'socket-not-authenticated': 'Socket not authenticated',
|
||||||
|
'unable-to-fetch-data': 'Unable to fetch time to first frame data. Please try again'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usageErrorMessages = {
|
||||||
|
'socket-not-authenticated': 'Socket not authenticated',
|
||||||
|
'unable-to-fetch-data': 'Unable to fetch usage statistics data. Please try again'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const concurrentAndIngestErrorMessages = {
|
||||||
|
...reportsErrorMessages,
|
||||||
|
'excessive-report-interval': 'The interval for the report exceed 1 day',
|
||||||
|
default: 'Unable to fetch data for the report'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forkingHistoryErrorMessages = {
|
||||||
|
...reportsErrorMessages,
|
||||||
|
'excessive-report-interval': 'The interval for the report exceed 1 day',
|
||||||
|
'maximal-result-size-exceeded': 'The size of the result exceeds the maximum allowed (10GB)',
|
||||||
|
'unable-to-clear-forking-history': 'Unable to clear forking history',
|
||||||
|
'unable-to-change-forking-history-data': 'Unable to change forking history data'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishingHistoryErrorMessages = {
|
||||||
|
...reportsErrorMessages,
|
||||||
|
'excessive-report-interval': 'The interval for the report exceed 1 year',
|
||||||
|
'maximal-result-size-exceeded': 'The size of the result exceeds the maximum allowed (10GB)',
|
||||||
|
'unable-to-clear-publishing-history': 'Unable to clear publishing history',
|
||||||
|
'unable-to-change-publishing-history-data': 'Unable to change publishing history data'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const messagesPageErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The channel does not exist',
|
||||||
|
default: 'Unable to fetch messages'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const channelListErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to fetch channels list'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const channelListPublishingStateErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The channel was not found',
|
||||||
|
default: 'Unable to fetch publishing state'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roomsListErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to fetch rooms list'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roomsListPublishingStateErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The room was not found',
|
||||||
|
default: 'Unable to fetch publishing state'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forkChannelErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'One or both of the source and destination channels are not found',
|
||||||
|
default: 'Unable to fork a channel'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteChannelErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to delete a channel'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteRoomErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to delete a room'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createChannelErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'already-exists': 'The channel already exists',
|
||||||
|
'type-conflict': 'A room with the provided alias already exists',
|
||||||
|
default: 'Unable to create a channel'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRoomErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'already-exists': 'The room already exists',
|
||||||
|
'type-conflict': 'A channel with the provided alias already exists',
|
||||||
|
default: 'Unable to create a room'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const killChannelErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The channel was not found',
|
||||||
|
default: 'Unable to kill a channel'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tokenErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to generate a token'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const playlistErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to fetch playlist data'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const channelDetailsErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The channel does not exist',
|
||||||
|
default: 'Unable to fetch channel details'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const channelStreamsErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not found': 'The channel was not found',
|
||||||
|
default: 'Unable to fetch streams for this channel'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roomDetailsErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The room does not exist',
|
||||||
|
default: 'Unable to fetch room details'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roomMembersErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
'not-found': 'The room was not found',
|
||||||
|
default: 'Unable to fetch members for this room'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishPullErrorMessages = {
|
||||||
|
...commonErrorMessages,
|
||||||
|
default: 'Unable to publish with uri'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const qosErrorMessages = {
|
||||||
|
...reportsErrorMessages,
|
||||||
|
...commonErrorMessages,
|
||||||
|
'excessive-report-interval': 'The interval for the report exceed 1 day',
|
||||||
|
unsupported: 'The parameter combination is not supported',
|
||||||
|
'request-timeout': 'Request timed out likely due to temporary resource or network conditions. Please try again',
|
||||||
|
capacity: 'The system is temporarily overloaded. Please try again later'
|
||||||
|
};
|
||||||
7
src/constants/index.ts
Normal file
7
src/constants/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export * from './capabilities';
|
||||||
|
export * from './data';
|
||||||
|
export * from './links';
|
||||||
|
export * from './error-messages';
|
||||||
26
src/constants/links.ts
Normal file
26
src/constants/links.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export const documentationLinks = {
|
||||||
|
portal: 'https://phenixrts.com/docs/portal/',
|
||||||
|
createChannel: 'https://phenixrts.com/docs/sdk_ref/rest-api/channel/#creating-a-channel',
|
||||||
|
deleteChannel: 'https://phenixrts.com/docs/sdk_ref/rest-api/channel/#deleting-a-channel',
|
||||||
|
forkChannel: 'https://phenixrts.com/docs/sdk_ref/rest-api/channel/#fork-a-channel',
|
||||||
|
killChannel: 'https://phenixrts.com/docs/sdk_ref/rest-api/channel/#kill-a-channel',
|
||||||
|
createRoom: 'https://phenixrts.com/docs/sdk_ref/rest-api/room/#creating-a-room',
|
||||||
|
deleteRoom: 'https://phenixrts.com/docs/sdk_ref/rest-api/room/#deleting-a-room',
|
||||||
|
terminateStream: 'https://phenixrts.com/docs/sdk_ref/rest-api/overview/#terminating-a-stream',
|
||||||
|
RTMP: 'https://phenixrts.com/docs/integration-guides/rtmp/',
|
||||||
|
FFmpeg: 'https://phenixrts.com/docs/integration-guides/3rd-party-encoders/ffmpeg/',
|
||||||
|
WHIP: 'https://phenixrts.com/docs/integration-guides/whip/',
|
||||||
|
supportedStreamCapabilities: 'https://phenixrts.com/docs/knowledge-base/reference/capabilities/#supported-stream-capabilities',
|
||||||
|
releaseNotes: 'https://phenixrts.com/docs/portal/portal-release-notes/'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const phenixWebSiteLinks = {
|
||||||
|
privacyPolicyProduction: 'https://www.phenixrts.com/privacy-policy',
|
||||||
|
privacyPolicyStaging: 'https://www-stg.phenixrts.com/privacy-policy',
|
||||||
|
termsOfServiceProduction: 'https://www.phenixrts.com/terms-of-service',
|
||||||
|
termsOfServiceStaging: 'https://www-stg.phenixrts.com/terms-of-service',
|
||||||
|
mainWebSite: 'https://www.phenixrts.com'
|
||||||
|
};
|
||||||
23
src/declaredTypes.d.ts
vendored
Normal file
23
src/declaredTypes.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
type WindowProps = Window & typeof globalThis & {URL: URL};
|
||||||
|
|
||||||
|
type JsonSimpleType = number | string | boolean | null | undefined;
|
||||||
|
|
||||||
|
type JsonObjType = {
|
||||||
|
[key: string]: JsonSimpleType | JsonSimpleType[] | JsonObjType | JsonObjType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type JsonType = JsonSimpleType | JsonSimpleType[] | JsonObjType | JsonObjType[];
|
||||||
|
|
||||||
|
declare module 'config/version.json' {
|
||||||
|
const value: {version: string};
|
||||||
|
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module '*.gif';
|
||||||
|
declare module '*.png';
|
||||||
|
declare module '*.jpg';
|
||||||
|
declare module '*.jpeg';
|
||||||
|
declare module '*.svg';
|
||||||
@@ -1,68 +1,4 @@
|
|||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
padding: 0;
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
import {Provider} from 'react-redux';
|
import {Provider} from 'react-redux';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import App from './App.tsx';
|
import App from './App';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
21
src/routers/index.tsx
Normal file
21
src/routers/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {BrowserRouter, Route, Routes, Navigate} from 'react-router-dom';
|
||||||
|
import {ProtectedRoute} from 'components';
|
||||||
|
import {LoginForm, ChannelList} from 'views';
|
||||||
|
|
||||||
|
export default function Router() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
{/* Public routes */}
|
||||||
|
<Route path="/login" element={<LoginForm />} />
|
||||||
|
|
||||||
|
{/* Protected routes */}
|
||||||
|
<Route path="/" element={<Navigate to="/channels" replace />} />
|
||||||
|
<Route path="/channels" element={<ProtectedRoute component={<ChannelList />} />} />
|
||||||
|
|
||||||
|
{/* Fallback route */}
|
||||||
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/services/PCastApi.service.ts
Normal file
41
src/services/PCastApi.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {ApplicationCredentials, Channels, PCastApi, Reporting, Streams} from '@techniker-me/pcast-api';
|
||||||
|
|
||||||
|
export default class PCastApiService {
|
||||||
|
private static _instance: PCastApi;
|
||||||
|
|
||||||
|
public static initialize(pcastUri: string, applciationCredentials: ApplicationCredentials) {
|
||||||
|
PCastApiService._instance = PCastApi.create(pcastUri, applciationCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): PCastApiService {
|
||||||
|
if (!PCastApiService._instance) {
|
||||||
|
throw new Error('PCastApiService has not been initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return PCastApiService._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get channels(): Channels {
|
||||||
|
if (!PCastApiService._instance) {
|
||||||
|
throw new Error('PCastApiService has not been initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return PCastApiService._instance.channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get streams(): Streams {
|
||||||
|
if (!PCastApiService._instance) {
|
||||||
|
throw new Error('PCastApiService has not been initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return PCastApiService._instance.streams;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get reporting(): Reporting {
|
||||||
|
if (!PCastApiService._instance) {
|
||||||
|
throw new Error('PCastApiService has not been initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return PCastApiService._instance.reporting;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/services/UserDataStore/IUserDataStore.ts
Normal file
6
src/services/UserDataStore/IUserDataStore.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default interface IUserDataStore {
|
||||||
|
setItem(key: string, value: string): void;
|
||||||
|
getItem(key: string): string | null;
|
||||||
|
removeItem(key: string): void;
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
23
src/services/UserDataStore/IndexedDB.ts
Normal file
23
src/services/UserDataStore/IndexedDB.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import IUserDataStore from './IUserDataStore';
|
||||||
|
|
||||||
|
export class IndexedDB implements IUserDataStore {
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return 'indexedDB' in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem(key: string): string | null {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItem(key: string, value: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(key: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/services/UserDataStore/LocalStorage.ts
Normal file
23
src/services/UserDataStore/LocalStorage.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import IUserDataStore from './IUserDataStore';
|
||||||
|
|
||||||
|
export class LocalStorage implements IUserDataStore {
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return 'localStorage' in window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem(key: string): string | null {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItem(key: string, value: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(key: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/services/UserDataStore/ObjectStore.ts
Normal file
23
src/services/UserDataStore/ObjectStore.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import IUserDataStore from './IUserDataStore';
|
||||||
|
|
||||||
|
export class ObjectStrore implements IUserDataStore {
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItem(key: string): string | null {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public setItem(key: string, value: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(key: string): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/services/UserDataStore/index.ts
Normal file
24
src/services/UserDataStore/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import IUserDataStore from './IUserDataStore';
|
||||||
|
import {IndexedDB} from './IndexedDB';
|
||||||
|
import {LocalStorage} from './LocalStorage';
|
||||||
|
import {ObjectStrore} from './ObjectStore';
|
||||||
|
|
||||||
|
class UserDataStoreService {
|
||||||
|
private static _instance: IUserDataStore;
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (IndexedDB.isSupported()) {
|
||||||
|
this._instance = new IndexedDB();
|
||||||
|
} else if (LocalStorage.isSupported()) {
|
||||||
|
this._instance = new LocalStorage();
|
||||||
|
} else {
|
||||||
|
this._instance = new ObjectStrore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): IUserDataStore {
|
||||||
|
return this._instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserDataStoreService.getInstance();
|
||||||
@@ -20,6 +20,7 @@ export interface IPhenixWebSocketResponse {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
redirect: string;
|
redirect: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PhenixWebSocket extends MQWebSocket {
|
export class PhenixWebSocket extends MQWebSocket {
|
||||||
@@ -60,7 +61,9 @@ export class PhenixWebSocket extends MQWebSocket {
|
|||||||
|
|
||||||
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
|
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
|
||||||
if (this._status.value !== PhenixWebSocketStatus.Online) {
|
if (this._status.value !== PhenixWebSocketStatus.Online) {
|
||||||
throw new Error(`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`);
|
throw new Error(
|
||||||
|
`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._pendingRequests++;
|
this._pendingRequests++;
|
||||||
@@ -89,27 +92,27 @@ export class PhenixWebSocket extends MQWebSocket {
|
|||||||
private initialize(): void {
|
private initialize(): void {
|
||||||
super.onEvent('connected', () => {
|
super.onEvent('connected', () => {
|
||||||
this.setStatus(PhenixWebSocketStatus.Online);
|
this.setStatus(PhenixWebSocketStatus.Online);
|
||||||
})
|
});
|
||||||
|
|
||||||
super.onEvent('disconnected', () => {
|
super.onEvent('disconnected', () => {
|
||||||
this.setStatus(PhenixWebSocketStatus.Offline);
|
this.setStatus(PhenixWebSocketStatus.Offline);
|
||||||
})
|
});
|
||||||
|
|
||||||
super.onEvent('error', (error: unknown) => {
|
super.onEvent('error', (error: unknown) => {
|
||||||
this._logger.error('Error [%s]', error);
|
this._logger.error('Error [%s]', error);
|
||||||
this.setStatus(PhenixWebSocketStatus.Error);
|
this.setStatus(PhenixWebSocketStatus.Error);
|
||||||
})
|
});
|
||||||
|
|
||||||
super.onEvent('reconnecting', () => {
|
super.onEvent('reconnecting', () => {
|
||||||
this.setStatus(PhenixWebSocketStatus.Reconnecting);
|
this.setStatus(PhenixWebSocketStatus.Reconnecting);
|
||||||
})
|
});
|
||||||
|
|
||||||
super.onEvent('reconnected', () => {
|
super.onEvent('reconnected', () => {
|
||||||
this.setStatus(PhenixWebSocketStatus.Online);
|
this.setStatus(PhenixWebSocketStatus.Online);
|
||||||
})
|
});
|
||||||
|
|
||||||
super.onEvent('timeout', () => {
|
super.onEvent('timeout', () => {
|
||||||
this.setStatus(PhenixWebSocketStatus.Error);
|
this.setStatus(PhenixWebSocketStatus.Error);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
191
src/services/platform-detection.service.ts
Normal file
191
src/services/platform-detection.service.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type NavigatorUAData = {
|
||||||
|
brands?: { brand: string; version: string }[];
|
||||||
|
mobile?: boolean;
|
||||||
|
platform?: string;
|
||||||
|
getHighEntropyValues?: (hints: string[]) => Promise<Record<string, string>>;
|
||||||
|
toJSON?: () => object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PlatformDetectionService {
|
||||||
|
private static readonly _userAgent: string = globalThis.navigator?.userAgent ?? '';
|
||||||
|
// @ts-expect-error NavigatorUAData is experimental and not defined in the lib dom yet https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
|
||||||
|
private static readonly _userAgentData: NavigatorUAData | undefined = globalThis.navigator?.userAgentData;
|
||||||
|
|
||||||
|
private static readonly _areClientHintsSupported: boolean = !!PlatformDetectionService._userAgentData;
|
||||||
|
private static _platform: string = 'Unknown';
|
||||||
|
private static _platformVersion: string = '';
|
||||||
|
private static _browserName: string = 'Unknown';
|
||||||
|
private static _browserVersion: string = '?';
|
||||||
|
private static _isWebview: boolean = false;
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (PlatformDetectionService._areClientHintsSupported) {
|
||||||
|
PlatformDetectionService.initFromClientHints();
|
||||||
|
} else {
|
||||||
|
PlatformDetectionService.initFromUserAgent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('PlatformDetectionService is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Public API ----
|
||||||
|
static get platform(): string {
|
||||||
|
return PlatformDetectionService._platform;
|
||||||
|
}
|
||||||
|
static get platformVersion(): string {
|
||||||
|
return PlatformDetectionService._platformVersion;
|
||||||
|
}
|
||||||
|
static get userAgent(): string {
|
||||||
|
return PlatformDetectionService._userAgent;
|
||||||
|
}
|
||||||
|
static get browserName(): string {
|
||||||
|
return PlatformDetectionService._browserName;
|
||||||
|
}
|
||||||
|
static get browserVersion(): string {
|
||||||
|
return PlatformDetectionService._browserVersion;
|
||||||
|
}
|
||||||
|
static get isWebview(): boolean {
|
||||||
|
return PlatformDetectionService._isWebview;
|
||||||
|
}
|
||||||
|
static get areClientHintsSupported(): boolean {
|
||||||
|
return PlatformDetectionService._areClientHintsSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional async initialization for high-entropy values like platformVersion
|
||||||
|
*/
|
||||||
|
static async initAsync(): Promise<void> {
|
||||||
|
if (PlatformDetectionService._areClientHintsSupported && PlatformDetectionService._userAgentData?.getHighEntropyValues) {
|
||||||
|
const values = await PlatformDetectionService._userAgentData.getHighEntropyValues(['platformVersion']);
|
||||||
|
|
||||||
|
if (values.platformVersion) {
|
||||||
|
PlatformDetectionService._platformVersion = values.platformVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Init strategies ----
|
||||||
|
private static initFromClientHints() {
|
||||||
|
const data = PlatformDetectionService._userAgentData as NavigatorUAData;
|
||||||
|
const nonChromiumBrand = data.brands?.find(b => b.brand !== 'Chromium');
|
||||||
|
|
||||||
|
PlatformDetectionService._browserName = nonChromiumBrand?.brand ?? 'Unknown';
|
||||||
|
PlatformDetectionService._browserVersion = nonChromiumBrand?.version ?? '?';
|
||||||
|
PlatformDetectionService._platform = data.platform ?? 'Unknown';
|
||||||
|
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent(); // Fallback check
|
||||||
|
}
|
||||||
|
|
||||||
|
private static initFromUserAgent() {
|
||||||
|
PlatformDetectionService._platform = PlatformDetectionService.extractPlatformFromUserAgent();
|
||||||
|
PlatformDetectionService._platformVersion = PlatformDetectionService.extractPlatformVersionFromUserAgent();
|
||||||
|
PlatformDetectionService._browserName = PlatformDetectionService.extractBrowserNameFromUserAgent();
|
||||||
|
PlatformDetectionService._browserVersion = PlatformDetectionService.extractBrowserVersionFromUserAgent();
|
||||||
|
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helpers ----
|
||||||
|
private static extractBrowserNameFromUserAgent(): string {
|
||||||
|
if (/Edg\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Edge';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/OPR\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Opera';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Firefox\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Firefox';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Trident\/.*rv:/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'IE';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Chrome\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Chrome';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Safari\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Safari';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/ReactNative\//.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'ReactNative';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractBrowserVersionFromUserAgent(): string {
|
||||||
|
return (
|
||||||
|
PlatformDetectionService.matchVersion(/Edg\/([\d.]+)/) ??
|
||||||
|
PlatformDetectionService.matchVersion(/OPR\/([\d.]+)/) ??
|
||||||
|
PlatformDetectionService.matchVersion(/Firefox\/([\d.]+)/) ??
|
||||||
|
PlatformDetectionService.matchVersion(/rv:([\d.]+)/) ?? // IE
|
||||||
|
PlatformDetectionService.matchVersion(/Chrome\/([\d.]+)/) ??
|
||||||
|
PlatformDetectionService.matchVersion(/Version\/([\d.]+)/) ?? // Safari often uses "Version/"
|
||||||
|
PlatformDetectionService.matchVersion(/Safari\/([\d.]+)/) ??
|
||||||
|
PlatformDetectionService.matchVersion(/ReactNative\/([\d.]+)/) ??
|
||||||
|
'?'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractPlatformFromUserAgent(): string {
|
||||||
|
if (/Windows/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Windows';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/iPhone|iPad|iPod/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'iOS';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Mac OS X/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'macOS';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Android/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Android';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Linux/.test(PlatformDetectionService._userAgent)) {
|
||||||
|
return 'Linux';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractPlatformVersionFromUserAgent(): string {
|
||||||
|
switch (PlatformDetectionService._platform) {
|
||||||
|
case 'Windows':
|
||||||
|
return PlatformDetectionService.matchVersion(/Windows NT ([\d.]+)/) ?? '';
|
||||||
|
case 'iOS':
|
||||||
|
return PlatformDetectionService.matchVersion(/OS ([\d_]+)/)?.replace(/_/g, '.') ?? '';
|
||||||
|
case 'macOS':
|
||||||
|
return PlatformDetectionService.matchVersion(/Mac OS X ([\d_]+)/)?.replace(/_/g, '.') ?? '';
|
||||||
|
case 'Android':
|
||||||
|
return PlatformDetectionService.matchVersion(/Android ([\d.]+)/) ?? '';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractIsWebviewFromUserAgent(): boolean {
|
||||||
|
return (
|
||||||
|
/; wv/.test(PlatformDetectionService._userAgent) || // Android webview
|
||||||
|
(/Android/.test(PlatformDetectionService._userAgent) && /Version\/[\d.]+/.test(PlatformDetectionService._userAgent)) || // Some Android webviews
|
||||||
|
/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/.test(PlatformDetectionService._userAgent) // IOS webview
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static matchVersion(pattern: RegExp): string | null {
|
||||||
|
const match = PlatformDetectionService._userAgent.match(pattern);
|
||||||
|
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/store/middlewares/authenticationMiddleware.ts
Normal file
73
src/store/middlewares/authenticationMiddleware.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
authenticateCredentialsThunk,
|
||||||
|
setError,
|
||||||
|
selectIsAuthenticated,
|
||||||
|
selectIsLoading,
|
||||||
|
selectApplicationId,
|
||||||
|
selectSecret,
|
||||||
|
setUnauthorized
|
||||||
|
} from 'store/slices/Authentication.slice';
|
||||||
|
import {Middleware} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const authenticateRequestMiddleware: Middleware = store => next => async action => {
|
||||||
|
const state = store.getState();
|
||||||
|
const isAuthenticated = selectIsAuthenticated(state);
|
||||||
|
const isLoading = selectIsLoading(state);
|
||||||
|
const applicationId = selectApplicationId(state);
|
||||||
|
const secret = selectSecret(state);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[authenticateRequest] action [%o] isAuthenticated [%o] isLoading [%o] applicationId [%o] secret [%o]',
|
||||||
|
action,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
applicationId,
|
||||||
|
secret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip authentication middleware for authentication-related actions
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof action === 'object' &&
|
||||||
|
action !== null &&
|
||||||
|
'type' in action &&
|
||||||
|
typeof (action as any).type === 'string' &&
|
||||||
|
(action as any).type.startsWith('authentication/')
|
||||||
|
) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already authenticated, proceed normally
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If currently loading, wait for it to complete
|
||||||
|
if (isLoading) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no credentials, set unauthorized
|
||||||
|
if (!applicationId || !secret) {
|
||||||
|
console.log('[authenticateRequest] No credentials available, proceeding with action');
|
||||||
|
return next(setUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have credentials but are not authenticated, try to authenticate
|
||||||
|
try {
|
||||||
|
console.log('[authenticateRequest] Attempting auto-authentication');
|
||||||
|
// Use the Redux thunk to properly update the state
|
||||||
|
const authResult = await store.dispatch(authenticateCredentialsThunk({applicationId, secret}) as any);
|
||||||
|
|
||||||
|
if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') {
|
||||||
|
console.log('[authenticateRequest] Authentication failed');
|
||||||
|
return next(setUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[authenticateRequest] Auto-authentication successful, proceeding with action');
|
||||||
|
return next(action);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[authenticateRequest] Auto-authentication failed:', error);
|
||||||
|
return next(setUnauthorized());
|
||||||
|
}
|
||||||
|
};
|
||||||
3
src/store/middlewares/index.ts
Normal file
3
src/store/middlewares/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './authenticationMiddleware';
|
||||||
|
export * from './promiseMiddleware';
|
||||||
|
export * from './loggerMiddleware';
|
||||||
13
src/store/middlewares/loggerMiddleware.ts
Normal file
13
src/store/middlewares/loggerMiddleware.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import {Middleware} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs all actions and states after they are dispatched.
|
||||||
|
*/
|
||||||
|
export const loggerMiddleware: Middleware = store => next => action => {
|
||||||
|
console.group((action as any).type);
|
||||||
|
console.info('dispatching', action);
|
||||||
|
const result = next(action);
|
||||||
|
console.log('next state', store.getState());
|
||||||
|
console.groupEnd();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
9
src/store/middlewares/promiseMiddleware.ts
Normal file
9
src/store/middlewares/promiseMiddleware.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import {Middleware} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const vanillaPromiseMiddleware: Middleware = store => next => (action: any) => {
|
||||||
|
if (typeof action.then !== 'function') {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(action).then((resolvedAction: any) => store.dispatch(resolvedAction));
|
||||||
|
};
|
||||||
@@ -46,6 +46,18 @@ export const selectSessionInfo = createSelector([selectAuthentication], authenti
|
|||||||
roles: authentication.roles
|
roles: authentication.roles
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const selectApplicationId = createSelector([selectAuthentication], authentication => authentication.applicationId);
|
||||||
|
|
||||||
|
export const selectSecret = createSelector([selectAuthentication], authentication => authentication.secret);
|
||||||
|
|
||||||
|
export const selectSessionId = createSelector([selectAuthentication], authentication => authentication.sessionId);
|
||||||
|
|
||||||
|
export const selectRoles = createSelector([selectAuthentication], authentication => authentication.roles);
|
||||||
|
|
||||||
|
export const selectHasRole = createSelector([selectAuthentication, (_, role: string) => role], (authentication, role) => authentication.roles.includes(role));
|
||||||
|
|
||||||
|
export const selectIsOnline = createSelector([selectAuthentication], authentication => authentication.status === 'Online');
|
||||||
|
|
||||||
const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
|
const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse, {applicationId: string; secret: string}>(
|
||||||
'authentication/authenticate',
|
'authentication/authenticate',
|
||||||
async (credentials, {rejectWithValue}) => {
|
async (credentials, {rejectWithValue}) => {
|
||||||
@@ -56,6 +68,7 @@ const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse,
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Convert error to serializable format
|
// Convert error to serializable format
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
|
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
|
||||||
|
|
||||||
return rejectWithValue(errorMessage);
|
return rejectWithValue(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +80,7 @@ const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejec
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Convert error to serializable format
|
// Convert error to serializable format
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
|
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
|
||||||
|
|
||||||
return rejectWithValue(errorMessage);
|
return rejectWithValue(errorMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -95,14 +109,16 @@ const authenticationSlice = createSlice({
|
|||||||
setSessionId: (state, action: PayloadAction<string>) => {
|
setSessionId: (state, action: PayloadAction<string>) => {
|
||||||
state.sessionId = action.payload;
|
state.sessionId = action.payload;
|
||||||
},
|
},
|
||||||
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
|
setError: (state, action: PayloadAction<string | null>) => {
|
||||||
state.isAuthenticated = action.payload;
|
state.error = action.payload;
|
||||||
},
|
},
|
||||||
setRoles: (state, action: PayloadAction<string[]>) => {
|
setUnauthorized: state => {
|
||||||
state.roles = action.payload;
|
state.isAuthenticated = false;
|
||||||
},
|
state.isLoading = false;
|
||||||
setApplicationId: (state, action: PayloadAction<string>) => {
|
state.error = 'Unauthorized';
|
||||||
state.applicationId = action.payload;
|
state.secret = null;
|
||||||
|
state.status = 'Offline';
|
||||||
|
state.roles = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
@@ -119,17 +135,18 @@ const authenticationSlice = createSlice({
|
|||||||
state.sessionId = authenticationResponse.sessionId ?? null;
|
state.sessionId = authenticationResponse.sessionId ?? null;
|
||||||
state.isAuthenticated = true;
|
state.isAuthenticated = true;
|
||||||
state.roles = authenticationResponse.roles ?? [];
|
state.roles = authenticationResponse.roles ?? [];
|
||||||
|
state.status = 'Online';
|
||||||
|
state.isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
state.applicationId = null;
|
state.applicationId = null;
|
||||||
state.sessionId = null;
|
state.sessionId = null;
|
||||||
state.isAuthenticated = false;
|
state.isAuthenticated = false;
|
||||||
state.secret = null;
|
state.secret = null;
|
||||||
state.roles = [];
|
state.roles = [];
|
||||||
}
|
state.status = 'Offline';
|
||||||
|
state.error = 'Invalid credentials. Please check your Application ID and Secret.';
|
||||||
state.status = 'Online';
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = null;
|
}
|
||||||
})
|
})
|
||||||
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
||||||
state.applicationId = null;
|
state.applicationId = null;
|
||||||
@@ -164,6 +181,6 @@ const authenticationSlice = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {setIsLoading, setCredentials, clearState, setSessionId, setIsAuthenticated, setRoles, setApplicationId} = authenticationSlice.actions;
|
export const {setUnauthorized, setIsLoading, setCredentials, clearState, setSessionId, setError} = authenticationSlice.actions;
|
||||||
export {authenticateCredentialsThunk};
|
export {authenticateCredentialsThunk};
|
||||||
export default authenticationSlice.reducer;
|
export default authenticationSlice.reducer;
|
||||||
103
src/store/slices/Channels.slice.ts
Normal file
103
src/store/slices/Channels.slice.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import {createAsyncThunk, createSelector, createSlice, PayloadAction, WritableDraft} from '@reduxjs/toolkit';
|
||||||
|
import {ApplicationCredentials, Channel} from '@techniker-me/pcast-api';
|
||||||
|
import PCastApiService from 'services/PCastApi.service';
|
||||||
|
|
||||||
|
export interface IChannelsState {
|
||||||
|
isLoading: boolean;
|
||||||
|
channels: Channel[];
|
||||||
|
selectedChannel: Channel | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialChannelsState: IChannelsState = {
|
||||||
|
isLoading: false,
|
||||||
|
channels: [],
|
||||||
|
selectedChannel: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectChannels = (state: {channels: IChannelsState}) => state.channels;
|
||||||
|
|
||||||
|
export const selectChannelList = createSelector([selectChannels], channels => channels.channels);
|
||||||
|
|
||||||
|
export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => {
|
||||||
|
try {
|
||||||
|
return PCastApiService.channels.list();
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchChannelsListPublisherStatus = createAsyncThunk(
|
||||||
|
'channels/fetchChannelsListPublisherStatus',
|
||||||
|
async (channels: Channel[], {rejectWithValue}) => {
|
||||||
|
try {
|
||||||
|
const channelResponses = await Promise.all(
|
||||||
|
channels.map(async channel => {
|
||||||
|
const publisherCount = await PCastApiService.channels.getPublisherCount(channel.channelId);
|
||||||
|
return {
|
||||||
|
...channel,
|
||||||
|
isActivePublisher: publisherCount > 0
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return channelResponses as Channel[];
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const channelsSlice = createSlice({
|
||||||
|
name: 'channels',
|
||||||
|
initialState: {...initialChannelsState},
|
||||||
|
reducers: {
|
||||||
|
initializeChannels: (state, action: PayloadAction<{pcastUri: string; applicationCredentials: ApplicationCredentials}>) => {
|
||||||
|
PCastApiService.initialize(action.payload.pcastUri, action.payload.applicationCredentials);
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
setChannels: (state, action: PayloadAction<Channel[]>) => {
|
||||||
|
state.channels = action.payload as WritableDraft<Channel>[];
|
||||||
|
},
|
||||||
|
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isLoading = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedChannel: (state, action: PayloadAction<Channel | null>) => {
|
||||||
|
state.selectedChannel = action.payload as WritableDraft<Channel> | null;
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: builder => {
|
||||||
|
builder.addCase(fetchChannelList.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
});
|
||||||
|
builder.addCase(fetchChannelList.fulfilled, (state, action) => {
|
||||||
|
state.channels = action.payload as WritableDraft<Channel[]>;
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(fetchChannelList.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
builder.addCase(fetchChannelsListPublisherStatus.pending, state => {
|
||||||
|
state.isLoading = true;
|
||||||
|
});
|
||||||
|
builder.addCase(fetchChannelsListPublisherStatus.fulfilled, (state, action) => {
|
||||||
|
state.channels = action.payload as WritableDraft<Channel[]>;
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(fetchChannelsListPublisherStatus.rejected, (state, action) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {initializeChannels, setChannels, setIsLoading, setSelectedChannel, setError} = channelsSlice.actions;
|
||||||
|
export default channelsSlice.reducer;
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import {configureStore} from '@reduxjs/toolkit';
|
import {configureStore} from '@reduxjs/toolkit';
|
||||||
import AuthenticationState from './slices/Authentication.slice';
|
import AuthenticationReducer from './slices/Authentication.slice';
|
||||||
|
import ChannelsReducer from './slices/Channels.slice';
|
||||||
|
import {authenticateRequestMiddleware, loggerMiddleware, vanillaPromiseMiddleware} from './middlewares';
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
authentication: AuthenticationState
|
authentication: AuthenticationReducer,
|
||||||
}
|
channels: ChannelsReducer
|
||||||
|
},
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
thunk: true,
|
||||||
|
serializableCheck: false
|
||||||
|
}).concat(authenticateRequestMiddleware, vanillaPromiseMiddleware, loggerMiddleware)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
214
src/theme/README.md
Normal file
214
src/theme/README.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Theme System - SOLID Principles Refactor
|
||||||
|
|
||||||
|
This document describes the refactored theme system that follows SOLID principles for better maintainability, extensibility, and testability.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The theme system has been refactored from a monolithic class into a modular, extensible architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
theme/
|
||||||
|
├── interfaces/ # Type definitions (abstractions)
|
||||||
|
│ ├── ITheme.ts
|
||||||
|
│ ├── IColorSystem.ts
|
||||||
|
│ ├── ISpacingSystem.ts
|
||||||
|
│ ├── ITypographySystem.ts
|
||||||
|
│ └── IScreenSystem.ts
|
||||||
|
├── systems/ # Concrete implementations
|
||||||
|
│ ├── ColorSystem.ts
|
||||||
|
│ ├── SpacingSystem.ts
|
||||||
|
│ ├── TypographySystem.ts
|
||||||
|
│ └── ScreenSystem.ts
|
||||||
|
├── utils/ # Utility functions
|
||||||
|
│ ├── ColorUtils.ts
|
||||||
|
│ └── ViewportUtils.ts
|
||||||
|
├── examples/ # Usage examples
|
||||||
|
│ └── ThemeUsageExamples.ts
|
||||||
|
├── ThemeFactory.ts # Factory for creating themes
|
||||||
|
└── index.ts # Main entry point
|
||||||
|
```
|
||||||
|
|
||||||
|
## SOLID Principles Implementation
|
||||||
|
|
||||||
|
### 1. Single Responsibility Principle (SRP)
|
||||||
|
- **ColorSystem**: Only handles color definitions
|
||||||
|
- **SpacingSystem**: Only handles spacing values
|
||||||
|
- **TypographySystem**: Only handles typography settings
|
||||||
|
- **ScreenSystem**: Only handles responsive breakpoints
|
||||||
|
- **ColorUtils**: Only handles color-related utilities
|
||||||
|
- **ViewportUtils**: Only handles viewport-related utilities
|
||||||
|
|
||||||
|
### 2. Open/Closed Principle (OCP)
|
||||||
|
- The system is open for extension but closed for modification
|
||||||
|
- New color systems, spacing systems, etc. can be created by implementing the respective interfaces
|
||||||
|
- No need to modify existing code when adding new themes
|
||||||
|
|
||||||
|
### 3. Liskov Substitution Principle (LSP)
|
||||||
|
- All implementations can be substituted for their interfaces
|
||||||
|
- Custom color systems can replace the default ColorSystem without breaking functionality
|
||||||
|
|
||||||
|
### 4. Interface Segregation Principle (ISP)
|
||||||
|
- Interfaces are focused and specific
|
||||||
|
- Clients only depend on the interfaces they actually use
|
||||||
|
- No fat interfaces that force unnecessary dependencies
|
||||||
|
|
||||||
|
### 5. Dependency Inversion Principle (DIP)
|
||||||
|
- High-level modules depend on abstractions (interfaces)
|
||||||
|
- Low-level modules implement these abstractions
|
||||||
|
- Factory pattern enables dependency injection
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Usage (Backward Compatible)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Theme from './theme';
|
||||||
|
|
||||||
|
// Old way (still works)
|
||||||
|
const primaryColor = Theme.colors.blue;
|
||||||
|
const spacing = Theme.paddings.medium;
|
||||||
|
|
||||||
|
// New way (recommended)
|
||||||
|
const theme = Theme.instance;
|
||||||
|
const newPrimaryColor = theme.colors.blue;
|
||||||
|
const newSpacing = theme.spacing.medium;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Custom Themes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ThemeFactory, ColorSystem, SpacingSystem, TypographySystem, ScreenSystem } from './theme';
|
||||||
|
|
||||||
|
// Create a custom color system
|
||||||
|
class DarkColorSystem extends ColorSystem {
|
||||||
|
readonly white = '#1a1a1a';
|
||||||
|
readonly black = '#ffffff';
|
||||||
|
// ... override other colors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create custom theme
|
||||||
|
const darkTheme = ThemeFactory.createCustomTheme(
|
||||||
|
new DarkColorSystem(),
|
||||||
|
new SpacingSystem(),
|
||||||
|
new TypographySystem(),
|
||||||
|
new ScreenSystem()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply the theme
|
||||||
|
Theme.setTheme(darkTheme);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using in React Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Theme from './theme';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const theme = Theme.instance;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: theme.colors.primaryBackground,
|
||||||
|
color: theme.colors.white,
|
||||||
|
padding: theme.spacing.medium,
|
||||||
|
fontFamily: theme.typography.primaryFont
|
||||||
|
}}>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Theme Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Theme {
|
||||||
|
// Get the current theme instance
|
||||||
|
static get instance(): ITheme;
|
||||||
|
|
||||||
|
// Set a custom theme
|
||||||
|
static setTheme(theme: ITheme): void;
|
||||||
|
|
||||||
|
// Reset to default theme
|
||||||
|
static resetToDefault(): void;
|
||||||
|
|
||||||
|
// Backward compatibility
|
||||||
|
static get colors(): IColorSystem;
|
||||||
|
static get paddings(): ISpacingSystem;
|
||||||
|
static get theme(): ITheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ThemeFactory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ThemeFactory {
|
||||||
|
// Create default theme
|
||||||
|
static createDefaultTheme(): ITheme;
|
||||||
|
|
||||||
|
// Create custom theme
|
||||||
|
static createCustomTheme(
|
||||||
|
colorSystem: IColorSystem,
|
||||||
|
spacingSystem: IExtendedSpacingSystem,
|
||||||
|
typographySystem: ITypographySystem,
|
||||||
|
screenSystem: IScreenSystem
|
||||||
|
): ITheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Color utilities
|
||||||
|
ColorUtils.hexToRgba(hex: string, opacityPercentage: number): string | null;
|
||||||
|
|
||||||
|
// Viewport utilities
|
||||||
|
ViewportUtils.pxToVw(screenWidthInPixels: number, elementSizeInPixels: number): string;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### From Old Theme System
|
||||||
|
|
||||||
|
The refactored system maintains backward compatibility. Existing code will continue to work:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// This still works
|
||||||
|
const color = Theme.colors.blue;
|
||||||
|
const padding = Theme.paddings.medium;
|
||||||
|
const themeConfig = Theme.theme;
|
||||||
|
|
||||||
|
// But this is recommended
|
||||||
|
const theme = Theme.instance;
|
||||||
|
const color = theme.colors.blue;
|
||||||
|
const padding = theme.spacing.medium;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits of Migration
|
||||||
|
|
||||||
|
1. **Better Performance**: Lazy loading and singleton pattern
|
||||||
|
2. **Extensibility**: Easy to create custom themes
|
||||||
|
3. **Testability**: Each component can be tested in isolation
|
||||||
|
4. **Maintainability**: Clear separation of concerns
|
||||||
|
5. **Type Safety**: Strong typing with interfaces
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use the new API**: Prefer `Theme.instance` over static properties
|
||||||
|
2. **Create custom themes**: Use the factory pattern for theme variations
|
||||||
|
3. **Implement interfaces**: When extending, implement the appropriate interfaces
|
||||||
|
4. **Use utility classes**: Leverage ColorUtils and ViewportUtils for common operations
|
||||||
|
5. **Test in isolation**: Each system can be unit tested independently
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
The refactored system prioritizes performance through:
|
||||||
|
|
||||||
|
- **Singleton Pattern**: Single theme instance across the application
|
||||||
|
- **Lazy Loading**: Theme is created only when first accessed
|
||||||
|
- **Immutable Properties**: Readonly properties prevent accidental mutations
|
||||||
|
- **Efficient Factory**: Minimal overhead in theme creation
|
||||||
|
|
||||||
|
This architecture ensures the theme system is both performant and maintainable while following industry best practices for object-oriented design.
|
||||||
139
src/theme/ThemeFactory.ts
Normal file
139
src/theme/ThemeFactory.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITheme } from './interfaces/ITheme';
|
||||||
|
import { IColorSystem } from './interfaces/IColorSystem';
|
||||||
|
import { IExtendedSpacingSystem } from './interfaces/ISpacingSystem';
|
||||||
|
import { ITypographySystem } from './interfaces/ITypographySystem';
|
||||||
|
import { IScreenSystem } from './interfaces/IScreenSystem';
|
||||||
|
import { IBackgroundSystem } from './interfaces/IBackgroundSystem';
|
||||||
|
import { ColorSystem } from './defaultTheme/ColorSystem';
|
||||||
|
import { SpacingSystem } from './defaultTheme/SpacingSystem';
|
||||||
|
import { TypographySystem } from './defaultTheme/TypographySystem';
|
||||||
|
import { ScreenSystem } from './defaultTheme/ScreenSystem';
|
||||||
|
import { BackgroundSystem } from './defaultTheme/BackgroundSystem';
|
||||||
|
|
||||||
|
export class ThemeFactory {
|
||||||
|
/**
|
||||||
|
* Creates a default theme instance
|
||||||
|
* @returns A complete theme object
|
||||||
|
*/
|
||||||
|
static createDefaultTheme(): ITheme {
|
||||||
|
const colors = new ColorSystem();
|
||||||
|
const spacing = new SpacingSystem();
|
||||||
|
const typography = new TypographySystem();
|
||||||
|
const screenSizes = new ScreenSystem();
|
||||||
|
const backgrounds = new BackgroundSystem();
|
||||||
|
|
||||||
|
return new DefaultTheme(colors, spacing, typography, screenSizes, backgrounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a custom theme with provided systems
|
||||||
|
* @param colorSystem - Custom color system
|
||||||
|
* @param spacingSystem - Custom spacing system
|
||||||
|
* @param typographySystem - Custom typography system
|
||||||
|
* @param screenSystem - Custom screen system
|
||||||
|
* @returns A complete theme object
|
||||||
|
*/
|
||||||
|
static createCustomTheme(
|
||||||
|
colorSystem: IColorSystem,
|
||||||
|
spacingSystem: IExtendedSpacingSystem,
|
||||||
|
typographySystem: ITypographySystem,
|
||||||
|
screenSystem: IScreenSystem,
|
||||||
|
backgroundSystem?: IBackgroundSystem
|
||||||
|
): ITheme {
|
||||||
|
const backgrounds = backgroundSystem || new BackgroundSystem();
|
||||||
|
return new DefaultTheme(colorSystem, spacingSystem, typographySystem, screenSystem, backgrounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultTheme implements ITheme {
|
||||||
|
constructor(
|
||||||
|
public readonly colors: IColorSystem,
|
||||||
|
public readonly spacing: IExtendedSpacingSystem,
|
||||||
|
public readonly typography: ITypographySystem,
|
||||||
|
public readonly screenSizes: IScreenSystem,
|
||||||
|
public readonly backgrounds: IBackgroundSystem
|
||||||
|
) {}
|
||||||
|
|
||||||
|
readonly footerHeight = '51px';
|
||||||
|
readonly headerAllowance = 200;
|
||||||
|
readonly formFieldWidth = 350;
|
||||||
|
readonly formFieldMaxWidth = 500;
|
||||||
|
readonly formFieldMaxHeight = 250;
|
||||||
|
readonly inputIconWidth = 16;
|
||||||
|
|
||||||
|
get primaryColor(): string {
|
||||||
|
return this.colors.blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get secondaryColor(): string {
|
||||||
|
return this.colors.gray700;
|
||||||
|
}
|
||||||
|
|
||||||
|
get successColor(): string {
|
||||||
|
return this.colors.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
get infoColor(): string {
|
||||||
|
return this.colors.cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
get warningColor(): string {
|
||||||
|
return this.colors.yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dangerColor(): string {
|
||||||
|
return this.colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
get secondaryLight(): string {
|
||||||
|
return this.colors.gray800;
|
||||||
|
}
|
||||||
|
|
||||||
|
get secondaryDark(): string {
|
||||||
|
return this.colors.gray500;
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryThemeColor(): string {
|
||||||
|
return this.colors.lightBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryBackground(): string {
|
||||||
|
return this.colors.gray900;
|
||||||
|
}
|
||||||
|
|
||||||
|
get linkColor(): string {
|
||||||
|
return this.colors.green;
|
||||||
|
}
|
||||||
|
|
||||||
|
get blackWithOpacity(): string {
|
||||||
|
return this.colors.blackWithOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryInputHeight(): string {
|
||||||
|
return '2.2rem';
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryBorderColor(): string {
|
||||||
|
return this.colors.gray400;
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryBorderWidth(): string {
|
||||||
|
return '1px';
|
||||||
|
}
|
||||||
|
|
||||||
|
get primaryBorderRadius(): string {
|
||||||
|
return '1.2rem';
|
||||||
|
}
|
||||||
|
|
||||||
|
get loaderSize() {
|
||||||
|
return {
|
||||||
|
small: 12,
|
||||||
|
medium: 20,
|
||||||
|
large: 50
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/theme/defaultTheme/BackgroundSystem.ts
Normal file
11
src/theme/defaultTheme/BackgroundSystem.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IBackgroundSystem } from '../interfaces/IBackgroundSystem';
|
||||||
|
import bgImage from '../../assets/images/background-1415x959.png';
|
||||||
|
|
||||||
|
export class BackgroundSystem implements IBackgroundSystem {
|
||||||
|
readonly loginBackground = bgImage;
|
||||||
|
readonly defaultBackground = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
||||||
|
}
|
||||||
39
src/theme/defaultTheme/ColorSystem.ts
Normal file
39
src/theme/defaultTheme/ColorSystem.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IColorSystem } from '../interfaces/IColorSystem';
|
||||||
|
|
||||||
|
export class ColorSystem implements IColorSystem {
|
||||||
|
readonly white = '#F9F9F9';
|
||||||
|
readonly whiteWithOpacity = 'rgba(249, 249, 249, 0.5)';
|
||||||
|
readonly gray100 = '#f8f9fa';
|
||||||
|
readonly gray200 = '#ebebeb';
|
||||||
|
readonly gray300 = '#dee2e6';
|
||||||
|
readonly gray400 = '#ced4da';
|
||||||
|
readonly gray500 = '#adb5bd';
|
||||||
|
readonly gray600 = '#999';
|
||||||
|
readonly gray700 = '#444';
|
||||||
|
readonly gray800 = '#303030';
|
||||||
|
readonly gray900 = '#222222';
|
||||||
|
readonly gray1000 = '#1f1f1f';
|
||||||
|
readonly black = '#000000';
|
||||||
|
|
||||||
|
readonly blue = '#375a7f';
|
||||||
|
readonly indigo = '#6610f2';
|
||||||
|
readonly purple = '#6f42c1';
|
||||||
|
readonly pink = '#e83e8c';
|
||||||
|
readonly lightBlue = '#66b3ff';
|
||||||
|
readonly linkBlue = 'blue';
|
||||||
|
readonly red = '#EE2D52';
|
||||||
|
readonly lightRed = '#f70d1a';
|
||||||
|
readonly orange = '#fd7e14';
|
||||||
|
readonly yellow = '#F39C12';
|
||||||
|
readonly green = '#00bc8c';
|
||||||
|
readonly teal = '#20c997';
|
||||||
|
readonly cyan = '#3498DB';
|
||||||
|
readonly headerColor = '#2C2D37';
|
||||||
|
readonly transparent = 'transparent';
|
||||||
|
readonly blackWithOpacity = 'rgba(0, 0, 0, 0.95)';
|
||||||
|
readonly halfTransparentBlack = 'rgba(0, 0, 0, 0.5)';
|
||||||
|
}
|
||||||
22
src/theme/defaultTheme/ScreenSystem.ts
Normal file
22
src/theme/defaultTheme/ScreenSystem.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IScreenSystem } from '../interfaces/IScreenSystem';
|
||||||
|
|
||||||
|
export class ScreenSystem implements IScreenSystem {
|
||||||
|
readonly large = 1170;
|
||||||
|
readonly desktop = 992;
|
||||||
|
readonly tablet = 768;
|
||||||
|
readonly phone = 660;
|
||||||
|
readonly smallPhone = 360;
|
||||||
|
|
||||||
|
readonly mediaPhone = `@media only screen
|
||||||
|
and (max-device-width: 737px)
|
||||||
|
and (-webkit-min-device-pixel-ratio: 2)
|
||||||
|
`;
|
||||||
|
|
||||||
|
readonly mediaLargeScreen = `@media only screen
|
||||||
|
and (min-width: 1920px)
|
||||||
|
`;
|
||||||
|
}
|
||||||
18
src/theme/defaultTheme/SpacingSystem.ts
Normal file
18
src/theme/defaultTheme/SpacingSystem.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IExtendedSpacingSystem } from '../interfaces/ISpacingSystem';
|
||||||
|
|
||||||
|
export class SpacingSystem implements IExtendedSpacingSystem {
|
||||||
|
readonly xsmall = '0.25rem';
|
||||||
|
readonly small = '0.5rem';
|
||||||
|
readonly medium = '1rem';
|
||||||
|
readonly large = '1.5rem';
|
||||||
|
readonly xlarge = '2rem';
|
||||||
|
|
||||||
|
readonly xxSmall = '4px';
|
||||||
|
readonly xSmall = '8px';
|
||||||
|
readonly xLarge = '48px';
|
||||||
|
readonly xxLarge = '64px';
|
||||||
|
}
|
||||||
19
src/theme/defaultTheme/TypographySystem.ts
Normal file
19
src/theme/defaultTheme/TypographySystem.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITypographySystem } from '../interfaces/ITypographySystem';
|
||||||
|
|
||||||
|
export class TypographySystem implements ITypographySystem {
|
||||||
|
readonly primaryFontSize = '0.9375rem';
|
||||||
|
readonly primaryLineHeight = '1.6rem';
|
||||||
|
readonly primaryFont = '\'Montserrat\', Helvetica, Arial, sans-serif';
|
||||||
|
|
||||||
|
readonly fontSizeXS = '0.65rem';
|
||||||
|
readonly fontSizeS = '0.8rem';
|
||||||
|
readonly fontSizeL = '1.2rem';
|
||||||
|
readonly fontSizeXl = '1.5rem';
|
||||||
|
readonly fontSizeXxl = '2rem';
|
||||||
|
readonly fontSizeXxxl = '2.5rem';
|
||||||
|
readonly fontSizeXxxxl = '3rem';
|
||||||
|
}
|
||||||
63
src/theme/index.ts
Normal file
63
src/theme/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { ITheme } from './interfaces/ITheme';
|
||||||
|
export type { IColorSystem } from './interfaces/IColorSystem';
|
||||||
|
export type { ISpacingSystem, IExtendedSpacingSystem } from './interfaces/ISpacingSystem';
|
||||||
|
export type { ITypographySystem } from './interfaces/ITypographySystem';
|
||||||
|
export type { IScreenSystem } from './interfaces/IScreenSystem';
|
||||||
|
export type { IBackgroundSystem } from './interfaces/IBackgroundSystem';
|
||||||
|
export { ColorSystem } from './defaultTheme/ColorSystem';
|
||||||
|
export { SpacingSystem } from './defaultTheme/SpacingSystem';
|
||||||
|
export { TypographySystem } from './defaultTheme/TypographySystem';
|
||||||
|
export { ScreenSystem } from './defaultTheme/ScreenSystem';
|
||||||
|
export { BackgroundSystem } from './defaultTheme/BackgroundSystem';
|
||||||
|
export { ThemeFactory } from './ThemeFactory';
|
||||||
|
export { ColorUtils } from './utils/ColorUtils';
|
||||||
|
export { ViewportUtils } from './utils/ViewportUtils';
|
||||||
|
|
||||||
|
import { ITheme } from './interfaces/ITheme';
|
||||||
|
import { ThemeFactory } from './ThemeFactory';
|
||||||
|
import { ColorUtils } from './utils/ColorUtils';
|
||||||
|
import { ViewportUtils } from './utils/ViewportUtils';
|
||||||
|
|
||||||
|
export class Theme {
|
||||||
|
private static _instance: ITheme = ThemeFactory.createDefaultTheme();
|
||||||
|
|
||||||
|
static get instance(): ITheme {
|
||||||
|
return Theme._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setTheme(theme: ITheme): void {
|
||||||
|
Theme._instance = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
static resetToDefault(): void {
|
||||||
|
Theme._instance = ThemeFactory.createDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility properties
|
||||||
|
static get colors() {
|
||||||
|
return Theme.instance.colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get paddings() {
|
||||||
|
return {
|
||||||
|
xsmall: Theme.instance.spacing.xsmall,
|
||||||
|
small: Theme.instance.spacing.small,
|
||||||
|
medium: Theme.instance.spacing.medium,
|
||||||
|
large: Theme.instance.spacing.large,
|
||||||
|
xlarge: Theme.instance.spacing.xlarge
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get theme() {
|
||||||
|
return Theme.instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Theme.instance
|
||||||
|
export const theme = Theme.instance;
|
||||||
|
export const pxToVw = ViewportUtils.pxToVw;
|
||||||
|
export const hexToRgba = ColorUtils.hexToRgba;
|
||||||
8
src/theme/interfaces/IBackgroundSystem.ts
Normal file
8
src/theme/interfaces/IBackgroundSystem.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IBackgroundSystem {
|
||||||
|
readonly loginBackground: string;
|
||||||
|
readonly defaultBackground: string;
|
||||||
|
}
|
||||||
36
src/theme/interfaces/IColorSystem.ts
Normal file
36
src/theme/interfaces/IColorSystem.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IColorSystem {
|
||||||
|
readonly white: string;
|
||||||
|
readonly whiteWithOpacity: string;
|
||||||
|
readonly gray100: string;
|
||||||
|
readonly gray200: string;
|
||||||
|
readonly gray300: string;
|
||||||
|
readonly gray400: string;
|
||||||
|
readonly gray500: string;
|
||||||
|
readonly gray600: string;
|
||||||
|
readonly gray700: string;
|
||||||
|
readonly gray800: string;
|
||||||
|
readonly gray900: string;
|
||||||
|
readonly gray1000: string;
|
||||||
|
readonly black: string;
|
||||||
|
readonly blue: string;
|
||||||
|
readonly indigo: string;
|
||||||
|
readonly purple: string;
|
||||||
|
readonly pink: string;
|
||||||
|
readonly lightBlue: string;
|
||||||
|
readonly linkBlue: string;
|
||||||
|
readonly red: string;
|
||||||
|
readonly lightRed: string;
|
||||||
|
readonly orange: string;
|
||||||
|
readonly yellow: string;
|
||||||
|
readonly green: string;
|
||||||
|
readonly teal: string;
|
||||||
|
readonly cyan: string;
|
||||||
|
readonly headerColor: string;
|
||||||
|
readonly transparent: string;
|
||||||
|
readonly blackWithOpacity: string;
|
||||||
|
readonly halfTransparentBlack: string;
|
||||||
|
}
|
||||||
13
src/theme/interfaces/IScreenSystem.ts
Normal file
13
src/theme/interfaces/IScreenSystem.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IScreenSystem {
|
||||||
|
readonly large: number;
|
||||||
|
readonly desktop: number;
|
||||||
|
readonly tablet: number;
|
||||||
|
readonly phone: number;
|
||||||
|
readonly smallPhone: number;
|
||||||
|
readonly mediaPhone: string;
|
||||||
|
readonly mediaLargeScreen: string;
|
||||||
|
}
|
||||||
18
src/theme/interfaces/ISpacingSystem.ts
Normal file
18
src/theme/interfaces/ISpacingSystem.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ISpacingSystem {
|
||||||
|
readonly xsmall: string;
|
||||||
|
readonly small: string;
|
||||||
|
readonly medium: string;
|
||||||
|
readonly large: string;
|
||||||
|
readonly xlarge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtendedSpacingSystem extends ISpacingSystem {
|
||||||
|
readonly xxSmall: string;
|
||||||
|
readonly xSmall: string;
|
||||||
|
readonly xLarge: string;
|
||||||
|
readonly xxLarge: string;
|
||||||
|
}
|
||||||
44
src/theme/interfaces/ITheme.ts
Normal file
44
src/theme/interfaces/ITheme.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IColorSystem } from './IColorSystem';
|
||||||
|
import { IExtendedSpacingSystem } from './ISpacingSystem';
|
||||||
|
import { ITypographySystem } from './ITypographySystem';
|
||||||
|
import { IScreenSystem } from './IScreenSystem';
|
||||||
|
import { IBackgroundSystem } from './IBackgroundSystem';
|
||||||
|
|
||||||
|
export interface ITheme {
|
||||||
|
readonly colors: IColorSystem;
|
||||||
|
readonly spacing: IExtendedSpacingSystem;
|
||||||
|
readonly typography: ITypographySystem;
|
||||||
|
readonly screenSizes: IScreenSystem;
|
||||||
|
readonly backgrounds: IBackgroundSystem;
|
||||||
|
readonly footerHeight: string;
|
||||||
|
readonly primaryColor: string;
|
||||||
|
readonly secondaryColor: string;
|
||||||
|
readonly successColor: string;
|
||||||
|
readonly infoColor: string;
|
||||||
|
readonly warningColor: string;
|
||||||
|
readonly dangerColor: string;
|
||||||
|
readonly secondaryLight: string;
|
||||||
|
readonly secondaryDark: string;
|
||||||
|
readonly headerAllowance: number;
|
||||||
|
readonly primaryThemeColor: string;
|
||||||
|
readonly primaryBackground: string;
|
||||||
|
readonly linkColor: string;
|
||||||
|
readonly blackWithOpacity: string;
|
||||||
|
readonly primaryInputHeight: string;
|
||||||
|
readonly formFieldWidth: number;
|
||||||
|
readonly formFieldMaxWidth: number;
|
||||||
|
readonly formFieldMaxHeight: number;
|
||||||
|
readonly inputIconWidth: number;
|
||||||
|
readonly primaryBorderColor: string;
|
||||||
|
readonly primaryBorderWidth: string;
|
||||||
|
readonly primaryBorderRadius: string;
|
||||||
|
readonly loaderSize: {
|
||||||
|
readonly small: number;
|
||||||
|
readonly medium: number;
|
||||||
|
readonly large: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
16
src/theme/interfaces/ITypographySystem.ts
Normal file
16
src/theme/interfaces/ITypographySystem.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ITypographySystem {
|
||||||
|
readonly primaryFontSize: string;
|
||||||
|
readonly primaryLineHeight: string;
|
||||||
|
readonly primaryFont: string;
|
||||||
|
readonly fontSizeXS: string;
|
||||||
|
readonly fontSizeS: string;
|
||||||
|
readonly fontSizeL: string;
|
||||||
|
readonly fontSizeXl: string;
|
||||||
|
readonly fontSizeXxl: string;
|
||||||
|
readonly fontSizeXxxl: string;
|
||||||
|
readonly fontSizeXxxxl: string;
|
||||||
|
}
|
||||||
31
src/theme/utils/ColorUtils.ts
Normal file
31
src/theme/utils/ColorUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ColorUtils {
|
||||||
|
/**
|
||||||
|
* Converts a hex color to RGBA format
|
||||||
|
* @param hex - The hex color string (with or without #)
|
||||||
|
* @param opacityPercentage - Opacity as percentage (1-100)
|
||||||
|
* @returns RGBA string or null if invalid hex
|
||||||
|
*/
|
||||||
|
static hexToRgba(hex: string, opacityPercentage: number): string | null {
|
||||||
|
const rgbParsed = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
|
||||||
|
if (!rgbParsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgb = {
|
||||||
|
r: parseInt(rgbParsed[1], 16),
|
||||||
|
g: parseInt(rgbParsed[2], 16),
|
||||||
|
b: parseInt(rgbParsed[3], 16)
|
||||||
|
};
|
||||||
|
|
||||||
|
const rgba = opacityPercentage > 100 || opacityPercentage < 1 ?
|
||||||
|
`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)` :
|
||||||
|
`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacityPercentage / 100})`;
|
||||||
|
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/theme/utils/ViewportUtils.ts
Normal file
15
src/theme/utils/ViewportUtils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class ViewportUtils {
|
||||||
|
/**
|
||||||
|
* Converts pixels to viewport width units
|
||||||
|
* @param screenWidthInPixels - The screen width in pixels
|
||||||
|
* @param elementSizeInPixels - The element size in pixels
|
||||||
|
* @returns Viewport width string
|
||||||
|
*/
|
||||||
|
static pxToVw(screenWidthInPixels: number, elementSizeInPixels: number): string {
|
||||||
|
return `${100 * elementSizeInPixels / screenWidthInPixels}vw`;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/views/ChannelList/ChannelList.tsx
Normal file
44
src/views/ChannelList/ChannelList.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {JSX, useEffect} from 'react';
|
||||||
|
import {useAppDispatch, useAppSelector} from 'store';
|
||||||
|
import {fetchChannelsListPublisherStatus, selectChannels, fetchChannelList} from 'store/slices/Channels.slice';
|
||||||
|
import {Channel} from '@techniker-me/pcast-api';
|
||||||
|
|
||||||
|
type ChannelWithPublisherStatus = Channel & {
|
||||||
|
isActivePublisher: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ChannelList(): JSX.Element {
|
||||||
|
const channelsList = useAppSelector(selectChannels);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeChannels = async () => {
|
||||||
|
const channelListResult = await dispatch(fetchChannelList());
|
||||||
|
|
||||||
|
if (channelListResult.payload && Array.isArray(channelListResult.payload) && channelListResult.payload.length > 0) {
|
||||||
|
dispatch(fetchChannelsListPublisherStatus(channelListResult.payload as ChannelWithPublisherStatus[]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeChannels();
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Channels List</h1>
|
||||||
|
<ul>
|
||||||
|
{channelsList.channels?.map(channel => {
|
||||||
|
const channelWithStatus = channel as ChannelWithPublisherStatus;
|
||||||
|
return (
|
||||||
|
<li key={channel.channelId}>
|
||||||
|
<div>
|
||||||
|
<div>{channel.name}</div>
|
||||||
|
{channelWithStatus.isActivePublisher && <div>Status: {channelWithStatus.isActivePublisher ? 'Active' : 'Inactive'}</div>}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/views/ChannelList/index.ts
Normal file
1
src/views/ChannelList/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ChannelList';
|
||||||
118
src/views/LoginForm/LoginForm.tsx
Normal file
118
src/views/LoginForm/LoginForm.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {FC, useState, useEffect} from 'react';
|
||||||
|
import text from './text';
|
||||||
|
import {useAppDispatch, useAppSelector} from 'store/index';
|
||||||
|
import {authenticateCredentialsThunk, selectIsLoading, selectError, setError} from 'store/slices/Authentication.slice';
|
||||||
|
import {
|
||||||
|
LoginFormBackground,
|
||||||
|
LogoContainer,
|
||||||
|
LoginContainer,
|
||||||
|
LoginForm as StyledForm,
|
||||||
|
LoginButton,
|
||||||
|
LoginHeader,
|
||||||
|
LoginText,
|
||||||
|
ErrorText,
|
||||||
|
InputContainer,
|
||||||
|
InputField,
|
||||||
|
InputIcon,
|
||||||
|
Footer
|
||||||
|
} from './style';
|
||||||
|
import personImage from 'assets/images/symbol-person-24x24.png';
|
||||||
|
import lockImage from 'assets/images/symbol-lock-24x24.png';
|
||||||
|
import phenixLogo from 'assets/images/phenix-logo-101x41.png';
|
||||||
|
import { LoadingWheel } from 'components';
|
||||||
|
import links from './links';
|
||||||
|
|
||||||
|
export const LoginForm: FC = () => {
|
||||||
|
const {headerText, headerTextSmall, signInText} = text;
|
||||||
|
const [applicationId, setApplicationId] = useState('');
|
||||||
|
const [secret, setSecret] = useState('');
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const error = useAppSelector(selectError);
|
||||||
|
const isLoading = useAppSelector(selectIsLoading);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch(authenticateCredentialsThunk({applicationId, secret}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (setter: (value: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
// Clear error when user starts typing
|
||||||
|
if (error) {
|
||||||
|
dispatch(setError(null));
|
||||||
|
}
|
||||||
|
setter(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(setError(null));
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoginFormBackground>
|
||||||
|
<LogoContainer>
|
||||||
|
<img src={phenixLogo} alt="Phenix RTS" />
|
||||||
|
</LogoContainer>
|
||||||
|
<LoginContainer>
|
||||||
|
<LoginHeader>
|
||||||
|
{headerText}
|
||||||
|
</LoginHeader>
|
||||||
|
<LoginText>
|
||||||
|
{headerTextSmall}
|
||||||
|
</LoginText>
|
||||||
|
|
||||||
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
|
<InputContainer>
|
||||||
|
<InputIcon src={personImage} alt="User" />
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
value={applicationId}
|
||||||
|
placeholder="Application ID"
|
||||||
|
onChange={handleInputChange(setApplicationId)}
|
||||||
|
disabled={isLoading}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
|
||||||
|
<InputContainer>
|
||||||
|
<InputIcon src={lockImage} alt="Lock" />
|
||||||
|
<InputField
|
||||||
|
type="password"
|
||||||
|
value={secret}
|
||||||
|
placeholder="Secret"
|
||||||
|
onChange={handleInputChange(setSecret)}
|
||||||
|
disabled={isLoading}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<ErrorText className="error-message testId-displayMessage">
|
||||||
|
{error}
|
||||||
|
</ErrorText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LoginButton
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="testId-loginButton"
|
||||||
|
>
|
||||||
|
{isLoading ? <LoadingWheel size={'small'} /> : signInText}
|
||||||
|
</LoginButton>
|
||||||
|
</StyledForm>
|
||||||
|
|
||||||
|
<Footer>
|
||||||
|
© 2025 Phenix RTS
|
||||||
|
<br />
|
||||||
|
<a href={links.privacyPolicy} target="_blank">Privacy Policy</a> • <a href={links.termsOfService} target="_blank">Terms of Service</a> • <a href={links.documentation} target="_blank">Documentation</a>
|
||||||
|
</Footer>
|
||||||
|
</LoginContainer>
|
||||||
|
</LoginFormBackground>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
src/views/LoginForm/index.ts
Normal file
1
src/views/LoginForm/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './LoginForm';
|
||||||
6
src/views/LoginForm/links.ts
Normal file
6
src/views/LoginForm/links.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
privacyPolicy: 'https://www.phenixrts.com/privacy-policy',
|
||||||
|
termsOfService: 'https://www.phenixrts.com/terms-of-service',
|
||||||
|
documentation: 'https://www.phenixrts.com/docs'
|
||||||
|
}
|
||||||
189
src/views/LoginForm/style.ts
Normal file
189
src/views/LoginForm/style.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import * as styled from 'styled-components';
|
||||||
|
import {H1, P} from 'components/typography';
|
||||||
|
import Theme from 'theme';
|
||||||
|
import phenixLogo from 'assets/images/phenix-logo-101x41.png';
|
||||||
|
|
||||||
|
export const LoginFormBackground = styled.default.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
background-image: url(${Theme.backgrounds.loginBackground});
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure content is above the overlay */
|
||||||
|
> * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LogoContainer = styled.default.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
left: 2rem;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
filter: brightness(0) invert(1); /* Makes the logo white */
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginContainer = styled.default.div`
|
||||||
|
background: rgba(25, 25, 25, 0.9);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2.5rem;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 450px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginHeader = styled.default(H1)`
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginText = styled.default(P)`
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginForm = styled.default.form`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InputContainer = styled.default.div`
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InputField = styled.default.input`
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1rem 1rem 3rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(35, 35, 35, 0.7);
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: ${Theme.colors.gray500};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: ${Theme.colors.white};
|
||||||
|
background: rgba(45, 45, 45, 0.9);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InputIcon = styled.default.img`
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
opacity: 0.7;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginButton = styled.default.button`
|
||||||
|
background: linear-gradient(135deg, ${Theme.colors.red}, ${Theme.colors.lightRed});
|
||||||
|
color: ${Theme.colors.white};
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 20px rgba(238, 45, 82, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LoginLink = styled.default.a`
|
||||||
|
color: ${Theme.colors.cyan};
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorText = styled.default.div`
|
||||||
|
text-align: center;
|
||||||
|
color: ${Theme.colors.red};
|
||||||
|
background: rgba(238, 45, 82, 0.1);
|
||||||
|
border: 1px solid rgba(238, 45, 82, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Footer = styled.default.div`
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 0.3rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
13
src/views/LoginForm/text.ts
Normal file
13
src/views/LoginForm/text.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
phenixText: 'Phenix',
|
||||||
|
headerText: 'Customer Portal',
|
||||||
|
headerTextSmall: 'Please sign in',
|
||||||
|
loginForgottenText: 'Forgot password?',
|
||||||
|
signInText: 'Sign In',
|
||||||
|
failAuthenticationText: 'Failed to authenticate. Please check your credentials',
|
||||||
|
emptyCredentialsText: 'Please enter Application Id and Secret',
|
||||||
|
unknownErrorText: 'An error has occurred. Please try again'
|
||||||
|
};
|
||||||
93
src/views/LoginForm/view.tsx
Normal file
93
src/views/LoginForm/view.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
useRef,
|
||||||
|
FC
|
||||||
|
} from 'react';
|
||||||
|
import text from './text';
|
||||||
|
|
||||||
|
import {InputWithRef} from 'components/inputs';
|
||||||
|
import Theme from 'theme';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoginLayout as Layout,
|
||||||
|
LoginButton,
|
||||||
|
LoginHeader,
|
||||||
|
LoginText,
|
||||||
|
LoginLink,
|
||||||
|
ErrorText
|
||||||
|
} from './style';
|
||||||
|
|
||||||
|
import personImage from 'assets/images/symbol-person-24x24.png';
|
||||||
|
import lockImage from 'assets/images/symbol-lock-24x24.png';
|
||||||
|
import { LoadingWheel } from 'components/loaders';
|
||||||
|
|
||||||
|
interface ILoginContainer {
|
||||||
|
onSubmit: () => void;
|
||||||
|
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
errorMessage: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
isWebsocketConnected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Login: FC<ILoginContainer> = (props: ILoginContainer) => {
|
||||||
|
const {headerText, headerTextSmall, signInText, loginForgottenText} = text;
|
||||||
|
const {onChange, onSubmit, errorMessage, isLoading, isWebsocketConnected} = props;
|
||||||
|
const applicationIdRef = useRef(null);
|
||||||
|
const secretRef = useRef(null);
|
||||||
|
const handleKeySubmit = ({key}) => {
|
||||||
|
if (key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<LoginHeader>
|
||||||
|
{headerText}
|
||||||
|
</LoginHeader>
|
||||||
|
<LoginText>
|
||||||
|
{headerTextSmall}
|
||||||
|
</LoginText>
|
||||||
|
{/* <InputWithRef
|
||||||
|
imagePath={personImage}
|
||||||
|
imageAltText="Application Id"
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyPress={handleKeySubmit}
|
||||||
|
name="applicationId"
|
||||||
|
type="text"
|
||||||
|
ref={applicationIdRef}
|
||||||
|
disabled={isLoading}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<InputWithRef
|
||||||
|
imagePath={lockImage}
|
||||||
|
imageAltText="Secret"
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyPress={handleKeySubmit}
|
||||||
|
ref={secretRef}
|
||||||
|
name="secret"
|
||||||
|
type="password"
|
||||||
|
disabled={isLoading}
|
||||||
|
autocomplete="new-password"
|
||||||
|
/> */}
|
||||||
|
<ErrorText className="error-message testId-displayMessage">{errorMessage || null}</ErrorText>
|
||||||
|
<LoginButton
|
||||||
|
backgroundColor={Theme.colors.red}
|
||||||
|
onClick={onSubmit}
|
||||||
|
disabled={isLoading || !isWebsocketConnected}
|
||||||
|
className="testId-loginButton"
|
||||||
|
>
|
||||||
|
{(!isWebsocketConnected || isLoading) && <span><LoadingWheel size={'small'} /></span>}
|
||||||
|
{isWebsocketConnected && signInText}
|
||||||
|
</LoginButton>
|
||||||
|
<LoginLink>
|
||||||
|
{loginForgottenText}
|
||||||
|
</LoginLink>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
149
src/views/channels/channels.tsx
Normal file
149
src/views/channels/channels.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||||
|
import {channelListErrorMessages, channelListPublishingStateErrorMessages} from 'constants/index';
|
||||||
|
import {transformToPortalError} from 'utility/error-handler';
|
||||||
|
import {ITableSortSearch} from 'interfaces/tableProps';
|
||||||
|
|
||||||
|
import {IAppStore} from 'store';
|
||||||
|
import {channelsSelector, listChannels} from 'store/action/channels';
|
||||||
|
import {fetchChannelsPublishingState} from 'store/action/channels-publishing';
|
||||||
|
import {StoreScreensType} from 'store/reducers/screens';
|
||||||
|
import {screensSelector, setScreenProps} from 'store/action/screens';
|
||||||
|
|
||||||
|
import Loader from 'components/loaders';
|
||||||
|
import RequireAuthentication from 'components/requiresAuth';
|
||||||
|
import {Body, Main} from 'components/layout';
|
||||||
|
import {TableHeaderKey, ITableWithPaginationHeader} from 'components/table';
|
||||||
|
import {TableWithPagination} from 'components/table-with-pagination';
|
||||||
|
|
||||||
|
import {Error} from 'components/error-renderer/style';
|
||||||
|
import {columns} from './columns-config';
|
||||||
|
import {CreateChannelModal} from './create-channel';
|
||||||
|
|
||||||
|
const intervalFetchChannelsPublishingState = moment.duration(5, 'seconds').asMilliseconds();
|
||||||
|
|
||||||
|
export const ChannelsContainer = (): JSX.Element => {
|
||||||
|
const logger = LoggerFactory.getLogger('views/channels/ChannelsContainer');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const interval = useRef(null);
|
||||||
|
const {channelList = [], isFetching, error} = useSelector((state: IAppStore) => channelsSelector(state));
|
||||||
|
const {searchValue, sortDirection, sortColumn} = useSelector((state: IAppStore) => screensSelector(state)[StoreScreensType.Channels]);
|
||||||
|
const [isCreateChannelModalOpened, setCreateChannelModalOpened] = useState(false);
|
||||||
|
const [channels, setChannels] = useState([]);
|
||||||
|
const [channelsIdOnDisplay, setChannelsIdOnDisplay] = useState([]);
|
||||||
|
const channelsColumns = {...columns};
|
||||||
|
const getChannelList = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
logger.info('Fetching the list of channels');
|
||||||
|
|
||||||
|
await dispatch(listChannels());
|
||||||
|
|
||||||
|
logger.info('List of channels was fetched successfully');
|
||||||
|
} catch (e) {
|
||||||
|
const {status, message, requestPayload} = transformToPortalError(e);
|
||||||
|
|
||||||
|
logger.error(`${channelListErrorMessages[status] || message} [%s]`, status, requestPayload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChannelsPublishingState = async (): Promise<void> => {
|
||||||
|
if (isFetching || !channelsIdOnDisplay?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Checking channels publishing state');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(fetchChannelsPublishingState(channelsIdOnDisplay));
|
||||||
|
} catch (e) {
|
||||||
|
const {status, message, requestPayload} = transformToPortalError(e);
|
||||||
|
|
||||||
|
return logger.error(
|
||||||
|
`${channelListPublishingStateErrorMessages[status] || message || channelListPublishingStateErrorMessages['default']} [%s]`,
|
||||||
|
status,
|
||||||
|
requestPayload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getChannelList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const modifiedChannels = channelList.map(channel => ({
|
||||||
|
...channel,
|
||||||
|
extraPath: `${encodeURIComponent(channel.channelId)}/preview`
|
||||||
|
}));
|
||||||
|
|
||||||
|
setChannels(modifiedChannels || []);
|
||||||
|
}, [channelList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearInterval(interval.current);
|
||||||
|
interval.current = setInterval(getChannelsPublishingState, intervalFetchChannelsPublishingState);
|
||||||
|
|
||||||
|
return () => clearInterval(interval.current);
|
||||||
|
}, [channelsIdOnDisplay, isFetching]);
|
||||||
|
|
||||||
|
const screenHeader: ITableWithPaginationHeader = {
|
||||||
|
[TableHeaderKey.Search]: {},
|
||||||
|
[TableHeaderKey.AddRow]: {
|
||||||
|
openAddRowModal: () => {
|
||||||
|
setCreateChannelModalOpened(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getCurrentDisplayList = (data: Record<string, string | number | null>[]) => {
|
||||||
|
const newChannelsIdOnDisplay: string[] = data.map(val => val?.channelId) as string[];
|
||||||
|
|
||||||
|
setChannelsIdOnDisplay(newChannelsIdOnDisplay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeScreenProps = (data: Partial<ITableSortSearch>) =>
|
||||||
|
dispatch(
|
||||||
|
setScreenProps({
|
||||||
|
screen: StoreScreensType.Channels,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Error>{channelListErrorMessages[error.status] || error.message}</Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Body className="table-container">
|
||||||
|
{isFetching ? (
|
||||||
|
<Main>
|
||||||
|
<Loader />
|
||||||
|
</Main>
|
||||||
|
) : (
|
||||||
|
<TableWithPagination
|
||||||
|
title={'Channels'}
|
||||||
|
screenHeader={screenHeader}
|
||||||
|
columns={channelsColumns}
|
||||||
|
data={channels}
|
||||||
|
sortColumn={sortColumn}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
paginationItemText={'channels'}
|
||||||
|
getCurrentDisplayList={getCurrentDisplayList}
|
||||||
|
changeSortProps={changeScreenProps}
|
||||||
|
searchValue={searchValue}
|
||||||
|
changeSearch={changeScreenProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
{isCreateChannelModalOpened && <CreateChannelModal getChannelList={getChannelList} setCreateChannelModalOpened={setCreateChannelModalOpened} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequireAuthentication(ChannelsContainer);
|
||||||
65
src/views/channels/columns-config.tsx
Normal file
65
src/views/channels/columns-config.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {faEllipsisV} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {CellType, ColumnsType, DataRowType} from 'components/table';
|
||||||
|
import {ChannelIconMenu} from 'components/channel-icon-menu';
|
||||||
|
import {IconMenuPosition} from 'components/icon-menu/icon-menu';
|
||||||
|
import PublishingStateIndicator from 'components/indicator-component/publishing-state-indicator';
|
||||||
|
import {theme} from 'theme';
|
||||||
|
|
||||||
|
const ChannelPublishingStateIndicator = (row?: DataRowType) => <PublishingStateIndicator row={row} idKey="channelId" publishingStateKey="channelsPublishing" />;
|
||||||
|
|
||||||
|
export const columns: ColumnsType = {
|
||||||
|
indicator: {
|
||||||
|
title: '',
|
||||||
|
hasBorder: false,
|
||||||
|
width: 40,
|
||||||
|
type: CellType.Component,
|
||||||
|
renderCell: ChannelPublishingStateIndicator
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
title: 'Channel Name',
|
||||||
|
type: CellType.Link,
|
||||||
|
textCell: {propName: 'name'},
|
||||||
|
thStyle: {
|
||||||
|
textAlign: 'left',
|
||||||
|
paddingLeft: 16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
title: 'Alias',
|
||||||
|
textCell: {propName: 'alias'}
|
||||||
|
},
|
||||||
|
channelId: {
|
||||||
|
title: 'Channel Id',
|
||||||
|
hideColumnAt: theme.screenSizes.desktop,
|
||||||
|
textCell: {propName: 'channelId'}
|
||||||
|
},
|
||||||
|
streamKey: {
|
||||||
|
title: 'Stream Key',
|
||||||
|
hideColumnAt: theme.screenSizes.tablet,
|
||||||
|
textCell: {propName: 'streamKey'}
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
title: 'Created',
|
||||||
|
hideColumnAt: theme.screenSizes.large,
|
||||||
|
type: CellType.Date,
|
||||||
|
textCell: {propName: 'created'}
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
title: '',
|
||||||
|
hasBorder: false,
|
||||||
|
width: 50,
|
||||||
|
type: CellType.DropDown,
|
||||||
|
dropdownCell: {
|
||||||
|
Component: ChannelIconMenu,
|
||||||
|
keys: ['channelId', 'name', 'alias'],
|
||||||
|
componentProps: {
|
||||||
|
icon: faEllipsisV,
|
||||||
|
showTail: false,
|
||||||
|
position: IconMenuPosition.Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
124
src/views/channels/create-channel/create-channel-modal.tsx
Normal file
124
src/views/channels/create-channel/create-channel-modal.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {ChangeEvent, useState} from 'react';
|
||||||
|
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||||
|
import {createChannel} from 'services/channel.service';
|
||||||
|
|
||||||
|
import {transformToPortalError} from 'utility/error-handler';
|
||||||
|
import {createChannelErrorMessages} from 'constants/error-messages';
|
||||||
|
import {documentationLinks} from 'constants/links';
|
||||||
|
|
||||||
|
import Loader from 'components/loaders';
|
||||||
|
import {Input} from 'components/forms/Input';
|
||||||
|
import {NewTabLink} from 'components/new-tab-link';
|
||||||
|
import {Label} from 'components/label';
|
||||||
|
import {DialogForm, Error, FormLoaderContainer} from 'components/modal/modal-form-response/style';
|
||||||
|
import {Modal} from 'components/modal';
|
||||||
|
|
||||||
|
interface ICreateChannelInput {
|
||||||
|
setCreateChannelModalOpened: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
getChannelList: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateChannelModal = ({setCreateChannelModalOpened, getChannelList}: ICreateChannelInput): JSX.Element => {
|
||||||
|
const logger = LoggerFactory.getLogger('view/channels/create-channel/CreateChannelModal');
|
||||||
|
const initialState = {
|
||||||
|
alias: '',
|
||||||
|
name: '',
|
||||||
|
description: ''
|
||||||
|
};
|
||||||
|
const [inputValues, setInputValue] = useState(initialState);
|
||||||
|
const {alias, name, description} = inputValues;
|
||||||
|
const [isFormValid, setIsFormValid] = useState(true);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const {name, value} = event.target;
|
||||||
|
|
||||||
|
setInputValue({
|
||||||
|
...inputValues,
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setCreateChannelModalOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (): Promise<void> => {
|
||||||
|
const valid = alias.trim() && name.trim() && description.trim();
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
setIsFormValid(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Creating a channel with the following parameters : [%j]', {
|
||||||
|
alias,
|
||||||
|
name,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
await createChannel(alias, name, description);
|
||||||
|
|
||||||
|
logger.info('Channel [%s] was created successfully', alias);
|
||||||
|
|
||||||
|
setInputValue(initialState);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
await getChannelList();
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
} catch (e) {
|
||||||
|
const {status, message, requestPayload} = transformToPortalError(e);
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(createChannelErrorMessages[status] || message || createChannelErrorMessages['default']);
|
||||||
|
|
||||||
|
logger.error(`${createChannelErrorMessages[status] || message || createChannelErrorMessages['default']} [%s]`, status, requestPayload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
close={handleClose}
|
||||||
|
submitButton={{
|
||||||
|
className: 'testId-createChannel',
|
||||||
|
disabled: isLoading,
|
||||||
|
onClick: handleSubmit,
|
||||||
|
label: 'Create Channel'
|
||||||
|
}}
|
||||||
|
cancelButton={{onClick: handleClose}}>
|
||||||
|
{isLoading ? (
|
||||||
|
<FormLoaderContainer>
|
||||||
|
<Loader color="dark" />
|
||||||
|
</FormLoaderContainer>
|
||||||
|
) : (
|
||||||
|
<DialogForm>
|
||||||
|
<h3 className="testId-createChannelForm">
|
||||||
|
Create Channel <NewTabLink link={documentationLinks.createChannel} icon={faQuestionCircle} iconColor="black" />
|
||||||
|
</h3>
|
||||||
|
<Label htmlFor="alias" text="Alias" />
|
||||||
|
<Input onChange={handleChange} value={alias} name="alias" placeholder={'Alias'} error={!isFormValid && !alias} />
|
||||||
|
{!isFormValid && !alias && <Error className="error-message testId-displayMessage">Alias can not be empty</Error>}
|
||||||
|
<Label htmlFor="name" text="Name" />
|
||||||
|
<Input onChange={handleChange} value={name} name="name" placeholder={'Name'} error={!isFormValid && !name} />
|
||||||
|
{!isFormValid && !name && <Error className="error-message testId-displayMessage">Name can not be empty</Error>}
|
||||||
|
<Label htmlFor="description" text="Description" />
|
||||||
|
<Input onChange={handleChange} value={description} name="description" placeholder={'Description'} error={!isFormValid && !description} />
|
||||||
|
{!isFormValid && !description && <Error className="error-message testId-displayMessage">Description can not be empty</Error>}
|
||||||
|
{error && <Error className="error-message testId-displayMessage">{error}</Error>}
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateChannelModal;
|
||||||
4
src/views/channels/create-channel/index.tsx
Normal file
4
src/views/channels/create-channel/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export {default as CreateChannelModal} from './create-channel-modal';
|
||||||
4
src/views/channels/index.tsx
Normal file
4
src/views/channels/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
export {default} from './channels';
|
||||||
2
src/views/index.ts
Normal file
2
src/views/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './LoginForm';
|
||||||
|
export * from './ChannelList';
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noEmitHelpers": true,
|
"noEmitHelpers": false,
|
||||||
"importHelpers": true,
|
"importHelpers": false,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"routers/*": ["./routers/*"],
|
"routers/*": ["./routers/*"],
|
||||||
"store/*": ["./store/*"],
|
"store/*": ["./store/*"],
|
||||||
"services/*": ["./services/*"],
|
"services/*": ["./services/*"],
|
||||||
|
"theme/*": ["./theme/*"],
|
||||||
"lang/*": ["./lang/*"],
|
"lang/*": ["./lang/*"],
|
||||||
"views/*": ["./views/*"]
|
"views/*": ["./views/*"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
babel({
|
babel({
|
||||||
babelConfig: {
|
babelConfig: {
|
||||||
plugins: ['transform-amd-to-commonjs']
|
plugins: ['transform-amd-to-commonjs', 'babel-plugin-styled-components']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
@@ -26,6 +26,7 @@ export default defineConfig({
|
|||||||
routers: path.resolve(process.cwd(), 'src', 'routers'),
|
routers: path.resolve(process.cwd(), 'src', 'routers'),
|
||||||
store: path.resolve(process.cwd(), 'src', 'store'),
|
store: path.resolve(process.cwd(), 'src', 'store'),
|
||||||
services: path.resolve(process.cwd(), 'src', 'services'),
|
services: path.resolve(process.cwd(), 'src', 'services'),
|
||||||
|
theme: path.resolve(process.cwd(), 'src', 'theme'),
|
||||||
lang: path.resolve(process.cwd(), 'src', 'lang'),
|
lang: path.resolve(process.cwd(), 'src', 'lang'),
|
||||||
utility: path.resolve(process.cwd(), 'src', 'utility'),
|
utility: path.resolve(process.cwd(), 'src', 'utility'),
|
||||||
views: path.resolve(process.cwd(), 'src', 'views')
|
views: path.resolve(process.cwd(), 'src', 'views')
|
||||||
|
|||||||
Reference in New Issue
Block a user