diff --git a/.gitignore b/.gitignore index 50222648..c46b39c1 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,3 @@ yarn-error.log* next-env.d.ts .env - -# Sentry Auth Token -.sentryclirc diff --git a/README.md b/README.md index 776076d6..a987d0f0 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ The project uses a variety of dependencies for different purposes: - **[Material-UI](https://mui.com/) and [Emotion](https://emotion.sh/)**: For UI components and styling. - **[Highcharts](https://www.highcharts.com/)**: For data visualization. - **[Axios](https://axios-http.com/)**: For making HTTP requests. -- **[Sentry](https://sentry.io/welcome/)**: For error tracking and monitoring. - **[Zustand](https://github.com/pmndrs/zustand)**: For state management. And many others that enhance the functionality and performance of the application. diff --git a/next.config.js b/next.config.js index 8e77c481..999eb343 100644 --- a/next.config.js +++ b/next.config.js @@ -12,41 +12,5 @@ const nextConfig = { module.exports = nextConfig -// Inected Content via Sentry Wizard Below -const { withSentryConfig } = require("@sentry/nextjs"); -module.exports = withSentryConfig( - module.exports, - { - // For all available options, see: - // https://github.com/getsentry/sentry-webpack-plugin#options - - // Suppresses source map uploading logs during build - silent: true, - - url: process.env.NEXT_PUBLIC_SENTRY_URL, - authToken: process.env.SENTRY_TOKEN, - org: process.env.NEXT_PUBLIC_ORG_NAME, - project: process.env.NEXT_PUBLIC_PROJECT_NAME, - }, - { - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, - - // Transpiles SDK to be compatible with IE11 (increases bundle size) - transpileClientSDK: true, - - // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) - tunnelRoute: "/monitoring", - - // Hides source maps from generated client bundles - hideSourceMaps: true, - - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - } -); diff --git a/package-lock.json b/package-lock.json index 18367f7c..4c8d3d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@amplitude/analytics-browser": "^1.9.4", + "@date-io/date-fns": "^2.17.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fortawesome/fontawesome-svg-core": "^6.2.0", @@ -19,7 +20,7 @@ "@hassanmojab/react-modern-calendar-datepicker": "^3.1.7", "@mui/lab": "^5.0.0-alpha.121", "@mui/material": "^5.10.13", - "@sentry/nextjs": "^7.50.0", + "@mui/x-date-pickers": "^6.18.6", "@types/node": "18.11.9", "@types/react": "18.0.25", "@types/react-dom": "18.0.8", @@ -30,6 +31,7 @@ "axios": "^1.2.2", "clsx": "^1.2.1", "d3-force": "^3.0.0", + "date-fns": "^2.30.0", "eslint": "8.27.0", "eslint-config-next": "13.0.2", "eslint-config-prettier": "^8.6.0", @@ -38,6 +40,7 @@ "highcharts": "^10.3.1", "highcharts-react-official": "^3.1.0", "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", "moment": "^2.29.4", "moment-timezone": "^0.5.38", "next": "13.0.2", @@ -64,6 +67,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/d3-force": "^3.0.4", + "@types/lodash": "^4.14.202", "@types/papaparse": "^5.3.8", "@types/testing-library__user-event": "^4.2.0", "autoprefixer": "^10.4.13", @@ -600,11 +604,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -662,6 +666,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@date-io/core": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.17.0.tgz", + "integrity": "sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==" + }, + "node_modules/@date-io/date-fns": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.17.0.tgz", + "integrity": "sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==", + "dependencies": { + "@date-io/core": "^2.17.0" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + } + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -844,18 +869,39 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.1.tgz", - "integrity": "sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } }, "node_modules/@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "dependencies": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz", @@ -2077,11 +2123,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", - "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2090,13 +2136,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz", - "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==", + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.2.tgz", + "integrity": "sha512-6dGM9/guFKBlFRHA7/mbM+E7wE7CYDy9Ny4JLtD3J+NTyhi8nd8YxlzgAgTaTVqY0BpdQ2zdfB/q6+p2EdGM0w==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^16.7.1 || ^17.0.0", + "@babel/runtime": "^7.23.6", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -2105,10 +2150,120 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "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/x-date-pickers": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.6.tgz", + "integrity": "sha512-pqOrGPUDVY/1xXrM1hofqwgquno/SB9aG9CVS1m2Rs8hKF1VWRC+jYlEa1Qk08xKmvkia5g7NsdV/BBb+tHUZw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.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 + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/base": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.29.tgz", + "integrity": "sha512-OXfUssYrB6ch/xpBVHMKAjThPlI9VyGGKdvQLMXef2j39wXfcxPlUVQlwia/lmE3rxWIGvbwkZsDtNYzLMsDUg==", + "dependencies": { + "@babel/runtime": "^7.23.6", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.2", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.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/x-date-pickers/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" } }, "node_modules/@next/env": { @@ -2352,9 +2507,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2368,384 +2523,11 @@ "node": ">=14" } }, - "node_modules/@rollup/plugin-commonjs": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz", - "integrity": "sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "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" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, - "node_modules/@sentry-internal/tracing": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.50.0.tgz", - "integrity": "sha512-4TQ4vN0aMBWsUXfJWk2xbe4x7fKfwCXgXKTtHC/ocwwKM+0EefV5Iw9YFG8IrIQN4vMtuRzktqcs9q0/Sbv7tg==", - "dependencies": { - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry-internal/tracing/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/browser": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.50.0.tgz", - "integrity": "sha512-a+UYbP89+SAvW47/p9wxEi9eWlyp/SkYl52OCdZNXnplQY4kQIOVyiaIs5nnCxIxZgXKrhAX4eo1E9ykleFuNQ==", - "dependencies": { - "@sentry-internal/tracing": "7.50.0", - "@sentry/core": "7.50.0", - "@sentry/replay": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/cli": { - "version": "1.75.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.75.2.tgz", - "integrity": "sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw==", - "hasInstallScript": true, - "dependencies": { - "https-proxy-agent": "^5.0.0", - "mkdirp": "^0.5.5", - "node-fetch": "^2.6.7", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@sentry/core": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.50.0.tgz", - "integrity": "sha512-6oD1a3fYs4aiNK7tuJSd88LHjYJAetd7ZK/AfJniU7zWKj4jxIYfO8nhm0qdnhEDs81RcweVDmPhWm3Kwrzzsg==", - "dependencies": { - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/integrations": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.50.0.tgz", - "integrity": "sha512-HUmPN2sHNx37m+lOWIoCILHimILdI0Df9nGmWA13fIhny8mxJ6Dbazyis11AW4/lrZ1a6F1SQ2epLEq7ZesiRw==", - "dependencies": { - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "localforage": "^1.8.1", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/integrations/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/nextjs": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-7.50.0.tgz", - "integrity": "sha512-4/utwzYIZmjJ/QyYlez1H8PxrNThYBY7MdqzL7XM+Nj432mlyWEDHKYri1FM2GX3HSM4l9lLrpExTkDNVO6dBQ==", - "dependencies": { - "@rollup/plugin-commonjs": "24.0.0", - "@sentry/core": "7.50.0", - "@sentry/integrations": "7.50.0", - "@sentry/node": "7.50.0", - "@sentry/react": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "@sentry/webpack-plugin": "1.20.0", - "chalk": "3.0.0", - "rollup": "2.78.0", - "stacktrace-parser": "^0.1.10", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "next": "^10.0.8 || ^11.0 || ^12.0 || ^13.0", - "react": "16.x || 17.x || 18.x", - "webpack": ">= 4.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/@sentry/nextjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@sentry/nextjs/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/nextjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@sentry/nextjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@sentry/nextjs/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/nextjs/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/nextjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/node": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.50.0.tgz", - "integrity": "sha512-11UJBKoQFMp7f8sbzeO2gENsKIUkVCNBTzuPRib7l2K1HMjSfacXmwwma7ZEs0mc3ofIZ1UYuyONAXmI1lK9cQ==", - "dependencies": { - "@sentry-internal/tracing": "7.50.0", - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/node/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/react": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.50.0.tgz", - "integrity": "sha512-V/KfIhwLezefnRz0y9pGJn5x0RBL8Q1347LowcOZWoNiDoaaLI9hRBTqJGyvCstG5NNhsLTKMM3UDk0WNXflPg==", - "dependencies": { - "@sentry/browser": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": "15.x || 16.x || 17.x || 18.x" - } - }, - "node_modules/@sentry/react/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/replay": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.50.0.tgz", - "integrity": "sha512-EYRk+DTZ5luwfkiCaDpBC3YBKIEdkReTUNZtWDVUytSVjsCnttkAipx/y6bxy3HN+rSXungMd3XKQT5RNMRUNA==", - "dependencies": { - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry/types": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.50.0.tgz", - "integrity": "sha512-Zo9vyI98QNeYT0K0y57Rb4JRWDaPEgmp+QkQ4CRQZFUTWetO5fvPZ4Gb/R7TW16LajuHZlbJBHmvmNj2pkL2kw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.50.0.tgz", - "integrity": "sha512-iyPwwC6fwJsiPhH27ZbIiSsY5RaccHBqADS2zEjgKYhmP4P9WGgHRDrvLEnkOjqQyKNb6c0yfmv83n0uxYnolw==", - "dependencies": { - "@sentry/types": "7.50.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/@sentry/webpack-plugin": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz", - "integrity": "sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw==", - "dependencies": { - "@sentry/cli": "^1.74.6", - "webpack-sources": "^2.0.0 || ^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -3054,11 +2836,6 @@ "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==", "dev": true }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" - }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -3149,6 +2926,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -3175,9 +2958,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { "version": "18.0.25", @@ -3197,18 +2980,10 @@ "@types/react": "*" } }, - "node_modules/@types/react-is": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", - "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "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==", "dependencies": { "@types/react": "*" } @@ -3724,6 +3499,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "dependencies": { "debug": "4" }, @@ -4540,11 +4316,6 @@ "node": ">= 0.8" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4555,14 +4326,6 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4917,6 +4680,21 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5947,11 +5725,6 @@ "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==" - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6243,6 +6016,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -6598,6 +6372,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -6666,11 +6441,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -7001,14 +6771,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9253,14 +9015,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -9290,14 +9044,6 @@ "xtend": "^4.0.0" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "dependencies": { - "lie": "3.1.1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9315,8 +9061,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", @@ -9339,11 +9084,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9361,17 +9101,6 @@ "lz-string": "bin/bin.js" } }, - "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -9526,17 +9255,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -9718,25 +9436,6 @@ "nice-color-palettes": "bin/index.js" } }, - "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10412,14 +10111,6 @@ "node": ">= 0.6.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise-polyfill": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-3.1.0.tgz", @@ -10825,9 +10516,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "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.4.3", @@ -10948,20 +10639,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "2.78.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.0.tgz", - "integrity": "sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11189,25 +10866,6 @@ "node": ">=8" } }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "engines": { - "node": ">=8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -11623,11 +11281,6 @@ "node": ">=6" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -11863,19 +11516,6 @@ "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webvr-polyfill": { "version": "0.10.12", "resolved": "https://registry.npmjs.org/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz", @@ -11905,18 +11545,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "dev": true, + "engines": { + "node": ">=12" } }, "node_modules/which": { @@ -12630,11 +12261,11 @@ } }, "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -12680,6 +12311,19 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@date-io/core": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.17.0.tgz", + "integrity": "sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==" + }, + "@date-io/date-fns": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.17.0.tgz", + "integrity": "sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==", + "requires": { + "@date-io/core": "^2.17.0" + } + }, "@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -12822,18 +12466,35 @@ } }, "@floating-ui/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.1.tgz", - "integrity": "sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "requires": { + "@floating-ui/utils": "^0.1.3" + } }, "@floating-ui/dom": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.1.tgz", - "integrity": "sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "requires": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", "requires": { - "@floating-ui/core": "^1.2.1" + "@floating-ui/dom": "^1.5.1" } }, + "@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "@fortawesome/fontawesome-common-types": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz", @@ -13661,23 +13322,57 @@ } }, "@mui/types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", - "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", "requires": {} }, "@mui/utils": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.11.tgz", - "integrity": "sha512-neMM5rrEXYQrOrlxUfns/TGgX4viS8K2zb9pbQh11/oUUYFlGI32Tn+PHePQx7n6Fy/0zq6WxdBFC9VpnJ5JrQ==", + "version": "5.15.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.2.tgz", + "integrity": "sha512-6dGM9/guFKBlFRHA7/mbM+E7wE7CYDy9Ny4JLtD3J+NTyhi8nd8YxlzgAgTaTVqY0BpdQ2zdfB/q6+p2EdGM0w==", "requires": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^16.7.1 || ^17.0.0", + "@babel/runtime": "^7.23.6", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" } }, + "@mui/x-date-pickers": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.6.tgz", + "integrity": "sha512-pqOrGPUDVY/1xXrM1hofqwgquno/SB9aG9CVS1m2Rs8hKF1VWRC+jYlEa1Qk08xKmvkia5g7NsdV/BBb+tHUZw==", + "requires": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "@mui/base": { + "version": "5.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.29.tgz", + "integrity": "sha512-OXfUssYrB6ch/xpBVHMKAjThPlI9VyGGKdvQLMXef2j39wXfcxPlUVQlwia/lmE3rxWIGvbwkZsDtNYzLMsDUg==", + "requires": { + "@babel/runtime": "^7.23.6", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.2", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + } + }, + "clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" + } + } + }, "@next/env": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.2.tgz", @@ -13793,309 +13488,20 @@ } }, "@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@remix-run/router": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==" }, - "@rollup/plugin-commonjs": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz", - "integrity": "sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==", - "requires": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" }, - "@sentry-internal/tracing": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.50.0.tgz", - "integrity": "sha512-4TQ4vN0aMBWsUXfJWk2xbe4x7fKfwCXgXKTtHC/ocwwKM+0EefV5Iw9YFG8IrIQN4vMtuRzktqcs9q0/Sbv7tg==", - "requires": { - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/browser": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.50.0.tgz", - "integrity": "sha512-a+UYbP89+SAvW47/p9wxEi9eWlyp/SkYl52OCdZNXnplQY4kQIOVyiaIs5nnCxIxZgXKrhAX4eo1E9ykleFuNQ==", - "requires": { - "@sentry-internal/tracing": "7.50.0", - "@sentry/core": "7.50.0", - "@sentry/replay": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/cli": { - "version": "1.75.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.75.2.tgz", - "integrity": "sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw==", - "requires": { - "https-proxy-agent": "^5.0.0", - "mkdirp": "^0.5.5", - "node-fetch": "^2.6.7", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - } - }, - "@sentry/core": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.50.0.tgz", - "integrity": "sha512-6oD1a3fYs4aiNK7tuJSd88LHjYJAetd7ZK/AfJniU7zWKj4jxIYfO8nhm0qdnhEDs81RcweVDmPhWm3Kwrzzsg==", - "requires": { - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/integrations": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.50.0.tgz", - "integrity": "sha512-HUmPN2sHNx37m+lOWIoCILHimILdI0Df9nGmWA13fIhny8mxJ6Dbazyis11AW4/lrZ1a6F1SQ2epLEq7ZesiRw==", - "requires": { - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "localforage": "^1.8.1", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/nextjs": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-7.50.0.tgz", - "integrity": "sha512-4/utwzYIZmjJ/QyYlez1H8PxrNThYBY7MdqzL7XM+Nj432mlyWEDHKYri1FM2GX3HSM4l9lLrpExTkDNVO6dBQ==", - "requires": { - "@rollup/plugin-commonjs": "24.0.0", - "@sentry/core": "7.50.0", - "@sentry/integrations": "7.50.0", - "@sentry/node": "7.50.0", - "@sentry/react": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "@sentry/webpack-plugin": "1.20.0", - "chalk": "3.0.0", - "rollup": "2.78.0", - "stacktrace-parser": "^0.1.10", - "tslib": "^1.9.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/node": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.50.0.tgz", - "integrity": "sha512-11UJBKoQFMp7f8sbzeO2gENsKIUkVCNBTzuPRib7l2K1HMjSfacXmwwma7ZEs0mc3ofIZ1UYuyONAXmI1lK9cQ==", - "requires": { - "@sentry-internal/tracing": "7.50.0", - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/react": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.50.0.tgz", - "integrity": "sha512-V/KfIhwLezefnRz0y9pGJn5x0RBL8Q1347LowcOZWoNiDoaaLI9hRBTqJGyvCstG5NNhsLTKMM3UDk0WNXflPg==", - "requires": { - "@sentry/browser": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/replay": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.50.0.tgz", - "integrity": "sha512-EYRk+DTZ5luwfkiCaDpBC3YBKIEdkReTUNZtWDVUytSVjsCnttkAipx/y6bxy3HN+rSXungMd3XKQT5RNMRUNA==", - "requires": { - "@sentry/core": "7.50.0", - "@sentry/types": "7.50.0", - "@sentry/utils": "7.50.0" - } - }, - "@sentry/types": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.50.0.tgz", - "integrity": "sha512-Zo9vyI98QNeYT0K0y57Rb4JRWDaPEgmp+QkQ4CRQZFUTWetO5fvPZ4Gb/R7TW16LajuHZlbJBHmvmNj2pkL2kw==" - }, - "@sentry/utils": { - "version": "7.50.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.50.0.tgz", - "integrity": "sha512-iyPwwC6fwJsiPhH27ZbIiSsY5RaccHBqADS2zEjgKYhmP4P9WGgHRDrvLEnkOjqQyKNb6c0yfmv83n0uxYnolw==", - "requires": { - "@sentry/types": "7.50.0", - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } - } - }, - "@sentry/webpack-plugin": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz", - "integrity": "sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw==", - "requires": { - "@sentry/cli": "^1.74.6", - "webpack-sources": "^2.0.0 || ^3.0.0" - } - }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -14345,11 +13751,6 @@ "integrity": "sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw==", "dev": true }, - "@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" - }, "@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -14433,6 +13834,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, "@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -14459,9 +13866,9 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "@types/react": { "version": "18.0.25", @@ -14481,18 +13888,10 @@ "@types/react": "*" } }, - "@types/react-is": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", - "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "requires": { - "@types/react": "*" - } - }, "@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "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==", "requires": { "@types/react": "*" } @@ -14863,6 +14262,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "requires": { "debug": "4" } @@ -15434,11 +14834,6 @@ "delayed-stream": "~1.0.0" } }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -15449,11 +14844,6 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, - "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" - }, "cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -15725,6 +15115,14 @@ } } }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -16486,11 +15884,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" }, - "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==" - }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -16707,6 +16100,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true }, "function-bind": { @@ -16958,6 +16352,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "requires": { "agent-base": "6", "debug": "4" @@ -16997,11 +16392,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, "immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -17226,14 +16616,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "requires": { - "@types/estree": "*" - } - }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -18891,14 +18273,6 @@ "type-check": "~0.4.0" } }, - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "requires": { - "immediate": "~3.0.5" - } - }, "lilconfig": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", @@ -18925,14 +18299,6 @@ "xtend": "^4.0.0" } }, - "localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "requires": { - "lie": "3.1.1" - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -18944,8 +18310,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.21", @@ -18965,11 +18330,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" - }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -18984,14 +18344,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true }, - "magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -19109,14 +18461,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -19243,14 +18587,6 @@ "xhr-request": "^1.0.1" } }, - "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -19710,11 +19046,6 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, "promise-polyfill": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-3.1.0.tgz", @@ -20008,9 +19339,9 @@ } }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { "version": "1.4.3", @@ -20090,14 +19421,6 @@ "glob": "^7.1.3" } }, - "rollup": { - "version": "2.78.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.0.tgz", - "integrity": "sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==", - "requires": { - "fsevents": "~2.3.2" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20265,21 +19588,6 @@ } } }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "requires": { - "type-fest": "^0.7.1" - }, - "dependencies": { - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" - } - } - }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -20598,11 +19906,6 @@ "url-parse": "^1.5.3" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -20774,16 +20077,6 @@ "makeerror": "1.0.12" } }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" - }, "webvr-polyfill": { "version": "0.10.12", "resolved": "https://registry.npmjs.org/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz", @@ -20812,15 +20105,6 @@ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 85d1b24e..85891dc1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@amplitude/analytics-browser": "^1.9.4", + "@date-io/date-fns": "^2.17.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@fortawesome/fontawesome-svg-core": "^6.2.0", @@ -23,7 +24,7 @@ "@hassanmojab/react-modern-calendar-datepicker": "^3.1.7", "@mui/lab": "^5.0.0-alpha.121", "@mui/material": "^5.10.13", - "@sentry/nextjs": "^7.50.0", + "@mui/x-date-pickers": "^6.18.6", "@types/node": "18.11.9", "@types/react": "18.0.25", "@types/react-dom": "18.0.8", @@ -34,6 +35,7 @@ "axios": "^1.2.2", "clsx": "^1.2.1", "d3-force": "^3.0.0", + "date-fns": "^2.30.0", "eslint": "8.27.0", "eslint-config-next": "13.0.2", "eslint-config-prettier": "^8.6.0", @@ -42,6 +44,7 @@ "highcharts": "^10.3.1", "highcharts-react-official": "^3.1.0", "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", "moment": "^2.29.4", "moment-timezone": "^0.5.38", "next": "13.0.2", @@ -68,6 +71,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/d3-force": "^3.0.4", + "@types/lodash": "^4.14.202", "@types/papaparse": "^5.3.8", "@types/testing-library__user-event": "^4.2.0", "autoprefixer": "^10.4.13", diff --git a/sentry.client.config.ts b/sentry.client.config.ts deleted file mode 100644 index 553f7531..00000000 --- a/sentry.client.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file configures the initialization of Sentry on the client. -// The config you add here will be used whenever a users loads a page in their browser. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from '@sentry/nextjs'; -import { conf } from './src/configs'; - -const SENTRY_DSN = conf.SENTRY_DSN; -const ENVIRONMENT = conf.SENTRY_ENV; - -Sentry.init({ - dsn: SENTRY_DSN, - environment: ENVIRONMENT, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: ENVIRONMENT === 'production' ? 0.1 : 1.0, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: ENVIRONMENT !== 'production', - - replaysOnErrorSampleRate: 1.0, - - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - new Sentry.Replay({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ], -}); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts deleted file mode 100644 index b3e0bda1..00000000 --- a/sentry.edge.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file configures the initialization of Sentry on the server. -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from '@sentry/nextjs'; -import { conf } from './src/configs'; - -const SENTRY_DSN = conf.SENTRY_DSN; -const ENVIRONMENT = process.env.NODE_ENV || 'staging'; - -Sentry.init({ - dsn: SENTRY_DSN, - environment: ENVIRONMENT, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: ENVIRONMENT === 'production' ? 0.1 : 1.0, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: ENVIRONMENT !== 'production', -}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts deleted file mode 100644 index 6f67d5ec..00000000 --- a/sentry.server.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file configures the initialization of Sentry on the server. -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from '@sentry/nextjs'; -import { conf } from './src/configs'; - -const SENTRY_DSN = conf.SENTRY_DSN; -const ENVIRONMENT = process.env.NODE_ENV || 'production'; - -Sentry.init({ - dsn: SENTRY_DSN, - environment: ENVIRONMENT, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: ENVIRONMENT === 'production' ? 0.1 : 1.0, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: ENVIRONMENT !== 'production', -}); diff --git a/src/axiosInstance.ts b/src/axiosInstance.ts index 4b118797..e1bf0f15 100644 --- a/src/axiosInstance.ts +++ b/src/axiosInstance.ts @@ -1,8 +1,6 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import { conf } from './configs/index'; import { StorageService } from './services/StorageService'; -import * as Sentry from '@sentry/nextjs'; - import { toast } from 'react-toastify'; import { IToken } from './utils/types'; import { tokenRefreshEventEmitter } from './services/EventEmitter'; @@ -149,11 +147,6 @@ axiosInstance.interceptors.response.use( draggable: true, progress: 0, }); - Sentry.captureException( - new Error( - `API responded with status code ${error.response.status}: ${error.response.data.message}` - ) - ); break; case 440: StorageService.removeLocalStorage('user'); @@ -168,12 +161,17 @@ axiosInstance.interceptors.response.use( progress: 0, }); window.location.href = '/'; - - Sentry.captureException( - new Error( - `API responded with status code ${error.response.status}: ${error.response.data.message}` - ) - ); + break; + case 500: + toast.error(`${error.response.data.message}`, { + position: 'bottom-left', + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: 0, + }); break; case 590: toast.error(`${error.response.data.message}`, { @@ -185,11 +183,6 @@ axiosInstance.interceptors.response.use( draggable: true, progress: 0, }); - Sentry.captureException( - new Error( - `API responded with status code ${error.response.status}: ${error.response.data.message}` - ) - ); break; default: diff --git a/src/components/announcements/TcAnnouncementsAlert.spec.tsx b/src/components/announcements/TcAnnouncementsAlert.spec.tsx new file mode 100644 index 00000000..2da01b59 --- /dev/null +++ b/src/components/announcements/TcAnnouncementsAlert.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcAnnouncementsAlert from './TcAnnouncementsAlert'; + +const mockGrantWritePermissions = jest.fn(); + +jest.mock('../../store/useStore', () => () => ({ + grantWritePermissions: mockGrantWritePermissions, +})); + +jest.mock('../../context/TokenContext', () => ({ + useToken: () => ({ + community: { + platforms: [ + { id: '1', disconnectedAt: null, metadata: { id: '3123141414221' } }, + ], + }, + }), +})); + +describe('TcAnnouncementsAlert', () => { + it('renders correctly', () => { + render(); + expect( + screen.getByText(/Announcements needs write access at the server-level/i) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/TcAnnouncementsAlert.tsx b/src/components/announcements/TcAnnouncementsAlert.tsx new file mode 100644 index 00000000..be5f9b7b --- /dev/null +++ b/src/components/announcements/TcAnnouncementsAlert.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import TcAlert from '../shared/TcAlert'; +import TcCollapse from '../shared/TcCollapse'; +import TcText from '../shared/TcText'; +import TcButton from '../shared/TcButton'; +import useAppStore from '../../store/useStore'; +import { useToken } from '../../context/TokenContext'; + +function TcAnnouncementsAlert() { + const { grantWritePermissions } = useAppStore(); + + const { community } = useToken(); + + const handleGrantAccess = () => { + const guildId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.metadata.id; + + if (guildId) + grantWritePermissions({ + platformType: 'discord', + moduleType: 'Announcement', + id: guildId, + }); + }; + return ( + + +
+ + +
+
+
+ ); +} + +export default TcAnnouncementsAlert; diff --git a/src/components/announcements/TcAnnouncementsTable.tsx b/src/components/announcements/TcAnnouncementsTable.tsx new file mode 100644 index 00000000..0fec5768 --- /dev/null +++ b/src/components/announcements/TcAnnouncementsTable.tsx @@ -0,0 +1,540 @@ +import React, { useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + IconButton, + MenuItem, + Menu, + Chip, +} from '@mui/material'; +import { BsThreeDotsVertical } from 'react-icons/bs'; +import Router from 'next/router'; +import TcDialog from '../shared/TcDialog'; +import TcButton from '../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../shared/TcText'; +import useAppStore from '../../store/useStore'; +import { useSnackbar } from '../../context/SnackbarContext'; +import { MdModeEdit, MdDelete } from 'react-icons/md'; +import Loading from '../global/Loading'; +import { truncateCenter } from '../../helpers/helper'; +import moment from 'moment'; + +interface Channel { + channelId: string; + name: string; +} + +interface User { + discordId: string; + ngu: string; +} + +interface Role { + roleId: string; + color: string; + name: string; +} + +interface AnnouncementData { + platform: string; + template: string; + type: 'discord_public' | 'discord_private'; + options: { + channels: Channel[]; + users?: User[]; + roles?: Role[]; + }; +} + +interface Announcement { + id: string; + title: string; + scheduledAt: string; + draft: boolean; + data: AnnouncementData[]; + community: string; +} + +interface AnnouncementsTableProps { + announcements: Announcement[]; + isLoading: boolean; + selectedZone: string; + handleRefreshList: () => void; +} + +function TcAnnouncementsTable({ + announcements, + isLoading, + selectedZone, + handleRefreshList, +}: AnnouncementsTableProps) { + const { deleteAnnouncements } = useAppStore(); + const { showMessage } = useSnackbar(); + const [anchorEl, setAnchorEl] = useState(null); + const [deleteConfirmDialogOpen, setDeleteConfirmDialogOpen] = + useState(false); + const [selectedAnnouncementId, setSelectedAnnouncementId] = useState< + string | null + >(null); + + const formatDateBasedOnTimezone = ( + date: string | number | Date, + timezone: string + ) => { + return moment(date).tz(timezone).format('DD/MM/YYYY, h:mm:ss a'); + }; + + const handleClick = ( + event: React.MouseEvent, + id: string + ) => { + setAnchorEl(event.currentTarget); + setSelectedAnnouncementId(id); + }; + + const handleClose = () => { + setAnchorEl(null); + setSelectedAnnouncementId(null); + }; + + const handleEdit = (id: string) => { + Router.push(`/announcements/edit-announcements/?announcementsId=${id}`); + handleClose(); + }; + + const handleDelete = () => { + setDeleteConfirmDialogOpen(true); + }; + + const handleDeleteAnnouncements = async (id: string) => { + try { + await deleteAnnouncements(id); + handleRefreshList(); + showMessage('Scheduled announcement removed successfully.', 'success'); + } catch (error) { + console.error('Error deleting announcement:', error); + showMessage('Error occurred while deleting the announcement.', 'error'); + } finally { + setDeleteConfirmDialogOpen(false); + setSelectedAnnouncementId(null); + } + }; + + const getAnnouncementTypeLabel = (type: string) => { + if (type === 'discord_public') { + return 'Public'; + } else if (type === 'discord_private') { + return 'Private'; + } + return 'Unknown'; + }; + + const renderTableCell = ( + announcement: { + draft: any; + data: any[]; + scheduledAt: string | number | Date; + id: string; + }, + cellType: any + ) => { + switch (cellType) { + case 'title': + return ( +
+ + + {announcement && + announcement.data && + announcement.data[0] && + announcement.data[0].template + ? truncateCenter(announcement.data[0]?.template, 20) + : ''} + + } + variant="subtitle2" + /> + + {announcement.data + .reduce((unique: string[], item: AnnouncementData) => { + const itemType = item.type; + if (!unique.includes(itemType)) { + unique.push(itemType); + } + return unique; + }, [] as string[]) + .map((type: string, index: React.Key | null | undefined) => ( + + + {getAnnouncementTypeLabel(type)} +
+ } + size="small" + sx={{ + borderRadius: '4px', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', + }} + /> + ))} + + + ); + case 'channels': + return ( +
+ { + const channels = item.options.channels; + if (channels && channels.length > 0) { + const displayedChannels = channels + .slice(0, 2) + .map((channel: { name: any }) => `#${channel.name}`) + .join(', '); + const moreChannelsIndicator = + channels.length > 2 ? '...' : ''; + return dataIndex > 0 + ? `, ${displayedChannels}${moreChannelsIndicator}` + : `${displayedChannels}${moreChannelsIndicator}`; + } + return ''; + } + ) + .filter((text: string) => text !== '') + .join('')} + variant="subtitle2" + /> +
+ ); + // case 'users': + // return ( + //
+ // { + // const users = item.options.users; + // if (users && users.length > 0) { + // const displayedUsers = users + // .slice(0, 2) + // .map((user: { ngu: any }) => `@${user.ngu}`) + // .join(', '); + // const moreUsersIndicator = users.length > 2 ? '...' : ''; + // return `${displayedUsers}${moreUsersIndicator}`; + // } + // return ''; + // }) + // .filter((text: string) => text !== '') + // .join(', ')} + // variant="subtitle2" + // /> + //
+ // ); + // case 'roles': + // return ( + //
+ // { + // const roles = item.options.roles; + // if (roles && roles.length > 0) { + // const displayedRoles = roles + // .slice(0, 2) + // .map((role: { name: any }) => role.name) + // .join(', '); + // const moreRolesIndicator = roles.length > 2 ? '...' : ''; + // return `${displayedRoles}${moreRolesIndicator}`; + // } + // return ''; + // }) + // .filter((text: string) => text !== '') + // .join(', ')} + // variant="subtitle2" + // /> + //
+ // ); + case 'scheduledAt': + return ( +
+ +
+ ); + case 'actions': + return ( + <> + handleClick(event, announcement.id)} + > + + + + handleEdit(announcement.id)}> + + Edit + + + + Delete + + + + ); + default: + return null; + } + }; + + const renderTableBody = () => { + if (isLoading) { + return ( + + + + + + + + ); + } + + return ( + + {announcements.map((announcement, index) => ( + + {['title', 'channels', 'scheduledAt', 'actions'].map( + (cellType, cellIndex, array) => ( + + {renderTableCell(announcement, cellType)} + + ) + )} + + ))} + + ); + }; + + return ( + <> + + + + + + + + + + {/* + + */} + {/* + + */} + + + + + + + {renderTableBody()} +
+ +
+ setDeleteConfirmDialogOpen(false)} + /> +
+
+ +
+ +
+ setDeleteConfirmDialogOpen(false)} + /> + + selectedAnnouncementId && + handleDeleteAnnouncements(selectedAnnouncementId) + } + /> +
+
+
+ + } + open={deleteConfirmDialogOpen} + /> + + ); +} + +export default TcAnnouncementsTable; diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx new file mode 100644 index 00000000..e3e0ce30 --- /dev/null +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TcConfirmSchaduledAnnouncementsDialog from './TcConfirmSchaduledAnnouncementsDialog'; + +const mockHandleCreateAnnouncements = jest.fn(); + +const defaultProps = { + buttonLabel: 'Select Date for Announcement', + selectedChannels: [{ id: '1', label: 'General' }], + schaduledDate: '2024-01-20T12:00:00', + isDisabled: false, + handleCreateAnnouncements: mockHandleCreateAnnouncements, +}; + +test('renders the dialog with button and calls handleCreateAnnouncements when confirmed', () => { + const { getByText, getByTestId } = render( + + ); + + const button = getByText('Select Date for Announcement'); + expect(button).toBeInTheDocument(); + + fireEvent.click(button); + + const dialogTitle = getByText('Confirm Schedule'); + expect(dialogTitle).toBeInTheDocument(); + + const confirmButton = getByText('Confirm'); + expect(confirmButton).toBeInTheDocument(); + fireEvent.click(confirmButton); + + expect(mockHandleCreateAnnouncements).toHaveBeenCalledWith(false); +}); diff --git a/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx new file mode 100644 index 00000000..3e06c7ac --- /dev/null +++ b/src/components/announcements/TcConfirmSchaduledAnnouncementsDialog.tsx @@ -0,0 +1,178 @@ +import React, { useState } from 'react'; +import TcButton from '../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcDialog from '../shared/TcDialog'; +import TcText from '../shared/TcText'; +import { FaDiscord } from 'react-icons/fa6'; +import moment from 'moment'; +import { IRoles, IUser } from '../../utils/interfaces'; + +interface ITcConfirmSchaduledAnnouncementsDialogProps { + buttonLabel: string; + selectedChannels: { id: string; label: string }[]; + selectedRoles?: IRoles[]; + selectedUsernames?: IUser[]; + schaduledDate: string; + isDisabled: boolean; + handleCreateAnnouncements: (isDrafted: boolean) => void; +} + +const formatDateToLocalTimezone = (scheduledDate: string) => { + if (!scheduledDate) { + console.error('Scheduled date is undefined or null'); + return 'Invalid Date'; + } + + const formattedDate = moment(scheduledDate).format('MMMM D [at] hh:mm A'); + + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + return `${formattedDate} (${timezone})`; +}; + +function TcConfirmSchaduledAnnouncementsDialog({ + buttonLabel, + schaduledDate, + selectedRoles, + selectedUsernames, + selectedChannels, + isDisabled = true, + handleCreateAnnouncements, +}: ITcConfirmSchaduledAnnouncementsDialogProps) { + const [confirmSchadulerDialog, setConfirmSchadulerDialog] = + useState(false); + + return ( + <> + setConfirmSchadulerDialog(true)} + /> + +
+ setConfirmSchadulerDialog(false)} + /> +
+
+ +
+
+ + +
+
+
+ + +
+ {selectedChannels + .map((channel) => `#${channel.label}`) + .join(', ')} +
+ {selectedUsernames && selectedUsernames.length > 0 ? ( +
+
+ + {' '} +
+ {selectedChannels + .map((channel) => `#${channel.label}`) + .join(', ')} +
+ ) : ( + '' + )} + {selectedRoles && selectedRoles.length > 0 ? ( +
+
+ + +
+ {selectedRoles.map((role) => `#${role.name}`).join(', ')} +
+ ) : ( + '' + )} +
+
+ { + setConfirmSchadulerDialog(false); + handleCreateAnnouncements(false); + }} + sx={{ width: '100%' }} + /> +
+
+ + } + open={confirmSchadulerDialog} + /> + + ); +} + +export default TcConfirmSchaduledAnnouncementsDialog; diff --git a/src/components/announcements/TcTimeZone.spec.tsx b/src/components/announcements/TcTimeZone.spec.tsx new file mode 100644 index 00000000..351de2bc --- /dev/null +++ b/src/components/announcements/TcTimeZone.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import TcTimeZone from './TcTimeZone'; + +test('should handle zone selection', async () => { + const handleZoneFunction = jest.fn(); + + const { getByTestId, getByLabelText } = render( + + ); + + const globeIcon = getByTestId('globe-icon'); + fireEvent.click(globeIcon); + + const searchInput = getByLabelText('Search timezone'); + expect(searchInput).toBeInTheDocument(); +}); diff --git a/src/components/announcements/TcTimeZone.tsx b/src/components/announcements/TcTimeZone.tsx new file mode 100644 index 00000000..b9fdc857 --- /dev/null +++ b/src/components/announcements/TcTimeZone.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import TcButton from '../shared/TcButton'; +import { FaGlobeAmericas } from 'react-icons/fa'; +import TcPopover from '../shared/TcPopover'; + +import momentTZ from 'moment-timezone'; +import moment from 'moment'; +import 'moment-timezone'; +import TcInput from '../shared/TcInput'; +import { InputAdornment } from '@mui/material'; +import { MdSearch } from 'react-icons/md'; + +const timeZonesList = momentTZ.tz.names(); + +interface ITcTimeZoneProps { + handleZone: (zone: string) => void; +} +function TcTimeZone({ handleZone }: ITcTimeZoneProps) { + const [activeZone, setActiveZone] = useState(moment.tz.guess()); + + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + setZones(timeZonesList); + }; + + const open = Boolean(anchorEl); + const id = open ? 'simple-popover' : undefined; + + const [zones, setZones] = useState(timeZonesList); + + const searchZones = (e: { target: { value: string } }) => { + const results = timeZonesList.filter((zone) => { + if (e.target.value === '') { + return timeZonesList; + } + return zone.toLowerCase().includes(e.target.value.toLowerCase()); + }); + setZones(results); + }; + + const handleTimeZoneSelect = (timeZone: string) => { + setActiveZone(timeZone); + setAnchorEl(null); + setZones(timeZonesList); + }; + + useEffect(() => { + handleZone(activeZone); + }, [activeZone]); + + return ( +
+ } + aria-describedby={id} + onClick={handleClick} + /> + +
+ + + + ), + }} + onChange={searchZones} + /> +
+
    + {zones.length > 0 ? ( + zones.map((el) => ( +
  • handleTimeZoneSelect(el)} + > +
    {el}
    +
  • + )) + ) : ( +
    + Not founded +
    + )} +
