Dashboard with the new UI (#2486)

This commit is contained in:
atyabbin
2024-07-19 12:41:31 +05:30
committed by GitHub
16 changed files with 2943 additions and 984 deletions

View File

@@ -8,12 +8,16 @@
"name": "staff",
"version": "0.0.0",
"dependencies": {
"@mui/icons-material": "^5.16.0",
"date-fns": "^3.6.0",
"react": "^18",
"react-datepicker": "^7.1.0",
"react-dom": "^18",
"react-toastify": "^10.0.5",
"zod": "^3"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^7",
@@ -26,7 +30,7 @@
"prettier": "^3",
"prettier-plugin-organize-imports": "^3.2",
"prettier-plugin-packagejson": "^2.5",
"typescript": "^5.4.5",
"typescript": "^5",
"vite": "^5.2"
}
},
@@ -339,6 +343,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
"integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz",
@@ -388,6 +403,43 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
"peer": true,
"dependencies": {
"@emotion/memoize": "^0.8.1",
"@emotion/sheet": "^1.2.2",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
"peer": true
},
"node_modules/@emotion/sheet": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==",
"peer": true
},
"node_modules/@emotion/utils": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==",
"peer": true
},
"node_modules/@emotion/weak-memoize": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==",
"peer": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -849,6 +901,54 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz",
"integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==",
"dependencies": {
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
"integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.19",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz",
"integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==",
"dependencies": {
"@floating-ui/react-dom": "^2.1.1",
"@floating-ui/utils": "^0.2.4",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
"integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -952,6 +1052,271 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
"integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.0.tgz",
"integrity": "sha512-8SLffXYPRVpcZx5QzxNE8fytTqzp+IuU3deZbQWg/vSaTlDpR5YVrQ4qQtXTi5cRdhOufV5INylmwlKK+//nPw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.0.tgz",
"integrity": "sha512-6ISoOhkp9w5gD0PEW9JklrcbyARDkFWNTBdwXZ1Oy5IGlyu9B0zG0hnUIe4H17IaF1Vgj6C8VI+v4tkSdK0veg==",
"dependencies": {
"@babel/runtime": "^7.23.9"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.0.tgz",
"integrity": "sha512-DbR1NckTLpjt9Zut9EGQ70th86HfN0BYQgyYro6aXQrNfjzSwe3BJS1AyBQ5mJ7TdL6YVRqohfukxj9JlqZZUg==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.40",
"@mui/core-downloads-tracker": "^5.16.0",
"@mui/system": "^5.16.0",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.16.0",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"peer": true
},
"node_modules/@mui/private-theming": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.0.tgz",
"integrity": "sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.16.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
"integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.0.tgz",
"integrity": "sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.16.0",
"@mui/styled-engine": "^5.15.14",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.16.0",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
"integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
"peer": true,
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.0.tgz",
"integrity": "sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"peer": true
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -999,6 +1364,80 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-builtin-module": "^3.2.1",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve/node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
@@ -1257,14 +1696,12 @@
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
},
"node_modules/@types/react": {
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1279,6 +1716,21 @@
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"peer": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
@@ -1778,6 +2230,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -1892,8 +2356,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
@@ -1946,6 +2409,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
@@ -1969,6 +2441,15 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -2048,6 +2529,16 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.788",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz",
@@ -2616,6 +3107,12 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3194,6 +3691,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"dev": true,
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -3308,6 +3820,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-negative-zero": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -3732,7 +4250,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4070,7 +4587,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -4117,6 +4633,22 @@
"node": ">=0.10.0"
}
},
"node_modules/react-datepicker": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz",
"integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==",
"dependencies": {
"@floating-ui/react": "^0.26.2",
"clsx": "^2.1.0",
"date-fns": "^3.3.1",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -4132,8 +4664,20 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-onclickoutside": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz",
"integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
@@ -4156,6 +4700,22 @@
"react-dom": ">=18"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -4177,6 +4737,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
@@ -4604,6 +5169,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"peer": true
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -4644,6 +5215,11 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

View File

@@ -11,9 +11,17 @@
"preview": "vite preview"
},
"dependencies": {
"@date-io/date-fns": "^3.0.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.16.0",
"@mui/lab": "^5.0.0-alpha.171",
"@mui/material": "^5.16.0",
"@mui/x-date-pickers": "^7.9.0",
"@types/react-datepicker": "^6.2.0",
"date-fns": "^3.6.0",
"react": "^18",
"react-datepicker": "^7.1.0",
"react-datepicker": "^7.3.0",
"react-dom": "^18",
"react-toastify": "^10.0.5",
"zod": "^3"

View File

@@ -1,456 +1,347 @@
.fetch-button-container {
margin-right: 180px;
margin-left: 10px;
.container {
position: relative; /* Ensure the parent is relatively positioned */
height: 100vh; /* Full viewport height */
width: 100vw; /* Full viewport width */
display: flex;
justify-content: center;
align-items: center;
}
.content-wrapper {
.center-table {
display: table;
}
.input-form {
display: flex;
align-items: flex-start;
margin-left: 175px;
flex-direction: column;
align-items: center;
}
.horizontal-group {
position: absolute; /* Use absolute positioning */
top: 32px; /* 32px below the top of the page */
left: 32px; /* 32px from the leftmost edge of the page */
right: 32px;
display: flex;
align-items: center; /* Align items vertically centered */
gap: 20px; /* Adjust the gap between elements as needed */
background-color: #fafafa;
height: 104px;
width: 1375px;
border-radius: 10px;
}
.fetch-button-container button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #009879;
background-color: #00b33c;
color: white;
border: none;
border-radius: 5px;
margin-top: 20px;
}
#submitbtn {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #009879;
color: white;
border: none;
border-radius: 5px;
margin-top: 20px;
}
#submitbtn:hover {
background-color: #007c6c;
width: 199px;
height: 56px;
margin-left: 20px;
}
.fetch-button-container button:hover {
background-color: #007c6c;
}
.sidebar {
padding: 20px;
flex: 0 0 auto;
}
.button-container {
.link-text {
display: flex;
gap: 10px;
align-items: center;
padding: 0 16px; /* Add padding for better appearance */
text-decoration: none; /* Remove underline */
color: inherit; /* Inherit color from parent */
font-weight: bold; /* Make the text bold */
font-size: 40px;
}
button {
padding: 10px 20px;
font-size: 16px;
.text-field-token {
margin-left: 70px !important; /* Adjust margin for Token field */
}
.text-field-email {
margin-left: 20px !important; /* Adjust margin for Email field */
}
.duckie-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.center-content {
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* Adjust as necessary */
position: relative;
}
.error-message {
margin-top: 20px;
color: red;
font-size: 1.5em;
}
.message {
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
padding: 10px;
border-radius: 5px;
background-color: #f0f0f0;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
.duckie-container {
display: flex;
justify-content: center;
align-items: center;
}
.container {
position: relative; /* Ensure the parent is relatively positioned */
height: 100vh; /* Full viewport height */
width: 100vw; /* Full viewport width */
display: flex;
flex-direction: column; /* Add this */
justify-content: center; /* Center vertically */
align-items: center; /* Center horizontally */
}
.message.error {
background-color: #f44336;
color: white;
.input-form {
display: flex;
flex-direction: column;
align-items: center;
z-index: 1; /* Ensure form is on top */
}
.message.success {
background-color: #4caf50;
color: white;
.horizontal-group {
display: flex;
align-items: center;
gap: 20px; /* Adjust the gap between elements as needed */
background-color: #fafafa;
padding: 20px; /* Add padding around content */
border-radius: 10px;
}
.dropdown-container {
margin-bottom: 20px;
position: relative; /* Ensure relative positioning for dropdown menu */
}
.more-button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #009879;
.fetch-button-container button {
background-color: #00b33c;
color: white;
border: none;
border-radius: 5px;
margin-top: 1px;
width: 199px;
height: 56px;
}
.more-button:hover {
.fetch-button-container button:hover {
background-color: #007c6c;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 5px); /* Position right below the More button */
left: 0;
z-index: 100;
display: block;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
max-height: 150px; /* Example: Set max height for scrollability */
overflow-y: auto; /* Enable vertical scroll */
}
/* Remove bullets from unordered list */
.dropdown-menu ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.dropdown-menu button {
display: block;
width: 100%;
padding: 8px 16px;
font-size: 14px;
color: #333;
background-color: transparent;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.dropdown-menu button:hover {
background-color: #f0f0f0;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 80%;
max-height: 80%;
overflow: auto;
}
.modal-header {
.link-text {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
padding: 0 16px; /* Add padding for better appearance */
text-decoration: none; /* Remove underline */
color: inherit; /* Inherit color from parent */
font-weight: bold; /* Make the text bold */
font-size: 40px;
}
.modal-header .close-btn {
cursor: pointer;
color: #777;
font-size: 20px;
.text-field-token {
margin-left: 70px !important; /* Adjust margin for Token field */
}
.modal-content {
margin-top: 10px;
.text-field-email {
margin-left: 50px !important; /* Adjust margin for Email field */
}
/* Styles for draggable modal */
.modal.draggable {
cursor: move;
}
.modal.draggable .modal-header {
cursor: move;
}
.popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: lightgreen;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.popup-content {
.center-content {
display: flex;
flex-direction: column;
}
.popup-content div {
margin-bottom: 10px;
}
:root {
--popup-bg-color-light: #fff;
--popup-bg-color-dark: #2c2c2c;
--popup-border-color-light: #ccc;
--popup-border-color-dark: #444;
--popup-text-color-light: #000;
--popup-text-color-dark: #fff;
--popup-shadow-light: rgba(0, 0, 0, 0.1);
--popup-shadow-dark: rgba(255, 255, 255, 0.1);
}
.update-subscription-popup {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
transform: translate(-50%, -50%);
background-color: var(--popup-bg-color-light);
border: 1px solid var(--popup-border-color-light);
padding: 20px;
z-index: 1000;
box-shadow: 0px 0px 10px var(--popup-shadow-light);
}
.popup-content {
display: flex;
flex-direction: column;
}
.close-button {
align-self: flex-end;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--popup-text-color-light);
}
.popup-content h2 {
margin-top: 0;
}
.popup-content form label {
display: block;
margin-bottom: 10px;
}
.popup-content form input,
.popup-content form select {
width: 100%;
padding: 8px;
margin-top: 5px;
background-color: var(--popup-bg-color-light);
color: var(--popup-text-color-light);
border: 1px solid var(--popup-border-color-light);
}
.popup-content form button {
padding: 10px 15px;
margin-top: 10px;
cursor: pointer;
background-color: var(--popup-bg-color-light);
color: var(--popup-text-color-light);
border: 1px solid var(--popup-border-color-light);
}
.custom-select {
position: relative;
width: 100%;
}
.custom-select select {
width: 100%;
padding: 8px;
cursor: pointer;
background-color: var(--popup-bg-color-light);
color: var(--popup-text-color-light);
border: 1px solid var(--popup-border-color-light);
appearance: none;
}
.custom-select::after {
content: "\25BC";
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
left: 50%;
transform: translate(-50%, -50%);
}
.message {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
.duckie-container {
display: flex;
justify-content: center;
align-items: center;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
/* Example CSS to maintain tabs fixed position */
.tabs-container {
position: sticky;
top: 20px; /* Adjust as needed */
z-index: 1000; /* Ensure tabs are above other content */
}
.container {
position: relative; /* Ensure the parent is relatively positioned */
height: 100vh; /* Full viewport height */
width: 100vw; /* Full viewport width */
display: flex;
flex-direction: column; /* Ensure children stack vertically */
justify-content: center; /* Center vertically */
align-items: center; /* Center horizontally */
}
.message.success {
background-color: #d4edda;
color: #155724;
.input-form {
display: flex;
flex-direction: column;
align-items: center;
z-index: 1; /* Ensure form is on top */
margin-bottom: 20px; /* Add space between form and tabs */
}
@media (prefers-color-scheme: dark) {
.update-subscription-popup {
background-color: var(--popup-bg-color-dark);
border-color: var(--popup-border-color-dark);
color: var(--popup-text-color-dark);
box-shadow: 0px 0px 10px var(--popup-shadow-dark);
}
.close-button {
color: var(--popup-text-color-dark);
}
.popup-content form input,
.popup-content form select {
background-color: var(--popup-bg-color-dark);
color: var(--popup-text-color-dark);
border: 1px solid var(--popup-border-color-dark);
}
.popup-content form button {
background-color: var(--popup-bg-color-dark);
color: var(--popup-text-color-dark);
border: 1px solid var(--popup-border-color-dark);
}
}
:root {
--popup-bg-color-light: #fff;
--popup-bg-color-dark: #2c2c2c;
--popup-border-color-light: #ccc;
--popup-border-color-dark: #444;
--popup-text-color-light: #000;
--popup-text-color-dark: #fff;
--popup-shadow-light: rgba(0, 0, 0, 0.1);
--popup-shadow-dark: rgba(255, 255, 255, 0.1);
.horizontal-group {
display: flex;
align-items: center;
gap: 20px; /* Adjust the gap between elements as needed */
background-color: #fafafa;
padding: 20px; /* Add padding around content */
border-radius: 10px;
}
.update-subscription-popup {
position: fixed;
.fetch-button-container button {
background-color: #00b33c;
color: white;
border: none;
width: 199px;
height: 56px;
}
.fetch-button-container button:hover {
background-color: #007c6c;
}
.link-text {
display: flex;
align-items: center;
padding: 0 16px; /* Add padding for better appearance */
text-decoration: none; /* Remove underline */
color: inherit; /* Inherit color from parent */
font-weight: bold; /* Make the text bold */
font-size: 40px;
}
.text-field-token {
margin-left: 70px !important; /* Adjust margin for Token field */
}
.text-field-email {
margin-left: 50px !important; /* Adjust margin for Email field */
}
.center-content {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--popup-bg-color-light);
border: 1px solid var(--popup-border-color-light);
padding: 20px;
z-index: 1000;
box-shadow: 0px 0px 10px var(--popup-shadow-light);
}
.popup-content {
.duckie-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.close-button {
align-self: flex-end;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--popup-text-color-light);
.tabs-container {
position: sticky; /* Make the tabs sticky */
top: 0; /* Stick to the top of the viewport */
z-index: 1000; /* Ensure tabs are above other content */
background-color: #fafafa; /* Optional: Add background color to tabs */
padding: 10px 20px; /* Optional: Add padding for better appearance */
border-bottom: 1px solid #ccc; /* Optional: Add bottom border */
}
.dialog-popup-container {
/* Styles for the overlay/background */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
display: flex;
align-items: center;
justify-content: center;
}
.popup-content h2 {
margin-top: 0;
.dialog-popup {
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
.form-group {
margin-bottom: 20px;
.dialog-popup-header h2 {
margin-bottom: 20px; /* Add space between heading and fields */
}
.form-group label {
.dialog-popup-field {
margin-bottom: 15px;
}
.dialog-popup-field label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
.dialog-popup-field input {
width: 100%;
padding: 8px;
background-color: var(--popup-bg-color-light);
color: var(--popup-text-color-light);
border: 1px solid var(--popup-border-color-light);
border-radius: 5px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
.custom-select {
position: relative;
width: 100%;
}
.custom-select select {
width: 100%;
padding: 8px;
.submit-button {
background-color: #4caf50; /* Green */
color: white;
padding: 10px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
background-color: var(--popup-bg-color-light);
color: var(--popup-text-color-light);
border: 1px solid var(--popup-border-color-light);
appearance: none;
}
.custom-select::after {
content: "\25BC";
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
/* Specific styles for each field to match the image */
.name-field input,
.shortname-field input,
.code-field input {
width: calc(100% - 100px); /* Adjust as needed */
}
.message {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
.active-field input {
width: 50px;
}
/* Add to App.css or your CSS file */
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
min-height: 100vh;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
.input-form {
margin-bottom: 16px;
}
.message.success {
background-color: #d4edda;
color: #155724;
.content-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 300px;
}
@media (prefers-color-scheme: dark) {
.update-subscription-popup {
background-color: var(--popup-bg-color-dark);
border-color: var(--popup-border-color-dark);
color: var(--popup-text-color-dark);
box-shadow: 0px 0px 10px var(--popup-shadow-dark);
}
.close-button {
color: var(--popup-text-color-dark);
}
.form-group input,
.form-group select {
background-color: var(--popup-bg-color-dark);
color: var(--popup-text-color-dark);
border: 1px solid var(--popup-border-color-dark);
}
.form-group button {
background-color: var(--popup-bg-color-dark);
color: var(--popup-text-color-dark);
border: 1px solid var(--popup-border-color-dark);
}
.horizontal-group {
display: flex;
align-items: center;
}
.fetch-button-container {
margin-right: 10px;
margin-left: 10px;
}
.text-field-token,
.text-field-email {
margin: 0 8px;
}
.error-message {
color: red;
font-weight: bold;
margin-top: 16px;
}

View File

@@ -1,38 +1,122 @@
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import TextField from "@mui/material/TextField";
import * as React from "react";
import { useEffect, useState } from "react";
import "./App.css";
import { Sidebar } from "./components/Sidebar";
import FamilyTableComponent from "./components/FamilyComponentTable";
import StorageBonusTableComponent from "./components/StorageBonusTableComponent";
import type { UserData } from "./components/UserComponent";
import UserComponent from "./components/UserComponent";
import duckieimage from "./components/duckie.png";
import { apiOrigin } from "./services/support";
import S from "./utils/strings";
type User = Record<
string,
string | number | boolean | null | undefined | Record<string, unknown>
>;
type UserData = Record<string, User>;
export let email = "";
export let token = "";
export const App: React.FC = () => {
const [token, setToken] = useState<string>("");
const [email, setEmail] = useState<string>("");
export const setEmail = (newEmail: string) => {
email = newEmail;
};
export const setToken = (newToken: string) => {
token = newToken;
};
export const getEmail = () => email;
export const getToken = () => token;
interface User {
ID: string;
email: string;
creationTime: number;
}
interface Subscription {
productID: string;
paymentProvider: string;
expiryTime: number;
storage: number;
}
interface Security {
isEmailMFAEnabled: boolean;
isTwoFactorEnabled: boolean;
passkeys: string;
}
interface UserResponse {
user: User;
subscription: Subscription;
details?: {
usage?: number;
storageBonus?: number;
profileData: Security;
};
}
const App: React.FC = () => {
const [localEmail, setLocalEmail] = useState<string>("");
const [localToken, setLocalToken] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [fetchSuccess, setFetchSuccess] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<number>(0);
const [userData, setUserData] = useState<UserData | null>(null);
const [error, setError] = useState<string | null>(null);
const [isDataFetched, setIsDataFetched] = useState<boolean>(false);
useEffect(() => {
const storedToken = localStorage.getItem("token");
if (storedToken) {
setToken(storedToken);
setLocalToken(storedToken);
}
}, []);
useEffect(() => {
if (token) {
localStorage.setItem("token", token);
if (localToken) {
setToken(localToken);
localStorage.setItem("token", localToken);
} else {
localStorage.removeItem("token");
}
}, [token]);
}, [localToken]);
useEffect(() => {
if (localEmail) {
setEmail(localEmail);
localStorage.setItem("email", localEmail);
} else {
localStorage.removeItem("email");
}
}, [localEmail]);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const urlEmail = urlParams.get("email");
const urlToken = urlParams.get("token");
if (urlEmail && urlToken) {
setLocalEmail(urlEmail);
setLocalToken(urlToken);
console.log(localEmail);
console.log(localToken);
setEmail(urlEmail);
setToken(urlToken);
fetchData().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}
console.log(email);
console.log(token);
}, []);
const fetchData = async () => {
setLoading(true);
setError("");
setFetchSuccess(false);
const startTime = Date.now();
try {
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
@@ -42,119 +126,79 @@ export const App: React.FC = () => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
const userDataResponse = (await response.json()) as UserData;
const userDataResponse: UserResponse =
(await response.json()) as UserResponse;
console.log("API Response:", userDataResponse);
setUserData(userDataResponse);
setError(null);
setIsDataFetched(true);
const extractedUserData: UserData = {
User: {
"User ID": userDataResponse.user.ID || "None",
Email: userDataResponse.user.email || "None",
"Creation time":
new Date(
userDataResponse.user.creationTime / 1000,
).toLocaleString() || "None",
},
Storage: {
Total: userDataResponse.subscription.storage
? userDataResponse.subscription.storage >= 1024 ** 3
? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB`
: `${(userDataResponse.subscription.storage / 1024 ** 2).toFixed(2)} MB`
: "None",
Consumed:
userDataResponse.details?.usage !== undefined
? userDataResponse.details.usage >= 1024 ** 3
? `${(userDataResponse.details.usage / 1024 ** 3).toFixed(2)} GB`
: `${(userDataResponse.details.usage / 1024 ** 2).toFixed(2)} MB`
: "None",
Bonus:
userDataResponse.details?.storageBonus !== undefined
? userDataResponse.details.storageBonus >= 1024 ** 3
? `${(userDataResponse.details.storageBonus / 1024 ** 3).toFixed(2)} GB`
: `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB`
: "None",
},
Subscription: {
"Product ID":
userDataResponse.subscription.productID || "None",
Provider:
userDataResponse.subscription.paymentProvider || "None",
"Expiry time":
new Date(
userDataResponse.subscription.expiryTime / 1000,
).toLocaleString() || "None",
},
Security: {
"Email MFA": userDataResponse.details?.profileData
.isEmailMFAEnabled
? "Enabled"
: "Disabled",
"Two factor 2FA": userDataResponse.details?.profileData
.isTwoFactorEnabled
? "Enabled"
: "Disabled",
Passkeys: "None",
},
};
const elapsedTime = Date.now() - startTime;
const delay = Math.max(3000 - elapsedTime, 0);
setTimeout(() => {
setLoading(false);
setFetchSuccess(true);
setUserData(extractedUserData);
}, delay);
} catch (error) {
console.error("Error fetching data:", error);
setError((error as Error).message);
setIsDataFetched(false);
const elapsedTime = Date.now() - startTime;
const delay = Math.max(3000 - elapsedTime, 0);
setTimeout(() => {
setLoading(false);
setError("Invalid token or email id");
}, delay);
}
};
const renderAttributes = (
data: Record<string, unknown> | User | null,
): React.ReactNode => {
if (!data) return null;
const nullAttributes: string[] = [];
const rows = Object.entries(data).map(([key, value]) => {
console.log("Processing key:", key, "value:", value);
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
return (
<React.Fragment key={key}>
<tr>
<td
colSpan={2}
style={{
fontWeight: "bold",
backgroundColor: "#f1f1f1",
padding: "10px",
}}
>
{key.toUpperCase()}
</td>
</tr>
{renderAttributes(
value as Record<string, unknown> | User,
)}
</React.Fragment>
);
} else {
if (value === null) {
nullAttributes.push(key);
}
let displayValue: React.ReactNode;
if (key === "expiryTime" && typeof value === "number") {
displayValue = new Date(value / 1000).toLocaleString();
} else if (
key === "creationTime" &&
typeof value === "number"
) {
displayValue = new Date(value / 1000).toLocaleString();
} else if (key === "storage" && typeof value === "number") {
displayValue = `${(value / 1024 ** 3).toFixed(2)} GB`;
} else if (typeof value === "string") {
try {
const parsedValue = JSON.parse(
value,
) as React.ReactNode;
displayValue = parsedValue;
} catch (error) {
displayValue = value;
}
} else if (typeof value === "object" && value !== null) {
displayValue = JSON.stringify(value, null, 2);
} else if (value === null) {
displayValue = "null";
} else if (
typeof value === "boolean" ||
typeof value === "number"
) {
displayValue = value.toString();
} else if (typeof value === "undefined") {
displayValue = "undefined";
} else {
displayValue = value as string;
}
return (
<tr key={key}>
<td
style={{
padding: "10px",
border: "1px solid #ddd",
}}
>
{key}
</td>
<td
style={{
padding: "10px",
border: "1px solid #ddd",
}}
>
{displayValue}
</td>
</tr>
);
}
});
console.log("Attributes with null values:", nullAttributes);
return rows;
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLFormElement>) => {
if (event.key === "Enter") {
event.preventDefault();
@@ -164,94 +208,134 @@ export const App: React.FC = () => {
}
};
const handleTabChange = (
_event: React.SyntheticEvent,
newValue: number,
) => {
setTabValue(newValue);
};
return (
<div className="container center-table">
<h1>{S.hello}</h1>
<div className="container">
<form className="input-form" onKeyPress={handleKeyPress}>
<div className="input-group">
<label>
Token:
<input
type="text"
value={token}
onChange={(e) => setToken(e.target.value)}
style={{
padding: "10px",
margin: "10px",
width: "100%",
<div className="horizontal-group">
<a
href="https://staff.ente.sh"
target="_blank"
rel="noopener noreferrer"
className="link-text"
>
staff.ente.sh
</a>
<TextField
label="Token"
value={localToken}
onChange={(e) => {
setLocalToken(e.target.value);
setToken(e.target.value);
}}
size="medium"
className="text-field-token"
style={{ width: "350px" }}
/>
<TextField
label="Email"
value={localEmail}
onChange={(e) => {
setLocalEmail(e.target.value);
setEmail(e.target.value);
}}
size="medium"
className="text-field-email"
style={{ width: "350px" }}
/>
<div className="fetch-button-container">
<Button
variant="contained"
onClick={() => {
fetchData().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}}
/>
</label>
</div>
<div className="input-group">
<label>
Email id:
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="fetch-button"
style={{
padding: "10px",
margin: "10px",
width: "100%",
padding: "0 16px",
}}
/>
</label>
>
FETCH
</Button>
</div>
</div>
</form>
<div className="content-wrapper">
{isDataFetched && <Sidebar token={token} email={email} />}
<div className="fetch-button-container">
<button
onClick={() => {
fetchData().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}}
>
FETCH
</button>
</div>
<div className="content-container">
{loading ? (
<CircularProgress sx={{ color: "black" }} />
) : error ? (
<div className="error-message">{error}</div>
) : fetchSuccess ? (
<>
<Box
sx={{
width: "100%",
maxWidth: "600px",
bgcolor: "#FAFAFA",
borderRadius: "7px",
position: "relative",
zIndex: 1000,
}}
>
<Tabs
value={tabValue}
onChange={handleTabChange}
centered
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "#00B33C",
height: "5px",
borderRadius: "20px",
},
"& .MuiTab-root": {
textTransform: "none",
},
}}
>
<Tab label="User" />
<Tab label="Family" />
<Tab label="Bonuses" />
</Tabs>
</Box>
<Box
sx={{
width: "100%",
maxWidth: "900px",
bgcolor: "#FAFAFA",
borderRadius: "7px",
padding: "20px",
position: "relative",
zIndex: 999,
marginTop: "16px",
}}
>
{tabValue === 0 && userData && (
<UserComponent userData={userData} />
)}
{tabValue === 1 && userData && (
<div>
<FamilyTableComponent />
</div>
)}
{tabValue === 2 && userData && (
<div>
<StorageBonusTableComponent />
</div>
)}
</Box>
</>
) : (
<img src={duckieimage} alt="duckie" />
)}
</div>
<br />
{error && <p style={{ color: "red" }}>{`Error: ${error}`}</p>}
{userData && (
<table
style={{
width: "100%",
borderCollapse: "collapse",
margin: "20px 0",
fontSize: "1em",
minWidth: "400px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
}}
>
<tbody>
{Object.keys(userData).map((category) => (
<React.Fragment key={category}>
<tr>
<td
colSpan={2}
style={{
fontWeight: "bold",
backgroundColor: "#f1f1f1",
padding: "10px",
}}
>
{category.toUpperCase()}
</td>
</tr>
{renderAttributes(userData[category] ?? null)}
</React.Fragment>
))}
</tbody>
</table>
)}
<footer className="footer">
<p>
<a href="https://help.ente.io">help.ente.io</a>
</p>
</footer>
</div>
);
};

View File

@@ -0,0 +1,192 @@
import CloseIcon from "@mui/icons-material/Close";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
import React, { useEffect, useState } from "react";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
interface ErrorResponse {
message: string;
}
interface ChangeEmailProps {
open: boolean;
onClose: () => void;
}
interface UserDataResponse {
subscription: {
userID: string;
} | null;
}
const ChangeEmail: React.FC<ChangeEmailProps> = ({ open, onClose }) => {
const [newEmail, setNewEmail] = useState<string>("");
const [userID, setUserID] = useState<string>("");
useEffect(() => {
const fetchUserID = async () => {
const token = getToken();
const email = getEmail();
setNewEmail(email); // Set initial email state
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
try {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-AUTH-TOKEN": token,
},
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = (await response.json()) as UserDataResponse;
if (data.subscription) {
setUserID(data.subscription.userID); // Update userID state
} else {
throw new Error("Subscription data not found");
}
} catch (error) {
console.error("Error fetching user ID:", error);
}
};
if (open) {
fetchUserID().catch((error: unknown) =>
console.error("Error in fetchUserID:", error),
);
}
}, [open]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewEmail(event.target.value); // Update newEmail state on input change
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const token = getToken();
const url = `${apiOrigin}/admin/user/change-email?token=${token}`;
const body = {
userID,
email: newEmail,
};
try {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-AUTH-TOKEN": token,
},
body: JSON.stringify(body),
});
if (!response.ok) {
let errorData;
try {
errorData = (await response.json()) as ErrorResponse;
} catch (error) {
console.error("Error parsing error response:", error);
}
throw new Error(
errorData?.message ?? "Network response was not ok",
);
}
console.log("Email updated successfully");
onClose();
} catch (error) {
console.error("Error updating email:", error);
}
};
const handleSubmitSync: React.FormEventHandler<HTMLFormElement> = (
event,
) => {
handleSubmit(event).catch((error: unknown) => {
console.error("Error in handleSubmit:", error);
});
};
return (
<Dialog
open={open}
onClose={onClose}
BackdropProps={{
style: {
backdropFilter: "blur(5px)",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
}}
PaperProps={{
style: {
width: "444px",
height: "300px",
},
}}
>
<DialogTitle style={{ marginBottom: "20px", marginTop: "20px" }}>
Change Email
<Button
onClick={onClose}
style={{ position: "absolute", right: 10, top: 10 }}
>
<CloseIcon style={{ color: "black" }} />
</Button>
</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmitSync}>
<div style={{ marginBottom: "16px" }}>
<label
htmlFor="newEmail"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Email
</label>
<TextField
id="newEmail"
name="newEmail"
value={newEmail}
onChange={handleChange}
fullWidth
/>
</div>
<DialogActions
style={{ justifyContent: "center", marginTop: "40px" }}
>
<Button
type="submit"
variant="contained"
style={{
backgroundColor: "#00B33C",
color: "white",
}}
>
Change Email
</Button>
</DialogActions>
</form>
</DialogContent>
</Dialog>
);
};
export default ChangeEmail;

View File

@@ -0,0 +1,157 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Paper,
} from "@mui/material";
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
interface CloseFamilyProps {
open: boolean;
handleClose: () => void;
handleCloseFamily: () => void; // Callback to handle closing family
}
const CloseFamily: React.FC<CloseFamilyProps> = ({
open,
handleClose,
handleCloseFamily,
}) => {
const [loading, setLoading] = useState(false);
const handleClosure = async () => {
try {
setLoading(true);
const email = getEmail();
const token = getToken();
if (!email) {
throw new Error("Email not found");
}
if (!token) {
throw new Error("Token not found");
}
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
// Fetch user data
const userUrl = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const userResponse = await fetch(userUrl);
if (!userResponse.ok) {
throw new Error("Failed to fetch user data");
}
const userData = (await userResponse.json()) as UserData;
const userId = userData.subscription?.userID;
if (!userId) {
throw new Error("User ID not found");
}
// Close family action
const closeFamilyUrl = `${apiOrigin}/admin/user/close-family?token=${encodedToken}`;
const body = JSON.stringify({ userId });
const closeFamilyResponse = await fetch(closeFamilyUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body,
});
if (!closeFamilyResponse.ok) {
const errorResponse = await closeFamilyResponse.text();
throw new Error(`Failed to close family: ${errorResponse}`);
}
handleCloseFamily(); // Notify parent component of successful action
handleClose(); // Close dialog on successful action
console.log("Family closed successfully");
} catch (error) {
console.error("Error closing family:", error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
handleClose(); // Close dialog
};
return (
<div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperComponent={Paper}
sx={{
width: "499px",
height: "286px",
margin: "auto",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
BackdropProps={{
style: {
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
},
}}
>
<DialogTitle id="alert-dialog-title">
{"Close Family?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to close family relations for this
account?
</DialogContentText>
</DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
<Button
onClick={handleCancel}
sx={{
bgcolor: "white",
color: "black",
"&:hover": { bgcolor: "#FAFAFA" },
}}
>
Cancel
</Button>
<Button
onClick={() => {
handleClosure().catch((error: unknown) =>
console.error(error),
);
}}
sx={{
bgcolor: "#F4473D",
color: "white",
"&:hover": { bgcolor: "#E53935" },
}}
disabled={loading}
>
{loading ? "Closing..." : "Close"}
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default CloseFamily;

View File

@@ -0,0 +1,100 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Paper,
} from "@mui/material";
import React from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface DeleteAccountProps {
open: boolean;
handleClose: () => void;
}
const DeleteAccount: React.FC<DeleteAccountProps> = ({ open, handleClose }) => {
const handleDelete = async () => {
try {
const encodedEmail = encodeURIComponent(getEmail());
console.log(encodedEmail);
const encodedToken = encodeURIComponent(getToken());
console.log(encodedToken);
const deleteUrl = `${apiOrigin}/admin/user/delete?email=${encodedEmail}&token=${encodedToken}`;
const response = await fetch(deleteUrl, { method: "DELETE" });
if (!response.ok) {
throw new Error("Failed to delete user account");
}
handleClose(); // Close dialog on successful delete
console.log("Account deleted successfully");
} catch (error) {
console.error("Error deleting user account:", error);
}
};
return (
<div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperComponent={Paper}
sx={{
width: "499px",
height: "286px",
margin: "auto",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
BackdropProps={{
style: {
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
},
}}
>
<DialogTitle id="alert-dialog-title">
{"Delete Account?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete the account?
</DialogContentText>
</DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
<Button
onClick={handleClose}
sx={{
bgcolor: "white",
color: "black",
"&:hover": { bgcolor: "#FAFAFA" },
}}
>
Cancel
</Button>
<Button
onClick={() => {
handleDelete().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}}
sx={{
bgcolor: "#F4473D",
color: "white",
"&:hover": { bgcolor: "#E53935" },
}}
>
Delete{" "}
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default DeleteAccount;

View File

@@ -0,0 +1,156 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Paper,
} from "@mui/material";
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
interface Disable2FAProps {
open: boolean;
handleClose: () => void;
handleDisable2FA: () => void; // Callback to handle 2FA disablement
}
const Disable2FA: React.FC<Disable2FAProps> = ({
open,
handleClose,
handleDisable2FA,
}) => {
const [loading, setLoading] = useState(false);
const handleDisable = async () => {
try {
setLoading(true);
const email = getEmail();
const token = getToken();
if (!email) {
throw new Error("Email not found");
}
if (!token) {
throw new Error("Token not found");
}
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
// Fetch user data
const userUrl = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const userResponse = await fetch(userUrl);
if (!userResponse.ok) {
throw new Error("Failed to fetch user data");
}
const userData = (await userResponse.json()) as UserData;
const userId = userData.subscription?.userID;
if (!userId) {
throw new Error("User ID not found");
}
// Disable 2FA
const disableUrl = `${apiOrigin}/admin/user/disable-2fa?token=${encodedToken}`;
const body = JSON.stringify({ userId });
const disableResponse = await fetch(disableUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body,
});
if (!disableResponse.ok) {
const errorResponse = await disableResponse.text();
throw new Error(`Failed to disable 2FA: ${errorResponse}`);
}
handleDisable2FA(); // Notify parent component of successful disable
handleClose(); // Close dialog on successful disable
console.log("2FA disabled successfully");
} catch (error) {
console.error("Error disabling 2FA:", error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
handleClose(); // Close dialog
};
return (
<div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperComponent={Paper}
sx={{
width: "499px",
height: "286px",
margin: "auto",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
BackdropProps={{
style: {
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
},
}}
>
<DialogTitle id="alert-dialog-title">
{"Disable 2FA?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to disable 2FA for this account?
</DialogContentText>
</DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
<Button
onClick={handleCancel}
sx={{
bgcolor: "white",
color: "black",
"&:hover": { bgcolor: "#FAFAFA" },
}}
>
Cancel
</Button>
<Button
onClick={() => {
handleDisable().catch((error: unknown) =>
console.error(error),
);
}}
sx={{
bgcolor: "#F4473D",
color: "white",
"&:hover": { bgcolor: "#E53935" },
}}
disabled={loading}
>
{loading ? "Disabling..." : "Disable"}
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default Disable2FA;

View File

@@ -0,0 +1,157 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Paper,
} from "@mui/material";
import React, { useState } from "react";
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
import { apiOrigin } from "../services/support";
interface UserData {
subscription?: {
userID: string;
// Add other properties as per your API response structure
};
// Add other properties as per your API response structure
}
interface DisablePasskeysProps {
open: boolean;
handleClose: () => void;
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
}
const DisablePasskeys: React.FC<DisablePasskeysProps> = ({
open,
handleClose,
handleDisablePasskeys,
}) => {
const [loading, setLoading] = useState(false);
const handleDisabling = async () => {
try {
setLoading(true);
const email = getEmail();
const token = getToken();
if (!email) {
throw new Error("Email not found");
}
if (!token) {
throw new Error("Token not found");
}
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
// Fetch user data
const userUrl = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const userResponse = await fetch(userUrl);
if (!userResponse.ok) {
throw new Error("Failed to fetch user data");
}
const userData = (await userResponse.json()) as UserData;
const userId = userData.subscription?.userID;
if (!userId) {
throw new Error("User ID not found");
}
// Disable passkeys action
const disablePasskeysUrl = `${apiOrigin}/admin/user/disable-passkeys?token=${encodedToken}`;
const body = JSON.stringify({ userId });
const disablePasskeysResponse = await fetch(disablePasskeysUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body,
});
if (!disablePasskeysResponse.ok) {
const errorResponse = await disablePasskeysResponse.text();
throw new Error(`Failed to disable passkeys: ${errorResponse}`);
}
handleDisablePasskeys(); // Notify parent component of successful action
handleClose(); // Close dialog on successful action
console.log("Passkeys disabled successfully");
} catch (error) {
console.error("Error disabling passkeys:", error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
handleClose(); // Close dialog
};
return (
<div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperComponent={Paper}
sx={{
width: "499px",
height: "286px",
margin: "auto",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
BackdropProps={{
style: {
backgroundColor: "rgba(255, 255, 255, 0.9)", // Semi-transparent backdrop
},
}}
>
<DialogTitle id="alert-dialog-title">
{"Disable Passkeys?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to disable passkeys for this
account?
</DialogContentText>
</DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
<Button
onClick={handleCancel}
sx={{
bgcolor: "white",
color: "black",
"&:hover": { bgcolor: "#FAFAFA" },
}}
>
Cancel
</Button>
<Button
onClick={() => {
handleDisabling().catch((error: unknown) =>
console.error(error),
);
}}
sx={{
bgcolor: "#F4473D",
color: "white",
"&:hover": { bgcolor: "#E53935" },
}}
disabled={loading}
>
{loading ? "Disabling..." : "Disable"}
</Button>
</DialogActions>
</Dialog>
</div>
);
};
export default DisablePasskeys;

View File

@@ -0,0 +1,176 @@
import {
Button,
CircularProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import * as React from "react";
import { useEffect, useState } from "react";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
import CloseFamily from "./CloseFamily";
interface FamilyMember {
id: string;
email: string;
status: string;
usage: number;
}
interface UserData {
details: {
familyData: {
members: FamilyMember[];
};
};
}
const FamilyTableComponent: React.FC = () => {
const [familyMembers, setFamilyMembers] = useState<FamilyMember[]>([]);
const [closeFamilyOpen, setCloseFamilyOpen] = useState(false);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const encodedEmail = encodeURIComponent(getEmail());
const encodedToken = encodeURIComponent(getToken());
const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const userData = (await response.json()) as UserData; // Typecast to UserData interface
const members: FamilyMember[] =
userData.details.familyData.members;
setFamilyMembers(members);
} catch (error) {
console.error("Error fetching family data:", error);
setError("No family data");
} finally {
setLoading(false);
}
};
fetchData().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}, []);
const formatUsageToGB = (usage: number): string => {
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
return `${usageInGB} GB`;
};
const handleOpenCloseFamily = () => {
setCloseFamilyOpen(true);
};
const handleCloseCloseFamily = () => {
setCloseFamilyOpen(false);
};
const handleCloseFamily = () => {
console.log("Close family action");
handleOpenCloseFamily();
};
if (loading) {
return <CircularProgress />;
}
if (error) {
return <div>Error: {error}</div>;
}
if (familyMembers.length === 0) {
return <div>No family data available</div>;
}
return (
<>
<TableContainer
component={Paper}
style={{
marginTop: "20px",
backgroundColor: "#F1F1F3",
}}
>
<Table aria-label="family-table">
<TableHead>
<TableRow>
<TableCell>
<b>User</b>
</TableCell>
<TableCell>
<b>Status</b>
</TableCell>
<TableCell>
<b>Usage</b>
</TableCell>
<TableCell>
<b>ID</b>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{familyMembers.map((member) => (
<TableRow key={member.id}>
<TableCell>{member.email}</TableCell>
<TableCell>
<span
style={{
backgroundColor:
member.status === "SELF"
? "#00B33C"
: "transparent",
color:
member.status === "SELF"
? "white"
: "inherit",
padding: "4px 8px",
borderRadius: "10px",
}}
>
{member.status === "SELF"
? "ADMIN"
: member.status}
</span>
</TableCell>
<TableCell>
{formatUsageToGB(member.usage)}
</TableCell>
<TableCell>{member.id}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<div style={{ marginTop: "20px" }}>
<Button
variant="contained"
color="error"
onClick={handleCloseFamily}
>
Close Family
</Button>
</div>
{closeFamilyOpen && (
<CloseFamily
open={closeFamilyOpen}
handleClose={handleCloseCloseFamily}
handleCloseFamily={handleCloseCloseFamily}
/>
)}
</>
);
};
export default FamilyTableComponent;

View File

@@ -1,256 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import "../App.css";
import { apiOrigin } from "../services/support";
import UpdateSubscription from "./UpdateSubscription"; // Import the UpdateSubscription component
interface SidebarProps {
token: string;
email: string;
}
interface UserData {
user: {
ID: string;
};
}
interface ActionResponse {
success?: boolean;
message?: string;
}
export const Sidebar: React.FC<SidebarProps> = ({ token, email }) => {
const [userId, setUserId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [dropdownVisible, setDropdownVisible] = useState<boolean>(false);
const [showUpdateSubscription, setShowUpdateSubscription] =
useState<boolean>(false); // State to control UpdateSubscription popup
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setDropdownVisible(false);
}
};
const fetchData = async (): Promise<string | null> => {
if (!email || !token) {
setError("Email or token is missing.");
return null;
}
try {
const url = `${apiOrigin}/admin/user?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const userDataResponse = (await response.json()) as UserData;
const fetchedUserId = userDataResponse.user.ID;
if (!fetchedUserId) {
throw new Error("User ID not found in response");
}
setUserId(fetchedUserId);
setError(null);
return fetchedUserId;
} catch (error) {
console.error("Error fetching data:", error);
setError(
error instanceof Error && typeof error.message === "string"
? error.message
: "An unexpected error occurred",
);
setTimeout(() => {
setError(null);
}, 1000);
return null;
}
};
const performAction = async (userId: string, action: string) => {
try {
const actionUrls: Record<string, string> = {
Disable2FA: "/admin/user/disable-2fa",
DisablePasskeys: "/admin/user/disable-passkeys",
Closefamily: "/admin/user/close-family",
};
const url = `${apiOrigin}${actionUrls[action]}?id=${encodeURIComponent(userId)}&token=${encodeURIComponent(token)}`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
if (!response.ok) {
throw new Error(
`Network response was not ok: ${response.status}`,
);
}
const result = (await response.json()) as ActionResponse;
console.log("API Response:", result);
setMessage(`${action} completed successfully`);
setError(null);
setTimeout(() => {
setMessage(null);
}, 1000);
setDropdownVisible(false);
} catch (error) {
console.error(`Error ${action}:`, error);
setError(
error instanceof Error && typeof error.message === "string"
? error.message
: "An unexpected error occurred",
);
setTimeout(() => {
setError(null);
}, 1000);
setMessage(null);
}
};
const deleteUser = async () => {
try {
const url = `${apiOrigin}/admin/user/delete?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`;
const response = await fetch(url, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(
`Network response was not ok: ${response.status}`,
);
}
setMessage("Delete Account completed successfully");
setError(null);
setTimeout(() => {
setMessage(null);
}, 1000);
setDropdownVisible(false);
} catch (error) {
console.error(`Error deleting account:`, error);
setError(
error instanceof Error && typeof error.message === "string"
? error.message
: "An unexpected error occurred",
);
setTimeout(() => {
setError(null);
}, 1000);
setMessage(null);
}
};
const handleActionClick = async (action: string) => {
try {
if (action === "UpdateSubscription") {
const fetchedUserId = await fetchData();
if (fetchedUserId) {
setShowUpdateSubscription(true);
}
return;
}
if (action === "DeleteAccount") {
await deleteUser();
return;
}
const fetchedUserId = await fetchData();
if (!fetchedUserId) {
throw new Error("Incorrect email id or token");
}
await performAction(fetchedUserId, action);
} catch (error) {
console.error(`Error performing ${action}:`, error);
setError(
error instanceof Error && typeof error.message === "string"
? error.message
: "An unexpected error occurred",
);
setTimeout(() => {
setError(null);
}, 1000);
setMessage(null);
}
};
const toggleDropdown = () => {
setDropdownVisible(!dropdownVisible);
};
const dropdownOptions = [
{ value: "Disable2FA", label: "Disable 2FA" },
{ value: "Closefamily", label: "Close Family" },
{ value: "DisablePasskeys", label: "Disable Passkeys" },
{ value: "DeleteAccount", label: "Delete Account" },
{ value: "UpdateSubscription", label: "Update Subscription" }, // New option added here
];
return (
<div className="sidebar">
<div className="dropdown-container">
<button className="more-button" onClick={toggleDropdown}>
MORE
</button>
{dropdownVisible && (
<div className="dropdown-menu" ref={dropdownRef}>
<ul>
{dropdownOptions.map((option) => (
<li key={option.value}>
<button
onClick={() => {
handleActionClick(
option.value,
).catch((error: unknown) =>
console.error(
"Error handling action:",
error,
),
);
}}
>
{option.label}
</button>
</li>
))}
</ul>
</div>
)}
</div>
{(error ?? message) && (
<div className={`message ${error ? "error" : "success"}`}>
{error ? `Error: ${error}` : `Success: ${message}`}
</div>
)}
{showUpdateSubscription && userId && (
<UpdateSubscription
token={token}
userId={userId}
onClose={() => setShowUpdateSubscription(false)}
/>
)}
</div>
);
};
export default Sidebar;

View File

@@ -0,0 +1,160 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import * as React from "react";
import { useEffect, useState } from "react";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
interface BonusData {
storage: number;
type: string;
createdAt: number;
validTill: number;
isRevoked: boolean;
}
interface UserData {
details: {
bonusData: {
storageBonuses: BonusData[];
};
};
}
const StorageBonusTableComponent: React.FC = () => {
const [storageBonuses, setStorageBonuses] = useState<BonusData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const encodedEmail = encodeURIComponent(getEmail());
const encodedToken = encodeURIComponent(getToken());
const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to fetch bonus data");
}
const userData = (await response.json()) as UserData; // Typecast to UserData interface
const bonuses: BonusData[] =
userData.details.bonusData.storageBonuses;
setStorageBonuses(bonuses);
} catch (error) {
console.error("Error fetching bonus data:", error);
setError("No bonus data");
} finally {
setLoading(false);
}
};
fetchData().catch((error: unknown) =>
console.error("Fetch data error:", error),
);
}, []);
const formatCreatedAt = (createdAt: number): string => {
const date = new Date(createdAt / 1000);
return date.toLocaleDateString(); // Adjust date formatting as needed
};
const formatValidTill = (validTill: number): string => {
if (validTill === 0) {
return "Forever";
} else {
const date = new Date(validTill / 1000);
return date.toLocaleDateString(); // Adjust date formatting as needed
}
};
const formatStorage = (storage: number): string => {
const inGB = storage / (1024 * 1024 * 1024);
return `${inGB.toFixed(2)} GB`;
};
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
if (storageBonuses.length === 0) {
return <p>No bonus data available</p>;
}
return (
<div style={{ marginTop: "20px", marginBottom: "20px" }}>
<TableContainer
component={Paper}
style={{
backgroundColor: "#F1F1F3",
}}
>
<Table aria-label="storage-bonus-table">
<TableHead>
<TableRow>
<TableCell>
<b>Storage</b>
</TableCell>
<TableCell>
<b>Type</b>
</TableCell>
<TableCell>
<b>Created At</b>
</TableCell>
<TableCell>
<b>Valid Till</b>
</TableCell>
<TableCell>
<b>Is Revoked</b>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{storageBonuses.map((bonus, index) => (
<TableRow key={index}>
<TableCell>
{formatStorage(bonus.storage)}
</TableCell>
<TableCell>{bonus.type}</TableCell>
<TableCell>
{formatCreatedAt(bonus.createdAt)}
</TableCell>
<TableCell>
{formatValidTill(bonus.validTill)}
</TableCell>
<TableCell>
<span
style={{
backgroundColor: bonus.isRevoked
? "#494949"
: "transparent",
color: bonus.isRevoked
? "white"
: "inherit",
padding: "4px 8px",
borderRadius: "10px",
}}
>
{bonus.isRevoked ? "Yes" : "No"}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
};
export default StorageBonusTableComponent;

View File

@@ -1,178 +1,367 @@
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CloseIcon from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import Grid from "@mui/material/Grid";
import InputAdornment from "@mui/material/InputAdornment";
import MenuItem from "@mui/material/MenuItem";
import Select, { type SelectChangeEvent } from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import React, { useEffect, useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import "../App.css";
import { getEmail, getToken } from "../App";
import { apiOrigin } from "../services/support";
interface Subscription {
productID: string;
paymentProvider: string;
storage: number;
originalTransactionID: string;
expiryTime: number;
userID: string;
}
interface UserDataResponse {
subscription: Subscription | null;
}
interface UpdateSubscriptionProps {
token: string;
userId: string;
open: boolean;
onClose: () => void;
}
export const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
token,
userId,
interface FormValues {
productId: string;
provider: string;
storage: number;
transactionId: string;
expiryTime: string | Date | null;
userId: string;
}
const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
open,
onClose,
}) => {
const [expiryTime, setExpiryTime] = useState<Date | null>(null);
const [productId, setProductId] = useState<string>("50gb_monthly");
const [paymentProvider, setPaymentProvider] = useState<string>("bitpay");
const [transactionId, setTransactionId] = useState<string>("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [storage, setStorage] = useState<number | "">("");
const [values, setValues] = useState<FormValues>({
productId: "",
provider: "",
storage: 0,
transactionId: "",
expiryTime: "",
userId: "",
});
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
useEffect(() => {
if (productId === "50gb_yearly" || productId === "50gb_monthly") {
setStorage(50 * 1024 * 1024 * 1024);
} else if (
productId === "200gb_yearly" ||
productId === "200gb_monthly"
) {
setStorage(200 * 1024 * 1024 * 1024);
} else if (
productId === "500gb_yearly" ||
productId === "500gb_monthly"
) {
setStorage(500 * 1024 * 1024 * 1024);
} else if (
productId === "2000gb_yearly" ||
productId === "2000gb_monthly"
) {
setStorage(2000 * 1024 * 1024 * 1024);
} else {
setStorage("");
}
}, [productId]);
const fetchData = async () => {
try {
const email = getEmail();
const token = getToken();
const encodedEmail = encodeURIComponent(email);
const encodedToken = encodeURIComponent(token);
const url = `${apiOrigin}/admin/user?email=${encodedEmail}&token=${encodedToken}`;
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
const response = await fetch(url);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const userDataResponse =
(await response.json()) as UserDataResponse;
const expiryTimeTimestamp = expiryTime
? expiryTime.getTime() * 1000
: "";
if (!userDataResponse.subscription) {
throw new Error("Subscription data not found");
}
const url = `${apiOrigin}/admin/user/subscription`;
const body = {
userId,
storage,
expiryTime: expiryTimeTimestamp,
productId,
paymentProvider,
transactionId,
const expiryTime = new Date(
userDataResponse.subscription.expiryTime / 1000,
);
setValues({
productId: userDataResponse.subscription.productID || "",
provider:
userDataResponse.subscription.paymentProvider || "",
storage:
userDataResponse.subscription.storage /
(1024 * 1024 * 1024) || 0,
transactionId:
userDataResponse.subscription.originalTransactionID ||
"",
expiryTime: expiryTime,
userId: userDataResponse.subscription.userID || "",
});
} catch (error) {
console.error("Error fetching data:", error);
}
};
try {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-AUTH-TOKEN": token,
},
body: JSON.stringify(body),
fetchData().catch((error: unknown) => {
console.error("Unhandled promise rejection:", error);
});
}, []);
const handleCalendarClick = () => {
setIsDatePickerOpen(true);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setValues({
...values,
[name]: name === "storage" ? parseInt(value, 10) : value,
});
};
const handleChangeProvider = (event: SelectChangeEvent) => {
const { name, value } = event.target;
if (name) {
setValues({
...values,
[name]: value,
});
if (!response.ok) {
throw new Error(
`Network response was not ok: ${response.status}`,
);
}
setMessage("Subscription updated successfully");
setError(null);
setTimeout(() => {
setMessage(null);
onClose();
}, 1000);
} catch (error) {
console.error("Error updating subscription:", error);
setError(
error instanceof Error && typeof error.message === "string"
? error.message
: "An unexpected error occurred",
);
setTimeout(() => {
setError(null);
}, 1000);
}
};
const handleSubmitWrapper = (event: React.FormEvent) => {
handleSubmit(event).catch((error: unknown) => {
console.error("Error in handleSubmit:", error);
const handleDatePickerChange = (date: Date | null) => {
setValues({
...values,
expiryTime: date,
});
setIsDatePickerOpen(false);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
(async () => {
const token = getToken();
const url = `${apiOrigin}/admin/user/subscription`;
let expiryTime = null;
if (values.expiryTime instanceof Date) {
const utcExpiryTime = new Date(values.expiryTime);
expiryTime = utcExpiryTime.getTime() * 1000;
}
const body = {
userId: values.userId,
storage: values.storage * (1024 * 1024 * 1024),
expiryTime: expiryTime,
productId: values.productId,
paymentProvider: values.provider,
transactionId: values.transactionId,
};
try {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-AUTH-TOKEN": token,
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error("Network response was not ok");
}
console.log("Subscription updated successfully");
onClose();
} catch (error) {
console.error("Error updating subscription:", error);
}
})().catch((error: unknown) => {
console.error("Unhandled promise rejection:", error);
});
};
return (
<div className="update-subscription-popup">
<div className="popup-content">
<button className="close-button" onClick={onClose}>
X
</button>
<h2>Update Subscription</h2>
<form onSubmit={handleSubmitWrapper}>
<div className="form-group">
<label htmlFor="expiry-time">Expiry Time:</label>
<DatePicker
id="expiry-time"
selected={expiryTime}
onChange={(date) => setExpiryTime(date)}
dateFormat="dd/MM/yyyy"
showYearDropdown
scrollableYearDropdown
yearDropdownItemNumber={15}
/>
</div>
<div className="form-group">
<label htmlFor="product-id">Choose Your Plan:</label>
<select
id="product-id"
value={productId}
onChange={(e) => setProductId(e.target.value)}
<Dialog
open={open}
onClose={onClose}
BackdropProps={{
style: {
backdropFilter: "blur(5px)",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
}}
>
<DialogTitle style={{ marginBottom: "20px", marginTop: "20px" }}>
Update Subscription
<Button
onClick={onClose}
style={{ position: "absolute", right: 10, top: 10 }}
>
<CloseIcon style={{ color: "black" }} />
</Button>
</DialogTitle>
<DialogContent>
<form onSubmit={handleSubmit}>
<Grid container spacing={4}>
<Grid item xs={6}>
<div style={{ marginBottom: "8px" }}>
<label
htmlFor="productId"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Product ID
</label>
<TextField
id="productId"
name="productId"
value={values.productId}
onChange={handleChange}
fullWidth
/>
</div>
</Grid>
<Grid item xs={6}>
<div style={{ marginBottom: "8px" }}>
<label
htmlFor="provider"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Provider
</label>
<Select
id="provider"
name="provider"
value={values.provider}
onChange={handleChangeProvider}
fullWidth
style={{ textAlign: "left" }}
>
<MenuItem value="stripe">Stripe</MenuItem>
<MenuItem value="paypal">PayPal</MenuItem>
<MenuItem value="bitpay">BitPay</MenuItem>
<MenuItem value="None">None</MenuItem>
</Select>
</div>
</Grid>
<Grid item xs={6}>
<div style={{ marginBottom: "8px" }}>
<label
htmlFor="storage"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Storage (GB)
</label>
<TextField
id="storage"
name="storage"
type="number"
value={values.storage}
onChange={handleChange}
fullWidth
/>
</div>
</Grid>
<Grid item xs={6}>
<div style={{ marginBottom: "8px" }}>
<label
htmlFor="transactionId"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Transaction ID
</label>
<TextField
id="transactionId"
name="transactionId"
value={values.transactionId}
onChange={handleChange}
fullWidth
/>
</div>
</Grid>
<Grid item xs={6}>
<div style={{ marginBottom: "8px" }}>
<label
htmlFor="expiryTime"
style={{
textAlign: "left",
display: "block",
marginBottom: "4px",
}}
>
Expiry Time
</label>
<TextField
id="expiryTime"
name="expiryTime"
value={
values.expiryTime instanceof Date
? values.expiryTime.toLocaleDateString(
"en-GB",
)
: ""
}
onClick={handleCalendarClick}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<CalendarTodayIcon />
</InputAdornment>
),
readOnly: true,
}}
fullWidth
/>
{isDatePickerOpen && (
<DatePicker
showYearDropdown
scrollableYearDropdown
yearDropdownItemNumber={15}
selected={
values.expiryTime instanceof Date
? values.expiryTime
: null
}
onChange={handleDatePickerChange}
onClickOutside={() =>
setIsDatePickerOpen(false)
}
withPortal
inline
/>
)}
</div>
</Grid>
</Grid>
<DialogActions style={{ justifyContent: "center" }}>
<Button
type="submit"
variant="contained"
style={{
backgroundColor: "#00B33C",
color: "white",
}}
>
<option value="50gb_monthly">50GB/Month</option>
<option value="50gb_yearly">50GB/Year</option>
<option value="200gb_monthly">200GB/Month</option>
<option value="200gb_yearly">200GB/Year</option>
<option value="500gb_monthly">500GB/Month</option>
<option value="500gb_yearly">500GB/Year</option>
<option value="2000gb_monthly">2000GB/Month</option>
<option value="2000gb_yearly">2000GB/Year</option>
</select>
</div>
<div className="form-group">
<label htmlFor="payment-provider">
Payment Provider:
</label>
<select
id="payment-provider"
value={paymentProvider}
onChange={(e) => setPaymentProvider(e.target.value)}
>
<option value="bitpay">BitPay</option>
<option value="paypal">PayPal</option>
</select>
</div>
<div className="form-group">
<label htmlFor="transaction-id">Transaction ID:</label>
<input
id="transaction-id"
type="text"
value={transactionId}
onChange={(e) => setTransactionId(e.target.value)}
/>
</div>
<button type="submit" id="submitbtn">
Update
</button>
Update
</Button>
</DialogActions>
</form>
{(error ?? message) && (
<div className={`message ${error ? "error" : "success"}`}>
{error ? `Error: ${error}` : `Success: ${message}`}
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,369 @@
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import Switch from "@mui/material/Switch";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import * as React from "react";
import ChangeEmail from "./ChangeEmail";
import DeleteAccount from "./DeleteAccont";
import Disable2FA from "./Disable2FA";
import DisablePasskeys from "./DisablePasskeys";
import UpdateSubscription from "./UpdateSubscription";
export interface UserData {
User: Record<string, string>;
Storage: Record<string, string>;
Subscription: Record<string, string>;
Security: Record<string, string>;
}
interface UserComponentProps {
userData: UserData | null;
}
const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
const [disable2FAOpen, setDisable2FAOpen] = React.useState(false);
const [twoFactorEnabled, setTwoFactorEnabled] = React.useState(false);
const [is2FADisabled, setIs2FADisabled] = React.useState(false);
const [updateSubscriptionOpen, setUpdateSubscriptionOpen] =
React.useState(false);
const [changeEmailOpen, setChangeEmailOpen] = React.useState(false); // State for ChangeEmail dialog
const [DisablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false);
React.useEffect(() => {
if (userData?.Security["Two factor 2FA"] === "Enabled") {
setTwoFactorEnabled(true);
} else {
setTwoFactorEnabled(false);
}
}, [userData]);
const handleEditEmail = () => {
console.log("Edit Email clicked");
setChangeEmailOpen(true);
};
const handleCloseChangeEmail = () => {
setChangeEmailOpen(false); // Close ChangeEmail dialog
};
const handleDeleteAccountClick = () => {
setDeleteAccountOpen(true);
};
const handleCloseDeleteAccount = () => {
setDeleteAccountOpen(false);
};
const handleOpenDisable2FA = () => {
setDisable2FAOpen(true);
};
const handleCloseDisable2FA = () => {
setDisable2FAOpen(false);
};
const handleDisable2FA = () => {
setIs2FADisabled(true);
};
const handleCancelDisable2FA = () => {
setTwoFactorEnabled(true);
handleCloseDisable2FA();
};
const handleEditSubscription = () => {
setUpdateSubscriptionOpen(true);
};
const handleCloseUpdateSubscription = () => {
setUpdateSubscriptionOpen(false);
};
const handleOpenDisablePasskeys = () => {
setDisablePasskeysOpen(true); // Open the CloseFamily dialog
};
const handleCloseDisablePasskeys = () => {
setDisablePasskeysOpen(false); // Close the CloseFamily dialog
};
const handleDisablePasskeys = () => {
// Implement your logic to close family here
console.log("Close family action");
handleOpenDisablePasskeys(); // Open CloseFamily dialog after closing family
};
if (!userData) {
return null;
}
return (
<Grid container spacing={6} justifyContent="center">
{Object.entries(userData).map(([title, data]) => (
<Grid item xs={12} sm={10} md={6} key={title}>
<TableContainer
component={Paper}
variant="outlined"
sx={{
backgroundColor: "#F1F1F3",
minHeight: 300,
display: "flex",
flexDirection: "column",
marginBottom: "20px",
height: "100%",
width: "100%",
padding: "13px",
"&:not(:last-child)": {
marginBottom: "40px",
},
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "16px",
width: "100%",
}}
>
<Typography
variant="h6"
component="div"
sx={{
fontWeight: "bold",
textAlign: "center",
width: "100%",
}}
>
{title}
</Typography>
{title === "User" && (
<IconButton
edge="start"
aria-label="delete"
onClick={handleDeleteAccountClick}
>
<DeleteIcon style={{ color: "" }} />
</IconButton>
)}
{title === "Subscription" && (
<IconButton
edge="end"
aria-label="edit"
onClick={handleEditSubscription}
>
<EditIcon style={{ color: "black" }} />
</IconButton>
)}
</Box>
<Table
sx={{
width: "100%",
tableLayout: "fixed",
height: "100%",
borderBottom: "none",
}}
aria-label={title}
>
<TableBody>
{Object.entries(
data as Record<string, string>,
).map(([label, value], index) => (
<TableRow key={label}>
<TableCell
component="th"
scope="row"
style={{
padding: "16px",
borderBottom:
index === 1 || index === 0
? "1px solid rgba(224, 224, 224, 1)"
: "none",
}}
>
{label}
</TableCell>
<TableCell
align="right"
style={{
padding: "10px",
borderBottom:
index === 1 || index === 0
? "1px solid rgba(224, 224, 224, 1)"
: "none",
}}
>
{label === "Email" ? (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent:
"flex-end",
}}
>
<Typography>
{value}
</Typography>
<IconButton
edge="end"
aria-label="edit-email"
onClick={
handleEditEmail
}
>
<EditIcon
style={{
color: "black",
}}
/>
</IconButton>
</Box>
) : label === "Passkeys" ? (
<Button
variant="outlined"
onClick={
handleDisablePasskeys
}
>
Disable Passkeys
</Button>
) : typeof value === "string" ? (
label === "Two factor 2FA" ? (
is2FADisabled ||
value === "Disabled" ? (
<Typography
sx={{
textAlign:
"center",
width: "100%",
paddingLeft:
"30px",
}}
>
{value}
</Typography>
) : (
<Box
sx={{
display: "flex",
alignItems:
"center",
justifyContent:
"center",
width: "100%",
}}
>
<Typography>
{value}
</Typography>
{value ===
"Enabled" && (
<Switch
checked={
twoFactorEnabled
}
onChange={(
e,
) => {
const isChecked =
e
.target
.checked;
setTwoFactorEnabled(
isChecked,
);
if (
!isChecked
) {
handleOpenDisable2FA();
}
}}
sx={{
"& .MuiSwitch-switchBase.Mui-checked":
{
color: "#00B33C",
"&:hover":
{
backgroundColor:
"rgba(0, 179, 60, 0.08)",
},
},
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track":
{
backgroundColor:
"#00B33C",
},
}}
/>
)}
</Box>
)
) : (
<Typography>
{value}
</Typography>
)
) : (
<Typography>
{String(value)}
</Typography>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
))}
{/* Render DeleteAccount dialog */}
<DeleteAccount
open={deleteAccountOpen}
handleClose={handleCloseDeleteAccount}
/>
{/* Render Disable2FA dialog */}
<Disable2FA
open={disable2FAOpen}
handleClose={handleCancelDisable2FA}
handleDisable2FA={handleDisable2FA}
/>
{/* Render UpdateSubscription dialog */}
<UpdateSubscription
open={updateSubscriptionOpen}
onClose={handleCloseUpdateSubscription}
/>
{/* Render ChangeEmail dialog */}
<ChangeEmail
open={changeEmailOpen}
onClose={handleCloseChangeEmail}
/>
{/* Render Passkeys Dialog */}
<DisablePasskeys
open={DisablePasskeysOpen}
handleClose={handleCloseDisablePasskeys}
handleDisablePasskeys={handleCloseDisablePasskeys}
/>
</Grid>
);
};
export default UserComponent;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
import App from "./App";
import "./styles/globals.css";
const root = document.getElementById("root");