mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge 7b69071672 into 069b62957f
This commit is contained in:
commit
be1891ad05
21 changed files with 945 additions and 271 deletions
176
client/package-lock.json
generated
176
client/package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
||||||
"@mui/material": "^7.0.2",
|
"@mui/material": "^7.0.2",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"bootstrap": "^5.3.4",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.5",
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
"gift-pegjs": "^2.0.0-beta.1",
|
"gift-pegjs": "^2.0.0-beta.1",
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-bootstrap": "^2.10.9",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
|
@ -3812,6 +3814,20 @@
|
||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-aria/ssr": {
|
||||||
|
"version": "3.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
|
||||||
|
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.0",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||||
|
|
@ -3821,6 +3837,56 @@
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@restart/hooks": {
|
||||||
|
"version": "0.4.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||||
|
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@restart/ui": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@react-aria/ssr": "^3.5.0",
|
||||||
|
"@restart/hooks": "^0.5.0",
|
||||||
|
"@types/warning": "^3.0.3",
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"dom-helpers": "^5.2.0",
|
||||||
|
"uncontrollable": "^8.0.4",
|
||||||
|
"warning": "^4.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.14.0",
|
||||||
|
"react-dom": ">=16.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@restart/ui/node_modules/@restart/hooks": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@restart/ui/node_modules/uncontrollable": {
|
||||||
|
"version": "8.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
|
||||||
|
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.34.8",
|
"version": "4.34.8",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
|
||||||
|
|
@ -4321,6 +4387,14 @@
|
||||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
|
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/types": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.21",
|
"version": "0.1.21",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz",
|
||||||
|
|
@ -4757,6 +4831,11 @@
|
||||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/warning": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q=="
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.33",
|
"version": "17.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||||
|
|
@ -5534,6 +5613,24 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.4.tgz",
|
||||||
|
"integrity": "sha512-q2oK3ZPDTa5I44FTyY3H76+SDTJREvOBxtX1HNLHcxMni50jMvUtOh+dgFdgpsAHtJ9bfNAWr6d6VezJHJ/7tg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-arraybuffer": {
|
"node_modules/base64-arraybuffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
|
@ -5827,6 +5924,11 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
|
@ -8022,6 +8124,14 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/invariant": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
|
|
@ -11203,6 +11313,23 @@
|
||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types-extra": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.3.2",
|
||||||
|
"warning": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=0.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types-extra/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
|
},
|
||||||
"node_modules/prop-types/node_modules/react-is": {
|
"node_modules/prop-types/node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|
@ -11309,6 +11436,36 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-bootstrap": {
|
||||||
|
"version": "2.10.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.9.tgz",
|
||||||
|
"integrity": "sha512-TJUCuHcxdgYpOqeWmRApM/Dy0+hVsxNRFvq2aRFQuxhNi/+ivOxC5OdWIeHS3agxvzJ4Ev4nDw2ZdBl9ymd/JQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.24.7",
|
||||||
|
"@restart/hooks": "^0.4.9",
|
||||||
|
"@restart/ui": "^1.9.4",
|
||||||
|
"@types/prop-types": "^15.7.12",
|
||||||
|
"@types/react-transition-group": "^4.4.6",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"dom-helpers": "^5.2.1",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"prop-types-extra": "^1.1.0",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
|
"uncontrollable": "^7.2.1",
|
||||||
|
"warning": "^4.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.14.8",
|
||||||
|
"react": ">=16.14.0",
|
||||||
|
"react-dom": ">=16.14.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|
@ -12620,6 +12777,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|
@ -12787,6 +12949,20 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uncontrollable": {
|
||||||
|
"version": "7.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||||
|
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.6.3",
|
||||||
|
"@types/react": ">=16.9.11",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"react-lifecycles-compat": "^3.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"@mui/material": "^7.0.2",
|
"@mui/material": "^7.0.2",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
|
"bootstrap": "^5.3.4",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.5",
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
"gift-pegjs": "^2.0.0-beta.1",
|
"gift-pegjs": "^2.0.0-beta.1",
|
||||||
|
|
@ -34,6 +35,7 @@
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-bootstrap": "^2.10.9",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ export interface StudentType {
|
||||||
id: string;
|
id: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
answers: Answer[];
|
answers: Answer[];
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,5 +209,33 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
||||||
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calculates and displays pick rates correctly when showResults is true', () => {
|
||||||
|
const question = parse(`::MCQ:: What is 2+2? {
|
||||||
|
=Four
|
||||||
|
~Three
|
||||||
|
~Five
|
||||||
|
}`)[0] as MultipleChoiceQuestion;
|
||||||
|
|
||||||
|
const mockStudents = [
|
||||||
|
{ id: '1', name: 'Alice', answers: [{ idQuestion: 1, answer: ['Four'], isCorrect: true }] },
|
||||||
|
{ id: '2', name: 'Bob', answers: [{ idQuestion: 1, answer: ['Three'], isCorrect: false }] },
|
||||||
|
{ id: '3', name: 'Charlie', answers: [{ idQuestion: 1, answer: ['Four'], isCorrect: true }] }
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MultipleChoiceQuestionDisplay
|
||||||
|
question={{ ...question, id: '1' }}
|
||||||
|
students={mockStudents}
|
||||||
|
showResults={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expect pick rate for "Four" to be 2/3
|
||||||
|
expect(screen.getByText('✅2/3 (66.7%)')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expect pick rate for "Three" to be 1/3
|
||||||
|
expect(screen.getByText('❌1/3 (33.3%)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,4 +81,37 @@ describe('NumericalQuestion Component', () => {
|
||||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]);
|
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]);
|
||||||
mockHandleOnSubmitAnswer.mockClear();
|
mockHandleOnSubmitAnswer.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calculates and displays correct answer rate when showResults is true', () => {
|
||||||
|
const mockStudents = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Alice',
|
||||||
|
answers: [{ idQuestion: 1, answer: [7], isCorrect: true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Bob',
|
||||||
|
answers: [{ idQuestion: 1, answer: [3], isCorrect: false }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Charlie',
|
||||||
|
answers: [{ idQuestion: 1, answer: [6], isCorrect: true }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<NumericalQuestionDisplay
|
||||||
|
question={{ ...question, id: '1' }}
|
||||||
|
showResults={true}
|
||||||
|
students={mockStudents}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Taux de réponse correcte: 2/3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('66.7%')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ describe('Questions Component', () => {
|
||||||
showAnswer: false
|
showAnswer: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = (question: Question) => {
|
const renderComponent = (question: Question, showAnswerToggle = false) => {
|
||||||
render(<QuestionDisplay question={question} {...sampleProps} />);
|
render(<QuestionDisplay question={question} showAnswerToggle={showAnswerToggle} {...sampleProps} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
// describe('question type parsing', () => {
|
// describe('question type parsing', () => {
|
||||||
|
|
@ -122,6 +122,11 @@ describe('Questions Component', () => {
|
||||||
|
|
||||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows "Afficher les résultats" toggle when showAnswerToggle is true', () => {
|
||||||
|
renderComponent(sampleTrueFalseQuestion, true);
|
||||||
|
expect(screen.getByText('Afficher les résultats')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { render, screen, fireEvent, within } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
|
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
|
||||||
import ShortAnswerQuestionDisplay from 'src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay';
|
import ShortAnswerQuestionDisplay from 'src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
describe('ShortAnswerQuestion Component', () => {
|
describe('ShortAnswerQuestion Component', () => {
|
||||||
const mockHandleSubmitAnswer = jest.fn();
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
@ -64,4 +65,54 @@ describe('ShortAnswerQuestion Component', () => {
|
||||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||||
mockHandleSubmitAnswer.mockClear();
|
mockHandleSubmitAnswer.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('calculates and displays correct answer rate when showResults is true', () => {
|
||||||
|
const mockStudents = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Alice',
|
||||||
|
answers: [{ idQuestion: 1, answer: ['Paris'], isCorrect: true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Bob',
|
||||||
|
answers: [{ idQuestion: 1, answer: ['Lyon'], isCorrect: false }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Charlie',
|
||||||
|
answers: [{ idQuestion: 1, answer: ['Paris'], isCorrect: true }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
const question: ShortAnswerQuestion = {
|
||||||
|
id: '1',
|
||||||
|
type: 'Short',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
formattedStem: {
|
||||||
|
text: 'What is the capital of France?',
|
||||||
|
format: 'html'
|
||||||
|
},
|
||||||
|
choices: [{ text: 'Paris', isCorrect: true }],
|
||||||
|
formattedGlobalFeedback: {
|
||||||
|
text: '',
|
||||||
|
format: 'html'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ShortAnswerQuestionDisplay
|
||||||
|
question={question}
|
||||||
|
showResults={true}
|
||||||
|
students={mockStudents}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Taux de réponse correcte: 2/3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('66.7%')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -134,4 +134,43 @@ describe('TrueFalseQuestion Component', () => {
|
||||||
expect(wrongAnswer1).toBeInTheDocument();
|
expect(wrongAnswer1).toBeInTheDocument();
|
||||||
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calculates and displays pick rates correctly when showResults is true', () => {
|
||||||
|
const mockStudents = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Alice',
|
||||||
|
answers: [{ idQuestion: 1, answer: [true], isCorrect: true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Bob',
|
||||||
|
answers: [{ idQuestion: 1, answer: [false], isCorrect: false }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TrueFalseQuestionDisplay
|
||||||
|
question={{ ...trueFalseQuestion, id: '1' }}
|
||||||
|
students={mockStudents}
|
||||||
|
showResults={true}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
const pickRateDivs = screen.getAllByText((_, element) =>
|
||||||
|
element !== null &&
|
||||||
|
(element as HTMLElement).classList.contains('pick-rate') &&
|
||||||
|
(element as HTMLElement).textContent!.includes('1/2')
|
||||||
|
);
|
||||||
|
expect(pickRateDivs.length).toBe(2);
|
||||||
|
|
||||||
|
const percentDivs = screen.getAllByText((_, element) =>
|
||||||
|
element !== null &&
|
||||||
|
(element as HTMLElement).classList.contains('pick-rate') &&
|
||||||
|
(element as HTMLElement).textContent!.includes('50.0%')
|
||||||
|
);
|
||||||
|
expect(percentDivs.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// LiveResults.tsx
|
// LiveResults.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { QuestionType } from '../../Types/QuestionType';
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
import './liveResult.css';
|
import './liveResult.css';
|
||||||
|
|
@ -26,11 +27,9 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
|
||||||
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
|
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="action-bar mb-1">
|
<div className="action-bar mb-1">
|
||||||
<div className="text-2xl text-bold">Résultats du quiz</div>
|
<div className="text-2xl text-bold">Résultats du quiz</div>
|
||||||
<FormGroup row>
|
<FormGroup row>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={<div className="text-sm">Afficher les noms</div>}
|
label={<div className="text-sm">Afficher les noms</div>}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,12 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
||||||
return (
|
return (
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{students.map((student) => (
|
{students.map((student) => (
|
||||||
<TableRow key={student.id}>
|
<TableRow
|
||||||
|
key={student.id}
|
||||||
|
style={{
|
||||||
|
opacity: student.isActive === false ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="sticky-column"
|
className="sticky-column"
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { TableCell, TableHead, TableRow } from "@mui/material";
|
import { TableCell, TableHead, TableRow } from "@mui/material";
|
||||||
|
|
||||||
interface LiveResultsHeaderProps {
|
interface LiveResultsHeaderProps {
|
||||||
|
|
@ -10,6 +10,12 @@ const LiveResultsTableHeader: React.FC<LiveResultsHeaderProps> = ({
|
||||||
maxQuestions,
|
maxQuestions,
|
||||||
showSelectedQuestion,
|
showSelectedQuestion,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [selectedQuestionIndex, setSelectedQuestionIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleQuestionClick = (index: number) => {
|
||||||
|
setSelectedQuestionIndex(index);
|
||||||
|
showSelectedQuestion(index);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
|
@ -25,9 +31,10 @@ const LiveResultsTableHeader: React.FC<LiveResultsHeaderProps> = ({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(224, 224, 224, 1)'
|
borderColor: 'rgba(224, 224, 224, 1)',
|
||||||
|
backgroundColor: selectedQuestionIndex === index ? '#dedede' : 'transparent'
|
||||||
}}
|
}}
|
||||||
onClick={() => showSelectedQuestion(index)}
|
onClick={() => handleQuestionClick(index)}
|
||||||
>
|
>
|
||||||
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
|
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
// MultipleChoiceQuestionDisplay.tsx
|
// MultipleChoiceQuestionDisplay.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import '../questionStyle.css';
|
import '../questionStyle.css';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||||
import { MultipleChoiceQuestion } from 'gift-pegjs';
|
import { MultipleChoiceQuestion } from 'gift-pegjs';
|
||||||
|
import { StudentType } from 'src/Types/StudentType';
|
||||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -11,33 +12,30 @@ interface Props {
|
||||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||||
showAnswer?: boolean;
|
showAnswer?: boolean;
|
||||||
passedAnswer?: AnswerType;
|
passedAnswer?: AnswerType;
|
||||||
|
students?: StudentType[];
|
||||||
|
isDisplayOnly?: boolean;
|
||||||
|
showResults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
|
const { question, showAnswer, handleOnSubmitAnswer, students, showResults, passedAnswer } = props;
|
||||||
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
|
||||||
|
|
||||||
const [answer, setAnswer] = useState<AnswerType>(() => {
|
const [answer, setAnswer] = useState<AnswerType>(() => {
|
||||||
if (passedAnswer && passedAnswer.length > 0) {
|
if (passedAnswer && passedAnswer.length > 0) {
|
||||||
return passedAnswer;
|
return passedAnswer;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
const [pickRates, setPickRates] = useState<{ percentages: number[], counts: number[], totalCount: number }>({
|
||||||
|
percentages: [],
|
||||||
|
counts: [],
|
||||||
|
totalCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
let disableButton = false;
|
let disableButton = false;
|
||||||
if (handleOnSubmitAnswer === undefined) {
|
if (handleOnSubmitAnswer === undefined) {
|
||||||
disableButton = true;
|
disableButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
|
||||||
if (passedAnswer !== undefined) {
|
|
||||||
setAnswer(passedAnswer);
|
|
||||||
} else {
|
|
||||||
setAnswer([]);
|
|
||||||
}
|
|
||||||
}, [passedAnswer, question.id]);
|
|
||||||
|
|
||||||
const handleOnClickAnswer = (choice: string) => {
|
const handleOnClickAnswer = (choice: string) => {
|
||||||
setAnswer((prevAnswer) => {
|
setAnswer((prevAnswer) => {
|
||||||
console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`);
|
console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`);
|
||||||
|
|
@ -58,74 +56,119 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculatePickRates = () => {
|
||||||
|
if (!students || students.length === 0) {
|
||||||
|
setPickRates({ percentages: new Array(question.choices.length).fill(0), counts: new Array(question.choices.length).fill(0), totalCount: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rates: number[] = [];
|
||||||
|
const counts: number[] = [];
|
||||||
|
let totalResponses = 0;
|
||||||
|
|
||||||
|
question.choices.forEach(choice => {
|
||||||
|
const choiceCount = students.filter(student =>
|
||||||
|
student.answers.some(ans =>
|
||||||
|
ans.idQuestion === Number(question.id) && ans.answer.includes(choice.formattedText.text)
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
totalResponses += choiceCount;
|
||||||
|
rates.push((choiceCount / students.length) * 100);
|
||||||
|
counts.push(choiceCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
setPickRates({ percentages: rates, counts: counts, totalCount: totalResponses });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (passedAnswer !== undefined) {
|
||||||
|
setAnswer(passedAnswer);
|
||||||
|
} else {
|
||||||
|
setAnswer([]);
|
||||||
|
calculatePickRates();
|
||||||
|
}
|
||||||
|
}, [passedAnswer, students, question.id]);
|
||||||
|
|
||||||
|
|
||||||
const alpha = Array.from(Array(26)).map((_e, i) => i + 65);
|
const alpha = Array.from(Array(26)).map((_e, i) => i + 65);
|
||||||
const alphabet = alpha.map((x) => String.fromCharCode(x));
|
const alphabet = alpha.map((x) => String.fromCharCode(x));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="question-container">
|
<div className="container">
|
||||||
<div className="question content">
|
<div className="row justify-content-center">
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
<div className="col-auto question-container">
|
||||||
</div>
|
<div className="question content">
|
||||||
<div className="choices-wrapper mb-1">
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||||
{question.choices.map((choice, i) => {
|
</div>
|
||||||
console.log(`answer: ${answer}, choice: ${choice.formattedText.text}`);
|
<div className="choices-wrapper mb-1">
|
||||||
const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
|
{question.choices.map((choice, i) => {
|
||||||
return (
|
const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
|
||||||
<div key={choice.formattedText.text + i} className="choice-container">
|
const rateStyle = showResults ? {
|
||||||
<Button
|
backgroundImage: `linear-gradient(to right, ${choice.isCorrect ? 'lightgreen' : 'lightcoral'} ${pickRates.percentages[i]}%, transparent ${pickRates.percentages[i]}%)`,
|
||||||
variant="text"
|
color: 'black'
|
||||||
className="button-wrapper"
|
} : {};
|
||||||
disabled={disableButton}
|
return (
|
||||||
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
|
<div key={choice.formattedText.text + i} className="choice-container">
|
||||||
>
|
<Button
|
||||||
{showAnswer ? (
|
variant="text"
|
||||||
<div>{choice.isCorrect ? '✅' : '❌'}</div>
|
className={`button-wrapper ${selected}`}
|
||||||
) : (
|
disabled={disableButton}
|
||||||
''
|
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
|
||||||
)}
|
>
|
||||||
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
{showAnswer ? (
|
||||||
<div className={`answer-text ${selected}`}>
|
<div>{choice.isCorrect ? '✅' : '❌'}</div>
|
||||||
<div
|
) : (
|
||||||
dangerouslySetInnerHTML={{
|
''
|
||||||
__html: FormattedTextTemplate(choice.formattedText),
|
)}
|
||||||
}}
|
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
||||||
/>
|
<div className={`answer-text ${selected}`}
|
||||||
|
style={rateStyle}>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} />
|
||||||
|
</div>
|
||||||
|
{choice.formattedFeedback && showAnswer && (
|
||||||
|
<div className="feedback-container mb-1 mt-1/2">
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: FormattedTextTemplate(choice.formattedFeedback),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showResults && pickRates.percentages.length > i && (
|
||||||
|
<div className="pick-rate">
|
||||||
|
{choice.isCorrect ? '✅' : '❌'}
|
||||||
|
{`${pickRates.counts[i]}/${pickRates.totalCount} (${pickRates.percentages[i].toFixed(1)}%)`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{choice.formattedFeedback && showAnswer && (
|
);
|
||||||
<div className="feedback-container mb-1 mt-1/2">
|
})}
|
||||||
<div
|
</div>
|
||||||
dangerouslySetInnerHTML={{
|
{question.formattedGlobalFeedback && showAnswer && (
|
||||||
__html: FormattedTextTemplate(choice.formattedFeedback),
|
<div className="global-feedback mb-2">
|
||||||
}}
|
<div
|
||||||
/>
|
dangerouslySetInnerHTML={{
|
||||||
</div>
|
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
|
||||||
)}
|
}}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
</div>
|
{!showAnswer && handleOnSubmitAnswer && (
|
||||||
{question.formattedGlobalFeedback && showAnswer && (
|
<Button
|
||||||
<div className="global-feedback mb-2">
|
variant="contained"
|
||||||
<div
|
onClick={() =>
|
||||||
dangerouslySetInnerHTML={{
|
answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||||
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
|
}
|
||||||
}}
|
disabled={answer.length === 0}
|
||||||
/>
|
>
|
||||||
|
Répondre
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{!showAnswer && handleOnSubmitAnswer && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() =>
|
|
||||||
answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
|
||||||
}
|
|
||||||
disabled={answer.length === 0}
|
|
||||||
>
|
|
||||||
Répondre
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,64 @@
|
||||||
// NumericalQuestion.tsx
|
// NumericalQuestion.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import '../questionStyle.css';
|
import '../questionStyle.css';
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||||
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
|
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
|
||||||
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
|
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
|
||||||
|
import { StudentType } from 'src/Types/StudentType';
|
||||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
question: NumericalQuestion;
|
question: NumericalQuestion;
|
||||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||||
showAnswer?: boolean;
|
showAnswer?: boolean;
|
||||||
passedAnswer?: AnswerType;
|
passedAnswer?: AnswerType;
|
||||||
|
students?: StudentType[];
|
||||||
|
showResults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
const { question, showAnswer, handleOnSubmitAnswer, students, showResults, passedAnswer } =
|
||||||
props;
|
props;
|
||||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||||
const correctAnswers = question.choices;
|
const correctAnswers = question.choices;
|
||||||
let correctAnswer = '';
|
let correctAnswer = '';
|
||||||
|
const [correctAnswerRate, setCorrectAnswerRate] = useState<number>(0);
|
||||||
|
const [submissionCounts, setSubmissionCounts] = useState({
|
||||||
|
correctSubmissions: 0,
|
||||||
|
totalSubmissions: 0
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (passedAnswer !== null && passedAnswer !== undefined) {
|
if (passedAnswer !== null && passedAnswer !== undefined) {
|
||||||
setAnswer(passedAnswer);
|
setAnswer(passedAnswer);
|
||||||
}
|
}
|
||||||
}, [passedAnswer]);
|
if (showResults && students) {
|
||||||
|
calculateCorrectAnswerRate();
|
||||||
|
}
|
||||||
|
}, [passedAnswer, showResults, students]);
|
||||||
|
|
||||||
|
const calculateCorrectAnswerRate = () => {
|
||||||
|
if (!students || students.length === 0) {
|
||||||
|
setSubmissionCounts({ correctSubmissions: 0, totalSubmissions: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSubmissions = students.length;
|
||||||
|
const correctSubmissions = students.filter(student =>
|
||||||
|
student.answers.some(ans =>
|
||||||
|
ans.idQuestion === Number(question.id) && ans.isCorrect
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
setSubmissionCounts({
|
||||||
|
correctSubmissions,
|
||||||
|
totalSubmissions
|
||||||
|
});
|
||||||
|
|
||||||
|
setCorrectAnswerRate((correctSubmissions / totalSubmissions) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
//const isSingleAnswer = correctAnswers.length === 1;
|
//const isSingleAnswer = correctAnswers.length === 1;
|
||||||
|
|
||||||
if (isSimpleNumericalAnswer(correctAnswers[0])) {
|
if (isSimpleNumericalAnswer(correctAnswers[0])) {
|
||||||
|
|
@ -44,57 +76,78 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="question-wrapper">
|
<>
|
||||||
<div>
|
<div className="container question-wrapper">
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
<div className="row justify-content-center">
|
||||||
</div>
|
<div className="col-auto">
|
||||||
{showAnswer ? (
|
<div>
|
||||||
<>
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||||
<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>}
|
|
||||||
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="answer-wrapper mb-1">
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
id={question.formattedStem.text}
|
|
||||||
name={question.formattedStem.text}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setAnswer([e.target.valueAsNumber]);
|
|
||||||
}}
|
|
||||||
inputProps={{ 'data-testid': 'number-input' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{question.formattedGlobalFeedback && showAnswer && (
|
|
||||||
<div className="global-feedback mb-2">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{showAnswer ? (
|
||||||
{handleOnSubmitAnswer && (
|
<>
|
||||||
<Button
|
<div className="correct-answer-text mb-2">
|
||||||
variant="contained"
|
<strong>La bonne réponse est: </strong>
|
||||||
onClick={() =>
|
{correctAnswer}</div>
|
||||||
answer !== undefined &&
|
<span>
|
||||||
handleOnSubmitAnswer &&
|
<strong>Votre réponse est: </strong>{answer.toString()}
|
||||||
handleOnSubmitAnswer(answer)
|
</span>
|
||||||
}
|
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
|
||||||
disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||||
>
|
</div>}
|
||||||
Répondre
|
|
||||||
</Button>
|
</>
|
||||||
)}
|
) : (
|
||||||
</>
|
<>
|
||||||
)}
|
<div className="answer-wrapper mb-1">
|
||||||
</div>
|
<TextField
|
||||||
|
type="number"
|
||||||
|
id={question.formattedStem.text}
|
||||||
|
name={question.formattedStem.text}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setAnswer([e.target.valueAsNumber]);
|
||||||
|
}}
|
||||||
|
inputProps={{ 'data-testid': 'number-input' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{question.formattedGlobalFeedback && showAnswer && (
|
||||||
|
<div className="global-feedback mb-2">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{handleOnSubmitAnswer && (
|
||||||
|
<div className="col-auto d-flex flex-column align-items-center">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
answer !== undefined &&
|
||||||
|
handleOnSubmitAnswer &&
|
||||||
|
handleOnSubmitAnswer(answer)
|
||||||
|
}
|
||||||
|
disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
|
||||||
|
>
|
||||||
|
Répondre
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showResults && (
|
||||||
|
<div className="col-auto">
|
||||||
|
<div>
|
||||||
|
Taux de réponse correcte: {submissionCounts.correctSubmissions}/{submissionCounts.totalSubmissions}
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar-container">
|
||||||
|
<div className="progress-bar-fill" style={{ width: `${correctAnswerRate}%` }}></div>
|
||||||
|
<div className="progress-bar-text">
|
||||||
|
{correctAnswerRate.toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Question } from 'gift-pegjs';
|
import { Question } from 'gift-pegjs';
|
||||||
|
|
||||||
|
import { FormControlLabel, Switch } from '@mui/material';
|
||||||
|
|
||||||
import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuestionDisplay';
|
import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuestionDisplay';
|
||||||
import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
|
import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
|
||||||
import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay';
|
import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay';
|
||||||
|
|
@ -8,10 +10,15 @@ import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswer
|
||||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||||
// import useCheckMobileScreen from '../../services/useCheckMobileScreen';
|
// import useCheckMobileScreen from '../../services/useCheckMobileScreen';
|
||||||
|
|
||||||
|
import { StudentType } from '../../Types/StudentType';
|
||||||
|
|
||||||
interface QuestionProps {
|
interface QuestionProps {
|
||||||
question: Question;
|
question: Question;
|
||||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||||
showAnswer?: boolean;
|
showAnswer?: boolean;
|
||||||
|
students?: StudentType[];
|
||||||
|
showResults?: boolean;
|
||||||
|
showAnswerToggle?: boolean;
|
||||||
answer?: AnswerType;
|
answer?: AnswerType;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -19,12 +26,16 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||||
question,
|
question,
|
||||||
handleOnSubmitAnswer,
|
handleOnSubmitAnswer,
|
||||||
showAnswer,
|
showAnswer,
|
||||||
|
showAnswerToggle = false,
|
||||||
|
students,
|
||||||
answer,
|
answer,
|
||||||
}) => {
|
}) => {
|
||||||
// const isMobile = useCheckMobileScreen();
|
// const isMobile = useCheckMobileScreen();
|
||||||
// const imgWidth = useMemo(() => {
|
// const imgWidth = useMemo(() => {
|
||||||
// return isMobile ? '100%' : '20%';
|
// return isMobile ? '100%' : '20%';
|
||||||
// }, [isMobile]);
|
// }, [isMobile]);
|
||||||
|
|
||||||
|
const [showResults, setShowResults] = useState<boolean>(false);
|
||||||
|
|
||||||
let questionTypeComponent = null;
|
let questionTypeComponent = null;
|
||||||
switch (question?.type) {
|
switch (question?.type) {
|
||||||
|
|
@ -34,6 +45,8 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||||
question={question}
|
question={question}
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
|
students={students}
|
||||||
|
showResults={showResults}
|
||||||
passedAnswer={answer}
|
passedAnswer={answer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -45,6 +58,8 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||||
question={question}
|
question={question}
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
|
students={students}
|
||||||
|
showResults={showResults}
|
||||||
passedAnswer={answer}
|
passedAnswer={answer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -57,7 +72,8 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
passedAnswer={answer}
|
passedAnswer={answer}
|
||||||
|
students={students}
|
||||||
|
showResults={showResults}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -68,21 +84,39 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||||
question={question}
|
question={question}
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
|
students={students}
|
||||||
|
showResults={showResults}
|
||||||
passedAnswer={answer}
|
passedAnswer={answer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="question-container">
|
<>
|
||||||
{questionTypeComponent ? (
|
{showAnswerToggle && (
|
||||||
<>
|
<FormControlLabel
|
||||||
{questionTypeComponent}
|
label={<div className="text-sm">Afficher les résultats</div>}
|
||||||
</>
|
control={
|
||||||
) : (
|
<Switch
|
||||||
<div>Question de type inconnue</div>
|
value={showResults}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setShowResults(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
<div className="question-container">
|
||||||
|
{questionTypeComponent ? (
|
||||||
|
<>
|
||||||
|
{questionTypeComponent}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>Question de type inconnue</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import '../questionStyle.css';
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||||
import { ShortAnswerQuestion } from 'gift-pegjs';
|
import { ShortAnswerQuestion } from 'gift-pegjs';
|
||||||
|
import { StudentType } from 'src/Types/StudentType';
|
||||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -10,76 +11,129 @@ interface Props {
|
||||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||||
showAnswer?: boolean;
|
showAnswer?: boolean;
|
||||||
passedAnswer?: AnswerType;
|
passedAnswer?: AnswerType;
|
||||||
|
students?: StudentType[];
|
||||||
|
showResults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
|
const { question, showAnswer, handleOnSubmitAnswer, students, showResults, passedAnswer } = props;
|
||||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
|
|
||||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||||
|
const [correctAnswerRate, setCorrectAnswerRate] = useState<number>(0);
|
||||||
|
const [submissionCounts, setSubmissionCounts] = useState({
|
||||||
|
correctSubmissions: 0,
|
||||||
|
totalSubmissions: 0
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (passedAnswer !== undefined) {
|
if (passedAnswer !== undefined) {
|
||||||
setAnswer(passedAnswer);
|
setAnswer(passedAnswer);
|
||||||
}
|
}
|
||||||
}, [passedAnswer]);
|
|
||||||
console.log("Answer" , answer);
|
if (showResults && students) {
|
||||||
|
calculateCorrectAnswerRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [passedAnswer, showResults, students, answer]);
|
||||||
|
console.log("Answer", answer);
|
||||||
|
|
||||||
|
const calculateCorrectAnswerRate = () => {
|
||||||
|
if (!students || students.length === 0) {
|
||||||
|
setSubmissionCounts({ correctSubmissions: 0, totalSubmissions: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSubmissions = students.length;
|
||||||
|
const correctSubmissions = students.filter(student =>
|
||||||
|
student.answers.some(ans =>
|
||||||
|
ans.idQuestion === Number(question.id) && ans.isCorrect
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
setSubmissionCounts({
|
||||||
|
correctSubmissions,
|
||||||
|
totalSubmissions
|
||||||
|
});
|
||||||
|
|
||||||
|
setCorrectAnswerRate((correctSubmissions / totalSubmissions) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="question-wrapper">
|
<>
|
||||||
<div className="question content">
|
<div className="container question-wrapper">
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
<div className="row justify-content-center">
|
||||||
</div>
|
<div className="col-auto">
|
||||||
{showAnswer ? (
|
<div>
|
||||||
<>
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||||
<div className="correct-answer-text mb-1">
|
</div>
|
||||||
<span>
|
{showAnswer ? (
|
||||||
<strong>La bonne réponse est: </strong>
|
<>
|
||||||
|
<div className="correct-answer-text mb-1">
|
||||||
{question.choices.map((choice) => (
|
<span>
|
||||||
<div key={choice.text} className="mb-1">
|
<strong>La bonne réponse est: </strong>
|
||||||
{choice.text}
|
{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) }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="answer-wrapper mb-1">
|
||||||
|
<TextField
|
||||||
|
type="text"
|
||||||
|
id={question.formattedStem.text}
|
||||||
|
name={question.formattedStem.text}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAnswer([e.target.value]);
|
||||||
|
}}
|
||||||
|
disabled={showAnswer}
|
||||||
|
aria-label="short-answer-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{handleOnSubmitAnswer && (
|
||||||
|
<div className="col-auto d-flex flex-column align-items-center">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
answer !== undefined &&
|
||||||
|
handleOnSubmitAnswer &&
|
||||||
|
handleOnSubmitAnswer(answer)
|
||||||
|
}
|
||||||
|
disabled={answer === null || answer === undefined || answer.length === 0}
|
||||||
|
>
|
||||||
|
Répondre
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showResults && (
|
||||||
|
<div className="col-auto">
|
||||||
|
<div>
|
||||||
|
Taux de réponse correcte: {submissionCounts.correctSubmissions}/{submissionCounts.totalSubmissions}
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar-container">
|
||||||
|
<div className="progress-bar-fill" style={{ width: `${correctAnswerRate}%` }}></div>
|
||||||
|
<div className="progress-bar-text">
|
||||||
|
{correctAnswerRate.toFixed(1)}%
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>Votre réponse est: </strong>{answer}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
|
)}
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
</div>
|
||||||
</div>}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="answer-wrapper mb-1">
|
|
||||||
<TextField
|
|
||||||
type="text"
|
|
||||||
id={question.formattedStem.text}
|
|
||||||
name={question.formattedStem.text}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAnswer([e.target.value]);
|
|
||||||
}}
|
|
||||||
disabled={showAnswer}
|
|
||||||
aria-label="short-answer-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{handleOnSubmitAnswer && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() =>
|
|
||||||
answer !== undefined &&
|
|
||||||
handleOnSubmitAnswer &&
|
|
||||||
handleOnSubmitAnswer(answer)
|
|
||||||
}
|
|
||||||
disabled={answer === null || answer === undefined || answer.length === 0}
|
|
||||||
>
|
|
||||||
Répondre
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,27 @@ import '../questionStyle.css';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { TrueFalseQuestion } from 'gift-pegjs';
|
import { TrueFalseQuestion } from 'gift-pegjs';
|
||||||
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
|
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
|
||||||
|
import { StudentType } from 'src/Types/StudentType';
|
||||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
question: TrueFalseQuestion;
|
question: TrueFalseQuestion;
|
||||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||||
showAnswer?: boolean;
|
showAnswer?: boolean;
|
||||||
passedAnswer?: AnswerType;
|
passedAnswer?: AnswerType;
|
||||||
|
students?: StudentType[];
|
||||||
|
showResults?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
const { question, showAnswer, handleOnSubmitAnswer, students, passedAnswer, showResults } = props;
|
||||||
props;
|
const [pickRates, setPickRates] = useState<{ trueRate: number, falseRate: number, trueCount: number, falseCount: number, totalCount: number }>({
|
||||||
|
trueRate: 0,
|
||||||
|
falseRate: 0,
|
||||||
|
trueCount: 0,
|
||||||
|
falseCount: 0,
|
||||||
|
totalCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
||||||
|
|
||||||
|
|
@ -31,75 +40,140 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
disableButton = true;
|
disableButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnClickAnswer = (choice: boolean) => {
|
||||||
|
setAnswer(choice);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("passedAnswer", passedAnswer);
|
|
||||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
||||||
setAnswer(passedAnswer[0]);
|
setAnswer(passedAnswer[0]);
|
||||||
} else {
|
} else {
|
||||||
setAnswer(undefined);
|
setAnswer(undefined);
|
||||||
}
|
}
|
||||||
}, [passedAnswer, question.id]);
|
|
||||||
|
|
||||||
const handleOnClickAnswer = (choice: boolean) => {
|
if (!passedAnswer && passedAnswer !== false) {
|
||||||
setAnswer(choice);
|
setAnswer(undefined);
|
||||||
};
|
calculatePickRates();
|
||||||
|
}
|
||||||
|
}, [passedAnswer, question.id, students]);
|
||||||
|
|
||||||
const selectedTrue = answer ? 'selected' : '';
|
const selectedTrue = answer ? 'selected' : '';
|
||||||
const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
|
const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
|
||||||
|
|
||||||
|
// Calcul le pick rate de chaque réponse
|
||||||
|
const calculatePickRates = () => {
|
||||||
|
if (!students) {
|
||||||
|
setPickRates({ trueRate: 0, falseRate: 0, trueCount: 0, falseCount: 0, totalCount: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAnswers = students.length;
|
||||||
|
const trueAnswers = students.filter(student =>
|
||||||
|
student.answers.some(ans =>
|
||||||
|
ans.idQuestion === Number(question.id) && ans.answer.some(a => a === true)
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
const falseAnswers = students.filter(student =>
|
||||||
|
student.answers.some(ans =>
|
||||||
|
ans.idQuestion === Number(question.id) && ans.answer.some(a => a === false)
|
||||||
|
)
|
||||||
|
).length;
|
||||||
|
|
||||||
|
setPickRates({
|
||||||
|
trueRate: (trueAnswers / totalAnswers) * 100,
|
||||||
|
falseRate: (falseAnswers / totalAnswers) * 100,
|
||||||
|
trueCount: trueAnswers,
|
||||||
|
falseCount: falseAnswers,
|
||||||
|
totalCount: totalAnswers
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="question-container">
|
<div className="container">
|
||||||
<div className="question content">
|
<div className="row justify-content-center">
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
<div className="col-auto question-container">
|
||||||
</div>
|
<div className="question content">
|
||||||
<div className="choices-wrapper mb-1">
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||||
<Button
|
</div>
|
||||||
className="button-wrapper"
|
<div className="choices-wrapper mb-1">
|
||||||
onClick={() => !showAnswer && handleOnClickAnswer(true)}
|
<Button
|
||||||
fullWidth
|
className="button-wrapper"
|
||||||
disabled={disableButton}
|
onClick={() => !showAnswer && handleOnClickAnswer(true)}
|
||||||
>
|
fullWidth
|
||||||
{showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
|
disabled={disableButton}
|
||||||
<div className={`answer-text ${selectedTrue}`}>Vrai</div>
|
>
|
||||||
|
{showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||||
|
<div className={`circle ${selectedTrue}`}>V</div>
|
||||||
|
<div className={`answer-text ${selectedTrue}`}
|
||||||
|
style={showResults ? {
|
||||||
|
backgroundImage: `linear-gradient(to right, ${question.isTrue ? 'lightgreen' : 'lightcoral'} ${pickRates.trueRate}%, transparent ${pickRates.trueRate}%)`
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
Vrai
|
||||||
|
</div>
|
||||||
|
{showResults && (
|
||||||
|
<>
|
||||||
|
<div className="pick-rate">{question.isTrue ? '✅' : '❌'} {pickRates.trueCount}/{pickRates.totalCount} ({pickRates.trueRate.toFixed(1)}%)</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{showAnswer && answer && question.trueFormattedFeedback && (
|
{showAnswer && answer && question.trueFormattedFeedback && (
|
||||||
<div className="true-feedback mb-2">
|
<div className="true-feedback mb-2">
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`button-wrapper ${selectedFalse}`}
|
||||||
|
onClick={() => !showResults && handleOnClickAnswer(false)}
|
||||||
|
fullWidth
|
||||||
|
disabled={disableButton}
|
||||||
|
|
||||||
|
>
|
||||||
|
{showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||||
|
<div className={`circle ${selectedFalse}`}>F</div>
|
||||||
|
<div
|
||||||
|
className={`answer-text ${selectedFalse}`}
|
||||||
|
style={showResults ? {
|
||||||
|
backgroundImage: `linear-gradient(to right, ${!question.isTrue ? 'lightgreen' : 'lightcoral'} ${pickRates.falseRate}%, transparent ${pickRates.falseRate}%)`,
|
||||||
|
} : {}}
|
||||||
|
>
|
||||||
|
Faux
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showResults && (
|
||||||
|
<>
|
||||||
|
<div className="pick-rate">{!question.isTrue ? '✅' : '❌'} {pickRates.falseCount}/{pickRates.totalCount} ({pickRates.falseRate.toFixed(1)}%)</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAnswer && !answer && question.falseFormattedFeedback && (
|
||||||
|
<div className="false-feedback mb-2">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{question.formattedGlobalFeedback && showAnswer && (
|
||||||
|
<div className="global-feedback mb-2">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
{!showAnswer && handleOnSubmitAnswer && (
|
||||||
<Button
|
<Button
|
||||||
className="button-wrapper"
|
variant="contained"
|
||||||
onClick={() => !showAnswer && handleOnClickAnswer(false)}
|
onClick={() =>
|
||||||
fullWidth
|
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
|
||||||
disabled={disableButton}
|
}
|
||||||
|
disabled={answer === undefined}
|
||||||
>
|
>
|
||||||
{showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
|
Répondre
|
||||||
<div className={`answer-text ${selectedFalse}`}>Faux</div>
|
</Button>
|
||||||
|
|
||||||
{showAnswer && !answer && question.falseFormattedFeedback && (
|
|
||||||
<div className="false-feedback mb-2">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{question.formattedGlobalFeedback && showAnswer && (
|
|
||||||
<div className="global-feedback mb-2">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{!showAnswer && handleOnSubmitAnswer && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() =>
|
|
||||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
|
|
||||||
}
|
|
||||||
disabled={answer === undefined}
|
|
||||||
>
|
|
||||||
Répondre
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -169,3 +169,35 @@
|
||||||
.choices-wrapper {
|
.choices-wrapper {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #FEFEFE;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: royalblue;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-text {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
top: 0;
|
||||||
|
line-height: 20px;
|
||||||
|
color: Black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick-rate{
|
||||||
|
color: rgba(0,0,0,1);
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { ThemeProvider, createTheme } from '@mui/material';
|
import { ThemeProvider, createTheme } from '@mui/material';
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
import './cssReset.css';
|
import './cssReset.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,12 @@ const ManageRoom: React.FC = () => {
|
||||||
|
|
||||||
socket.on('user-disconnected', (userId: string) => {
|
socket.on('user-disconnected', (userId: string) => {
|
||||||
console.log(`Student left: id = ${userId}`);
|
console.log(`Student left: id = ${userId}`);
|
||||||
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
|
//setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
|
||||||
|
setStudents(prevStudents =>
|
||||||
|
prevStudents.map(student =>
|
||||||
|
student.id === userId ? { ...student, isActive: false } : student
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
setSocket(socket);
|
setSocket(socket);
|
||||||
|
|
@ -520,7 +525,6 @@ const ManageRoom: React.FC = () => {
|
||||||
{quizQuestions?.length}
|
{quizQuestions?.length}
|
||||||
</strong>
|
</strong>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
{quizMode === 'teacher' && (
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* <QuestionNavigation
|
{/* <QuestionNavigation
|
||||||
|
|
@ -537,7 +541,9 @@ const ManageRoom: React.FC = () => {
|
||||||
{currentQuestion && (
|
{currentQuestion && (
|
||||||
<QuestionDisplay
|
<QuestionDisplay
|
||||||
showAnswer={false}
|
showAnswer={false}
|
||||||
|
showAnswerToggle={true}
|
||||||
question={currentQuestion?.question as Question}
|
question={currentQuestion?.question as Question}
|
||||||
|
students={students}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
31
package-lock.json
generated
31
package-lock.json
generated
|
|
@ -5,7 +5,18 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios-mock-adapter": "^2.1.0"
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
|
"bootstrap": "^5.3.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
|
|
@ -37,6 +48,24 @@
|
||||||
"axios": ">= 0.17.0"
|
"axios": ">= 0.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind-apply-helpers": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios-mock-adapter": "^2.1.0"
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
|
"bootstrap": "^5.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue