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
|
||||
|
||||
## Setup
|
||||
|
||||
```
|
||||
nvm use
|
||||
npm i
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
npm run start
|
||||
```
|
||||
@@ -14,11 +16,13 @@ npm run start
|
||||
## Build
|
||||
|
||||
### Development
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```
|
||||
npm run build:prod
|
||||
```
|
||||
72
bun.lock
72
bun.lock
@@ -5,12 +5,16 @@
|
||||
"name": "webcontrolcenter-zinn-2",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"moment": "2.30.1",
|
||||
"phenix-web-proto": "2020.0.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-redux": "9.2.0",
|
||||
"react-router-dom": "7.8.2",
|
||||
"styled-components": "6.1.19",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.34.0",
|
||||
@@ -18,6 +22,7 @@
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@vitejs/plugin-react-swc": "4.0.1",
|
||||
"babel-plugin-styled-components": "2.1.4",
|
||||
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
@@ -25,8 +30,9 @@
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"globals": "16.3.0",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.42.0",
|
||||
"typescript-plugin-styled-components": "3.0.0",
|
||||
"vite": "7.1.4",
|
||||
"vite-plugin-babel": "1.3.2",
|
||||
"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/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-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-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-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/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/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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -238,6 +256,8 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
@@ -320,6 +344,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"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=="],
|
||||
@@ -574,6 +608,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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-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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -14,12 +14,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"moment": "2.30.1",
|
||||
"phenix-web-proto": "2020.0.3",
|
||||
"react": "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": {
|
||||
"@eslint/js": "9.34.0",
|
||||
@@ -27,6 +31,7 @@
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@vitejs/plugin-react-swc": "4.0.1",
|
||||
"babel-plugin-styled-components": "2.1.4",
|
||||
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
@@ -34,8 +39,9 @@
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"globals": "16.3.0",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.42.0",
|
||||
"typescript-plugin-styled-components": "3.0.0",
|
||||
"vite": "7.1.4",
|
||||
"vite-plugin-babel": "1.3.2",
|
||||
"vite-plugin-commonjs": "0.10.4"
|
||||
|
||||
28
src/App.tsx
28
src/App.tsx
@@ -1,26 +1,12 @@
|
||||
import {JSX, useState} from 'react';
|
||||
import {useAppDispatch} from './store';
|
||||
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);
|
||||
};
|
||||
import {JSX} from 'react';
|
||||
import Router from './routers';
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<h1>Hello World</h1>
|
||||
<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>
|
||||
<Router />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {Provider} from 'react-redux';
|
||||
import store from './store';
|
||||
import App from './App.tsx';
|
||||
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
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;
|
||||
redirect: string;
|
||||
roles: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class PhenixWebSocket extends MQWebSocket {
|
||||
@@ -60,7 +61,9 @@ export class PhenixWebSocket extends MQWebSocket {
|
||||
|
||||
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
|
||||
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++;
|
||||
@@ -89,27 +92,27 @@ export class PhenixWebSocket extends MQWebSocket {
|
||||
private initialize(): void {
|
||||
super.onEvent('connected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Online);
|
||||
})
|
||||
});
|
||||
|
||||
super.onEvent('disconnected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Offline);
|
||||
})
|
||||
});
|
||||
|
||||
super.onEvent('error', (error: unknown) => {
|
||||
this._logger.error('Error [%s]', error);
|
||||
this.setStatus(PhenixWebSocketStatus.Error);
|
||||
})
|
||||
});
|
||||
|
||||
super.onEvent('reconnecting', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Reconnecting);
|
||||
})
|
||||
});
|
||||
|
||||
super.onEvent('reconnected', () => {
|
||||
this.setStatus(PhenixWebSocketStatus.Online);
|
||||
})
|
||||
});
|
||||
|
||||
super.onEvent('timeout', () => {
|
||||
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
|
||||
}));
|
||||
|
||||
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}>(
|
||||
'authentication/authenticate',
|
||||
async (credentials, {rejectWithValue}) => {
|
||||
@@ -56,6 +68,7 @@ const authenticateCredentialsThunk = createAsyncThunk<IPhenixWebSocketResponse,
|
||||
} catch (error) {
|
||||
// Convert error to serializable format
|
||||
const errorMessage = error instanceof Error ? error.message : 'Authentication failed';
|
||||
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +80,7 @@ const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejec
|
||||
} catch (error) {
|
||||
// Convert error to serializable format
|
||||
const errorMessage = error instanceof Error ? error.message : 'Signout failed';
|
||||
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
@@ -95,14 +109,16 @@ const authenticationSlice = createSlice({
|
||||
setSessionId: (state, action: PayloadAction<string>) => {
|
||||
state.sessionId = action.payload;
|
||||
},
|
||||
setIsAuthenticated: (state, action: PayloadAction<boolean>) => {
|
||||
state.isAuthenticated = action.payload;
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
setRoles: (state, action: PayloadAction<string[]>) => {
|
||||
state.roles = action.payload;
|
||||
},
|
||||
setApplicationId: (state, action: PayloadAction<string>) => {
|
||||
state.applicationId = action.payload;
|
||||
setUnauthorized: state => {
|
||||
state.isAuthenticated = false;
|
||||
state.isLoading = false;
|
||||
state.error = 'Unauthorized';
|
||||
state.secret = null;
|
||||
state.status = 'Offline';
|
||||
state.roles = [];
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
@@ -119,17 +135,18 @@ const authenticationSlice = createSlice({
|
||||
state.sessionId = authenticationResponse.sessionId ?? null;
|
||||
state.isAuthenticated = true;
|
||||
state.roles = authenticationResponse.roles ?? [];
|
||||
state.status = 'Online';
|
||||
state.isLoading = false;
|
||||
} else {
|
||||
state.applicationId = null;
|
||||
state.sessionId = null;
|
||||
state.isAuthenticated = false;
|
||||
state.secret = null;
|
||||
state.roles = [];
|
||||
}
|
||||
|
||||
state.status = 'Online';
|
||||
state.status = 'Offline';
|
||||
state.error = 'Invalid credentials. Please check your Application ID and Secret.';
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
}
|
||||
})
|
||||
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
||||
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 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 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({
|
||||
reducer: {
|
||||
authentication: AuthenticationState
|
||||
}
|
||||
authentication: AuthenticationReducer,
|
||||
channels: ChannelsReducer
|
||||
},
|
||||
middleware: getDefaultMiddleware =>
|
||||
getDefaultMiddleware({
|
||||
thunk: true,
|
||||
serializableCheck: false
|
||||
}).concat(authenticateRequestMiddleware, vanillaPromiseMiddleware, loggerMiddleware)
|
||||
});
|
||||
|
||||
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,
|
||||
"sourceMap": true,
|
||||
"noEmit": true,
|
||||
"noEmitHelpers": true,
|
||||
"importHelpers": true,
|
||||
"noEmitHelpers": false,
|
||||
"importHelpers": false,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": true,
|
||||
@@ -28,6 +28,7 @@
|
||||
"routers/*": ["./routers/*"],
|
||||
"store/*": ["./store/*"],
|
||||
"services/*": ["./services/*"],
|
||||
"theme/*": ["./theme/*"],
|
||||
"lang/*": ["./lang/*"],
|
||||
"views/*": ["./views/*"]
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ export default defineConfig({
|
||||
react(),
|
||||
babel({
|
||||
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'),
|
||||
store: path.resolve(process.cwd(), 'src', 'store'),
|
||||
services: path.resolve(process.cwd(), 'src', 'services'),
|
||||
theme: path.resolve(process.cwd(), 'src', 'theme'),
|
||||
lang: path.resolve(process.cwd(), 'src', 'lang'),
|
||||
utility: path.resolve(process.cwd(), 'src', 'utility'),
|
||||
views: path.resolve(process.cwd(), 'src', 'views')
|
||||
|
||||
Reference in New Issue
Block a user