+
+ } + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + /> + + ); +} + +export default TcTimeZone; diff --git a/src/components/announcements/create/TcIconContainer.spec.tsx b/src/components/announcements/create/TcIconContainer.spec.tsx new file mode 100644 index 00000000..4c3f102b --- /dev/null +++ b/src/components/announcements/create/TcIconContainer.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcIconContainer from './TcIconContainer'; + +describe('TcIconContainer', () => { + it('renders its children', () => { + render( + +
Test Child
+
+ ); + expect(screen.getByText('Test Child')).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/TcIconContainer.tsx b/src/components/announcements/create/TcIconContainer.tsx new file mode 100644 index 00000000..4b5c68a0 --- /dev/null +++ b/src/components/announcements/create/TcIconContainer.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +/** + * Interface defining the properties for TcIconContainer. + * @interface ITcIconContainerProps + */ +interface ITcIconContainerProps { + /** + * Children elements to be rendered inside the container. + * This should be a single React element, typically an icon or a small component. + * @type {React.ReactElement} + */ + children: React.ReactElement; +} + +/** + * A container component designed to display its children in a circular, centered fashion. + * Ideal for icons or small elements. + * + * @param {ITcIconContainerProps} props - The properties passed to the component. + * @returns {JSX.Element} A div element with applied styling and containing the children. + */ +function TcIconContainer({ children }: ITcIconContainerProps): JSX.Element { + return ( +
+ {children} +
+ ); +} + +export default TcIconContainer; diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx new file mode 100644 index 00000000..5d7f4a9f --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer.tsx @@ -0,0 +1,280 @@ +import React, { useEffect, useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdOutlineAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcButton from '../../../shared/TcButton'; +import { FormControl, FormControlLabel } from '@mui/material'; +import TcInput from '../../../shared/TcInput'; +import TcSwitch from '../../../shared/TcSwitch'; +import TcIconWithTooltip from '../../../shared/TcIconWithTooltip'; +import TcButtonGroup from '../../../shared/TcButtonGroup'; +import clsx from 'clsx'; +import TcPrivateMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; +import TcRolesAutoComplete from './TcRolesAutoComplete'; +import TcUsersAutoComplete from './TcUsersAutoComplete'; +import { IRoles, IUser } from '../../../../utils/interfaces'; +import { + DiscordData, + DiscordPrivateOptions, +} from '../../../../pages/announcements/edit-announcements'; + +export enum MessageType { + Both = 'Both', + RoleOnly = 'Role Only', + UserOnly = 'User Only', +} + +export interface ITcPrivateMessageContainerProps { + isEdit?: boolean; + privateAnnouncementsData?: DiscordData[] | undefined; + handlePrivateAnnouncements: ({ + message, + selectedRoles, + selectedUsers, + }: { + message: string; + selectedRoles?: IRoles[]; + selectedUsers?: IUser[]; + }) => void; +} + +function TcPrivateMessageContainer({ + handlePrivateAnnouncements, + isEdit = false, + privateAnnouncementsData, +}: ITcPrivateMessageContainerProps) { + const [privateMessage, setPrivateMessage] = useState(false); + const [messageType, setMessageType] = useState(MessageType.Both); + const [selectedUsers, setSelectedUsers] = useState([]); + const [selectedRoles, setSelectedRoles] = useState([]); + + const [message, setMessage] = useState(''); + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + const handlePrivateMessageChange = ( + event: React.ChangeEvent + ) => { + setPrivateMessage(event.target.checked); + }; + + const messageTypesArray = Object.values(MessageType); + + const isPreviewDialogEnabled = message.length > 0 && privateMessage == true; + + const getSelectedRolesLabels = () => { + return selectedRoles.map((role) => role.name || ''); + }; + + const selectedRolesLables = getSelectedRolesLabels(); + + const getSelectedUsersLabels = () => { + return selectedUsers.map((user) => user.ngu || ''); + }; + + const selectedUsersLables = getSelectedUsersLabels(); + + useEffect(() => { + const prepareAndSendData = () => { + switch (messageType) { + case MessageType.Both: + handlePrivateAnnouncements({ message, selectedRoles, selectedUsers }); + break; + + case MessageType.RoleOnly: + handlePrivateAnnouncements({ message, selectedRoles }); + break; + + case MessageType.UserOnly: + handlePrivateAnnouncements({ message, selectedUsers }); + break; + + default: + handlePrivateAnnouncements({ message, selectedRoles, selectedUsers }); + break; + } + }; + + if (message && privateMessage) { + prepareAndSendData(); + } + }, [message, selectedRoles, selectedUsers, messageType, privateMessage]); + + useEffect(() => { + if (isEdit && privateAnnouncementsData) { + const rolesArray: IRoles[] = []; + const usersArray: IUser[] = []; + let templateText = ''; + + privateAnnouncementsData.forEach((item) => { + if (item.type === 'discord_private') { + const privateOptions = item.options as DiscordPrivateOptions; + + if (privateOptions.roles) { + rolesArray.push(...privateOptions.roles); + } + + if (privateOptions.users) { + usersArray.push(...privateOptions.users); + } + + if (!templateText) { + templateText = item.template; + setPrivateMessage(true); + } + } + }); + + setSelectedRoles(rolesArray); + setSelectedUsers(usersArray); + setMessage(templateText); + } + }, [isEdit, privateAnnouncementsData]); + + return ( +
+
+
+ + + + + + } + label={ +
+ +
+ } + /> +
+
+ + {messageTypesArray.map((el) => ( + setMessageType(el)} + /> + ))} + + +
+
+ {privateMessage && ( +
+
+ + +
+
+ + + + + + +
+
+ + +
+ + + +
+ )} +
+ ); +} + +export default TcPrivateMessageContainer; diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx new file mode 100644 index 00000000..b8a8dec9 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.spec.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import TcPublicMessagePreviewDialog from './TcPrivateMessagePreviewDialog'; + +describe('TcPublicMessagePreviewDialog', () => { + const textMessage = 'This is a test message'; + const roles = ['Admin', 'User']; + const usernames = ['user1', 'user2']; + + it('renders without crashing', () => { + render( + + ); + expect(screen.getByText('Preview')).toBeInTheDocument(); + }); + + it('opens dialog on preview button click', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText('Preview Private Message')).toBeInTheDocument(); + }); + + it('closes dialog on close icon click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByTestId('close-icon')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Private Message') + ).not.toBeInTheDocument(); + }); + }); + + it('closes dialog on confirm button click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Private Message') + ).not.toBeInTheDocument(); + }); + }); + + it('displays the correct text message', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText(textMessage)).toBeInTheDocument(); + }); + + it('displays roles and usernames when provided', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + roles.forEach((role) => { + expect(screen.getByText(role)).toBeInTheDocument(); + }); + usernames.forEach((username) => { + expect(screen.getByText(username)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx new file mode 100644 index 00000000..6be77de4 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcPrivateMessagePreviewDialog.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import TcDialog from '../../../shared/TcDialog'; +import TcButton from '../../../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../../../shared/TcText'; + +interface ITcPublicMessagePreviewDialogProps { + textMessage: string; + selectedRoles?: string[]; + selectedUsernames?: string[]; + isPreviewDialogEnabled: boolean; +} + +function TcPublicMessagePreviewDialog({ + textMessage, + selectedRoles, + selectedUsernames, + isPreviewDialogEnabled, +}: ITcPublicMessagePreviewDialogProps) { + const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); + return ( + <> + setPreviewDialogOpen(true)} + /> + +
+ setPreviewDialogOpen(false)} + /> +
+
+ +
+
+ + {selectedRoles && + selectedRoles.map((role, index, array) => ( + + {'@'} + + {index < array.length - 1 && ', '} + + ))} +
+
+ + {selectedUsernames && + selectedUsernames.map((username, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))} +
+
+ +
+ setPreviewDialogOpen(false)} + sx={{ width: '100%' }} + /> +
+
+ + } + open={isPreviewDialogOpen} + /> + + ); +} + +export default TcPublicMessagePreviewDialog; diff --git a/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx new file mode 100644 index 00000000..9fa74550 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcRolesAutoComplete.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useState } from 'react'; +import { useToken } from '../../../../context/TokenContext'; +import useAppStore from '../../../../store/useStore'; +import { FetchedData, IRoles } from '../../../../utils/interfaces'; +import { debounce } from '../../../../helpers/helper'; +import TcAutocomplete from '../../../shared/TcAutocomplete'; +import { Chip, CircularProgress } from '@mui/material'; + +interface ITcRolesAutoCompleteProps { + isEdit?: boolean; + privateSelectedRoles?: IRoles[]; + isDisabled: boolean; + handleSelectedUsers: (roles: IRoles[]) => void; +} + +function TcRolesAutoComplete({ + isEdit = false, + privateSelectedRoles, + isDisabled, + handleSelectedUsers, +}: ITcRolesAutoCompleteProps) { + const { community } = useToken(); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const { retrievePlatformProperties } = useAppStore(); + const [selectedRoles, setSelectedRoles] = useState([]); + + const [fetchedRoles, setFetchedRoles] = useState({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + const [filteredRolesByName, setFilteredRolesByName] = useState(''); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const fetchDiscordRoles = async ( + platformId: string, + page?: number, + limit?: number, + name?: string + ) => { + try { + setIsLoading(true); + + const fetchedRoles = await retrievePlatformProperties({ + platformId, + name: name, + property: 'role', + page: page, + limit: limit, + }); + + if (name) { + setFilteredRolesByName(name); + setFetchedRoles(fetchedRoles); + } else { + setFetchedRoles((prevData: { results: any }) => { + const updatedResults = [ + ...prevData.results, + ...fetchedRoles.results, + ].filter( + (role, index, self) => + index === self.findIndex((r) => r.id === role.id) + ); + + return { + ...prevData, + ...fetchedRoles, + results: updatedResults, + }; + }); + } + } catch (error) { + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!platformId) return; + fetchDiscordRoles(platformId, fetchedRoles.page, fetchedRoles.limit); + }, []); + + const debouncedFetchDiscordRoles = debounce(fetchDiscordRoles, 700); + + const handleSearchChange = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + const inputValue = target.value; + + if (!platformId) return; + + if (inputValue === '') { + setFilteredRolesByName(''); + setFetchedRoles({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + + debouncedFetchDiscordRoles(platformId, 1, 100); + } else { + debouncedFetchDiscordRoles(platformId, 1, 100, inputValue); + } + }; + + const handleScroll = (event: React.UIEvent) => { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight === + listboxNode.scrollHeight + ) { + const nextPage = + Math.ceil(fetchedRoles.results.length / fetchedRoles.limit) + 1; + if (fetchedRoles.totalPages >= nextPage) { + if (!platformId) return; + fetchDiscordRoles(platformId, nextPage, fetchedRoles.limit); + } + } + }; + + const handleChange = ( + event: React.SyntheticEvent, + value: any[] + ): void => { + setSelectedRoles(value); + }; + + useEffect(() => { + if (!selectedRoles) return; + handleSelectedUsers(selectedRoles); + }, [selectedRoles]); + + useEffect(() => { + if (isEdit && !isInitialized) { + if (privateSelectedRoles !== undefined) { + setSelectedRoles(privateSelectedRoles); + } else { + setSelectedRoles([]); + } + setIsInitialized(true); + } + }, [privateSelectedRoles, isEdit, isInitialized]); + + return ( + option.name} + label={'Select Role(s)'} + multiple={true} + loading={isLoading} + loadingText={ +
+ +
+ } + disabled={isDisabled} + value={selectedRoles} + onChange={handleChange} + onInputChange={handleSearchChange} + isOptionEqualToValue={(option, value) => option.roleId === value.roleId} + disableCloseOnSelect + renderOption={(props, option) => ( +
  • + {option.name} +
  • + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + + {option.name} + + } + size="small" + sx={{ + borderRadius: '4px', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', + }} + {...getTagProps({ index })} + /> + )) + } + textFieldProps={{ variant: 'filled' }} + ListboxProps={{ + onScroll: handleScroll, + style: { + maxHeight: '280px', + overflow: 'auto', + }, + }} + /> + ); +} + +export default TcRolesAutoComplete; diff --git a/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx new file mode 100644 index 00000000..7ffc8edc --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/TcUsersAutoComplete.tsx @@ -0,0 +1,261 @@ +import React, { useEffect, useState } from 'react'; +import { useToken } from '../../../../context/TokenContext'; +import useAppStore from '../../../../store/useStore'; +import { FetchedData, IUser } from '../../../../utils/interfaces'; +import { debounce, truncateCenter } from '../../../../helpers/helper'; +import TcAutocomplete from '../../../shared/TcAutocomplete'; +import { Chip, CircularProgress } from '@mui/material'; +import TcAvatar from '../../../shared/TcAvatar'; +import TcText from '../../../shared/TcText'; +import { conf } from '../../../../configs'; + +interface ITcUsersAutoCompleteProps { + isEdit?: boolean; + privateSelectedUsers?: IUser[]; + isDisabled: boolean; + handleSelectedUsers: (users: IUser[]) => void; +} + +function TcUsersAutoComplete({ + isEdit = false, + privateSelectedUsers, + isDisabled, + handleSelectedUsers, +}: ITcUsersAutoCompleteProps) { + const { community } = useToken(); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const { retrievePlatformProperties } = useAppStore(); + const [selectedUsers, setSelectedUsers] = useState([]); + + const [fetchedUsers, setFetchedUsers] = useState({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + const [filteredUsersByName, setFilteredUsersByName] = useState(''); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const fetchDiscordUsers = async ( + platformId: string, + page?: number, + limit?: number, + ngu?: string + ) => { + try { + setIsLoading(true); + + const fetchedUsers = await retrievePlatformProperties({ + platformId, + ngu: ngu, + property: 'guildMember', + page: page, + limit: limit, + }); + + if (ngu) { + setFilteredUsersByName(ngu); + setFetchedUsers(fetchedUsers); + } else { + setFetchedUsers((prevData: { results: any }) => { + const updatedResults = [ + ...prevData.results, + ...fetchedUsers.results, + ].filter( + (role, index, self) => + index === self.findIndex((r) => r.discordId === role.discordId) + ); + + return { + ...prevData, + ...fetchedUsers, + results: updatedResults, + }; + }); + } + } catch (error) { + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!platformId) return; + fetchDiscordUsers(platformId, fetchedUsers.page, fetchedUsers.limit); + }, []); + + const debouncedFetchDiscordUsers = debounce(fetchDiscordUsers, 700); + + const handleClearAll = () => { + if (!platformId) return; + fetchDiscordUsers(platformId, fetchedUsers.page, fetchedUsers.limit); + }; + + const handleSearchChange = (event: React.SyntheticEvent) => { + const target = event.target as HTMLInputElement; + const inputValue = target.value; + + if (!platformId) return; + + if (inputValue === '') { + setFilteredUsersByName(''); + setFetchedUsers({ + limit: 8, + page: 1, + results: [], + totalPages: 0, + totalResults: 0, + }); + + debouncedFetchDiscordUsers(platformId, 1, 8); + } else { + debouncedFetchDiscordUsers(platformId, 1, 8, inputValue); + } + }; + + const handleScroll = (event: React.UIEvent) => { + const listboxNode = event.currentTarget; + if ( + listboxNode.scrollTop + listboxNode.clientHeight === + listboxNode.scrollHeight + ) { + const nextPage = + Math.ceil(fetchedUsers.results.length / fetchedUsers.limit) + 1; + if (fetchedUsers.totalPages >= nextPage) { + if (!platformId) return; + fetchDiscordUsers(platformId, nextPage, fetchedUsers.limit); + } + } + }; + + const handleChange = ( + event: React.SyntheticEvent, + value: any[] + ): void => { + setSelectedUsers(value); + }; + + useEffect(() => { + if (!selectedUsers) return; + handleSelectedUsers(selectedUsers); + }, [selectedUsers]); + + useEffect(() => { + if (isEdit && !isInitialized) { + if (privateSelectedUsers !== undefined) { + setSelectedUsers(privateSelectedUsers); + } else { + setSelectedUsers([]); + } + setIsInitialized(true); + } + }, [privateSelectedUsers, isEdit, isInitialized]); + + return ( + option.ngu} + label={'Select User(s)'} + multiple={true} + loading={isLoading} + loadingText={ +
    + +
    + } + disabled={isDisabled} + value={selectedUsers} + onChange={handleChange} + onInputChange={(event, value, reason) => { + if (reason === 'clear') { + handleClearAll(); + } else { + handleSearchChange(event); + } + }} + isOptionEqualToValue={(option, value) => + option.discordId === value.discordId + } + disableCloseOnSelect + renderOption={(props, option) => ( +
  • +
    + + + +
    +
  • + )} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + +
    + + +
    + + + } + size="small" + sx={{ + borderRadius: '4px', + borderColor: '#D1D1D1', + backgroundColor: 'white', + color: 'black', + }} + {...getTagProps({ index })} + /> + )) + } + textFieldProps={{ variant: 'filled' }} + ListboxProps={{ + onScroll: handleScroll, + style: { + maxHeight: '280px', + overflow: 'auto', + }, + }} + /> + ); +} + +export default TcUsersAutoComplete; diff --git a/src/components/announcements/create/privateMessaageContainer/index.ts b/src/components/announcements/create/privateMessaageContainer/index.ts new file mode 100644 index 00000000..cfd2afd6 --- /dev/null +++ b/src/components/announcements/create/privateMessaageContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcPrivateMessaageContainer } from './TcPrivateMessageContainer'; + +export default TcPrivateMessaageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx new file mode 100644 index 00000000..88d1b3d1 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.spec.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcPublicMessageContainer from './TcPublicMessageContainer'; +import { TokenContext } from '../../../../context/TokenContext'; + +const mockToken = { + accessToken: 'mockAccessToken', + refreshToken: 'mockRefreshToken', +}; + +const mockCommunity = { + name: 'Test Community', + platforms: [], + id: 'mockCommunityId', + users: [], + avatarURL: 'mockAvatarURL', +}; + +const mockTokenContextValue = { + token: mockToken, + community: mockCommunity, + updateToken: jest.fn(), + updateCommunity: jest.fn(), + deleteCommunity: jest.fn(), + clearToken: jest.fn(), +}; + +describe('TcPublicMessageContainer', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('renders the "Public Message" text', () => { + expect(screen.getByText(/Public Message/i)).toBeInTheDocument(); + }); + + it('renders the message about bot distribute', () => { + const message = + /Our bot will distribute the announcement through selected channels with the required access to share the designated message./i; + expect(screen.getByText(message)).toBeInTheDocument(); + }); + + it('renders the "Write message here:" text', () => { + expect(screen.getByText(/Write message here:/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx new file mode 100644 index 00000000..fba55ba9 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessageContainer.tsx @@ -0,0 +1,205 @@ +import React, { useContext, useEffect, useState } from 'react'; +import TcText from '../../../shared/TcText'; +import { MdAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../TcIconContainer'; +import TcSelect from '../../../shared/TcSelect'; +import { FormControl, FormHelperText, InputLabel } from '@mui/material'; +import TcInput from '../../../shared/TcInput'; +import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; +import { ChannelContext } from '../../../../context/ChannelContext'; +import TcPlatformChannelList from '../../../communitySettings/platform/TcPlatformChannelList'; +import { IGuildChannels } from '../../../../utils/types'; +import { DiscordData } from '../../../../pages/announcements/edit-announcements'; +import TcPermissionHints from '../../../global/TcPermissionHints'; +import TcButton from '../../../shared/TcButton'; + +export interface FlattenedChannel { + id: string; + label: string; +} + +export interface ITcPublicMessageContainerProps { + isEdit?: boolean; + publicAnnouncementsData?: DiscordData | undefined; + handlePublicAnnouncements: ({ + message, + selectedChannels, + }: { + message: string; + selectedChannels: FlattenedChannel[] | []; + }) => void; +} + +function TcPublicMessageContainer({ + handlePublicAnnouncements, + isEdit = false, + publicAnnouncementsData, +}: ITcPublicMessageContainerProps) { + const channelContext = useContext(ChannelContext); + + const { channels, selectedSubChannels } = channelContext; + const [hasInteracted, setHasInteracted] = useState(false); + const [showError, setShowError] = useState(false); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + + const flattenChannels = (channels: IGuildChannels[]): FlattenedChannel[] => { + let flattened: FlattenedChannel[] = []; + + channels.forEach((channel) => { + if (channel.subChannels) { + channel.subChannels.forEach((subChannel) => { + if (selectedSubChannels[channel.channelId]?.[subChannel.channelId]) { + flattened.push({ + id: subChannel.channelId, + label: subChannel.name, + }); + } + }); + } + }); + + return flattened; + }; + + const [selectedChannels, setSelectedChannels] = useState( + [] + ); + const [confirmedSelectedChannels, setConfirmedSelectedChannels] = + useState(false); + + useEffect(() => { + setSelectedChannels(flattenChannels(channels)); + }, [channels, selectedSubChannels]); + + const [message, setMessage] = useState(''); + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + const isPreviewDialogEnabled = + selectedChannels.length > 0 && message.length > 0; + + useEffect(() => { + if (confirmedSelectedChannels) { + handlePublicAnnouncements({ message, selectedChannels }); + } else { + handlePublicAnnouncements({ message, selectedChannels: [] }); + } + }, [message, selectedChannels, confirmedSelectedChannels]); + + useEffect(() => { + if (isEdit && publicAnnouncementsData) { + if ( + publicAnnouncementsData.type === 'discord_public' && + 'channels' in publicAnnouncementsData.options + ) { + const formattedChannels = publicAnnouncementsData.options.channels.map( + (channel) => ({ + id: channel.channelId, + label: channel.name, + }) + ); + setConfirmedSelectedChannels(true); + setSelectedChannels(formattedChannels); + setMessage(publicAnnouncementsData.template); + } + } + }, [isEdit, publicAnnouncementsData]); + + const handleSaveChannels = () => { + setConfirmedSelectedChannels(true); + setIsDropdownVisible(false); + setShowError(hasInteracted && selectedChannels.length === 0); + }; + + const toggleDropdownVisibility = () => { + setHasInteracted(true); + setIsDropdownVisible(!isDropdownVisible); + }; + + return ( +
    +
    +
    + + + + +
    + channel.label)} + /> +
    +
    + + Select Channels + + (selected as FlattenedChannel[]) + .map((channel) => `#${channel.label}`) + .join(', ') + } + > +
    +
    + +
    + +
    + +
    +
    +
    + {showError && ( + + + + )} +
    +
    + + +
    + + + +
    +
    + ); +} + +export default TcPublicMessageContainer; diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx new file mode 100644 index 00000000..1a1e2f98 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import TcPublicMessagePreviewDialog from './TcPublicMessagePreviewDialog'; + +describe('TcPublicMessagePreviewDialog', () => { + const textMessage = 'This is a test message'; + + it('renders without crashing', () => { + render( + + ); + expect(screen.getByText('Preview')).toBeInTheDocument(); + }); + + it('opens dialog on preview button click', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText('Preview Public Message')).toBeInTheDocument(); + }); + + it('closes dialog on close icon click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByTestId('close-icon')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Public Message') + ).not.toBeInTheDocument(); + }); + }); + + it('closes dialog on confirm button click', async () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + fireEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect( + screen.queryByText('Preview Public Message') + ).not.toBeInTheDocument(); + }); + }); + it('displays the correct text message', () => { + render( + + ); + fireEvent.click(screen.getByText('Preview')); + expect(screen.getByText(textMessage)).toBeInTheDocument(); + }); + + it('preview button is disabled when isPreviewDialogEnabled is false', () => { + render( + + ); + const previewButton = screen.getByRole('button', { name: 'Preview' }); + expect(previewButton).toBeDisabled(); + }); +}); diff --git a/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx new file mode 100644 index 00000000..15274c87 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/TcPublicMessagePreviewDialog.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import TcDialog from '../../../shared/TcDialog'; +import TcButton from '../../../shared/TcButton'; +import { AiOutlineClose } from 'react-icons/ai'; +import TcText from '../../../shared/TcText'; + +interface ITcPublicMessagePreviewDialogProps { + textMessage: string; + selectedChannels: string[]; + isPreviewDialogEnabled: boolean; +} + +function TcPublicMessagePreviewDialog({ + textMessage, + selectedChannels, + isPreviewDialogEnabled, +}: ITcPublicMessagePreviewDialogProps) { + const [isPreviewDialogOpen, setPreviewDialogOpen] = useState(false); + return ( + <> + setPreviewDialogOpen(true)} + /> + +
    + setPreviewDialogOpen(false)} + /> +
    +
    + +
    + + {selectedChannels && + selectedChannels.map((channel, index, array) => ( + + {'#'} + + {index < array.length - 1 && ', '} + + ))} +
    + +
    + setPreviewDialogOpen(false)} + sx={{ width: '100%' }} + /> +
    +
    + + } + open={isPreviewDialogOpen} + /> + + ); +} + +export default TcPublicMessagePreviewDialog; diff --git a/src/components/announcements/create/publicMessageContainer/index.ts b/src/components/announcements/create/publicMessageContainer/index.ts new file mode 100644 index 00000000..53cda254 --- /dev/null +++ b/src/components/announcements/create/publicMessageContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcPublicMessageContainer } from './TcPublicMessageContainer'; + +export default TcPublicMessageContainer; diff --git a/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx b/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx new file mode 100644 index 00000000..fbe8f715 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/TcDateTimePopover.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { StaticTimePicker } from '@mui/x-date-pickers'; +import { FiCalendar } from 'react-icons/fi'; +import { MdAccessTime } from 'react-icons/md'; +import TcPopover from '../../../shared/TcPopover'; +import TcTabs from '../../../shared/TcTabs'; +import TcTab from '../../../shared/TcTabs/TcTab'; + +interface IDateTimePopoverProps { + open: boolean; + anchorEl: HTMLButtonElement | null; + onClose: () => void; + selectedDate: Date | null; + handleDateChange: (date: Date | null) => void; + selectedTime: Date | null; + handleTimeChange: (time: Date | null) => void; + activeTab: number; + setActiveTab: React.Dispatch>; +} + +function TcDateTimePopover({ + open, + anchorEl, + onClose, + selectedDate, + handleDateChange, + selectedTime, + handleTimeChange, + activeTab, + setActiveTab, +}: IDateTimePopoverProps) { + const disablePastDates = (date: Date): boolean => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + }; + + const tabContent = [ + + + , + + + , + ]; + + return ( + + {tabContent[activeTab]} + + setActiveTab(newValue)} + indicatorColor="secondary" + className="w-full border-t border-gray-200" + > + } + className="w-1/2" + data-testid="calendar-icon" + /> + } + className="w-1/2" + data-testid="time-icon" + /> + + + } + /> + ); +} + +export default TcDateTimePopover; diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx new file mode 100644 index 00000000..b07aef74 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.spec.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import TcScheduleAnnouncement from './TcScheduleAnnouncement'; + +describe('TcScheduleAnnouncement Tests', () => { + // Mock functions for the new props + const mockHandleScheduledDate = jest.fn(); + const mockSetIsDateValid = jest.fn(); + + test('renders the component without crashing', () => { + render( + + ); + + // Since the initial text is "Select Date for Announcement", we should assert this text. + expect( + screen.getByText('Select Date for Announcement') + ).toBeInTheDocument(); + }); + + test('initially displays the calendar icon', () => { + render( + + ); + + // Assuming your TcButton component renders an icon, this test checks for its presence. + const calendarIcon = screen.getByTestId('MdCalendarMonth'); + expect(calendarIcon).toBeInTheDocument(); + }); + + test('displays the button to open date-time popover', () => { + render( + + ); + + // Check if the button that is supposed to open the date-time popover is rendered. + const button = screen.getByRole('button', { + name: /select date for announcement/i, + }); + expect(button).toBeInTheDocument(); + }); +}); diff --git a/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx new file mode 100644 index 00000000..394f88f4 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/TcScheduleAnnouncement.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from 'react'; +import TcIconContainer from '../TcIconContainer'; +import { MdCalendarMonth } from 'react-icons/md'; +import TcText from '../../../shared/TcText'; +import TcButton from '../../../shared/TcButton'; +import moment from 'moment'; +import TcDateTimePopover from './TcDateTimePopover'; +import { validateDateTime } from '../../../../helpers/helper'; + +export interface ITcScheduleAnnouncementProps { + isEdit?: boolean; + preSelectedTime?: string; + handleSchaduledDate: ({ selectedTime }: { selectedTime: string }) => void; + isDateValid: boolean; + setIsDateValid: (isValid: boolean) => void; +} + +function TcScheduleAnnouncement({ + isEdit = false, + preSelectedTime, + handleSchaduledDate, + isDateValid, + setIsDateValid, +}: ITcScheduleAnnouncementProps) { + const [anchorEl, setAnchorEl] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedTime, setSelectedTime] = useState(null); + + const [dateTimeDisplay, setDateTimeDisplay] = useState( + 'Select Date for Announcement' + ); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'date-time-popover' : undefined; + + const handleDateChange = (date: Date | null) => { + if (date) { + setSelectedDate(date); + setActiveTab(1); + const isValid = validateDateTime(date, selectedTime); + setIsDateValid(isValid); + } + }; + + const handleTimeChange = (time: Date | null) => { + if (time) { + setSelectedTime(time); + + if (selectedDate) { + const fullDateTime = moment(selectedDate).set({ + hour: time.getHours(), + minute: time.getMinutes(), + }); + setDateTimeDisplay(fullDateTime.format('D MMMM YYYY @ hh:mm A')); + } + const isValid = validateDateTime(selectedDate, time); + setIsDateValid(isValid); + } + }; + + useEffect(() => { + if (!selectedDate || !selectedTime) return; + + const fullDateTime = moment(selectedDate).set({ + hour: selectedTime.getHours(), + minute: selectedTime.getMinutes(), + }); + + const fullDateTimeUTC = fullDateTime.utc(); + + const formattedUTCDate = fullDateTimeUTC.format(); + + handleSchaduledDate({ selectedTime: formattedUTCDate }); + }, [selectedDate, selectedTime]); + + useEffect(() => { + if (isEdit && preSelectedTime) { + const dateTime = moment(preSelectedTime); + const date = dateTime.toDate(); + setSelectedDate(date); + setSelectedTime(date); + setDateTimeDisplay(dateTime.format('D MMMM YYYY @ hh:mm A')); + setIsDateValid(validateDateTime(date, date)); + } + }, [isEdit, preSelectedTime]); + + return ( +
    +
    +
    + + + + +
    + } + disableElevation={true} + className="border border-black bg-gray-100 shadow-md" + sx={{ color: 'black', height: '2.4rem', paddingX: '1rem' }} + aria-describedby={id} + onClick={handleOpen} + /> + {!isDateValid && ( + + )} + +
    +
    + ); +} + +export default TcScheduleAnnouncement; diff --git a/src/components/announcements/create/scheduleAnnouncement/index.ts b/src/components/announcements/create/scheduleAnnouncement/index.ts new file mode 100644 index 00000000..143415a6 --- /dev/null +++ b/src/components/announcements/create/scheduleAnnouncement/index.ts @@ -0,0 +1,3 @@ +import { default as TcScheduleAnnouncement } from './TcScheduleAnnouncement'; + +export default TcScheduleAnnouncement; diff --git a/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx new file mode 100644 index 00000000..8fac03f6 --- /dev/null +++ b/src/components/announcements/create/selectPlatform/TcSelectPlatform.tsx @@ -0,0 +1,56 @@ +import { FormControl, InputLabel } from '@mui/material'; +import React from 'react'; +import TcSelect from '../../../shared/TcSelect'; +import TcText from '../../../shared/TcText'; +import { BsDiscord, BsTelegram } from 'react-icons/bs'; + +const announcementsPlatforms = [ + { + label: 'Discord', + value: '1', + icon: , + }, + { + label: 'Telegram(TBA)', + value: '2', + disabled: true, + icon: , + }, +]; + +interface ITcSelectPlatformProps { + isEdit: boolean; +} + +function TcSelectPlatform({ isEdit }: ITcSelectPlatformProps) { + return ( +
    +
    + + +
    + + Select Platform + + +
    + ); +} + +export default TcSelectPlatform; diff --git a/src/components/announcements/create/selectPlatform/index.ts b/src/components/announcements/create/selectPlatform/index.ts new file mode 100644 index 00000000..802608b8 --- /dev/null +++ b/src/components/announcements/create/selectPlatform/index.ts @@ -0,0 +1,3 @@ +import { default as TcSelectPlatform } from './TcSelectPlatform'; + +export default TcSelectPlatform; diff --git a/src/components/centric/selectCommunity/TcSelectCommunity.tsx b/src/components/centric/selectCommunity/TcSelectCommunity.tsx index dd3619ff..d4b304db 100644 --- a/src/components/centric/selectCommunity/TcSelectCommunity.tsx +++ b/src/components/centric/selectCommunity/TcSelectCommunity.tsx @@ -111,6 +111,7 @@ function TcSelectCommunity() { text="Continue" className="secondary" variant="contained" + sx={{ width: '15rem', padding: '0.5rem' }} disabled={!activeCommunity} onClick={handleSelectedCommunity} /> @@ -124,6 +125,7 @@ function TcSelectCommunity() { } text="Create" + sx={{ width: '15rem', padding: '0.5rem' }} variant="outlined" onClick={() => router.push('/centric/create-new-community')} /> diff --git a/src/components/communitySettings/platform/TcPlatform.tsx b/src/components/communitySettings/platform/TcPlatform.tsx index 7dc798a1..86aa358b 100644 --- a/src/components/communitySettings/platform/TcPlatform.tsx +++ b/src/components/communitySettings/platform/TcPlatform.tsx @@ -165,6 +165,7 @@ function TcPlatform({ platformName = 'Discord' }: TcPlatformProps) { diff --git a/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx b/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx index edf07c3b..e705a215 100644 --- a/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx +++ b/src/components/communitySettings/platform/TcPlatformChannelDialog.tsx @@ -69,6 +69,7 @@ function TcPlatformChannelDialog() { setOpenDialog(false)} /> diff --git a/src/components/communitySettings/platform/TcPlatformChannelList.tsx b/src/components/communitySettings/platform/TcPlatformChannelList.tsx index 9baa339d..13bd52e4 100644 --- a/src/components/communitySettings/platform/TcPlatformChannelList.tsx +++ b/src/components/communitySettings/platform/TcPlatformChannelList.tsx @@ -95,7 +95,8 @@ function TcPlatformChannelList({ } disabled={channel?.subChannels?.some( (subChannel) => - !subChannel.canReadMessageHistoryAndViewChannel + !subChannel.canReadMessageHistoryAndViewChannel || + !subChannel.announcementAccess )} /> } diff --git a/src/components/global/.CustomTab.tsx.swp b/src/components/global/.CustomTab.tsx.swp new file mode 100644 index 00000000..1c19527f Binary files /dev/null and b/src/components/global/.CustomTab.tsx.swp differ diff --git a/src/components/global/Accardion.tsx b/src/components/global/Accardion.tsx deleted file mode 100644 index e4400f2a..00000000 --- a/src/components/global/Accardion.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'; -import { MdExpandMore } from 'react-icons/md'; - -type AcProps = { - readonly title?: string; - childs: AcChildProps[]; -}; - -type AcChildProps = { - title: string; - id: string; - icon?: ReactElement; - detailsComponent: ReactElement; -}; - -export default function Accardion({ title, childs }: AcProps) { - const [expanded, setExpanded] = React.useState(false); - - const handleChange = - (panel: string) => (_event: React.SyntheticEvent, isExpanded: boolean) => { - setExpanded(isExpanded ? panel : false); - }; - - return ( - <> -

    {title}

    - {childs.map((el) => ( - - - } - aria-controls={`${el.id}-content`} - id={el.id} - > -
    -
    - {el.icon} -
    -

    {el.title}

    -
    -
    - - {el.detailsComponent} - -
    - ))} - - ); -} diff --git a/src/components/global/Card.tsx b/src/components/global/Card.tsx deleted file mode 100644 index fc0ae5c2..00000000 --- a/src/components/global/Card.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Image from "next/image"; - -type Props = { - className?: string; - title: string; - srcImage: string; - srcWidth: number; -}; - -export default function Card({ className, title, srcImage, srcWidth }: Props) { - return ( -
    -

    {title}

    -
    -
    - Picture of the author -
    -
    -
    - ); -} - -Card.defaultProps = { - title: "", - srcWidth: "400", -}; diff --git a/src/components/global/CustomDatePicker.tsx b/src/components/global/CustomDatePicker.tsx deleted file mode 100644 index 45ede02b..00000000 --- a/src/components/global/CustomDatePicker.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { FC, RefObject, useState } from 'react'; -import '@hassanmojab/react-modern-calendar-datepicker/lib/DatePicker.css'; -import DatePicker, { - DayRange, -} from '@hassanmojab/react-modern-calendar-datepicker'; -import { FiCalendar } from 'react-icons/fi'; -import clsx from 'clsx'; -import moment from 'moment'; - -interface IProps { - placeholder?: string; - className: string; - onClick: any; -} - -const CustomDatePicker: FC = ({ - placeholder, - className, - onClick, -}): JSX.Element => { - const [dayRange, setDayRange] = useState({ - from: null, - to: null, - }); - - const renderCustomInput = ({ - ref, - }: { - ref: RefObject | any; - }) => ( -
    - - -
    - ); - - return ( - setDayRange(date)} - renderInput={renderCustomInput} - colorPrimary="#35B9B7" // added this - colorPrimaryLight="#D0FBF8" // and this - calendarPopperPosition="bottom" - /> - ); -}; - -CustomDatePicker.defaultProps = { - placeholder: 'Specific date', -}; - -export default CustomDatePicker; diff --git a/src/components/global/CustomModal.tsx b/src/components/global/CustomModal.tsx deleted file mode 100644 index 3209fa9a..00000000 --- a/src/components/global/CustomModal.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Dialog, DialogTitle, DialogContent } from '@mui/material'; -import { IoClose } from 'react-icons/io5'; - -type IModalProps = { - isOpen: boolean; - toggleModal: (arg0: boolean) => void; - children: any; - hasClose: boolean; -}; -export default function ConfirmModal({ - isOpen, - toggleModal, - children, - hasClose, - ...props -}: IModalProps) { - const handleClose = () => { - toggleModal(false); - }; - return ( - <> - - {hasClose ? ( - - - - ) : ( - '' - )} - {children} - - - ); -} diff --git a/src/components/global/CustomTab.tsx b/src/components/global/CustomTab.tsx index 3381e444..99af64eb 100644 --- a/src/components/global/CustomTab.tsx +++ b/src/components/global/CustomTab.tsx @@ -48,6 +48,25 @@ function CustomTab({ width: '50%', padding: '0', }, + textTransform: 'none', + borderRadius: '10px 10px 0 0', + padding: '8px 24px', + gap: '10px', + borderBottom: 'none', + '&.Mui-selected': { + background: '#804EE1', + color: 'white', + border: 0, + borderBottom: 'none', + }, + '&$selected': { + borderBottom: 'none', + }, + '&:not(.Mui-selected)': { + backgroundColor: '#EDEDED', + color: '#222222', + }, + selected: {}, }} /> ))} diff --git a/src/components/global/DatePeriodRange.tsx b/src/components/global/DatePeriodRange.tsx deleted file mode 100644 index 826d0b95..00000000 --- a/src/components/global/DatePeriodRange.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from 'clsx'; -import React, { useState } from 'react'; -import CustomDatePicker from './CustomDatePicker'; - -type dateItems = { - title: string; - icon?: JSX.Element; - value: any; -}; - -const datePeriod: dateItems[] = [ - { - title: 'Last 35 days', - value: 1, - }, - { - title: '3M', - value: 2, - }, - { - title: '6M', - value: 3, - }, - { - title: '1Y', - value: 4, - }, -]; - -type datePeriodRangeProps = { - activePeriod: string | number; - onChangeActivePeriod: (e: number) => void; -}; - -export default function DatePeriodRange({ - activePeriod, - onChangeActivePeriod, -}: datePeriodRangeProps) { - return ( -
    -
      - {datePeriod.length > 0 - ? datePeriod.map((el) => ( -
    • onChangeActivePeriod(el.value)} - > - {el.icon ? el.icon : ''} -
      {el.title}
      -
    • - )) - : ''} -
    -
    - ); -} diff --git a/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx b/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx new file mode 100644 index 00000000..13710ad1 --- /dev/null +++ b/src/components/global/TcPermissionHints/TcPermissionHints.spec.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PermissionHints from './TcPermissionHints'; +describe('PermissionHints Component', () => { + test('renders PermissionHints component', () => { + render(); + expect(screen.getByText('Access Settings')).toBeInTheDocument(); + expect(screen.getByText('Server Level')).toBeInTheDocument(); + expect(screen.getByText('Category Level')).toBeInTheDocument(); + expect(screen.getByText('Channel Level')).toBeInTheDocument(); + }); + + test('initial active category is Access Settings', async () => { + render(); + await waitFor(() => { + expect( + screen.getByText('What does “Read” and “Write” access mean?') + ).toBeInTheDocument(); + }); + }); + + test('clicking on a category button changes active category to Server Level', async () => { + render(); + const serverLevelButton = screen.getByText('Server Level'); + userEvent.click(serverLevelButton); + await waitFor(() => { + expect( + screen.getByText( + 'Please note that your platform’s permission settings enable the above permission controls' + ) + ).toBeInTheDocument(); + }); + }); + + test('clicking on a category button changes active category to Category Level', async () => { + render(); + const categoryLevelButton = screen.getByText('Category Level'); + userEvent.click(categoryLevelButton); + await waitFor(() => { + expect( + screen.getByText( + 'Please note that Category-level permissions override Server-level permissions' + ) + ).toBeInTheDocument(); + }); + }); + + test('clicking on a category button changes active category to Channel Level', async () => { + render(); + + const channelLevelButton = screen.getByText('Channel Level'); + + userEvent.click(channelLevelButton); + + await waitFor(() => { + expect( + screen.getByText( + 'Please note that Channel-level permissions override Category-level permissions, which in turn override Server-level permissions' + ) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/global/TcPermissionHints/TcPermissionHints.tsx b/src/components/global/TcPermissionHints/TcPermissionHints.tsx new file mode 100644 index 00000000..10d023d7 --- /dev/null +++ b/src/components/global/TcPermissionHints/TcPermissionHints.tsx @@ -0,0 +1,329 @@ +import React, { useState } from 'react'; +import TcButtonGroup from '../../shared/TcButtonGroup'; +import TcButton from '../../shared/TcButton'; +import clsx from 'clsx'; +import TcText from '../../shared/TcText'; + +const permissionCategories = [ + 'Access Settings', + 'Server Level', + 'Category Level', + 'Channel Level', +]; + +function PermissionHints() { + const [activeCategory, setActiveCategory] = + useState('Access Settings'); + + const handleButtonClick = (category: string) => { + setActiveCategory(category); + }; + + const getDescription = (category: string) => { + switch (category) { + case 'Access Settings': + return ( +
    + +
      +
    1. + +
        +
      • + +
      • +
      • + +
      • +
      +
    2. +
    +
      +
    1. + +
        +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      • + +
      • +
      +
    2. +
    +
    + ); + case 'Server Level': + return ( +
    + +
      +
    1. + + Navigate to the “Server Settings” in the top-left + corner of Discord + + } + variant="body2" + /> +
    2. +
    3. + + Select “Role/Members” (left sidebar), and then in + the middle of the screen check Advanced permissions + + } + variant="body2" + /> +
    4. +
    5. + + Then select “TogetherCrew” and under Advanced + Permissions, make sure that the following are marked as + [✓] + + } + variant="body2" + /> + +
        +
      1. + +
      2. +
      3. + +
      4. +
      +
    6. +
    + + Finally: Click on the Refresh List button on this page + and select the channels that have now been made available to + you + + } + variant="body2" + /> +
    + ); + case 'Category Level': + return ( +
    + +
      +
    1. + + Navigate to the “Edit Category” in the top-left + corner of Discord + + } + variant="body2" + /> +
    2. +
    3. + + Select “Permissions” (left sidebar), and then in + the middle of the screen check Advanced permissions + + } + variant="body2" + /> +
    4. +
    5. + + Then select “TogetherCrew” and under Advanced + Permissions, make sure that the following are marked as + [✓] + + } + variant="body2" + /> + +
        +
      1. + +
      2. +
      3. + +
      4. +
      +
    6. +
    + + Finally: Click on the Refresh List button on this page + and select the channels that have now been made available to + you + + } + variant="body2" + /> +
    + ); + case 'Channel Level': + return ( +
    + +
      +
    1. + + Navigate to the settings for a specific channel (select + the wheel on the right of the channel name) + + } + variant="body2" + /> +
    2. +
    3. + + Select “Permissions” (left sidebar), and then in + the middle of the screen check Advanced permissions + + } + variant="body2" + /> +
    4. +
    5. + + Then select “TogetherCrew” and under Advanced + Permissions, make sure that the following are marked as + [✓] + + } + variant="body2" + /> + +
        +
      1. + +
      2. +
      3. + +
      4. +
      +
    6. +
    + + Finally: Click on the Refresh List button on this page + and select the channels that have now been made available to + you + + } + variant="body2" + /> +
    + ); + default: + return null; + } + }; + + return ( +
    + + {permissionCategories.map((category) => ( + handleButtonClick(category)} + className={clsx( + 'border', + category === activeCategory + ? 'bg-secondary text-white border-secondary' + : 'border-secondary bg-white text-secondary' + )} + sx={{ + width: 'auto', + padding: { + xs: 'auto', + sm: '0.4rem 1rem', + }, + }} + /> + ))} + +
    {getDescription(activeCategory)}
    +
    + ); +} + +export default PermissionHints; diff --git a/src/components/global/TcPermissionHints/index.ts b/src/components/global/TcPermissionHints/index.ts new file mode 100644 index 00000000..3bfc5a13 --- /dev/null +++ b/src/components/global/TcPermissionHints/index.ts @@ -0,0 +1,3 @@ +import { default as TcPermissionHints } from './TcPermissionHints'; + +export default TcPermissionHints; diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index 3fb55c95..64cfeffa 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -11,6 +11,7 @@ import { conf } from '../../configs/index'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUserGroup, faHeartPulse } from '@fortawesome/free-solid-svg-icons'; +import { MdOutlineAnnouncement } from 'react-icons/md'; import { useRouter } from 'next/router'; import Link from 'next/link'; @@ -60,6 +61,15 @@ const Sidebar = () => { /> ), }, + { + name: 'Smart Announcements', + path: '/announcements', + icon: ( + + ), + }, { name: 'Community Settings', path: '/community-settings', @@ -83,7 +93,7 @@ const Sidebar = () => { > {el.icon} -

    {el.name}

    +

    {el.name}

    )); diff --git a/src/components/layouts/xs/SidebarXs.tsx b/src/components/layouts/xs/SidebarXs.tsx index bd95d84d..8a1791d8 100644 --- a/src/components/layouts/xs/SidebarXs.tsx +++ b/src/components/layouts/xs/SidebarXs.tsx @@ -16,7 +16,7 @@ import Link from 'next/link'; import { Drawer } from '@mui/material'; import { FaBars } from 'react-icons/fa'; -import { MdKeyboardBackspace } from 'react-icons/md'; +import { MdKeyboardBackspace, MdOutlineAnnouncement } from 'react-icons/md'; import { conf } from '../../../configs'; import { FiSettings } from 'react-icons/fi'; import { useToken } from '../../../context/TokenContext'; @@ -65,12 +65,21 @@ const Sidebar = () => { /> ), }, + { + name: 'Smart Announcements', + path: '/announcements', + icon: ( + + ), + }, { name: 'Community Settings', path: '/community-settings', icon: ( ), }, diff --git a/src/components/pages/login/ChannelList.tsx b/src/components/pages/login/ChannelList.tsx deleted file mode 100644 index 91c38db6..00000000 --- a/src/components/pages/login/ChannelList.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { FormControlLabel, Checkbox } from '@mui/material'; -import { FiAlertTriangle } from 'react-icons/fi'; -import { ISubChannels } from '../../../utils/types'; - -type IChannelListProps = { - guild: any; - showFlag: boolean; - onChange: (channelId: string, subChannelId: string, status: boolean) => void; - handleCheckAll: (guild: any, status: boolean) => void; -}; - -export default function ChannelList({ - guild, - onChange, - handleCheckAll, - showFlag, -}: IChannelListProps) { - const subChannelsList = ( - <> -

    Channels

    - {guild.subChannels.map((channel: ISubChannels, index: any) => ( -
    -
    - - onChange( - guild.channelId, - channel.channelId, - e.target.checked - ) - } - /> - } - label={channel.name} - /> - {showFlag && !channel.canReadMessageHistoryAndViewChannel ? ( -
    - - - {!channel.canReadMessageHistoryAndViewChannel - ? 'Bot needs access' - : ''} - -
    - ) : ( - '' - )} -
    -
    - ))} - - ); - - return ( -
    -

    {guild.title}

    -
    - item)} - color="secondary" - onChange={(e) => handleCheckAll(guild, e.target.checked)} - /> - } - /> - {subChannelsList} -
    -
    - ); -} - -ChannelList.defaultProps = { - showFlag: false, -}; diff --git a/src/components/pages/pageIndex/FooterSection.tsx b/src/components/pages/pageIndex/FooterSection.tsx deleted file mode 100644 index be4b8392..00000000 --- a/src/components/pages/pageIndex/FooterSection.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import Image from 'next/image'; - -import graph from '../../../assets/svg/graph.svg'; -import members from '../../../assets/svg/members.svg'; -import metrics from '../../../assets/svg/metrics.svg'; -import arrowBottom from '../../../assets/svg/arrowBottom.svg'; -import benchmark from '../../../assets/svg/benchmark.svg'; - -import { BsClockHistory } from 'react-icons/bs'; -import { HiOutlineArrowRight } from 'react-icons/hi'; -import Link from 'next/link'; - -export const FooterSection = (): JSX.Element => { - return ( - <> -
    -
    -
    -

    - Spot value-adding members in your community -

    -
    - Image Alt -
    -
    -
    -

    - Use data to improve onboarding -

    -
    - Image Alt -
    -
    -
    -
    -

    - Explore all the metrics that determine
    the health of your - community -

    -
    - Picture of the author -
    - Read our research on - -
    -
    -
    -

    - Monitor members who disengage and take action to bring them back -

    -
    - Image Alt -
    -
    -
    -

    - Benchmark your metrics and learn from others -

    -
    - Image Alt -
    -
    -
    -
    - - ); -}; diff --git a/src/components/pages/pageIndex/HeaderSection.tsx b/src/components/pages/pageIndex/HeaderSection.tsx deleted file mode 100644 index c0620b51..00000000 --- a/src/components/pages/pageIndex/HeaderSection.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { ImArrowDown } from 'react-icons/im'; - -export const HeaderSection = (): JSX.Element => { - return ( - <> -
    -
    -
    -

    - The new way to manage your community -

    -
    -
    -

    - We believe communities are the beating heart of DAOs. But there - was no way to assess and improve. We assembled a team of - scientists to empower you with deep, actionable insights. -

    -

    - And while the team is busy building a suite of tools, below is a - - small appetizer to get you started - - -

    -
    -
    -
    - - ); -}; diff --git a/src/components/pages/settings/ChannelSelection.tsx b/src/components/pages/settings/ChannelSelection.tsx deleted file mode 100644 index fdd03676..00000000 --- a/src/components/pages/settings/ChannelSelection.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Dialog, -} from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import { IoClose } from 'react-icons/io5'; -import useAppStore from '../../../store/useStore'; -import ChannelList from '../login/ChannelList'; -import { StorageService } from '../../../services/StorageService'; -import { - IGuild, - IGuildChannels, - IUser, - ISubChannels, - IChannelWithoutId, -} from '../../../utils/types'; -import { BiError } from 'react-icons/bi'; -import CustomButton from '../../global/CustomButton'; -import { FiRefreshCcw } from 'react-icons/fi'; -import Loading from '../../global/Loading'; -import { MdExpandMore } from 'react-icons/md'; -import ConfirmStartProcessing from './ConfirmStartProcessing'; -import clsx from 'clsx'; - -type IProps = { - emitable?: boolean; - submit?: (selectedChannels: IChannelWithoutId[]) => unknown; -}; -export default function ChannelSelection({ emitable, submit }: IProps) { - const [open, setOpen] = useState(false); - const [openProcessing, SetOpenProcessing] = useState(false); - - const [fullWidth, setFullWidth] = React.useState(true); - const [guild, setGuild] = useState(); - const [channels, setChannels] = useState>([]); - const [selectedChannels, setSelectedChannels] = useState< - Array - >([]); - - const { - guildChannels, - guildInfo, - updateSelectedChannels, - getUserGuildInfo, - guilds, - isRefetchLoading, - refetchGuildChannels, - } = useAppStore(); - - useEffect(() => { - const user = StorageService.readLocalStorage('user'); - if (user) { - setGuild(user.guild); - } - - const activeChannles = - guildInfo && guildInfo.selectedChannels - ? guildInfo.selectedChannels.map( - (channel: { - channelId: string; - channelName: string; - _id: string; - }) => { - return channel.channelId; - } - ) - : []; - - const channels = guildChannels.map( - (guild: IGuildChannels, _index: number) => { - const selected: Record = {}; - - guild.subChannels.forEach((subChannel: ISubChannels) => { - if (activeChannles.includes(subChannel.channelId)) { - selected[subChannel.channelId] = true; - } else { - selected[subChannel.channelId] = false; - } - }); - - return { ...guild, selected: selected }; - } - ); - - const subChannelsStatus = channels.map((channel: IGuildChannels) => { - return channel.selected; - }); - - const selectedChannelsStatus = Object.assign({}, ...subChannelsStatus); - let activeChannel: string[] = []; - for (const key in selectedChannelsStatus) { - if (selectedChannelsStatus[key]) { - activeChannel.push(key); - } - } - - const result = ([] as IChannelWithoutId[]).concat( - ...channels.map((channel: IGuildChannels) => { - return channel.subChannels - .filter((subChannel: ISubChannels) => { - if (activeChannel.includes(subChannel.channelId)) { - return subChannel; - } - }) - .map((filterdItem: ISubChannels) => { - return { - channelId: filterdItem.channelId, - channelName: filterdItem.name, - }; - }); - }) - ); - setSelectedChannels(result); - setChannels(channels); - }, [guildChannels]); - - const onChange = ( - channelId: string, - subChannelId: string, - status: boolean - ) => { - setChannels((preChannels) => { - return preChannels.map((preChannel) => { - if (preChannel.channelId !== channelId) return preChannel; - - const selected = preChannel.selected ?? {}; - selected[subChannelId] = status; - - return { ...preChannel, selected }; - }); - }); - }; - const handleCheckAll = (guild: IGuildChannels, status: boolean) => { - const selectedGuild = channels.find( - (channel) => channel.channelId === guild.channelId - ); - if (!selectedGuild) return; - - const updatedChannels = channels.map((channel: IGuildChannels) => { - if (channel === selectedGuild) { - const selected = { ...channel.selected }; - Object.keys(selected).forEach((key) => (selected[key] = status)); - return { ...channel, selected }; - } - return channel; - }); - - setChannels(updatedChannels); - }; - - const refetchChannels = () => { - refetchGuildChannels(guild?.guildId); - }; - - const submitChannels = () => { - const subChannelsStatus = channels.map((channel: IGuildChannels) => { - return channel.selected; - }); - - const selectedChannelsStatus = Object.assign({}, ...subChannelsStatus); - let activeChannel: string[] = []; - for (const key in selectedChannelsStatus) { - if (selectedChannelsStatus[key]) { - activeChannel.push(key); - } - } - - const result = ([] as IChannelWithoutId[]).concat( - ...channels.map((channel: IGuildChannels) => { - return channel.subChannels - .filter((subChannel: ISubChannels) => { - if ( - activeChannel.includes(subChannel.channelId) && - subChannel.canReadMessageHistoryAndViewChannel - ) { - return subChannel; - } - }) - .map((filterdItem: ISubChannels) => { - return { - channelId: filterdItem.channelId, - channelName: filterdItem.name, - }; - }); - }) - ); - - setSelectedChannels(result); - if (emitable) { - if (submit) submit(result); - setOpen(false); - } else { - setOpen(false); - SetOpenProcessing(true); - } - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleCloseProcessingModal = () => { - SetOpenProcessing(false); - }; - const handleToProcess = () => { - updateSelectedChannels(guild?.guildId, selectedChannels).then( - (_res: unknown) => { - SetOpenProcessing(false); - getUserGuildInfo(guild?.guildId); - } - ); - }; - - if (guilds.length === 0) { - return ( -
    - Selected channels: {selectedChannels.length}{' '} - setOpen(true)} - > - Show Channels - -

    - - - There is no community connected at the moment. To be able to change - channels, please
    connect your community first. -
    -

    -
    - ); - } - - return ( -
    -

    - Selected channels:{' '} - - {guildInfo && guildInfo.isDisconnected ? 0 : selectedChannels.length} - {' '} - setOpen(true)} - > - Show Channels - -

    - {guildInfo && guildInfo.isInProgress ? ( -
    - -

    - We are processing data from selected channels. It might take up to 6 - hours to complete. -

    -
    - ) : ( - '' - )} - -
    -
    -
    -

    - Import activities from channels -

    - -
    -

    - Select channels to import activity in this workspace. Please give - Together Crew access to all selected private channels by updating - the channels permissions in Discord. Discord permission will - affect the channels the bot can see. -

    -
    -
    - {isRefetchLoading ? ( - - ) : ( -
    -
    - } - size="large" - variant="outlined" - onClick={refetchChannels} - /> -
    - {channels && channels.length > 0 - ? channels.map((guild: IGuildChannels, index: number) => { - return ( -
    - -
    - ); - }) - : ''} -
    - )} -
    - - - } - > -

    - How to give access to the channel you want to import? -

    -
    - -
    -
      -
    1. - Navigate to the channel you want to import on{' '} - - Discord - -
    2. -
    3. - Go to the settings for that specific channel (select the - wheel on the right of the channel name) -
    4. -
    5. - Select Permissions (left sidebar), and then in the - middle of the screen check Advanced permissions -
    6. -
    7. - With the TogetherCrew Bot selected, under Advanced - Permissions, make sure that [View channel] and [Read message - history] are marked as [✓] -
    8. -
    9. - Select the plus sign to the right of Roles/Members and under - members select TogetherCrew bot -
    10. -
    11. - Click on the Refresh List button on this window and - select the new channels -
    12. -
    -
    -
    -
    -
    - -
    -
    -
    - {' '} -
    - ); -} - -ChannelSelection.defaultProps = { - emitable: false, -}; diff --git a/src/components/pages/settings/ConfirmStartProcessing.spec.tsx b/src/components/pages/settings/ConfirmStartProcessing.spec.tsx deleted file mode 100644 index 7d6ed312..00000000 --- a/src/components/pages/settings/ConfirmStartProcessing.spec.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import ConfirmStartProcessing from './ConfirmStartProcessing'; - -describe('ConfirmStartProcessing', () => { - const onClose = jest.fn(); - const onSubmitProcess = jest.fn(); - - beforeEach(() => { - onClose.mockClear(); - onSubmitProcess.mockClear(); - }); - - it('renders the correct text', () => { - render( - - ); - - expect( - screen.getByText( - /Data from selected channels may take some time to process/i - ) - ).toBeInTheDocument(); - expect( - screen.getByText( - /Please confirm you want to start data processing. It might take up to 6 hours to complete. Once it is done we will send you a message on Discord./i - ) - ).toBeInTheDocument(); - expect( - screen.getByText( - /During this period, it will not be possible to change your imported channels./i - ) - ).toBeInTheDocument(); - expect(screen.getByText(/Cancel/i)).toBeInTheDocument(); - expect(screen.getByText('Start data processing')).toBeInTheDocument(); - }); - - it('calls onClose when cancel button is clicked', () => { - render( - - ); - - const cancelButton = screen.getByText(/Cancel/i); - fireEvent.click(cancelButton); - - expect(onClose).toHaveBeenCalledTimes(1); - }); - - it('calls onSubmitProcess when start button is clicked', () => { - render( - - ); - - const startButton = screen.getByText('Start data processing'); - fireEvent.click(startButton); - - expect(onSubmitProcess).toHaveBeenCalledTimes(1); - }); - - it('calls onClose when close icon is clicked', () => { - render( - - ); - - const closeIcon = screen.getByTestId('close-modal-icon'); - fireEvent.click(closeIcon); - - expect(onClose).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/components/pages/settings/ConfirmStartProcessing.tsx b/src/components/pages/settings/ConfirmStartProcessing.tsx deleted file mode 100644 index 2e952cb4..00000000 --- a/src/components/pages/settings/ConfirmStartProcessing.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Dialog, DialogTitle } from '@mui/material'; -import React from 'react'; -import { BsClockHistory } from 'react-icons/bs'; -import CustomButton from '../../global/CustomButton'; -import { IoClose } from 'react-icons/io5'; - -interface ConfirmStartProcessingProps { - open: boolean; - onClose: () => void; - onSubmitProcess: () => void; -} - -function ConfirmStartProcessing(props: ConfirmStartProcessingProps) { - const { open, onClose, onSubmitProcess } = props; - return ( - - - - -
    - -

    - Data from selected channels may take some time to process -

    -

    - Please confirm you want to start data processing. It might take up to - 6 hours to complete. Once it is done we will send you a message on - Discord. -

    -

    - During this period, it will not be possible to change your imported - channels. -

    -
    - - -
    -
    -
    - ); -} - -export default ConfirmStartProcessing; diff --git a/src/components/pages/settings/ConnectCommunities.tsx b/src/components/pages/settings/ConnectCommunities.tsx deleted file mode 100644 index 19a3f4b3..00000000 --- a/src/components/pages/settings/ConnectCommunities.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Paper, Tooltip, Typography } from '@mui/material'; -import { useEffect, useState } from 'react'; -import { FaDiscord } from 'react-icons/fa'; -import { GoPlus } from 'react-icons/go'; -import CustomButton from '../../global/CustomButton'; -import DatePeriodRange from '../../global/DatePeriodRange'; -import CustomModal from '../../global/CustomModal'; -import ChannelSelection from './ChannelSelection'; -import { BsClockHistory, BsTwitter } from 'react-icons/bs'; -import useAppStore from '../../../store/useStore'; -import { useRouter } from 'next/router'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import { IUser } from '../../../utils/types'; - -import { - setAmplitudeUserIdFromToken, - trackAmplitudeEvent, -} from '../../../helpers/amplitudeHelper'; -import { decodeUserTokenDiscordId } from '../../../helpers/helper'; - -export default function ConnectCommunities() { - const router = useRouter(); - - const user = StorageService.readLocalStorage('user'); - - const [open, setOpen] = useState(false); - const [confirmModalOpen, setConfirmModalOpen] = useState(false); - const [guildId, setGuildId] = useState(''); - const [activePeriod, setActivePeriod] = useState(1); - const [datePeriod, setDatePeriod] = useState(''); - const [selectedChannels, setSelectedChannels] = useState([]); - - const { - guilds, - connectNewGuild, - patchGuildById, - getUserGuildInfo, - authorizeTwitter, - } = useAppStore(); - - if (typeof window !== 'undefined') { - useEffect(() => { - if (Object.keys(router?.query).length > 0 && router.query.isSuccessful) { - const { guildId, guildName } = router?.query; - let user: any = StorageService.readLocalStorage('user'); - user = { token: user.token, guild: { guildId, guildName } }; - StorageService.writeLocalStorage('user', user); - setGuildId(guildId); - toggleModal(true); - setDatePeriod( - moment().subtract('35', 'days').format('YYYY-MM-DDTHH:mm:ss[Z]') - ); - } - }, [router]); - } - - const updateSelectedChannels = (channels: any) => { - setSelectedChannels(channels); - }; - - const handleActivePeriod = (dateRangeType: number | string) => { - let dateTime = ''; - switch (dateRangeType) { - case 1: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('35', 'days') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 2: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('3', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 3: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('6', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 4: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('1', 'year') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - default: - break; - } - setDatePeriod(dateTime); - }; - - const submitGuild = async () => { - await patchGuildById(guildId, datePeriod, selectedChannels).then( - (_res: any) => { - setOpen(false); - toggleConfirmModal(true); - } - ); - }; - - const toggleModal = (e: boolean) => { - setOpen(e); - }; - - const toggleConfirmModal = (e: boolean) => { - setConfirmModalOpen(e); - router.replace({ - pathname: '/settings', - }); - }; - - const handleConnectedGuild = () => { - const user: IUser | undefined = - StorageService.readLocalStorage('user'); - - setAmplitudeUserIdFromToken(); - - trackAmplitudeEvent({ - eventType: 'update_connected_guild_on_settings', - eventProperties: { - guild: user?.guild, - }, - }); - getUserGuildInfo(guildId); - setConfirmModalOpen(false); - }; - - const handleAuthorizeTwitter = () => { - authorizeTwitter(decodeUserTokenDiscordId(user)); - }; - const isAllTwitterPropertiesNull = - user && - user.twitter && - Object.values(user.twitter).every((value) => value == null); - - return ( - <> - -
    - -

    {"Perfect, you're all set!"}

    -

    - Data import just started. It might take up to 6 hours to finish. - Once it is done we will send you a message on Discord. -

    - { - handleConnectedGuild(); - }} - /> -
    -
    - -
    -

    - Choose date period for data analysis -

    -

    - You will be able to change date period and selected channels in the - future. -

    - -
    -
    -

    - Confirm your imported channels -

    - updateSelectedChannels(channels)} - /> -
    - { - submitGuild(); - }} - /> -
    -
    -
    -
    -
    -

    - Connect your communities -

    -
    - {isAllTwitterPropertiesNull ? ( -
    - handleAuthorizeTwitter()} - > -

    Twitter

    - -
    - -

    Connect

    -
    -
    -
    - ) : ( - <> - )} -
    - {guilds.length >= 1 ? ( - - It will be possible to connect more communities soon. - - } - arrow - placement="right" - > - -

    Discord

    - -
    - -

    Connect

    -
    -
    -
    - ) : ( - connectNewGuild()} - > -

    Discord

    - -
    - -

    Connect

    -
    -
    - )} -
    -
    -
    -
    - - ); -} diff --git a/src/components/pages/settings/ConnectedCommunitiesItem.tsx b/src/components/pages/settings/ConnectedCommunitiesItem.tsx deleted file mode 100644 index 787156e5..00000000 --- a/src/components/pages/settings/ConnectedCommunitiesItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Paper, Tooltip, Typography } from '@mui/material'; -import { FaDiscord } from 'react-icons/fa'; -import Image from 'next/image'; -import moment from 'moment'; - -type IProps = { - guild: any; - onClick: (guildId: string) => void; -}; -export default function ConnectedCommunitiesItem({ guild, onClick }: IProps) { - return ( -
    - -
    -
    -

    Discord

    - {!guild.isInProgress || guild.isDisconnected ? ( - - {guild.isDisconnected - ? 'We don’t have access to your server anymore. Please make sure the Bot is installed properly.' - : !guild.isInProgress - ? 'Discord is connected' - : 'The Discord bot has been connected, and we need time to analyze your data'} - - } - arrow - placement="right" - > - - - ) : ( - - )} -
    - -
    -
    - {guild.guildId && guild.icon ? ( - {guild.name - ) : ( -
    - )} -
    -

    {guild.name}

    -

    - {!guild.isInProgress || guild.isDisconnected - ? `Connected ${moment(guild.connectedAt).format('DD MMM yyyy')}` - : 'Data import in progress'} -

    -
    -
    -
    onClick(guild.guildId)} - > - Disconnect -
    - -
    - ); -} diff --git a/src/components/pages/settings/ConnectedCommunitiesList.tsx b/src/components/pages/settings/ConnectedCommunitiesList.tsx deleted file mode 100644 index f1972bb2..00000000 --- a/src/components/pages/settings/ConnectedCommunitiesList.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useState } from 'react'; -import CustomModal from '../../global/CustomModal'; -import CustomButton from '../../global/CustomButton'; -import ConnectedCommunitiesItem from './ConnectedCommunitiesItem'; -import { toast } from 'react-toastify'; -import { FaRegCheckCircle } from 'react-icons/fa'; -import { Paper } from '@mui/material'; -import useAppStore from '../../../store/useStore'; -import { DISCONNECT_TYPE } from '../../../store/types/ISetting'; -import { StorageService } from '../../../services/StorageService'; -import { ITwitter, IUser } from '../../../utils/types'; - -import { - setAmplitudeUserIdFromToken, - trackAmplitudeEvent, -} from '../../../helpers/amplitudeHelper'; -import ConnectedTwitter from './ConnectedTwitter'; - -export default function ConnectedCommunitiesList({ guilds }: any) { - const { disconnecGuildById, getGuilds } = useAppStore(); - const [open, setOpen] = useState(false); - const [guildId, setGuildId] = useState(''); - const toggleModal = (e: boolean) => { - setOpen(e); - }; - let user: IUser | undefined = StorageService.readLocalStorage('user'); - const notify = () => { - toast('The integration has been disconnected succesfully.', { - position: 'top-center', - autoClose: 3000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: false, - progress: undefined, - closeButton: false, - theme: 'light', - icon: , - }); - }; - - const disconnectGuild = (discconectType: DISCONNECT_TYPE) => { - disconnecGuildById(guildId, discconectType).then((_res: any) => { - notify(); - getGuilds(); - - setAmplitudeUserIdFromToken(); - - trackAmplitudeEvent({ - eventType: 'disconnect_guild_on_setting', - eventProperties: { - guild: user?.guild, - }, - }); - - if (user) { - user = { token: user.token, guild: { guildId: '', guildName: '' } }; - StorageService.writeLocalStorage('user', user); - } - }); - }; - - function isAllTwitterPropertiesNull(twitter: ITwitter): boolean { - return ( - twitter.twitterConnectedAt === null && - twitter.twitterId === null && - twitter.twitterProfileImageUrl === null && - twitter.twitterUsername === null - ); - } - - return ( - <> - {guilds && guilds.length > 0 ? ( -
    -

    Connected communities

    -
    - {guilds && guilds.length > 0 - ? guilds.map((guild: any) => ( -
    - { - setGuildId(guildId), setOpen(true); - }} - /> -
    - )) - : ''} - {user?.twitter && !isAllTwitterPropertiesNull(user.twitter) ? ( -
    - -
    - ) : ( - <> - )} -
    -
    - ) : ( - '' - )} - -
    -

    - Are you sure you want to disconnect{' '} -
    your community? -

    -
    - -
    -

    Disconnect and delete data

    -

    - Importing activities and members will be stopped. Historical - activities will be deleted. -

    -
    - { - disconnectGuild('hard'); - }} - /> -
    - -
    -

    Disconnect only

    -

    - Importing activities and members will be stopped. Historical - activities will not be affected. -

    -
    - { - disconnectGuild('soft'); - }} - /> -
    -
    -
    -
    - - ); -} diff --git a/src/components/pages/settings/ConnectedTwitter.tsx b/src/components/pages/settings/ConnectedTwitter.tsx deleted file mode 100644 index de5be7b0..00000000 --- a/src/components/pages/settings/ConnectedTwitter.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Avatar, Paper } from '@mui/material'; -import React from 'react'; -import { ITwitter } from '../../../utils/types'; -import useAppStore from '../../../store/useStore'; -import { BsTwitter } from 'react-icons/bs'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import clsx from 'clsx'; - -interface IConnectedTwitter { - twitter?: ITwitter; -} - -function ConnectedTwitter({ twitter }: IConnectedTwitter) { - const { disconnectTwitter, getUserInfo } = useAppStore(); - - const handleDisconnect = async () => { - try { - await disconnectTwitter(); - - const userInfo = await getUserInfo(); - const { - twitterConnectedAt, - twitterId, - twitterProfileImageUrl, - twitterUsername, - } = userInfo; - - StorageService.updateLocalStorageWithObject('user', 'twitter', { - twitterConnectedAt, - twitterId, - twitterProfileImageUrl, - twitterUsername, - }); - - StorageService.removeLocalStorage('lastTwitterMetricsRefreshDate'); - } catch (error) { - console.error('Error handling disconnect:', error); - } - }; - - return ( -
    - -
    -
    -

    Twitter

    - -
    - -
    -
    - -
    -

    {twitter?.twitterUsername}

    -

    {`Connected ${moment( - twitter?.twitterConnectedAt - ).format('DD MMM yyyy')}`}

    -
    -
    -
    - Disconnect -
    -
    -
    - ); -} - -export default ConnectedTwitter; diff --git a/src/components/pages/settings/DataAnalysis.tsx b/src/components/pages/settings/DataAnalysis.tsx deleted file mode 100644 index cd80b222..00000000 --- a/src/components/pages/settings/DataAnalysis.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import CustomModal from '../../global/CustomModal'; -import CustomButton from '../../global/CustomButton'; -import DatePeriodRange from '../../global/DatePeriodRange'; -import { BsClockHistory } from 'react-icons/bs'; -import useAppStore from '../../../store/useStore'; -import { FiInfo } from 'react-icons/fi'; -import { BiError } from 'react-icons/bi'; -import moment from 'moment'; -import { StorageService } from '../../../services/StorageService'; -import { IGuild, IUser } from '../../../utils/types'; - -export default function DataAnalysis() { - const [activePeriod, setActivePeriod] = useState(1); - const [guild, setGuild] = useState(); - const [analysisStateDate, setAnalysisStartDate] = useState(''); - const [open, setOpen] = useState(false); - const [datePeriod, setDatePeriod] = useState(''); - const [isDisabled, toggleDisabled] = useState(true); - const { guildInfo, updateAnalysisDatePeriod, getUserGuildInfo, guilds } = - useAppStore(); - - const handleActivePeriod = (dateRangeType: number | string) => { - let dateTime = ''; - switch (dateRangeType) { - case 1: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('35', 'days') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 2: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('3', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 3: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('6', 'months') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - case 4: - setActivePeriod(dateRangeType); - dateTime = moment() - .subtract('1', 'year') - .format('YYYY-MM-DDTHH:mm:ss[Z]'); - break; - default: - break; - } - setDatePeriod(dateTime); - toggleDisabled(false); - }; - - useEffect(() => { - const user = StorageService.readLocalStorage('user'); - if (user) { - setGuild(user.guild); - } - const start = moment(guildInfo.period, 'YYYY-MM-DD'); - const end = moment(); - - const datePeriod = Math.round(moment.duration(end.diff(start)).asMonths()); - - if (datePeriod <= 1) { - setActivePeriod(1); - } else if (datePeriod <= 3) { - setActivePeriod(2); - } else if (datePeriod > 3 && datePeriod <= 6) { - setActivePeriod(3); - } else { - setActivePeriod(4); - } - - setAnalysisStartDate(guildInfo.period); - }, [guildInfo]); - - const toggleModal = (e: boolean) => { - setOpen(e); - }; - - const submitNewDatePeriod = () => { - updateAnalysisDatePeriod(guild?.guildId, datePeriod).then((_res: any) => { - getUserGuildInfo(guild?.guildId); - setOpen(false); - }); - }; - - if (guilds.length === 0) { - return ( -
    -

    - It might take up to 6 hours to finish new data import. Once it is done - we will
    send you a message on Discord. -

    -

    - - - There is no community connected at the moment. To be able to select - the date period, -
    please connect your community - first. -
    -

    -
    - - toggleModal(true)} - /> -
    -
    - ); - } - - return ( -
    -

    - It might take up to 6 hours to finish new data import. Once it is done - we will
    send you a message on Discord. -

    -

    - - - Data analysis runs from:{' '} - {moment(analysisStateDate).format('DD MMMM yyyy')} - -

    - -
    - toggleModal(true)} - /> -
    - -
    - -

    - We are changing date period for data analysis now -

    -

    - It might take up to 6 hours to finish new data import.{' '} -
    Once it is done we will send you a - message on Discord. -

    - -
    -
    -
    - ); -} diff --git a/src/components/pages/settings/IntegrateDiscord.tsx b/src/components/pages/settings/IntegrateDiscord.tsx deleted file mode 100644 index 951025aa..00000000 --- a/src/components/pages/settings/IntegrateDiscord.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import useAppStore from '../../../store/useStore'; -import ConnectCommunities from './ConnectCommunities'; -import ConnectedCommunitiesList from './ConnectedCommunitiesList'; - -export default function IntegrateDiscord() { - const { guilds } = useAppStore(); - - return ( -
    - - -
    - ); -} diff --git a/src/components/shared/TcAutocomplete.tsx b/src/components/shared/TcAutocomplete.tsx new file mode 100644 index 00000000..287497ea --- /dev/null +++ b/src/components/shared/TcAutocomplete.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; + +interface TcAutocompleteProps + extends Omit, 'renderInput'> { + label?: string; + placeholder?: string; + textFieldProps?: TextFieldProps; +} + +function TcAutocomplete({ + options, + label, + placeholder, + textFieldProps, + ...props +}: TcAutocompleteProps) { + return ( + ( + + )} + {...props} + /> + ); +} + +export default TcAutocomplete; diff --git a/src/components/shared/TcBreadcrumbs.tsx b/src/components/shared/TcBreadcrumbs.tsx index 465b792b..4abdae16 100644 --- a/src/components/shared/TcBreadcrumbs.tsx +++ b/src/components/shared/TcBreadcrumbs.tsx @@ -1,37 +1,13 @@ -/** - * `TcBreadcrumbs` Component - * - * This component is used for displaying a breadcrumb navigation interface. It is built - * using Material-UI's `Breadcrumbs` and a custom `TcLink` component for navigation. - * - * Props: - * - `items`: An array of `BreadcrumbItem` objects. Each `BreadcrumbItem` should have: - * - `label` (string): The text displayed for the breadcrumb link. - * - `path` (string): The navigation path the breadcrumb link points to. - * - * Usage: - * - * - * This component renders breadcrumbs for the provided `items` array. Each item in the array - * represents a single breadcrumb link. The component uses flexbox for alignment and spacing, - * and includes a hover effect on the links for better user interaction. - */ - import React from 'react'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import { useRouter } from 'next/router'; import TcLink from './TcLink'; -import { MdOutlineKeyboardArrowLeft } from 'react-icons/md'; +import { MdChevronRight } from 'react-icons/md'; +import TcText from './TcText'; interface BreadcrumbItem { label: string; - path: string; + path?: string; } interface TcBreadcrumbsProps { @@ -50,22 +26,25 @@ function TcBreadcrumbs({ items }: TcBreadcrumbsProps) { }; return ( - - {items.map((item) => ( -
    } + > + {items.map((item, index) => ( + handleClick(event, item.path || '')} + underline={'none'} + className={`${ + index === items.length - 1 + ? 'pointer-events-none text-black' + : 'text-gray-500' + }`} + to={item.path || '#'} > - - handleClick(event, item.path)} - > - {item.label} - -
    + + ))}
    ); diff --git a/src/components/shared/TcButtonGroup/TcButtonGroup.tsx b/src/components/shared/TcButtonGroup/TcButtonGroup.tsx index a6433c31..3ea16c9f 100644 --- a/src/components/shared/TcButtonGroup/TcButtonGroup.tsx +++ b/src/components/shared/TcButtonGroup/TcButtonGroup.tsx @@ -1,11 +1,11 @@ import { ButtonGroup, ButtonGroupProps } from '@mui/material'; -import React, { ReactElement, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; -interface TcButtonGroup extends ButtonGroupProps { +interface ITcButtonGroup extends ButtonGroupProps { children: ReactNode; } -function TcButtonGroup({ children, ...props }: TcButtonGroup) { +function TcButtonGroup({ children, ...props }: ITcButtonGroup) { return {children}; } diff --git a/src/components/shared/TcDatePickerPopover.tsx b/src/components/shared/TcDatePickerPopover.tsx new file mode 100644 index 00000000..6e00729a --- /dev/null +++ b/src/components/shared/TcDatePickerPopover.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import Popover from '@mui/material/Popover'; +import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import TcButton from './TcButton'; + +interface ITcDatePickerPopoverProps { + open: boolean; + anchorEl: HTMLElement | null; + onClose: () => void; + selectedDate: Date | null; + onDateChange: (date: Date | null) => void; + onResetDate: () => void; +} + +function TcDatePickerPopover({ + open, + anchorEl, + onClose, + selectedDate, + onDateChange, + onResetDate, +}: ITcDatePickerPopoverProps) { + return ( + + + + +
    + +
    +
    + ); +} + +export default TcDatePickerPopover; diff --git a/src/components/shared/TcLink.tsx b/src/components/shared/TcLink.tsx index 192004b5..c7b2d7a3 100644 --- a/src/components/shared/TcLink.tsx +++ b/src/components/shared/TcLink.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { Link, LinkProps as MuiLinkProps } from '@mui/material'; interface CustomLinkProps extends MuiLinkProps { - to: string; + to?: string; } function TcLink({ children, to, ...props }: CustomLinkProps) { diff --git a/src/components/shared/TcPagination/TcPagination.spec.tsx b/src/components/shared/TcPagination/TcPagination.spec.tsx new file mode 100644 index 00000000..772ec1be --- /dev/null +++ b/src/components/shared/TcPagination/TcPagination.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TcPagination from './TcPagination'; + +describe('TcPagination', () => { + const totalItems = 100; + const itemsPerPage = 10; + const currentPage = 1; + const onChangePage = jest.fn(); + + it('renders the pagination component correctly', () => { + const { getByText } = render( + + ); + + // Ensure the pagination component renders with the correct total pages and current page. + expect(getByText('1')).toBeInTheDocument(); + expect(getByText('10')).toBeInTheDocument(); + }); + + it('calls onChangePage when a page is clicked', () => { + const { getByText } = render( + + ); + + // Click on page 2 + fireEvent.click(getByText('2')); + + // Ensure onChangePage is called with the correct page number (2) + expect(onChangePage).toHaveBeenCalledWith(2); + }); +}); diff --git a/src/components/shared/TcPagination/TcPagination.tsx b/src/components/shared/TcPagination/TcPagination.tsx new file mode 100644 index 00000000..8a95a1d2 --- /dev/null +++ b/src/components/shared/TcPagination/TcPagination.tsx @@ -0,0 +1,60 @@ +import { Pagination, PaginationItem, PaginationProps } from '@mui/material'; + +interface ITcPaginationProps extends PaginationProps { + totalItems: number; + itemsPerPage: number; + currentPage: number; + onChangePage: (page: number) => void; +} + +/** + * TcPagination Component + * + * A pagination component using Material-UI's `Pagination` to handle page navigation. + * + * @component + * @param {ITcPaginationProps} props - The props for configuring the pagination. + * @param {number} props.totalItems - The total number of items to paginate. + * @param {number} props.itemsPerPage - The number of items per page. + * @param {number} props.currentPage - The current active page. + * @param {(page: number) => void} props.onChangePage - A callback function to handle page changes. + * @returns {JSX.Element} - The rendered pagination component. + * + * @example + * // Usage: + * handlePageChange(page)} + * /> + */ + +function TcPagination({ + onChangePage, + currentPage, + itemsPerPage, + totalItems, + ...props +}: ITcPaginationProps): JSX.Element { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const handleChangePage = (page: number) => { + if (page !== currentPage) { + onChangePage(page); + } + }; + + return ( + handleChangePage(page)} + {...props} + renderItem={(item) => } + /> + ); +} + +export default TcPagination; diff --git a/src/components/shared/TcPagination/index.ts b/src/components/shared/TcPagination/index.ts new file mode 100644 index 00000000..8995d5b0 --- /dev/null +++ b/src/components/shared/TcPagination/index.ts @@ -0,0 +1,3 @@ +import { default as TcPagination } from './TcPagination'; + +export default TcPagination; diff --git a/src/components/shared/TcSelect/TcSelect.tsx b/src/components/shared/TcSelect/TcSelect.tsx index d911cd2d..124904ca 100644 --- a/src/components/shared/TcSelect/TcSelect.tsx +++ b/src/components/shared/TcSelect/TcSelect.tsx @@ -1,47 +1,57 @@ +import { MenuItem, Select, SelectProps } from '@mui/material'; +import React, { ReactElement } from 'react'; +import { IconType } from 'react-icons'; +import TcText from '../TcText'; + /** - * TcSelect Component - * - * This component is a wrapper around Material-UI's Select component. - * It provides a dropdown select box functionality. - * - * Props: - * - options: Array of objects with 'value' and 'label' keys. These are used to populate the dropdown menu. - * - * Example Usage: - * + * Interface for TcSelect props */ - -import React, { useState } from 'react'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; - -interface TcSelectProps { - options: { - value: string; +interface ITcSelectProps extends SelectProps { + /** + * options - Array of option objects for the select dropdown + * Each object can have: + * - value (string | number): The value of the option + * - label (string): The display label for the option + * - icon (ReactElement): Optional icon to display alongside the label + */ + options?: Array<{ + value: string | number; label: string; - }[]; + icon?: ReactElement; + disabled?: boolean; + }>; + children?: React.ReactNode; } -function TcSelect({ options, ...props }: TcSelectProps) { - const [value, setValue] = useState(''); - - const handleChange = (event: SelectChangeEvent) => { - const newValue = event.target.value as string; - setValue(newValue); - }; +/** + * TcSelect is a custom select component built on Material-UI's Select component. + * It allows displaying a list of options with optional icons. + * + * @param {ITcSelectProps} props - The props for the component + * @returns {ReactElement} The TcSelect component + */ +function TcSelect({ + options, + children, + ...props +}: ITcSelectProps): ReactElement { return ( - + {options && options.length > 0 + ? options.map((option) => ( + +
    + {option.icon && React.cloneElement(option.icon)} + +
    +
    + )) + : children} ); } diff --git a/src/components/shared/TcSwitch.spec.tsx b/src/components/shared/TcSwitch.spec.tsx new file mode 100644 index 00000000..3988c15c --- /dev/null +++ b/src/components/shared/TcSwitch.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TcSwitch from './TcSwitch'; + +describe('TcSwitch', () => { + test('it should toggle switch', () => { + const handleChange = jest.fn(); + const { getByRole } = render(); + + const switchControl = getByRole('checkbox'); + expect(switchControl).not.toBeChecked(); + + fireEvent.click(switchControl); + expect(handleChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/shared/TcSwitch.tsx b/src/components/shared/TcSwitch.tsx new file mode 100644 index 00000000..6d980173 --- /dev/null +++ b/src/components/shared/TcSwitch.tsx @@ -0,0 +1,37 @@ +import { Switch, SwitchProps } from '@mui/material'; +import React from 'react'; + +interface ITcSwitchProps extends SwitchProps {} + +/** + * `TcSwitch` Component + * + * This component is a wrapper around Material-UI's `Switch` component. + * It can be used anywhere a Material-UI Switch would be used. It accepts all props + * that a standard Material-UI Switch accepts. + * + * Usage: + * + * + * Props: + * - All props available to Material-UI's `Switch` component. + * - `checked`: Boolean indicating whether the switch is on or off. + * - `onChange`: Function to handle the change event when the switch is toggled. + * + * Example: + * ``` + * { this.setState({ isChecked: e.target.checked }) }} + * /> + * ``` + * + * For more details on Material-UI's `Switch` props, + * see: https://mui.com/api/switch/ + */ + +function TcSwitch({ ...props }: ITcSwitchProps) { + return ; +} + +export default TcSwitch; diff --git a/src/components/shared/TcTableContainer/TcTableBody.spec.tsx b/src/components/shared/TcTableContainer/TcTableBody.spec.tsx new file mode 100644 index 00000000..2e8d1bb4 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableBody.spec.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableBody from './TcTableBody'; + +describe('TcTableBody', () => { + const mockRowItems = [ + { Name: 'Alice', Age: 28, Location: 'New York' }, + { Name: 'Bob', Age: 34, Location: 'San Francisco' }, + ]; + + it('renders correctly with rowItems', () => { + render( + + +
    + ); + const tableRows = screen.getAllByRole('row'); + expect(tableRows.length).toBe(mockRowItems.length); + }); + + it('applies alternate background color for rows', () => { + render( + + +
    + ); + const firstRow = screen.getAllByRole('row')[0]; + expect(firstRow).toHaveClass('bg-gray-100'); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableBody.tsx b/src/components/shared/TcTableContainer/TcTableBody.tsx new file mode 100644 index 00000000..67b08449 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableBody.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { TableBody, TableBodyProps } from '@mui/material'; +import TcTableRow from './TcTableRow'; + +interface ITcTableBodyProps extends TableBodyProps { + rowItems: { [key: string]: any }[]; +} + +/** + * TcTableBody Component + * + * Renders a Material-UI TableBody with custom row items. + * Each row is rendered using the TcTableRow component. + * + * Props: + * - rowItems: Array of objects, each representing data for a single row. + * + * @param {ITcTableBodyProps} props - Props including rowItems and other TableBodyProps + */ + +function TcTableBody({ rowItems, ...props }: ITcTableBodyProps) { + return ( + + {rowItems.map((row, index) => ( + + ))} + + ); +} + +TcTableBody.defaultProps = { + rowItems: [], +}; + +export default TcTableBody; diff --git a/src/components/shared/TcTableContainer/TcTableCell.spec.tsx b/src/components/shared/TcTableContainer/TcTableCell.spec.tsx new file mode 100644 index 00000000..a605d4a4 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableCell.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableCell from './TcTableCell'; + +describe('TcTableCell', () => { + it('renders the children content', () => { + render(Test Content); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableCell.tsx b/src/components/shared/TcTableContainer/TcTableCell.tsx new file mode 100644 index 00000000..0717b3d2 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableCell.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TableCell, TableCellProps } from '@mui/material'; + +interface ITcTableCellProps extends TableCellProps { + children: React.ReactNode; +} + +/** + * TcTableCell Component + * + * Custom TableCell component that extends Material-UI's TableCell. + * It can be used within Material-UI's Table components to display cell data. + * + * Props: + * - children: ReactNode - The content of the cell. + * - Other props inherited from Material-UI TableCellProps. + * + * @param {ITcTableCellProps} props - Props including children and TableCellProps + */ + +function TcTableCell({ children, ...props }: ITcTableCellProps) { + return {children}; +} + +export default TcTableCell; diff --git a/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx b/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx new file mode 100644 index 00000000..bca231fe --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableContainer.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableContainer from './TcTableContainer'; + +describe('TcTableContainer', () => { + const mockHeaders = ['Header 1', 'Header 2']; + const mockBodyRowItems = [ + { Column1: 'Row 1, Column 1', Column2: 'Row 1, Column 2' }, + { Column1: 'Row 2, Column 1', Column2: 'Row 2, Column 2' }, + ]; + + it('renders the table with body row items', () => { + render(); + + // Check if each row text content is present in the document + mockBodyRowItems.forEach((rowData) => { + Object.values(rowData).forEach((cellText) => { + const cell = screen.getByText(cellText); + expect(cell).toBeInTheDocument(); + }); + }); + }); + + it('applies custom classes for border separation and spacing', () => { + render( + + ); + const table = screen.getByRole('table'); + expect(table).toHaveClass('border-separate border-spacing-y-2'); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableContainer.tsx b/src/components/shared/TcTableContainer/TcTableContainer.tsx new file mode 100644 index 00000000..9e580c73 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableContainer.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Table, TableProps } from '@mui/material'; +import TcTableHead from './TcTableHead'; +import TcTableBody from './TcTableBody'; + +interface ITcTableContainerProps extends TableProps { + headers?: string[]; + bodyRowItems?: any[]; +} + +/** + * TcTableContainer Component + * + * Custom Table component that extends Material-UI's Table. + * It can be used to display tabular data with optional custom border separation and spacing. + * + * Props: + * - headers: Array of strings - The table column headers. + * - bodyRowItems: Array of objects - The data for table rows. + * - Other props inherited from Material-UI TableProps. + * + * @param {ITcTableContainerProps} props - Props including headers, bodyRowItems, and TableProps + */ + +function TcTableContainer({ + headers, + bodyRowItems, + ...props +}: ITcTableContainerProps) { + return ( + + {headers && headers.length > 0 && } + {bodyRowItems && bodyRowItems.length > 0 && ( + + )} +
    + ); +} + +export default TcTableContainer; diff --git a/src/components/shared/TcTableContainer/TcTableHead.spec.tsx b/src/components/shared/TcTableContainer/TcTableHead.spec.tsx new file mode 100644 index 00000000..60d30645 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableHead.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableHead from './TcTableHead'; + +describe('TcTableHead', () => { + const mockHeaders = ['Header 1', 'Header 2']; + + it('renders the table head with headers', () => { + render(); + + // Check if each header text content is present in the document + mockHeaders.forEach((headerText) => { + const header = screen.getByText(headerText); + expect(header).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableHead.tsx b/src/components/shared/TcTableContainer/TcTableHead.tsx new file mode 100644 index 00000000..f301c81e --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableHead.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { TableHead, TableHeadProps, TableRow, TableCell } from '@mui/material'; +import TcTableRow from './TcTableRow'; + +interface ITcTableHeadProps extends TableHeadProps { + headers: string[]; +} + +/** + * Component to render the table head with headers. + * + * @param {ITcTableHeadProps} props - The component props. + */ + +function TcTableHead({ headers, ...props }: ITcTableHeadProps) { + return ( + + + + ); +} + +export default TcTableHead; diff --git a/src/components/shared/TcTableContainer/TcTableRow.spec.tsx b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx new file mode 100644 index 00000000..cdedc478 --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableRow.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TcTableRow from './TcTableRow'; + +describe('TcTableRow', () => { + it('renders correctly with row data', () => { + const rowData = { column1: 'Data1', column2: 'Data2' }; + render(); + expect(screen.getByText('Data1')).toBeInTheDocument(); + expect(screen.getByText('Data2')).toBeInTheDocument(); + }); + + it('applies custom renderers', () => { + const rowData = { column1: 'Data1' }; + const customRenderers = { + column1: (value: any) => {value}, + }; + render(); + const renderedData = screen.getByText('Data1'); + expect(renderedData).toBeInTheDocument(); + expect(renderedData).toHaveProperty('nodeName', 'STRONG'); + }); + + it('applies custom table cell classes', () => { + const rowData = { column1: 'Data1' }; + const customClasses = 'test-class'; + render( + + ); + const cell = screen.getByText('Data1').closest('td'); + expect(cell).toHaveClass(customClasses); + }); +}); diff --git a/src/components/shared/TcTableContainer/TcTableRow.tsx b/src/components/shared/TcTableContainer/TcTableRow.tsx new file mode 100644 index 00000000..bcadf15e --- /dev/null +++ b/src/components/shared/TcTableContainer/TcTableRow.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { TableRow, TableRowProps } from '@mui/material'; +import TcTableCell from './TcTableCell'; +import clsx from 'clsx'; + +interface ITcTableRowProps extends TableRowProps { + rowItem: { [key: string]: any }; + customTableCellClasses?: string; + customRenderers?: { [key: string]: (value: any) => React.ReactNode }; +} + +/** + * Component to render a table row with custom rendering options. + * + * @param {ITcTableRowProps} props - The component props. + */ + +function TcTableRow({ + rowItem, + customRenderers, + customTableCellClasses, + ...props +}: ITcTableRowProps) { + return ( + + {rowItem && + Object.entries(rowItem).map(([key, value], index) => { + const CustomRenderer = customRenderers?.[key]; + return ( + + {CustomRenderer ? CustomRenderer(value) : value} + + ); + })} + + ); +} + +export default TcTableRow; diff --git a/src/components/shared/TcTableContainer/index.ts b/src/components/shared/TcTableContainer/index.ts new file mode 100644 index 00000000..e1a070a6 --- /dev/null +++ b/src/components/shared/TcTableContainer/index.ts @@ -0,0 +1,3 @@ +import { default as TcTableContainer } from './TcTableContainer'; + +export default TcTableContainer; diff --git a/src/components/shared/TcTabs/TcTab/TcTab.tsx b/src/components/shared/TcTabs/TcTab/TcTab.tsx new file mode 100644 index 00000000..e05b7b2e --- /dev/null +++ b/src/components/shared/TcTabs/TcTab/TcTab.tsx @@ -0,0 +1,10 @@ +import { Tab, TabProps } from '@mui/material'; +import React from 'react'; + +interface ITcTabProps extends TabProps {} + +function TcTab({ ...props }: ITcTabProps) { + return ; +} + +export default TcTab; diff --git a/src/components/shared/TcTabs/TcTab/index.ts b/src/components/shared/TcTabs/TcTab/index.ts new file mode 100644 index 00000000..da866174 --- /dev/null +++ b/src/components/shared/TcTabs/TcTab/index.ts @@ -0,0 +1,3 @@ +import { default as TcTab } from './TcTab'; + +export default TcTab; diff --git a/src/components/shared/TcTabs/TcTabs.tsx b/src/components/shared/TcTabs/TcTabs.tsx new file mode 100644 index 00000000..46b53e53 --- /dev/null +++ b/src/components/shared/TcTabs/TcTabs.tsx @@ -0,0 +1,26 @@ +import { Tabs, TabsProps } from '@mui/material'; +import React from 'react'; + +interface ITcTabsProps extends TabsProps { + children: React.ReactElement | React.ReactElement[]; +} + +/** + * `TcTabs` is a functional React component that renders Material-UI's `Tabs` component + * along with any child components passed to it. This component allows for the standard + * functionality of MUI's `Tabs` while also enabling the insertion of `Tab` components + * or other custom elements as children. + * + * @param {ITcTabsProps} props - Includes standard properties of MUI's `Tabs` component + * and any additional props defined in `ITcTabsProps`. The `children` prop is explicitly + * typed to accept either a single React element or an array of React elements, which are + * typically `Tab` components. + * + * @returns {React.ReactElement} - A `Tabs` component from Material-UI, rendering the passed + * children within. + */ +function TcTabs({ children, ...props }: ITcTabsProps): React.ReactElement { + return {children}; +} + +export default TcTabs; diff --git a/src/components/shared/TcTabs/index.ts b/src/components/shared/TcTabs/index.ts new file mode 100644 index 00000000..6dd42fa9 --- /dev/null +++ b/src/components/shared/TcTabs/index.ts @@ -0,0 +1,3 @@ +import { default as TcTabs } from './TcTabs'; + +export default TcTabs; diff --git a/src/configs/index.ts b/src/configs/index.ts index 65054cb1..c75829df 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -7,8 +7,5 @@ export const conf = { AMPLITUDEANALYTICS_TOKEN: process.env.NEXT_PUBLIC_AMPLITUDEANALYTICS_TOKEN, PROPERTY_ID: process.env.NEXT_PUBLIC_TAWK_PROPERTY_ID, WEIGHT_ID: process.env.NEXT_PUBLIC_TAWK_WEIGHT_ID, - SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, - SENTRY_ENV: process.env.NEXT_PUBLIC_SENTRY_ENV, - SENTRY_TOKEN: process.env.SENTRY_TOKEN, DISCORD_CDN: process.env.NEXT_PUBLIC_DISCORD_CDN, }; diff --git a/src/context/ChannelContext.tsx b/src/context/ChannelContext.tsx index a789f5e8..deb54d0f 100644 --- a/src/context/ChannelContext.tsx +++ b/src/context/ChannelContext.tsx @@ -6,6 +6,7 @@ export interface SubChannel { name: string; parentId: string; canReadMessageHistoryAndViewChannel: boolean; + announcementAccess: boolean; } export interface Channel { @@ -30,7 +31,8 @@ interface ChannelContextProps { platformId: string, property?: 'channel', selectedChannels?: string[], - hideDeactiveSubchannels?: boolean + hideDeactiveSubchannels?: boolean, + allDefaultChecked?: boolean ) => Promise; handleSubChannelChange: (channelId: string, subChannelId: string) => void; handleSelectAll: (channelId: string, subChannels: SubChannel[]) => void; @@ -57,7 +59,8 @@ const initialChannelContextData: ChannelContextProps = { platformId: string, property?: 'channel', selectedChannels?: string[], - hideDeactiveSubchannels?: boolean + hideDeactiveSubchannels?: boolean, + allDefaultChecked?: boolean ) => {}, handleSubChannelChange: (channelId: string, subChannelId: string) => {}, handleSelectAll: (channelId: string, subChannels: SubChannel[]) => {}, @@ -84,7 +87,8 @@ export const ChannelProvider = ({ children }: ChannelProviderProps) => { platformId: string, property: 'channel' = 'channel', selectedChannels?: string[], - hideDeactiveSubchannels: boolean = false + hideDeactiveSubchannels: boolean = false, + allDefaultChecked: boolean = true ) => { setLoading(true); try { @@ -101,8 +105,13 @@ export const ChannelProvider = ({ children }: ChannelProviderProps) => { (acc: any, channel: any) => { acc[channel.channelId] = channel.subChannels.reduce( (subAcc: any, subChannel: any) => { - subAcc[subChannel.channelId] = - subChannel.canReadMessageHistoryAndViewChannel; + if (allDefaultChecked) { + subAcc[subChannel.channelId] = + subChannel.canReadMessageHistoryAndViewChannel; + } else { + subAcc[subChannel.channelId] = false; + } + return subAcc; }, {} as { [subChannelId: string]: boolean } diff --git a/src/context/TokenContext.tsx b/src/context/TokenContext.tsx index becf358c..25852924 100644 --- a/src/context/TokenContext.tsx +++ b/src/context/TokenContext.tsx @@ -21,7 +21,7 @@ type TokenContextType = { clearToken: () => void; }; -const TokenContext = createContext(null); +export const TokenContext = createContext(null); type TokenProviderProps = { children: ReactNode; diff --git a/src/helpers/helper.ts b/src/helpers/helper.ts index ee764a08..b7819636 100644 --- a/src/helpers/helper.ts +++ b/src/helpers/helper.ts @@ -1,3 +1,4 @@ +import moment from 'moment'; import { SelectedSubChannels } from '../context/ChannelContext'; import { IDecodedToken } from '../utils/interfaces'; import { IUser } from '../utils/types'; @@ -120,3 +121,16 @@ export function hexToRGBA(hex: string, opacity: number): string { return `rgba(${r}, ${g}, ${b}, ${opacity})`; } + +export function validateDateTime(date: Date | null, time: Date | null) { + if (date && time) { + const selectedDateTime = moment(date).set({ + hour: time.getHours(), + minute: time.getMinutes(), + second: 0, + }); + + return selectedDateTime.isAfter(moment()); + } + return false; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 39959383..ea9dc52d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -24,6 +24,7 @@ import Script from 'next/script'; import { usePageViewTracking } from '../helpers/amplitudeHelper'; import SafaryClubScript from '../components/global/SafaryClubScript'; import { TokenProvider } from '../context/TokenContext'; +import { ChannelProvider } from '../context/ChannelContext'; export default function App({ Component, pageProps }: ComponentWithPageLayout) { usePageViewTracking(); @@ -58,15 +59,17 @@ export default function App({ Component, pageProps }: ComponentWithPageLayout) { - {Component.pageLayout ? ( - - - - - - ) : ( - - )} + + {Component.pageLayout ? ( + + + + + + ) : ( + + )} + diff --git a/src/pages/announcements/create-new-announcements.tsx b/src/pages/announcements/create-new-announcements.tsx new file mode 100644 index 00000000..03448945 --- /dev/null +++ b/src/pages/announcements/create-new-announcements.tsx @@ -0,0 +1,287 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { defaultLayout } from '../../layouts/defaultLayout'; +import SEO from '../../components/global/SEO'; +import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; +import TcPublicMessageContainer from '../../components/announcements/create/publicMessageContainer/TcPublicMessageContainer'; +import TcPrivateMessageContainer from '../../components/announcements/create/privateMessaageContainer/TcPrivateMessageContainer'; +import TcButton from '../../components/shared/TcButton'; +import TcScheduleAnnouncement from '../../components/announcements/create/scheduleAnnouncement/'; +import TcSelectPlatform from '../../components/announcements/create/selectPlatform'; +import TcBreadcrumbs from '../../components/shared/TcBreadcrumbs'; +import TcConfirmSchaduledAnnouncementsDialog from '../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; +import useAppStore from '../../store/useStore'; +import { useToken } from '../../context/TokenContext'; +import { ChannelContext } from '../../context/ChannelContext'; +import { IRoles, IUser } from '../../utils/interfaces'; +import { useSnackbar } from '../../context/SnackbarContext'; +import { useRouter } from 'next/router'; +import SimpleBackdrop from '../../components/global/LoadingBackdrop'; +import { FormControlLabel } from '@mui/material'; +import { MdOutlineAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../../components/announcements/create/TcIconContainer'; +import TcIconWithTooltip from '../../components/shared/TcIconWithTooltip'; +import TcSwitch from '../../components/shared/TcSwitch'; +import TcText from '../../components/shared/TcText'; + +export type CreateAnnouncementsPayloadDataOptions = + | { channelIds: string[]; userIds?: string[]; roleIds?: string[] } + | { channelIds?: string[]; userIds: string[]; roleIds?: string[] } + | { channelIds?: string[]; userIds?: string[]; roleIds: string[] }; + +export interface CreateAnnouncementsPayloadData { + platformId: string; + template: string; + options: CreateAnnouncementsPayloadDataOptions; +} +export interface CreateAnnouncementsPayload { + title: string; + communityId: string; + scheduledAt: string; + draft: boolean; + data: CreateAnnouncementsPayloadData[]; +} + +function CreateNewAnnouncements() { + const router = useRouter(); + const { createNewAnnouncements, retrievePlatformById } = useAppStore(); + + const { community } = useToken(); + + const channelContext = useContext(ChannelContext); + const { showMessage } = useSnackbar(); + + const { refreshData } = channelContext; + + const [channels, setChannels] = useState([]); + const [roles, setRoles] = useState([]); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [isDateValid, setIsDateValid] = useState(true); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [publicAnnouncements, setPublicAnnouncements] = + useState(); + + const [privateAnnouncements, setPrivateAnnouncements] = + useState(); + + const [scheduledAt, setScheduledAt] = useState(); + + const fetchPlatformChannels = async () => { + setLoading(true); + try { + if (platformId) { + await retrievePlatformById(platformId); + await refreshData(platformId, 'channel', undefined, undefined, false); + } + setLoading(false); + } catch (error) { + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!platformId) { + return; + } + + fetchPlatformChannels(); + }, [platformId]); + + const handleCreateAnnouncements = async (isDrafted: boolean) => { + if (!community) return; + + const data = [publicAnnouncements]; + + if (privateAnnouncements && privateAnnouncements.length > 0) { + data.push(...privateAnnouncements); + } + + const announcementsPayload = { + communityId: community.id, + draft: isDrafted, + scheduledAt: scheduledAt, + data: data, + }; + + try { + setLoading(true); + const data = await createNewAnnouncements(announcementsPayload); + if (data) { + showMessage('Announcement created successfully', 'success'); + router.push('/announcements'); + } + } catch (error) { + showMessage('Failed to create announcement', 'error'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ; + } + + return ( + <> + +
    + + +
    + + { + setScheduledAt(selectedTime); + }} + isDateValid={isDateValid} + setIsDateValid={setIsDateValid} + /> + { + if (!platformId) return; + setChannels(selectedChannels); + setPublicAnnouncements({ + platformId: platformId, + template: message, + options: { + channelIds: selectedChannels.map( + (channel) => channel.id + ), + }, + }); + }} + /> +
    + + + + + +
    + {/* { + if (!platformId) return; + + const commonData = { + platformId: platformId, + template: message, + }; + + let privateAnnouncementsOptions: { + roleIds: string[]; + userIds: string[]; + } = { + roleIds: [], + userIds: [], + }; + + if (selectedRoles && selectedRoles.length > 0) { + setRoles(selectedRoles); + privateAnnouncementsOptions.roleIds = selectedRoles.map( + (role) => role.roleId.toString() + ); + } + + if (selectedUsers && selectedUsers.length > 0) { + setUsers(selectedUsers); + privateAnnouncementsOptions.userIds = selectedUsers.map( + (user) => user.discordId + ); + } + + if ( + privateAnnouncementsOptions.roleIds.length > 0 || + privateAnnouncementsOptions.userIds.length > 0 + ) { + const combinedPrivateAnnouncement = { + ...commonData, + options: privateAnnouncementsOptions, + }; + + setPrivateAnnouncements([combinedPrivateAnnouncement]); + } + }} + /> */} +
    +
    + router.push('/announcements')} + variant="outlined" + sx={{ + maxWidth: { + xs: '100%', + sm: '8rem', + }, + }} + /> +
    + handleCreateAnnouncements(true)} + /> + + handleCreateAnnouncements(e) + } + /> +
    +
    +
    + } + /> +
    + + ); +} + +CreateNewAnnouncements.pageLayout = defaultLayout; + +export default CreateNewAnnouncements; diff --git a/src/pages/announcements/edit-announcements/index.tsx b/src/pages/announcements/edit-announcements/index.tsx new file mode 100644 index 00000000..678688fd --- /dev/null +++ b/src/pages/announcements/edit-announcements/index.tsx @@ -0,0 +1,313 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { defaultLayout } from '../../../layouts/defaultLayout'; +import SEO from '../../../components/global/SEO'; +import { useRouter } from 'next/router'; +import TcPrivateMessageContainer from '../../../components/announcements/create/privateMessaageContainer'; +import TcPublicMessaageContainer from '../../../components/announcements/create/publicMessageContainer'; +import TcScheduleAnnouncement from '../../../components/announcements/create/scheduleAnnouncement'; +import TcSelectPlatform from '../../../components/announcements/create/selectPlatform'; +import TcBoxContainer from '../../../components/shared/TcBox/TcBoxContainer'; +import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; +import TcConfirmSchaduledAnnouncementsDialog from '../../../components/announcements/TcConfirmSchaduledAnnouncementsDialog'; +import useAppStore from '../../../store/useStore'; +import { IRoles, IUser } from '../../../utils/interfaces'; +import { ChannelContext } from '../../../context/ChannelContext'; +import { useSnackbar } from '../../../context/SnackbarContext'; +import { useToken } from '../../../context/TokenContext'; +import { CreateAnnouncementsPayloadData } from '../create-new-announcements'; +import SimpleBackdrop from '../../../components/global/LoadingBackdrop'; +import { MdOutlineAnnouncement } from 'react-icons/md'; +import TcIconContainer from '../../../components/announcements/create/TcIconContainer'; +import TcText from '../../../components/shared/TcText'; + +export interface DiscordChannel { + channelId: string; + name: string; +} + +interface DiscordUser { + discordId: string; + ngu: string; +} + +interface DiscordPublicOptions { + channels: DiscordChannel[]; +} + +export interface DiscordPrivateOptions { + roles?: IRoles[]; + users?: DiscordUser[]; +} + +type DiscordOptions = DiscordPublicOptions | DiscordPrivateOptions; + +export interface DiscordData { + platform: string; + template: string; + options: DiscordOptions; + type: 'discord_public' | 'discord_private'; +} + +export interface AnnouncementsDiscordResponseProps { + id: string; + scheduledAt: string; + draft: boolean; + data: DiscordData[]; + community: string; +} + +function Index() { + const { retrieveAnnouncementById, patchExistingAnnouncement } = useAppStore(); + + const router = useRouter(); + + const { community } = useToken(); + + const channelContext = useContext(ChannelContext); + const { refreshData } = channelContext; + + const { showMessage } = useSnackbar(); + + const [channels, setChannels] = useState([]); + const [roles, setRoles] = useState([]); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [isDateValid, setIsDateValid] = useState(true); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [publicAnnouncements, setPublicAnnouncements] = + useState(); + + const [privateAnnouncements, setPrivateAnnouncements] = + useState(); + + const [fetchedAnnouncements, setFetchedAnnouncements] = + useState(); + + const id = router.query.announcementsId as string; + + const [scheduledAt, setScheduledAt] = useState(); + + const publicSelectedAnnouncements = useMemo(() => { + return fetchedAnnouncements?.data.filter( + (item) => item.type === 'discord_public' + )[0]; + }, [fetchedAnnouncements]); + + const privateSelectedAnnouncements = useMemo(() => { + return fetchedAnnouncements?.data.filter( + (item) => item.type === 'discord_private' + ); + }, [fetchedAnnouncements]); + + const fetchPlatformChannels = async () => { + try { + setLoading(true); + if (platformId) { + let channelIds: string[] = []; + + if ( + publicSelectedAnnouncements?.type === 'discord_public' && + 'channels' in publicSelectedAnnouncements.options + ) { + channelIds = publicSelectedAnnouncements.options.channels.map( + (channel) => channel.channelId + ); + } + + await refreshData(platformId, 'channel', channelIds, undefined, false); + } + } catch (error) { + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!id) return; + const fetchAnnouncement = async () => { + const data = await retrieveAnnouncementById(id); + + setFetchedAnnouncements(data); + setScheduledAt(data.scheduledAt); + }; + + fetchAnnouncement(); + }, [id]); + + useEffect(() => { + fetchPlatformChannels(); + }, [fetchedAnnouncements]); + + const handleEditAnnouncements = async (isDrafted: boolean) => { + if (!community) return; + + const data = [publicAnnouncements]; + + if (privateAnnouncements && privateAnnouncements.length > 0) { + data.push(...privateAnnouncements); + } + + const announcementsPayload = { + draft: isDrafted, + scheduledAt: scheduledAt, + data: data, + }; + + try { + setLoading(true); + const data = await patchExistingAnnouncement(id, announcementsPayload); + + if (data) { + showMessage('Announcement updated successfully', 'success'); + router.push('/announcements'); + } else { + fetchPlatformChannels(); + } + } catch (error) { + showMessage('Failed to create announcement', 'error'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ; + } + + return ( + <> + +
    + + +
    + + { + setScheduledAt(selectedTime); + }} + isDateValid={isDateValid} + setIsDateValid={setIsDateValid} + /> + { + if (!platformId) return; + setChannels(selectedChannels); + setPublicAnnouncements({ + platformId: platformId, + template: message, + options: { + channelIds: selectedChannels.map( + (channel) => channel.id + ), + }, + }); + }} + /> +
    + + + + + +
    + {/* { + if (!platformId) return; + + const commonData = { + platformId: platformId, + template: message, + }; + + let privateAnnouncementsOptions: { + roleIds: string[]; + userIds: string[]; + } = { + roleIds: [], + userIds: [], + }; + + if (selectedRoles && selectedRoles.length > 0) { + setRoles(selectedRoles); + privateAnnouncementsOptions.roleIds = selectedRoles.map( + (role) => role.roleId.toString() + ); + } + + if (selectedUsers && selectedUsers.length > 0) { + setUsers(selectedUsers); + privateAnnouncementsOptions.userIds = selectedUsers.map( + (user) => user.discordId + ); + } + + if ( + privateAnnouncementsOptions.roleIds.length > 0 || + privateAnnouncementsOptions.userIds.length > 0 + ) { + const combinedPrivateAnnouncement = { + ...commonData, + options: privateAnnouncementsOptions, + }; + + setPrivateAnnouncements([combinedPrivateAnnouncement]); + } + }} + /> */} +
    +
    + handleEditAnnouncements(e)} + /> +
    +
    + } + /> + + + ); +} + +Index.pageLayout = defaultLayout; + +export default Index; diff --git a/src/pages/announcements/index.tsx b/src/pages/announcements/index.tsx new file mode 100644 index 00000000..2e8c06c6 --- /dev/null +++ b/src/pages/announcements/index.tsx @@ -0,0 +1,254 @@ +import React, { useEffect, useState } from 'react'; +import { defaultLayout } from '../../layouts/defaultLayout'; +import TcBoxContainer from '../../components/shared/TcBox/TcBoxContainer'; +import SEO from '../../components/global/SEO'; +import TcText from '../../components/shared/TcText'; +import TcButton from '../../components/shared/TcButton'; +import { BsPlus } from 'react-icons/bs'; +import router from 'next/router'; +import TcPagination from '../../components/shared/TcPagination'; +import TcTimeZone from '../../components/announcements/TcTimeZone'; +import moment from 'moment'; +import { MdCalendarMonth } from 'react-icons/md'; +import useAppStore from '../../store/useStore'; +import { StorageService } from '../../services/StorageService'; +import { + FetchedData, + IDiscordModifiedCommunity, + IPlatformProps, +} from '../../utils/interfaces'; +import TcAnnouncementsTable from '../../components/announcements/TcAnnouncementsTable'; +import TcDatePickerPopover from '../../components/shared/TcDatePickerPopover'; +import TcAnnouncementsAlert from '../../components/announcements/TcAnnouncementsAlert'; +import { useToken } from '../../context/TokenContext'; +import SimpleBackdrop from '../../components/global/LoadingBackdrop'; + +function Index() { + const { retrieveAnnouncements, retrievePlatformById } = useAppStore(); + + const { community } = useToken(); + + const [loading, setLoading] = useState(false); + const [isFirstLoad, setIsFirstLoad] = useState(true); + const communityId = + StorageService.readLocalStorage('community')?.id; + + const [anchorEl, setAnchorEl] = useState(null); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedZone, setSelectedZone] = useState(moment.tz.guess()); + const [dateTimeDisplay, setDateTimeDisplay] = useState('Filter Date'); + + const [page, setPage] = useState(1); + + const platformId = community?.platforms.find( + (platform) => platform.disconnectedAt === null + )?.id; + + const [announcementsPermissions, setAnnouncementsPermissions] = + useState(true); + + const fetchPlatform = async () => { + if (platformId) { + try { + setLoading(true); + const data: IPlatformProps = await retrievePlatformById(platformId); + const { metadata } = data; + + if (metadata) { + const announcements = metadata.permissions.Announcement; + const allPermissionsTrue = Object.values(announcements).every( + (value) => value === true + ); + + setAnnouncementsPermissions(allPermissionsTrue); + } + setLoading(false); + } catch (error) { + } finally { + setLoading(false); + if (isFirstLoad) setIsFirstLoad(false); + } + } + }; + + useEffect(() => { + fetchPlatform(); + }, [platformId]); + + const [fetchedAnnouncements, setFetchedAnnouncements] = useState( + { + limit: 8, + page: page, + results: [], + totalPages: 0, + totalResults: 0, + } + ); + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + const id = open ? 'date-time-popover' : undefined; + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleDateChange = (date: Date | null) => { + if (date) { + setSelectedDate(date); + setPage(1); + const fullDateTime = moment(date); + setDateTimeDisplay(fullDateTime.format('D MMMM YYYY')); + + setAnchorEl(null); + } + }; + + const resetDateFilter = () => { + setSelectedDate(null); + setDateTimeDisplay('Filter Date'); + + setAnchorEl(null); + }; + + const fetchData = async (date?: Date | null, zone?: string) => { + try { + setLoading(true); + + let startDate, endDate; + if (date) { + startDate = moment(date) + .tz(zone || selectedZone) + .startOf('day') + .utcOffset(0, true) + .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + endDate = moment(date) + .tz(zone || selectedZone) + .endOf('day') + .utcOffset(0, true) + .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + } + const data = await retrieveAnnouncements({ + page: page, + limit: 8, + timeZone: zone || selectedZone, + ...(startDate ? { startDate: startDate } : {}), + ...(endDate ? { endDate: endDate } : {}), + community: communityId, + }); + + setFetchedAnnouncements(data); + } catch (error) { + console.error('An error occurred:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(selectedDate, selectedZone); + }, [selectedZone, selectedDate, page]); + + const handlePageChange = (selectedPage: number) => { + setPage(selectedPage); + }; + + if (isFirstLoad && loading) { + return ; + } + + return ( + <> + + {!announcementsPermissions && } +
    + +
    +
    + + } + variant="outlined" + onClick={() => + router.push('/announcements/create-new-announcements') + } + /> +
    +
    + } + onClick={handleClick} + text={dateTimeDisplay} + aria-describedby={id} + /> + + +
    + {fetchedAnnouncements.results.length > 0 ? ( +
    + +
    + ) : ( +
    + + +
    + )} +
    +
    + {fetchedAnnouncements.totalResults > 8 && ( +
    + +
    + )} +
    +
    + } + /> + + + ); +} + +Index.pageLayout = defaultLayout; + +export default Index; diff --git a/src/pages/callback.tsx b/src/pages/callback.tsx index 71f5617c..7ce0c5f1 100644 --- a/src/pages/callback.tsx +++ b/src/pages/callback.tsx @@ -137,6 +137,14 @@ function Callback() { setMessage('Discord Authorization during setup on setting faield.'); router.push('/community-settings'); + case StatusCode.ANNOUNCEMENTS_PERMISSION_FAILURE: + setMessage('Announcements grant write permissions faield.'); + router.push('/announcements'); + + case StatusCode.ANNOUNCEMENTS_PERMISSION_SUCCESS: + setMessage('Announcements grant write permissions success.'); + router.push('/announcements'); + default: console.error('Unexpected status code received:', code); setMessage('An unexpected error occurred. Please try again later.'); diff --git a/src/pages/centric/create-new-community.tsx b/src/pages/centric/create-new-community.tsx index d32928c8..cf13cdaa 100644 --- a/src/pages/centric/create-new-community.tsx +++ b/src/pages/centric/create-new-community.tsx @@ -98,6 +98,7 @@ function CreateNewCommunity() { handleCreateNewCommunitie()} diff --git a/src/pages/centric/index.tsx b/src/pages/centric/index.tsx index e6b655f5..c6ea8714 100644 --- a/src/pages/centric/index.tsx +++ b/src/pages/centric/index.tsx @@ -23,6 +23,7 @@ function Index() {
    discordAuthorization()} /> diff --git a/src/pages/centric/tac.tsx b/src/pages/centric/tac.tsx index 65cfbdbc..cfdff9dd 100644 --- a/src/pages/centric/tac.tsx +++ b/src/pages/centric/tac.tsx @@ -96,6 +96,7 @@ function Tac() { handleAcceptTerms()} /> diff --git a/src/pages/community-settings/index.tsx b/src/pages/community-settings/index.tsx index 4f7cbc10..a7835f67 100644 --- a/src/pages/community-settings/index.tsx +++ b/src/pages/community-settings/index.tsx @@ -8,7 +8,6 @@ import TcIntegrationDialog from '../../components/pages/communitySettings/TcInte import { useRouter } from 'next/router'; import TcSwitchCommunity from '../../components/communitySettings/switchCommunity/TcSwitchCommunity'; import SimpleBackdrop from '../../components/global/LoadingBackdrop'; -import { ChannelProvider } from '../../context/ChannelContext'; function index() { const router = useRouter(); @@ -48,29 +47,27 @@ function index() { return ( <> - - -
    - - -
    - - -
    + +
    + + +
    + +
    - } - /> -
    - + } /> - +
    + ); } diff --git a/src/pages/community-settings/platform/index.tsx b/src/pages/community-settings/platform/index.tsx index 716703ef..f1f6e5a3 100644 --- a/src/pages/community-settings/platform/index.tsx +++ b/src/pages/community-settings/platform/index.tsx @@ -2,23 +2,21 @@ import TcPlatform from '../../../components/communitySettings/platform'; import SEO from '../../../components/global/SEO'; import TcBreadcrumbs from '../../../components/shared/TcBreadcrumbs'; -import { ChannelProvider } from '../../../context/ChannelContext'; import { defaultLayout } from '../../../layouts/defaultLayout'; function Index() { return ( <> - - -
    - - -
    -
    + +
    + + +
    ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6bcb0683..a5613a93 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,7 +1,5 @@ import { defaultLayout } from '../layouts/defaultLayout'; import SEO from '../components/global/SEO'; -import { useState } from 'react'; -import { StorageService } from '../services/StorageService'; import EmptyState from '../components/global/EmptyState'; import Image from 'next/image'; import emptyState from '../assets/svg/empty-state.svg'; @@ -9,21 +7,11 @@ import React from 'react'; import ActiveMemberComposition from '../components/pages/pageIndex/ActiveMemberComposition'; import HeatmapChart from '../components/pages/pageIndex/HeatmapChart'; import MemberInteractionGraph from '../components/pages/pageIndex/MemberInteractionGraph'; -import { ChannelProvider } from '../context/ChannelContext'; import { useToken } from '../context/TokenContext'; function Dashboard(): JSX.Element { - const [alertStateOpen, setAlertStateOpen] = useState(false); const { community } = useToken(); - const toggleAnalysisState = () => { - StorageService.writeLocalStorage('analysis_state', { - isRead: true, - visible: false, - }); - setAlertStateOpen(false); - }; - if (!community || community?.platforms?.length === 0) { return ( <> @@ -36,20 +24,18 @@ function Dashboard(): JSX.Element { return ( <> - -
    -
    -

    - Community Insights -

    -
    - - - -
    +
    +
    +

    + Community Insights +

    +
    + + +
    - +
    ); } diff --git a/src/pages/statistics.tsx b/src/pages/statistics.tsx index 45378585..f5c60d2a 100644 --- a/src/pages/statistics.tsx +++ b/src/pages/statistics.tsx @@ -19,13 +19,26 @@ import { useToken } from '../context/TokenContext'; import EmptyState from '../components/global/EmptyState'; import emptyState from '../assets/svg/empty-state.svg'; import Image from 'next/image'; +import { useRouter } from 'next/router'; const Statistics = () => { const { community } = useToken(); + const router = useRouter(); + const platformId = community?.platforms.find( (platform) => platform.disconnectedAt === null )?.id; + const tabMap: { [key: string]: string } = { + activeMembers: '1', + disengagedMembers: '2', + }; + + const reverseTabMap: { [key: string]: string } = { + '1': 'activeMembers', + '2': 'disengagedMembers', + }; + const [loading, setLoading] = useState(true); const [activeMemberDate, setActiveMemberDate] = useState(1); const [onBoardingMemberDate, setOnBoardingMemberDate] = useState(1); @@ -40,13 +53,36 @@ const Statistics = () => { fetchOnboardingMembers, } = useAppStore(); - const [activeTab, setActiveTab] = useState('1'); + const [activeTab, setActiveTab] = useState( + tabMap[router.query.tab as string] || '1' + ); + + useEffect(() => { + if (!router.isReady) return; + + const handleRouteChange = () => { + setActiveTab(tabMap[router.query.tab as string] || '1'); + }; + + handleRouteChange(); + + router.events.on('routeChangeComplete', handleRouteChange); + + return () => { + router.events.off('routeChangeComplete', handleRouteChange); + }; + }, [router.isReady, router.query.tab]); const handleTabChange = ( event: React.SyntheticEvent, newValue: string ): void => { - setActiveTab(newValue); + if (newValue in reverseTabMap) { + const urlTabIdentifier = reverseTabMap[newValue]; + router.push(`/statistics?tab=${urlTabIdentifier}`, undefined, { + shallow: true, + }); + } }; useEffect(() => { diff --git a/src/store/slices/announcementsSlice.ts b/src/store/slices/announcementsSlice.ts new file mode 100644 index 00000000..984afcd4 --- /dev/null +++ b/src/store/slices/announcementsSlice.ts @@ -0,0 +1,83 @@ +import { StateCreator } from 'zustand'; +import { axiosInstance } from '../../axiosInstance'; +import IAnnouncements, { + IRetrieveAnnouncementsProps, +} from '../types/IAnnouncements'; +import { CreateAnnouncementsPayload } from '../../pages/announcements/create-new-announcements'; + +const createAnnouncementsSlice: StateCreator = (set, get) => ({ + retrieveAnnouncements: async ({ + page, + limit, + sortBy, + timeZone, + startDate, + endDate, + community, + }: IRetrieveAnnouncementsProps) => { + try { + const params = { + page, + limit, + sortBy, + ...(timeZone ? { timeZone } : {}), + ...(startDate ? { startDate } : {}), + ...(endDate ? { endDate } : {}), + }; + + const { data } = await axiosInstance.get( + `/announcements/?communityId=${community}`, + { params } + ); + + return data; + } catch (error) { + console.error('Failed to retrieve announcements:', error); + } + }, + retrieveAnnouncementById: async (id: string) => { + try { + const { data } = await axiosInstance.get(`/announcements/${id}`); + return data; + } catch (error) { + console.error('Failed to retrieve announcement:', error); + } + }, + createNewAnnouncements: async ( + announcementPayload: CreateAnnouncementsPayload + ) => { + try { + const { data } = await axiosInstance.post( + `/announcements/`, + announcementPayload + ); + return data; + } catch (error) { + console.error('Failed to create announcements:', error); + } + }, + patchExistingAnnouncement: async ( + id: string, + announcementPayload: CreateAnnouncementsPayload + ) => { + try { + const { data } = await axiosInstance.patch( + `/announcements/${id}`, + announcementPayload + ); + return data; + } catch (error) { + console.error('Failed to patch announcements:', error); + } + }, + deleteAnnouncements: async (id: string) => { + try { + const { data } = await axiosInstance.delete(`/announcements/${id}`); + return data; + } catch (error) { + console.error('Failed to delete announcements:', error); + } + }, +}); + +export default createAnnouncementsSlice; diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts deleted file mode 100644 index 0e116464..00000000 --- a/src/store/slices/authSlice.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { StateCreator } from 'zustand'; -import IAuth, { IUser } from '../types/IAuth'; -import { conf } from '../../configs'; -import { axiosInstance } from '../../axiosInstance'; -import { StorageService } from '../../services/StorageService'; - -const BASE_URL = conf.API_BASE_URL; - -const createAuthSlice: StateCreator = (set, get) => ({ - isLoggedIn: false, - isLoading: false, - user: {}, - guildChannels: [], - - signUp: () => { - location.replace(`${BASE_URL}/auth/try-now`); - }, - - login: () => { - location.replace(`${BASE_URL}/auth/login`); - }, - - loginWithDiscord: (user: IUser) => - set(() => { - StorageService.writeLocalStorage('user', { - guild: { - guildId: user.guildId, - guildName: user.guildName, - }, - token: { - accessToken: user.accessToken, - accessExp: user.accessExp, - refreshToken: user.refreshToken, - refreshExp: user.refreshExp, - }, - }); - - return { user }; - }), - - fetchGuildChannels: async (guild_id: string) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guild_id}/channels`); - set({ guildChannels: [...data], isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - - updateGuildById: async (guildId, period, selectedChannels) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - selectedChannels: selectedChannels, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - - changeEmail: async (emailAddress: string) => { - try { - await axiosInstance.patch(`/users/@me`, { - email: emailAddress, - }); - } catch (error) {} - }, -}); - -export default createAuthSlice; diff --git a/src/store/slices/platformSlice.ts b/src/store/slices/platformSlice.ts index 1d20b05a..8bee293b 100644 --- a/src/store/slices/platformSlice.ts +++ b/src/store/slices/platformSlice.ts @@ -5,6 +5,7 @@ import IPlatfrom, { IRetrievePlatformsProps, IRetrivePlatformRolesOrChannels, IPatchPlatformInput, + IGrantWritePermissionsProps, } from '../types/IPlatform'; import { conf } from '../../configs'; import { IPlatformProps } from '../../utils/interfaces'; @@ -71,6 +72,7 @@ const createPlatfromSlice: StateCreator = (set, get) => ({ platformId, property = 'channel', name, + ngu, sortBy, page, limit, @@ -88,6 +90,8 @@ const createPlatfromSlice: StateCreator = (set, get) => ({ if (name) params.append('name', name); + if (ngu) params.append('ngu', ngu); + if (page !== undefined) { params.append('page', page.toString()); } @@ -108,6 +112,15 @@ const createPlatfromSlice: StateCreator = (set, get) => ({ return data; } catch (error) {} }, + grantWritePermissions: ({ + platformType, + moduleType, + id, + }: IGrantWritePermissionsProps) => { + location.replace( + `${BASE_URL}/platforms/request-access/${platformType}/${moduleType}/${id}` + ); + }, }); export default createPlatfromSlice; diff --git a/src/store/slices/settingSlice.ts b/src/store/slices/settingSlice.ts deleted file mode 100644 index 175ede5d..00000000 --- a/src/store/slices/settingSlice.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { StateCreator } from 'zustand'; -import { axiosInstance } from '../../axiosInstance'; -import ISetting from '../types/ISetting'; -import { conf } from '../../configs'; - -const BASE_URL = conf.API_BASE_URL; - -const createSettingSlice: StateCreator = (set, get) => ({ - isLoading: false, - isRefetchLoading: false, - guildInfo: {}, - userInfo: {}, - guildInfoByDiscord: {}, - guilds: [], - guildChannels: [], - getUserGuildInfo: async (guildId: string) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guildId}`); - - set({ guildInfo: data, isLoading: false }); - } catch (error) { - set(() => ({ guildInfo: {}, isLoading: false })); - } - }, - getUserInfo: async () => { - try { - const { data } = await axiosInstance.get('/users/@me'); - set({ userInfo: data }); - return data; - } catch (error) {} - }, - getGuildInfoByDiscord: async (guildId) => { - try { - set(() => ({ isLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guildId}`); - set({ guildInfoByDiscord: data, isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - updateSelectedChannels: async (guildId, selectedChannels) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - selectedChannels: selectedChannels, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - patchGuildById: async (guildId, period, selectedChannels) => { - try { - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - selectedChannels: selectedChannels, - }); - } catch (error) {} - }, - updateAnalysisDatePeriod: async (guildId, period) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.patch(`/guilds/${guildId}`, { - period, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - getGuilds: async () => { - try { - const { data } = await axiosInstance.get(`/guilds?isDisconnected=false`); - set({ - guilds: [...data.results], - }); - } catch (error) {} - }, - disconnecGuildById: async (guildId, disconnectType) => { - try { - set(() => ({ isLoading: true })); - await axiosInstance.post(`/guilds/${guildId}/disconnect`, { - disconnectType: disconnectType, - }); - set({ isLoading: false }); - } catch (error) { - set(() => ({ isLoading: false })); - } - }, - connectNewGuild: async () => { - try { - location.replace(`${BASE_URL}/guilds/connect`); - } catch (error) {} - }, - - refetchGuildChannels: async (guild_id: string) => { - try { - set(() => ({ isRefetchLoading: true })); - const { data } = await axiosInstance.get(`/guilds/${guild_id}/channels`); - set({ guildChannels: [...data], isRefetchLoading: false }); - } catch (error) { - set(() => ({ isRefetchLoading: false })); - } - }, -}); - -export default createSettingSlice; diff --git a/src/store/types/IAnnouncements.ts b/src/store/types/IAnnouncements.ts new file mode 100644 index 00000000..672897f2 --- /dev/null +++ b/src/store/types/IAnnouncements.ts @@ -0,0 +1,18 @@ +export interface IRetrieveAnnouncementsProps { + page: number; + limit: number; + sortBy?: string; + community: string; + startDate?: string; + endDate?: string; + timeZone: string; +} + +export default interface IAnnouncements { + retrieveAnnouncements: ({ + page, + limit, + sortBy, + community, + }: IRetrieveAnnouncementsProps) => void; +} diff --git a/src/store/types/IAuth.ts b/src/store/types/IAuth.ts deleted file mode 100644 index ab03f71c..00000000 --- a/src/store/types/IAuth.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { IChannelWithoutId, IGuildChannels } from '../../utils/types'; - -export type IUser = { - readonly accessToken: string; - readonly accessExp: string; - readonly guildId: string; - readonly guildName: string; - readonly refreshExp: string; - readonly refreshToken: string; -}; - -export type ISubChannels = { - readonly id: string; - readonly name: string; - readonly canReadMessageHistoryAndViewChannel: boolean; - readonly parent_id: string; -}; - -export default interface IAuth { - user: IUser | {}; - isLoading: boolean; - isLoggedIn: boolean; - guildChannels: IGuildChannels[]; - signUp: () => void; - login: () => void; - loginWithDiscord: (user: IUser) => void; - fetchGuildChannels: (guild_id: string) => void; - updateGuildById: ( - guildId: string, - period: string, - selectedChannels: IChannelWithoutId[] - ) => any; - changeEmail: (email: string) => any; -} diff --git a/src/store/types/IPlatform.ts b/src/store/types/IPlatform.ts index 6ad1e5f5..16c7574d 100644 --- a/src/store/types/IPlatform.ts +++ b/src/store/types/IPlatform.ts @@ -13,8 +13,9 @@ export interface IRetrivePlatformRolesOrChannels { limit?: number; sortBy?: string; name?: string; + ngu?: string; platformId: string; - property: 'channel' | 'role'; + property: 'channel' | 'role' | 'guildMember'; } export interface IDeletePlatformProps { @@ -31,6 +32,12 @@ export interface IPatchPlatformInput { }; } +export interface IGrantWritePermissionsProps { + platformType: 'discord' | 'telegram'; + moduleType: 'Announcements'; + id: string; +} + export default interface IPlatfrom { connectedPlatforms: any[]; connectNewPlatform: (platfromType: string) => void; @@ -56,4 +63,9 @@ export default interface IPlatfrom { page, limit, }: IRetrivePlatformRolesOrChannels) => void; + grantWritePermissions: ({ + platformType, + moduleType, + id, + }: IGrantWritePermissionsProps) => void; } diff --git a/src/store/types/ISetting.ts b/src/store/types/ISetting.ts deleted file mode 100644 index 5de57107..00000000 --- a/src/store/types/ISetting.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { IChannelWithoutId, IGuildChannels } from '../../utils/types'; - -export type IGuildInfo = { - id?: string; - guildId?: string; - ownerId?: string; - name?: boolean; - period?: string; - selectedChannels?: IChannelWithoutId[]; -}; - -export type DISCONNECT_TYPE = 'soft' | 'hard'; - -export interface IUserInfo { - discordId: string; - email: string; - verified: boolean; - avatar: string; - twitterConnectedAt: string; - twitterId: string; - twitterProfileImageUrl: string; - twitterUsername: string; - twitterIsInProgress: boolean; - id: string; -} - -export default interface IGuildList extends IGuildInfo { - isInProgress?: boolean; - isDisconnected?: boolean; - connectedAt?: string; -} -export default interface ISetting { - isLoading: boolean; - isRefetchLoading: boolean; - guildInfo?: IGuildInfo | {}; - userInfo: IUserInfo | {}; - guildInfoByDiscord: {}; - guilds: IGuildList[]; - guildChannels: IGuildChannels[]; - getUserGuildInfo: (guildId: string) => void; - getUserInfo: () => any; - getGuildInfoByDiscord: (guildId: string) => void; - updateSelectedChannels: ( - guildId: string, - selectedChannels: IChannelWithoutId[] - ) => void; - patchGuildById: ( - guildId: string, - period: string, - selectedChannels: IChannelWithoutId[] - ) => any; - updateAnalysisDatePeriod: (guildId: string, period: string) => void; - getGuilds: () => void; - disconnecGuildById: ( - guildId: string, - disconnectType: DISCONNECT_TYPE - ) => void; - refetchGuildChannels: (guild_id: string) => void; -} diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 63526092..4a6fd939 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -1,7 +1,5 @@ import { create } from 'zustand'; -import createAuthSlice from './slices/authSlice'; import createChartSlice from './slices/chartSlice'; -import createSettingSlice from './slices/settingSlice'; import createBreakdownsSlice from './slices/breakdownsSlice'; import createMemberInteractionSlice from './slices/memberInteractionSlice'; import communityHealthSlice from './slices/communityHealthSlice'; @@ -9,11 +7,10 @@ import twitterSlice from './slices/twitterSlice'; import centricSlice from './slices/centricSlice'; import platformSlice from './slices/platformSlice'; import userSlice from './slices/userSlice'; +import announcementsSlice from './slices/announcementsSlice'; const useAppStore = create()((...a) => ({ - ...createAuthSlice(...a), ...createChartSlice(...a), - ...createSettingSlice(...a), ...createBreakdownsSlice(...a), ...createMemberInteractionSlice(...a), ...communityHealthSlice(...a), @@ -21,6 +18,7 @@ const useAppStore = create()((...a) => ({ ...centricSlice(...a), ...platformSlice(...a), ...userSlice(...a), + ...announcementsSlice(...a), })); export default useAppStore; diff --git a/src/styles/globals.css b/src/styles/globals.css index a09541cc..e677196b 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -78,3 +78,6 @@ body { .highcharts-credits { pointer-events: none !important; } +.no-border td { + border: 0; +} diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 957793c5..59908f05 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -14,4 +14,17 @@ export enum StatusCode { DISCORD_AUTHORIZATION_FAILURE_FROM_SETTINGS = '1005', TWITTER_AUTHORIZATION_SUCCESSFUL = '1006', TWITTER_AUTHORIZATION_FAILURE = '1007', + ANNOUNCEMENTS_PERMISSION_SUCCESS = '1008', + ANNOUNCEMENTS_PERMISSION_FAILURE = '1009', +} + +export enum Permission { + AttachFiles = 'Attach Files', + CreatePrivateThreads = 'Create Private Threads', + CreatePublicThreads = 'Create Public Threads', + EmbedLinks = 'Embed Links', + MentionEveryone = 'Mention Everyone', + SendMessages = 'Send Messages', + SendMessagesInThreads = 'Send Messages In Threads', + ViewChannel = 'View Channel', } diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index d8218423..f34013e6 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -27,8 +27,8 @@ export interface IRoles { roleId: string; color: number | string; name: string; - deletedAt: string; - id: number | string; + deletedAt?: string; + id?: number | string; } export interface IUserProfile { @@ -148,6 +148,27 @@ export interface IPlatformProps { metadata: metaData; } +export interface UserPermissions { + AttachFiles: boolean; + CreatePrivateThreads: boolean; + CreatePublicThreads: boolean; + EmbedLinks: boolean; + MentionEveryone: boolean; + SendMessages: boolean; + SendMessagesInThreads: boolean; + ViewChannel: boolean; +} + +export interface ReadData { + ViewChannel: boolean; + ReadMessageHistory: boolean; +} + +export interface Permissions { + permissions: UserPermissions; + ReadData: ReadData; +} + export interface ICommunityDiscordPlatfromProps { id: string; name: string; @@ -157,6 +178,7 @@ export interface ICommunityDiscordPlatfromProps { name: string; selectedChannels?: string[]; period?: string; + permissions: Permissions; analyzerStartedAt?: string; isInProgress?: boolean; }; @@ -171,3 +193,12 @@ export interface IDiscordModifiedCommunity extends Omit { platforms: ICommunityDiscordPlatfromProps[]; } + +export interface IUser { + discordId: string; + discriminator?: string; + globalName?: string | null; + ngu: string; + nickname?: string | null; + username?: string; +} diff --git a/src/utils/privateRoute.tsx b/src/utils/privateRoute.tsx index eddcb90a..6af49fcf 100644 --- a/src/utils/privateRoute.tsx +++ b/src/utils/privateRoute.tsx @@ -17,9 +17,6 @@ export default function PrivateRoute({ [router.pathname] ); - const isObjectNotEmpty = (obj: Record): boolean => { - return Object.keys(obj).length > 0; - }; useEffect(() => { if (!isCentricRoute) { const storedToken = StorageService.readLocalStorage('user'); diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 38e0d8b1..851c6743 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -14,10 +14,6 @@ export const theme = createTheme({ components: { MuiButton: { styleOverrides: { - sizeMedium: { - width: '15rem', - padding: '0.5rem', - }, root: { textTransform: 'none', borderRadius: '4px', @@ -109,31 +105,7 @@ export const theme = createTheme({ }, }, MuiTab: { - styleOverrides: { - root: { - textTransform: 'none', - borderRadius: '10px 10px 0 0', - padding: '8px 24px', - width: '214px', - height: '40px', - gap: '10px', - borderBottom: 'none', - '&.Mui-selected': { - background: '#804EE1', - color: 'white', - border: 0, - borderBottom: 'none', - }, - '&$selected': { - borderBottom: 'none', - }, - '&:not(.Mui-selected)': { - backgroundColor: '#EDEDED', - color: '#222222', - }, - selected: {}, - }, - }, + styleOverrides: {}, }, }, }); diff --git a/src/utils/types.ts b/src/utils/types.ts index c5c59ae7..1b606fea 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -41,7 +41,7 @@ export type ISubChannels = { readonly channelId: string; readonly name: string; readonly canReadMessageHistoryAndViewChannel: boolean; - readonly parent_id: string; + readonly parent_id?: string; }; export type IChannel = { diff --git a/tsconfig.json b/tsconfig.json index b194c6be..e313683d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "jest.config.js", "jest.setup.js" ], - "exclude": ["node_modules", "./src/components/global/CustomDatePicker.tsx"] + "exclude": ["node_modules"] }