diff --git a/package-lock.json b/package-lock.json index 01ddfb7..72c6cee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,18 +14,29 @@ "@chakra-ui/system": "^2.6.2", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@reduxjs/toolkit": "^2.2.7", + "@fingerprintjs/fingerprintjs": "^4.5.1", + "@fontsource/inter": "^5.1.0", + "@reduxjs/toolkit": "^2.5.1", + "@sentry/react": "^8.41.0", "@types/react-router-dom": "^5.3.3", "apexcharts": "^3.54.0", "axios": "^1.7.7", "chakra-react-select": "^4.9.2", "embla-carousel-react": "^8.3.1", "framer-motion": "^11.11.10", + "i18next": "^23.16.4", + "is-mobile": "^5.0.0", "json-schema": "^0.4.0", + "jwt-decode": "^4.0.0", + "lodash": "^4.17.21", + "moment": "^2.30.1", "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-dom": "^18.3.1", + "react-i18next": "^15.1.1", "react-intersection-observer": "^9.13.1", + "react-modal": "^3.16.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.27.0", "react-slick": "^0.30.2", "react-spring": "^9.7.4", @@ -36,7 +47,7 @@ "devDependencies": { "@eslint/js": "^9.13.0", "@types/node": "^22.9.0", - "@types/react": "^18.3.11", + "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "eslint": "^9.13.0", @@ -2883,6 +2894,14 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fingerprintjs/fingerprintjs": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.5.1.tgz", + "integrity": "sha512-hKJaRoLHNeUUPhb+Md3pTlY/Js2YR4aXjroaDHpxrjoM8kGnEFyZVZxXo6l3gRyKnQN52Uoqsycd3M73eCdMzw==", + "dependencies": { + "tslib": "^2.4.1" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", @@ -2905,6 +2924,11 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@fontsource/inter": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.1.1.tgz", + "integrity": "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw==" + }, "node_modules/@humanfs/core": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", @@ -3703,9 +3727,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz", - "integrity": "sha512-WC7Yd6cNGfHx8zf+iu+Q1UPTfEcXhQ+ATi7CV1hlrSAaQBdlPzg7Ww/wJHNQem7qG9rxmWoFCDCPubSvFObGzA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", + "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", "dependencies": { "immer": "^10.0.3", "redux": "^5.0.1", @@ -3713,7 +3737,7 @@ "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -4034,6 +4058,91 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.52.0.tgz", + "integrity": "sha512-ojFldpRpGrgacIQMbbMZeqLYetNJJ61n+Pz29FpggaIRrbkq84ocoy4FCy+9BuLo6ywgxtUFrjOXD9pPRcZtUA==", + "dependencies": { + "@sentry/core": "8.52.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.52.0.tgz", + "integrity": "sha512-r62Ufg4uGlvQsQ+nRSiq9y0ieVFRlZvUaoT/zMjmPuMg29O9rRAMdPJuiCpBH4++x8KJoJ9c2HBRizn6/3uc5Q==", + "dependencies": { + "@sentry/core": "8.52.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.52.0.tgz", + "integrity": "sha512-b4hQPni1G2tcV5XuAPSV4RTX3vqPXO9RfUXLuTBzOTNzBHDoj8nQv0yVvcysGy5tBAuVRo5ya5A+PG/iC6FA9A==", + "dependencies": { + "@sentry-internal/browser-utils": "8.52.0", + "@sentry/core": "8.52.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.52.0.tgz", + "integrity": "sha512-4ES2uCUb9yEO1cbg15UBqiYU/syQYj5GviI+TvYvnPX3I8K2mK941ZRqfHh2HpFMhMxLgfX4jDqDGizNhXWdqg==", + "dependencies": { + "@sentry-internal/replay": "8.52.0", + "@sentry/core": "8.52.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.52.0.tgz", + "integrity": "sha512-7JpJ9zpInozBzy61eJf/6RPHoKUCFcoFuKd9rttkN1gyY9xkU1cQK+x1f0deiIHnF9ydftmDtXW+kGFI/+xqtw==", + "dependencies": { + "@sentry-internal/browser-utils": "8.52.0", + "@sentry-internal/feedback": "8.52.0", + "@sentry-internal/replay": "8.52.0", + "@sentry-internal/replay-canvas": "8.52.0", + "@sentry/core": "8.52.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.52.0.tgz", + "integrity": "sha512-2j3B7IKmseTKFm6AyheJ+RSgXqIsx+3blFSuxpkdvsEt60Lbzva2uDkCENfBDOclioo1kvHgsyuXLfWW4A+wwA==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.52.0.tgz", + "integrity": "sha512-7tbRuTFOgKLM2fM5/TjrfDWbkqOOsxkb2QldnAUHTAcArIx77DpsurAgEx5L9lqn+UAhuw1X2IGbzk9fVV/ZDQ==", + "dependencies": { + "@sentry/browser": "8.52.0", + "@sentry/core": "8.52.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4275,6 +4384,11 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@types/webxr": { "version": "0.5.20", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz", @@ -6333,6 +6447,11 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/exponential-backoff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", @@ -7002,6 +7121,14 @@ "react-is": "^16.7.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -7036,6 +7163,28 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -7368,6 +7517,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==" + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -8039,6 +8193,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8146,8 +8308,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.debounce": { "version": "4.0.8", @@ -8781,6 +8942,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9586,6 +9755,27 @@ } } }, + "node_modules/react-i18next": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", + "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-intersection-observer": { "version": "9.13.1", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", @@ -9661,6 +9851,26 @@ "react": "^18.3.1" } }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, "node_modules/react-native": { "version": "0.76.0", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.0.tgz", @@ -9774,6 +9984,28 @@ "loose-envify": "^1.1.0" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -11531,6 +11763,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11641,6 +11881,14 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "peer": true }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -11650,6 +11898,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index e7c3f5a..da49f98 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -7,16 +7,16 @@ "LOGIN_FORGOT_PASSWORD": "FORGOT PASSWORD?", "LOGIN_YOUR_PASSWORD_IS_CREATED_IN_THE_FORMAT_AS_SHOWN_BELOW": "Your password is created in the format as shown below", "LOGIN_PASSWORD_EXAMPLE_EXPLANATION_KA": "If your name is Anish Kumar, and your date of birth is 30-12-1988, then your password will be ", - "LOGIN_USERNAME_EXAMPLE_EXPLANATION_KA" : "If your name is Anish Kumar, and your date of birth is 30-12-1988, then your password will be ", + "LOGIN_USERNAME_EXAMPLE_EXPLANATION_KA": "If your name is Anish Kumar, and your date of birth is 30-12-1988, then your password will be ", "LOGIN_ENTER_USER_NAME": "Enter Username", "LOGIN_FORGOT_USERNAME": "FORGOT USERNAME?", "LOGIN_YOUR_USERNAME_IS_CREATED_IN_THE_FORMAT_AS_SHOWN_BELOW": "Your username is created in the format as shown below", "LOGIN_USERNAME": "Username", "LOGIN_PASSWORD": "Password", "LOGIN": "LOGIN", - "LOGIN_INVALID_PASSWORD" : "Incorrect password", - "LOGIN_INVALID_USER_NAME" : "Incorrect username", - "LOGIN_REQUIRED_FIELD" : "This is Required Field", + "LOGIN_INVALID_PASSWORD": "Incorrect password", + "LOGIN_INVALID_USER_NAME": "Incorrect username", + "LOGIN_REQUIRED_FIELD": "This is Required Field", "POPUP_FIRST_TWO_LETTERS_OF_YOUR_FIRST_NAME": "First 2 letters of your first name", "POPUP_FIRST_TWO_LETTERS_OF_YOUR_LAST_NAME": "First 2 letters of your last name", @@ -29,32 +29,46 @@ "HOME_LEARN_SOMETHING_NOW": "Learn Something Now", "HOME_SEARCH": "Search...", "HOME_WATCH": "WATCH", - "POPUP_CONFIRM_LOGOUT" : "LOGGING OUT ?", - "POPUP_CONFIRM_MSG" : "Don’t worry, your progress will be saved the next time you log back in.", - "POPUP_LOGOUT" : "LOGOUT", - "HOME_NO_VIDEOS_AVAILABLE" : "No Videos Availabe to show", - "SEE_ALL_RESULTS" : "See all results", - "NO_PROGRAM_FOUND" : "No active program available at the moment. Please check back later or contact system administrator", - "NO_SCHOOL_FOUND" : "Your profile is not mapped to any school. Please contact system administrator.", - "NO_BOARD_FOUND" : "Your profile is not mapped to any board. Please contact system administrator.", - "NO_CLASS_FOUND" : "Your profile is not mapped to any class. Please contact system administrator.", - "LEADERBOARD" : "LEADERBOARD", - "LEADERBOARD_NAME" : "NAME", + "POPUP_CONFIRM_LOGOUT": "LOGGING OUT ?", + "POPUP_CONFIRM_MSG": "Don’t worry, your progress will be saved the next time you log back in.", + "POPUP_LOGOUT": "LOGOUT", + "HOME_NO_VIDEOS_AVAILABLE": "No Videos Availabe to show", + "SEE_ALL_RESULTS": "See all results", + "NO_PROGRAM_FOUND": "No active program available at the moment. Please check back later or contact system administrator", + "NO_SCHOOL_FOUND": "Your profile is not mapped to any school. Please contact system administrator.", + "NO_BOARD_FOUND": "Your profile is not mapped to any board. Please contact system administrator.", + "NO_CLASS_FOUND": "Your profile is not mapped to any class. Please contact system administrator.", + "LEADERBOARD": "LEADERBOARD", + "LEADERBOARD_NAME": "NAME", + "LEADERBOARD_NO_DATA": "No more data", "LEADERBOARD_RANK": "RANK", - "LEADERBOARD_COINS" : "COINS", - "LEADERBOARD_YOUR_COINS" : "YOUR COINS", - "LEADERBOARD_YOUR_RANK" : "YOUR RANK", - "LEADERBOARD_VIEW_HISTORY" : "View History", - "LEADERBOARD_COINS_HISTORY" : "Coins History", - "LEADERBOARD_CLOSE" : "Close", - "LEADERBOARD_CONIS_FOR_QUIZ" : "coins for quiz", - "LEADERBOARD_VIEW" : "View", + "LEADERBOARD_COINS": "COINS", + "LEADERBOARD_YOUR_COINS": "YOUR COINS", + "LEADERBOARD_YOUR_RANK": "YOUR RANK", + "LEADERBOARD_VIEW_HISTORY": "View History", + "LEADERBOARD_COINS_HISTORY": "Coins History", + "LEADERBOARD_CLOSE": "Close", + "LEADERBOARD_QUIZ": "quiz", + "LEADERBOARD_CONTENT": "content", + "LEADERBOARD_CONIS": "coins for", + "LEADERBOARD_SUBJECT": "subject completion", + "LEADERBOARD_VIEW": "View", "LEADERBOARD_HOW_YOU_STAND_WITH_OTHER": "See how you stand with others", - "LEADERBOARD_SCHOOL" : "School", - "LEADERBOARD_CLASS" : "Class", - "LEADERBOARD_BOARD" : "Board", - "LEADERBOARD_DEFAULT" : "(default)", - "LEADERBOARD_APPLY" : "Apply", - "HOME_RECENTLY_SEARCHED" : "RECENTLY SEARCHED" - + "LEADERBOARD_SCHOOL": "School", + "LEADERBOARD_CLASS": "Class", + "LEADERBOARD_BOARD": "Board", + "LEADERBOARD_DEFAULT": "(default)", + "LEADERBOARD_APPLY": "Apply", + "HOME_RECENTLY_SEARCHED": "RECENTLY SEARCHED", + "TEACHER_PAGE_VIEW_YOUR_IMPACT": "View your impact!", + "TEACHER_OVERALL_PROGRESS": "OVERALL PROGRESS", + "TEACHER_STUDENT_PROGRESS": "Student Progress", + "TEACHER_COMPLETION_RATE": "COMPLETION RATE", + "TEACHER_SEARCH_STUDENT": "Search student", + "TEACHER_SORT": "SORT", + "TEACHER_A_Z": "A-Z", + "TEACHER_Z_A": "Z-A", + "TEACHER_COMPLETIOIN_RATE_HIGH": "Completion Rate (High - Low)", + "TEACHER_COMPLETIOIN_RATE_LOW": "Completion Rate (Low - High)", + "TEACHER_APPLY": "APPLY" } diff --git a/src/App.jsx b/src/App.jsx index be9b682..614073a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,6 +13,8 @@ import Loading from "./components/common/Loading"; import customTheme from "./utils/theme"; import FingerprintJS from "@fingerprintjs/fingerprintjs"; import { checkUserDetails } from "./services/auth/auth"; +import teacherAuthRoutes from "./routes/teacherAuth"; +import { jwtDecode } from "jwt-decode"; const theme = extendTheme(customTheme); @@ -24,7 +26,13 @@ function AppRouter() { const navigate = useNavigate(); useEffect(() => { if (token && token !== "not-logged-in") { - setRoutes(authRoutes); + const tokenDecoded = jwtDecode(token); + const roles = tokenDecoded?.resource_access?.["hasura-app"]?.roles; + if (authUser && Array.isArray(roles) && roles.includes("teacher")) { + setRoutes(teacherAuthRoutes); + } else if (authRoutes) { + setRoutes(authRoutes); + } } else { setRoutes(guestRoutes); } diff --git a/src/assets/images/home-bg.png b/src/assets/images/home-bg.png index 8b962ee..5cda93c 100644 Binary files a/src/assets/images/home-bg.png and b/src/assets/images/home-bg.png differ diff --git a/src/components/common/ActionSheet.tsx b/src/components/common/ActionSheet.tsx new file mode 100644 index 0000000..34de6e6 --- /dev/null +++ b/src/components/common/ActionSheet.tsx @@ -0,0 +1,57 @@ +import React, { ReactNode, memo } from "react"; +import { + Drawer, + DrawerOverlay, + DrawerContent, + DrawerCloseButton, + DrawerHeader, + DrawerBody, +} from "@chakra-ui/react"; +import useDeviceSize from "./layout/useDeviceSize"; +import IconByName from "./icons/Icon"; + +type RectComponentProps = { + isOpen: boolean; + onClose: () => void; + headerComponent?: ReactNode; + children?: ReactNode; + _props?: any; + _header?: any; +}; + +const ActionSheet: React.FC = memo( + ({ isOpen, onClose, headerComponent, children, _props, _header }) => { + const { width } = useDeviceSize(); + + return ( + + + + + {headerComponent && ( + {headerComponent} + )} + {children} + + + ); + } +); + +export default ActionSheet; diff --git a/src/components/common/cards/ClassCard.tsx b/src/components/common/cards/ClassCard.tsx new file mode 100644 index 0000000..1ee91c7 --- /dev/null +++ b/src/components/common/cards/ClassCard.tsx @@ -0,0 +1,119 @@ +import React, { memo } from "react"; +import { Box, Progress, Text, HStack, VStack } from "@chakra-ui/react"; +import IconByName from "../icons/Icon"; +import { useTranslation } from "react-i18next"; +import SubjectGrid from "./SubjectGrid"; +import { use } from "i18next"; + +interface ClassCardProps { + title: boolean; + selectedSubject?: string | null; + subjectClick?: (subject: string) => void; + data: any; + onClick?: () => void; +} + +const ClassCard: React.FC = memo( + ({ title, data, onClick, subjectClick, selectedSubject }) => { + const { t } = useTranslation(); + const [selectedSubjectNew, setSelectedSubject] = React.useState< + string | undefined + >(undefined); + + React.useEffect(() => { + setSelectedSubject(selectedSubject || ""); + }, [selectedSubject]); + + const onSelectSubject = (subject: string) => { + setSelectedSubject(subject); + subjectClick?.(subject); + }; + return ( + + + {/* Class Header */} + {title && ( + + + + {data?.title} + + + + + + {t("TEACHER_OVERALL_PROGRESS")} + + + {/* Overall Progress Bar with Percentage */} + + + + + + {data?.classCompletionPercentage} + + + % + + + + + )} + + + + ); + } +); + +export default ClassCard; diff --git a/src/components/common/cards/SubjectGrid.tsx b/src/components/common/cards/SubjectGrid.tsx new file mode 100644 index 0000000..1237488 --- /dev/null +++ b/src/components/common/cards/SubjectGrid.tsx @@ -0,0 +1,170 @@ +import { HStack, Image, Progress, VStack } from "@chakra-ui/react"; +import React, { memo } from "react"; +import english from "../../../assets/icons/english_icon.svg"; +import kannada from "../../../assets/icons/kannada_icon.svg"; +import math from "../../../assets/icons/maths_icon.svg"; +import odia from "../../../assets/icons/odiya_icon.svg"; +import physics from "../../../assets/icons/physics_icon.svg"; +import IconByName from "../icons/Icon"; +import CustomHeading from "../typography/Heading"; +import { chunk } from "lodash"; + +const subjectIcons = { + science: { icon: physics, label: "Science" }, + mathematics: { icon: math, label: "Math" }, + math: { icon: math, label: "Math" }, + english: { icon: english, label: "English" }, + kannada: { icon: kannada, label: "Kannada" }, + odia: { icon: odia, label: "Odia" }, +}; + +interface SubjectGridProps { + subjects: any[]; + selectedSubject?: string | null; + onSelectSubject?: (subject: string) => void; + isClickable: boolean; + isAllSubjectPersentage?: number; +} + +const SubjectGrid: React.FC = memo( + ({ + subjects, + selectedSubject, + onSelectSubject, + isClickable, + isAllSubjectPersentage, + }) => { + const [subjectItems, setSubjects] = React.useState>([]); + const handleSelectSubject = (subject: string) => { + onSelectSubject?.(subject); + }; + React.useEffect(() => { + if (!subjects) return; + let newSubject = [...subjects]; + if (isAllSubjectPersentage) { + newSubject = [ + { + subject: "all", + percentage: isAllSubjectPersentage, + isClickable, + }, + ...newSubject, + ]; + } + const data = chunk(newSubject, 4); + setSubjects(data); + }, [subjects]); + + return ( + + {subjectItems?.map((subject, index) => ( + } + justifyContent={subject.length < 4 ? "flex-start" : "space-around"} + > + {subject && + subject?.map( + (subjectRow: any, index: number) => + subjectRow?.subject && ( + + ) + )} + + ))} + + ); + } +); + +export default SubjectGrid; + +const SubjectCard = ({ + isClickable, + percentage, + subject, + selectedSubject, + onSelectSubject, +}: { + percentage: number; + subject: string; + isClickable?: boolean; + selectedSubject?: string; + onSelectSubject?: (subject: string) => void; +}) => { + const handleSelectSubject = (subject: string) => { + if (isClickable) { + onSelectSubject?.(subject); + } + }; + + return ( + handleSelectSubject(subject)} + cursor={isClickable ? "pointer" : "default"} + rounded={subject === selectedSubject ? "1rem" : "16px"} + > + + {subjectIcons[subject?.toLowerCase() as keyof typeof subjectIcons] + ?.icon && ( + {`${subject} + )} + + + {(typeof percentage === "string" && parseInt(percentage) < 100) || + percentage < 100 ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/components/common/icons/Icon.tsx b/src/components/common/icons/Icon.tsx index 2865d2d..993c438 100644 --- a/src/components/common/icons/Icon.tsx +++ b/src/components/common/icons/Icon.tsx @@ -334,6 +334,16 @@ const iconsMap: Record> = { ); }, + SwapVertIcon: ({ color, active, ...props }) => { + return ( + + + + ); + }, ChevronRightIcon, ChevronLeftIcon, ChevronUpIcon, @@ -359,7 +369,7 @@ const IconByName: React.FC = ({ - - ) : ( -
- )} - - )} - + {/* Sticky UserCoinInfo Footer */} + ); }; export default LeaderboardScreen; -interface PopupProps { - activeCollapse: "history" | "view" | "none"; - width: string | number; - defaultValue: string; - handleCollapseToggle: (collapse: "history" | "view") => void; - handleViewChange: (type: string) => void; - t: any; +interface HeaderWithFiltersProps { + heading: string; + onBack?: () => void; + onFilterClick?: (type: string) => void; + selectedView?: string; + rightComponent?: React.ReactNode; } -const PopupCustom: React.FC = ({ - activeCollapse, - width, - defaultValue, - handleCollapseToggle, - handleViewChange, - t, -}) => { - const [radioValue, setRadioValue] = useState(defaultValue || "School"); - - return ( - - - +const HeaderWithFilters: React.FC = memo( + ({ heading, onBack, onFilterClick, selectedView, rightComponent }) => { + return ( + + {/* Back Icon & Heading */} + + - {t("LEADERBOARD_VIEW")} + {heading} - - + + {/* Right Side (Customizable) */} + {rightComponent || ( + + + VIEW + + onFilterClick && onFilterClick("dropdown")} cursor="pointer" - width="14px" - height="14px" - onClick={() => handleCollapseToggle("view")} - /> - - - - {t("LEADERBOARD_HOW_YOU_STAND_WITH_OTHER")} - - setRadioValue(value)} - gap="8px" - > - - - - - {t("LEADERBOARD_SCHOOL")} - - - {t("LEADERBOARD_DEFAULT")} - - - - - - {t("LEADERBOARD_CLASS")} + > + + {selectedView || "School" || "Select"} - - - - {t("LEADERBOARD_BOARD")} - - - - - - - - ); -}; + + + + )} + + ); + } +); diff --git a/src/pages/leaderboard/Students.tsx b/src/pages/leaderboard/Students.tsx new file mode 100644 index 0000000..81e9166 --- /dev/null +++ b/src/pages/leaderboard/Students.tsx @@ -0,0 +1,183 @@ +import React from "react"; +import { + Table, + TableContainer, + Thead, + Th, + Tr, + Tbody, + Td, + Text, + VStack, + RadioGroup, + Radio, + HStack, + Button, +} from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import ActionSheet from "../../components/common/ActionSheet"; + +interface StudentsProps { + data: any[]; + bodyHeight: string; + activeCollapse: string | undefined; + radioValue: string; + handleViewChange: (radioSelection: string) => void; +} + +const Students: React.FC = ({ + data, + bodyHeight, + activeCollapse, + radioValue, + handleViewChange, +}) => { + const { t } = useTranslation(); + const [localRadioValue, setLocalRadioValue] = React.useState(radioValue); + + React.useEffect(() => { + setLocalRadioValue(radioValue); + }, [radioValue]); + + return ( + + + + + + + + + + + {data.map((item: any, index) => ( + + + + + + ))} + +
{t("LEADERBOARD_NAME")} + {t("LEADERBOARD_RANK")} + + {t("LEADERBOARD_COINS")} +
+ + {item?.userId === localStorage.getItem("id") + ? "You" + : item?.name} + + + {item?.className} + + + + {item?.rank} + + + + {item?.points} + +
+ handleViewChange(localRadioValue)} + headerComponent={ + + + {t("LEADERBOARD_VIEW")} + + + {t("LEADERBOARD_HOW_YOU_STAND_WITH_OTHER")} + + + } + > + + + + + + + {t("LEADERBOARD_SCHOOL")} + + + {t("LEADERBOARD_DEFAULT")} + + + + + + {t("LEADERBOARD_CLASS")} + + + + + {t("LEADERBOARD_BOARD")} + + + + + + + +
+ ); +}; + +export default Students; diff --git a/src/pages/leaderboard/UserCoinInfo.tsx b/src/pages/leaderboard/UserCoinInfo.tsx new file mode 100644 index 0000000..9342a24 --- /dev/null +++ b/src/pages/leaderboard/UserCoinInfo.tsx @@ -0,0 +1,414 @@ +import { + Box, + Button, + Divider, + Flex, + HStack, + Radio, + RadioGroup, + Spinner, + Text, + VStack, +} from "@chakra-ui/react"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import ActionSheet from "../../components/common/ActionSheet"; +import { getCurrentUserdetail } from "../../services/home"; + +const IdentifiersList = { + subject_completion: { + label: "Subject Completion", + }, + assesment_completion: { + label: "Quiz Completion", + }, + content: { + label: "Content Completion", + }, + lesson_completion: { + label: "Content Completion", + }, +}; + +interface CoinInfoProps { + activeCollapse: string | undefined; + width: string; + handleCollapseToggle: (content: string) => void; + handleViewChange: (radioSelection: string) => void; + myData: any; +} + +const UserCoinInfo: React.FC = ({ + activeCollapse, + width, + handleCollapseToggle, + handleViewChange, + myData, +}) => { + const { t } = useTranslation(); + return ( + + + + + } + > + + + {t("LEADERBOARD_YOUR_RANK")} + + + {myData?.rank} + + + {/* Vertical Divider */} + {/* YOUR COINS */} + + + {t("LEADERBOARD_YOUR_COINS")} + + + {myData?.points} + + + {/* +32 Coins for Quiz */} + + + + +{myData?.lastEarnedPoints?.[0]?.points} + + + {t("LEADERBOARD_CONIS")} + + + + {t( + myData?.lastEarnedPoints?.[0]?.identifier === + "subject_completion" + ? "LEADERBOARD_SUBJECT" + : myData?.lastEarnedPoints?.[0]?.identifier === + "assesment_completion" + ? "LEADERBOARD_QUIZ" + : "LEADERBOARD_CONTENT" + )} + + + {myData?.lastEarnedPoints?.[0]?.created_at && + moment(myData?.lastEarnedPoints?.[0]?.created_at).fromNow()} + + + {/* VIEW HISTORY Button */} + + + + + ); +}; + +export default React.memo(UserCoinInfo); + +interface PopupProps { + activeCollapse: string | undefined; + width: string | number; + handleCollapseToggle: (content: string) => void; + myData: any; +} + +const PopupCustom: React.FC = ({ + activeCollapse, + handleCollapseToggle, + myData, +}) => { + const [coninsData, setCoinsData] = useState(); + const [page, setPage] = useState(1); + const [hasMoreData, setHasMoreData] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const { t } = useTranslation(); + const handleClose = () => { + handleCollapseToggle(""); + setPage(1); + setCoinsData([]); + setHasMoreData(true); + setLoadingMore(false); + }; + + const fetchUserData = async (currentPage: number) => { + try { + if (loadingMore) return; + setLoadingMore(true); + const data: any = await getCurrentUserdetail(currentPage); + + if (!data || (data.totalPages === page && data.totalPages !== 1)) { + setHasMoreData(false); + setLoadingMore(false); + return; + } + setCoinsData((prevData: any) => [...(prevData || []), ...data?.points]); + setLoadingMore(false); + } catch (error) { + console.error("Error fetching user stats:", error); + setLoadingMore(false); + } finally { + } + }; + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - scrollTop === clientHeight && hasMoreData) { + setPage((prevPage) => prevPage + 1); + } + }; + React.useEffect(() => { + if (!page || !hasMoreData || !activeCollapse) return; + fetchUserData(page); + }, [page, activeCollapse]); + return ( + + {/* Title: Coins History */} + + {t("LEADERBOARD_COINS_HISTORY")} + + {/* Right Section: Rank, Coins, and Close Button */} + {/* Your Rank */} + + + } + > + + + {t("LEADERBOARD_YOUR_RANK")} + + + {myData?.rank} + + + {/* Your Coins */} + + + {t("LEADERBOARD_YOUR_COINS")} + + + {myData?.points} + + + + + } + > + + + {coninsData?.map((item: any, index: number) => ( + + + + {item?.points} {t("LEADERBOARD_COINS")} + + + {moment(item?.created_at).format("D MMM, YYYY")} + + + + + {IdentifiersList[ + item?.identifier?.toLowerCase() as keyof typeof IdentifiersList + ]?.label || item?.identifier} + + + + ))} + {loadingMore ? ( + + + + ) : ( + + {t("LEADERBOARD_NO_DATA")} + + )} + + + + ); +}; diff --git a/src/pages/teacher/ClassDetails.tsx b/src/pages/teacher/ClassDetails.tsx new file mode 100644 index 0000000..f0931d3 --- /dev/null +++ b/src/pages/teacher/ClassDetails.tsx @@ -0,0 +1,104 @@ +import { Box, Text, VStack } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import ClassCard from "../../components/common/cards/ClassCard"; +import Layout from "../../components/common/layout/layout"; +import { getTeacherData } from "../../services/home"; +import { impression } from "../../services/telemetry"; +import Students from "./class/Students"; + +export default function ClassDetails(props: any) { + const { t } = useTranslation(); + const [error, setError] = useState(null); + const { authUser } = props; + const { board, schoolUdise, grade, medium, groupId, subject } = useParams(); + const [classDetails, setClassDetails] = useState({}); + const navigate = useNavigate(); + const [selectedSubject, setSelectedSubject] = useState(subject); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchProgramId = async () => { + try { + const payload = { + groupId: groupId, + schoolUdise: schoolUdise, + grade: String(grade), + medium: medium, + board: board, + }; + let data = await getTeacherData(payload); + const classObj = { + ...data, + subjects: data?.subjectResults || [], + }; + setClassDetails(classObj); + } catch (error) { + console.error("Error fetching program data:", error); + setError(t("An unexpected error occurred. Please try again later.")); + } finally { + setLoading(false); + } + }; + + fetchProgramId(); + }, [authUser]); + + useEffect(() => { + impression({ + edata: { + type: "TeacherPage", + pageid: "TeacherPage", + uri: "/teacher", + query: Object.fromEntries( + new URLSearchParams(location.search).entries() + ), + visits: [], + }, + }); + }, []); + + return ( + navigate("/class"), + }} + > + + {error ? ( + + {error} + + ) : ( + + setSelectedSubject(e)} + selectedSubject={selectedSubject || "all"} + /> + {/* Table Section */} + + + )} + + + ); +} diff --git a/src/pages/teacher/TeacherPage.tsx b/src/pages/teacher/TeacherPage.tsx new file mode 100644 index 0000000..0fa585d --- /dev/null +++ b/src/pages/teacher/TeacherPage.tsx @@ -0,0 +1,134 @@ +import { Center, VStack } from "@chakra-ui/react"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { impression } from "../../services/telemetry"; +import CustomHeading from "../../components/common/typography/Heading"; +import Layout from "../../components/common/layout/layout"; +import ClassCard from "../../components/common/cards/ClassCard"; +import { getTeacherData } from "../../services/home"; +import { checkUserDetails } from "../../services/auth/auth"; + +export default function TeacherHomepage(props: any) { + const { t } = useTranslation(); + const [error, setError] = useState(null); + const navigate = useNavigate(); + const { authUser } = props; + const [classes, setClasses] = useState([]); + const [loading, setLoading] = useState(true); + const fetchTeacherDataForClasses = React.useCallback(async () => { + try { + const result = await checkUserDetails(); + if (!result) { + setError("No class details found."); + return; + } + + const groupMemberships = result?.data?.GroupMemberships || []; + + const classDataArray = await Promise.all( + groupMemberships.map(async (classDetail: any) => { + const payload = { + groupId: classDetail?.Group?.groupId, + schoolUdise: classDetail?.School?.udiseCode, + grade: String(classDetail?.Group?.grade), + medium: classDetail?.Group?.medium, + board: classDetail?.Group?.board, + }; + + const data = await getTeacherData(payload); + + // Create class object + const classObj = { + ...data, + subjects: data?.subjectResults || [], + title: `Class ${classDetail?.Group?.grade}`, + groupId: classDetail?.Group?.groupId, + schoolUdise: classDetail?.School?.udiseCode, + grade: String(classDetail?.Group?.grade), + medium: classDetail?.Group?.medium, + board: classDetail?.Group?.board, + }; + + return classObj; + }) + ); + setClasses(classDataArray); + } catch (error: any) { + setError(`Error fetching teacher data for classes:${error?.message}`); + } finally { + setLoading(false); + } + }, [authUser]); + + useEffect(() => { + fetchTeacherDataForClasses(); + }, [authUser]); + + useEffect(() => { + impression({ + edata: { + type: "TeacherHomepage", + pageid: "TeacherHomepage", + uri: "/teacher", + query: Object.fromEntries( + new URLSearchParams(location.search).entries() + ), + visits: [], + }, + }); + }, []); + + const handleCardClick = (group: any, subject?: any) => { + navigate( + subject + ? `/class/${group.board}/${group.schoolUdise}/${group.grade}/${group.medium}/${group.groupId}/${subject}` + : `/class/${group.board}/${group.schoolUdise}/${group.grade}/${group.medium}/${group.groupId}`, + {} + ); + }; + + return ( + + {error ? ( +
+ + {error} + +
+ ) : ( + + + + {classes?.map((group: any) => ( + handleCardClick(group)} + subjectClick={(sub) => handleCardClick(group, sub)} + /> + ))} + + )} +
+ ); +} diff --git a/src/pages/teacher/class/Students.tsx b/src/pages/teacher/class/Students.tsx new file mode 100644 index 0000000..a15d495 --- /dev/null +++ b/src/pages/teacher/class/Students.tsx @@ -0,0 +1,254 @@ +import { SearchIcon } from "@chakra-ui/icons"; +import { + Button, + Flex, + HStack, + Input, + InputGroup, + InputRightElement, + Radio, + RadioGroup, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + VStack, +} from "@chakra-ui/react"; +import React, { memo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import ActionSheet from "../../../components/common/ActionSheet"; +import IconByName from "../../../components/common/icons/Icon"; +import CustomHeading from "../../../components/common/typography/Heading"; +import { getTeacherData } from "../../../services/home"; + +interface payload { + groupId: string; + schoolUdise: string; + grade: string; + medium: string; + board: string; + subject?: string; +} + +const subjectSort = [ + { label: "TEACHER_A_Z", value: "a-z" }, + { label: "TEACHER_Z_A", value: "z-a" }, + { label: "TEACHER_COMPLETIOIN_RATE_HIGH", value: "highToLow" }, + { label: "TEACHER_COMPLETIOIN_RATE_LOW", value: "lowToHigh" }, +]; + +const Students: React.FC = (payload) => { + const { t } = useTranslation(); + + const [students, setstudents] = useState | null>(null); + + const [activeCollapse, setActiveCollapse] = useState( + undefined + ); + const [radioSelection, setRadioSelection] = useState( + undefined + ); + const [selectedView, setSelectedView] = useState( + undefined + ); + const [searchText, setSearchText] = useState(""); + + const handleCollapseToggle = (collapse: string) => { + setActiveCollapse(collapse === activeCollapse ? undefined : collapse); + }; + + const handleViewChange = (view: string) => { + setSelectedView(view); + setActiveCollapse(undefined); + }; + + const filterSubjects = (students: Array) => { + const filteredStudents = students.filter((student) => + student.username.toLowerCase().includes(searchText.toLowerCase()) + ); + if (selectedView === "highToLow") { + filteredStudents.sort( + (a, b) => + Number(b.completionPercentage) - Number(a.completionPercentage) + ); + } else if (selectedView === "lowToHigh") { + filteredStudents.sort( + (a, b) => + Number(a.completionPercentage) - Number(b.completionPercentage) + ); + } else if (selectedView === "a-z") { + filteredStudents.sort((a, b) => a.username.localeCompare(b.username)); + } else if (selectedView === "z-a") { + filteredStudents.sort((a, b) => b.username.localeCompare(a.username)); + } + return filteredStudents; + }; + + React.useEffect(() => { + const init = async () => { + let students = await getTeacherData({ + ...payload, + studentProgress: "true", + }); + setstudents( + students?.userCompletionPercentages || students?.classResults || [] + ); + }; + init(); + }, [payload]); + + return ( + + handleCollapseToggle("none")} + headerComponent={ + + + {t("TEACHER_SORT")} + + + } + > + + setRadioSelection(value)} + w="100%" + > + + {subjectSort.map(({ label, value }) => ( + + + {t(label)} + + + ))} + + + + + + {/* Header Section */} + + + + + + + + setSearchText(e.target.value)} + value={searchText} + /> + + + + + + + + + + + + + + {filterSubjects(students || []).map((student, index) => ( + + + + + ))} + +
+ + {t("LEADERBOARD_NAME")} + + + + {t("TEACHER_COMPLETION_RATE")} + +
{student.username} + {student.completionPercentage} % +
+
+ ); +}; + +export default memo(Students); diff --git a/src/pages/videos/SearchPage.tsx b/src/pages/videos/SearchPage.tsx index 2432f5c..a1b033d 100644 --- a/src/pages/videos/SearchPage.tsx +++ b/src/pages/videos/SearchPage.tsx @@ -78,6 +78,7 @@ const SearchPage: React.FC = () => { _header={{ searchTerm: searchTerm, onSearchChange: setSearchTerm, + userInfo: false, }} > {loading && } diff --git a/src/pages/videos/WatchScreen.tsx b/src/pages/videos/WatchScreen.tsx index 86dd994..4f09eed 100644 --- a/src/pages/videos/WatchScreen.tsx +++ b/src/pages/videos/WatchScreen.tsx @@ -158,6 +158,10 @@ const Watch = (prop: any) => { onSelectItem={handleSelectSubject} /> ), + userInfo: false, + isShowBackButton: true, + headingTitle: t("HOME_WATCH"), + onBack: () => navigate("/home"), }} isFooterVisible={false} isHeaderVisible={true} diff --git a/src/pages/videos/videoReel/AssessmentPlayer.tsx b/src/pages/videos/videoReel/AssessmentPlayer.tsx index b97df14..a957b79 100644 --- a/src/pages/videos/videoReel/AssessmentPlayer.tsx +++ b/src/pages/videos/videoReel/AssessmentPlayer.tsx @@ -288,6 +288,7 @@ const AssessmentPlayer: React.FC = ({ ) : ( ( )} diff --git a/src/routes/teacherAuth.tsx b/src/routes/teacherAuth.tsx new file mode 100644 index 0000000..a0a3cf7 --- /dev/null +++ b/src/routes/teacherAuth.tsx @@ -0,0 +1,19 @@ +import { lazy } from "react"; + +const TeacherHomepage = lazy(() => import("../pages/teacher/TeacherPage")); +const ClassDetails = lazy(() => import("../pages/teacher/ClassDetails")); + +export default [ + { + path: "/class/:board/:schoolUdise/:grade/:medium/:groupId", + component: ClassDetails, + }, + { + path: "/class/:board/:schoolUdise/:grade/:medium/:groupId/:subject", + component: ClassDetails, + }, + { + path: "*", + component: TeacherHomepage, + }, +]; diff --git a/src/services/home.ts b/src/services/home.ts index ab4bcaf..c6528db 100644 --- a/src/services/home.ts +++ b/src/services/home.ts @@ -60,9 +60,7 @@ export const getSubjectList = async () => { }; const response = await fetch( - `${ - import.meta.env.VITE_API_AUTH_URL - }${URL.SUBJECT_LIST}`, + `${import.meta.env.VITE_API_AUTH_URL}${URL.SUBJECT_LIST}`, { method: "POST", headers: headers, @@ -93,36 +91,32 @@ export const getSubjectList = async () => { } }; -export const getLeaderboardFilter = async (payload:any) => { +export const getLeaderboardFilter = async (payload: any) => { try { + const headers = { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "Content-Type": "application/json", + }; - const headers = { - Authorization: `Bearer ${localStorage.getItem("token")}`, - "Content-Type": "application/json", - }; - - const response = await fetch( - `${ - import.meta.env.VITE_API_AUTH_URL - }${URL.LEADERBOARD_FILTER_LIST}`, - { - method: "POST", - headers: headers, - body: JSON.stringify(payload), - } - ); - - if (!response.ok) { - throw new Error("Failed to fetch subject list"); + const response = await fetch( + `${import.meta.env.VITE_API_AUTH_URL}${URL.LEADERBOARD_FILTER_LIST}`, + { + method: "POST", + headers: headers, + body: JSON.stringify(payload), } + ); - const subjectList = await response.json(); + if (!response.ok) { + throw new Error("Failed to fetch subject list"); + } - if (subjectList?.data) { - return _.sortBy(subjectList.data, "rules"); - } + const subjectList = await response.json(); + + if (subjectList?.data) { + return _.sortBy(subjectList.data, "rules"); } - catch (error) { + } catch (error) { console.error("Error in getting subject list:", error); throw error; } @@ -163,4 +157,34 @@ export const getCurrentUserdetail = async ( throw error; } }; +export const getTeacherData = async (payload: any) => { + try { + const headers = { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "Content-Type": "application/json", + }; + + const response = await fetch( + `${import.meta.env.VITE_API_AUTH_URL}${URL.PROGRESS}`, + { + method: "POST", + headers: headers, + body: JSON.stringify(payload), + } + ); + if (!response.ok) { + throw new Error("Failed to fetch user data"); + } + const userData = await response.json(); + + if (userData?.data) { + return userData?.data; + } else { + return []; + } + } catch (error) { + console.error("Error in getting user data:", error); + throw error; + } +}; diff --git a/src/services/telemetry.ts b/src/services/telemetry.ts index 1106bcf..43c2bba 100644 --- a/src/services/telemetry.ts +++ b/src/services/telemetry.ts @@ -1,3 +1,4 @@ +import { updateCdataTag } from "../pages/videos/utils"; import { getSid, uniqueId } from "./utilService"; // generate manually const VITE_TELEMETRY_BASE_URL = import.meta.env.VITE_TELEMETRY_BASE_URL; const VITE_TELEMETRY_END_POINT = import.meta.env.VITE_TELEMETRY_END_POINT; @@ -11,18 +12,6 @@ import { merge } from "lodash"; const getDefaultStartEvent = (props: Record = {}) => { const cdataNew = [ - { - id: localStorage.getItem("grade") || "", - type: "grade", - }, - { - id: localStorage.getItem("medium") || "", - type: "medium", - }, - { - id: localStorage.getItem("board") || "", - type: "board", - }, { id: localStorage.getItem("username"), type: "username", @@ -36,6 +25,7 @@ const getDefaultStartEvent = (props: Record = {}) => { type: "program", }, ]; + const updatedContext = updateCdataTag(cdataNew); const defaultEvent = { eid: "START", ets: Date.now(), diff --git a/src/services/utilService.ts b/src/services/utilService.ts index 9793315..b26536d 100644 --- a/src/services/utilService.ts +++ b/src/services/utilService.ts @@ -14,6 +14,9 @@ export function uniqueId(length = 32) { } export function getSid() { + if (!localStorage.getItem("token")) { + return ""; + } const tokenDecoded: any = jwtDecode(localStorage.getItem("token") || ""); const date = new Date( Date.now() + new Date().getTimezoneOffset() * 60 * 1000 diff --git a/src/utils/constants/url-constants.json b/src/utils/constants/url-constants.json index 430caf6..733e9fb 100644 --- a/src/utils/constants/url-constants.json +++ b/src/utils/constants/url-constants.json @@ -9,9 +9,11 @@ "ALT_PROGRAM_BMGS": "/api/v1/altprogram/bmgs", "USER_VALIDATION": "/api/v1/user/validateToken", "SEARCH": "/altprogramassociation/contentSearch", - "CONTENT_LIKE" : "/altprogramassociation/contentLike", - "CONTENT_IS_LIKED" : "/altprogramassociation/isContentLiked", + "CONTENT_LIKE": "/altprogramassociation/contentLike", + "CONTENT_IS_LIKED": "/altprogramassociation/isContentLiked", "LEADERBOARD_FILTER_LIST": "/api/v1/altprogramassociation/leaderBoardPoints", "LEADERBOARD_USER_DATA": "/api/v1/altprogramassociation/getUserPoints", - "RATE_QUIZ": "/altprogramassociation/rateQuiz" + "RATE_QUIZ": "/altprogramassociation/rateQuiz", + "CLASS_PROGRESS": "/api/v1/teacher/classProgress", + "PROGRESS": "/api/v1/teacher/progress" } diff --git a/src/utils/theme.tsx b/src/utils/theme.tsx index d9ace9f..3846927 100644 --- a/src/utils/theme.tsx +++ b/src/utils/theme.tsx @@ -100,6 +100,9 @@ const customTheme = extendTheme({ borderGrey: "#C5C5C5", lightGrey: "#828282", greenColor: "#219653", + tsSeaBlue20: "#03627C33", + tsSeaBlue40: "#023B4A", + green40: "#03570E", }, components: { Table: {