From 5e2125d5e7240e84a17ab22c05cfcd61dd4bc2ee Mon Sep 17 00:00:00 2001 From: kappa118 Date: Tue, 25 Feb 2025 17:13:56 -0500 Subject: [PATCH] near-complete feature parity with current frontend, another big push of features, some styling to dialogs / forms --- frontend/eslint.config.mjs | 12 + frontend/package-lock.json | 147 ++++- frontend/package.json | 5 +- frontend/prettier.config.js | 10 + frontend/src/App.js | 137 +++-- frontend/src/api.js | 151 +++-- frontend/src/components/HeaderBar.js | 55 -- frontend/src/components/Sidebar.js | 41 +- frontend/src/components/forms/Channel.js | 525 ++++++++++-------- frontend/src/components/forms/ChannelGroup.js | 94 ++++ frontend/src/components/forms/EPG.js | 130 +++-- frontend/src/components/forms/LoginForm.js | 29 +- frontend/src/components/forms/M3U.js | 180 +++--- frontend/src/components/forms/NewTable.js | 0 frontend/src/components/forms/Stream.js | 111 ++-- .../src/components/forms/StreamProfile.js | 127 +++-- frontend/src/components/forms/UserAgent.js | 120 ++-- .../src/components/tables/ChannelsTable.js | 148 ++--- frontend/src/components/tables/EPGsTable.js | 111 ++-- frontend/src/components/tables/Filter.js | 86 --- frontend/src/components/tables/M3UsTable.js | 121 ++-- .../components/tables/StreamProfilesTable.js | 120 ++-- .../src/components/tables/StreamsTable.js | 128 +++-- .../src/components/tables/UserAgentsTable.js | 112 ++-- frontend/src/helpers/index.js | 4 +- frontend/src/helpers/table.js | 10 +- frontend/src/index.js | 34 +- frontend/src/pages/Channels.js | 38 +- frontend/src/pages/Dashboard.js | 4 +- frontend/src/pages/EPG.js | 22 +- frontend/src/pages/Guide.js | 70 ++- .../src/pages/Guide/components/ChannelItem.js | 16 - .../src/pages/Guide/components/ProgramItem.js | 45 -- .../src/pages/Guide/components/Timeline.js | 47 -- frontend/src/pages/Guide/components/index.js | 3 - frontend/src/pages/Guide/theme.js | 43 -- frontend/src/pages/Guide/useApp.js | 143 ----- frontend/src/pages/Home.js | 4 +- frontend/src/pages/Login.js | 6 +- frontend/src/pages/M3U.js | 57 +- frontend/src/pages/StreamProfiles.js | 8 +- frontend/src/store/auth.js | 70 ++- frontend/src/store/channels.js | 51 +- frontend/src/store/epgs.js | 22 +- frontend/src/store/playlists.js | 33 +- frontend/src/store/streamProfiles.js | 27 +- frontend/src/store/streams.js | 29 +- frontend/src/store/userAgents.js | 33 +- 48 files changed, 1853 insertions(+), 1666 deletions(-) create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/prettier.config.js delete mode 100644 frontend/src/components/HeaderBar.js create mode 100644 frontend/src/components/forms/ChannelGroup.js delete mode 100644 frontend/src/components/forms/NewTable.js delete mode 100644 frontend/src/components/tables/Filter.js delete mode 100644 frontend/src/pages/Guide/components/ChannelItem.js delete mode 100644 frontend/src/pages/Guide/components/ProgramItem.js delete mode 100644 frontend/src/pages/Guide/components/Timeline.js delete mode 100644 frontend/src/pages/Guide/components/index.js delete mode 100644 frontend/src/pages/Guide/theme.js delete mode 100644 frontend/src/pages/Guide/useApp.js diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 00000000..bb72d115 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,12 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReact from "eslint-plugin-react"; + + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + {files: ["**/*.{js,mjs,cjs,jsx}"]}, + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + pluginReact.configs.flat.recommended, +]; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cadc68e8..b986bae7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,9 +14,11 @@ "@mui/icons-material": "^6.4.5", "@mui/material": "^6.4.5", "axios": "^1.7.9", + "eslint": "^8.57.1", "formik": "^2.4.6", "material-react-table": "^3.2.0", "planby": "^1.1.7", + "prettier": "^3.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.2.0", @@ -111,6 +113,7 @@ "version": "7.26.8", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.26.8.tgz", "integrity": "sha512-3tBctaHRW6xSub26z7n8uyOTwwUsCdvIug/oxBH9n6yCO5hMj2vwDJAo7RbBMKrM7P+W2j61zLKviJQFGOYKMg==", + "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -128,6 +131,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "license": "Apache-2.0", "engines": { "node": ">=10" } @@ -136,6 +140,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -2393,6 +2398,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2414,12 +2420,14 @@ "node_modules/@eslint/eslintrc/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2434,6 +2442,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2445,6 +2454,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2456,6 +2466,7 @@ "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2470,6 +2481,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -2495,7 +2507,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead" + "deprecated": "Use @eslint/object-schema instead", + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -3296,6 +3309,7 @@ "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "license": "MIT", "dependencies": { "eslint-scope": "5.1.1" } @@ -3304,6 +3318,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -3316,6 +3331,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -3494,12 +3510,14 @@ "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz", - "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==" + "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", + "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.24.51", @@ -3968,7 +3986,8 @@ "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", @@ -4053,7 +4072,8 @@ "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", @@ -4125,6 +4145,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -4158,6 +4179,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "license": "MIT", "dependencies": { "@typescript-eslint/utils": "5.62.0" }, @@ -4176,6 +4198,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4202,6 +4225,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" @@ -4218,6 +4242,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", @@ -4244,6 +4269,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4256,6 +4282,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", @@ -4282,6 +4309,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", @@ -4307,6 +4335,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -4319,6 +4348,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -4327,6 +4357,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "license": "MIT", "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" @@ -4342,7 +4373,8 @@ "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -4546,6 +4578,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -4740,6 +4773,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -4768,6 +4802,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4795,6 +4830,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4814,6 +4850,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4833,6 +4870,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4850,6 +4888,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -4887,6 +4926,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4926,7 +4966,8 @@ "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==" + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "license": "MIT" }, "node_modules/async": { "version": "3.2.6", @@ -5008,6 +5049,7 @@ "version": "4.10.2", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -5040,6 +5082,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", "engines": { "node": ">= 0.4" } @@ -5914,7 +5957,8 @@ "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "license": "MIT" }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", @@ -6387,7 +6431,8 @@ "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "license": "BSD-2-Clause" }, "node_modules/data-urls": { "version": "2.0.0", @@ -6667,6 +6712,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -6988,6 +7034,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -7044,6 +7091,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -7125,6 +7173,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7179,6 +7228,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "license": "MIT", "dependencies": { "@babel/core": "^7.16.0", "@babel/eslint-parser": "^7.16.3", @@ -7206,6 +7256,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -7216,6 +7267,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7224,6 +7276,7 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -7240,6 +7293,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7248,6 +7302,7 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "license": "BSD-3-Clause", "dependencies": { "lodash": "^4.17.21", "string-natural-compare": "^3.0.1" @@ -7265,6 +7320,7 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -7297,6 +7353,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -7305,6 +7362,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -7316,6 +7374,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7324,6 +7383,7 @@ "version": "25.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "license": "MIT", "dependencies": { "@typescript-eslint/experimental-utils": "^5.0.0" }, @@ -7347,6 +7407,7 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "license": "MIT", "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -7375,6 +7436,7 @@ "version": "7.37.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7406,6 +7468,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -7417,6 +7480,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -7428,6 +7492,7 @@ "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -7444,6 +7509,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7452,6 +7518,7 @@ "version": "5.11.1", "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "license": "MIT", "dependencies": { "@typescript-eslint/utils": "^5.58.0" }, @@ -7467,6 +7534,7 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -7493,6 +7561,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", + "license": "MIT", "dependencies": { "@types/eslint": "^7.29.0 || ^8.4.1", "jest-worker": "^28.0.2", @@ -7516,6 +7585,7 @@ "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7529,6 +7599,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7542,7 +7613,8 @@ "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", @@ -7563,6 +7635,7 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -7577,6 +7650,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7630,6 +7704,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -7641,6 +7716,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -7918,6 +7994,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -8074,6 +8151,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -8086,7 +8164,8 @@ "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -9400,6 +9479,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9695,6 +9775,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -10667,7 +10748,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -10745,6 +10827,7 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -10759,6 +10842,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -10790,12 +10874,14 @@ "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==" + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "license": "CC0-1.0" }, "node_modules/language-tags": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "license": "MIT", "dependencies": { "language-subtag-registry": "^0.3.20" }, @@ -11290,7 +11376,8 @@ "node_modules/natural-compare-lite": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.4", @@ -11444,6 +11531,7 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -11457,6 +11545,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -11494,6 +11583,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -13153,6 +13243,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14875,7 +14980,8 @@ "node_modules/string-natural-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "license": "MIT" }, "node_modules/string-width": { "version": "4.2.3", @@ -14918,6 +15024,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -14957,6 +15064,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -15673,6 +15781,7 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -15684,6 +15793,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -15695,6 +15805,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -15708,6 +15819,7 @@ "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "license": "MIT", "dependencies": { "tslib": "^1.8.1" }, @@ -15721,7 +15833,8 @@ "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/turbo-stream": { "version": "2.4.0", diff --git a/frontend/package.json b/frontend/package.json index e150a95a..39e73287 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,9 +9,11 @@ "@mui/icons-material": "^6.4.5", "@mui/material": "^6.4.5", "axios": "^1.7.9", + "eslint": "^8.57.1", "formik": "^2.4.6", "material-react-table": "^3.2.0", "planby": "^1.1.7", + "prettier": "^3.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.2.0", @@ -24,7 +26,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "format": "prettier --write ." }, "eslintConfig": { "extends": [ diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js new file mode 100644 index 00000000..33fe4a21 --- /dev/null +++ b/frontend/prettier.config.js @@ -0,0 +1,10 @@ +// prettier.config.js or .prettierrc.js +module.exports = { + semi: true, // Add semicolons at the end of statements + singleQuote: true, // Use single quotes instead of double + tabWidth: 2, // Set the indentation width + trailingComma: "es5", // Add trailing commas where valid in ES5 + printWidth: 80, // Wrap lines at 80 characters + bracketSpacing: true, // Add spaces inside object braces + arrowParens: "always", // Always include parentheses around arrow function parameters +}; diff --git a/frontend/src/App.js b/frontend/src/App.js index 3db316bd..f2662885 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,13 +1,19 @@ -// src/App.js -import React, { useState } from 'react'; -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { + BrowserRouter as Router, + Route, + Routes, + Navigate, +} from 'react-router-dom'; import Sidebar from './components/Sidebar'; import Login from './pages/Login'; -import useAuthStore from './store/auth'; import Channels from './pages/Channels'; import M3U from './pages/M3U'; -import { ThemeProvider } from '@mui/material/styles'; // Import theme tools +import { ThemeProvider } from '@mui/material/styles'; // Import theme tools import { + AppBar, + Toolbar, + Typography, Box, CssBaseline, Drawer, @@ -17,32 +23,63 @@ import { ListItemText, Divider, } from '@mui/material'; -import theme from './theme' +import theme from './theme'; import EPG from './pages/EPG'; import Guide from './pages/Guide'; import StreamProfiles from './pages/StreamProfiles'; +import useAuthStore from './store/auth'; +import API from './api'; const drawerWidth = 240; const miniDrawerWidth = 60; -// Protected Route Component -const ProtectedRoute = ({ element, ...rest }) => { - const { isAuthenticated } = useAuthStore(); - - return isAuthenticated ? element : ; -}; +const defaultRoute = '/channels'; const App = () => { const [open, setOpen] = useState(true); + const { + isAuthenticated, + setIsAuthenticated, + logout, + initData, + initializeAuth, + } = useAuthStore(); const toggleDrawer = () => { setOpen(!open); }; + useEffect(() => { + const checkAuth = async () => { + const loggedIn = await initializeAuth(); + + if (loggedIn) { + await initData(); + setIsAuthenticated(true); + } else { + await logout(); + } + }; + + checkAuth(); + }, [initializeAuth, initData, setIsAuthenticated, logout]); + return ( + {/* theme.zIndex.drawer + 1, + width: `calc(100% - ${open ? drawerWidth : miniDrawerWidth}px)`, + ml: `${open ? drawerWidth : miniDrawerWidth}px`, + transition: 'width 0.3s, margin-left 0.3s', + }} + > + + */} + { }} > {/* Drawer Toggle Button */} - + - - - {open && } + + + {open && ( + + )} @@ -71,28 +117,57 @@ const App = () => { - + {/* Fixed Header */} {/* */} {/* Main Content Area between Header and Footer */} - + - } /> - }/>} /> - }/>} /> - }/>} /> - }/>} /> - }/>} /> + {isAuthenticated ? ( + <> + } /> + } /> + } /> + } + /> + } /> + + ) : ( + } /> + )} + {/* Redirect if no match */} + + } + /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 510b4e36..a19d556c 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,4 +1,4 @@ -import Axios from 'axios' +import Axios from 'axios'; import useAuthStore from './store/auth'; import useChannelsStore from './store/channels'; import useUserAgentsStore from './store/userAgents'; @@ -7,11 +7,11 @@ import useEPGsStore from './store/epgs'; import useStreamsStore from './store/streams'; import useStreamProfilesStore from './store/streamProfiles'; -const axios = Axios.create({ - withCredentials: true, -}) +// const axios = Axios.create({ +// withCredentials: true, +// }); -const host = "http://192.168.1.151:9191" +const host = 'http://192.168.1.151:9191'; const getAuthToken = async () => { const token = await useAuthStore.getState().getToken(); // Assuming token is stored in Zustand store @@ -28,14 +28,14 @@ export default class API { body: JSON.stringify({ username, password }), }); - return await response.json() + return await response.json(); } static async refreshToken(refreshToken) { const response = await fetch(`${host}/api/accounts/token/refresh/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refresh: refreshToken }) + body: JSON.stringify({ refresh: refreshToken }), }); const retval = await response.json(); @@ -45,9 +45,9 @@ export default class API { static async logout() { const response = await fetch(`${host}/api/accounts/auth/logout/`, { method: 'POST', - }) + }); - return response.data.data + return response.data.data; } static async getChannels() { @@ -74,33 +74,72 @@ export default class API { return retval; } + static async addChannelGroup(values) { + const response = await fetch(`${host}/api/channels/groups/`, { + method: 'POST', + headers: { + Authorization: `Bearer ${await getAuthToken()}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(values), + }); + + const retval = await response.json(); + if (retval.id) { + useChannelsStore.getState().addChannelGroup(retval); + } + + return retval; + } + + static async updateChannelGroup(values) { + const { id, ...payload } = values; + const response = await fetch(`${host}/api/channels/groups/${id}/`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${await getAuthToken()}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + const retval = await response.json(); + if (retval.id) { + useChannelsStore.getState().updateChannelGroup(retval); + } + + return retval; + } + static async addChannel(channel) { - let body = null + let body = null; if (channel.logo_file) { body = new FormData(); for (const prop in channel) { - body.append(prop, channel[prop]) + body.append(prop, channel[prop]); } } else { - body = {...channel} - delete body.logo_file - body = JSON.stringify(body) + body = { ...channel }; + delete body.logo_file; + body = JSON.stringify(body); } const response = await fetch(`${host}/api/channels/channels/`, { method: 'POST', headers: { Authorization: `Bearer ${await getAuthToken()}`, - ...(channel.logo_file ? {} :{ - 'Content-Type': 'application/json', - }) + ...(channel.logo_file + ? {} + : { + 'Content-Type': 'application/json', + }), }, body: body, }); const retval = await response.json(); if (retval.id) { - useChannelsStore.getState().addChannel(retval) + useChannelsStore.getState().addChannel(retval); } return retval; @@ -115,7 +154,7 @@ export default class API { }, }); - useChannelsStore.getState().removeChannels([id]) + useChannelsStore.getState().removeChannels([id]); } // @TODO: the bulk delete endpoint is currently broken @@ -133,7 +172,7 @@ export default class API { // } static async updateChannel(values) { - const {id, ...payload} = values + const { id, ...payload } = values; const response = await fetch(`${host}/api/channels/channels/${id}/`, { method: 'PUT', headers: { @@ -145,7 +184,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useChannelsStore.getState().updateChannel(retval) + useChannelsStore.getState().updateChannel(retval); } return retval; @@ -163,7 +202,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useChannelsStore.getState().addChannel(retval) + useChannelsStore.getState().addChannel(retval); } return retval; @@ -181,7 +220,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useChannelsStore.getState().addChannel(retval) + useChannelsStore.getState().addChannel(retval); } return retval; @@ -211,14 +250,14 @@ export default class API { const retval = await response.json(); if (retval.id) { - useStreamsStore.getState().addStream(retval) + useStreamsStore.getState().addStream(retval); } return retval; } static async updateStream(values) { - const {id, ...payload} = values + const { id, ...payload } = values; const response = await fetch(`${host}/api/channels/streams/${id}/`, { method: 'PUT', headers: { @@ -230,7 +269,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useStreamsStore.getState().updateStream(retval) + useStreamsStore.getState().updateStream(retval); } return retval; @@ -245,7 +284,7 @@ export default class API { }, }); - useStreamsStore.getState().removeStreams([id]) + useStreamsStore.getState().removeStreams([id]); } static async deleteStreams(ids) { @@ -255,10 +294,10 @@ export default class API { Authorization: `Bearer ${await getAuthToken()}`, 'Content-Type': 'application/json', }, - body: JSON.stringify({ stream_ids: ids }) + body: JSON.stringify({ stream_ids: ids }), }); - useStreamsStore.getState().removeStreams(ids) + useStreamsStore.getState().removeStreams(ids); } static async getUserAgents() { @@ -285,14 +324,14 @@ export default class API { const retval = await response.json(); if (retval.id) { - useUserAgentsStore.getState().addUserAgent(retval) + useUserAgentsStore.getState().addUserAgent(retval); } return retval; } static async updateUserAgent(values) { - const {id, ...payload} = values + const { id, ...payload } = values; const response = await fetch(`${host}/api/core/useragents/${id}/`, { method: 'PUT', headers: { @@ -304,7 +343,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useUserAgentsStore.getState().updateUserAgent(retval) + useUserAgentsStore.getState().updateUserAgent(retval); } return retval; @@ -319,7 +358,7 @@ export default class API { }, }); - useUserAgentsStore.getState().removeUserAgents([id]) + useUserAgentsStore.getState().removeUserAgents([id]); } static async getPlaylists() { @@ -346,7 +385,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - usePlaylistsStore.getState().addPlaylist(retval) + usePlaylistsStore.getState().addPlaylist(retval); } return retval; @@ -387,11 +426,11 @@ export default class API { }, }); - usePlaylistsStore.getState().removePlaylists([id]) + usePlaylistsStore.getState().removePlaylists([id]); } static async updatePlaylist(values) { - const {id, ...payload} = values + const { id, ...payload } = values; const response = await fetch(`${host}/api/m3u/accounts/${id}/`, { method: 'PUT', headers: { @@ -403,7 +442,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - usePlaylistsStore.getState().updatePlaylist(retval) + usePlaylistsStore.getState().updatePlaylist(retval); } return retval; @@ -435,32 +474,34 @@ export default class API { } static async addEPG(values) { - let body = null + let body = null; if (values.epg_file) { body = new FormData(); for (const prop in values) { - body.append(prop, values[prop]) + body.append(prop, values[prop]); } } else { - body = {...values} - delete body.epg_file - body = JSON.stringify(body) + body = { ...values }; + delete body.epg_file; + body = JSON.stringify(body); } const response = await fetch(`${host}/api/epg/sources/`, { method: 'POST', headers: { Authorization: `Bearer ${await getAuthToken()}`, - ...(values.epg_file ? {} :{ - 'Content-Type': 'application/json', - }) + ...(values.epg_file + ? {} + : { + 'Content-Type': 'application/json', + }), }, body, }); const retval = await response.json(); if (retval.id) { - useEPGsStore.getState().addEPG(retval) + useEPGsStore.getState().addEPG(retval); } return retval; @@ -475,7 +516,7 @@ export default class API { }, }); - useEPGsStore.getState().removeEPGs([id]) + useEPGsStore.getState().removeEPGs([id]); } static async refreshEPG(id) { @@ -511,18 +552,18 @@ export default class API { Authorization: `Bearer ${await getAuthToken()}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(values) + body: JSON.stringify(values), }); const retval = await response.json(); if (retval.id) { - useStreamProfilesStore.getState().addStreamProfile(retval) + useStreamProfilesStore.getState().addStreamProfile(retval); } return retval; } static async updateStreamProfile(values) { - const {id, ...payload} = values + const { id, ...payload } = values; const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, { method: 'PUT', headers: { @@ -534,7 +575,7 @@ export default class API { const retval = await response.json(); if (retval.id) { - useStreamProfilesStore.getState().updateStreamProfile(retval) + useStreamProfilesStore.getState().updateStreamProfile(retval); } return retval; @@ -549,7 +590,7 @@ export default class API { }, }); - useStreamProfilesStore.getState().removeStreamProfiles([id]) + useStreamProfilesStore.getState().removeStreamProfiles([id]); } static async getGrid() { @@ -560,8 +601,8 @@ export default class API { }, }); - const retval = await response.json() - console.log(retval) - return retval + const retval = await response.json(); + console.log(retval); + return retval; } } diff --git a/frontend/src/components/HeaderBar.js b/frontend/src/components/HeaderBar.js deleted file mode 100644 index 1e5132d0..00000000 --- a/frontend/src/components/HeaderBar.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -const HeaderBar = () => { - return ( -
- - -
- ); -}; - -export default HeaderBar; diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js index 3ecc7bb4..47c9ef4d 100644 --- a/frontend/src/components/Sidebar.js +++ b/frontend/src/components/Sidebar.js @@ -1,33 +1,30 @@ -import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import React from "react"; +import { Link, useLocation } from "react-router-dom"; import { - Drawer, List, ListItem, ListItemButton, ListItemText, ListItemIcon, - Divider, -} from '@mui/material'; +} from "@mui/material"; import { - Menu as MenuIcon, - Home as HomeIcon, - Settings as SettingsIcon, - Info as InfoIcon, - Description as DescriptionIcon, Tv as TvIcon, CalendarMonth as CalendarMonthIcon, VideoFile as VideoFileIcon, LiveTv as LiveTvIcon, PlaylistPlay as PlaylistPlayIcon, -} from '@mui/icons-material'; +} from "@mui/icons-material"; const items = [ - { text: 'Channels', icon: , route: "/channels" }, - { text: 'M3U', icon: , route: "/m3u" }, - { text: 'EPG', icon: , route: "/epg" }, - { text: 'Stream Profiles', icon: , route: "/stream-profiles" }, - { text: 'TV Guide', icon: , route: "/guide" }, + { text: "Channels", icon: , route: "/channels" }, + { text: "M3U", icon: , route: "/m3u" }, + { text: "EPG", icon: , route: "/epg" }, + { + text: "Stream Profiles", + icon: , + route: "/stream-profiles", + }, + { text: "TV Guide", icon: , route: "/guide" }, ]; const Sidebar = ({ open }) => { @@ -37,10 +34,14 @@ const Sidebar = ({ open }) => { {items.map((item) => ( - - {item.icon} - {open && } - + + {item.icon} + {open && } + ))} diff --git a/frontend/src/components/forms/Channel.js b/frontend/src/components/forms/Channel.js index 33e511a0..b3a24fa6 100644 --- a/frontend/src/components/forms/Channel.js +++ b/frontend/src/components/forms/Channel.js @@ -1,8 +1,6 @@ -// Modal.js -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo } from 'react'; import { Box, - Modal, Typography, Stack, TextField, @@ -14,47 +12,53 @@ import { FormControl, CircularProgress, IconButton, -} from "@mui/material"; + Dialog, + DialogTitle, + DialogContent, + DialogActions, + FormHelperText, +} from '@mui/material'; import { useFormik } from 'formik'; import * as Yup from 'yup'; -import useChannelsStore from "../../store/channels"; -import API from "../../api" -import useStreamProfilesStore from "../../store/streamProfiles"; +import useChannelsStore from '../../store/channels'; +import API from '../../api'; +import useStreamProfilesStore from '../../store/streamProfiles'; +import { Add as AddIcon, Remove as RemoveIcon } from '@mui/icons-material'; +import useStreamsStore from '../../store/streams'; import { - Add as AddIcon, - Delete as DeleteIcon, - } from "@mui/icons-material"; -import useStreamsStore from "../../store/streams"; -import usePlaylistsStore from "../../store/playlists"; -import { MaterialReactTable, useMaterialReactTable } from "material-react-table"; + MaterialReactTable, + useMaterialReactTable, +} from 'material-react-table'; +import ChannelGroupForm from './ChannelGroup'; +import usePlaylistsStore from '../../store/playlists'; const Channel = ({ channel = null, isOpen, onClose }) => { const channelGroups = useChannelsStore((state) => state.channelGroups); - const streams = useStreamsStore(state => state.streams) - const playlists = usePlaylistsStore(state => state.playlists) - const streamProfiles = useStreamProfilesStore((state) => state.profiles); + const streams = useStreamsStore((state) => state.streams); + const { profiles: streamProfiles } = useStreamProfilesStore(); + const { playlists } = usePlaylistsStore(); - const [logo, setLogo] = useState(null) - const [logoPreview, setLogoPreview] = useState(null) - - const [channelStreams, setChannelStreams] = useState([]) + const [logo, setLogo] = useState(null); + const [logoPreview, setLogoPreview] = useState('/images/logo.png'); + const [channelStreams, setChannelStreams] = useState([]); + const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false); const addStream = (stream) => { - const streamSet = new Set(channelStreams) - streamSet.add(stream) - setChannelStreams(Array.from(streamSet)) - } + const streamSet = new Set(channelStreams); + streamSet.add(stream); + setChannelStreams(Array.from(streamSet)); + }; const removeStream = (stream) => { - const streamSet = new Set(channelStreams) - streamSet.delete(stream) - setChannelStreams(Array.from(streamSet)) - } + const streamSet = new Set(channelStreams); + streamSet.delete(stream); + setChannelStreams(Array.from(streamSet)); + }; const handleLogoChange = (e) => { const file = e.target.files[0]; if (file) { - setLogo(file) + setLogo(file); setLogoPreview(URL.createObjectURL(file)); } }; @@ -74,23 +78,29 @@ const Channel = ({ channel = null, isOpen, onClose }) => { channel_group_id: Yup.string().required('Channel group is required'), }), onSubmit: async (values, { setSubmitting, resetForm }) => { + console.log(values); if (channel?.id) { - await API.updateChannel({id: channel.id, ...values, logo_file: logo, streams: channelStreams.map(stream => stream.id)}) + await API.updateChannel({ + id: channel.id, + ...values, + logo_file: logo, + streams: channelStreams.map((stream) => stream.id), + }); } else { await API.addChannel({ ...values, logo_file: logo, - streams: channelStreams.map(stream => stream.id), - }) + streams: channelStreams.map((stream) => stream.id), + }); } resetForm(); - setLogo(null) - setLogoPreview(null) + setLogo(null); + setLogoPreview('/images/logo.png'); setSubmitting(false); - onClose() - } - }) + onClose(); + }, + }); useEffect(() => { if (channel) { @@ -98,11 +108,22 @@ const Channel = ({ channel = null, isOpen, onClose }) => { channel_name: channel.channel_name, channel_number: channel.channel_number, channel_group_id: channel.channel_group?.id, + stream_profile_id: channel.stream_profile_id, tvg_id: channel.tvg_id, tvg_name: channel.tvg_name, }); - setChannelStreams(streams.filter(stream => channel.streams.includes(stream.id))) + console.log('channel streams'); + console.log(channel.streams); + const filteredStreams = streams + .filter((stream) => channel.streams.includes(stream.id)) + .sort( + (a, b) => + channel.streams.indexOf(a.id) - channel.streams.indexOf(b.id) + ); + console.log('filtered streams'); + console.log(filteredStreams); + setChannelStreams(filteredStreams); } else { formik.resetForm(); } @@ -110,27 +131,32 @@ const Channel = ({ channel = null, isOpen, onClose }) => { const activeStreamsTable = useMaterialReactTable({ data: channelStreams, - columns: useMemo(() => [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'M3U', - accessorKey: 'group_name', - }, - ], []), + columns: useMemo( + () => [ + { + header: 'Name', + accessorKey: 'name', + }, + { + header: 'M3U', + accessorKey: 'group_name', + }, + ], + [] + ), + enableSorting: false, enableBottomToolbar: false, enableTopToolbar: false, columnFilterDisplayMode: 'popover', enablePagination: false, enableRowVirtualization: true, - enableRowSelection: true, + enableRowOrdering: true, rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { density: 'compact', }, enableRowActions: true, + positionActionsColumn: 'last', renderRowActions: ({ row }) => ( <> { color="error" // Red color for delete actions onClick={() => removeStream(row.original)} > - {/* Small icon size */} + {/* Small icon size */} ), - positionActionsColumn: 'last', muiTableContainerProps: { sx: { height: '200px', }, }, - }) + muiRowDragHandleProps: ({ table }) => ({ + onDragEnd: () => { + const { draggingRow, hoveredRow } = table.getState(); + + if (hoveredRow && draggingRow) { + channelStreams.splice( + hoveredRow.index, + 0, + channelStreams.splice(draggingRow.index, 1)[0] + ); + + setChannelStreams([...channelStreams]); + } + }, + }), + }); const availableStreamsTable = useMaterialReactTable({ data: streams, - columns: useMemo(() => [ - { - header: 'Name', - accessorKey: 'name', - }, - { - header: 'M3U', - accessorKey: 'group_name', - }, - ], []), + columns: useMemo( + () => [ + { + header: 'Name', + accessorKey: 'name', + }, + { + header: 'M3U', + accessorFn: (row) => + playlists.find((playlist) => playlist.id === row.m3u_account)?.name, + }, + ], + [] + ), enableBottomToolbar: false, enableTopToolbar: false, columnFilterDisplayMode: 'popover', enablePagination: false, enableRowVirtualization: true, - enableRowSelection: true, rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { density: 'compact', @@ -190,196 +233,242 @@ const Channel = ({ channel = null, isOpen, onClose }) => { height: '200px', }, }, - }) + }); if (!isOpen) { - return <> + return <>; } return ( - - - + <> + + Channel - +
- - - - - - - Channel Group - - + /> - - Stream Profile - + {channelGroups.map((option, index) => ( + + {option.name} + + ))} + + + {formik.touched.channel_group_id && + formik.errors.channel_group_id} + + + + + setChannelGroupModalOpen(true)} + title="Create new group" + size="small" + variant="filled" + > + + + + + + + + Stream Profile + + + + + - {streamProfiles.map((option, index) => ( - - {option.name} - - ))} - - + /> + - + + - + - - - - - - - {/* File upload input */} - - Logo - {/* Display selected image */} - - {logo && ( + + {/* File upload input */} + + Logo + {/* Display selected image */} + Selected - )} - - - - handleLogoChange(event)} - style={{ display: 'none' }} - /> - - + + handleLogoChange(event)} + style={{ display: 'none' }} + /> + + + + - - + + + + Active Streams + + + + + Available Streams + + + + + + {/* Submit button */} - + - - - - Active Streams - - - - - Available Streams - - - -
-
+ + setChannelGroupModalOpen(false)} + /> + ); }; -const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: "1200px", - bgcolor: 'background.paper', - boxShadow: 24, - p: 4, -}; - export default Channel; diff --git a/frontend/src/components/forms/ChannelGroup.js b/frontend/src/components/forms/ChannelGroup.js new file mode 100644 index 00000000..dd4f2ee4 --- /dev/null +++ b/frontend/src/components/forms/ChannelGroup.js @@ -0,0 +1,94 @@ +// Modal.js +import React, { useEffect } from "react"; +import { + TextField, + Button, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import { useFormik } from "formik"; +import * as Yup from "yup"; +import API from "../../api"; + +const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => { + const formik = useFormik({ + initialValues: { + name: "", + }, + validationSchema: Yup.object({ + name: Yup.string().required("Name is required"), + }), + onSubmit: async (values, { setSubmitting, resetForm }) => { + if (channelGroup?.id) { + await API.updateChannelGroup({ id: channelGroup.id, ...values }); + } else { + await API.addChannelGroup(values); + } + + resetForm(); + setSubmitting(false); + onClose(); + }, + }); + + useEffect(() => { + if (channelGroup) { + formik.setValues({ + name: channelGroup.name, + }); + } else { + formik.resetForm(); + } + }, [channelGroup]); + + if (!isOpen) { + return <>; + } + + return ( + + + Channel Group + + +
+ + + + + {/* Submit button */} + + +
+
+ ); +}; + +export default ChannelGroup; diff --git a/frontend/src/components/forms/EPG.js b/frontend/src/components/forms/EPG.js index 3481dd20..df2c4707 100644 --- a/frontend/src/components/forms/EPG.js +++ b/frontend/src/components/forms/EPG.js @@ -1,50 +1,67 @@ // Modal.js import React, { useState, useEffect } from "react"; -import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress } from "@mui/material"; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import API from "../../api" +import { + Box, + Modal, + Typography, + Stack, + TextField, + Button, + Select, + MenuItem, + Grid2, + InputLabel, + FormControl, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import { useFormik } from "formik"; +import * as Yup from "yup"; +import API from "../../api"; import useEPGsStore from "../../store/epgs"; const EPG = ({ epg = null, isOpen, onClose }) => { - const epgs = useEPGsStore(state => state.epgs) - const [file, setFile] = useState(null) + const epgs = useEPGsStore((state) => state.epgs); + const [file, setFile] = useState(null); const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { - setFile(file) + setFile(file); } }; const formik = useFormik({ initialValues: { - name: '', - source_type: '', - url: '', - api_key: '', + name: "", + source_type: "", + url: "", + api_key: "", is_active: true, }, validationSchema: Yup.object({ - name: Yup.string().required('Name is required'), - source_type: Yup.string().required('Source type is required'), + name: Yup.string().required("Name is required"), + source_type: Yup.string().required("Source type is required"), }), onSubmit: async (values, { setSubmitting, resetForm }) => { if (epg?.id) { - await API.updateEPG({id: epg.id, ...values, epg_file: file}) + await API.updateEPG({ id: epg.id, ...values, epg_file: file }); } else { await API.addEPG({ ...values, epg_file: file, - }) + }); } resetForm(); - setFile(null) + setFile(null); setSubmitting(false); - onClose() - } - }) + onClose(); + }, + }); useEffect(() => { if (epg) { @@ -61,20 +78,21 @@ const EPG = ({ epg = null, isOpen, onClose }) => { }, [epg]); if (!isOpen) { - return <> + return <>; } return ( - - - - EPG Source - - -
+ + + EPG Source + + + { variant="standard" /> -< TextField + { value={formik.values.source_type} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.source_type && Boolean(formik.errors.source_type)} - helperText={formik.touched.source_type && formik.errors.source_type} + error={ + formik.touched.source_type && Boolean(formik.errors.source_type) + } + helperText={ + formik.touched.source_type && formik.errors.source_type + } variant="standard" > @@ -136,35 +158,23 @@ const EPG = ({ epg = null, isOpen, onClose }) => { + - - {/* Submit button */} - - - -
-
+ + {/* Submit button */} + + + + ); }; -const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - boxShadow: 24, - p: 4, -}; - export default EPG; diff --git a/frontend/src/components/forms/LoginForm.js b/frontend/src/components/forms/LoginForm.js index e1c54101..2272d6f4 100644 --- a/frontend/src/components/forms/LoginForm.js +++ b/frontend/src/components/forms/LoginForm.js @@ -1,18 +1,25 @@ import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; // For redirection -import useAuthStore from '../../store/auth' -import { Box, TextField, Button, Typography, Grid2, Paper } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import useAuthStore from '../../store/auth'; +import { + Box, + TextField, + Button, + Typography, + Grid2, + Paper, +} from '@mui/material'; const LoginForm = () => { - const { login, isAuthenticated } = useAuthStore(); // Get login function from AuthContext + const { login, isAuthenticated, initData } = useAuthStore(); // Get login function from AuthContext const navigate = useNavigate(); // Hook to navigate to other routes const [formData, setFormData] = useState({ username: '', password: '' }); useEffect(() => { if (isAuthenticated) { - navigate('/channels') + navigate('/channels'); } - }, [isAuthenticated, navigate]) + }, [isAuthenticated, navigate]); const handleInputChange = (e) => { setFormData({ @@ -23,7 +30,8 @@ const LoginForm = () => { const handleSubmit = async (e) => { e.preventDefault(); - await login(formData) + await login(formData); + initData(); navigate('/channels'); // Or any other route you'd like }; @@ -52,7 +60,12 @@ const LoginForm = () => { Login
- + { - const userAgents = useUserAgentsStore(state => state.userAgents) - const [file, setFile] = useState(null) + const userAgents = useUserAgentsStore((state) => state.userAgents); + const [file, setFile] = useState(null); const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { - setFile(file) + setFile(file); } }; const formik = useFormik({ initialValues: { - name: '', - server_url: '', + name: "", + server_url: "", max_streams: 0, - user_agent: '', + user_agent: "", is_active: true, }, validationSchema: Yup.object({ - name: Yup.string().required('Name is required'), - user_agent: Yup.string().required('User-Agent is required'), + name: Yup.string().required("Name is required"), + user_agent: Yup.string().required("User-Agent is required"), }), onSubmit: async (values, { setSubmitting, resetForm }) => { if (playlist?.id) { - await API.updatePlaylist({id: playlist.id, ...values, uploaded_file: file}) + await API.updatePlaylist({ + id: playlist.id, + ...values, + uploaded_file: file, + }); } else { await API.addPlaylist({ ...values, uploaded_file: file, - }) + }); } resetForm(); - setFile(null) + setFile(null); setSubmitting(false); - onClose() - } - }) + onClose(); + }, + }); useEffect(() => { if (playlist) { @@ -62,20 +82,22 @@ const M3U = ({ playlist = null, isOpen, onClose }) => { }, [playlist]); if (!isOpen) { - return <> + return <>; } return ( - - - - M3U Account - + + + M3U Account + - + + { value={formik.values.server_url} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.server_url && Boolean(formik.errors.server_url)} + error={ + formik.touched.server_url && Boolean(formik.errors.server_url) + } helperText={formik.touched.server_url && formik.errors.server_url} variant="standard" /> - {/* File upload input */} - + File - - handleFileChange(event)} - style={{ display: 'none' }} - /> - + handleFileChange(event)} + style={{ display: "none" }} + /> + + { value={formik.values.max_streams} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.max_streams && Boolean(formik.errors.max_streams)} + error={ + formik.touched.max_streams && Boolean(formik.errors.max_streams) + } helperText={formik.touched.max_streams && formik.errors.max_streams} variant="standard" /> @@ -146,7 +178,9 @@ const M3U = ({ playlist = null, isOpen, onClose }) => { value={formik.values.user_agent} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.user_agent && Boolean(formik.errors.user_agent)} + error={ + formik.touched.user_agent && Boolean(formik.errors.user_agent) + } helperText={formik.touched.user_agent && formik.errors.user_agent} variant="standard" > @@ -163,39 +197,31 @@ const M3U = ({ playlist = null, isOpen, onClose }) => { formik.setFieldValue('is_active', e.target.checked)} + onChange={(e) => + formik.setFieldValue("is_active", e.target.checked) + } /> - } label="Is Active" + } + label="Is Active" /> + - - {/* Submit button */} - - - - - + + {/* Submit button */} + + + + ); }; -const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - boxShadow: 24, - p: 4, -}; - export default M3U; diff --git a/frontend/src/components/forms/NewTable.js b/frontend/src/components/forms/NewTable.js deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/components/forms/Stream.js b/frontend/src/components/forms/Stream.js index 32219a6a..f167a416 100644 --- a/frontend/src/components/forms/Stream.js +++ b/frontend/src/components/forms/Stream.js @@ -1,22 +1,26 @@ // Modal.js -import React, { useState, useEffect, useMemo } from "react"; -import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, IconButton } from "@mui/material"; +import React, { useEffect } from 'react'; +import { + TextField, + Button, + Select, + MenuItem, + Grid2, + InputLabel, + FormControl, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from '@mui/material'; import { useFormik } from 'formik'; import * as Yup from 'yup'; -import useChannelsStore from "../../store/channels"; -import API from "../../api" -import useStreamProfilesStore from "../../store/streamProfiles"; -import { - Add as AddIcon, - Delete as DeleteIcon, - } from "@mui/icons-material"; -import useStreamsStore from "../../store/streams"; -import usePlaylistsStore from "../../store/playlists"; -import { MaterialReactTable, useMaterialReactTable } from "material-react-table"; +import API from '../../api'; +import useStreamProfilesStore from '../../store/streamProfiles'; const Stream = ({ stream = null, isOpen, onClose }) => { const streamProfiles = useStreamProfilesStore((state) => state.profiles); - console.log(stream) const formik = useFormik({ initialValues: { @@ -31,16 +35,16 @@ const Stream = ({ stream = null, isOpen, onClose }) => { }), onSubmit: async (values, { setSubmitting, resetForm }) => { if (stream?.id) { - await API.updateStream({id: stream.id, ...values}) + await API.updateStream({ id: stream.id, ...values }); } else { - await API.addStream(values) + await API.addStream(values); } resetForm(); setSubmitting(false); - onClose() - } - }) + onClose(); + }, + }); useEffect(() => { if (stream) { @@ -55,20 +59,22 @@ const Stream = ({ stream = null, isOpen, onClose }) => { }, [stream]); if (!isOpen) { - return <> + return <>; } return ( - - - - Stream - + + + Stream + -
+ + { /> - Stream Profile + + Stream Profile + + - - {/* Submit button */} - - -
-
-
+ + + + + ); }; -const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - boxShadow: 24, - p: 4, -}; - export default StreamProfile; diff --git a/frontend/src/components/forms/UserAgent.js b/frontend/src/components/forms/UserAgent.js index dee370bb..6f5f80c4 100644 --- a/frontend/src/components/forms/UserAgent.js +++ b/frontend/src/components/forms/UserAgent.js @@ -1,34 +1,43 @@ // Modal.js -import React, { useState, useEffect } from "react"; -import { Box, Modal, Typography, Stack, TextField, Button, Select, MenuItem, Grid2, InputLabel, FormControl, CircularProgress, Checkbox } from "@mui/material"; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import API from "../../api" +import React, { useEffect } from "react"; +import { + TextField, + Button, + CircularProgress, + Checkbox, + Dialog, + DialogTitle, + DialogContent, + DialogActions, +} from "@mui/material"; +import { useFormik } from "formik"; +import * as Yup from "yup"; +import API from "../../api"; const UserAgent = ({ userAgent = null, isOpen, onClose }) => { const formik = useFormik({ initialValues: { - user_agent_name: '', - user_agent: '', - description: '', + user_agent_name: "", + user_agent: "", + description: "", is_active: true, }, validationSchema: Yup.object({ - user_agent_name: Yup.string().required('Name is required'), - user_agent: Yup.string().required('User-Agent is required'), + user_agent_name: Yup.string().required("Name is required"), + user_agent: Yup.string().required("User-Agent is required"), }), onSubmit: async (values, { setSubmitting, resetForm }) => { if (userAgent?.id) { - await API.updateUserAgent({id: userAgent.id, ...values}) + await API.updateUserAgent({ id: userAgent.id, ...values }); } else { - await API.addUserAgent(values) + await API.addUserAgent(values); } resetForm(); setSubmitting(false); - onClose() - } - }) + onClose(); + }, + }); useEffect(() => { if (userAgent) { @@ -44,20 +53,22 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { }, [userAgent]); if (!isOpen) { - return <> + return <>; } return ( - - - - User-Agent - + + + User-Agent + -
+ + { value={formik.values.user_agent_name} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.user_agent_name && Boolean(formik.errors.user_agent_name)} - helperText={formik.touched.user_agent_name && formik.errors.user_agent_name} + error={ + formik.touched.user_agent_name && + Boolean(formik.errors.user_agent_name) + } + helperText={ + formik.touched.user_agent_name && formik.errors.user_agent_name + } variant="standard" /> @@ -79,7 +95,9 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { value={formik.values.user_agent} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.user_agent && Boolean(formik.errors.user_agent)} + error={ + formik.touched.user_agent && Boolean(formik.errors.user_agent) + } helperText={formik.touched.user_agent && formik.errors.user_agent} variant="standard" /> @@ -92,7 +110,9 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { value={formik.values.description} onChange={formik.handleChange} onBlur={formik.handleBlur} - error={formik.touched.description && Boolean(formik.errors.description)} + error={ + formik.touched.description && Boolean(formik.errors.description) + } helperText={formik.touched.description && formik.errors.description} variant="standard" /> @@ -102,34 +122,22 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => { checked={formik.values.is_active} onChange={formik.handleChange} /> - - {/* Submit button */} - - - -
-
+ + + + + + + ); }; -const style = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 400, - bgcolor: 'background.paper', - boxShadow: 24, - p: 4, -}; - export default UserAgent; diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js index 080a2296..c2030296 100644 --- a/frontend/src/components/tables/ChannelsTable.js +++ b/frontend/src/components/tables/ChannelsTable.js @@ -1,36 +1,46 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { MaterialReactTable, - MRT_ShowHideColumnsButton, - MRT_ToggleFullScreenButton, useMaterialReactTable, } from 'material-react-table'; -import { Box, Grid2, Stack, Typography, Tooltip, IconButton, Button, ButtonGroup, Snackbar, Popover, TextField } from '@mui/material'; +import { + Box, + Grid2, + Stack, + Typography, + Tooltip, + IconButton, + Button, + ButtonGroup, + Snackbar, + Popover, + TextField, +} from '@mui/material'; import useChannelsStore from '../../store/channels'; import { Delete as DeleteIcon, Edit as EditIcon, Add as AddIcon, SwapVert as SwapVertIcon, -} from '@mui/icons-material' -import API from '../../api' -import ChannelForm from '../forms/Channel' -import { TableHelper } from '../../helpers' +} from '@mui/icons-material'; +import API from '../../api'; +import ChannelForm from '../forms/Channel'; +import { TableHelper } from '../../helpers'; import utils from '../../utils'; import { ContentCopy } from '@mui/icons-material'; const Example = () => { - const [channel, setChannel] = useState(null) + const [channel, setChannel] = useState(null); const [channelModelOpen, setChannelModalOpen] = useState(false); - const [rowSelection, setRowSelection] = useState([]) + const [rowSelection, setRowSelection] = useState([]); const [anchorEl, setAnchorEl] = useState(null); const [textToCopy, setTextToCopy] = useState(''); - const [snackbarMessage, setSnackbarMessage] = useState("") - const [snackbarOpen, setSnackbarOpen] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState(''); + const [snackbarOpen, setSnackbarOpen] = useState(false); - const channels = useChannelsStore((state) => state.channels); + const { channels, isLoading: channelsLoading } = useChannelsStore(); const columns = useMemo( //column definitions... @@ -46,7 +56,7 @@ const Example = () => { }, { header: 'Group', - accessorFn: row => row.channel_group?.name || '', + accessorFn: (row) => row.channel_group?.name || '', }, { header: 'Logo', @@ -57,11 +67,11 @@ const Example = () => { container direction="row" sx={{ - justifyContent: "center", - alignItems: "center", + justifyContent: 'center', + alignItems: 'center', }} > - +
), meta: { @@ -69,7 +79,7 @@ const Example = () => { }, }, ], - [], + [] ); //optionally access the underlying virtualizer instance @@ -79,32 +89,39 @@ const Example = () => { const [sorting, setSorting] = useState([]); const closeSnackbar = () => { - setSnackbarOpen(false) - } + setSnackbarOpen(false); + }; const editChannel = async (channel = null) => { - setChannel(channel) - setChannelModalOpen(true) - } + setChannel(channel); + setChannelModalOpen(true); + }; const deleteChannel = async (id) => { - await API.deleteChannel(id) - } + await API.deleteChannel(id); + }; // @TODO: the bulk delete endpoint is currently broken const deleteChannels = async () => { - setIsLoading(true) - const selected = table.getRowModel().rows.filter(row => row.getIsSelected()) - await utils.Limiter(4, selected.map(chan => () => { - return deleteChannel(chan.original.id) - })) - setIsLoading(false) - } + setIsLoading(true); + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); + await utils.Limiter( + 4, + selected.map((chan) => () => { + return deleteChannel(chan.original.id); + }) + ); + setIsLoading(false); + }; const assignChannels = async () => { - const selected = table.getRowModel().rows.filter(row => row.getIsSelected()) - await API.assignChannelNumbers(selected.map(sel => sel.id)) - } + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); + await API.assignChannelNumbers(selected.map((sel) => sel.id)); + }; useEffect(() => { if (typeof window !== 'undefined') { @@ -134,25 +151,25 @@ const Example = () => { setSnackbarMessage('Failed to copy'); } - setSnackbarOpen(true) + setSnackbarOpen(true); }; const open = Boolean(anchorEl); const copyM3UUrl = async (event) => { setAnchorEl(event.currentTarget); - setTextToCopy('m3u url') - } + setTextToCopy('m3u url'); + }; const copyEPGUrl = async (event) => { setAnchorEl(event.currentTarget); - setTextToCopy('epg url') - } + setTextToCopy('epg url'); + }; const copyHDHRUrl = async (event) => { setAnchorEl(event.currentTarget); - setTextToCopy('hdhr url') - } + setTextToCopy('hdhr url'); + }; const table = useMaterialReactTable({ ...TableHelper.defaultProperties, @@ -166,7 +183,7 @@ const Example = () => { onRowSelectionChange: setRowSelection, onSortingChange: setSorting, state: { - isLoading, + isLoading: isLoading || channelsLoading, sorting, rowSelection, }, @@ -182,7 +199,7 @@ const Example = () => { size="small" // Makes the button smaller color="warning" // Red color for delete actions onClick={() => { - editChannel(row.original) + editChannel(row.original); }} > {/* Small icon size */} @@ -198,17 +215,20 @@ const Example = () => { ), muiTableContainerProps: { sx: { - height: 'calc(100vh - 100px)', // Subtract padding to avoid cutoff + height: 'calc(100vh - 90px)', // Subtract padding to avoid cutoff overflowY: 'auto', // Internal scrolling for the table }, }, muiSearchTextFieldProps: { - variant: "standard", + variant: 'standard', }, renderTopToolbarCustomActions: ({ table }) => ( - + Channels { - - - - + + + + ), @@ -282,7 +301,6 @@ const Example = () => {
{ { const [epg, setEPG] = useState(null); const [epgModalOpen, setEPGModalOpen] = useState(false); - const [rowSelection, setRowSelection] = useState([]) - const [snackbarMessage, setSnackbarMessage] = useState("") - const [snackbarOpen, setSnackbarOpen] = useState(false) + const [rowSelection, setRowSelection] = useState([]); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarOpen, setSnackbarOpen] = useState(false); - const epgs = useEPGsStore(state => state.epgs) + const epgs = useEPGsStore((state) => state.epgs); const columns = useMemo( //column definitions... () => [ { - header: 'Name', + header: "Name", size: 10, - accessorKey: 'name', + accessorKey: "name", }, { - header: 'Source Type', - accessorKey: 'source_type', + header: "Source Type", + accessorKey: "source_type", size: 50, }, { - header: 'URL / API Key', - accessorKey: 'max_streams', + header: "URL / API Key", + accessorKey: "max_streams", }, ], [], @@ -56,26 +68,26 @@ const EPGsTable = () => { const [sorting, setSorting] = useState([]); const closeSnackbar = () => { - setSnackbarOpen(false) - } + setSnackbarOpen(false); + }; const editEPG = async (epg = null) => { - setEPG(epg) - setEPGModalOpen(true) - } + setEPG(epg); + setEPGModalOpen(true); + }; const deleteEPG = async (id) => { - await API.deleteEPG(id) - } + await API.deleteEPG(id); + }; const refreshEPG = async (id) => { - await API.refreshEPG(id) - setSnackbarMessage("EPG refresh initiated") - setSnackbarOpen(true) - } + await API.refreshEPG(id); + setSnackbarMessage("EPG refresh initiated"); + setSnackbarOpen(true); + }; useEffect(() => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { setIsLoading(false); } }, []); @@ -90,13 +102,10 @@ const EPGsTable = () => { }, [sorting]); const table = useMaterialReactTable({ + ...TableHelper.defaultProperties, columns, data: epgs, - enableBottomToolbar: false, - // enableGlobalFilterModes: true, - columnFilterDisplayMode: 'popover', enablePagination: false, - // enableRowNumbers: true, enableRowVirtualization: true, enableRowSelection: true, onRowSelectionChange: setRowSelection, @@ -109,7 +118,7 @@ const EPGsTable = () => { rowVirtualizerInstanceRef, //optional rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { - density: 'compact', + density: "compact", }, enableRowActions: true, renderRowActions: ({ row }) => ( @@ -137,19 +146,18 @@ const EPGsTable = () => { ), - positionActionsColumn: 'last', muiTableContainerProps: { sx: { height: "calc(42vh - 0px)", }, }, - renderTopToolbar: ({ table }) => ( - + renderTopToolbarCustomActions: ({ table }) => ( + EPGs { {/* Small icon size */} - - {/* */} - + ), }); return ( - <> - - - + }} + > + + { /> - + ); }; diff --git a/frontend/src/components/tables/Filter.js b/frontend/src/components/tables/Filter.js deleted file mode 100644 index 128df17b..00000000 --- a/frontend/src/components/tables/Filter.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import { TableCell, TextField } from '@mui/material' - -const Filter = ({ column }) => { - const columnFilterValue = column.getFilterValue() - const { filterVariant } = column.columnDef.meta ?? {} - - if (filterVariant === null) { - return {column.columnDef.header} - } - - return filterVariant === 'range' ? ( -
-
- {/* See faceted column filters example for min max values functionality */} - - column.setFilterValue((old) => [value, old?.[1]]) - } - placeholder={`Min`} - className="w-24 border shadow rounded" - /> - - column.setFilterValue((old) => [old?.[0], value]) - } - placeholder={`Max`} - className="w-24 border shadow rounded" - /> -
-
-
- ) : filterVariant === 'select' ? ( - - ) : ( - column.setFilterValue(value)} - placeholder={column.columnDef.header} - type="text" - value={(columnFilterValue ?? '')} - /> - // See faceted column filters example for datalist search suggestions - ) -} - -// A typical debounced input react component -const DebouncedInput = ({ - value: initialValue, - onChange, - debounce = 500, - ...props -}) => { - const [value, setValue] = React.useState(initialValue) - - React.useEffect(() => { - setValue(initialValue) - }, [initialValue]) - - React.useEffect(() => { - const timeout = setTimeout(() => { - onChange(value) - }, debounce) - - return () => clearTimeout(timeout) - }, [value]) - - return ( - setValue(e.target.value)} /> - ) -} - -export default Filter; diff --git a/frontend/src/components/tables/M3UsTable.js b/frontend/src/components/tables/M3UsTable.js index 84ed41a6..52706f23 100644 --- a/frontend/src/components/tables/M3UsTable.js +++ b/frontend/src/components/tables/M3UsTable.js @@ -1,12 +1,22 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from "react"; import { MaterialReactTable, MRT_ShowHideColumnsButton, MRT_ToggleFullScreenButton, useMaterialReactTable, -} from 'material-react-table'; -import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Checkbox, Select, MenuItem } from '@mui/material'; -import API from '../../api' +} from "material-react-table"; +import { + Box, + Grid2, + Stack, + Typography, + IconButton, + Tooltip, + Checkbox, + Select, + MenuItem, +} from "@mui/material"; +import API from "../../api"; import { Delete as DeleteIcon, Edit as EditIcon, @@ -15,15 +25,16 @@ import { Check as CheckIcon, Close as CloseIcon, Refresh as RefreshIcon, -} from '@mui/icons-material' -import usePlaylistsStore from '../../store/playlists'; -import M3UForm from '../forms/M3U' +} from "@mui/icons-material"; +import usePlaylistsStore from "../../store/playlists"; +import M3UForm from "../forms/M3U"; +import { TableHelper } from "../../helpers"; const Example = () => { const [playlist, setPlaylist] = useState(null); const [playlistModalOpen, setPlaylistModalOpen] = useState(false); - const [rowSelection, setRowSelection] = useState([]) - const [activeFilterValue, setActiveFilterValue] = useState('all'); + const [rowSelection, setRowSelection] = useState([]); + const [activeFilterValue, setActiveFilterValue] = useState("all"); const playlists = usePlaylistsStore((state) => state.playlists); @@ -31,29 +42,33 @@ const Example = () => { //column definitions... () => [ { - header: 'Name', - accessorKey: 'name', + header: "Name", + accessorKey: "name", }, { - header: 'URL / File', - accessorKey: 'server_url', + header: "URL / File", + accessorKey: "server_url", }, { - header: 'Max Streams', - accessorKey: 'max_streams', + header: "Max Streams", + accessorKey: "max_streams", size: 200, }, { - header: 'Active', - accessorKey: 'is_active', + header: "Active", + accessorKey: "is_active", size: 100, - sortingFn: 'basic', + sortingFn: "basic", muiTableBodyCellProps: { - align: 'left', + align: "left", }, Cell: ({ cell }) => ( - - {cell.getValue() ? : } + + {cell.getValue() ? ( + + ) : ( + + )} ), Filter: ({ column }) => ( @@ -77,7 +92,7 @@ const Example = () => { ), filterFn: (row, _columnId, activeFilterValue) => { if (!activeFilterValue) return true; // Show all if no filter - return String(row.getValue('is_active')) === activeFilterValue; + return String(row.getValue("is_active")) === activeFilterValue; }, }, ], @@ -91,25 +106,27 @@ const Example = () => { const [sorting, setSorting] = useState([]); const editPlaylist = async (playlist = null) => { - setPlaylist(playlist) - setPlaylistModalOpen(true) - } + setPlaylist(playlist); + setPlaylistModalOpen(true); + }; const refreshPlaylist = async (id) => { - await API.refreshPlaylist(id) - } + await API.refreshPlaylist(id); + }; const deletePlaylist = async (id) => { - await API.deletePlaylist(id) - } + await API.deletePlaylist(id); + }; const deletePlaylists = async (ids) => { - const selected = table.getRowModel().rows.filter(row => row.getIsSelected()) + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); // await API.deleteStreams(selected.map(stream => stream.original.id)) - } + }; useEffect(() => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { setIsLoading(false); } }, []); @@ -124,13 +141,10 @@ const Example = () => { }, [sorting]); const table = useMaterialReactTable({ + ...TableHelper.defaultProperties, columns, data: playlists, - enableBottomToolbar: false, - // enableGlobalFilterModes: true, - columnFilterDisplayMode: 'popover', enablePagination: false, - // enableRowNumbers: true, enableRowVirtualization: true, enableRowSelection: true, onRowSelectionChange: setRowSelection, @@ -143,7 +157,7 @@ const Example = () => { rowVirtualizerInstanceRef, //optional rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { - density: 'compact', + density: "compact", }, enableRowActions: true, renderRowActions: ({ row }) => ( @@ -152,7 +166,7 @@ const Example = () => { size="small" // Makes the button smaller color="warning" // Red color for delete actions onClick={() => { - editPlaylist(row.original) + editPlaylist(row.original); }} > {/* Small icon size */} @@ -174,19 +188,18 @@ const Example = () => { ), - positionActionsColumn: 'last', muiTableContainerProps: { sx: { height: "calc(42vh - 0px)", }, }, - renderTopToolbar: ({ table }) => ( - + renderTopToolbarCustomActions: ({ table }) => ( + M3U Accounts { {/* Small icon size */} - - {/* */} - + ), }); return ( - <> - - - + }} + > + setPlaylistModalOpen(false)} /> - + ); }; diff --git a/frontend/src/components/tables/StreamProfilesTable.js b/frontend/src/components/tables/StreamProfilesTable.js index 33aeb933..0bdf971d 100644 --- a/frontend/src/components/tables/StreamProfilesTable.js +++ b/frontend/src/components/tables/StreamProfilesTable.js @@ -1,12 +1,22 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from "react"; import { MaterialReactTable, MRT_ShowHideColumnsButton, MRT_ToggleFullScreenButton, useMaterialReactTable, -} from 'material-react-table'; -import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Checkbox, Select, MenuItem } from '@mui/material'; -import API from '../../api' +} from "material-react-table"; +import { + Box, + Grid2, + Stack, + Typography, + IconButton, + Tooltip, + Checkbox, + Select, + MenuItem, +} from "@mui/material"; +import API from "../../api"; import { Delete as DeleteIcon, Edit as EditIcon, @@ -15,47 +25,52 @@ import { Check as CheckIcon, Close as CloseIcon, Refresh as RefreshIcon, -} from '@mui/icons-material' -import useEPGsStore from '../../store/epgs'; -import StreamProfileForm from '../forms/StreamProfile' -import useStreamProfilesStore from '../../store/streamProfiles'; +} from "@mui/icons-material"; +import useEPGsStore from "../../store/epgs"; +import StreamProfileForm from "../forms/StreamProfile"; +import useStreamProfilesStore from "../../store/streamProfiles"; +import { TableHelper } from "../../helpers"; const StreamProfiles = () => { const [profile, setProfile] = useState(null); const [profileModalOpen, setProfileModalOpen] = useState(false); - const [rowSelection, setRowSelection] = useState([]) - const [snackbarMessage, setSnackbarMessage] = useState("") - const [snackbarOpen, setSnackbarOpen] = useState(false) - const [activeFilterValue, setActiveFilterValue] = useState('all'); + const [rowSelection, setRowSelection] = useState([]); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [activeFilterValue, setActiveFilterValue] = useState("all"); - const streamProfiles = useStreamProfilesStore(state => state.profiles) + const streamProfiles = useStreamProfilesStore((state) => state.profiles); const columns = useMemo( //column definitions... () => [ { - header: 'Name', - accessorKey: 'profile_name', + header: "Name", + accessorKey: "profile_name", }, { - header: 'Command', - accessorKey: 'command', + header: "Command", + accessorKey: "command", }, { - header: 'Parameters', - accessorKey: 'parameters', + header: "Parameters", + accessorKey: "parameters", }, { - header: 'Active', - accessorKey: 'is_active', + header: "Active", + accessorKey: "is_active", size: 100, - sortingFn: 'basic', + sortingFn: "basic", muiTableBodyCellProps: { - align: 'left', + align: "left", }, Cell: ({ cell }) => ( - - {cell.getValue() ? : } + + {cell.getValue() ? ( + + ) : ( + + )} ), Filter: ({ column }) => ( @@ -79,7 +94,7 @@ const StreamProfiles = () => { ), filterFn: (row, _columnId, filterValue) => { if (filterValue == "all") return true; // Show all if no filter - return String(row.getValue('is_active')) === filterValue; + return String(row.getValue("is_active")) === filterValue; }, }, ], @@ -93,16 +108,16 @@ const StreamProfiles = () => { const [sorting, setSorting] = useState([]); const editStreamProfile = async (profile = null) => { - setProfile(profile) - setProfileModalOpen(true) - } + setProfile(profile); + setProfileModalOpen(true); + }; const deleteStreamProfile = async (ids) => { - await API.deleteStreamProfile(ids) - } + await API.deleteStreamProfile(ids); + }; useEffect(() => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { setIsLoading(false); } }, []); @@ -117,13 +132,10 @@ const StreamProfiles = () => { }, [sorting]); const table = useMaterialReactTable({ + ...TableHelper.defaultProperties, columns, data: streamProfiles, - enableBottomToolbar: false, - // enableGlobalFilterModes: true, - columnFilterDisplayMode: 'popover', enablePagination: false, - // enableRowNumbers: true, enableRowVirtualization: true, enableRowSelection: true, onRowSelectionChange: setRowSelection, @@ -136,7 +148,7 @@ const StreamProfiles = () => { rowVirtualizerInstanceRef, //optional rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { - density: 'compact', + density: "compact", }, enableRowActions: true, renderRowActions: ({ row }) => ( @@ -157,19 +169,19 @@ const StreamProfiles = () => { ), - positionActionsColumn: 'last', muiTableContainerProps: { sx: { - // height: "calc(42vh - 0px)", + height: "calc(100vh - 100px)", // Subtract padding to avoid cutoff + overflowY: "auto", // Internal scrolling for the table }, }, - renderTopToolbar: ({ table }) => ( - + renderTopToolbarCustomActions: ({ table }) => ( + Stream Profiles { {/* Small icon size */} - - {/* */} - + ), }); return ( - <> - - - + }} + > + setProfileModalOpen(false)} /> - + ); }; diff --git a/frontend/src/components/tables/StreamsTable.js b/frontend/src/components/tables/StreamsTable.js index 6a54f6fa..b62898fd 100644 --- a/frontend/src/components/tables/StreamsTable.js +++ b/frontend/src/components/tables/StreamsTable.js @@ -3,23 +3,32 @@ import { MaterialReactTable, useMaterialReactTable, } from 'material-react-table'; -import { Box, Grid2, Stack, Typography, IconButton, Tooltip, Button } from '@mui/material'; -import useStreamsStore from '../../store/streams' -import API from '../../api' +import { + Box, + Stack, + Typography, + IconButton, + Tooltip, + Button, +} from '@mui/material'; +import useStreamsStore from '../../store/streams'; +import API from '../../api'; import { Delete as DeleteIcon, Edit as EditIcon, Add as AddIcon, -} from '@mui/icons-material' -import { TableHelper } from '../../helpers' +} from '@mui/icons-material'; +import { TableHelper } from '../../helpers'; import utils from '../../utils'; -import StreamForm from '../forms/Stream' +import StreamForm from '../forms/Stream'; +import usePlaylistsStore from '../../store/playlists'; const Example = () => { - const [rowSelection, setRowSelection] = useState([]) - const [stream, setStream] = useState(null) + const [rowSelection, setRowSelection] = useState([]); + const [stream, setStream] = useState(null); const [modalOpen, setModalOpen] = useState(false); - const streams = useStreamsStore((state) => state.streams); + const { streams, isLoading: streamsLoading } = useStreamsStore(); + const { playlists } = usePlaylistsStore(); const columns = useMemo( //column definitions... @@ -32,8 +41,14 @@ const Example = () => { header: 'Group', accessorKey: 'group_name', }, + { + header: 'M3U', + size: 100, + accessorFn: (row) => + playlists.find((playlist) => playlist.id === row.m3u_account)?.name, + }, ], - [], + [playlists] ); //optionally access the underlying virtualizer instance @@ -47,32 +62,39 @@ const Example = () => { channel_name: stream.name, channel_number: 0, stream_id: stream.id, - }) - } + }); + }; // @TODO: bulk create is broken, returning a 404 const createChannelsFromStreams = async () => { - setIsLoading(true) - const selected = table.getRowModel().rows.filter(row => row.getIsSelected()) - await utils.Limiter(4, selected.map(stream => () => { - return createChannelFromStream(stream.original) - })) - setIsLoading(false) - } + setIsLoading(true); + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); + await utils.Limiter( + 4, + selected.map((stream) => () => { + return createChannelFromStream(stream.original); + }) + ); + setIsLoading(false); + }; const editStream = async (stream = null) => { - setStream(stream) - setModalOpen(true) - } + setStream(stream); + setModalOpen(true); + }; const deleteStream = async (id) => { - await API.deleteStream(id) - } + await API.deleteStream(id); + }; const deleteStreams = async () => { - const selected = table.getRowModel().rows.filter(row => row.getIsSelected()) - await API.deleteStreams(selected.map(stream => stream.original.id)) - } + const selected = table + .getRowModel() + .rows.filter((row) => row.getIsSelected()); + await API.deleteStreams(selected.map((stream) => stream.original.id)); + }; useEffect(() => { if (typeof window !== 'undefined') { @@ -101,7 +123,7 @@ const Example = () => { onRowSelectionChange: setRowSelection, onSortingChange: setSorting, state: { - isLoading, + isLoading: isLoading || streamsLoading, sorting, rowSelection, }, @@ -110,13 +132,20 @@ const Example = () => { enableRowActions: true, renderRowActions: ({ row }) => ( <> - editStream(row.original)} + - {/* Small icon size */} - + editStream(row.original)} + disabled={row.original.m3u_account} + > + {/* Small icon size */} + + { ), muiTableContainerProps: { sx: { - height: 'calc(100vh - 100px)', // Subtract padding to avoid cutoff + height: 'calc(100vh - 90px)', // Subtract padding to avoid cutoff overflowY: 'auto', // Internal scrolling for the table }, }, renderTopToolbarCustomActions: ({ table }) => ( - + Streams { sx={{ marginLeft: 1, }} - >Create Channels + > + Create Channels + ), }); return ( - + { const [userAgent, setUserAgent] = useState(null); const [userAgentModalOpen, setUserAgentModalOpen] = useState(false); - const [rowSelection, setRowSelection] = useState([]) - const [activeFilterValue, setActiveFilterValue] = useState('all'); + const [rowSelection, setRowSelection] = useState([]); + const [activeFilterValue, setActiveFilterValue] = useState("all"); - const userAgents = useUserAgentsStore(state => state.userAgents) + const userAgents = useUserAgentsStore((state) => state.userAgents); const columns = useMemo( //column definitions... () => [ { - header: 'Name', + header: "Name", size: 10, - accessorKey: 'user_agent_name', + accessorKey: "user_agent_name", }, { - header: 'User-Agent', - accessorKey: 'user_agent', + header: "User-Agent", + accessorKey: "user_agent", size: 50, }, { - header: 'Desecription', - accessorKey: 'description', + header: "Desecription", + accessorKey: "description", }, { - header: 'Active', - accessorKey: 'is_active', + header: "Active", + accessorKey: "is_active", size: 100, - sortingFn: 'basic', + sortingFn: "basic", muiTableBodyCellProps: { - align: 'left', + align: "left", }, Cell: ({ cell }) => ( - - {cell.getValue() ? : } + + {cell.getValue() ? ( + + ) : ( + + )} ), Filter: ({ column }) => ( @@ -76,7 +90,7 @@ const UserAgentsTable = () => { ), filterFn: (row, _columnId, activeFilterValue) => { if (activeFilterValue == "all") return true; // Show all if no filter - return String(row.getValue('is_active')) === activeFilterValue; + return String(row.getValue("is_active")) === activeFilterValue; }, }, ], @@ -90,20 +104,20 @@ const UserAgentsTable = () => { const [sorting, setSorting] = useState([]); const editUserAgent = async (userAgent = null) => { - setUserAgent(userAgent) - setUserAgentModalOpen(true) - } + setUserAgent(userAgent); + setUserAgentModalOpen(true); + }; const deleteUserAgent = async (ids) => { if (Array.isArray(ids)) { - await API.deleteUserAgents(ids) + await API.deleteUserAgents(ids); } else { - await API.deleteUserAgent(ids) + await API.deleteUserAgent(ids); } - } + }; useEffect(() => { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { setIsLoading(false); } }, []); @@ -118,13 +132,10 @@ const UserAgentsTable = () => { }, [sorting]); const table = useMaterialReactTable({ + ...TableHelper.defaultProperties, columns, data: userAgents, - enableBottomToolbar: false, - // enableGlobalFilterModes: true, - columnFilterDisplayMode: 'popover', enablePagination: false, - // enableRowNumbers: true, enableRowVirtualization: true, enableRowSelection: true, onRowSelectionChange: setRowSelection, @@ -137,7 +148,7 @@ const UserAgentsTable = () => { rowVirtualizerInstanceRef, //optional rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer initialState: { - density: 'compact', + density: "compact", }, enableRowActions: true, renderRowActions: ({ row }) => ( @@ -146,7 +157,7 @@ const UserAgentsTable = () => { size="small" // Makes the button smaller color="warning" // Red color for delete actions onClick={() => { - editUserAgent(row.original) + editUserAgent(row.original); }} > {/* Small icon size */} @@ -160,19 +171,18 @@ const UserAgentsTable = () => { ), - positionActionsColumn: 'last', muiTableContainerProps: { sx: { - height: "calc(42vh - 0px)", + height: "calc(42vh - 10px)", }, }, - renderTopToolbar: ({ table }) => ( - + renderTopToolbarCustomActions: ({ table }) => ( + User-Agents { {/* Small icon size */} - - {/* */} - + ), }); return ( <> - + diff --git a/frontend/src/pages/Channels.js b/frontend/src/pages/Channels.js index 9ee5d438..99a1652e 100644 --- a/frontend/src/pages/Channels.js +++ b/frontend/src/pages/Channels.js @@ -1,7 +1,7 @@ -import React from 'react'; -import ChannelsTable from '../components/tables/ChannelsTable'; -import StreamsTable from '../components/tables/StreamsTable'; -import { Grid2, Box } from '@mui/material'; +import React from "react"; +import ChannelsTable from "../components/tables/ChannelsTable"; +import StreamsTable from "../components/tables/StreamsTable"; +import { Grid2, Box } from "@mui/material"; const ChannelsPage = () => { return ( @@ -9,13 +9,13 @@ const ChannelsPage = () => { @@ -24,20 +24,20 @@ const ChannelsPage = () => { - ) + ); }; export default ChannelsPage; diff --git a/frontend/src/pages/Dashboard.js b/frontend/src/pages/Dashboard.js index c3c0fb61..af2104e1 100644 --- a/frontend/src/pages/Dashboard.js +++ b/frontend/src/pages/Dashboard.js @@ -1,8 +1,8 @@ // src/components/Dashboard.js -import React, { useState } from 'react'; +import React, { useState } from "react"; const Dashboard = () => { - const [newStream, setNewStream] = useState(''); + const [newStream, setNewStream] = useState(""); return (
diff --git a/frontend/src/pages/EPG.js b/frontend/src/pages/EPG.js index 0442c711..24909f05 100644 --- a/frontend/src/pages/EPG.js +++ b/frontend/src/pages/EPG.js @@ -1,27 +1,27 @@ -import React, { useState } from 'react'; -import { Box, Snackbar } from '@mui/material'; -import UserAgentsTable from '../components/tables/UserAgentsTable'; -import EPGsTable from '../components/tables/EPGsTable'; +import React from "react"; +import { Box } from "@mui/material"; +import UserAgentsTable from "../components/tables/UserAgentsTable"; +import EPGsTable from "../components/tables/EPGsTable"; const EPGPage = () => { return ( - + - + - ) + ); }; export default EPGPage; diff --git a/frontend/src/pages/Guide.js b/frontend/src/pages/Guide.js index fdfc51b9..ce292fe8 100644 --- a/frontend/src/pages/Guide.js +++ b/frontend/src/pages/Guide.js @@ -1,63 +1,63 @@ -import React from "react"; +import React from 'react'; import { useEpg, Epg, Layout } from 'planby'; -import API from '../api' +import API from '../api'; function App() { - const [channels, setChannels] = React.useState([]) - const [epg, setEpg] = React.useState([]) + const [channels, setChannels] = React.useState([]); + const [epg, setEpg] = React.useState([]); const fetchChannels = async () => { - const channels = await API.getChannels() - const retval = [] + const channels = await API.getChannels(); + const retval = []; for (const channel of channels) { - if (!channel.channel_name.includes("Nickelod")) { - continue + if (!channel.tvg_id) { + continue; } - console.log(channel) + console.log(channel); retval.push({ - uuid: "Nickelodeon (East).us", - type: "channel", + uuid: channel.tvg_id, + type: 'channel', title: channel.channel_name, - country: "USA", - provider: channel.channel_group?.name || "Default", - logo: channel.logo_url || "/images/logo.png", + country: 'USA', + provider: channel.channel_group?.name || 'Default', + logo: channel.logo_url || '/images/logo.png', year: 2025, - }) + }); } - setChannels(retval) + setChannels(retval); return retval; - } + }; const fetchEpg = async () => { const programs = await API.getGrid(); - const retval = [] - console.log(programs) + const retval = []; + console.log(programs); for (const program of programs.data) { retval.push({ id: program.id, - channelUuid: "Nickelodeon (East).us", + channelUuid: 'Nickelodeon (East).us', description: program.description, title: program.title, since: program.start_time, till: program.end_time, - }) + }); } - setEpg(retval) + setEpg(retval); return retval; - } + }; const fetchData = async () => { - const channels = await fetchChannels() - const epg = await fetchEpg() + const channels = await fetchChannels(); + const epg = await fetchEpg(); - setChannels(channels) - setEpg(epg) - } + setChannels(channels); + setEpg(epg); + }; if (channels.length === 0) { - fetchData() + fetchData(); } const formatDate = (date) => date.toISOString().split('T')[0] + 'T00:00:00'; @@ -74,19 +74,17 @@ function App() { } = useEpg({ epg, channels, - startDate: '2025/02/24', // or 2022-02-02T00:00:00 + startDate: '2025-02-25T11:00:00', // or 2022-02-02T00:00:00 width: '100%', height: 600, }); return (
- - - -
+ + + +
); } diff --git a/frontend/src/pages/Guide/components/ChannelItem.js b/frontend/src/pages/Guide/components/ChannelItem.js deleted file mode 100644 index fcb67a26..00000000 --- a/frontend/src/pages/Guide/components/ChannelItem.js +++ /dev/null @@ -1,16 +0,0 @@ -import { ChannelBox, ChannelLogo } from "planby"; - -export const ChannelItem = ({ channel }) => { - const { position, logo } = channel; - return ( - - {/* Overwrite styles by add eg. style={{ maxHeight: 52, maxWidth: 52,... }} */} - {/* Or stay with default styles */} - - - ); -}; diff --git a/frontend/src/pages/Guide/components/ProgramItem.js b/frontend/src/pages/Guide/components/ProgramItem.js deleted file mode 100644 index 80b4d5c6..00000000 --- a/frontend/src/pages/Guide/components/ProgramItem.js +++ /dev/null @@ -1,45 +0,0 @@ -import { - ProgramBox, - ProgramContent, - ProgramFlex, - ProgramStack, - ProgramTitle, - ProgramText, - ProgramImage, - useProgram -} from "planby"; - -export const ProgramItem = ({ program, ...rest }) => { - const { - styles, - formatTime, - set12HoursTimeFormat, - isLive, - isMinWidth - } = useProgram({ - program, - ...rest - }); - - const { data } = program; - const { image, title, since, till } = data; - - const sinceTime = formatTime(since, set12HoursTimeFormat()).toLowerCase(); - const tillTime = formatTime(till, set12HoursTimeFormat()).toLowerCase(); - - return ( - - - - {isLive && isMinWidth && } - - {title} - - {sinceTime} - {tillTime} - - - - - - ); -}; diff --git a/frontend/src/pages/Guide/components/Timeline.js b/frontend/src/pages/Guide/components/Timeline.js deleted file mode 100644 index 3fbcb24b..00000000 --- a/frontend/src/pages/Guide/components/Timeline.js +++ /dev/null @@ -1,47 +0,0 @@ -import { - TimelineWrapper, - TimelineBox, - TimelineTime, - TimelineDivider, - TimelineDividers, - useTimeline -} from "planby"; - -export function Timeline({ - isBaseTimeFormat, - isSidebar, - dayWidth, - hourWidth, - numberOfHoursInDay, - offsetStartHoursRange, - sidebarWidth -}) { - const { time, dividers, formatTime } = useTimeline( - numberOfHoursInDay, - isBaseTimeFormat - ); - - const renderTime = (index) => ( - - - {formatTime(index + offsetStartHoursRange).toLowerCase()} - - {renderDividers()} - - ); - - const renderDividers = () => - dividers.map((_, index) => ( - - )); - - return ( - - {time.map((_, index) => renderTime(index))} - - ); -} diff --git a/frontend/src/pages/Guide/components/index.js b/frontend/src/pages/Guide/components/index.js deleted file mode 100644 index 961300fc..00000000 --- a/frontend/src/pages/Guide/components/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ChannelItem"; -export * from "./ProgramItem"; -export * from "./Timeline"; diff --git a/frontend/src/pages/Guide/theme.js b/frontend/src/pages/Guide/theme.js deleted file mode 100644 index 1e004563..00000000 --- a/frontend/src/pages/Guide/theme.js +++ /dev/null @@ -1,43 +0,0 @@ -export const theme = { - primary: { - 600: "#1a202c", - 900: "#171923" - }, - grey: { 300: "#d1d1d1" }, - white: "#fff", - green: { - 300: "#2c7a7b" - }, - scrollbar: { - border: "#ffffff", - thumb: { - bg: "#e1e1e1" - } - }, - loader: { - teal: "#5DDADB", - purple: "#3437A2", - pink: "#F78EB6", - bg: "#171923db" - }, - gradient: { - blue: { - 300: "#002eb3", - 600: "#002360", - 900: "#051937" - } - }, - - text: { - grey: { - 300: "#a0aec0", - 500: "#718096" - } - }, - - timeline: { - divider: { - bg: "#718096" - } - } -}; diff --git a/frontend/src/pages/Guide/useApp.js b/frontend/src/pages/Guide/useApp.js deleted file mode 100644 index cba2fbad..00000000 --- a/frontend/src/pages/Guide/useApp.js +++ /dev/null @@ -1,143 +0,0 @@ -import React from "react"; - -import { useEpg } from "planby"; - -// Import theme -import { theme } from "./theme"; -import API from "../../api"; -import { Description } from "@mui/icons-material"; - -const sampleChannel = { - uuid: "16fdfefe-e466-4090-bc1a-57c43937f826", - type: "channel", - title: "r tv", - country: "USA", - provider: 7427, - logo: - "https://raw.githubusercontent.com/karolkozer/planby-demo-resources/master/resources/channel-logos/png/r-channel.png", - year: 2002 -} - -const fetchChannels = async () => { - const channels = await API.getChannels() - const retval = [] - for (const channel of channels) { - if (!channel.channel_name.includes("Nickelod")) { - continue - } - console.log(channel) - retval.push({ - ...sampleChannel, - uuid: "Nickelodeon (East).us", - type: "channel", - title: channel.channel_name, - country: "USA", - provider: channel.channel_group?.name || "Default", - logo: channel.logo_url || "/images/logo.png", - year: 2025, - }) - } - - return retval; -} - -const sample = { - "id": "6f3caa7f-5b11-4edb-998e-80d4baa03373", - "description": "Bounty hunter Boba Fett & mercenary Fennec Shand navigate the underworld when they return to Tatooine to claim Jabba the Hutt's old turf.", - "title": "The Book of Boba Fett", - "isYesterday": true, - "since": "2022-10-17T23:50:00", - "till": "2022-10-18T00:55:00", - "channelUuid": "16fdfefe-e466-4090-bc1a-57c43937f826", - "image": "https://www.themoviedb.org/t/p/w1066_and_h600_bestv2/sjx6zjQI2dLGtEL0HGWsnq6UyLU.jpg", - "country": "Ghana", - "Year": "2021–", - "Rated": "TV-14", - "Released": "29 Dec 2021", - "Runtime": "N/A", - "Genre": "Action, Adventure, Sci-Fi", - "Director": "N/A", - "Writer": "Jon Favreau", - "Actors": "Temuera Morrison, Ming-Na Wen, Matt Berry", - "Language": "English", - "Country": "United States", - "Awards": "N/A", - "Metascore": "N/A", - "imdbRating": "8.0", - "imdbVotes": "20,147", - "imdbID": "tt13668894", - "Type": "series", - "totalSeasons": "1", - "Response": "True", - "Ratings": [ - { - "Source": "Internet Movie Database", - "Value": "8.0/10" - } - ], - "rating": 3 -} - -const fetchEpg = async () => { - const programs = await API.getGrid(); - const retval = [] - console.log(programs) - for (const program of programs.data) { - retval.push({ - ...sample, - id: program.id, - channelUuid: "Nickelodeon (East).us", - description: program.description, - title: program.title, - since: program.start_time.replace('Z', ''), - till: program.end_time.replace('Z', ''), - }) - } - - return retval; -} - -export function useApp() { - const [channels, setChannels] = React.useState([]); - const [epg, setEpg] = React.useState([]); - const [isLoading, setIsLoading] = React.useState(false); - - const channelsData = React.useMemo(() => channels, [channels]); - const epgData = React.useMemo(() => epg, [epg]); - - const formatDate = (date) => date.toISOString().split('T')[0] + 'T00:00:00'; - - const today = new Date(); - const tomorrow = new Date(today); - tomorrow.setDate(today.getDate() + 1); - - const { getEpgProps, getLayoutProps } = useEpg({ - channels: channelsData, - epg: epgData, - dayWidth: 7200, - sidebarWidth: 100, - itemHeight: 80, - isSidebar: true, - isTimeline: true, - isLine: true, - startDate: today, - endDate: tomorrow, - isBaseTimeFormat: true, - theme - }); - - const handleFetchResources = React.useCallback(async () => { - setIsLoading(true); - const epg = await fetchEpg(); - const channels = await fetchChannels(); - setEpg(epg); - setChannels(channels); - setIsLoading(false); - }, []); - - React.useEffect(() => { - handleFetchResources(); - }, [handleFetchResources]); - - return { getEpgProps, getLayoutProps, isLoading }; -} diff --git a/frontend/src/pages/Home.js b/frontend/src/pages/Home.js index e9751d8d..ac971332 100644 --- a/frontend/src/pages/Home.js +++ b/frontend/src/pages/Home.js @@ -1,8 +1,8 @@ // src/components/Home.js -import React, { useState } from 'react'; +import React, { useState } from "react"; const Home = () => { - const [newChannel, setNewChannel] = useState(''); + const [newChannel, setNewChannel] = useState(""); return (
diff --git a/frontend/src/pages/Login.js b/frontend/src/pages/Login.js index 4e69545b..5ed4f203 100644 --- a/frontend/src/pages/Login.js +++ b/frontend/src/pages/Login.js @@ -1,8 +1,8 @@ -import React from 'react'; -import LoginForm from '../components/forms/LoginForm'; +import React from "react"; +import LoginForm from "../components/forms/LoginForm"; const Login = () => { - return + return ; }; export default Login; diff --git a/frontend/src/pages/M3U.js b/frontend/src/pages/M3U.js index ea4bde55..9fd7e2c2 100644 --- a/frontend/src/pages/M3U.js +++ b/frontend/src/pages/M3U.js @@ -1,17 +1,16 @@ -import React, { useState } from 'react'; -import useUserAgentsStore from '../store/userAgents'; -import { Box } from '@mui/material'; -import M3UsTable from '../components/tables/M3UsTable'; -import UserAgentsTable from '../components/tables/UserAgentsTable'; -import usePlaylistsStore from '../store/playlists'; -import API from '../api' -import M3UForm from '../components/forms/M3U' +import React, { useState } from "react"; +import useUserAgentsStore from "../store/userAgents"; +import { Box } from "@mui/material"; +import M3UsTable from "../components/tables/M3UsTable"; +import UserAgentsTable from "../components/tables/UserAgentsTable"; +import usePlaylistsStore from "../store/playlists"; +import API from "../api"; +import M3UForm from "../components/forms/M3U"; const M3UPage = () => { const isLoading = useUserAgentsStore((state) => state.isLoading); const error = useUserAgentsStore((state) => state.error); - const playlists = usePlaylistsStore(state => state.playlists) - + const playlists = usePlaylistsStore((state) => state.playlists); const [playlist, setPlaylist] = useState(null); const [playlistModalOpen, setPlaylistModalOpen] = useState(false); @@ -20,26 +19,26 @@ const M3UPage = () => { const [userAgentModalOpen, setUserAgentModalOpen] = useState(false); const editUserAgent = async (userAgent = null) => { - setUserAgent(userAgent) - setUserAgentModalOpen(true) - } + setUserAgent(userAgent); + setUserAgentModalOpen(true); + }; const editPlaylist = async (playlist = null) => { - setPlaylist(playlist) - setPlaylistModalOpen(true) - } + setPlaylist(playlist); + setPlaylistModalOpen(true); + }; const deleteUserAgent = async (ids) => { if (Array.isArray(ids)) { - await API.deleteUserAgents(ids) + await API.deleteUserAgents(ids); } else { - await API.deleteUserAgent(ids) + await API.deleteUserAgent(ids); } - } + }; const deletePlaylist = async (id) => { - await API.deletePlaylist(id) - } + await API.deletePlaylist(id); + }; if (isLoading) return
Loading...
; if (error) return
Error: {error}
; @@ -47,17 +46,17 @@ const M3UPage = () => { return ( - + - + @@ -66,10 +65,8 @@ const M3UPage = () => { isOpen={playlistModalOpen} onClose={() => setPlaylistModalOpen(false)} /> - - - ) + ); }; export default M3UPage; diff --git a/frontend/src/pages/StreamProfiles.js b/frontend/src/pages/StreamProfiles.js index 2305f5c6..615bace2 100644 --- a/frontend/src/pages/StreamProfiles.js +++ b/frontend/src/pages/StreamProfiles.js @@ -1,10 +1,8 @@ -import React, { useState } from 'react'; -import StreamProfilesTable from '../components/tables/StreamProfilesTable'; +import React from "react"; +import StreamProfilesTable from "../components/tables/StreamProfilesTable"; const StreamProfilesPage = () => { - return ( - - ) + return ; }; export default StreamProfilesPage; diff --git a/frontend/src/store/auth.js b/frontend/src/store/auth.js index 6295a7b5..43421cca 100644 --- a/frontend/src/store/auth.js +++ b/frontend/src/store/auth.js @@ -1,7 +1,11 @@ -// src/auth/authStore.js - -import {create} from 'zustand'; -import API from '../api' +import { create } from 'zustand'; +import API from '../api'; +import useChannelsStore from './channels'; +import useStreamsStore from './streams'; +import useUserAgentsStore from './userAgents'; +import usePlaylistsStore from './playlists'; +import useEPGsStore from './epgs'; +import useStreamProfilesStore from './streamProfiles'; const decodeToken = (token) => { if (!token) return null; @@ -21,30 +25,45 @@ const useAuthStore = create((set, get) => ({ tokenExpiration: localStorage.getItem('tokenExpiration') || null, isAuthenticated: false, - getToken: async () => { - const expiration = localStorage.getItem('tokenExpiration') - const tokenExpiration = localStorage.getItem('tokenExpiration'); - let accessToken = null; - if (isTokenExpired(tokenExpiration)) { - accessToken = await get().refreshToken(); - } else { - accessToken = localStorage.getItem('accessToken'); - } + setIsAuthenticated: (isAuthenticated) => set({ isAuthenticated }), - return accessToken; + initData: async () => { + console.log('fetching data'); + await Promise.all([ + useChannelsStore.getState().fetchChannels(), + useChannelsStore.getState().fetchChannelGroups(), + useStreamsStore.getState().fetchStreams(), + useUserAgentsStore.getState().fetchUserAgents(), + usePlaylistsStore.getState().fetchPlaylists(), + useEPGsStore.getState().fetchEPGs(), + useStreamProfilesStore.getState().fetchProfiles(), + ]); + }, + + getToken: async () => { + const expiration = localStorage.getItem('tokenExpiration'); + const tokenExpiration = localStorage.getItem('tokenExpiration'); + let accessToken = null; + if (isTokenExpired(tokenExpiration)) { + accessToken = await get().refreshToken(); + } else { + accessToken = localStorage.getItem('accessToken'); + } + + return accessToken; }, // Action to login - login: async ({username, password}) => { + login: async ({ username, password }) => { try { - const response = await API.login(username, password) + const response = await API.login(username, password); if (response.access) { - const expiration = decodeToken(response.access) + const expiration = decodeToken(response.access); set({ accessToken: response.access, refreshToken: response.refresh, tokenExpiration: expiration, // 1 hour from now - isAuthenticated: true + isAuthenticated: true, }); // Store in localStorage localStorage.setItem('accessToken', response.access); @@ -62,7 +81,7 @@ const useAuthStore = create((set, get) => ({ if (!refreshToken) return; try { - const data = await API.refreshToken(refreshToken) + const data = await API.refreshToken(refreshToken); if (data.access) { set({ accessToken: data.access, @@ -76,7 +95,7 @@ const useAuthStore = create((set, get) => ({ } } catch (error) { console.error('Token refresh failed:', error); - get().logout() + get().logout(); } return false; @@ -88,7 +107,7 @@ const useAuthStore = create((set, get) => ({ accessToken: null, refreshToken: null, tokenExpiration: null, - isAuthenticated: false + isAuthenticated: false, }); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); @@ -99,10 +118,13 @@ const useAuthStore = create((set, get) => ({ const refreshToken = localStorage.getItem('refreshToken') || null; if (refreshToken) { - await get().refreshToken() - } else { - await get().logout() + const loggedIn = await get().refreshToken(); + if (loggedIn) { + return true; + } } + + return false; }, })); diff --git a/frontend/src/store/channels.js b/frontend/src/store/channels.js index 24a5aa8a..10338c7d 100644 --- a/frontend/src/store/channels.js +++ b/frontend/src/store/channels.js @@ -1,7 +1,5 @@ -// src/stores/channelsStore.js - -import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import { create } from "zustand"; +import api from "../api"; const useChannelsStore = create((set) => ({ channels: [], @@ -15,8 +13,8 @@ const useChannelsStore = create((set) => ({ const channels = await api.getChannels(); set({ channels: channels, isLoading: false }); } catch (error) { - console.error('Failed to fetch channels:', error); - set({ error: 'Failed to load channels.', isLoading: false }); + console.error("Failed to fetch channels:", error); + set({ error: "Failed to load channels.", isLoading: false }); } }, @@ -26,22 +24,41 @@ const useChannelsStore = create((set) => ({ const channelGroups = await api.getChannelGroups(); set({ channelGroups: channelGroups, isLoading: false }); } catch (error) { - console.error('Failed to fetch channel groups:', error); - set({ error: 'Failed to load channel groups.', isLoading: false }); + console.error("Failed to fetch channel groups:", error); + set({ error: "Failed to load channel groups.", isLoading: false }); } }, - addChannel: (newChannel) => set((state) => ({ - channels: [...state.channels, newChannel], - })), + addChannel: (newChannel) => + set((state) => ({ + channels: [...state.channels, newChannel], + })), - updateChannel: (userAgent) => set((state) => ({ - channels: state.channels.map(chan => chan.id === userAgent.id ? userAgent : chan), - })), + updateChannel: (userAgent) => + set((state) => ({ + channels: state.channels.map((chan) => + chan.id === userAgent.id ? userAgent : chan, + ), + })), - removeChannels: (channelIds) => set((state) => ({ - channels: state.channels.filter((channel) => !channelIds.includes(channel.id)), - })), + removeChannels: (channelIds) => + set((state) => ({ + channels: state.channels.filter( + (channel) => !channelIds.includes(channel.id), + ), + })), + + addChannelGroup: (newChannelGroup) => + set((state) => ({ + channelGroups: [...state.channelGroups, newChannelGroup], + })), + + updateChannelGroup: (channelGroup) => + set((state) => ({ + channelGroups: state.channelGroups.map((group) => + group.id === channelGroup.id ? channelGroup : group, + ), + })), })); export default useChannelsStore; diff --git a/frontend/src/store/epgs.js b/frontend/src/store/epgs.js index c4c52d64..14cfd623 100644 --- a/frontend/src/store/epgs.js +++ b/frontend/src/store/epgs.js @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import { create } from "zustand"; +import api from "../api"; const useEPGsStore = create((set) => ({ epgs: [], @@ -12,18 +12,20 @@ const useEPGsStore = create((set) => ({ const epgs = await api.getEPGs(); set({ epgs: epgs, isLoading: false }); } catch (error) { - console.error('Failed to fetch epgs:', error); - set({ error: 'Failed to load epgs.', isLoading: false }); + console.error("Failed to fetch epgs:", error); + set({ error: "Failed to load epgs.", isLoading: false }); } }, - addEPG: (newPlaylist) => set((state) => ({ - epgs: [...state.epgs, newPlaylist], - })), + addEPG: (newPlaylist) => + set((state) => ({ + epgs: [...state.epgs, newPlaylist], + })), - removeEPGs: (epgIds) => set((state) => ({ - epgs: state.epgs.filter((epg) => !epgIds.includes(epg.id)), - })), + removeEPGs: (epgIds) => + set((state) => ({ + epgs: state.epgs.filter((epg) => !epgIds.includes(epg.id)), + })), })); export default useEPGsStore; diff --git a/frontend/src/store/playlists.js b/frontend/src/store/playlists.js index ae5fde77..06930b73 100644 --- a/frontend/src/store/playlists.js +++ b/frontend/src/store/playlists.js @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import { create } from "zustand"; +import api from "../api"; const usePlaylistsStore = create((set) => ({ playlists: [], @@ -12,22 +12,29 @@ const usePlaylistsStore = create((set) => ({ const playlists = await api.getPlaylists(); set({ playlists: playlists, isLoading: false }); } catch (error) { - console.error('Failed to fetch playlists:', error); - set({ error: 'Failed to load playlists.', isLoading: false }); + console.error("Failed to fetch playlists:", error); + set({ error: "Failed to load playlists.", isLoading: false }); } }, - addPlaylist: (newPlaylist) => set((state) => ({ - playlists: [...state.playlists, newPlaylist], - })), + addPlaylist: (newPlaylist) => + set((state) => ({ + playlists: [...state.playlists, newPlaylist], + })), - updatePlaylist: (playlist) => set((state) => ({ - playlists: state.playlists.map(pl => pl.id === playlist.id ? playlist : pl), - })), + updatePlaylist: (playlist) => + set((state) => ({ + playlists: state.playlists.map((pl) => + pl.id === playlist.id ? playlist : pl, + ), + })), - removePlaylists: (playlistIds) => set((state) => ({ - playlists: state.playlists.filter((playlist) => !playlistIds.includes(playlist.id)), - })), + removePlaylists: (playlistIds) => + set((state) => ({ + playlists: state.playlists.filter( + (playlist) => !playlistIds.includes(playlist.id), + ), + })), })); export default usePlaylistsStore; diff --git a/frontend/src/store/streamProfiles.js b/frontend/src/store/streamProfiles.js index 94576f26..1b98c662 100644 --- a/frontend/src/store/streamProfiles.js +++ b/frontend/src/store/streamProfiles.js @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import api from '../api'; const useStreamProfilesStore = create((set) => ({ profiles: [], @@ -17,17 +17,24 @@ const useStreamProfilesStore = create((set) => ({ } }, - addStreamProfile: (profile) => set((state) => ({ - profiles: [...state.profiles, profile], - })), + addStreamProfile: (profile) => + set((state) => ({ + profiles: [...state.profiles, profile], + })), - updateStreamProfile: (profile) => set((state) => ({ - profiles: state.profiles.map(prof => prof.id === profile.id ? profile : prof), - })), + updateStreamProfile: (profile) => + set((state) => ({ + profiles: state.profiles.map((prof) => + prof.id === profile.id ? profile : prof + ), + })), - removeStreamProfiles: (propfileIds) => set((state) => ({ - profiles: state.profiles.filter((profile) => !propfileIds.includes(profile.id)), - })), + removeStreamProfiles: (propfileIds) => + set((state) => ({ + profiles: state.profiles.filter( + (profile) => !propfileIds.includes(profile.id) + ), + })), })); export default useStreamProfilesStore; diff --git a/frontend/src/store/streams.js b/frontend/src/store/streams.js index c330ef20..215a17c8 100644 --- a/frontend/src/store/streams.js +++ b/frontend/src/store/streams.js @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import { create } from "zustand"; +import api from "../api"; const useStreamsStore = create((set) => ({ streams: [], @@ -12,22 +12,25 @@ const useStreamsStore = create((set) => ({ const streams = await api.getStreams(); set({ streams: streams, isLoading: false }); } catch (error) { - console.error('Failed to fetch streams:', error); - set({ error: 'Failed to load streams.', isLoading: false }); + console.error("Failed to fetch streams:", error); + set({ error: "Failed to load streams.", isLoading: false }); } }, - addStream: (stream) => set((state) => ({ - streams: [...state.streams, stream], - })), + addStream: (stream) => + set((state) => ({ + streams: [...state.streams, stream], + })), - updateStream: (stream) => set((state) => ({ - streams: state.streams.map(st => st.id === stream.id ? stream : st), - })), + updateStream: (stream) => + set((state) => ({ + streams: state.streams.map((st) => (st.id === stream.id ? stream : st)), + })), - removeStreams: (streamIds) => set((state) => ({ - streams: state.streams.filter((stream) => !streamIds.includes(stream.id)), - })), + removeStreams: (streamIds) => + set((state) => ({ + streams: state.streams.filter((stream) => !streamIds.includes(stream.id)), + })), })); export default useStreamsStore; diff --git a/frontend/src/store/userAgents.js b/frontend/src/store/userAgents.js index 8d426b13..6693e830 100644 --- a/frontend/src/store/userAgents.js +++ b/frontend/src/store/userAgents.js @@ -1,5 +1,5 @@ -import { create } from 'zustand'; -import api from '../api'; // Your API helper that manages token & requests +import { create } from "zustand"; +import api from "../api"; const useUserAgentsStore = create((set) => ({ userAgents: [], @@ -12,22 +12,29 @@ const useUserAgentsStore = create((set) => ({ const userAgents = await api.getUserAgents(); set({ userAgents: userAgents, isLoading: false }); } catch (error) { - console.error('Failed to fetch userAgents:', error); - set({ error: 'Failed to load userAgents.', isLoading: false }); + console.error("Failed to fetch userAgents:", error); + set({ error: "Failed to load userAgents.", isLoading: false }); } }, - addUserAgent: (userAgent) => set((state) => ({ - userAgents: [...state.userAgents, userAgent], - })), + addUserAgent: (userAgent) => + set((state) => ({ + userAgents: [...state.userAgents, userAgent], + })), - updateUserAgent: (userAgent) => set((state) => ({ - userAgents: state.userAgents.map(ua => ua.id === userAgent.id ? userAgent : ua), - })), + updateUserAgent: (userAgent) => + set((state) => ({ + userAgents: state.userAgents.map((ua) => + ua.id === userAgent.id ? userAgent : ua, + ), + })), - removeUserAgents: (userAgentIds) => set((state) => ({ - userAgents: state.userAgents.filter((userAgent) => !userAgentIds.includes(userAgent.id)), - })), + removeUserAgents: (userAgentIds) => + set((state) => ({ + userAgents: state.userAgents.filter( + (userAgent) => !userAgentIds.includes(userAgent.id), + ), + })), })); export default useUserAgentsStore;