Compare commits

...

28 commits

Author SHA1 Message Date
KenChanA
99c6432105 Merge remote-tracking branch 'origin/main' into dev-percent-display 2025-03-16 02:39:24 -04:00
Christopher (Cris) Fuhrman
112062c0b2
Merge pull request #284 from ets-cfuhrman-pfe/fuhrmanator/issue283
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 0s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 0s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 0s
Tests / lint-and-tests (client) (push) Failing after 0s
Tests / lint-and-tests (server) (push) Failing after 0s
[BUG] étudiant qui se joint à une salle après le démarrage du quiz es…
2025-03-11 02:52:58 -04:00
Christopher (Cris) Fuhrman
c57a4d69ab
Merge pull request #287 from ets-cfuhrman-pfe/fuhrmanator/singleton-mongo-connection
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 19s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 18s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m10s
Tests / lint-and-tests (server) (push) Failing after 56s
Fixes #286 - Faire un vrai singleton pour la connexion à la BD
2025-03-10 15:23:13 -04:00
C. Fuhrman
dcaee719d1 Fixes #286 - Faire un vrai singleton pour la connexion à la base de données 2025-03-10 15:20:07 -04:00
C. Fuhrman
29de2a7671 Correction de bogue trouvé par test! 2025-03-09 01:19:31 -05:00
C. Fuhrman
fe67f020eb [BUG] étudiant qui se joint à une salle après le démarrage du quiz est bloqué
Fixes #283
Valeurs de l'état de la page (quizStarted) n'ont pas leur valeur actuelle dans un on(). Alors, on déplace la logique du traitement du nouvel étudiant dans un useEffect et on provoque le useEffect dans le on()
2025-03-09 00:54:21 -05:00
Christopher (Cris) Fuhrman
ba055070a3
Merge pull request #282 from ets-cfuhrman-pfe/bug/sso-email
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 59s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 59s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m26s
Tests / lint-and-tests (server) (push) Failing after 56s
FIX sso envoi email
2025-03-08 18:32:43 -05:00
Christopher (Cris) Fuhrman
cbfd37ae0e
Merge pull request #269 from ets-cfuhrman-pfe/JubaAzul/issue200
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 58s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 58s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / lint-and-tests (client) (push) Failing after 1m26s
Tests / lint-and-tests (server) (push) Failing after 58s
Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
2025-03-08 11:30:43 -05:00
C. Fuhrman
623b749e4f refactor AnswerType 2025-03-08 11:05:25 -05:00
C. Fuhrman
73e0326f44 tests améliorés (toujours pas idéal) 2025-03-08 02:31:55 -05:00
C. Fuhrman
ca1eb4737d Plus besoin de local storage si on passe toutes les questions à TeacherModeQuiz aussi
Nécessite un nouveau message launch-teacher-mode
2025-03-08 01:09:41 -05:00
JubaAzul
22482592cd Test 2025-03-07 16:33:57 -05:00
Eddi3_As
79066179ac FIX sso envoi email 2025-03-07 14:04:30 -05:00
JubaAzul
ec2de888ec Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
Fixes #200
2025-03-07 13:39:56 -05:00
JubaAzul
1c2d4134af Ajout de cross-env dans package.json 2025-03-07 13:12:32 -05:00
C. Fuhrman
2b7ea55f9c Merge branch 'JubaAzul/issue200' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into JubaAzul/issue200 2025-03-07 12:50:19 -05:00
C. Fuhrman
75b91346e3 Merge branch 'main' into JubaAzul/issue200 2025-03-07 12:46:45 -05:00
JubaAzul
70b788fbd0 Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
Fixes #200
2025-03-07 12:42:56 -05:00
JubaAzul
6a340556e2 Amélioration de beaucoup de features 2025-03-07 12:20:39 -05:00
JubaAzul
f8dd95f651 Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
Fixes #200
2025-03-07 11:36:43 -05:00
C. Fuhrman
74fcc23a07 Revert "Merge pull request #280 from ets-cfuhrman-pfe/bug/sso-consent"
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 18s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 18s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / lint-and-tests (client) (push) Failing after 1m2s
Tests / lint-and-tests (server) (push) Failing after 1m0s
This reverts commit 7418f53e31, reversing
changes made to 9d24507f41.
2025-03-06 21:19:57 -05:00
C. Fuhrman
7afaa54758 Revert "Merge pull request #281 from ets-cfuhrman-pfe/bug/sso-consent"
This reverts commit 59dc478eb1, reversing
changes made to 7418f53e31.
2025-03-06 21:18:49 -05:00
Christopher (Cris) Fuhrman
59dc478eb1
Merge pull request #281 from ets-cfuhrman-pfe/bug/sso-consent
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 17s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 17s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m6s
Tests / lint-and-tests (server) (push) Failing after 57s
corrections to received user
2025-03-06 20:28:01 -05:00
C. Fuhrman
6b0cca9929 corrections to received user 2025-03-06 20:24:12 -05:00
Christopher (Cris) Fuhrman
7418f53e31
Merge pull request #280 from ets-cfuhrman-pfe/bug/sso-consent
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 57s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 56s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m33s
Tests / lint-and-tests (server) (push) Failing after 56s
FIX consent & ajout tests pour oidc
2025-03-06 19:49:26 -05:00
Eddi3_As
50924f8576 FIX consent & ajout tests pour oidc 2025-03-06 18:22:40 -05:00
JubaAzul
60ad2df67d Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
Fixes #200
2025-03-04 16:49:12 -05:00
JubaAzul
fe44409e16 Réponse à une question non enregistrée lorsque Étudiant reviens en arrière dans le quiz
Fixes #200
2025-03-03 17:00:42 -05:00
26 changed files with 648 additions and 336 deletions

208
client/package-lock.json generated
View file

