diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8f388917..cadc68e8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,31 +13,19 @@
"@fontsource/roboto": "^5.1.1",
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
- "@reduxjs/toolkit": "^2.5.1",
- "@tanstack/react-table": "^8.21.2",
- "@tanstack/react-virtual": "^3.13.0",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.2.0",
- "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.9",
"formik": "^2.4.6",
+ "material-react-table": "^3.2.0",
+ "planby": "^1.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"react-scripts": "5.0.1",
- "react-virtualized": "^9.22.6",
"web-vitals": "^2.1.4",
"yup": "^1.6.1",
"zustand": "^5.0.3"
}
},
- "node_modules/@adobe/css-tools": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
- "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="
- },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -3206,6 +3194,104 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
},
+ "node_modules/@mui/x-date-pickers": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.27.0.tgz",
+ "integrity": "sha512-wSx8JGk4WQ2hTObfQITc+zlmUKNleQYoH1hGocaQlpWpo1HhauDtcQfX6sDN0J0dPT2eeyxDWGj4uJmiSfQKcw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0",
+ "@mui/x-internals": "7.26.0",
+ "@types/react-transition-group": "^4.4.11",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-date-pickers/node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz",
+ "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"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",
@@ -3331,38 +3417,6 @@
"url": "https://opencollective.com/popperjs"
}
},
- "node_modules/@reduxjs/toolkit": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz",
- "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==",
- "dependencies": {
- "immer": "^10.0.3",
- "redux": "^5.0.1",
- "redux-thunk": "^3.1.0",
- "reselect": "^5.1.0"
- },
- "peerDependencies": {
- "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
- "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-redux": {
- "optional": true
- }
- }
- },
- "node_modules/@reduxjs/toolkit/node_modules/immer": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
- "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/immer"
- }
- },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3686,12 +3740,13 @@
"url": "https://github.com/sponsors/gregberge"
}
},
- "node_modules/@tanstack/react-table": {
- "version": "8.21.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
- "integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
+ "node_modules/@tanstack/match-sorter-utils": {
+ "version": "8.19.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
+ "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
+ "license": "MIT",
"dependencies": {
- "@tanstack/table-core": "8.21.2"
+ "remove-accents": "0.5.0"
},
"engines": {
"node": ">=12"
@@ -3699,150 +3754,6 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
- }
- },
- "node_modules/@tanstack/react-virtual": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
- "integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
- "dependencies": {
- "@tanstack/virtual-core": "3.13.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/@tanstack/table-core": {
- "version": "8.21.2",
- "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
- "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/virtual-core": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
- "integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@testing-library/dom": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
- "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
- "dependencies": {
- "@babel/code-frame": "^7.10.4",
- "@babel/runtime": "^7.12.5",
- "@types/aria-query": "^5.0.1",
- "aria-query": "5.3.0",
- "chalk": "^4.1.0",
- "dom-accessibility-api": "^0.5.9",
- "lz-string": "^1.5.0",
- "pretty-format": "^27.0.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@testing-library/dom/node_modules/aria-query": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
- "dependencies": {
- "dequal": "^2.0.3"
- }
- },
- "node_modules/@testing-library/jest-dom": {
- "version": "6.6.3",
- "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
- "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
- "dependencies": {
- "@adobe/css-tools": "^4.4.0",
- "aria-query": "^5.0.0",
- "chalk": "^3.0.0",
- "css.escape": "^1.5.1",
- "dom-accessibility-api": "^0.6.3",
- "lodash": "^4.17.21",
- "redent": "^3.0.0"
- },
- "engines": {
- "node": ">=14",
- "npm": ">=6",
- "yarn": ">=1"
- }
- },
- "node_modules/@testing-library/jest-dom/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
- "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="
- },
- "node_modules/@testing-library/react": {
- "version": "16.2.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz",
- "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==",
- "dependencies": {
- "@babel/runtime": "^7.12.5"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@testing-library/dom": "^10.0.0",
- "@types/react": "^18.0.0 || ^19.0.0",
- "@types/react-dom": "^18.0.0 || ^19.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@testing-library/user-event": {
- "version": "13.5.0",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
- "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
- "dependencies": {
- "@babel/runtime": "^7.12.5"
- },
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- },
- "peerDependencies": {
- "@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tootallnate/once": {
@@ -3861,11 +3772,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/@types/aria-query": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="
- },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4194,11 +4100,6 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
- "node_modules/@types/use-sync-external-store": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
- "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
- },
"node_modules/@types/ws": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
@@ -5815,14 +5716,6 @@
"wrap-ansi": "^7.0.0"
}
},
- "node_modules/clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6330,11 +6223,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/css.escape": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
- },
"node_modules/cssdb": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz",
@@ -6562,6 +6450,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -6668,14 +6572,6 @@
"node": ">= 0.8"
}
},
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -6778,11 +6674,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/dom-accessibility-api": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
- },
"node_modules/dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -8830,6 +8721,16 @@
"he": "bin/he"
}
},
+ "node_modules/highlight-words": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-2.0.0.tgz",
+ "integrity": "sha512-If5n+IhSBRXTScE7wl16VPmd+44Vy7kof24EdqhjsZsDuHikpv1OCagVcJFpB4fS4UPUniedlWqrjIO8vWOsIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20",
+ "npm": ">= 9"
+ }
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -9192,14 +9093,6 @@
"node": ">=0.8.19"
}
},
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -11046,14 +10939,6 @@
"yallist": "^3.0.2"
}
},
- "node_modules/lz-string": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
- "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
- "bin": {
- "lz-string": "bin/bin.js"
- }
- },
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@@ -11092,6 +10977,94 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/material-react-table": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-3.2.0.tgz",
+ "integrity": "sha512-q2xWG1CQ30ji+aCLKX1XrIg3zrfHT/zZ4aR49Vk/oP4+ospCgjlIVcWLapjrPoqAIDNgB9/fRRG7d1se1Z3nUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/match-sorter-utils": "8.19.4",
+ "@tanstack/react-table": "8.20.6",
+ "@tanstack/react-virtual": "3.11.2",
+ "highlight-words": "2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kevinvandy"
+ },
+ "peerDependencies": {
+ "@emotion/react": ">=11.13",
+ "@emotion/styled": ">=11.13",
+ "@mui/icons-material": ">=6",
+ "@mui/material": ">=6",
+ "@mui/x-date-pickers": ">=7.15",
+ "react": ">=18.0",
+ "react-dom": ">=18.0"
+ }
+ },
+ "node_modules/material-react-table/node_modules/@tanstack/react-table": {
+ "version": "8.20.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
+ "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/table-core": "8.20.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/material-react-table/node_modules/@tanstack/react-virtual": {
+ "version": "3.11.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
+ "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.11.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/material-react-table/node_modules/@tanstack/table-core": {
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/material-react-table/node_modules/@tanstack/virtual-core": {
+ "version": "3.11.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
+ "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -11203,14 +11176,6 @@
"node": ">=6"
}
},
- "node_modules/min-indent": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
- "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/mini-css-extract-plugin": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz",
@@ -11921,6 +11886,28 @@
"node": ">=4"
}
},
+ "node_modules/planby": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/planby/-/planby-1.1.7.tgz",
+ "integrity": "sha512-Z/hZqMYTQ+uYbuEuvHVkUtzGw0A51mobb/zO4S9gy4TuUjiY848jXpceC8InsbNRicRhIcark1+XlM1HdVoxhg==",
+ "license": "Custom License",
+ "dependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "date-fns": "^2.28.0",
+ "use-debounce": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/planby"
+ },
+ "peerDependencies": {
+ "react": ">=16"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -13550,33 +13537,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
- "node_modules/react-lifecycles-compat": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
- "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
- },
- "node_modules/react-redux": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
- "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
- "dependencies": {
- "@types/use-sync-external-store": "^0.0.6",
- "use-sync-external-store": "^1.4.0"
- },
- "peerDependencies": {
- "@types/react": "^18.2.25 || ^19",
- "react": "^18.0 || ^19",
- "redux": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "redux": {
- "optional": true
- }
- }
- },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -13718,23 +13678,6 @@
"react-dom": ">=16.6.0"
}
},
- "node_modules/react-virtualized": {
- "version": "9.22.6",
- "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
- "integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
- "dependencies": {
- "@babel/runtime": "^7.7.2",
- "clsx": "^1.0.4",
- "dom-helpers": "^5.1.3",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.7.2",
- "react-lifecycles-compat": "^3.0.4"
- },
- "peerDependencies": {
- "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -13778,31 +13721,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/redent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
- "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
- "dependencies": {
- "indent-string": "^4.0.0",
- "strip-indent": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/redux": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
- },
- "node_modules/redux-thunk": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
- "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
- "peerDependencies": {
- "redux": "^5.0.0"
- }
- },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -13928,6 +13846,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
+ "license": "MIT"
+ },
"node_modules/renderkid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -13961,11 +13885,6 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
- "node_modules/reselect": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
- "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
- },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -15156,17 +15075,6 @@
"node": ">=6"
}
},
- "node_modules/strip-indent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
- "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
- "dependencies": {
- "min-indent": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -16103,10 +16011,24 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-debounce": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-7.0.1.tgz",
+ "integrity": "sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "optional": true,
+ "peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
diff --git a/frontend/package.json b/frontend/package.json
index 10097b50..e150a95a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,21 +8,14 @@
"@fontsource/roboto": "^5.1.1",
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
- "@reduxjs/toolkit": "^2.5.1",
- "@tanstack/react-table": "^8.21.2",
- "@tanstack/react-virtual": "^3.13.0",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.2.0",
- "@testing-library/user-event": "^13.5.0",
"axios": "^1.7.9",
"formik": "^2.4.6",
+ "material-react-table": "^3.2.0",
+ "planby": "^1.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"react-scripts": "5.0.1",
- "react-virtualized": "^9.22.6",
"web-vitals": "^2.1.4",
"yup": "^1.6.1",
"zustand": "^5.0.3"
diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png
new file mode 100644
index 00000000..41a99864
Binary files /dev/null and b/frontend/public/android-chrome-192x192.png differ
diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png
new file mode 100644
index 00000000..4a448aed
Binary files /dev/null and b/frontend/public/android-chrome-512x512.png differ
diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png
new file mode 100644
index 00000000..c4d863b1
Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ
diff --git a/frontend/public/favicon-16x16.png b/frontend/public/favicon-16x16.png
new file mode 100644
index 00000000..26bd66ae
Binary files /dev/null and b/frontend/public/favicon-16x16.png differ
diff --git a/frontend/public/favicon-32x32.png b/frontend/public/favicon-32x32.png
new file mode 100644
index 00000000..f8801a86
Binary files /dev/null and b/frontend/public/favicon-32x32.png differ
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index a11777cc..d6beceff 100644
Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ
diff --git a/frontend/public/index.html b/frontend/public/index.html
index ec0e2fa3..cf7eea9b 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -2,7 +2,10 @@
-
+
+
+
+
, route: "/channels" },
- { text: 'M3U', icon: , route: "/m3u" },
- { text: 'EPG', icon: , route: "/epg" },
-];
-
// Protected Route Component
const ProtectedRoute = ({ element, ...rest }) => {
const { isAuthenticated } = useAuthStore();
@@ -64,54 +43,46 @@ const App = () => {
-
- {/* Drawer Toggle Button */}
-
-
-
-
- {open && }
-
-
-
+ transition: 'width 0.3s',
+ overflowX: 'hidden',
+ },
+ }}
+ >
+ {/* Drawer Toggle Button */}
+
+
+
+
+ {open && }
+
+
+
-
+
+
+
+
- {/* Drawer Navigation Items */}
-
- {items.map((item) => (
-
-
- {item.icon}
- {open && }
-
-
- ))}
-
-
{/* Fixed Header */}
-
+ {/*
-
+ */}
{/* Main Content Area between Header and Footer */}
@@ -120,9 +91,11 @@ const App = () => {
}/>} />
}/>} />
}/>} />
+ }/>} />
+ }/>} />
-
+
);
diff --git a/frontend/src/api.js b/frontend/src/api.js
index c74498b7..510b4e36 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -4,6 +4,8 @@ import useChannelsStore from './store/channels';
import useUserAgentsStore from './store/userAgents';
import usePlaylistsStore from './store/playlists';
import useEPGsStore from './store/epgs';
+import useStreamsStore from './store/streams';
+import useStreamProfilesStore from './store/streamProfiles';
const axios = Axios.create({
withCredentials: true,
@@ -116,17 +118,73 @@ export default class API {
useChannelsStore.getState().removeChannels([id])
}
- static async deleteChannels(channel_ids) {
- const response = await fetch(`${host}/api/channels/bulk-delete-channels/`, {
- method: 'DELETE',
+ // @TODO: the bulk delete endpoint is currently broken
+ // static async deleteChannels(channel_ids) {
+ // const response = await fetch(`${host}/api/channels/bulk-delete-channels/0/`, {
+ // method: 'DELETE',
+ // headers: {
+ // Authorization: `Bearer ${await getAuthToken()}`,
+ // 'Content-Type': 'application/json',
+ // },
+ // body: JSON.stringify({ channel_ids }),
+ // });
+
+ // useChannelsStore.getState().removeChannels(channel_ids)
+ // }
+
+ static async updateChannel(values) {
+ const {id, ...payload} = values
+ const response = await fetch(`${host}/api/channels/channels/${id}/`, {
+ method: 'PUT',
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
'Content-Type': 'application/json',
},
- body: JSON.stringify({ channel_ids }),
+ body: JSON.stringify(payload),
});
- useChannelsStore.getState().removeChannels(channel_ids)
+ const retval = await response.json();
+ if (retval.id) {
+ useChannelsStore.getState().updateChannel(retval)
+ }
+
+ return retval;
+ }
+
+ static async assignChannelNumbers(ids) {
+ const response = await fetch(`${host}/api/channels/channels/assign/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ channel_order: ids }),
+ });
+
+ const retval = await response.json();
+ if (retval.id) {
+ useChannelsStore.getState().addChannel(retval)
+ }
+
+ return retval;
+ }
+
+ static async createChannelFromStream(values) {
+ const response = await fetch(`${host}/api/channels/channels/from-stream/`, {
+ 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().addChannel(retval)
+ }
+
+ return retval;
}
static async getStreams() {
@@ -141,6 +199,68 @@ export default class API {
return retval;
}
+ static async addStream(values) {
+ const response = await fetch(`${host}/api/channels/streams/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(values),
+ });
+
+ const retval = await response.json();
+ if (retval.id) {
+ useStreamsStore.getState().addStream(retval)
+ }
+
+ return retval;
+ }
+
+ static async updateStream(values) {
+ const {id, ...payload} = values
+ const response = await fetch(`${host}/api/channels/streams/${id}/`, {
+ method: 'PUT',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const retval = await response.json();
+ if (retval.id) {
+ useStreamsStore.getState().updateStream(retval)
+ }
+
+ return retval;
+ }
+
+ static async deleteStream(id) {
+ const response = await fetch(`${host}/api/channels/streams/${id}/`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ useStreamsStore.getState().removeStreams([id])
+ }
+
+ static async deleteStreams(ids) {
+ const response = await fetch(`${host}/api/channels/streams/bulk-delete/`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ stream_ids: ids })
+ });
+
+ useStreamsStore.getState().removeStreams(ids)
+ }
+
static async getUserAgents() {
const response = await fetch(`${host}/api/core/useragents/`, {
headers: {
@@ -172,7 +292,6 @@ export default class API {
}
static async updateUserAgent(values) {
- console.log(values)
const {id, ...payload} = values
const response = await fetch(`${host}/api/core/useragents/${id}/`, {
method: 'PUT',
@@ -233,6 +352,32 @@ export default class API {
return retval;
}
+ static async refreshPlaylist(id) {
+ const response = await fetch(`${host}/api/m3u/refresh/${id}/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const retval = await response.json();
+ return retval;
+ }
+
+ static async refreshAllPlaylist() {
+ const response = await fetch(`${host}/api/m3u/refresh/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const retval = await response.json();
+ return retval;
+ }
+
static async deletePlaylist(id) {
const response = await fetch(`${host}/api/m3u/accounts/${id}/`, {
method: 'DELETE',
@@ -242,7 +387,7 @@ export default class API {
},
});
- useUserAgentsStore.getState().removePlaylists([id])
+ usePlaylistsStore.getState().removePlaylists([id])
}
static async updatePlaylist(values) {
@@ -258,7 +403,7 @@ export default class API {
const retval = await response.json();
if (retval.id) {
- useUserAgentsStore.getState().updatePlaylist(retval)
+ usePlaylistsStore.getState().updatePlaylist(retval)
}
return retval;
@@ -346,4 +491,77 @@ export default class API {
const retval = await response.json();
return retval;
}
+
+ static async getStreamProfiles() {
+ const response = await fetch(`${host}/api/core/streamprofiles/`, {
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const retval = await response.json();
+ return retval;
+ }
+
+ static async addStreamProfile(values) {
+ const response = await fetch(`${host}/api/core/streamprofiles/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(values)
+ });
+
+ const retval = await response.json();
+ if (retval.id) {
+ useStreamProfilesStore.getState().addStreamProfile(retval)
+ }
+ return retval;
+ }
+
+ static async updateStreamProfile(values) {
+ const {id, ...payload} = values
+ const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, {
+ method: 'PUT',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const retval = await response.json();
+ if (retval.id) {
+ useStreamProfilesStore.getState().updateStreamProfile(retval)
+ }
+
+ return retval;
+ }
+
+ static async deleteStreamProfile(id) {
+ const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ useStreamProfilesStore.getState().removeStreamProfiles([id])
+ }
+
+ static async getGrid() {
+ const response = await fetch(`${host}/api/epg/grid/`, {
+ headers: {
+ Authorization: `Bearer ${await getAuthToken()}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const retval = await response.json()
+ console.log(retval)
+ return retval
+ }
}
diff --git a/frontend/src/components/Sidebar.js b/frontend/src/components/Sidebar.js
index 5043be37..3ecc7bb4 100644
--- a/frontend/src/components/Sidebar.js
+++ b/frontend/src/components/Sidebar.js
@@ -1,53 +1,49 @@
import React from 'react';
-import { Link } from 'react-router-dom';
-import DescriptionIcon from '@mui/icons-material/Description';
+import { Link, useLocation } from 'react-router-dom';
+import {
+ Drawer,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemText,
+ ListItemIcon,
+ Divider,
+} 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';
+
+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" },
+];
+
+const Sidebar = ({ open }) => {
+ const location = useLocation();
-const Sidebar = () => {
return (
- <>
-
-
- >
+
+ {items.map((item) => (
+
+
+ {item.icon}
+ {open && }
+
+
+ ))}
+
);
};
diff --git a/frontend/src/components/StreamTableToolbar.js b/frontend/src/components/StreamTableToolbar.js
deleted file mode 100644
index b4bb2f42..00000000
--- a/frontend/src/components/StreamTableToolbar.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ButtonGroup, Button } from "@mui/material"
-
-const StreamTableToolbar = ({ selectedItems }) => {
- return (
-
- {/* */}
-
- )
-}
-
-export default StreamTableToolbar;
diff --git a/frontend/src/components/StreamsTable.js b/frontend/src/components/StreamsTable.js
deleted file mode 100644
index 62655577..00000000
--- a/frontend/src/components/StreamsTable.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, { useMemo, useState } from 'react'
-import { createRoot } from 'react-dom/client'
-import { useVirtualizer } from '@tanstack/react-virtual'
-import {
- flexRender,
- getCoreRowModel,
- getSortedRowModel,
- useReactTable,
- getFilteredRowModel,
-} from '@tanstack/react-table'
-import useStreamsStore from '../store/streams'
-import ChannelForm from './forms/Channel'
-import API from '../api'
-import { Button, ButtonGroup, Checkbox, Table, TableHead, TableRow, TableCell, TableBody, Box, TextField, IconButton } from '@mui/material'
-import DeleteIcon from '@mui/icons-material/Delete'
-import Filter from './tables/Filter';
-
-// Styles for fixed header and table container
-const styles = {
- fixedHeader: {
- position: "sticky", // Make it sticky
- top: 0, // Stick to the top
- backgroundColor: "#fff", // Ensure it has a background
- zIndex: 10, // Ensure it sits above the table
- padding: "10px",
- borderBottom: "2px solid #ccc",
- },
- tableContainer: {
- maxHeight: "400px", // Limit the height for scrolling
- overflowY: "scroll", // Make it scrollable
- marginTop: "50px", // Adjust margin to avoid overlap with fixed header
- },
- table: {
- width: "100%",
- borderCollapse: "collapse",
- },
-};
-
-const StreamsTable = () => {
- const sterams = useStreamsStore(state => state.streams)
- const [rowSelection, setRowSelection] = useState({})
- const [isModalOpen, setIsModalOpen] = useState(false); // State to control modal visibility
- const [columnFilters, setColumnFilters] = useState([])
-
- // Define columns with useMemo, this is a stable object and doesn't change unless explicitly modified
- const columns = useMemo(() => [
- {
- id: 'select-col',
- header: ({ table }) => (
-
- ),
- size: 10,
- cell: ({ row }) => (
-
- ),
- },
- {
- header: 'Name',
- accessorKey: 'name',
- },
- {
- header: 'Group',
- accessorKey: 'group_name',
- },
- {
- id: 'actions',
- header: 'Actions',
- cell: ({ row }) => {
- console.log(row)
- return (
- deleteStream(row.original.id)}
- >
- {/* Small icon size */}
-
- )
- }
- },
- ], []);
-
- const table = useReactTable({
- data: streams,
- columns,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getSortedRowModel: getSortedRowModel(),
- debugTable: true,
-
- filterFns: {},
-
- onRowSelectionChange: setRowSelection, //hoist up the row selection state to your own scope
- state: {
- rowSelection, //pass the row selection state back to the table instance
- columnFilters,
- },
-
- onColumnFiltersChange: setColumnFilters,
- })
-
- const deleteStream = async (id) => {
- // await API.deleteChannel(id)
- }
-
- const parentRef = React.useRef(null)
-
- const { rows } = table.getRowModel()
-
- const rowVirtualizer = useVirtualizer({
- count: rows.length,
- getScrollElement: () => parentRef.current,
- estimateSize: () => 34,
- overscan: 20,
- })
-
- return (
-
- {/* Fixed header */}
-
-
-
-
- {/* Add more buttons as needed */}
-
-
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => (
-
- {header.column.getCanFilter() ? (
-
-
-
- ) : header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- ))}
-
- ))}
-
-
- {/* Virtualized rows */}
- {rowVirtualizer.getVirtualItems().map((virtualRow, index) => {
- const row = rows[virtualRow.index];
- return (
-
- {row.getVisibleCells().map((cell) => (
- onClickRow?.(cell, row)}
- key={cell.id}
- className="text-xs font-graphik"
- sx={{
- paddingTop: 0,
- paddingBottom: 0,
- }}
- >
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
-
- ))}
-
- );
- })}
-
-
-
-
-
setIsModalOpen(false)}
- />
-
- )
-}
-
-export default StreamsTable;
diff --git a/frontend/src/components/forms/Channel.js b/frontend/src/components/forms/Channel.js
index e4c64394..33e511a0 100644
--- a/frontend/src/components/forms/Channel.js
+++ b/frontend/src/components/forms/Channel.js
@@ -1,16 +1,56 @@
// 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 React, { useState, useEffect, useMemo } from "react";
+import {
+ Box,
+ Modal,
+ Typography,
+ Stack,
+ TextField,
+ Button,
+ Select,
+ MenuItem,
+ Grid2,
+ InputLabel,
+ FormControl,
+ CircularProgress,
+ IconButton,
+} 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";
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 [logo, setLogo] = useState(null)
const [logoPreview, setLogoPreview] = useState(null)
+ const [channelStreams, setChannelStreams] = useState([])
+
+ const addStream = (stream) => {
+ 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 handleLogoChange = (e) => {
const file = e.target.files[0];
if (file) {
@@ -24,6 +64,9 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
channel_name: '',
channel_number: '',
channel_group_id: '',
+ stream_profile_id: '',
+ tvg_id: '',
+ tvg_name: '',
},
validationSchema: Yup.object({
channel_name: Yup.string().required('Name is required'),
@@ -32,11 +75,12 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
}),
onSubmit: async (values, { setSubmitting, resetForm }) => {
if (channel?.id) {
- await API.updateChannel({id: channel.id, ...values, logo_file: logo})
+ 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),
})
}
@@ -53,13 +97,101 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
formik.setValues({
channel_name: channel.channel_name,
channel_number: channel.channel_number,
- channel_group_id: channel.channel_group_id,
+ channel_group_id: channel.channel_group?.id,
+ tvg_id: channel.tvg_id,
+ tvg_name: channel.tvg_name,
});
+
+ setChannelStreams(streams.filter(stream => channel.streams.includes(stream.id)))
} else {
formik.resetForm();
}
}, [channel]);
+ const activeStreamsTable = useMaterialReactTable({
+ data: channelStreams,
+ columns: useMemo(() => [
+ {
+ header: 'Name',
+ accessorKey: 'name',
+ },
+ {
+ header: 'M3U',
+ accessorKey: 'group_name',
+ },
+ ], []),
+ enableBottomToolbar: false,
+ enableTopToolbar: false,
+ columnFilterDisplayMode: 'popover',
+ enablePagination: false,
+ enableRowVirtualization: true,
+ enableRowSelection: true,
+ rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
+ initialState: {
+ density: 'compact',
+ },
+ enableRowActions: true,
+ renderRowActions: ({ row }) => (
+ <>
+ removeStream(row.original)}
+ >
+ {/* Small icon size */}
+
+ >
+ ),
+ positionActionsColumn: 'last',
+ muiTableContainerProps: {
+ sx: {
+ height: '200px',
+ },
+ },
+ })
+
+ const availableStreamsTable = useMaterialReactTable({
+ data: streams,
+ columns: useMemo(() => [
+ {
+ header: 'Name',
+ accessorKey: 'name',
+ },
+ {
+ header: 'M3U',
+ accessorKey: 'group_name',
+ },
+ ], []),
+ enableBottomToolbar: false,
+ enableTopToolbar: false,
+ columnFilterDisplayMode: 'popover',
+ enablePagination: false,
+ enableRowVirtualization: true,
+ enableRowSelection: true,
+ rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
+ initialState: {
+ density: 'compact',
+ },
+ enableRowActions: true,
+ renderRowActions: ({ row }) => (
+ <>
+ addStream(row.original)}
+ >
+ {/* Small icon size */}
+
+ >
+ ),
+ positionActionsColumn: 'last',
+ muiTableContainerProps: {
+ sx: {
+ height: '200px',
+ },
+ },
+ })
+
if (!isOpen) {
return <>>
}
@@ -102,7 +234,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.channel_group_id && Boolean(formik.errors.channel_group_id)}
- helperText={formik.touched.channel_group_id && formik.errors.channel_group_id}
+ // helperText={formik.touched.channel_group_id && formik.errors.channel_group_id}
variant="standard"
>
{channelGroups.map((option, index) => (
@@ -113,6 +245,28 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
+
+ Stream Profile
+
+
+
{
+
+
+
+
{/* File upload input */}
@@ -137,7 +317,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
{logo && (
@@ -174,6 +354,18 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
+
+
+
+ Active Streams
+
+
+
+
+ Available Streams
+
+
+
);
@@ -184,7 +376,7 @@ const style = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
- width: 600,
+ width: "1200px",
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
diff --git a/frontend/src/components/forms/EPG.js b/frontend/src/components/forms/EPG.js
index 7ec9fc7f..3481dd20 100644
--- a/frontend/src/components/forms/EPG.js
+++ b/frontend/src/components/forms/EPG.js
@@ -49,6 +49,11 @@ const EPG = ({ epg = null, isOpen, onClose }) => {
useEffect(() => {
if (epg) {
formik.setValues({
+ name: epg.name,
+ source_type: epg.source_type,
+ url: epg.url,
+ api_key: epg.api_key,
+ is_active: epg.is_active,
});
} else {
formik.resetForm();
diff --git a/frontend/src/components/forms/M3U.js b/frontend/src/components/forms/M3U.js
index 445bafe7..f34bf3fa 100644
--- a/frontend/src/components/forms/M3U.js
+++ b/frontend/src/components/forms/M3U.js
@@ -34,7 +34,7 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
if (playlist?.id) {
await API.updatePlaylist({id: playlist.id, ...values, uploaded_file: file})
} else {
- await API.addChannel({
+ await API.addPlaylist({
...values,
uploaded_file: file,
})
@@ -137,9 +137,9 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
/>
- User-Agent
+ User-Agent