@ -53,6 +53,7 @@
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"cross-env": "^7.0.3",
"eslint": "^9.21.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^28.11.0",
@ -2536,7 +2537,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@ -2555,7 +2556,7 @@
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@ -2565,7 +2566,7 @@
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.6",
@ -2580,7 +2581,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -2591,7 +2592,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -2604,7 +2605,7 @@
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
@ -2617,7 +2618,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz",
"integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
@ -2641,7 +2642,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -2652,7 +2653,7 @@
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18"
@ -2665,7 +2666,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -2678,7 +2679,7 @@
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz",
"integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2688,7 +2689,7 @@
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2698,7 +2699,7 @@
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.12.0",
@ -2805,7 +2806,7 @@
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
@ -2815,7 +2816,7 @@
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
@ -2829,7 +2830,7 @@
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
@ -2843,7 +2844,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
@ -2857,7 +2858,7 @@
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
@ -3859,7 +3860,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3873,7 +3873,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3887,7 +3886,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3901,7 +3899,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3915,7 +3912,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3929,7 +3925,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3943,7 +3938,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3957,7 +3951,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3971,7 +3964,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3985,7 +3977,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -3999,7 +3990,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4013,7 +4003,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4027,7 +4016,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4041,7 +4029,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4055,7 +4042,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4069,7 +4055,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4083,7 +4068,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4097,7 +4081,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4111,7 +4094,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -4152,7 +4134,7 @@
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.5.tgz",
"integrity": "sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA==",
"dev": true,
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@ -4194,7 +4176,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4211,7 +4192,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4228,7 +4208,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -4245,7 +4224,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4262,7 +4240,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4279,7 +4256,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4296,7 +4272,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4313,7 +4288,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4330,7 +4304,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4347,7 +4320,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4361,14 +4333,14 @@
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz",
"integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
@ -4562,7 +4534,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
@ -4669,7 +4640,7 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/katex": {
@ -4718,7 +4689,6 @@
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@ -5052,7 +5022,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@ -5086,7 +5056,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@ -5177,7 +5147,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"devOptional": true,
"license": "Python-2.0"
},
"node_modules/aria-query": {
@ -5563,7 +5533,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
@ -5898,7 +5868,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
@ -5974,11 +5944,30 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@ -6149,7 +6138,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/deepmerge": {
@ -6715,7 +6704,7 @@
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz",
"integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@ -6945,7 +6934,7 @@
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@ -6962,7 +6951,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -6975,7 +6964,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -6986,7 +6975,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -6999,7 +6988,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -7012,7 +7001,7 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
@ -7030,7 +7019,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -7056,7 +7045,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
@ -7069,7 +7058,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@ -7162,7 +7151,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@ -7199,14 +7188,14 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/fastq": {
@ -7247,7 +7236,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
@ -7301,7 +7290,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
@ -7318,7 +7307,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
@ -7332,7 +7321,7 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
@ -7397,7 +7386,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@ -7581,7 +7569,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@ -7866,7 +7854,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 4"
@ -7912,7 +7900,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
@ -8106,7 +8094,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -8171,7 +8159,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@ -8406,7 +8394,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
@ -9518,7 +9506,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -9588,7 +9576,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
@ -9601,14 +9589,14 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/json5": {
@ -9668,7 +9656,7 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
@ -9698,7 +9686,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
@ -9718,7 +9706,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
@ -9755,7 +9743,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/longest-streak": {
@ -10545,7 +10533,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/node-int64": {
@ -10727,7 +10715,7 @@
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
@ -10763,7 +10751,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@ -10779,7 +10767,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
@ -10847,7 +10835,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -10867,7 +10855,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -11011,7 +10999,6 @@
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -11040,7 +11027,6 @@
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
@ -11059,7 +11045,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
@ -11570,7 +11556,6 @@
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@ -11774,7 +11759,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@ -11787,7 +11772,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -11967,7 +11952,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -12199,7 +12183,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -12483,7 +12467,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
@ -12596,7 +12580,6 @@
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -12842,7 +12825,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@ -12931,7 +12914,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@ -13209,7 +13191,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"devOptional": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@ -13313,7 +13295,7 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -13461,7 +13443,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"

View file

@ -57,6 +57,7 @@
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"cross-env": "^7.0.3",
"eslint": "^9.21.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^28.11.0",

View file

@ -1,5 +1,7 @@
import { AnswerType } from "src/pages/Student/JoinRoom/JoinRoom";
export interface Answer {
answer: string | number | boolean;
answer: AnswerType;
isCorrect: boolean;
idQuestion: number;
}

View file

@ -5,6 +5,7 @@ import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { MultipleChoiceQuestion, parse } from 'gift-pegjs';
import MultipleChoiceQuestionDisplay from 'src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
const questions = parse(
`::Sample Question 1:: Question stem
@ -21,7 +22,7 @@ describe('MultipleChoiceQuestionDisplay', () => {
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => {
const [showAnswerState, setShowAnswerState] = useState(showAnswer);
const handleOnSubmitAnswer = (answer: string) => {
const handleOnSubmitAnswer = (answer: AnswerType) => {
mockHandleOnSubmitAnswer(answer);
setShowAnswerState(true);
};

View file

@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';
import TrueFalseQuestionDisplay from 'src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay';
import { parse, TrueFalseQuestion } from 'gift-pegjs';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
describe('TrueFalseQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();
@ -16,7 +17,7 @@ describe('TrueFalseQuestion Component', () => {
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => {
const [showAnswerState, setShowAnswerState] = useState(showAnswer);
const handleOnSubmitAnswer = (answer: boolean) => {
const handleOnSubmitAnswer = (answer: AnswerType) => {
mockHandleSubmitAnswer(answer);
setShowAnswerState(true);
};

View file

@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import { BaseQuestion, parse } from 'gift-pegjs';
import { QuestionType } from 'src/Types/QuestionType';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
@ -15,21 +16,26 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
return { question: newMockQuestion as BaseQuestion };
});
const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn();
beforeEach(() => {
// Clear local storage before each test
// localStorage.clear();
render(
<MemoryRouter>
<StudentModeQuiz
questions={mockQuestions}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>);
</MemoryRouter>
);
});
describe('StudentModeQuiz', () => {
@ -51,6 +57,49 @@ describe('StudentModeQuiz', () => {
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
});
test('handles shows feedback for an already answered question', async () => {
// Answer the first question
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'});
expect(firstButtonA).toBeInTheDocument();
expect(firstButtonA.querySelector('.selected')).toBeInTheDocument();
expect(screen.getByRole("button", {name: '❌ B Option B'})).toBeInTheDocument();
expect(screen.queryByText('Répondre')).not.toBeInTheDocument();
// Navigate to the next question
act(() => {
fireEvent.click(screen.getByText('Question suivante'));
});
expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument();
// Navigate back to the first question
act(() => {
fireEvent.click(screen.getByText('Question précédente'));
});
expect(await screen.findByText('Sample Question 1')).toBeInTheDocument();
// Since answers are mocked, the it doesn't recognize the question as already answered
// TODO these tests are partially faked, need to be fixed if we can mock the answers
// const buttonA = screen.getByRole("button", {name: '✅ A Option A'});
const buttonA = screen.getByRole("button", {name: 'A Option A'});
expect(buttonA).toBeInTheDocument();
// const buttonB = screen.getByRole("button", {name: '❌ B Option B'});
const buttonB = screen.getByRole("button", {name: 'B Option B'});
expect(buttonB).toBeInTheDocument();
// // "Option A" div inside the name of button should have selected class
// expect(buttonA.querySelector('.selected')).toBeInTheDocument();
});
test('handles quit button click', async () => {
act(() => {
fireEvent.click(screen.getByText('Quitter'));
@ -65,16 +114,12 @@ describe('StudentModeQuiz', () => {
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
});
act(() => {
fireEvent.click(screen.getByText('Question suivante'));
});
const sampleQuestionElements = screen.queryAllByText(/Sample question 2/i);
expect(sampleQuestionElements.length).toBeGreaterThan(0);
expect(screen.getByText('V')).toBeInTheDocument();
expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument();
});
});

View file

@ -3,41 +3,52 @@ import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { MultipleChoiceQuestion, parse } from 'gift-pegjs';
import { BaseQuestion, MultipleChoiceQuestion, parse } from 'gift-pegjs';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom';
// import { mock } from 'node:test';
import { QuestionType } from 'src/Types/QuestionType';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
const mockGiftQuestions = parse(
`::Sample Question:: Sample Question {=Option A ~Option B}`);
describe('TeacherModeQuiz', () => {
it ('renders the initial question as MultipleChoiceQuestion', () => {
expect(mockGiftQuestions[0].type).toBe('MC');
});
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
const mockQuestion = mockGiftQuestions[0] as MultipleChoiceQuestion;
::Sample Question 2:: Sample Question 2 {=Option A ~Option B}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
describe('TeacherModeQuiz', () => {
let mockQuestion = mockQuestions[0].question as MultipleChoiceQuestion;
mockQuestion.id = '1';
const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn();
let rerender: (ui: React.ReactElement) => void;
beforeEach(async () => {
render(
const utils = render(
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket} />
</MemoryRouter>
);
rerender = utils.rerender;
});
test('renders the initial question', () => {
expect(screen.getByText('Question 1')).toBeInTheDocument();
expect(screen.getByText('Sample Question')).toBeInTheDocument();
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option A')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
expect(screen.getByText('Quitter')).toBeInTheDocument();
@ -53,9 +64,51 @@ describe('TeacherModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
expect(screen.getByText('Votre réponse est:')).toBeInTheDocument();
});
test('handles shows feedback for an already answered question', () => {
// Answer the first question
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
mockQuestion = mockQuestions[1].question as MultipleChoiceQuestion;
// Navigate to the next question by re-rendering with new props
act(() => {
rerender(
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>
);
});
mockQuestion = mockQuestions[0].question as MultipleChoiceQuestion;
act(() => {
rerender(
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>
);
});
// Check if the feedback dialog is shown again
expect(screen.getByText('Rétroaction')).toBeInTheDocument();
});
test('handles disconnect button click', () => {
act(() => {
fireEvent.click(screen.getByText('Quitter'));

View file

@ -1,7 +1,9 @@
//WebsocketService.test.tsx
import { BaseQuestion, parse } from 'gift-pegjs';
import WebsocketService from '../../services/WebsocketService';
import { io, Socket } from 'socket.io-client';
import { ENV_VARIABLES } from 'src/constants';
import { QuestionType } from 'src/Types/QuestionType';
jest.mock('socket.io-client');
@ -45,10 +47,16 @@ describe('WebSocketService', () => {
test('nextQuestion should emit next-question event with correct parameters', () => {
const roomName = 'testRoom';
const question = { id: 1, text: 'Sample Question' };
const mockGiftQuestions = parse('A {T}');
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
WebsocketService.nextQuestion(roomName, question);
WebsocketService.nextQuestion({roomName, questions: mockQuestions, questionIndex: 0, isLaunch: false});
const question = mockQuestions[0];
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
});

View file

@ -5,21 +5,28 @@ import { Button } from '@mui/material';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { MultipleChoiceQuestion } from 'gift-pegjs';
import { StudentType } from 'src/Types/StudentType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface Props {
question: MultipleChoiceQuestion;
handleOnSubmitAnswer?: (answer: string) => void;
handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean;
passedAnswer?: AnswerType;
students?: StudentType[];
isDisplayOnly?: boolean;
}
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, students, isDisplayOnly } = props;
const [answer, setAnswer] = useState<string>();
const { question, showAnswer, handleOnSubmitAnswer, students, isDisplayOnly, passedAnswer } = props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || '');
const [pickRates, setPickRates] = useState<number[]>([]);
const [showCorrectAnswers, setShowCorrectAnswers] = useState(false);
let disableButton = false;
if(handleOnSubmitAnswer === undefined){
disableButton = true;
}
const handleOnClickAnswer = (choice: string) => {
setAnswer(choice);
};
@ -47,19 +54,25 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
};
useEffect(() => {
setAnswer(undefined);
calculatePickRates();
}, [students, question, showCorrectAnswers]);
if (passedAnswer !== undefined) {
setAnswer(passedAnswer);
} else {
setAnswer('');
calculatePickRates();
}
}, [passedAnswer, students, question, showCorrectAnswers]);
const alpha = Array.from(Array(26)).map((_e, i) => i + 65);
const alphabet = alpha.map((x) => String.fromCharCode(x));
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
<div className="choices-wrapper mb-1">
{question.choices.map((choice, i) => {
const selected = answer === choice.formattedText.text ? 'selected' : '';
const rateStyle = showCorrectAnswers ? {
@ -71,6 +84,7 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
{/* <Button
variant="text"
className="button-wrapper"
disabled={disableButton}
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}>
{showAnswer? (<div> {(choice.isCorrect ? '✅' : '❌')}</div>)
:``}
@ -87,6 +101,7 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
<Button
variant="text"
className={`button-wrapper ${selected}`}
disabled={disableButton}
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
>
<div className={`circle ${selected}`}>{alphabet[i]}</div>
@ -121,9 +136,9 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
<Button
variant="contained"
onClick={() =>
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
answer !== "" && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
}
disabled={answer === undefined}
disabled={answer === '' || answer === null}
>
Répondre

View file

@ -6,21 +6,21 @@ import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemp
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
import { StudentType } from 'src/Types/StudentType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface Props {
question: NumericalQuestion;
handleOnSubmitAnswer?: (answer: number) => void;
handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean;
passedAnswer?: AnswerType;
students?: StudentType[];
isDisplayOnly?: boolean;
}
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, students, isDisplayOnly } =
const { question, showAnswer, handleOnSubmitAnswer, students, passedAnswer, isDisplayOnly } =
props;
const [answer, setAnswer] = useState<number>();
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || '');
const correctAnswers = question.choices;
let correctAnswer = '';
@ -32,10 +32,13 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
};
useEffect(() => {
if (passedAnswer !== null && passedAnswer !== undefined) {
setAnswer(passedAnswer);
}
if (showCorrectAnswers && students) {
calculateCorrectAnswerRate();
}
}, [showCorrectAnswers, students]);
}, [passedAnswer, showCorrectAnswers, students]);
const calculateCorrectAnswerRate = () => {
if (!students || students.length === 0) {
@ -75,10 +78,16 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-2">{correctAnswer}</div>
<div className="correct-answer-text mb-2">
<strong>La bonne réponse est: </strong>
{correctAnswer}</div>
<span>
<strong>Votre réponse est: </strong>{answer.toString()}
</span>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>}
</>
) : (
<>
@ -106,7 +115,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer)
}
disabled={answer === undefined || isNaN(answer)}
disabled={answer === "" || isNaN(answer as number)}
>
Répondre
</Button>

View file

@ -5,16 +5,19 @@ import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuesti
import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay';
import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
// import useCheckMobileScreen from '../../services/useCheckMobileScreen';
import { StudentType } from '../../Types/StudentType';
interface QuestionProps {
question: Question;
handleOnSubmitAnswer?: (answer: string | number | boolean) => void;
handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean;
students?: StudentType[];
isDisplayOnly?: boolean;
answer?: AnswerType;
}
const QuestionDisplay: React.FC<QuestionProps> = ({
question,
@ -22,6 +25,7 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
showAnswer,
students,
isDisplayOnly = false
answer,
}) => {
// const isMobile = useCheckMobileScreen();
// const imgWidth = useMemo(() => {
@ -38,10 +42,12 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
showAnswer={showAnswer}
students={students}
isDisplayOnly={isDisplayOnly}
passedAnswer={answer}
/>
);
break;
case 'MC':
questionTypeComponent = (
<MultipleChoiceQuestionDisplay
question={question}
@ -49,32 +55,22 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
showAnswer={showAnswer}
students={students}
isDisplayOnly={isDisplayOnly}
passedAnswer={answer}
/>
);
break;
case 'Numerical':
if (question.choices) {
if (!Array.isArray(question.choices)) {
questionTypeComponent = (
<NumericalQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
passedAnswer={answer}
students={students}
isDisplayOnly={isDisplayOnly}
/>
);
} else {
questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked)
<NumericalQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
students={students}
isDisplayOnly={isDisplayOnly}
/>
);
}
}
break;
case 'Short':
@ -85,6 +81,7 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
showAnswer={showAnswer}
students={students}
isDisplayOnly={isDisplayOnly}
passedAnswer={answer}
/>
);
break;

View file

@ -1,21 +1,23 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { ShortAnswerQuestion } from 'gift-pegjs';
import { StudentType } from 'src/Types/StudentType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface Props {
question: ShortAnswerQuestion;
handleOnSubmitAnswer?: (answer: string) => void;
handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean;
passedAnswer?: AnswerType;
students?: StudentType[];
isDisplayOnly?: boolean;
}
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, students, isDisplayOnly } = props;
const [answer, setAnswer] = useState<string>();
const { question, showAnswer, handleOnSubmitAnswer, students, passedAnswer, isDisplayOnly } = props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || '');
const [showCorrectAnswers, setShowCorrectAnswers] = useState(false);
const [correctAnswerRate, setCorrectAnswerRate] = useState<number>(0);
@ -24,10 +26,16 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
};
useEffect(() => {
if (passedAnswer !== undefined) {
setAnswer(passedAnswer);
}
if (showCorrectAnswers && students) {
calculateCorrectAnswerRate();
}
}, [showCorrectAnswers, students]);
}, [passedAnswer, showCorrectAnswers, students, answer]);
console.log("Answer", answer);
const calculateCorrectAnswerRate = () => {
if (!students || students.length === 0) {
@ -52,11 +60,18 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
{showAnswer ? (
<>
<div className="correct-answer-text mb-1">
<span>
<strong>La bonne réponse est: </strong>
{question.choices.map((choice) => (
<div key={choice.text} className="mb-1">
{choice.text}
</div>
))}
</span>
<span>
<strong>Votre réponse est: </strong>{answer}
</span>
</div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
@ -84,7 +99,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer)
}
disabled={answer === undefined || answer === ''}
disabled={answer === null || answer === ''}
>
Répondre
</Button>

View file

@ -1,29 +1,49 @@
// TrueFalseQuestion.tsx
import React, { useState, useEffect } from 'react';
import React, { useState,useEffect } from 'react';
import '../questionStyle.css';
import { Button } from '@mui/material';
import { TrueFalseQuestion } from 'gift-pegjs';
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
import { StudentType } from 'src/Types/StudentType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface Props {
question: TrueFalseQuestion;
handleOnSubmitAnswer?: (answer: boolean) => void;
handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean;
passedAnswer?: AnswerType;
students?: StudentType[];
isDisplayOnly?: boolean;
}
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, students, isDisplayOnly } = props;
const { question, showAnswer, handleOnSubmitAnswer, students, passedAnswer, isDisplayOnly } = props;
const [answer, setAnswer] = useState<boolean | undefined>(undefined);
const [pickRates, setPickRates] = useState<{ trueRate: number, falseRate: number }>({ trueRate: 0, falseRate: 0 });
const [showCorrectAnswers, setShowCorrectAnswers] = useState(false);
let disableButton = false;
if(handleOnSubmitAnswer === undefined){
disableButton = true;
}
const handleOnClickAnswer = (choice: boolean) => {
setAnswer(choice);
};
useEffect(() => {
setAnswer(undefined);
calculatePickRates();
}, [question, students]);
console.log("passedAnswer", passedAnswer);
if (passedAnswer === true || passedAnswer === false) {
setAnswer(passedAnswer);
} else {
setAnswer(undefined);
}
if (!passedAnswer && passedAnswer !== false) {
setAnswer(undefined);
calculatePickRates();
}
}, [passedAnswer, question, students]);
const selectedTrue = answer ? 'selected' : '';
const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
@ -68,28 +88,38 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
</div>
<div className="choices-wrapper mb-1">
<Button
className={`button-wrapper ${selectedTrue}`}
onClick={() => !showCorrectAnswers && setAnswer(true)}
className="button-wrapper"
onClick={() => !showAnswer && handleOnClickAnswer(true)}
fullWidth
disabled={disableButton}
>
<div className={`circle ${selectedTrue}`}>V</div>
<div
className={`answer-text ${selectedTrue}`}
<div className={`answer-text ${selectedTrue}`}
style={showCorrectAnswers ? {
backgroundImage: `linear-gradient(to right, ${question.isTrue ? 'lightgreen' : 'lightcoral'} ${pickRates.trueRate}%, transparent ${pickRates.trueRate}%)`
} : {}}
>
Vrai
</div>
{showCorrectAnswers && <div>{question.isTrue ? '✅' : '❌'}</div>}
{showCorrectAnswers && (
<div className="pick-rate">{pickRates.trueRate.toFixed(1)}%</div>
<>
<div>{question.isTrue ? '✅' : '❌'}</div>
<div className="pick-rate">{pickRates.trueRate.toFixed(1)}%</div>
</>
)}
{showAnswer && answer && question.trueFormattedFeedback && (
<div className="true-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
</div>
)}
</Button>
<Button
className={`button-wrapper ${selectedFalse}`}
onClick={() => !showCorrectAnswers && setAnswer(false)}
onClick={() => !showCorrectAnswers && handleOnClickAnswer(false)}
fullWidth
disabled={disableButton}
>
<div className={`circle ${selectedFalse}`}>F</div>
<div
@ -100,26 +130,23 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
>
Faux
</div>
{showCorrectAnswers && <div>{!question.isTrue ? '✅' : '❌'}</div>}
{showCorrectAnswers && (
<div className="pick-rate">{pickRates.falseRate.toFixed(1)}%</div>
<>
<div>{!question.isTrue ? '✅' : '❌'}</div>
<div className="pick-rate">{pickRates.falseRate.toFixed(1)}%</div>
</>
)}
{showAnswer && !answer && question.falseFormattedFeedback && (
<div className="false-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
</div>
)}
</Button>
</div>
{/* selected TRUE, show True feedback if it exists */}
{showAnswer && answer && question.trueFormattedFeedback && (
<div className="true-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
</div>
)}
{/* selected FALSE, show False feedback if it exists */}
{showAnswer && !answer && question.falseFormattedFeedback && (
<div className="false-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
</div>
)}
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
@ -130,6 +157,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
variant="contained"
onClick={() =>
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
}
disabled={answer === undefined}
>

View file

@ -147,6 +147,25 @@
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
}
.true-feedback {
position: relative;
padding: 0 1rem;
background-color: hsl(43, 100%, 94%);
color: hsl(43, 95%, 9%);
border: hsl(36, 84%, 93%) 1px solid;
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
}
.false-feedback {
position: relative;
padding: 0 1rem;
background-color: hsl(43, 100%, 94%);
color: hsl(43, 95%, 9%);
border: hsl(36, 84%, 93%) 1px solid;
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
}
.choices-wrapper {
width: 90%;
}

View file

@ -3,41 +3,47 @@ import React, { useEffect, useState } from 'react';
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
import { Button } from '@mui/material';
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
//import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Question } from 'gift-pegjs';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface StudentModeQuizProps {
questions: QuestionType[];
submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void;
answers: AnswerSubmissionToBackendType[];
submitAnswer: (_answer: AnswerType, _idQuestion: number) => void;
disconnectWebSocket: () => void;
}
const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
questions,
answers,
submitAnswer,
disconnectWebSocket
}) => {
//Ajouter type AnswerQuestionType en remplacement de QuestionType
const [questionInfos, setQuestion] = useState<QuestionType>(questions[0]);
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
// const [imageUrl, setImageUrl] = useState('');
// const [answer, setAnswer] = useState<AnswerType>('');
// const previousQuestion = () => {
// setQuestion(questions[Number(questionInfos.question?.id) - 2]);
// setIsAnswerSubmitted(false);
// };
const previousQuestion = () => {
setQuestion(questions[Number(questionInfos.question?.id) - 2]);
};
useEffect(() => {}, [questionInfos]);
useEffect(() => {
const savedAnswer = answers[Number(questionInfos.question.id)-1]?.answer;
console.log(`StudentModeQuiz: useEffect: savedAnswer: ${savedAnswer}`);
setIsAnswerSubmitted(savedAnswer !== undefined);
}, [questionInfos.question, answers]);
const nextQuestion = () => {
setQuestion(questions[Number(questionInfos.question?.id)]);
setIsAnswerSubmitted(false);
};
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const handleOnSubmitAnswer = (answer: AnswerType) => {
const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion);
setIsAnswerSubmitted(true);
@ -46,11 +52,13 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
return (
<div className='room'>
<div className='roomHeader'>
<DisconnectButton
onReturn={disconnectWebSocket}
message={`Êtes-vous sûr de vouloir quitter?`} />
</div>
<div >
<b>Question {questionInfos.question.id}/{questions.length}</b>
</div>
<div className="overflow-auto">
<div className="question-component-container">
@ -66,31 +74,30 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={isAnswerSubmitted}
answer={answers[Number(questionInfos.question.id)-1]?.answer}
/>
<div className="center-h-align mt-1/2">
<div className="w-12">
{/* <Button
variant="outlined"
onClick={previousQuestion}
fullWidth
startIcon={<ChevronLeft />}
disabled={Number(questionInfos.question.id) <= 1}
>
Question précédente
</Button> */}
</div>
<div className="w-12">
<Button style={{ display: isAnswerSubmitted ? 'block' : 'none' }}
variant="outlined"
onClick={nextQuestion}
fullWidth
//endIcon={<ChevronRight />}
disabled={Number(questionInfos.question.id) >= questions.length}
>
Question suivante
</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: '1rem' }}>
<div>
<Button
variant="outlined"
onClick={previousQuestion}
fullWidth
disabled={Number(questionInfos.question.id) <= 1}
>
Question précédente
</Button>
</div>
<div>
<Button
variant="outlined"
onClick={nextQuestion}
fullWidth
disabled={Number(questionInfos.question.id) >= questions.length}
>
Question suivante
</Button>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,55 +1,59 @@
// TeacherModeQuiz.tsx
import React, { useEffect, useState } from 'react';
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { Question } from 'gift-pegjs';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
// import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface TeacherModeQuizProps {
questionInfos: QuestionType;
submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void;
answers: AnswerSubmissionToBackendType[];
submitAnswer: (_answer: AnswerType, _idQuestion: number) => void;
disconnectWebSocket: () => void;
}
const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
questionInfos,
answers,
submitAnswer,
disconnectWebSocket
}) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState<React.ReactNode>('');
const renderFeedbackMessage = (answer: string) => {
const [answer, setAnswer] = useState<AnswerType>();
if(answer === 'true' || answer === 'false'){
return (<span>
<strong>Votre réponse est: </strong>{answer==="true" ? 'Vrai' : 'Faux'}
</span>)
}
else{
return (
<span>
<strong>Votre réponse est: </strong>{answer.toString()}
</span>
);}
};
// arrive here the first time after waiting for next question
useEffect(() => {
// Close the feedback dialog when the question changes
handleFeedbackDialogClose();
setIsAnswerSubmitted(false);
}, [questionInfos.question]);
console.log(`TeacherModeQuiz: useEffect: answers: ${JSON.stringify(answers)}`);
console.log(`TeacherModeQuiz: useEffect: questionInfos.question.id: ${questionInfos.question.id} answer: ${answer}`);
const oldAnswer = answers[Number(questionInfos.question.id) -1 ]?.answer;
console.log(`TeacherModeQuiz: useEffect: oldAnswer: ${oldAnswer}`);
setAnswer(oldAnswer);
setIsFeedbackDialogOpen(false);
}, [questionInfos.question, answers]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
// handle showing the feedback dialog
useEffect(() => {
console.log(`TeacherModeQuiz: useEffect: answer: ${answer}`);
setIsAnswerSubmitted(answer !== undefined);
setIsFeedbackDialogOpen(answer !== undefined);
}, [answer]);
useEffect(() => {
console.log(`TeacherModeQuiz: useEffect: isAnswerSubmitted: ${isAnswerSubmitted}`);
setIsFeedbackDialogOpen(isAnswerSubmitted);
}, [isAnswerSubmitted]);
const handleOnSubmitAnswer = (answer: AnswerType) => {
const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion);
setFeedbackMessage(renderFeedbackMessage(answer.toString()));
// setAnswer(answer);
setIsFeedbackDialogOpen(true);
};
@ -60,21 +64,21 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
return (
<div className='room'>
<div className='roomHeader'>
<div className='roomHeader'>
<DisconnectButton
onReturn={disconnectWebSocket}
message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Question {questionInfos.question.id}</div>
</div>
<div className='dumb'></div>
<DisconnectButton
onReturn={disconnectWebSocket}
message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Question {questionInfos.question.id}</div>
</div>
{isAnswerSubmitted ? (
<div className='dumb'></div>
</div>
{isAnswerSubmitted ? (
<div>
En attente pour la prochaine question...
</div>
@ -82,6 +86,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
answer={answer}
/>
)}
@ -92,20 +97,21 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
<DialogTitle>Rétroaction</DialogTitle>
<DialogContent>
<div style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
{feedbackMessage}
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px'}}
>Question : </div>
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px' }}
>Question : </div>
</div>
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={true}
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={true}
answer={answer}
/>
</DialogContent>
<DialogActions>
@ -114,7 +120,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
</Button>
</DialogActions>
</Dialog>
</div>
</div>
);
};

View file

@ -17,6 +17,8 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService'
export type AnswerType = string | number | boolean;
const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState('');
const [username, setUsername] = useState(ApiService.getUsername());
@ -25,6 +27,7 @@ const JoinRoom: React.FC = () => {
const [question, setQuestion] = useState<QuestionType>();
const [quizMode, setQuizMode] = useState<string>();
const [questions, setQuestions] = useState<QuestionType[]>([]);
const [answers, setAnswers] = useState<AnswerSubmissionToBackendType[]>([]);
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting, setIsConnecting] = useState<boolean>(false);
@ -35,6 +38,12 @@ const JoinRoom: React.FC = () => {
};
}, []);
useEffect(() => {
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
}, [questions]);
const handleCreateSocket = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
@ -45,16 +54,25 @@ const JoinRoom: React.FC = () => {
console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`);
});
socket.on('next-question', (question: QuestionType) => {
console.log('on(next-question): Received next-question:', question);
console.log('JoinRoom: on(next-question): Received next-question:', question);
setQuizMode('teacher');
setIsWaitingForTeacher(false);
setQuestion(question);
});
socket.on('launch-teacher-mode', (questions: QuestionType[]) => {
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
setQuizMode('teacher');
setIsWaitingForTeacher(true);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions);
// wait for next-question
});
socket.on('launch-student-mode', (questions: QuestionType[]) => {
console.log('on(launch-student-mode): Received launch-student-mode:', questions);
setQuizMode('student');
setIsWaitingForTeacher(false);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions);
setQuestion(questions[0]);
});
@ -83,6 +101,7 @@ const JoinRoom: React.FC = () => {
};
const disconnect = () => {
// localStorage.clear();
webSocketService.disconnect();
setSocket(null);
setQuestion(undefined);
@ -107,14 +126,22 @@ const JoinRoom: React.FC = () => {
}
};
const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => {
const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => {
console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`);
const answerData: AnswerSubmissionToBackendType = {
roomName: roomName,
answer: answer,
username: username,
idQuestion: idQuestion
};
// localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer));
setAnswers((prevAnswers) => {
console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`);
const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array
newAnswers[idQuestion - 1] = answerData; // Update the specific answer
return newAnswers; // Return the new array
});
console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`);
webSocketService.submitAnswer(answerData);
};
@ -152,6 +179,7 @@ const JoinRoom: React.FC = () => {
return (
<StudentModeQuiz
questions={questions}
answers={answers}
submitAnswer={handleOnSubmitAnswer}
disconnectWebSocket={disconnect}
/>
@ -161,6 +189,7 @@ const JoinRoom: React.FC = () => {
question && (
<TeacherModeQuiz
questionInfos={question}
answers={answers}
submitAnswer={handleOnSubmitAnswer}
disconnectWebSocket={disconnect}
/>

View file

@ -24,6 +24,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType';
import { Button } from '@mui/material';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
const ManageRoom: React.FC = () => {
const navigate = useNavigate();
@ -35,8 +36,40 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false);
const [quizStarted, setQuizStarted] = useState<boolean>(false);
const [formattedRoomName, setFormattedRoomName] = useState("");
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null);
// Handle the newly connected user in useEffect, because it needs state info
// not available in the socket.on() callback
useEffect(() => {
if (newlyConnectedUser) {
console.log(`Handling newly connected user: ${newlyConnectedUser.name}`);
setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]);
// only send nextQuestion if the quiz has started
if (!quizStarted) {
console.log(`!quizStarted: returning.... `);
return;
}
if (quizMode === 'teacher') {
webSocketService.nextQuestion({
roomName: formattedRoomName,
questions: quizQuestions,
questionIndex: Number(currentQuestion?.question.id) - 1,
isLaunch: true // started late
});
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
} else {
console.error('Invalid quiz mode:', quizMode);
}
// Reset the newly connected user state
setNewlyConnectedUser(null);
}
}, [newlyConnectedUser]);
useEffect(() => {
const verifyLogin = async () => {
@ -109,6 +142,17 @@ const ManageRoom: React.FC = () => {
const roomNameUpper = roomName.toUpperCase();
setFormattedRoomName(roomNameUpper);
console.log(`Creating WebSocket room named ${roomNameUpper}`);
/**
* ATTENTION: Lire les variables d'état dans
* les .on() n'est pas une bonne pratique.
* Les valeurs sont celles au moment de la création
* de la fonction et non au moment de l'exécution.
* Il faut utiliser des refs pour les valeurs qui
* changent fréquemment. Sinon, utiliser un trigger
* de useEffect pour mettre déclencher un traitement
* (voir user-joined plus bas).
*/
socket.on('connect', () => {
webSocketService.createRoom(roomNameUpper);
});
@ -123,19 +167,9 @@ const ManageRoom: React.FC = () => {
});
socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
setStudents((prevStudents) => [...prevStudents, student]);
// only send nextQuestion if the quiz has started
if (!quizStarted) return;
if (quizMode === 'teacher') {
webSocketService.nextQuestion(formattedRoomName, currentQuestion);
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
}
setNewlyConnectedUser(student);
});
socket.on('join-failure', (message) => {
setConnectingError(message);
setSocket(null);
@ -224,7 +258,10 @@ const ManageRoom: React.FC = () => {
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]);
webSocketService.nextQuestion({roomName: formattedRoomName,
questions: quizQuestions,
questionIndex: nextQuestionIndex,
isLaunch: false});
};
const previousQuestion = () => {
@ -234,7 +271,7 @@ const ManageRoom: React.FC = () => {
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false});
};
const initializeQuizQuestion = () => {
@ -262,7 +299,7 @@ const ManageRoom: React.FC = () => {
}
setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]);
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true});
};
const launchStudentMode = () => {
@ -278,21 +315,19 @@ const ManageRoom: React.FC = () => {
};
const launchQuiz = () => {
setQuizStarted(true);
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly
console.log(
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
);
setQuizStarted(true);
return;
}
console.log(`Launching quiz in ${quizMode} mode...`);
switch (quizMode) {
case 'student':
setQuizStarted(true);
return launchStudentMode();
case 'teacher':
setQuizStarted(true);
return launchTeacherMode();
}
};
@ -300,9 +335,8 @@ const ManageRoom: React.FC = () => {
const showSelectedQuestion = (questionIndex: number) => {
if (quiz?.content && quizQuestions) {
setCurrentQuestion(quizQuestions[questionIndex]);
if (quizMode === 'teacher') {
webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]);
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false});
}
}
};
@ -313,7 +347,7 @@ const ManageRoom: React.FC = () => {
};
function checkIfIsCorrect(
answer: string | number | boolean,
answer: AnswerType,
idQuestion: number,
questions: QuestionType[]
): boolean {

View file

@ -1,18 +1,20 @@
import { io, Socket } from 'socket.io-client';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
import { QuestionType } from 'src/Types/QuestionType';
// Must (manually) sync these types to server/socket/socket.js
export type AnswerSubmissionToBackendType = {
roomName: string;
username: string;
answer: string | number | boolean;
answer: AnswerType;
idQuestion: number;
};
export type AnswerReceptionFromBackendType = {
idUser: string;
username: string;
answer: string | number | boolean;
answer: AnswerType;
idQuestion: number;
};
@ -59,12 +61,19 @@ class WebSocketService {
// }
// }
nextQuestion(roomName: string, question: unknown) {
console.log('WebsocketService: nextQuestion', roomName, question);
if (!question) {
nextQuestion(args: {roomName: string, questions: QuestionType[] | undefined, questionIndex: number, isLaunch: boolean}) {
// deconstruct args
const { roomName, questions, questionIndex, isLaunch } = args;
console.log('WebsocketService: nextQuestion', roomName, questions, questionIndex, isLaunch);
if (!questions || !questions[questionIndex]) {
throw new Error('WebsocketService: nextQuestion: question is null');
}
if (this.socket) {
if (isLaunch) {
this.socket.emit('launch-teacher-mode', { roomName, questions });
}
const question = questions[questionIndex];
this.socket.emit('next-question', { roomName, question });
}
}

View file

@ -109,17 +109,29 @@ describe("websocket server", () => {
});
});
test("should send next question", (done) => {
studentSocket.on("next-question", (question) => {
expect(question).toEqual({ question: "question2" });
test("should launch teacher mode", (done) => {
studentSocket.on("launch-teacher-mode", (questions) => {
expect(questions).toEqual([
{ question: "question1" },
{ question: "question2" },
]);
done();
});
teacherSocket.emit("next-question", {
teacherSocket.emit("launch-teacher-mode", {
roomName: "ROOM1",
question: { question: "question2" },
questions: [{ question: "question1" }, { question: "question2" }],
});
});
test("should send next question", (done) => {
studentSocket.on("next-question", ( question ) => {
expect(question).toBe("question2");
done();
});
teacherSocket.emit("next-question", { roomName: "ROOM1", question: 'question2'},
);
});
test("should send answer", (done) => {
teacherSocket.on("submit-answer-room", (answer) => {
expect(answer).toEqual({

View file

@ -127,4 +127,11 @@ async function start() {
});
}
// Graceful shutdown on SIGINT (Ctrl+C)
process.on('SIGINT', async () => {
console.log('Shutting down...');
await db.closeConnection();
process.exit(0);
});
start();

View file

@ -73,13 +73,15 @@ class AuthManager{
console.info(`L'utilisateur '${userInfo.email}' vient de se connecter`)
}
async register(userInfos){
async register(userInfos, sendEmail=false){
console.log(userInfos);
if (!userInfos.email || !userInfos.password) {
throw new AppError(MISSING_REQUIRED_PARAMETER);
}
const user = await this.simpleregister.register(userInfos);
emailer.registerConfirmation(user.email)
if(sendEmail){
emailer.registerConfirmation(user.email);
}
return user
}
}

View file

@ -34,7 +34,7 @@ class SimpleAuth {
password: req.body.password,
roles: req.body.roles
};
let user = await self.authmanager.register(userInfos)
let user = await self.authmanager.register(userInfos, true);
if (user) {
return res.status(200).json({
message: 'User created'

View file

@ -1,28 +1,53 @@
const { MongoClient } = require('mongodb');
const dotenv = require('dotenv')
const dotenv = require('dotenv');
dotenv.config();
class DBConnection {
constructor() {
this.mongoURI = process.env.MONGO_URI;
this.databaseName = process.env.MONGO_DATABASE;
this.client = null;
this.connection = null;
}
// Connect to the database, but don't reconnect if already connected
async connect() {
const client = new MongoClient(this.mongoURI);
this.connection = await client.connect();
if (this.connection) {
console.log('Using existing MongoDB connection');
return this.connection;
}
try {
// Create the MongoClient only if the connection does not exist
this.client = new MongoClient(this.mongoURI);
await this.client.connect();
this.connection = this.client.db(this.databaseName);
console.log('MongoDB connected');
return this.connection;
} catch (error) {
console.error('MongoDB connection error:', error);
throw new Error('Failed to connect to MongoDB');
}
}
// Return the current database connection
getConnection() {
if (!this.connection) {
throw new Error('Connexion MongoDB non établie');
throw new Error('MongoDB connection not established');
}
return this.connection;
}
// Close the MongoDB connection gracefully
async closeConnection() {
if (this.client) {
await this.client.close();
console.log('MongoDB connection closed');
}
return this.connection.db(this.databaseName);
}
}
// Exporting the singleton instance
const instance = new DBConnection();
module.exports = instance;
module.exports = instance;

View file

@ -2498,6 +2498,7 @@
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},

View file

@ -81,6 +81,10 @@ const setupWebsocket = (io) => {
socket.to(roomName).emit("next-question", question);
});
socket.on("launch-teacher-mode", ({ roomName, questions }) => {
socket.to(roomName).emit("launch-teacher-mode", questions);
});
socket.on("launch-student-mode", ({ roomName, questions }) => {
socket.to(roomName).emit("launch-student-mode", questions);
});