mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Compare commits
No commits in common. "aa50af402e52b1c04c1822c93556fe9e1923056f" and "db21581535eb4798711ec85608f121ca6c211d7c" have entirely different histories.
aa50af402e
...
db21581535
148 changed files with 4127 additions and 11551 deletions
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Créez un rapport pour nous aider à améliorer / Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Décrivez le bug / Describe the bug**
|
||||
Une description claire et concise du bug. / A clear and concise description of what the bug is.
|
||||
|
||||
**Pour reproduire / To Reproduce**
|
||||
Étapes pour reproduire le comportement : / Steps to reproduce the behavior:
|
||||
1. Aller à '...' / Go to '...'
|
||||
2. Cliquer sur '...' / Click on '...'
|
||||
3. Faites défiler jusqu'à '...' / Scroll down to '...'
|
||||
4. Voir l'erreur / See error
|
||||
|
||||
**Comportement attendu / Expected behavior**
|
||||
Une description claire et concise de ce que vous attendiez. / A clear and concise description of what you expected to happen.
|
||||
|
||||
**Captures d'écran / Screenshots**
|
||||
Si applicable, ajoutez des captures d'écran pour aider à expliquer votre problème. / If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Ordinateur (veuillez compléter les informations suivantes) / Desktop (please complete the following information):**
|
||||
- Système d'exploitation : [par exemple, Windows, macOS, Linux] / OS: [e.g. Windows, macOS, Linux]
|
||||
- Navigateur : [par exemple, Chrome, Firefox, Safari] / Browser [e.g. Chrome, Firefox, Safari]
|
||||
- Version : [par exemple, 22] / Version [e.g. 22]
|
||||
|
||||
**Smartphone (veuillez compléter les informations suivantes) / Smartphone (please complete the following information):**
|
||||
- Appareil : [par exemple, iPhone6, Samsung Galaxy S10] / Device: [e.g. iPhone6, Samsung Galaxy S10]
|
||||
- Système d'exploitation : [par exemple, iOS 14.4, Android 11] / OS: [e.g. iOS 14.4, Android 11]
|
||||
- Navigateur : [par exemple, navigateur par défaut, Safari] / Browser [e.g. stock browser, Safari]
|
||||
- Version : [par exemple, 22] / Version [e.g. 22]
|
||||
|
||||
**Contexte supplémentaire / Additional context**
|
||||
Ajoutez tout autre contexte concernant le problème ici. / Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
20
.github/ISSUE_TEMPLATE/feature-request.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggérez une idée pour ce projet / Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Votre demande de fonctionnalité est-elle liée à un problème ? Veuillez décrire. / Is your feature request related to a problem? Please describe.**
|
||||
Une description claire et concise du problème. Par exemple, je suis toujours frustré lorsque [...] / A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Décrivez la solution que vous souhaitez / Describe the solution you'd like**
|
||||
Une description claire et concise de ce que vous voulez qu'il se passe. / A clear and concise description of what you want to happen.
|
||||
|
||||
**Décrivez les alternatives que vous avez envisagées / Describe alternatives you've considered**
|
||||
Une description claire et concise de toute autre solution ou fonctionnalité que vous avez envisagée. / A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Contexte supplémentaire / Additional context**
|
||||
Ajoutez tout autre contexte ou capture d'écran concernant la demande de fonctionnalité ici. / Add any other context or screenshots about the feature request here.
|
||||
43
.github/workflows/tests.yml
vendored
43
.github/workflows/tests.yml
vendored
|
|
@ -8,50 +8,29 @@ on:
|
|||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
MONGO_URI: mongodb://localhost:27017
|
||||
MONGO_DATABASE: evaluetonsavoir
|
||||
|
||||
jobs:
|
||||
lint-and-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
directory: [client, server]
|
||||
fail-fast: false
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: ${{ matrix.directory }}/package-lock.json
|
||||
|
||||
- name: Process ${{ matrix.directory }}
|
||||
working-directory: ${{ matrix.directory }}
|
||||
timeout-minutes: 5
|
||||
- name: Install Dependencies, lint and Run Tests
|
||||
run: |
|
||||
echo "Installing dependencies..."
|
||||
npm install
|
||||
npm ci
|
||||
echo "Running ESLint..."
|
||||
npx eslint .
|
||||
echo "Running tests..."
|
||||
echo "::group::Installing dependencies for ${{ matrix.directory }}"
|
||||
npm ci
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::Running ESLint"
|
||||
npx eslint . || {
|
||||
echo "ESLint failed with exit code $?"
|
||||
exit 1
|
||||
}
|
||||
echo "::endgroup::"
|
||||
|
||||
echo "::group::Running Tests"
|
||||
npm test
|
||||
echo "::endgroup::"
|
||||
working-directory: ${{ matrix.directory }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
directory: [client, server]
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -41,7 +41,6 @@ build/Release
|
|||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
mongo-backup/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
|
@ -123,9 +122,6 @@ dist
|
|||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
.env
|
||||
launch.json
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"path": "server"
|
||||
},
|
||||
{
|
||||
"name": "client",
|
||||
"path": "client"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"jest.disabledWorkspaceFolders": [
|
||||
"EvalueTonSavoir"
|
||||
]
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"javascriptreact",
|
||||
"typescriptreact"
|
||||
],
|
||||
// use the same eslint config as `npx eslint`
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"eslint.nodePath": "./node_modules"
|
||||
|
||||
}
|
||||
1
LICENSE
1
LICENSE
|
|
@ -3,7 +3,6 @@ MIT License
|
|||
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
|
||||
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
|
||||
Copyright (c) 2024 Gabriel Moisan-Matte, Mathieu Sévigny-Lavallée, Jerry Kwok Hiu Fung, Bruno Roesner, Florent Serres
|
||||
Copyright (c) 2025 Nouhaïla Aâter, Kendrick Chan Hing Wah, Philippe Côté, Edwin Stanley Lopez Andino, Ana Lucia Munteanu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
|
||||
|
||||
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/master/README.md)
|
||||
|
||||
# EvalueTonSavoir
|
||||
|
||||
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle.
|
||||
|
||||
## Fonctionnalités clés
|
||||
|
||||
* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure.
|
||||
* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage.
|
||||
* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage.
|
||||
|
||||
## Contribution
|
||||
|
||||
Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR)
|
||||
|
||||
## Liens utiles
|
||||
|
||||
* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
|
||||
* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
|
||||
* [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki)
|
||||
|
||||
## License
|
||||
|
||||
EvalueTonSavoir is open-sourced and licensed under the [MIT License](/LICENSE).
|
||||
20
README.md
20
README.md
|
|
@ -2,26 +2,24 @@
|
|||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
|
||||
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/main/README.fr-ca.md)
|
||||
|
||||
# EvalueTonSavoir
|
||||
|
||||
EvalueTonSavoir is an open-source and self-hosted platform that continues the development of the code from https://github.com/ETS-PFE004-Plateforme-sondage-minitest. This minimalist platform is designed as a learning and teaching tool, offering a simple and effective solution for creating quizzes using the GIFT format, similar to Moodle.
|
||||
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle.
|
||||
|
||||
## Key Features
|
||||
## Fonctionnalités clés
|
||||
|
||||
* **Open Source and Self-Hosted**: Own and control your data by deploying the platform on your own infrastructure.
|
||||
* **GIFT Compatibility**: Easily create quizzes using the GIFT format, enabling seamless integration with other learning systems.
|
||||
* **Minimalist and Efficient**: A bare-bones approach to ensure simplicity and ease of use, focusing on the essentials of learning.
|
||||
* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure.
|
||||
* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage.
|
||||
* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage.
|
||||
|
||||
## Contribution
|
||||
|
||||
Currently, there is no established model for contributions. If you notice something missing or think an improvement is possible, feel free to open an issue and/or a PR.
|
||||
Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR)
|
||||
|
||||
## Useful Links
|
||||
## Liens utiles
|
||||
|
||||
* [Original Frontend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
|
||||
* [Original Backend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
|
||||
* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
|
||||
* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
|
||||
* [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki)
|
||||
|
||||
## License
|
||||
|
|
|
|||
19
client/.eslintrc.cjs
Normal file
19
client/.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,77 +1,29 @@
|
|||
import react from "eslint-plugin-react";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import typescriptParser from "@typescript-eslint/parser";
|
||||
import globals from "globals";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import eslintComments from "eslint-plugin-eslint-comments";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
ignores: ["node_modules", "dist/**/*"],
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
{
|
||||
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx}"],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
...globals.serviceworker,
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
...globals.node,
|
||||
process: "readonly",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
react,
|
||||
jest,
|
||||
"react-refresh": reactRefresh,
|
||||
"unused-imports": unusedImports,
|
||||
"eslint-comments": eslintComments
|
||||
},
|
||||
rules: {
|
||||
// Auto-fix unused variables
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
|
||||
// Handle directive comments
|
||||
"eslint-comments/no-unused-disable": "warn",
|
||||
"eslint-comments/no-unused-enable": "warn",
|
||||
|
||||
// Jest configurations
|
||||
"jest/valid-expect": ["error", { "alwaysAwait": true }],
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-focused-tests": "error",
|
||||
"jest/no-identical-title": "error",
|
||||
|
||||
// React refresh
|
||||
"react-refresh/only-export-components": ["warn", {
|
||||
allowConstantExport: true
|
||||
}],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
}
|
||||
rules: {
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
|
||||
}],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect", // Automatically detect the React version
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
/* eslint-disable no-undef */
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
/* eslint-disable no-undef */
|
||||
process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
|
||||
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';
|
||||
|
|
|
|||
3327
client/package-lock.json
generated
3327
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,77 +4,70 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "cross-env MODE=development VITE_BACKEND_URL=http://localhost:4400 vite --host",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test": "jest --colors --silent",
|
||||
"test": "jest --colors",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@mui/icons-material": "^7.0.1",
|
||||
"@mui/icons-material": "^6.1.0",
|
||||
"@mui/lab": "^5.0.0-alpha.153",
|
||||
"@mui/material": "^7.0.1",
|
||||
"@mui/material": "^6.1.0",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.8.1",
|
||||
"dompurify": "^3.2.5",
|
||||
"esbuild": "^0.25.2",
|
||||
"axios": "^1.6.7",
|
||||
"dompurify": "^3.2.3",
|
||||
"esbuild": "^0.23.1",
|
||||
"gift-pegjs": "^2.0.0-beta.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jspdf": "^2.5.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.11",
|
||||
"marked": "^15.0.8",
|
||||
"nanoid": "^5.1.5",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"marked": "^14.1.2",
|
||||
"nanoid": "^5.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"remark-math": "^6.0.0",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vite-plugin-checker": "^0.9.1"
|
||||
"uuid": "^9.0.1",
|
||||
"vite-plugin-checker": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-latex": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||
"@typescript-eslint/parser": "^8.29.1",
|
||||
"@vitejs/plugin-react-swc": "^3.8.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"globals": "^15.14.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.3.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite": "^6.2.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"vite": "^5.4.5",
|
||||
"vite-plugin-environment": "^1.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
// App.tsx
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
|
||||
// Page main
|
||||
import Home from './pages/Home/Home';
|
||||
|
|
@ -8,55 +8,37 @@ import Home from './pages/Home/Home';
|
|||
// Pages espace enseignant
|
||||
import Dashboard from './pages/Teacher/Dashboard/Dashboard';
|
||||
import Share from './pages/Teacher/Share/Share';
|
||||
import Register from './pages/AuthManager/providers/SimpleLogin/Register';
|
||||
import ResetPassword from './pages/AuthManager/providers/SimpleLogin/ResetPassword';
|
||||
import Login from './pages/Teacher/Login/Login';
|
||||
import Register from './pages/Teacher/Register/Register';
|
||||
import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword';
|
||||
import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom';
|
||||
import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
|
||||
|
||||
// Pages espace étudiant
|
||||
import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
// Pages authentification selection
|
||||
import AuthDrawer from './pages/AuthManager/AuthDrawer';
|
||||
|
||||
// Header/Footer import
|
||||
import Header from './components/Header/Header';
|
||||
import Footer from './components/Footer/Footer';
|
||||
|
||||
import ApiService from './services/ApiService';
|
||||
import OAuthCallback from './pages/AuthManager/callback/AuthCallback';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn());
|
||||
const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher());
|
||||
const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null);
|
||||
const location = useLocation();
|
||||
const handleLogout = () => {
|
||||
ApiService.logout();
|
||||
}
|
||||
|
||||
// Check login status every time the route changes
|
||||
useEffect(() => {
|
||||
const checkLoginStatus = () => {
|
||||
setIsAuthenticated(ApiService.isLoggedIn());
|
||||
setIsTeacherAuthenticated(ApiService.isLoggedInTeacher());
|
||||
};
|
||||
|
||||
const fetchAuthenticatedRooms = async () => {
|
||||
const data = await ApiService.getRoomsRequireAuth();
|
||||
setRoomsRequireAuth(data);
|
||||
};
|
||||
|
||||
checkLoginStatus();
|
||||
fetchAuthenticatedRooms();
|
||||
}, [location]);
|
||||
|
||||
const handleLogout = () => {
|
||||
ApiService.logout();
|
||||
setIsAuthenticated(false);
|
||||
setIsTeacherAuthenticated(false);
|
||||
};
|
||||
const isLoggedIn = () => {
|
||||
return ApiService.isLoggedIn();
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="content">
|
||||
<Header isLoggedIn={isAuthenticated} handleLogout={handleLogout} />
|
||||
|
||||
<Header
|
||||
isLoggedIn={isLoggedIn}
|
||||
handleLogout={handleLogout}/>
|
||||
|
||||
<div className="app">
|
||||
<main>
|
||||
<Routes>
|
||||
|
|
@ -64,46 +46,22 @@ const App: React.FC = () => {
|
|||
<Route path="/" element={<Home />} />
|
||||
|
||||
{/* Pages espace enseignant */}
|
||||
<Route
|
||||
path="/teacher/dashboard"
|
||||
element={isTeacherAuthenticated ? <Dashboard /> : <Navigate to="/login" />}
|
||||
/>
|
||||
<Route
|
||||
path="/teacher/share/:id"
|
||||
element={isTeacherAuthenticated ? <Share /> : <Navigate to="/login" />}
|
||||
/>
|
||||
<Route
|
||||
path="/teacher/editor-quiz/:id"
|
||||
element={isTeacherAuthenticated ? <QuizForm /> : <Navigate to="/login" />}
|
||||
/>
|
||||
<Route
|
||||
path="/teacher/manage-room/:quizId/:roomName"
|
||||
element={isTeacherAuthenticated ? <ManageRoom /> : <Navigate to="/login" />}
|
||||
/>
|
||||
<Route path="/teacher/login" element={<Login />} />
|
||||
<Route path="/teacher/register" element={<Register />} />
|
||||
<Route path="/teacher/resetPassword" element={<ResetPassword />} />
|
||||
<Route path="/teacher/dashboard" element={<Dashboard />} />
|
||||
<Route path="/teacher/share/:id" element={<Share />} />
|
||||
<Route path="/teacher/editor-quiz/:id" element={<QuizForm />} />
|
||||
<Route path="/teacher/manage-room/:id" element={<ManageRoom />} />
|
||||
|
||||
{/* Pages espace étudiant */}
|
||||
<Route
|
||||
path="/student/join-room"
|
||||
element={( !isRoomRequireAuthentication || isAuthenticated ) ? <JoinRoom /> : <Navigate to="/login" />}
|
||||
/>
|
||||
|
||||
{/* Pages authentification */}
|
||||
<Route path="/login" element={<AuthDrawer />} />
|
||||
|
||||
{/* Pages enregistrement */}
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Pages rest password */}
|
||||
<Route path="/resetPassword" element={<ResetPassword />} />
|
||||
|
||||
{/* Pages authentification sélection */}
|
||||
<Route path="/auth/callback" element={<OAuthCallback />} />
|
||||
<Route path="/student/join-room" element={<JoinRoom />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
<Footer/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
export interface ImageType {
|
||||
id: string;
|
||||
file_content: string;
|
||||
file_name: string;
|
||||
mime_type: string;
|
||||
}
|
||||
|
||||
export interface ImagesResponse {
|
||||
images: ImageType[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ImagesParams {
|
||||
page: number;
|
||||
limit: number;
|
||||
uid?: string;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
export interface Images {
|
||||
id: string;
|
||||
file_content: string;
|
||||
file_name: string;
|
||||
mime_type: string;
|
||||
}
|
||||
|
||||
export interface ImagesResponse {
|
||||
images: Images[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ImagesParams {
|
||||
page: number;
|
||||
limit: number;
|
||||
uid?: string;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export interface RoomType {
|
||||
_id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { AnswerType } from "src/pages/Student/JoinRoom/JoinRoom";
|
||||
|
||||
export interface Answer {
|
||||
answer: AnswerType;
|
||||
answer: string | number | boolean;
|
||||
isCorrect: boolean;
|
||||
idQuestion: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { RoomType } from "../../Types/RoomType";
|
||||
|
||||
const room: RoomType = {
|
||||
_id: '123',
|
||||
userId: '456',
|
||||
title: 'Test Room',
|
||||
created_at: '2025-02-21T00:00:00Z'
|
||||
};
|
||||
|
||||
describe('RoomType', () => {
|
||||
test('creates a room with _id, userId, title, and created_at', () => {
|
||||
expect(room._id).toBe('123');
|
||||
expect(room.userId).toBe('456');
|
||||
expect(room.title).toBe('Test Room');
|
||||
expect(room.created_at).toBe('2025-02-21T00:00:00Z');
|
||||
});
|
||||
});
|
||||
|
|
@ -12,6 +12,6 @@ describe('StudentType', () => {
|
|||
|
||||
expect(user.name).toBe('Student');
|
||||
expect(user.id).toBe('123');
|
||||
expect(user.answers).toHaveLength(0);
|
||||
expect(user.answers.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,87 +3,49 @@ import { render, screen } from '@testing-library/react';
|
|||
import '@testing-library/jest-dom';
|
||||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
||||
|
||||
const validQuestions = [
|
||||
'::TFTitle::[markdown]Troo statement {TRUE}',
|
||||
'::SATitle::[markdown]What is the answer? {=ShortAnswerOne =ShortAnswerTwo}',
|
||||
'::MCQTitle::[markdown]MultiChoice question? {=MQAnswerOne ~MQAnswerTwo#feedback####Gen feedback}',
|
||||
];
|
||||
|
||||
const unsupportedQuestions = [
|
||||
'::Title::[markdown]Essay {}',
|
||||
'::Title::[markdown]Matching {}',
|
||||
'::Title::[markdown]Description',
|
||||
'$CATEGORY a/b/c'
|
||||
];
|
||||
|
||||
describe('GIFTTemplatePreview Component', () => {
|
||||
it('renders error message when questions contain invalid syntax', () => {
|
||||
render(<GIFTTemplatePreview questions={['T{']} hideAnswers={false} />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
const errorMessage = previewContainer.querySelector('div[label="error-message"]');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
test('renders error message when questions contain invalid syntax', () => {
|
||||
render(<GIFTTemplatePreview questions={['Invalid GIFT syntax']} />);
|
||||
const errorMessage = screen.findByText(/Erreur inconnue/i, {}, { timeout: 5000 });
|
||||
expect(errorMessage).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders preview when valid questions are provided, including answers, has no errors', () => {
|
||||
render(<GIFTTemplatePreview questions={validQuestions} hideAnswers={false} />);
|
||||
test('renders preview when valid questions are provided', () => {
|
||||
const questions = [
|
||||
'Question 1 { A | B | C }',
|
||||
'Question 2 { D | E | F }',
|
||||
];
|
||||
render(<GIFTTemplatePreview questions={questions} />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
// Check that all question titles are rendered inside the previewContainer
|
||||
validQuestions.forEach((question) => {
|
||||
const title = question.split('::')[1].split('::')[0];
|
||||
expect(previewContainer).toHaveTextContent(title);
|
||||
});
|
||||
// There should be no errors
|
||||
const errorMessage = previewContainer.querySelector('div[label="error-message"]');
|
||||
expect(errorMessage).not.toBeInTheDocument();
|
||||
// Check that some stems and answers are rendered inside the previewContainer
|
||||
expect(previewContainer).toHaveTextContent('Troo statement');
|
||||
expect(previewContainer).toHaveTextContent('What is the answer?');
|
||||
expect(previewContainer).toHaveTextContent('MultiChoice question?');
|
||||
expect(previewContainer).toHaveTextContent('Vrai');
|
||||
// short answers are stored in a textbox
|
||||
const answerInputElements = screen.getAllByRole('textbox');
|
||||
const giftInputElements = answerInputElements.filter(element => element.classList.contains('gift-input'));
|
||||
|
||||
expect(giftInputElements).toHaveLength(1);
|
||||
expect(giftInputElements[0]).toHaveAttribute('placeholder', 'ShortAnswerOne, ShortAnswerTwo');
|
||||
|
||||
// Check for correct answer icon just after MQAnswerOne
|
||||
const mqAnswerOneElement = screen.getByText('MQAnswerOne');
|
||||
const correctAnswerIcon = mqAnswerOneElement.parentElement?.querySelector('[data-testid="correct-icon"]');
|
||||
expect(correctAnswerIcon).toBeInTheDocument();
|
||||
|
||||
// Check for incorrect answer icon just after MQAnswerTwo
|
||||
const mqAnswerTwoElement = screen.getByText('MQAnswerTwo');
|
||||
const incorrectAnswerIcon = mqAnswerTwoElement.parentElement?.querySelector('[data-testid="incorrect-icon"]');
|
||||
expect(incorrectAnswerIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides answers when hideAnswers prop is true', () => {
|
||||
render(<GIFTTemplatePreview questions={validQuestions} hideAnswers={true} />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
expect(previewContainer).toHaveTextContent('Troo statement');
|
||||
expect(previewContainer).toHaveTextContent('What is the answer?');
|
||||
expect(previewContainer).toHaveTextContent('MultiChoice question?');
|
||||
expect(previewContainer).toHaveTextContent('Vrai');
|
||||
expect(previewContainer).not.toHaveTextContent('ShortAnswerOne');
|
||||
expect(previewContainer).not.toHaveTextContent('ShortAnswerTwo');
|
||||
// shouldn't have correct/incorrect icons
|
||||
const correctAnswerIcon = screen.queryByTestId('correct-icon');
|
||||
expect(correctAnswerIcon).not.toBeInTheDocument();
|
||||
const incorrectAnswerIcon = screen.queryByTestId('incorrect-icon');
|
||||
expect(incorrectAnswerIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should indicate in the preview that unsupported GIFT questions are not supported', () => {
|
||||
render(<GIFTTemplatePreview questions={unsupportedQuestions} hideAnswers={false} />);
|
||||
test('hides answers when hideAnswers prop is true', () => {
|
||||
const questions = [
|
||||
'Question 1 { A | B | C }',
|
||||
'Question 2 { D | E | F }',
|
||||
];
|
||||
render(<GIFTTemplatePreview questions={questions} hideAnswers />);
|
||||
const previewContainer = screen.getByTestId('preview-container');
|
||||
expect(previewContainer).toBeInTheDocument();
|
||||
// find all unsupported errors (should be 4)
|
||||
const unsupportedMessages = previewContainer.querySelectorAll('div[label="error-message"]');
|
||||
expect(unsupportedMessages).toHaveLength(4);
|
||||
});
|
||||
|
||||
// it('renders images correctly', () => {
|
||||
// const questions = [
|
||||
// 'Question 1',
|
||||
// '<img src="image1.jpg" alt="Image 1">',
|
||||
// 'Question 2',
|
||||
// '<img src="image2.jpg" alt="Image 2">',
|
||||
// ];
|
||||
// const { getByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||
// const image1 = getByAltText('Image 1');
|
||||
// const image2 = getByAltText('Image 2');
|
||||
// expect(image1).toBeInTheDocument();
|
||||
// expect(image2).toBeInTheDocument();
|
||||
// });
|
||||
// it('renders non-images correctly', () => {
|
||||
// const questions = ['Question 1', 'Question 2'];
|
||||
// const { queryByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||
// const image1 = queryByAltText('Image 1');
|
||||
// const image2 = queryByAltText('Image 2');
|
||||
// expect(image1).toBeNull();
|
||||
// expect(image2).toBeNull();
|
||||
// });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LiveResults from 'src/components/LiveResults/LiveResults';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
const mockGiftQuestions = parse(
|
||||
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
|
||||
|
||||
::Sample Question 2:: Sample Question 2 {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};
|
||||
});
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockShowSelectedQuestion = jest.fn();
|
||||
|
||||
describe('LiveResults', () => {
|
||||
test('renders LiveResults component', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={null}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Résultats du quiz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('toggles show usernames switch', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={null}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
const switchElement = screen.getByLabelText('Afficher les noms');
|
||||
expect(switchElement).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(switchElement);
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
|
||||
test('toggles show correct answers switch', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={null}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
const switchElement = screen.getByLabelText('Afficher les réponses');
|
||||
expect(switchElement).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(switchElement);
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
|
||||
test('calls showSelectedQuestion when a table cell is clicked', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={null}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
const tableCell = screen.getByText('Q1');
|
||||
fireEvent.click(tableCell);
|
||||
|
||||
expect(mockShowSelectedQuestion).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import LiveResultsTable from 'src/components/LiveResults/LiveResultsTable/LiveResultsTable';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
const mockGiftQuestions = parse(
|
||||
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
|
||||
|
||||
::Sample Question 2:: Sample Question 2 {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};
|
||||
});
|
||||
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockShowSelectedQuestion = jest.fn();
|
||||
|
||||
describe('LiveResultsTable', () => {
|
||||
test('renders LiveResultsTable component', () => {
|
||||
render(
|
||||
<LiveResultsTable
|
||||
questions={mockQuestions}
|
||||
students={mockStudents}
|
||||
showCorrectAnswers={false}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
showUsernames={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Student 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Student 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays correct and incorrect answers', () => {
|
||||
render(
|
||||
<LiveResultsTable
|
||||
questions={mockQuestions}
|
||||
students={mockStudents}
|
||||
showCorrectAnswers={true}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
showUsernames={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Answer 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Answer 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls showSelectedQuestion when a table cell is clicked', () => {
|
||||
render(
|
||||
<LiveResultsTable
|
||||
questions={mockQuestions}
|
||||
students={mockStudents}
|
||||
showCorrectAnswers={true}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
showUsernames={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const tableCell = screen.getByText('Q1');
|
||||
fireEvent.click(tableCell);
|
||||
|
||||
expect(mockShowSelectedQuestion).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calculates and displays student grades', () => {
|
||||
render(
|
||||
<LiveResultsTable
|
||||
questions={mockQuestions}
|
||||
students={mockStudents}
|
||||
showCorrectAnswers={true}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
showUsernames={true}
|
||||
/>
|
||||
);
|
||||
|
||||
//50% because only one of the two questions have been answered (getALLByText, because there are a value 50% for the %reussite de la question
|
||||
// and a second one for the student grade)
|
||||
const gradeElements = screen.getAllByText('50 %');
|
||||
expect(gradeElements).toHaveLength(2);
|
||||
|
||||
const gradeElements2 = screen.getAllByText('0 %');
|
||||
expect(gradeElements2).toHaveLength(2); });
|
||||
|
||||
test('calculates and displays class average', () => {
|
||||
render(
|
||||
<LiveResultsTable
|
||||
questions={mockQuestions}
|
||||
students={mockStudents}
|
||||
showCorrectAnswers={true}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
showUsernames={true}
|
||||
/>
|
||||
);
|
||||
|
||||
//1 good answer out of 4 possible good answers (the second question has not been answered)
|
||||
expect(screen.getByText('25 %')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import LiveResultsTableBody from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
|
||||
const mockGiftQuestions = parse(
|
||||
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2}
|
||||
|
||||
::Sample Question 2:: Sample Question 2 {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};
|
||||
});
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockGetStudentGrade = jest.fn((student: StudentType) => {
|
||||
const correctAnswers = student.answers.filter(answer => answer.isCorrect).length;
|
||||
return (correctAnswers / mockQuestions.length) * 100;
|
||||
});
|
||||
|
||||
describe('LiveResultsTableBody', () => {
|
||||
test('renders LiveResultsTableBody component', () => {
|
||||
render(
|
||||
<LiveResultsTableBody
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
showUsernames={true}
|
||||
showCorrectAnswers={false}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Student 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Student 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays correct and incorrect answers', () => {
|
||||
render(
|
||||
<LiveResultsTableBody
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
showUsernames={true}
|
||||
showCorrectAnswers={true}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Answer 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Answer 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays icons for correct and incorrect answers when showCorrectAnswers is false', () => {
|
||||
render(
|
||||
<LiveResultsTableBody
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
showUsernames={true}
|
||||
showCorrectAnswers={false}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('correct')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('incorrect')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hides usernames when showUsernames is false', () => {
|
||||
render(
|
||||
<LiveResultsTableBody
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
showUsernames={false}
|
||||
showCorrectAnswers={true}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getAllByText('******')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter';
|
||||
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockGetStudentGrade = jest.fn((student: StudentType) => {
|
||||
const correctAnswers = student.answers.filter(answer => answer.isCorrect).length;
|
||||
return (correctAnswers / 2) * 100; // Assuming there are 2 questions
|
||||
});
|
||||
|
||||
describe('LiveResultsTableFooter', () => {
|
||||
test('renders LiveResultsTableFooter component', () => {
|
||||
render(
|
||||
<LiveResultsTableFooter
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('% réussite')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calculates and displays correct answers per question', () => {
|
||||
render(
|
||||
<LiveResultsTableFooter
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('50 %')).toBeInTheDocument();
|
||||
expect(screen.getByText('0 %')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calculates and displays class average', () => {
|
||||
render(
|
||||
<LiveResultsTableFooter
|
||||
maxQuestions={2}
|
||||
students={mockStudents}
|
||||
getStudentGrade={mockGetStudentGrade}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('50 %')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LiveResultsTableHeader from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader';
|
||||
|
||||
|
||||
const mockShowSelectedQuestion = jest.fn();
|
||||
|
||||
describe('LiveResultsTableHeader', () => {
|
||||
test('renders LiveResultsTableHeader component', () => {
|
||||
render(
|
||||
<LiveResultsTableHeader
|
||||
maxQuestions={5}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Nom d'utilisateur")).toBeInTheDocument();
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
expect(screen.getByText(`Q${i}`)).toBeInTheDocument();
|
||||
}
|
||||
expect(screen.getByText('% réussite')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls showSelectedQuestion when a question header is clicked', () => {
|
||||
render(
|
||||
<LiveResultsTableHeader
|
||||
maxQuestions={5}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
const questionHeader = screen.getByText('Q1');
|
||||
fireEvent.click(questionHeader);
|
||||
|
||||
expect(mockShowSelectedQuestion).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('renders the correct number of question headers', () => {
|
||||
render(
|
||||
<LiveResultsTableHeader
|
||||
maxQuestions={3}
|
||||
showSelectedQuestion={mockShowSelectedQuestion}
|
||||
/>
|
||||
);
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
expect(screen.getByText(`Q${i}`)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -54,10 +54,10 @@ describe('TextType', () => {
|
|||
format: ''
|
||||
};
|
||||
|
||||
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
// warning: there are zero-width spaces "" in the expected output -- you must enable seeing them with an extension such as Gremlins tracker in VSCode
|
||||
|
||||
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
const expectedOutput = `Inline matrix: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mo fence="true">(</mo><mtable rowspacing="0.16em"><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>a</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>b</mi></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>c</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>d</mi></mstyle></mtd></mtr></mtable><mo fence="true">)</mo></mrow> \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} </math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:2.4em;vertical-align:-0.95em;" class="strut"></span><span class="minner"><span style="top:0em;" class="mopen delimcenter"><span class="delimsizing size3">(</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span><span style="width:0.5em;" class="arraycolsep"></span><span style="width:0.5em;" class="arraycolsep"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">b</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span></span></span><span style="top:0em;" class="mclose delimcenter"><span class="delimsizing size3">)</span></span></span></span></span></span>`;
|
||||
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function convertStylesToObject(styles: string): React.CSSProperties {
|
|||
styles.split(';').forEach((style) => {
|
||||
const [property, value] = style.split(':');
|
||||
if (property && value) {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(styleObject as any)[property.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const katekMock: TemplateOptions & MultipleChoiceQuestion = {
|
|||
formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
|
||||
choices: [
|
||||
{ formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
||||
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 0 }
|
||||
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
|
||||
],
|
||||
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ exports[`MultipleChoice snapshot test 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 1
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -88,7 +88,7 @@ exports[`MultipleChoice snapshot test 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -180,7 +180,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
|
|||
Choice 1
|
||||
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -205,7 +205,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
|
|||
Choice 2
|
||||
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -229,7 +229,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
|
|||
" for="idmocked-id">
|
||||
<img alt="Sample Image" src="https://via.placeholder.com/150">
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -321,7 +321,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 1
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -345,7 +345,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -436,7 +436,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 1
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -460,7 +460,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -484,7 +484,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
|
|||
" for="idmocked-id">
|
||||
<img alt="Sample Image" src="https://via.placeholder.com/150">
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -577,7 +577,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
|
|||
Choice 1
|
||||
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -602,7 +602,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
|
|||
Choice 2
|
||||
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -626,7 +626,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
|
|||
" for="idmocked-id">
|
||||
<img alt="Sample Image" src="https://via.placeholder.com/150">
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -718,7 +718,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 1
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -733,7 +733,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
|||
|
||||
<div class='multiple-choice-answers-container'>
|
||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
||||
|
||||
<span class="answer-weight-container answer-positive-weight">1%</span>
|
||||
<label style="
|
||||
display: inline-block;
|
||||
padding: 0.2em 0 0.2em 0;
|
||||
|
|
@ -742,15 +742,15 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
margin-right: 0.2rem;
|
||||
|
||||
width: 0.75em;
|
||||
color: hsl(2, 64%, 58%);
|
||||
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>
|
||||
width: 1em;
|
||||
color: hsl(120, 39%, 54%);
|
||||
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>
|
||||
<span class="feedback-container">Correct!</span>
|
||||
</input>
|
||||
</div>
|
||||
|
|
@ -833,7 +833,7 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 1
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -857,7 +857,7 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
|
|||
" for="idmocked-id">
|
||||
Vrai
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -174,7 +174,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
|
|||
" for="idmocked-id">
|
||||
Faux
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -265,7 +265,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
|
|||
" for="idmocked-id">
|
||||
Vrai
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -289,7 +289,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
|
|||
" for="idmocked-id">
|
||||
Faux
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -380,7 +380,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
|
|||
" for="idmocked-id">
|
||||
Vrai
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
@ -404,7 +404,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
|
|||
" for="idmocked-id">
|
||||
Faux
|
||||
</label>
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
<svg style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
import React, { act } from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import ImageGallery from "../../../components/ImageGallery/ImageGallery";
|
||||
import ApiService from "../../../services/ApiService";
|
||||
import { Images } from "../../../Types/Images";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
jest.mock("../../../services/ApiService");
|
||||
|
||||
const mockImages: Images[] = [
|
||||
{ id: "1", file_name: "image1.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content1" },
|
||||
{ id: "2", file_name: "image2.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content2" },
|
||||
{ id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" },
|
||||
];
|
||||
|
||||
beforeAll(() => {
|
||||
global.URL.createObjectURL = jest.fn(() => 'mockedObjectUrl');
|
||||
Object.assign(navigator, {
|
||||
clipboard: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("ImageGallery", () => {
|
||||
let mockHandleDelete: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
(ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 });
|
||||
(ApiService.deleteImage as jest.Mock).mockResolvedValue(true);
|
||||
(ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl');
|
||||
await act(async () => {
|
||||
render(<ImageGallery />);
|
||||
});
|
||||
mockHandleDelete = jest.fn();
|
||||
});
|
||||
|
||||
it("should render images correctly", async () => {
|
||||
await act(async () => {
|
||||
await screen.findByText("Galerie");
|
||||
});
|
||||
|
||||
expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle copy action", async () => {
|
||||
const handleCopyMock = jest.fn();
|
||||
await act(async () => {
|
||||
render(<ImageGallery handleCopy={handleCopyMock} />);
|
||||
});
|
||||
|
||||
const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/));
|
||||
await act(async () => {
|
||||
fireEvent.click(copyButtons[0]);
|
||||
});
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete an image and update the gallery", async () => {
|
||||
const fetchImagesMock = jest.fn().mockResolvedValue({ images: mockImages.filter((image) => image.id !== "1"), total: 2 });
|
||||
|
||||
(ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock);
|
||||
|
||||
await act(async () => {
|
||||
render(<ImageGallery handleDelete={mockHandleDelete} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await screen.findByAltText("Image image1.jpg");
|
||||
});
|
||||
|
||||
const deleteButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-delete-/));
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Voulez-vous supprimer cette image?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const confirmDeleteButton = screen.getByText("Delete");
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmDeleteButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ApiService.deleteImage).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByAltText("Image image1.jpg")).toBeNull();
|
||||
expect(screen.getByText("Image supprimée avec succès!")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should upload an image and display success message", async () => {
|
||||
const importTab = screen.getByRole("tab", { name: /import/i });
|
||||
fireEvent.click(importTab);
|
||||
|
||||
const fileInputs = await screen.findAllByTestId("file-input");
|
||||
const fileInput = fileInputs[1];
|
||||
|
||||
expect(fileInput).toBeInTheDocument();
|
||||
|
||||
const file = new File(["image"], "image.jpg", { type: "image/jpeg" });
|
||||
await userEvent.upload(fileInput, file);
|
||||
|
||||
|
||||
await waitFor(() => screen.getByAltText("Preview"));
|
||||
const previewImage = screen.getByAltText("Preview");
|
||||
|
||||
expect(previewImage).toBeInTheDocument();
|
||||
|
||||
const uploadButton = screen.getByRole('button', { name: /téléverser/i });
|
||||
fireEvent.click(uploadButton);
|
||||
const successMessage = await screen.findByText(/téléversée avec succès/i);
|
||||
expect(successMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close the image preview dialog when close button is clicked", async () => {
|
||||
const imageCard = screen.getByAltText("Image image1.jpg");
|
||||
fireEvent.click(imageCard);
|
||||
|
||||
const dialogImage = await screen.findByAltText("Enlarged view");
|
||||
expect(dialogImage).toBeInTheDocument();
|
||||
|
||||
const closeButton = screen.getByTestId("close-button");
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByAltText("Enlarged view")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show an error message when no file is selected", async () => {
|
||||
const importTab = screen.getByRole("tab", { name: /import/i });
|
||||
fireEvent.click(importTab);
|
||||
const uploadButton = screen.getByRole('button', { name: /téléverser/i });
|
||||
fireEvent.click(uploadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Veuillez choisir une image à téléverser.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from "react";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import ImageGalleryModal from "../../../components/ImageGallery/ImageGalleryModal/ImageGalleryModal";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
jest.mock("../../../components/ImageGallery/ImageGallery", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => <div data-testid="image-gallery" />),
|
||||
}));
|
||||
|
||||
describe("ImageGalleryModal", () => {
|
||||
|
||||
it("renders button correctly", () => {
|
||||
render(<ImageGalleryModal />);
|
||||
|
||||
const button = screen.getByLabelText(/images-open/i);
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the modal when button is clicked", () => {
|
||||
render(<ImageGalleryModal />);
|
||||
|
||||
const button = screen.getByRole("button", { name: /images/i });
|
||||
fireEvent.click(button);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
it("closes the modal when close button is clicked", async () => {
|
||||
render(<ImageGalleryModal />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /images/i }));
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: /close/i });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LiveResults from 'src/components/LiveResults/LiveResults';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
const mockSocket: Socket = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
} as unknown as Socket;
|
||||
|
||||
const mockGiftQuestions = parse(
|
||||
`::Sample Question 1:: Question stem
|
||||
{
|
||||
=Choice 1
|
||||
=Choice 2
|
||||
~Choice 3
|
||||
~Choice 4
|
||||
}
|
||||
|
||||
::Sample Question 2:: Question stem {TRUE}
|
||||
`);
|
||||
|
||||
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
|
||||
if (question.type !== "Category")
|
||||
question.id = (index + 1).toString();
|
||||
const newMockQuestion = question;
|
||||
return { question: newMockQuestion as BaseQuestion };
|
||||
});
|
||||
|
||||
console.log(`mockQuestions: ${JSON.stringify(mockQuestions)}`);
|
||||
|
||||
// each student should have a different score for the tests to pass
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: '1', name: 'Student 1', answers: [] },
|
||||
{ id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: ['Choice 3'], isCorrect: false }, { idQuestion: 2, answer: [true], isCorrect: true}] },
|
||||
{ id: '3', name: 'Student 3', answers: [{ idQuestion: 1, answer: ['Choice 1', 'Choice 2'], isCorrect: true }, { idQuestion: 2, answer: [true], isCorrect: true}] },
|
||||
];
|
||||
|
||||
describe('LiveResults', () => {
|
||||
test('renders the component with questions and students', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(`Q${1}`)).toBeInTheDocument();
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Check if the component renders the students
|
||||
mockStudents.forEach((student) => {
|
||||
expect(screen.getByText(student.name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('toggles the display of usernames', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Check if the usernames are shown again
|
||||
mockStudents.forEach((student) => {
|
||||
expect(screen.getByText(student.name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('calculates and displays the correct student grades', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Check if the student grades are calculated and displayed correctly
|
||||
const getByTextInTableCellBody = (text: string) => {
|
||||
const elements = screen.getAllByText(text); // Get all elements with the specified text
|
||||
return elements.find((element) => element.closest('.MuiTableCell-body')); // don't get the footer element(s)
|
||||
};
|
||||
mockStudents.forEach((student) => {
|
||||
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
|
||||
const element = getByTextInTableCellBody(`${grade.toFixed()} %`);
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('calculates and displays the class average', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Calculate the class average
|
||||
const totalGrades = mockStudents.reduce((total, student) => {
|
||||
return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100);
|
||||
}, 0);
|
||||
const classAverage = totalGrades / mockStudents.length;
|
||||
|
||||
// Check if the class average is displayed correctly
|
||||
const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`);
|
||||
const classAverageElement = classAverageElements.find((element) => {
|
||||
return element.closest('td')?.classList.contains('MuiTableCell-footer');
|
||||
});
|
||||
expect(classAverageElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays the correct answers per question', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check if the correct answers per question are displayed correctly
|
||||
mockQuestions.forEach((_, index) => {
|
||||
const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length;
|
||||
const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100;
|
||||
const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`);
|
||||
const correctAnswersElement = correctAnswersElements.find((element) => {
|
||||
return element.closest('td')?.classList.contains('MuiTableCell-root');
|
||||
});
|
||||
expect(correctAnswersElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -5,33 +5,23 @@ 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
|
||||
{
|
||||
=Choice 1
|
||||
~Choice 2
|
||||
}
|
||||
}`) as MultipleChoiceQuestion[];
|
||||
|
||||
::Sample Question 2:: Question stem
|
||||
{
|
||||
=Choice 1
|
||||
=Choice 2
|
||||
~Choice 3
|
||||
}
|
||||
`) as MultipleChoiceQuestion[];
|
||||
|
||||
const questionWithOneCorrectChoice = questions[0];
|
||||
const questionWithMultipleCorrectChoices = questions[1];
|
||||
const question = questions[0];
|
||||
|
||||
describe('MultipleChoiceQuestionDisplay', () => {
|
||||
const mockHandleOnSubmitAnswer = jest.fn();
|
||||
|
||||
const TestWrapper = ({ showAnswer, question }: { showAnswer: boolean; question: MultipleChoiceQuestion }) => {
|
||||
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => {
|
||||
const [showAnswerState, setShowAnswerState] = useState(showAnswer);
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType) => {
|
||||
const handleOnSubmitAnswer = (answer: string) => {
|
||||
mockHandleOnSubmitAnswer(answer);
|
||||
setShowAnswerState(true);
|
||||
};
|
||||
|
|
@ -47,51 +37,28 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
);
|
||||
};
|
||||
|
||||
const twoChoices = questionWithOneCorrectChoice.choices;
|
||||
const threeChoices = questionWithMultipleCorrectChoices.choices;
|
||||
const choices = question.choices;
|
||||
|
||||
test('renders a question (that has only one correct choice) and its choices', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
beforeEach(() => {
|
||||
render(<TestWrapper showAnswer={false} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText(questionWithOneCorrectChoice.formattedStem.text)).toBeInTheDocument();
|
||||
twoChoices.forEach((choice) => {
|
||||
test('renders the question and choices', () => {
|
||||
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
|
||||
choices.forEach((choice) => {
|
||||
expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('only allows one choice to be selected when question only has one correct answer', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
|
||||
const choiceButton1 = screen.getByText('Choice 1').closest('button');
|
||||
const choiceButton2 = screen.getByText('Choice 2').closest('button');
|
||||
|
||||
if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found');
|
||||
|
||||
// Simulate selecting multiple answers
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton1);
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton2);
|
||||
});
|
||||
|
||||
// Verify that only the last answer is selected
|
||||
expect(choiceButton1.querySelector('.answer-text.selected')).not.toBeInTheDocument();
|
||||
expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not submit when no answer is selected', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
test('submits the selected answer', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
act(() => {
|
||||
|
|
@ -102,68 +69,10 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1']);
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
|
||||
test('renders a question (that has multiple correct choices) and its choices', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
|
||||
expect(screen.getByText(questionWithMultipleCorrectChoices.formattedStem.text)).toBeInTheDocument();
|
||||
threeChoices.forEach((choice) => {
|
||||
expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('allows multiple choices to be selected when question has multiple correct answers', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
|
||||
const choiceButton1 = screen.getByText('Choice 1').closest('button');
|
||||
const choiceButton2 = screen.getByText('Choice 2').closest('button');
|
||||
const choiceButton3 = screen.getByText('Choice 3').closest('button');
|
||||
|
||||
if (!choiceButton1 || !choiceButton2 || !choiceButton3) throw new Error('Choice buttons not found');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton1);
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton2);
|
||||
});
|
||||
|
||||
expect(choiceButton1.querySelector('.answer-text.selected')).toBeInTheDocument();
|
||||
expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument();
|
||||
expect(choiceButton3.querySelector('.answer-text.selected')).not.toBeInTheDocument(); // didn't click
|
||||
|
||||
});
|
||||
|
||||
test('submits multiple selected answers', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
|
||||
const choiceButton1 = screen.getByText('Choice 1').closest('button');
|
||||
const choiceButton2 = screen.getByText('Choice 2').closest('button');
|
||||
|
||||
if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found');
|
||||
|
||||
// Simulate selecting multiple answers
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton1);
|
||||
});
|
||||
act(() => {
|
||||
fireEvent.click(choiceButton2);
|
||||
});
|
||||
|
||||
// Simulate submitting the answers
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
// Verify that the mockHandleOnSubmitAnswer function is called with both answers
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']);
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||
});
|
||||
|
||||
it('should show ✅ next to the correct answer and ❌ next to the wrong answers when showAnswer is true', async () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
@ -179,17 +88,16 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
});
|
||||
|
||||
// Wait for the DOM to update
|
||||
const correctAnswer = screen.getByText("Choice 1").closest('button');
|
||||
expect(correctAnswer).toBeInTheDocument();
|
||||
expect(correctAnswer?.textContent).toContain('✅');
|
||||
const correctAnswer = screen.getByText("Choice 1").closest('button');
|
||||
expect(correctAnswer).toBeInTheDocument();
|
||||
expect(correctAnswer?.textContent).toContain('✅');
|
||||
|
||||
const wrongAnswer1 = screen.getByText("Choice 2").closest('button');
|
||||
expect(wrongAnswer1).toBeInTheDocument();
|
||||
expect(wrongAnswer1?.textContent).toContain('❌');
|
||||
const wrongAnswer1 = screen.getByText("Choice 2").closest('button');
|
||||
expect(wrongAnswer1).toBeInTheDocument();
|
||||
expect(wrongAnswer1?.textContent).toContain('❌');
|
||||
});
|
||||
|
||||
it('should not show ✅ or ❌ when Répondre button is not clicked', async () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
it('should not show ✅ or ❌ when repondre button is not clicked', async () => {
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
@ -209,5 +117,5 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ describe('NumericalQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
|
|
@ -78,7 +77,6 @@ describe('NumericalQuestion Component', () => {
|
|||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]);
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,24 +29,23 @@ describe('Questions Component', () => {
|
|||
render(<QuestionDisplay question={question} {...sampleProps} />);
|
||||
};
|
||||
|
||||
// describe('question type parsing', () => {
|
||||
// it('parses true/false question type correctly', () => {
|
||||
// expect(sampleTrueFalseQuestion.type).toBe('TF');
|
||||
// });
|
||||
describe('question type parsing', () => {
|
||||
it('parses true/false question type correctly', () => {
|
||||
expect(sampleTrueFalseQuestion.type).toBe('TF');
|
||||
});
|
||||
|
||||
// it('parses multiple choice question type correctly', () => {
|
||||
// expect(sampleMultipleChoiceQuestion.type).toBe('MC');
|
||||
// });
|
||||
it('parses multiple choice question type correctly', () => {
|
||||
expect(sampleMultipleChoiceQuestion.type).toBe('MC');
|
||||
});
|
||||
|
||||
// it('parses numerical question type correctly', () => {
|
||||
// expect(sampleNumericalQuestion.type).toBe('Numerical');
|
||||
// });
|
||||
|
||||
// it('parses short answer question type correctly', () => {
|
||||
// expect(sampleShortAnswerQuestion.type).toBe('Short');
|
||||
// });
|
||||
// });
|
||||
it('parses numerical question type correctly', () => {
|
||||
expect(sampleNumericalQuestion.type).toBe('Numerical');
|
||||
});
|
||||
|
||||
it('parses short answer question type correctly', () => {
|
||||
expect(sampleShortAnswerQuestion.type).toBe('Short');
|
||||
});
|
||||
});
|
||||
it('renders correctly for True/False question', () => {
|
||||
renderComponent(sampleTrueFalseQuestion);
|
||||
|
||||
|
|
@ -74,8 +73,7 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['Choice 1']);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||
});
|
||||
|
||||
it('renders correctly for Numerical question', () => {
|
||||
|
|
@ -95,8 +93,7 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([7]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
});
|
||||
|
||||
it('renders correctly for Short Answer question', () => {
|
||||
|
|
@ -120,7 +117,7 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
|
|
@ -61,7 +60,6 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ 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();
|
||||
|
|
@ -17,7 +16,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => {
|
||||
const [showAnswerState, setShowAnswerState] = useState(showAnswer);
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType) => {
|
||||
const handleOnSubmitAnswer = (answer: boolean) => {
|
||||
mockHandleSubmitAnswer(answer);
|
||||
setShowAnswerState(true);
|
||||
};
|
||||
|
|
@ -56,7 +55,6 @@ describe('TrueFalseQuestion Component', () => {
|
|||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly for True', () => {
|
||||
|
|
@ -71,8 +69,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([true]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('submits answer correctly for False', () => {
|
||||
|
|
@ -85,8 +82,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([false]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -115,7 +111,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
expect(wrongAnswer1?.textContent).toContain('❌');
|
||||
});
|
||||
|
||||
it('should not show ✅ or ❌ when Répondre button is not clicked', async () => {
|
||||
it('should not show ✅ or ❌ when repondre button is not clicked', async () => {
|
||||
const choiceButton = screen.getByText('Vrai').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal.tsx';
|
||||
import { QuizType } from '../../../Types/QuizType';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
describe('ShareQuizModal', () => {
|
||||
const mockQuiz: QuizType = {
|
||||
_id: '123',
|
||||
folderId: 'folder-123',
|
||||
folderName: 'Test Folder',
|
||||
userId: 'user-123',
|
||||
title: 'Test Quiz',
|
||||
content: ['Question 1', 'Question 2'],
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// Properly mock the clipboard API
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
writeText: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the share button', () => {
|
||||
render(<ShareQuizModal quiz={mockQuiz} />);
|
||||
expect(screen.getByLabelText('partager quiz')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ShareIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copies the quiz URL to clipboard when share button is clicked', async () => {
|
||||
render(<ShareQuizModal quiz={mockQuiz} />);
|
||||
const shareButton = screen.getByLabelText('partager quiz');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(shareButton);
|
||||
});
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
`${window.location.origin}/teacher/share/${mockQuiz._id}`
|
||||
);
|
||||
|
||||
// Check for feedback dialog content
|
||||
expect(screen.getByText(/L'URL de partage pour le quiz/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockQuiz.title)).toBeInTheDocument();
|
||||
expect(screen.getByText(/a été copiée\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error message when clipboard write fails', async () => {
|
||||
// Override the mock to reject
|
||||
(navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error('Clipboard write failed'));
|
||||
|
||||
render(<ShareQuizModal quiz={mockQuiz} />);
|
||||
const shareButton = screen.getByLabelText('partager quiz');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(shareButton);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Une erreur est survenue lors de la copie de l'URL\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the quiz title in the success message', async () => {
|
||||
render(<ShareQuizModal quiz={mockQuiz} />);
|
||||
const shareButton = screen.getByLabelText('partager quiz');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(shareButton);
|
||||
});
|
||||
|
||||
expect(screen.getByText(mockQuiz.title)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -10,15 +10,14 @@ describe('StudentWaitPage Component', () => {
|
|||
{ id: '1', name: 'User1', answers: new Array<Answer>() },
|
||||
{ id: '2', name: 'User2', answers: new Array<Answer>() },
|
||||
{ id: '3', name: 'User3', answers: new Array<Answer>() },
|
||||
];
|
||||
];
|
||||
|
||||
const mockProps = {
|
||||
const mockProps = {
|
||||
students: mockUsers,
|
||||
launchQuiz: jest.fn(),
|
||||
roomName: 'Test Room',
|
||||
setQuizMode: jest.fn(),
|
||||
setIsRoomSelectionVisible: jest.fn()
|
||||
};
|
||||
};
|
||||
|
||||
test('renders StudentWaitPage with correct content', () => {
|
||||
render(<StudentWaitPage {...mockProps} />);
|
||||
|
|
@ -29,15 +28,16 @@ describe('StudentWaitPage Component', () => {
|
|||
expect(launchButton).toBeInTheDocument();
|
||||
|
||||
mockUsers.forEach((user) => {
|
||||
expect(screen.getByText(user.name)).toBeInTheDocument();
|
||||
expect(screen.getByText(user.name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
||||
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
||||
render(<StudentWaitPage {...mockProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,359 +0,0 @@
|
|||
import { checkIfIsCorrect } from 'src/pages/Teacher/ManageRoom/useRooms';
|
||||
import { HighLowNumericalAnswer, MultipleChoiceQuestion, MultipleNumericalAnswer, NumericalQuestion, RangeNumericalAnswer, ShortAnswerQuestion, SimpleNumericalAnswer, TrueFalseQuestion } from 'gift-pegjs';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
|
||||
describe('checkIfIsCorrect', () => {
|
||||
const mockQuestions: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '1',
|
||||
type: 'MC',
|
||||
choices: [
|
||||
{ isCorrect: true, formattedText: { text: 'Answer1' } },
|
||||
{ isCorrect: true, formattedText: { text: 'Answer2' } },
|
||||
{ isCorrect: false, formattedText: { text: 'Answer3' } },
|
||||
],
|
||||
} as MultipleChoiceQuestion,
|
||||
},
|
||||
];
|
||||
|
||||
test('returns true when all selected answers are correct', () => {
|
||||
const answer: AnswerType = ['Answer1', 'Answer2'];
|
||||
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when some selected answers are incorrect', () => {
|
||||
const answer: AnswerType = ['Answer1', 'Answer3'];
|
||||
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when all correct answers are selected, but one incorrect is also selected', () => {
|
||||
const answer: AnswerType = ['Answer1', 'Answer2', 'Answer3'];
|
||||
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when no answers are selected', () => {
|
||||
const answer: AnswerType = [];
|
||||
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when no correct answers are provided in the question', () => {
|
||||
const mockQuestionsWithNoCorrectAnswers: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '1',
|
||||
type: 'MC',
|
||||
choices: [
|
||||
{ isCorrect: false, formattedText: { text: 'Answer1' } },
|
||||
{ isCorrect: false, formattedText: { text: 'Answer2' } },
|
||||
],
|
||||
} as MultipleChoiceQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = ['Answer1'];
|
||||
const result = checkIfIsCorrect(answer, 1, mockQuestionsWithNoCorrectAnswers);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct true/false answer', () => {
|
||||
const mockQuestionsTF: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '2',
|
||||
type: 'TF',
|
||||
isTrue: true,
|
||||
} as TrueFalseQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [true];
|
||||
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an incorrect true/false answer', () => {
|
||||
const mockQuestionsTF: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '2',
|
||||
type: 'TF',
|
||||
isTrue: true,
|
||||
} as TrueFalseQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [false];
|
||||
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for a true/false question with no answer', () => {
|
||||
const mockQuestionsTF: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '2',
|
||||
type: 'TF',
|
||||
isTrue: true,
|
||||
} as TrueFalseQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [];
|
||||
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct true/false answer when isTrue is false', () => {
|
||||
const mockQuestionsTF: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '3',
|
||||
type: 'TF',
|
||||
isTrue: false, // Correct answer is false
|
||||
} as TrueFalseQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [false];
|
||||
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an incorrect true/false answer when isTrue is false', () => {
|
||||
const mockQuestionsTF: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '3',
|
||||
type: 'TF',
|
||||
isTrue: false, // Correct answer is false
|
||||
} as TrueFalseQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [true];
|
||||
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct short answer', () => {
|
||||
const mockQuestionsShort: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '4',
|
||||
type: 'Short',
|
||||
choices: [
|
||||
{ text: 'CorrectAnswer1' },
|
||||
{ text: 'CorrectAnswer2' },
|
||||
],
|
||||
} as ShortAnswerQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = ['CorrectAnswer1'];
|
||||
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an incorrect short answer', () => {
|
||||
const mockQuestionsShort: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '4',
|
||||
type: 'Short',
|
||||
choices: [
|
||||
{ text: 'CorrectAnswer1' },
|
||||
{ text: 'CorrectAnswer2' },
|
||||
],
|
||||
} as ShortAnswerQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = ['WrongAnswer'];
|
||||
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct short answer with case insensitivity', () => {
|
||||
const mockQuestionsShort: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '4',
|
||||
type: 'Short',
|
||||
choices: [
|
||||
{ text: 'CorrectAnswer1' },
|
||||
{ text: 'CorrectAnswer2' },
|
||||
],
|
||||
} as ShortAnswerQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = ['correctanswer1']; // Lowercase version of the correct answer
|
||||
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for a short answer question with no answer', () => {
|
||||
const mockQuestionsShort: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '4',
|
||||
type: 'Short',
|
||||
choices: [
|
||||
{ text: 'CorrectAnswer1' },
|
||||
{ text: 'CorrectAnswer2' },
|
||||
],
|
||||
} as ShortAnswerQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [];
|
||||
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test('returns true for a correct simple numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '5',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [42]; // User's answer
|
||||
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an incorrect simple numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '5',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [43]; // User's answer
|
||||
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct range numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '6',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [52]; // User's answer within the range (50 ± 5)
|
||||
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an out-of-range numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '6',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [56]; // User's answer outside the range (50 ± 5)
|
||||
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct high-low numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '7',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [95]; // User's answer within the range (90 to 100)
|
||||
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an out-of-range high-low numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '7',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [105]; // User's answer outside the range (90 to 100)
|
||||
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true for a correct multiple numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '8',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{
|
||||
isCorrect: true,
|
||||
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
|
||||
} as MultipleNumericalAnswer,
|
||||
{
|
||||
isCorrect: false,
|
||||
answer: { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
|
||||
formattedFeedback: { text: 'You guessed way too high' },
|
||||
}
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [42]; // User's answer matches the correct multiple numerical answer
|
||||
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for an incorrect multiple numerical answer', () => {
|
||||
const mockQuestionsNumerical: QuestionType[] = [
|
||||
{
|
||||
question: {
|
||||
id: '8',
|
||||
type: 'Numerical',
|
||||
choices: [
|
||||
{
|
||||
type: 'multiple',
|
||||
isCorrect: true,
|
||||
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
|
||||
} as MultipleNumericalAnswer,
|
||||
],
|
||||
} as NumericalQuestion,
|
||||
},
|
||||
];
|
||||
const answer: AnswerType = [43]; // User's answer does not match the correct multiple numerical answer
|
||||
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,382 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter, useNavigate, useParams } from 'react-router-dom';
|
||||
import ManageRoom from 'src/pages/Teacher/ManageRoom/ManageRoom';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import { QuizType } from 'src/Types/QuizType';
|
||||
import webSocketService, { AnswerReceptionFromBackendType } from 'src/services/WebsocketService';
|
||||
import ApiService from 'src/services/ApiService';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { RoomProvider } from 'src/pages/Teacher/ManageRoom/RoomContext';
|
||||
|
||||
jest.mock('src/services/WebsocketService');
|
||||
jest.mock('src/services/ApiService');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
jest.mock('src/pages/Teacher/ManageRoom/RoomContext');
|
||||
|
||||
jest.mock('qrcode.react', () => ({
|
||||
__esModule: true,
|
||||
QRCodeCanvas: ({ value }: { value: string }) => <div data-testid="qr-code">{value}</div>,
|
||||
}));
|
||||
|
||||
const mockSocket = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
} as unknown as Socket;
|
||||
|
||||
const mockQuiz: QuizType = {
|
||||
_id: 'test-quiz-id',
|
||||
title: 'Test Quiz',
|
||||
content: ['::Q1:: Question 1 { =Answer1 ~Answer2 }', '::Q2:: Question 2 { =Answer1 ~Answer2 }'],
|
||||
folderId: 'folder-id',
|
||||
folderName: 'folder-name',
|
||||
userId: 'user-id',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
};
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: '1', name: 'Student 1', answers: [] },
|
||||
{ id: '2', name: 'Student 2', answers: [] },
|
||||
];
|
||||
|
||||
const mockAnswerData: AnswerReceptionFromBackendType = {
|
||||
answer: ['Answer1'],
|
||||
idQuestion: 1,
|
||||
idUser: '1',
|
||||
username: 'Student 1',
|
||||
};
|
||||
|
||||
describe('ManageRoom', () => {
|
||||
const navigate = jest.fn();
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
const mockSetSelectedRoom = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigate);
|
||||
useParamsMock.mockReturnValue({ quizId: 'test-quiz-id', roomName: 'Test Room' });
|
||||
(ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
|
||||
(webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
|
||||
(RoomProvider as jest.Mock).mockReturnValue({
|
||||
selectedRoom: { id: '1', title: 'Test Room' },
|
||||
setSelectedRoom: mockSetSelectedRoom,
|
||||
});
|
||||
});
|
||||
|
||||
test('prepares to launch quiz and fetches quiz data', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
|
||||
});
|
||||
|
||||
const launchButton = screen.getByText('Lancer');
|
||||
fireEvent.click(launchButton);
|
||||
|
||||
const rythmeButton = screen.getByText('Rythme du professeur');
|
||||
fireEvent.click(rythmeButton);
|
||||
|
||||
const secondLaunchButton = screen.getAllByText('Lancer');
|
||||
fireEvent.click(secondLaunchButton[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Quiz')).toBeInTheDocument();
|
||||
|
||||
const roomHeader = document.querySelector('h1');
|
||||
expect(roomHeader).toHaveTextContent('Salle : TEST ROOM');
|
||||
|
||||
expect(screen.getByText('0/60')).toBeInTheDocument();
|
||||
expect(screen.getByText('Question 1/2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles create-success event', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Salle\s*:\s*Test Room/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles user-joined event', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
|
||||
userJoinedCallback(mockStudents[0]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Student 1')).toBeInTheDocument();
|
||||
|
||||
});
|
||||
|
||||
const launchButton = screen.getByText('Lancer');
|
||||
fireEvent.click(launchButton);
|
||||
|
||||
const rythmeButton = screen.getByText('Rythme du professeur');
|
||||
fireEvent.click(rythmeButton);
|
||||
|
||||
const secondLaunchButton = screen.getAllByText('Lancer');
|
||||
fireEvent.click(secondLaunchButton[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1/60')).toBeInTheDocument();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('handles next question', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Lancer'));
|
||||
fireEvent.click(screen.getByText('Rythme du professeur'));
|
||||
fireEvent.click(screen.getAllByText('Lancer')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.debug();
|
||||
});
|
||||
|
||||
const nextQuestionButton = await screen.findByRole('button', { name: /Prochaine question/i });
|
||||
expect(nextQuestionButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(nextQuestionButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Question 2/2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('handles disconnect', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
const disconnectButton = screen.getByText('Quitter');
|
||||
fireEvent.click(disconnectButton);
|
||||
|
||||
const confirmButton = screen.getAllByText('Confirmer');
|
||||
fireEvent.click(confirmButton[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webSocketService.disconnect).toHaveBeenCalled();
|
||||
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
test('handles submit-answer-room event', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log');
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
const launchButton = screen.getByText('Lancer');
|
||||
fireEvent.click(launchButton);
|
||||
|
||||
const rythmeButton = screen.getByText('Rythme du professeur');
|
||||
fireEvent.click(rythmeButton);
|
||||
|
||||
const secondLaunchButton = screen.getAllByText('Lancer');
|
||||
fireEvent.click(secondLaunchButton[1]);
|
||||
|
||||
await act(async () => {
|
||||
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
|
||||
userJoinedCallback(mockStudents[0]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1];
|
||||
submitAnswerCallback(mockAnswerData);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// console.info(consoleSpy.mock.calls);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Received answer from Student 1 for question 1: Answer1'
|
||||
);
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('vide la liste des étudiants après déconnexion', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
|
||||
userJoinedCallback(mockStudents[0]);
|
||||
});
|
||||
|
||||
const disconnectButton = screen.getByText('Quitter');
|
||||
fireEvent.click(disconnectButton);
|
||||
|
||||
const confirmButton = screen.getAllByText('Confirmer');
|
||||
fireEvent.click(confirmButton[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Student 1')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('terminates the quiz and navigates to teacher dashboard when the "Terminer le quiz" button is clicked', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ManageRoom />
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
|
||||
createSuccessCallback('Test Room');
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Lancer'));
|
||||
fireEvent.click(screen.getByText('Rythme du professeur'));
|
||||
fireEvent.click(screen.getAllByText('Lancer')[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Quiz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const finishQuizButton = screen.getByText('Terminer le quiz');
|
||||
fireEvent.click(finishQuizButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
test("Affiche la modale QR Code lorsqu’on clique sur le bouton", async () => {
|
||||
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
|
||||
|
||||
const button = screen.getByRole('button', { name: /lien de participation/i });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByRole('heading', { name: /Rejoindre la salle/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Scannez ce QR code ou partagez le lien ci-dessous/i)).toBeInTheDocument();
|
||||
expect(screen.getByTestId('qr-code')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Ferme la modale QR Code lorsqu’on clique sur le bouton Fermer", async () => {
|
||||
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /fermer/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('Affiche le bon lien de participation', () => {
|
||||
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
|
||||
|
||||
const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`;
|
||||
expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl);
|
||||
});
|
||||
|
||||
test('Vérifie que le QR code contient la bonne URL', () => {
|
||||
render(<MemoryRouter><ManageRoom /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /lien de participation/i }));
|
||||
|
||||
const roomUrl = `${window.location.origin}/student/join-room?roomName=Test Room`;
|
||||
expect(screen.getByTestId('qr-code')).toHaveTextContent(roomUrl);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -5,10 +5,9 @@ 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 ~Option C}
|
||||
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
|
||||
|
||||
::Sample Question 2:: Sample Question 2 {T}`);
|
||||
|
||||
|
|
@ -16,7 +15,7 @@ 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();
|
||||
|
|
@ -27,12 +26,10 @@ beforeEach(() => {
|
|||
<MemoryRouter>
|
||||
<StudentModeQuiz
|
||||
questions={mockQuestions}
|
||||
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
|
||||
submitAnswer={mockSubmitAnswer}
|
||||
disconnectWebSocket={mockDisconnectWebSocket}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
</MemoryRouter>);
|
||||
});
|
||||
|
||||
describe('StudentModeQuiz', () => {
|
||||
|
|
@ -51,50 +48,7 @@ describe('StudentModeQuiz', () => {
|
|||
fireEvent.click(screen.getByText('Répondre'));
|
||||
});
|
||||
|
||||
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, 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();
|
||||
|
||||
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
|
||||
});
|
||||
|
||||
test('handles quit button click', async () => {
|
||||
|
|
@ -116,33 +70,11 @@ describe('StudentModeQuiz', () => {
|
|||
fireEvent.click(screen.getByText('Question suivante'));
|
||||
});
|
||||
|
||||
expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||
const sampleQuestionElements = screen.queryAllByText(/Sample question 2/i);
|
||||
expect(sampleQuestionElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('V')).toBeInTheDocument();
|
||||
|
||||
});
|
||||
|
||||
// le test suivant est fait dans MultipleChoiceQuestionDisplay.test.tsx
|
||||
// test('allows multiple answers to be selected for a question', async () => {
|
||||
// // Simulate selecting multiple answers
|
||||
// act(() => {
|
||||
// fireEvent.click(screen.getByText('Option A'));
|
||||
// });
|
||||
// act(() => {
|
||||
// fireEvent.click(screen.getByText('Option B'));
|
||||
// });
|
||||
|
||||
// // Simulate submitting the answers
|
||||
// act(() => {
|
||||
// fireEvent.click(screen.getByText('Répondre'));
|
||||
// });
|
||||
|
||||
// // Verify that the mockSubmitAnswer function is called with both answers
|
||||
// expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1);
|
||||
|
||||
// // Verify that the selected answers are displayed as selected
|
||||
// const buttonA = screen.getByRole('button', { name: '✅ A Option A' });
|
||||
// const buttonB = screen.getByRole('button', { name: '✅ B Option B' });
|
||||
// expect(buttonA).toBeInTheDocument();
|
||||
// expect(buttonB).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,52 +3,41 @@ import React from 'react';
|
|||
import { render, fireEvent, act } from '@testing-library/react';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom';
|
||||
import { BaseQuestion, MultipleChoiceQuestion, parse } from 'gift-pegjs';
|
||||
import { MultipleChoiceQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
|
||||
// import { mock } from 'node:test';
|
||||
|
||||
const mockGiftQuestions = parse(
|
||||
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
|
||||
`::Sample Question:: Sample Question {=Option A ~Option B}`);
|
||||
|
||||
::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', () => {
|
||||
it ('renders the initial question as MultipleChoiceQuestion', () => {
|
||||
expect(mockGiftQuestions[0].type).toBe('MC');
|
||||
});
|
||||
|
||||
|
||||
let mockQuestion = mockQuestions[0].question as MultipleChoiceQuestion;
|
||||
const mockQuestion = mockGiftQuestions[0] as MultipleChoiceQuestion;
|
||||
mockQuestion.id = '1';
|
||||
|
||||
const mockSubmitAnswer = jest.fn();
|
||||
const mockDisconnectWebSocket = jest.fn();
|
||||
|
||||
let rerender: (ui: React.ReactElement) => void;
|
||||
|
||||
beforeEach(async () => {
|
||||
const utils = render(
|
||||
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 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||
expect(screen.getByText('Quitter')).toBeInTheDocument();
|
||||
|
|
@ -63,52 +52,8 @@ describe('TeacherModeQuiz', () => {
|
|||
act(() => {
|
||||
fireEvent.click(screen.getByText('Répondre'));
|
||||
});
|
||||
expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
|
||||
mockSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
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);
|
||||
mockSubmitAnswer.mockClear();
|
||||
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();
|
||||
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
|
||||
expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('handles disconnect button click', () => {
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom';
|
||||
import Share from '../../../../pages/Teacher/Share/Share.tsx';
|
||||
import ApiService from '../../../../services/ApiService';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock the ApiService and react-router-dom
|
||||
jest.mock('../../../../services/ApiService');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Share Component', () => {
|
||||
const mockNavigate = jest.fn();
|
||||
const mockUseParams = useParams as jest.Mock;
|
||||
const mockApiService = ApiService as jest.Mocked<typeof ApiService>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseParams.mockReturnValue({ id: 'quiz123' });
|
||||
require('react-router-dom').useNavigate.mockReturnValue(mockNavigate);
|
||||
});
|
||||
|
||||
const renderComponent = (initialEntries = ['/share/quiz123']) => {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Routes>
|
||||
<Route path="/share/:id" element={<Share />} />
|
||||
<Route path="/teacher/dashboard" element={<div>Dashboard</div>} />
|
||||
<Route path="/login" element={<div>Login</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('should show loading state initially', () => {
|
||||
mockApiService.getAllQuizIds.mockResolvedValue([]);
|
||||
mockApiService.getUserFolders.mockResolvedValue([]);
|
||||
mockApiService.getSharedQuiz.mockResolvedValue('Test Quiz');
|
||||
|
||||
renderComponent();
|
||||
expect(screen.getByText('Chargement...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should redirect to login if not authenticated', async () => {
|
||||
mockApiService.isLoggedIn.mockReturnValue(false);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "quiz already exists" message when quiz exists', async () => {
|
||||
mockApiService.isLoggedIn.mockReturnValue(true);
|
||||
mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Quiz déjà existant')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Le quiz que vous essayez d'importer existe déjà sur votre compte/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Retour au tableau de bord')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to dashboard when clicking return button in "quiz exists" view', async () => {
|
||||
mockApiService.isLoggedIn.mockReturnValue(true);
|
||||
mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Retour au tableau de bord'));
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error when no folders exist', async () => {
|
||||
mockApiService.isLoggedIn.mockReturnValue(true);
|
||||
mockApiService.getAllQuizIds.mockResolvedValue([]);
|
||||
mockApiService.getUserFolders.mockResolvedValue([]);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import ApiService from '../../services/ApiService';
|
||||
import { ENV_VARIABLES } from '../../constants';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('getSharedQuiz', () => {
|
||||
it('should call the API to get a shared quiz and return the quiz data on success', async () => {
|
||||
const quizId = '123';
|
||||
const quizData = 'Quiz data';
|
||||
const response = { status: 200, data: { data: quizData } };
|
||||
mockedAxios.get.mockResolvedValue(response);
|
||||
|
||||
const result = await ApiService.getSharedQuiz(quizId);
|
||||
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
`${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/getShare/${quizId}`,
|
||||
{ headers: expect.any(Object) }
|
||||
);
|
||||
expect(result).toBe(quizData);
|
||||
});
|
||||
|
||||
it('should return an error message if the API call fails', async () => {
|
||||
const quizId = '123';
|
||||
const errorMessage = 'An unexpected error occurred.';
|
||||
mockedAxios.get.mockRejectedValue({ response: { data: { error: errorMessage } } });
|
||||
|
||||
const result = await ApiService.getSharedQuiz(quizId);
|
||||
|
||||
expect(result).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('should return a generic error message if an unexpected error occurs', async () => {
|
||||
const quizId = '123';
|
||||
mockedAxios.get.mockRejectedValue(new Error('Unexpected error'));
|
||||
|
||||
const result = await ApiService.getSharedQuiz(quizId);
|
||||
|
||||
expect(result).toBe('An unexpected error occurred.');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
//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');
|
||||
|
||||
|
|
@ -25,13 +23,13 @@ describe('WebSocketService', () => {
|
|||
});
|
||||
|
||||
test('connect should initialize socket connection', () => {
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
expect(io).toHaveBeenCalled();
|
||||
expect(WebsocketService['socket']).toBe(mockSocket);
|
||||
});
|
||||
|
||||
test('disconnect should terminate socket connection', () => {
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
expect(WebsocketService['socket']).toBeTruthy();
|
||||
WebsocketService.disconnect();
|
||||
expect(mockSocket.disconnect).toHaveBeenCalled();
|
||||
|
|
@ -39,24 +37,17 @@ describe('WebSocketService', () => {
|
|||
});
|
||||
|
||||
test('createRoom should emit create-room event', () => {
|
||||
const roomName = 'Test Room';
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
WebsocketService.createRoom(roomName);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room', roomName);
|
||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
WebsocketService.createRoom();
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||
});
|
||||
|
||||
test('nextQuestion should emit next-question event with correct parameters', () => {
|
||||
const roomName = 'testRoom';
|
||||
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, questions: mockQuestions, questionIndex: 0, isLaunch: false});
|
||||
const question = mockQuestions[0];
|
||||
const question = { id: 1, text: 'Sample Question' };
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
WebsocketService.nextQuestion(roomName, question);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
|
||||
});
|
||||
|
||||
|
|
@ -64,7 +55,7 @@ describe('WebSocketService', () => {
|
|||
const roomName = 'testRoom';
|
||||
const questions = [{ id: 1, text: 'Sample Question' }];
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
WebsocketService.launchStudentModeQuiz(roomName, questions);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
|
||||
roomName,
|
||||
|
|
@ -75,7 +66,7 @@ describe('WebSocketService', () => {
|
|||
test('endQuiz should emit end-quiz event with correct parameters', () => {
|
||||
const roomName = 'testRoom';
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
WebsocketService.endQuiz(roomName);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
|
||||
});
|
||||
|
|
@ -84,7 +75,7 @@ describe('WebSocketService', () => {
|
|||
const enteredRoomName = 'testRoom';
|
||||
const username = 'testUser';
|
||||
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
WebsocketService.joinRoom(enteredRoomName, username);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import ApiService from '../../services/ApiService';
|
||||
import { FolderType } from '../../Types/FolderType';
|
||||
import { QuizType } from '../../Types/QuizType';
|
||||
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('ApiService', () => {
|
||||
describe('getAllQuizIds', () => {
|
||||
it('should return all quiz IDs from all folders', async () => {
|
||||
const folders: FolderType[] = [
|
||||
{ _id: 'folder1', title: 'Folder 1', userId: 'user1', created_at: new Date().toISOString() },
|
||||
{ _id: 'folder2', title: 'Folder 2', userId: 'user2', created_at: new Date().toISOString() }
|
||||
];
|
||||
const quizzesFolder1: QuizType[] = [
|
||||
{ _id: 'quiz1', title: 'Quiz 1', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() },
|
||||
{ _id: 'quiz2', title: 'Quiz 2', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() }
|
||||
];
|
||||
const quizzesFolder2: QuizType[] = [
|
||||
{ _id: 'quiz3', title: 'Quiz 3', content: [], folderId: 'folder2', folderName: 'Folder 2', userId: 'user2', created_at: new Date(), updated_at: new Date() }
|
||||
];
|
||||
|
||||
mockedAxios.get
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: folders } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder1 } })
|
||||
.mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder2 } });
|
||||
|
||||
const result = await ApiService.getAllQuizIds();
|
||||
|
||||
expect(result).toEqual(['quiz1', 'quiz2', 'quiz3']);
|
||||
});
|
||||
|
||||
it('should return an empty array if no folders are found', async () => {
|
||||
mockedAxios.get.mockResolvedValueOnce({ status: 200, data: { data: [] } });
|
||||
|
||||
const result = await ApiService.getAllQuizIds();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -21,11 +21,11 @@ const GiftCheatSheet: React.FC = () => {
|
|||
};
|
||||
|
||||
|
||||
const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
|
||||
const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
|
||||
const QuestionChoixMulMany = "::Villes canadiennes:: \n Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
|
||||
const QuestionCourte ="::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
|
||||
const QuestionNum ="::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
|
||||
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
|
||||
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa";
|
||||
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
|
||||
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
|
||||
const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
|
||||
return (
|
||||
<div className="gift-cheat-sheet">
|
||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||
|
|
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="question-type">
|
||||
<h4> 5. Questions numériques </h4>
|
||||
<h4> 5. Question numérique </h4>
|
||||
<pre>
|
||||
<code className="question-code-block selectable-text">
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// GIFTTemplatePreview.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Template, { ErrorTemplate, UnsupportedQuestionTypeError } from './templates';
|
||||
import Template, { ErrorTemplate } from './templates';
|
||||
import { parse } from 'gift-pegjs';
|
||||
import './styles.css';
|
||||
import { FormattedTextTemplate } from './templates/TextTypeTemplate';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface GIFTTemplatePreviewProps {
|
||||
questions: string[];
|
||||
|
|
@ -22,6 +22,19 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
|||
try {
|
||||
let previewHTML = '';
|
||||
questions.forEach((giftQuestion) => {
|
||||
// TODO : afficher un message que les images spécifiées par <img> sont dépréciées et qu'il faut utiliser [markdown] et la syntaxe 
|
||||
|
||||
// const isImage = item.includes('<img');
|
||||
// if (isImage) {
|
||||
// const imageUrlMatch = item.match(/<img[^>]+>/i);
|
||||
// if (imageUrlMatch) {
|
||||
// let imageUrl = imageUrlMatch[0];
|
||||
// imageUrl = imageUrl.replace('img', 'img style="width:10vw;" src=');
|
||||
// item = item.replace(imageUrlMatch[0], '');
|
||||
// previewHTML += `${imageUrl}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
try {
|
||||
const question = parse(giftQuestion);
|
||||
previewHTML += Template(question[0], {
|
||||
|
|
@ -29,15 +42,11 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
|||
theme: 'light'
|
||||
});
|
||||
} catch (error) {
|
||||
let errorMsg: string;
|
||||
if (error instanceof UnsupportedQuestionTypeError) {
|
||||
errorMsg = ErrorTemplate(giftQuestion, `Erreur: ${error.message}`);
|
||||
} else if (error instanceof Error) {
|
||||
errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`);
|
||||
if (error instanceof Error) {
|
||||
previewHTML += ErrorTemplate(giftQuestion + '\n' + error.message);
|
||||
} else {
|
||||
errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
|
||||
previewHTML += ErrorTemplate(giftQuestion + '\n' + 'Erreur inconnue');
|
||||
}
|
||||
previewHTML += `<div label="error-message">${errorMsg}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -65,8 +74,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
|||
<div className="error">{error}</div>
|
||||
) : isPreviewReady ? (
|
||||
<div data-testid="preview-container">
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="loading">Chargement de la prévisualisation...</div>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ What is the capital of Canada? {=Canada -> Ottawa =Italy -> Rome =Japan -> Tokyo
|
|||
|
||||
|
||||
const items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
|
||||
const errorItemDark = ErrorTemplate('Hello', 'Error');
|
||||
const errorItemDark = ErrorTemplate('Hello');
|
||||
|
||||
const lightItems = multiple.map((item) => Template(item, { theme: 'light' })).join('');
|
||||
|
||||
const errorItem = ErrorTemplate('Hello', 'Error');
|
||||
const errorItem = ErrorTemplate('Hello');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.innerHTML = items + errorItemDark + lightItems + errorItem;
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ export default function AnswerIcon({ correct }: AnswerIconOptions): string {
|
|||
`;
|
||||
|
||||
const CorrectIcon = (): string => {
|
||||
return `<svg data-testid="correct-icon" style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`;
|
||||
return `<svg style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`;
|
||||
};
|
||||
|
||||
const IncorrectIcon = (): string => {
|
||||
return `<svg data-testid="incorrect-icon" style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`;
|
||||
return `<svg style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`;
|
||||
};
|
||||
|
||||
return correct ? CorrectIcon() : IncorrectIcon();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { theme, ParagraphStyle } from '../constants';
|
||||
import { state } from '.';
|
||||
|
||||
export default function (questionText: string, errorText: string): string {
|
||||
export default function (text: string): string {
|
||||
const Container = `
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
|
@ -13,49 +13,47 @@ export default function (questionText: string, errorText: string): string {
|
|||
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
|
||||
`;
|
||||
|
||||
// const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/);
|
||||
// return document[0] !== ``
|
||||
// ? `<section style="${Container}">${document
|
||||
// .map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`)
|
||||
// .join('')}</section>`
|
||||
// : ``;
|
||||
return `<section style="${Container}"><p style="${ParagraphStyle(state.theme)}">${questionText}<br><em>${errorText}</em></p></section>`;
|
||||
const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/);
|
||||
return document[0] !== ``
|
||||
? `<section style="${Container}">${document
|
||||
.map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`)
|
||||
.join('')}</section>`
|
||||
: ``;
|
||||
}
|
||||
|
||||
// function documentRegex(text: string): string {
|
||||
// const newText = text
|
||||
// .split(/\r?\n/)
|
||||
// .map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, ''))
|
||||
// .join('');
|
||||
function documentRegex(text: string): string {
|
||||
const newText = text
|
||||
.split(/\r?\n/)
|
||||
.map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, ''))
|
||||
.join('');
|
||||
|
||||
// const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g;
|
||||
// const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g;
|
||||
// const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g;
|
||||
const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g;
|
||||
const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g;
|
||||
const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g;
|
||||
|
||||
// return newText
|
||||
// .replace(newLineAnswer, `\n$2`)
|
||||
// .replace(correctAnswer, `$1<li>$4</li>`)
|
||||
// .replace(incorrectAnswer, `$1<li>$4</li>`);
|
||||
// }
|
||||
return newText
|
||||
.replace(newLineAnswer, `\n$2`)
|
||||
.replace(correctAnswer, `$1<li>$4</li>`)
|
||||
.replace(incorrectAnswer, `$1<li>$4</li>`);
|
||||
}
|
||||
|
||||
// function lineRegex(text: string): string {
|
||||
// return text
|
||||
// // CPF: disabled the following regex because it's not clear what it's supposed to do
|
||||
// // .split(/\r?\n/)
|
||||
// // .map((category) =>
|
||||
// // category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`)
|
||||
// // )
|
||||
// // .map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`))
|
||||
// // .map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`))
|
||||
// // .map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`))
|
||||
// // .join('');
|
||||
// }
|
||||
function lineRegex(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((category) =>
|
||||
category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`)
|
||||
)
|
||||
.map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`))
|
||||
.map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`))
|
||||
.map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`))
|
||||
.join('');
|
||||
}
|
||||
|
||||
// function removeBackslash(text: string): string {
|
||||
// return text
|
||||
// .split(/\r?\n/)
|
||||
// .map((colon) => colon.replace(/[\\]:/g, ':'))
|
||||
// .map((openBracket) => openBracket.replace(/[\\]{/g, '{'))
|
||||
// .map((closeBracket) => closeBracket.replace(/[\\]}/g, '}'))
|
||||
// .join('');
|
||||
// }
|
||||
function removeBackslash(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((colon) => colon.replace(/[\\]:/g, ':'))
|
||||
.map((openBracket) => openBracket.replace(/[\\]{/g, '{'))
|
||||
.map((closeBracket) => closeBracket.replace(/[\\]}/g, '}'))
|
||||
.join('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ type AnswerFeedbackOptions = TemplateOptions & Pick<TextChoice, 'formattedFeedba
|
|||
interface AnswerWeightOptions extends TemplateOptions {
|
||||
weight: TextChoice['weight'];
|
||||
}
|
||||
// careful -- this template is re-used by True/False questions!
|
||||
|
||||
export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoiceAnswerOptions) {
|
||||
const id = `id${nanoid(8)}`;
|
||||
|
||||
const hasManyCorrectChoices = choices.filter(({ isCorrect }) => isCorrect === true).length > 1;
|
||||
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0;
|
||||
|
||||
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
|
||||
hasManyCorrectChoices ? ` ou plusieurs` : ``
|
||||
isMultipleAnswer ? ` ou plusieurs` : ``
|
||||
}:</span>`;
|
||||
const result = choices
|
||||
.map(({ weight, isCorrect, formattedText, formattedFeedback }) => {
|
||||
|
|
@ -32,12 +32,12 @@ export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoic
|
|||
const inputId = `id${nanoid(6)}`;
|
||||
|
||||
const isPositiveWeight = (weight != undefined) && (weight > 0);
|
||||
const isCorrectOption = hasManyCorrectChoices ? isPositiveWeight || isCorrect : isCorrect;
|
||||
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
|
||||
|
||||
return `
|
||||
<div class='multiple-choice-answers-container'>
|
||||
<input class="gift-input" type="${
|
||||
hasManyCorrectChoices ? 'checkbox' : 'radio'
|
||||
isMultipleAnswer ? 'checkbox' : 'radio'
|
||||
}" id="${inputId}" name="${id}">
|
||||
${AnswerWeight({ weight: weight })}
|
||||
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
|
||||
|
|
|
|||
|
|
@ -4,24 +4,14 @@ import katex from 'katex';
|
|||
import { TextFormat } from 'gift-pegjs';
|
||||
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
|
||||
|
||||
function formatLatex(text: string): string {
|
||||
|
||||
let renderedText = '';
|
||||
|
||||
try {
|
||||
renderedText = text
|
||||
export function formatLatex(text: string): string {
|
||||
return text
|
||||
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||
.replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false }))
|
||||
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
|
||||
katex.renderToString(inner, { displayMode: false })
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Error rendering LaTeX (KaTeX):', error);
|
||||
renderedText = text;
|
||||
}
|
||||
|
||||
return renderedText;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,32 +6,20 @@ import {
|
|||
MultipleChoiceQuestion as MultipleChoiceType,
|
||||
NumericalQuestion as NumericalType,
|
||||
ShortAnswerQuestion as ShortAnswerType,
|
||||
// EssayQuestion as EssayType,
|
||||
// Essay as EssayType,
|
||||
TrueFalseQuestion as TrueFalseType,
|
||||
// MatchingQuestion as MatchingType,
|
||||
} from 'gift-pegjs';
|
||||
import { DisplayOptions } from './types';
|
||||
// import DescriptionTemplate from './DescriptionTemplate';
|
||||
// import EssayTemplate from './EssayTemplate';
|
||||
// import MatchingTemplate from './MatchingTemplate';
|
||||
import DescriptionTemplate from './DescriptionTemplate';
|
||||
import EssayTemplate from './EssayTemplate';
|
||||
import MatchingTemplate from './MatchingTemplate';
|
||||
import MultipleChoiceTemplate from './MultipleChoiceTemplate';
|
||||
import NumericalTemplate from './NumericalTemplate';
|
||||
import ShortAnswerTemplate from './ShortAnswerTemplate';
|
||||
import TrueFalseTemplate from './TrueFalseTemplate';
|
||||
import Error from './ErrorTemplate';
|
||||
// import CategoryTemplate from './CategoryTemplate';
|
||||
|
||||
export class UnsupportedQuestionTypeError extends globalThis.Error {
|
||||
constructor(type: string) {
|
||||
const userFriendlyType = (type === 'Essay') ? 'Réponse longue (Essay)'
|
||||
: (type === 'Matching') ? 'Association (Matching)'
|
||||
: (type === 'Category') ? 'Catégorie (Category)'
|
||||
: type;
|
||||
super(`Les questions du type ${userFriendlyType} ne sont pas supportées.`);
|
||||
this.name = 'UnsupportedQuestionTypeError';
|
||||
}
|
||||
}
|
||||
|
||||
import CategoryTemplate from './CategoryTemplate';
|
||||
|
||||
export const state: DisplayOptions = { preview: true, theme: 'light' };
|
||||
|
||||
|
|
@ -66,21 +54,23 @@ export default function Template(
|
|||
// case 'Matching':
|
||||
// return Matching({ ...(keys as MatchingType) });
|
||||
default:
|
||||
// convert type to human-readable string
|
||||
throw new UnsupportedQuestionTypeError(type); }
|
||||
// TODO: throw error for unsupported question types?
|
||||
// throw new Error(`Unsupported question type: ${type}`);
|
||||
return ``;
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorTemplate(questionText: string, errorText: string, options?: Partial<DisplayOptions>): string {
|
||||
export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>): string {
|
||||
Object.assign(state, options);
|
||||
|
||||
return Error(questionText, errorText);
|
||||
return Error(text);
|
||||
}
|
||||
|
||||
export {
|
||||
// CategoryTemplate,
|
||||
// DescriptionTemplate as Description,
|
||||
// EssayTemplate as Essay,
|
||||
// MatchingTemplate as Matching,
|
||||
CategoryTemplate,
|
||||
DescriptionTemplate as Description,
|
||||
EssayTemplate as Essay,
|
||||
MatchingTemplate as Matching,
|
||||
MultipleChoiceTemplate as MultipleChoice,
|
||||
NumericalTemplate as Numerical,
|
||||
ShortAnswerTemplate as ShortAnswer,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as React from 'react';
|
||||
import './header.css';
|
||||
import { Button } from '@mui/material';
|
||||
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
||||
|
||||
interface HeaderProps {
|
||||
isLoggedIn: boolean;
|
||||
isLoggedIn: () => boolean;
|
||||
handleLogout: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
|
|||
onClick={() => navigate('/')}
|
||||
/>
|
||||
|
||||
{isLoggedIn && (
|
||||
{isLoggedIn() && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
|
|
@ -29,19 +28,10 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
|
|||
handleLogout();
|
||||
navigate('/');
|
||||
}}
|
||||
startIcon={<ExitToAppIcon />}
|
||||
>
|
||||
Déconnexion
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isLoggedIn && (
|
||||
<div className="auth-selection-btn">
|
||||
<Link to="/login">
|
||||
<button className="auth-btn">Connexion</button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,309 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Button,
|
||||
IconButton,
|
||||
Card,
|
||||
CardContent,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
DialogContentText,
|
||||
Tabs,
|
||||
Tab,
|
||||
TextField, Snackbar,
|
||||
Alert
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { ImageType } from "../../Types/ImageType";
|
||||
import ApiService from "../../services/ApiService";
|
||||
import { Upload } from "@mui/icons-material";
|
||||
import { escapeForGIFT } from "src/utils/giftUtils";
|
||||
import { ENV_VARIABLES } from "src/constants";
|
||||
|
||||
interface ImagesProps {
|
||||
handleCopy?: (id: string) => void;
|
||||
handleDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
|
||||
const [images, setImages] = useState<ImageType[]>([]);
|
||||
const [totalImg, setTotalImg] = useState(0);
|
||||
const [imgPage, setImgPage] = useState(1);
|
||||
const [imgLimit] = useState(6);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<ImageType | null>(null);
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [imageToDelete, setImageToDelete] = useState<ImageType | null>(null);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [importedImage, setImportedImage] = useState<File | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success");
|
||||
|
||||
const fetchImages = async () => {
|
||||
setLoading(true);
|
||||
const data = await ApiService.getUserImages(imgPage, imgLimit);
|
||||
setImages(data.images);
|
||||
setTotalImg(data.total);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchImages();
|
||||
}, [imgPage]);
|
||||
|
||||
const defaultHandleDelete = async (id: string) => {
|
||||
if (imageToDelete) {
|
||||
setLoading(true);
|
||||
const isDeleted = await ApiService.deleteImage(id);
|
||||
setLoading(false);
|
||||
|
||||
if (isDeleted) {
|
||||
setImgPage(1);
|
||||
fetchImages();
|
||||
setSnackbarMessage("Image supprimée avec succès!");
|
||||
setSnackbarSeverity("success");
|
||||
} else {
|
||||
setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer.");
|
||||
setSnackbarSeverity("error");
|
||||
}
|
||||
|
||||
setSnackbarOpen(true);
|
||||
setSelectedImage(null);
|
||||
setImageToDelete(null);
|
||||
setOpenDeleteDialog(false);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultHandleCopy = (id: string) => {
|
||||
if (navigator.clipboard) {
|
||||
const link = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`;
|
||||
const imgTag = `[markdown] } "texte de l'infobulle (ne fonctionne pas sur écran tactile généralement)") `;
|
||||
setSnackbarMessage("Le lien Markdown de l'image a été copié dans le presse-papiers");
|
||||
setSnackbarSeverity("success");
|
||||
setSnackbarOpen(true);
|
||||
navigator.clipboard.writeText(imgTag);
|
||||
}
|
||||
if(handleCopy) {
|
||||
handleCopy(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFunction = handleDelete || defaultHandleDelete;
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files ? event.target.files[0] : null;
|
||||
setImportedImage(file);
|
||||
if (file) {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setPreview(objectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
if (!importedImage) {
|
||||
setSnackbarMessage("Veuillez choisir une image à téléverser.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = await ApiService.uploadImage(importedImage);
|
||||
|
||||
if (imageUrl.includes("ERROR")) {
|
||||
setSnackbarMessage("Une erreur est survenue. Veuillez réessayer plus tard.");
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
fetchImages();
|
||||
|
||||
setSnackbarMessage("Téléversée avec succès !");
|
||||
setSnackbarSeverity("success");
|
||||
setSnackbarOpen(true);
|
||||
|
||||
setImportedImage(null);
|
||||
setPreview(null);
|
||||
setTabValue(0);
|
||||
} catch (error) {
|
||||
setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`);
|
||||
setSnackbarSeverity("error");
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}>
|
||||
<Tab label="Galerie" />
|
||||
<Tab label="Import" />
|
||||
</Tabs>
|
||||
{tabValue === 0 && (
|
||||
<>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={200}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box display="grid" gridTemplateColumns="repeat(3, 1fr)" gridTemplateRows="repeat(2, 1fr)" gap={2} maxWidth="900px" margin="auto">
|
||||
{images.map((obj) => (
|
||||
<Card key={obj.id} sx={{ cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center" }} onClick={() => setSelectedImage(obj)}>
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
<img
|
||||
src={`data:${obj.mime_type};base64,${obj.file_content}`}
|
||||
alt={`Image ${obj.file_name}`}
|
||||
style={{ width: "100%", height: 250, objectFit: "cover", borderRadius: 8 }}
|
||||
/>
|
||||
</CardContent>
|
||||
<Box display="flex" justifyContent="center" mt={1}>
|
||||
<IconButton onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
defaultHandleCopy(obj.id);
|
||||
}}
|
||||
color="primary"
|
||||
data-testid={`gallery-tab-copy-${obj.id}`} >
|
||||
|
||||
<ContentCopyIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImageToDelete(obj);
|
||||
setOpenDeleteDialog(true);
|
||||
}}
|
||||
color="error"
|
||||
data-testid={`gallery-tab-delete-${obj.id}`} >
|
||||
|
||||
<DeleteIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="center" mt={2}>
|
||||
<Button onClick={() => setImgPage((prev) => Math.max(prev - 1, 1))} disabled={imgPage === 1} color="primary">
|
||||
Précédent
|
||||
</Button>
|
||||
<Button onClick={() => setImgPage((prev) => (prev * imgLimit < totalImg ? prev + 1 : prev))} disabled={imgPage * imgLimit >= totalImg} color="primary">
|
||||
Suivant
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{tabValue === 1 && (
|
||||
<Box display="flex" flexDirection="column" alignItems="center" width="100%" mt={3}>
|
||||
{/* Image Preview at the top */}
|
||||
{preview && (
|
||||
<Box
|
||||
mt={2}
|
||||
mb={2}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 600,
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
borderRadius: 8,
|
||||
maxHeight: "600px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}>
|
||||
<TextField
|
||||
type="file"
|
||||
data-testid="file-input"
|
||||
onChange={handleImageUpload}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
"data-testid": "file-input",
|
||||
accept: "image/*",
|
||||
},
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="Téléverser"
|
||||
onClick={() => { handleSaveImage() }}
|
||||
sx={{ ml: 2, height: "100%" }}
|
||||
>
|
||||
Téléverser <Upload sx={{ ml: 1 }} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md">
|
||||
<IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}
|
||||
data-testid="close-button">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<DialogContent>
|
||||
{selectedImage && (
|
||||
<img
|
||||
src={`data:${selectedImage.mime_type};base64,${selectedImage.file_content}`}
|
||||
alt="Enlarged view"
|
||||
style={{ width: "100%", height: "auto", borderRadius: 8, maxHeight: "500px" }}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)}>
|
||||
<DialogTitle>Supprimer</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Voulez-vous supprimer cette image?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenDeleteDialog(false)} color="primary">
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={() => imageToDelete && handleDeleteFunction(imageToDelete.id)} color="error">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={4000}
|
||||
onClose={handleCloseSnackbar}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleCloseSnackbar}
|
||||
severity={snackbarSeverity}
|
||||
sx={{ width: "100%" }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGallery;
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ImageGallery from "../ImageGallery";
|
||||
import { ImageSearch } from "@mui/icons-material";
|
||||
|
||||
|
||||
interface ImageGalleryModalProps {
|
||||
handleCopy?: (id: string) => void;
|
||||
}
|
||||
|
||||
|
||||
const ImageGalleryModal: React.FC<ImageGalleryModalProps> = ({ handleCopy }) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label='images-open'
|
||||
onClick={() => handleOpen()}>
|
||||
Images <ImageSearch />
|
||||
</Button>
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column", alignItems: "center", py: 3 }}>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
color="primary"
|
||||
aria-label="close"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 8,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
<ImageGallery handleCopy={handleCopy}/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGalleryModal;
|
||||
|
|
@ -47,10 +47,10 @@ const LaunchQuizDialog: React.FC<Props> = ({ open, handleOnClose, launchQuiz, se
|
|||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={handleOnClose}>
|
||||
<div>Annuler</div>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button variant="contained" onClick={launchQuiz}>
|
||||
<div>Lancer</div>
|
||||
Lancer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
// LiveResults.tsx
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { QuestionType } from '../../Types/QuestionType';
|
||||
|
||||
import './liveResult.css';
|
||||
import {
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Paper,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow
|
||||
} from '@mui/material';
|
||||
import { StudentType } from '../../Types/StudentType';
|
||||
|
||||
import LiveResultsTable from './LiveResultsTable/LiveResultsTable';
|
||||
import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate';
|
||||
|
||||
interface LiveResultsProps {
|
||||
socket: Socket | null;
|
||||
|
|
@ -20,14 +30,241 @@ interface LiveResultsProps {
|
|||
students: StudentType[]
|
||||
}
|
||||
|
||||
// interface Answer {
|
||||
// answer: string | number | boolean;
|
||||
// isCorrect: boolean;
|
||||
// idQuestion: number;
|
||||
// }
|
||||
|
||||
// interface StudentResult {
|
||||
// username: string;
|
||||
// idUser: string;
|
||||
// answers: Answer[];
|
||||
// }
|
||||
|
||||
const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => {
|
||||
const [showUsernames, setShowUsernames] = useState<boolean>(false);
|
||||
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
|
||||
// const [students, setStudents] = useState<StudentType[]>(initialStudents);
|
||||
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
|
||||
|
||||
const maxQuestions = questions.length;
|
||||
|
||||
// useEffect(() => {
|
||||
// // Initialize the map with the current students
|
||||
// const newStudentResultsMap = new Map<string, StudentResult>();
|
||||
|
||||
// for (const student of students) {
|
||||
// newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] });
|
||||
// }
|
||||
|
||||
// setStudentResultsMap(newStudentResultsMap);
|
||||
// }, [])
|
||||
|
||||
// update when students change
|
||||
// useEffect(() => {
|
||||
// // studentResultsMap is inconsistent with students -- need to update
|
||||
|
||||
// for (const student of students as StudentType[]) {
|
||||
// }
|
||||
|
||||
// }, [students])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (socket) {
|
||||
// const submitAnswerHandler = ({
|
||||
// idUser,
|
||||
// answer,
|
||||
// idQuestion
|
||||
// }: {
|
||||
// idUser: string;
|
||||
// username: string;
|
||||
// answer: string | number | boolean;
|
||||
// idQuestion: number;
|
||||
// }) => {
|
||||
// console.log(`Received answer from ${idUser} for question ${idQuestion}: ${answer}`);
|
||||
|
||||
// // print the list of current student names
|
||||
// console.log('Current students:');
|
||||
// students.forEach((student) => {
|
||||
// console.log(student.name);
|
||||
// });
|
||||
|
||||
// // Update the students state using the functional form of setStudents
|
||||
// setStudents((prevStudents) => {
|
||||
// let foundStudent = false;
|
||||
// const updatedStudents = prevStudents.map((student) => {
|
||||
// if (student.id === idUser) {
|
||||
// foundStudent = true;
|
||||
// const updatedAnswers = student.answers.map((ans) => {
|
||||
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), idQuestion };
|
||||
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
|
||||
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
|
||||
// }
|
||||
// );
|
||||
// return { ...student, answers: updatedAnswers };
|
||||
// }
|
||||
// return student;
|
||||
// });
|
||||
// if (!foundStudent) {
|
||||
// console.log(`Student ${idUser} not found in the list of students in LiveResults`);
|
||||
// }
|
||||
// return updatedStudents;
|
||||
// });
|
||||
|
||||
|
||||
// // make a copy of the students array so we can update it
|
||||
// // const updatedStudents = [...students];
|
||||
|
||||
// // const student = updatedStudents.find((student) => student.id === idUser);
|
||||
// // if (!student) {
|
||||
// // // this is a bad thing if an answer was submitted but the student isn't in the list
|
||||
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
|
||||
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
|
||||
// // student.answers.push(newAnswer);
|
||||
// // // print list of answers
|
||||
// // console.log('Answers:');
|
||||
// // student.answers.forEach((answer) => {
|
||||
// // console.log(answer.answer);
|
||||
// // });
|
||||
// // setStudents(updatedStudents); // update the state
|
||||
// };
|
||||
|
||||
// socket.on('submit-answer', submitAnswerHandler);
|
||||
// return () => {
|
||||
// socket.off('submit-answer');
|
||||
// };
|
||||
// }
|
||||
// }, [socket]);
|
||||
|
||||
const getStudentGrade = (student: StudentType): number => {
|
||||
if (student.answers.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uniqueQuestions = new Set();
|
||||
let correctAnswers = 0;
|
||||
|
||||
for (const answer of student.answers) {
|
||||
const { idQuestion, isCorrect } = answer;
|
||||
|
||||
if (!uniqueQuestions.has(idQuestion)) {
|
||||
uniqueQuestions.add(idQuestion);
|
||||
|
||||
if (isCorrect) {
|
||||
correctAnswers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (correctAnswers / questions.length) * 100;
|
||||
};
|
||||
|
||||
const classAverage: number = useMemo(() => {
|
||||
let classTotal = 0;
|
||||
|
||||
students.forEach((student) => {
|
||||
classTotal += getStudentGrade(student);
|
||||
});
|
||||
|
||||
return classTotal / students.length;
|
||||
}, [students]);
|
||||
|
||||
const getCorrectAnswersPerQuestion = (index: number): number => {
|
||||
return (
|
||||
(students.filter((student) =>
|
||||
student.answers.some(
|
||||
(answer) =>
|
||||
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
|
||||
)
|
||||
).length / students.length) * 100
|
||||
);
|
||||
};
|
||||
|
||||
// (studentResults.filter((student) =>
|
||||
// student.answers.some(
|
||||
// (answer) =>
|
||||
// parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
|
||||
// )
|
||||
// ).length /
|
||||
// studentResults.length) *
|
||||
// 100
|
||||
// );
|
||||
// };
|
||||
|
||||
// function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
|
||||
// const questionInfo = questions.find((q) =>
|
||||
// q.question.id ? q.question.id === idQuestion.toString() : false
|
||||
// ) as QuestionType | undefined;
|
||||
|
||||
// const answerText = answer.toString();
|
||||
// if (questionInfo) {
|
||||
// const question = questionInfo.question as GIFTQuestion;
|
||||
// if (question.type === 'TF') {
|
||||
// return (
|
||||
// (question.isTrue && answerText == 'true') ||
|
||||
// (!question.isTrue && answerText == 'false')
|
||||
// );
|
||||
// } else if (question.type === 'MC') {
|
||||
// return question.choices.some(
|
||||
// (choice) => choice.isCorrect && choice.text.text === answerText
|
||||
// );
|
||||
// } else if (question.type === 'Numerical') {
|
||||
// if (question.choices && !Array.isArray(question.choices)) {
|
||||
// if (
|
||||
// question.choices.type === 'high-low' &&
|
||||
// question.choices.numberHigh &&
|
||||
// question.choices.numberLow
|
||||
// ) {
|
||||
// const answerNumber = parseFloat(answerText);
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return (
|
||||
// answerNumber <= question.choices.numberHigh &&
|
||||
// answerNumber >= question.choices.numberLow
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (question.choices && Array.isArray(question.choices)) {
|
||||
// if (
|
||||
// question.choices[0].text.type === 'range' &&
|
||||
// question.choices[0].text.number &&
|
||||
// question.choices[0].text.range
|
||||
// ) {
|
||||
// const answerNumber = parseFloat(answerText);
|
||||
// const range = question.choices[0].text.range;
|
||||
// const correctAnswer = question.choices[0].text.number;
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return (
|
||||
// answerNumber <= correctAnswer + range &&
|
||||
// answerNumber >= correctAnswer - range
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// if (
|
||||
// question.choices[0].text.type === 'simple' &&
|
||||
// question.choices[0].text.number
|
||||
// ) {
|
||||
// const answerNumber = parseFloat(answerText);
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return answerNumber === question.choices[0].text.number;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } else if (question.type === 'Short') {
|
||||
// return question.choices.some(
|
||||
// (choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<div>
|
||||
<div className="action-bar mb-1">
|
||||
<div className="text-2xl text-bold">Résultats du quiz</div>
|
||||
|
|
@ -58,14 +295,146 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
|
|||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className="sticky-column">
|
||||
<div className="text-base text-bold">Nom d'utilisateur</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
onClick={() => showSelectedQuestion(index)}
|
||||
>
|
||||
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell
|
||||
className="sticky-header"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
>
|
||||
<div className="text-base text-bold">% réussite</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{students.map((student) => (
|
||||
<TableRow key={student.id}>
|
||||
<TableCell
|
||||
className="sticky-column"
|
||||
sx={{
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
>
|
||||
<div className="text-base">
|
||||
{showUsernames ? student.name : '******'}
|
||||
</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => {
|
||||
const answer = student.answers.find(
|
||||
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
|
||||
);
|
||||
const answerText = answer ? answer.answer.toString() : '';
|
||||
const isCorrect = answer ? answer.isCorrect : false;
|
||||
|
||||
<LiveResultsTable
|
||||
students={students}
|
||||
questions={questions}
|
||||
showCorrectAnswers={showCorrectAnswers}
|
||||
showSelectedQuestion={showSelectedQuestion}
|
||||
showUsernames={showUsernames}
|
||||
/>
|
||||
return (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
className={
|
||||
answerText === ''
|
||||
? ''
|
||||
: isCorrect
|
||||
? 'correct-answer'
|
||||
: 'incorrect-answer'
|
||||
}
|
||||
>
|
||||
{showCorrectAnswers ? (
|
||||
<div>{formatLatex(answerText)}</div>
|
||||
) : isCorrect ? (
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
) : (
|
||||
answerText !== '' && (
|
||||
<FontAwesomeIcon icon={faCircleXmark} />
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
<TableCell
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
}}
|
||||
>
|
||||
{getStudentGrade(student).toFixed()} %
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
|
||||
<TableCell className="sticky-column" sx={{ color: 'black' }}>
|
||||
<div className="text-base text-bold">% réussite</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
}}
|
||||
>
|
||||
{students.length > 0
|
||||
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
|
||||
: '-'}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
}}
|
||||
>
|
||||
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Paper, Table, TableContainer } from '@mui/material';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import { QuestionType } from '../../../Types/QuestionType';
|
||||
import LiveResultsTableFooter from './TableComponents/LiveResultTableFooter';
|
||||
import LiveResultsTableHeader from './TableComponents/LiveResultsTableHeader';
|
||||
import LiveResultsTableBody from './TableComponents/LiveResultsTableBody';
|
||||
|
||||
interface LiveResultsTableProps {
|
||||
students: StudentType[];
|
||||
questions: QuestionType[];
|
||||
showCorrectAnswers: boolean;
|
||||
showSelectedQuestion: (index: number) => void;
|
||||
showUsernames: boolean;
|
||||
}
|
||||
|
||||
const LiveResultsTable: React.FC<LiveResultsTableProps> = ({
|
||||
questions,
|
||||
students,
|
||||
showSelectedQuestion,
|
||||
showUsernames,
|
||||
showCorrectAnswers
|
||||
}) => {
|
||||
|
||||
const maxQuestions = questions.length;
|
||||
|
||||
const getStudentGrade = (student: StudentType): number => {
|
||||
if (student.answers.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uniqueQuestions = new Set();
|
||||
let correctAnswers = 0;
|
||||
|
||||
for (const answer of student.answers) {
|
||||
const { idQuestion, isCorrect } = answer;
|
||||
|
||||
if (!uniqueQuestions.has(idQuestion)) {
|
||||
uniqueQuestions.add(idQuestion);
|
||||
|
||||
if (isCorrect) {
|
||||
correctAnswers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (correctAnswers / questions.length) * 100;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small">
|
||||
<LiveResultsTableHeader
|
||||
maxQuestions={maxQuestions}
|
||||
showSelectedQuestion={showSelectedQuestion}
|
||||
/>
|
||||
<LiveResultsTableBody
|
||||
maxQuestions={maxQuestions}
|
||||
students={students}
|
||||
showUsernames={showUsernames}
|
||||
showCorrectAnswers={showCorrectAnswers}
|
||||
getStudentGrade={getStudentGrade}
|
||||
/>
|
||||
<LiveResultsTableFooter
|
||||
students={students}
|
||||
maxQuestions={maxQuestions}
|
||||
getStudentGrade={getStudentGrade}
|
||||
/>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveResultsTable;
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import { TableCell, TableFooter, TableRow } from "@mui/material";
|
||||
import React, { useMemo } from "react";
|
||||
import { StudentType } from "src/Types/StudentType";
|
||||
|
||||
interface LiveResultsFooterProps {
|
||||
students: StudentType[];
|
||||
maxQuestions: number;
|
||||
getStudentGrade: (student: StudentType) => number;
|
||||
}
|
||||
|
||||
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
||||
maxQuestions,
|
||||
students,
|
||||
getStudentGrade
|
||||
|
||||
}) => {
|
||||
|
||||
const getCorrectAnswersPerQuestion = (index: number): number => {
|
||||
return (
|
||||
(students.filter((student) =>
|
||||
student.answers.some(
|
||||
(answer) =>
|
||||
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
|
||||
)
|
||||
).length / students.length) * 100
|
||||
);
|
||||
};
|
||||
|
||||
const classAverage: number = useMemo(() => {
|
||||
let classTotal = 0;
|
||||
|
||||
students.forEach((student) => {
|
||||
classTotal += getStudentGrade(student);
|
||||
});
|
||||
|
||||
return classTotal / students.length;
|
||||
}, [students]);
|
||||
|
||||
return (
|
||||
<TableFooter>
|
||||
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
|
||||
<TableCell className="sticky-column" sx={{ color: 'black' }}>
|
||||
<div className="text-base text-bold">% réussite</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(0, 0, 0)',
|
||||
}}
|
||||
>
|
||||
{students.length > 0
|
||||
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
|
||||
: '-'}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem',
|
||||
color: 'rgba(0, 0, 0)',
|
||||
}}
|
||||
>
|
||||
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
);
|
||||
};
|
||||
export default LiveResultsTableFooter;
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { TableBody, TableCell, TableRow } from "@mui/material";
|
||||
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FormattedTextTemplate } from '../../../GiftTemplate/templates/TextTypeTemplate';
|
||||
import React from "react";
|
||||
import { StudentType } from "src/Types/StudentType";
|
||||
|
||||
interface LiveResultsFooterProps {
|
||||
maxQuestions: number;
|
||||
students: StudentType[];
|
||||
showUsernames: boolean;
|
||||
showCorrectAnswers: boolean;
|
||||
getStudentGrade: (student: StudentType) => number;
|
||||
|
||||
}
|
||||
|
||||
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
||||
maxQuestions,
|
||||
students,
|
||||
showUsernames,
|
||||
showCorrectAnswers,
|
||||
getStudentGrade
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<TableBody>
|
||||
{students.map((student) => (
|
||||
<TableRow key={student.id}>
|
||||
<TableCell
|
||||
className="sticky-column"
|
||||
sx={{
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
>
|
||||
<div className="text-base">
|
||||
{showUsernames ? student.name : '******'}
|
||||
</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => {
|
||||
const answer = student.answers.find(
|
||||
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
|
||||
);
|
||||
const answerText = answer ? answer.answer.toString() : '';
|
||||
const isCorrect = answer ? answer.isCorrect : false;
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
className={
|
||||
answerText === ''
|
||||
? ''
|
||||
: isCorrect
|
||||
? 'correct-answer'
|
||||
: 'incorrect-answer'
|
||||
}
|
||||
>
|
||||
{showCorrectAnswers ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: answerText }) }}></div>
|
||||
) : isCorrect ? (
|
||||
<FontAwesomeIcon icon={faCheck} aria-label="correct" />
|
||||
) : (
|
||||
answerText !== '' && (
|
||||
<FontAwesomeIcon icon={faCircleXmark} aria-label="incorrect"/>
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
<TableCell
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
}}
|
||||
>
|
||||
{getStudentGrade(student).toFixed()} %
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
);
|
||||
};
|
||||
export default LiveResultsTableFooter;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import React from "react";
|
||||
import { TableCell, TableHead, TableRow } from "@mui/material";
|
||||
|
||||
interface LiveResultsHeaderProps {
|
||||
maxQuestions: number;
|
||||
showSelectedQuestion: (index: number) => void;
|
||||
}
|
||||
|
||||
const LiveResultsTableHeader: React.FC<LiveResultsHeaderProps> = ({
|
||||
maxQuestions,
|
||||
showSelectedQuestion,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell className="sticky-column">
|
||||
<div className="text-base text-bold">Nom d'utilisateur</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
onClick={() => showSelectedQuestion(index)}
|
||||
>
|
||||
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell
|
||||
className="sticky-header"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)'
|
||||
}}
|
||||
>
|
||||
<div className="text-base text-bold">% réussite</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
export default LiveResultsTableHeader;
|
||||
|
|
@ -4,64 +4,27 @@ import '../questionStyle.css';
|
|||
import { Button } from '@mui/material';
|
||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||
import { MultipleChoiceQuestion } from 'gift-pegjs';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
interface Props {
|
||||
question: MultipleChoiceQuestion;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: string) => void;
|
||||
showAnswer?: boolean;
|
||||
passedAnswer?: AnswerType;
|
||||
}
|
||||
|
||||
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
|
||||
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
||||
|
||||
const [answer, setAnswer] = useState<AnswerType>(() => {
|
||||
if (passedAnswer && passedAnswer.length > 0) {
|
||||
return passedAnswer;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
let disableButton = false;
|
||||
if (handleOnSubmitAnswer === undefined) {
|
||||
disableButton = true;
|
||||
}
|
||||
const { question, showAnswer, handleOnSubmitAnswer } = props;
|
||||
const [answer, setAnswer] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
||||
if (passedAnswer !== undefined) {
|
||||
setAnswer(passedAnswer);
|
||||
} else {
|
||||
setAnswer([]);
|
||||
}
|
||||
}, [passedAnswer, question.id]);
|
||||
setAnswer(undefined);
|
||||
}, [question]);
|
||||
|
||||
const handleOnClickAnswer = (choice: string) => {
|
||||
setAnswer((prevAnswer) => {
|
||||
console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`);
|
||||
const correctAnswersCount = question.choices.filter((c) => c.isCorrect).length;
|
||||
|
||||
if (correctAnswersCount === 1) {
|
||||
// If only one correct answer, replace the current selection
|
||||
return prevAnswer.includes(choice) ? [] : [choice];
|
||||
} else {
|
||||
// Allow multiple selections if there are multiple correct answers
|
||||
if (prevAnswer.includes(choice)) {
|
||||
// Remove the choice if it's already selected
|
||||
return prevAnswer.filter((selected) => selected !== choice);
|
||||
} else {
|
||||
// Add the choice if it's not already selected
|
||||
return [...prevAnswer, choice];
|
||||
}
|
||||
}
|
||||
});
|
||||
setAnswer(choice);
|
||||
};
|
||||
|
||||
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">
|
||||
|
|
@ -69,61 +32,47 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
<div className="choices-wrapper mb-1">
|
||||
{question.choices.map((choice, i) => {
|
||||
console.log(`answer: ${answer}, choice: ${choice.formattedText.text}`);
|
||||
const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
|
||||
const selected = answer === choice.formattedText.text ? 'selected' : '';
|
||||
return (
|
||||
<div key={choice.formattedText.text + i} className="choice-container">
|
||||
<Button
|
||||
variant="text"
|
||||
className="button-wrapper"
|
||||
disabled={disableButton}
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
|
||||
>
|
||||
{showAnswer ? (
|
||||
<div>{choice.isCorrect ? '✅' : '❌'}</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}>
|
||||
{showAnswer? (<div> {(choice.isCorrect ? '✅' : '❌')}</div>)
|
||||
:``}
|
||||
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
||||
<div className={`answer-text ${selected}`}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: FormattedTextTemplate(choice.formattedText),
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
<div className="feedback-container mb-1 mt-1/2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{question.formattedGlobalFeedback && showAnswer && (
|
||||
<div className="global-feedback mb-2">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showAnswer && handleOnSubmitAnswer && (
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer.length === 0}
|
||||
disabled={answer === undefined}
|
||||
>
|
||||
Répondre
|
||||
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,26 @@
|
|||
// NumericalQuestion.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import '../questionStyle.css';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
|
||||
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
interface Props {
|
||||
question: NumericalQuestion;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: number) => void;
|
||||
showAnswer?: boolean;
|
||||
passedAnswer?: AnswerType;
|
||||
}
|
||||
|
||||
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
||||
const { question, showAnswer, handleOnSubmitAnswer } =
|
||||
props;
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||
|
||||
const [answer, setAnswer] = useState<number>();
|
||||
|
||||
const correctAnswers = question.choices;
|
||||
let correctAnswer = '';
|
||||
|
||||
useEffect(() => {
|
||||
if (passedAnswer !== null && passedAnswer !== undefined) {
|
||||
setAnswer(passedAnswer);
|
||||
}
|
||||
}, [passedAnswer]);
|
||||
|
||||
//const isSingleAnswer = correctAnswers.length === 1;
|
||||
|
||||
if (isSimpleNumericalAnswer(correctAnswers[0])) {
|
||||
|
|
@ -50,16 +44,10 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
{showAnswer ? (
|
||||
<>
|
||||
<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>
|
||||
<div className="correct-answer-text mb-2">{correctAnswer}</div>
|
||||
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||
</div>}
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -69,7 +57,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
|||
id={question.formattedStem.text}
|
||||
name={question.formattedStem.text}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAnswer([e.target.valueAsNumber]);
|
||||
setAnswer(e.target.valueAsNumber);
|
||||
}}
|
||||
inputProps={{ 'data-testid': 'number-input' }}
|
||||
/>
|
||||
|
|
@ -87,7 +75,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
|||
handleOnSubmitAnswer &&
|
||||
handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
|
||||
disabled={answer === undefined || isNaN(answer)}
|
||||
>
|
||||
Répondre
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -5,21 +5,17 @@ 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';
|
||||
|
||||
interface QuestionProps {
|
||||
question: Question;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: string | number | boolean) => void;
|
||||
showAnswer?: boolean;
|
||||
answer?: AnswerType;
|
||||
|
||||
}
|
||||
const QuestionDisplay: React.FC<QuestionProps> = ({
|
||||
question,
|
||||
handleOnSubmitAnswer,
|
||||
showAnswer,
|
||||
answer,
|
||||
}) => {
|
||||
// const isMobile = useCheckMobileScreen();
|
||||
// const imgWidth = useMemo(() => {
|
||||
|
|
@ -34,32 +30,37 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
|||
question={question}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
passedAnswer={answer}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'MC':
|
||||
|
||||
questionTypeComponent = (
|
||||
<MultipleChoiceQuestionDisplay
|
||||
question={question}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
passedAnswer={answer}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'Numerical':
|
||||
if (question.choices) {
|
||||
if (!Array.isArray(question.choices)) {
|
||||
questionTypeComponent = (
|
||||
<NumericalQuestionDisplay
|
||||
question={question}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
passedAnswer={answer}
|
||||
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked)
|
||||
<NumericalQuestionDisplay
|
||||
question={question}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Short':
|
||||
|
|
@ -68,7 +69,6 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
|
|||
question={question}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
passedAnswer={answer}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,18 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import '../questionStyle.css';
|
||||
import { Button, TextField } from '@mui/material';
|
||||
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
|
||||
import { ShortAnswerQuestion } from 'gift-pegjs';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
interface Props {
|
||||
question: ShortAnswerQuestion;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: string) => void;
|
||||
showAnswer?: boolean;
|
||||
passedAnswer?: AnswerType;
|
||||
|
||||
}
|
||||
|
||||
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
||||
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||
|
||||
useEffect(() => {
|
||||
if (passedAnswer !== undefined) {
|
||||
setAnswer(passedAnswer);
|
||||
}
|
||||
}, [passedAnswer]);
|
||||
console.log("Answer" , answer);
|
||||
const { question, showAnswer, handleOnSubmitAnswer } = props;
|
||||
const [answer, setAnswer] = useState<string>();
|
||||
|
||||
return (
|
||||
<div className="question-wrapper">
|
||||
|
|
@ -33,18 +22,11 @@ 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) }} />
|
||||
|
|
@ -58,7 +40,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
|||
id={question.formattedStem.text}
|
||||
name={question.formattedStem.text}
|
||||
onChange={(e) => {
|
||||
setAnswer([e.target.value]);
|
||||
setAnswer(e.target.value);
|
||||
}}
|
||||
disabled={showAnswer}
|
||||
aria-label="short-answer-input"
|
||||
|
|
@ -72,7 +54,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
|||
handleOnSubmitAnswer &&
|
||||
handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === null || answer === undefined || answer.length === 0}
|
||||
disabled={answer === undefined || answer === ''}
|
||||
>
|
||||
Répondre
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,86 +4,61 @@ import '../questionStyle.css';
|
|||
import { Button } from '@mui/material';
|
||||
import { TrueFalseQuestion } from 'gift-pegjs';
|
||||
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
|
||||
interface Props {
|
||||
question: TrueFalseQuestion;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: boolean) => void;
|
||||
showAnswer?: boolean;
|
||||
passedAnswer?: AnswerType;
|
||||
}
|
||||
|
||||
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
||||
const { question, showAnswer, handleOnSubmitAnswer } =
|
||||
props;
|
||||
|
||||
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
||||
|
||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
||||
return passedAnswer[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let disableButton = false;
|
||||
if (handleOnSubmitAnswer === undefined) {
|
||||
disableButton = true;
|
||||
}
|
||||
const [answer, setAnswer] = useState<boolean | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("passedAnswer", passedAnswer);
|
||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
||||
setAnswer(passedAnswer[0]);
|
||||
} else {
|
||||
setAnswer(undefined);
|
||||
}
|
||||
}, [passedAnswer, question.id]);
|
||||
|
||||
const handleOnClickAnswer = (choice: boolean) => {
|
||||
setAnswer(choice);
|
||||
};
|
||||
setAnswer(undefined);
|
||||
}, [question]);
|
||||
|
||||
const selectedTrue = answer ? 'selected' : '';
|
||||
const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
|
||||
return (
|
||||
<div className="question-container">
|
||||
<div className="question content">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||
</div>
|
||||
<div className="choices-wrapper mb-1">
|
||||
<Button
|
||||
className="button-wrapper"
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(true)}
|
||||
onClick={() => !showAnswer && setAnswer(true)}
|
||||
fullWidth
|
||||
disabled={disableButton}
|
||||
>
|
||||
{showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||
{showAnswer? (<div> {(question.isTrue ? '✅' : '❌')}</div>):``}
|
||||
<div className={`circle ${selectedTrue}`}>V</div>
|
||||
<div className={`answer-text ${selectedTrue}`}>Vrai</div>
|
||||
|
||||
{showAnswer && answer && question.trueFormattedFeedback && (
|
||||
<div className="true-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="button-wrapper"
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(false)}
|
||||
onClick={() => !showAnswer && setAnswer(false)}
|
||||
fullWidth
|
||||
disabled={disableButton}
|
||||
|
||||
>
|
||||
{showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||
{showAnswer? (<div> {(!question.isTrue ? '✅' : '❌')}</div>):``}
|
||||
<div className={`circle ${selectedFalse}`}>F</div>
|
||||
<div className={`answer-text ${selectedFalse}`}>Faux</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) }} />
|
||||
|
|
@ -93,7 +68,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
|||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
|
||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === undefined}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
}
|
||||
|
||||
.question-wrapper .katex {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
@ -119,9 +120,9 @@
|
|||
}
|
||||
|
||||
.feedback-container {
|
||||
display: inline-block !important; /* override the parent */
|
||||
align-items: center;
|
||||
margin-left: 1.1rem;
|
||||
display: inline-flex !important; /* override the parent */
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 0.5rem;
|
||||
background-color: hsl(43, 100%, 94%);
|
||||
|
|
@ -147,25 +148,6 @@
|
|||
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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Dialog, DialogTitle, DialogActions, Button, Tooltip, IconButton, Typography, Box } from '@mui/material';
|
||||
import { Share } from '@mui/icons-material';
|
||||
import { QuizType } from '../../Types/QuizType';
|
||||
|
||||
interface ShareQuizModalProps {
|
||||
quiz: QuizType;
|
||||
}
|
||||
|
||||
const ShareQuizModal: React.FC<ShareQuizModalProps> = ({ quiz }) => {
|
||||
const [_open, setOpen] = useState(false);
|
||||
const [feedback, setFeedback] = useState({
|
||||
open: false,
|
||||
title: '',
|
||||
isError: false
|
||||
});
|
||||
|
||||
const handleCloseModal = () => setOpen(false);
|
||||
|
||||
const handleShareByUrl = () => {
|
||||
const quizUrl = `${window.location.origin}/teacher/share/${quiz._id}`;
|
||||
navigator.clipboard.writeText(quizUrl)
|
||||
.then(() => {
|
||||
setFeedback({
|
||||
open: true,
|
||||
title: 'L\'URL de partage pour le quiz',
|
||||
isError: false
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setFeedback({
|
||||
open: true,
|
||||
title: 'Une erreur est survenue lors de la copie de l\'URL.',
|
||||
isError: true
|
||||
});
|
||||
});
|
||||
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
const closeFeedback = () => {
|
||||
setFeedback(prev => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Partager" placement="top">
|
||||
<IconButton color="primary" onClick={handleShareByUrl} aria-label="partager quiz">
|
||||
<Share />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* Feedback Dialog */}
|
||||
<Dialog
|
||||
open={feedback.open}
|
||||
onClose={closeFeedback}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
>
|
||||
<DialogTitle sx={{ textAlign: "center" }}>
|
||||
<Box>
|
||||
{feedback.isError ? (
|
||||
<Typography color="error.main">
|
||||
{feedback.title}
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Typography component="span">
|
||||
L'URL de partage pour le quiz{' '}
|
||||
</Typography>
|
||||
<Typography component="span" fontWeight="bold">
|
||||
{quiz.title}
|
||||
</Typography>
|
||||
<Typography component="span">
|
||||
{' '}a été copiée.
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogActions sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<Button
|
||||
onClick={closeFeedback}
|
||||
variant="contained"
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareQuizModal;
|
||||
|
|
@ -3,47 +3,41 @@ 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[];
|
||||
answers: AnswerSubmissionToBackendType[];
|
||||
submitAnswer: (_answer: AnswerType, _idQuestion: number) => void;
|
||||
submitAnswer: (answer: string | number | boolean, 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 [answer, setAnswer] = useState<AnswerType>('');
|
||||
// const [imageUrl, setImageUrl] = useState('');
|
||||
|
||||
// const previousQuestion = () => {
|
||||
// setQuestion(questions[Number(questionInfos.question?.id) - 2]);
|
||||
// setIsAnswerSubmitted(false);
|
||||
// };
|
||||
|
||||
const previousQuestion = () => {
|
||||
setQuestion(questions[Number(questionInfos.question?.id) - 2]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const savedAnswer = answers[Number(questionInfos.question.id)-1]?.answer;
|
||||
console.log(`StudentModeQuiz: useEffect: savedAnswer: ${savedAnswer}`);
|
||||
setIsAnswerSubmitted(savedAnswer !== undefined);
|
||||
}, [questionInfos.question, answers]);
|
||||
useEffect(() => {}, [questionInfos]);
|
||||
|
||||
const nextQuestion = () => {
|
||||
setQuestion(questions[Number(questionInfos.question?.id)]);
|
||||
setIsAnswerSubmitted(false);
|
||||
};
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType) => {
|
||||
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
|
||||
const idQuestion = Number(questionInfos.question.id) || -1;
|
||||
submitAnswer(answer, idQuestion);
|
||||
setIsAnswerSubmitted(true);
|
||||
|
|
@ -52,13 +46,11 @@ 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">
|
||||
|
|
@ -74,30 +66,31 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
|||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
question={questionInfos.question as Question}
|
||||
showAnswer={isAnswerSubmitted}
|
||||
answer={answers[Number(questionInfos.question.id)-1]?.answer}
|
||||
/>
|
||||
<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 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>
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={nextQuestion}
|
||||
fullWidth
|
||||
disabled={Number(questionInfos.question.id) >= questions.length}
|
||||
>
|
||||
Question suivante
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,24 +9,21 @@ import './studentWaitPage.css';
|
|||
interface Props {
|
||||
students: StudentType[];
|
||||
launchQuiz: () => void;
|
||||
setQuizMode: (_mode: 'student' | 'teacher') => void;
|
||||
setQuizMode: (mode: 'student' | 'teacher') => void;
|
||||
}
|
||||
|
||||
const StudentWaitPage: React.FC<Props> = ({ students, launchQuiz, setQuizMode }) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const handleLaunchClick = () => {
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wait">
|
||||
<div className='button'>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleLaunchClick}
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
startIcon={<PlayArrow />}
|
||||
sx={{ fontWeight: 600, fontSize: 20, width: 'auto' }}
|
||||
fullWidth
|
||||
sx={{ fontWeight: 600, fontSize: 20 }}
|
||||
>
|
||||
Lancer
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,59 +1,38 @@
|
|||
// 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;
|
||||
answers: AnswerSubmissionToBackendType[];
|
||||
submitAnswer: (_answer: AnswerType, _idQuestion: number) => void;
|
||||
submitAnswer: (answer: string | number | boolean, 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 [answer, setAnswer] = useState<AnswerType>();
|
||||
|
||||
|
||||
// arrive here the first time after waiting for next question
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
// handle showing the feedback dialog
|
||||
useEffect(() => {
|
||||
console.log(`TeacherModeQuiz: useEffect: answer: ${answer}`);
|
||||
setIsAnswerSubmitted(answer !== undefined);
|
||||
setIsFeedbackDialogOpen(answer !== undefined);
|
||||
}, [answer]);
|
||||
const [feedbackMessage, setFeedbackMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`TeacherModeQuiz: useEffect: isAnswerSubmitted: ${isAnswerSubmitted}`);
|
||||
setIsFeedbackDialogOpen(isAnswerSubmitted);
|
||||
}, [isAnswerSubmitted]);
|
||||
setIsAnswerSubmitted(false);
|
||||
}, [questionInfos]);
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType) => {
|
||||
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
|
||||
const idQuestion = Number(questionInfos.question.id) || -1;
|
||||
submitAnswer(answer, idQuestion);
|
||||
// setAnswer(answer);
|
||||
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`);
|
||||
setIsFeedbackDialogOpen(true);
|
||||
};
|
||||
|
||||
|
|
@ -64,21 +43,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?`} />
|
||||
<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>
|
||||
|
||||
<div className='centerTitle'>
|
||||
<div className='title'>Question {questionInfos.question.id}</div>
|
||||
</div>
|
||||
|
||||
<div className='dumb'></div>
|
||||
|
||||
</div>
|
||||
|
||||
{isAnswerSubmitted ? (
|
||||
{isAnswerSubmitted ? (
|
||||
<div>
|
||||
En attente pour la prochaine question...
|
||||
</div>
|
||||
|
|
@ -86,7 +65,6 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
|
|||
<QuestionComponent
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
question={questionInfos.question as Question}
|
||||
answer={answer}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -96,31 +74,20 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
|
|||
>
|
||||
<DialogTitle>Rétroaction</DialogTitle>
|
||||
<DialogContent>
|
||||
<div style={{
|
||||
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}
|
||||
answer={answer}
|
||||
|
||||
{feedbackMessage}
|
||||
<QuestionComponent
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
question={questionInfos.question as Question}
|
||||
showAnswer={true}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleFeedbackDialogClose} color="primary">
|
||||
Fermer
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// constants.tsx
|
||||
const ENV_VARIABLES = {
|
||||
MODE: process.env.MODE || "production",
|
||||
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
|
||||
BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}` : ''}` : process.env.VITE_BACKEND_URL || '',
|
||||
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}` : ''}` : ''
|
||||
MODE: 'production',
|
||||
VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
|
||||
VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
|
||||
};
|
||||
|
||||
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
|
||||
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
|
||||
|
||||
export { ENV_VARIABLES };
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './authDrawer.css';
|
||||
import SimpleLogin from './providers/SimpleLogin/Login';
|
||||
import authService from '../../services/AuthService';
|
||||
import { ENV_VARIABLES } from '../../constants';
|
||||
import ButtonAuth from './providers/OAuth-Oidc/ButtonAuth';
|
||||
|
||||
const AuthSelection: React.FC = () => {
|
||||
const [authData, setAuthData] = useState<any>(null); // Stocke les données d'auth
|
||||
const navigate = useNavigate();
|
||||
|
||||
ENV_VARIABLES.VITE_BACKEND_URL;
|
||||
// Récupérer les données d'authentification depuis l'API
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const data = await authService.fetchAuthData();
|
||||
setAuthData(data);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="auth-selection-page">
|
||||
<h1>Connexion</h1>
|
||||
|
||||
{/* Formulaire de connexion Simple Login */}
|
||||
{authData && authData['simpleauth'] && (
|
||||
<div className="form-container">
|
||||
<SimpleLogin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Conteneur OAuth/OIDC */}
|
||||
{authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && (
|
||||
<div className="auth-button-container">
|
||||
{Object.keys(authData).map((providerKey) => {
|
||||
const providerType = authData[providerKey].type;
|
||||
if (providerType === 'oidc' || providerType === 'oauth') {
|
||||
return (
|
||||
<ButtonAuth
|
||||
key={providerKey}
|
||||
providerName={providerKey}
|
||||
providerType={providerType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button className="home-button-container" onClick={() => navigate('/')}>Retour à l'accueil</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthSelection;
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
.auth-selection-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
input {
|
||||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #5271ff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* This hover was affecting the entire App */
|
||||
/* button:hover {
|
||||
background-color: #5271ff;
|
||||
} */
|
||||
.home-button-container {
|
||||
background: none;
|
||||
color: black;
|
||||
}
|
||||
.home-button-container:hover {
|
||||
background: none;
|
||||
color: black;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import apiService from '../../../services/ApiService';
|
||||
|
||||
const OAuthCallback: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const user = searchParams.get('user');
|
||||
const username = searchParams.get('username');
|
||||
|
||||
if (user) {
|
||||
apiService.saveToken(user);
|
||||
apiService.saveUsername(username || "");
|
||||
navigate('/teacher/dashboard');
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div>Loading...</div>;
|
||||
};
|
||||
|
||||
export default OAuthCallback;
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ENV_VARIABLES } from '../../../../constants';
|
||||
import '../css/buttonAuth.css';
|
||||
|
||||
interface ButtonAuthContainerProps {
|
||||
providerName: string;
|
||||
providerType: 'oauth' | 'oidc';
|
||||
}
|
||||
|
||||
const handleAuthLogin = (provider: string) => {
|
||||
window.location.href = `${ENV_VARIABLES.BACKEND_URL}/api/auth/${provider}`;
|
||||
};
|
||||
|
||||
const ButtonAuth: React.FC<ButtonAuthContainerProps> = ({ providerName, providerType }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={`${providerName}-${providerType}-container button-container`}>
|
||||
<h2>Se connecter avec {providerType.toUpperCase()}</h2>
|
||||
<button key={providerName} className={`provider-btn ${providerType}-btn`} onClick={() => handleAuthLogin(providerName)}>
|
||||
Continuer avec {providerName}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonAuth;
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
// JoinRoom.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { TextField, FormLabel, RadioGroup, FormControlLabel, Radio, Box } from '@mui/material';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
|
||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer';
|
||||
import ApiService from '../../../../services/ApiService';
|
||||
|
||||
const Register: React.FC = () => {
|
||||
|
||||
const [name, setName] = useState(''); // State for name
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [roles, setRoles] = useState<string[]>(['teacher']); // Set 'student' as the default role
|
||||
|
||||
const [connectionError, setConnectionError] = useState<string>('');
|
||||
const [isConnecting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
return () => { };
|
||||
}, []);
|
||||
|
||||
const handleRoleChange = (role: string) => {
|
||||
setRoles([role]); // Update the roles array to contain the selected role
|
||||
};
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
// Basic email format validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const register = async () => {
|
||||
if (!isValidEmail(email)) {
|
||||
setConnectionError("Veuillez entrer une adresse email valide.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ApiService.register(name, email, password, roles);
|
||||
|
||||
if (result !== true) {
|
||||
setConnectionError(result);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginContainer
|
||||
title="Créer un compte"
|
||||
error={connectionError}
|
||||
>
|
||||
<TextField
|
||||
label="Nom"
|
||||
variant="outlined"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Votre nom"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Adresse courriel"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
type="email"
|
||||
error={!!connectionError && !isValidEmail(email)}
|
||||
helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Mot de passe"
|
||||
variant="outlined"
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mot de passe"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<FormLabel component="legend" sx={{ marginRight: '1rem' }}>Choisir votre rôle</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label="role"
|
||||
name="role"
|
||||
value={roles[0]}
|
||||
onChange={(e) => handleRoleChange(e.target.value)}
|
||||
>
|
||||
<FormControlLabel value="student" control={<Radio />} label="Étudiant" />
|
||||
<FormControlLabel value="teacher" control={<Radio />} label="Professeur" />
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
||||
<LoadingButton
|
||||
loading={isConnecting}
|
||||
onClick={register}
|
||||
variant="contained"
|
||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!name || !email || !password}
|
||||
>
|
||||
S'inscrire
|
||||
</LoadingButton>
|
||||
</LoginContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// JoinRoom.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
|
||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
|
||||
import ApiService from '../../../../services/ApiService';
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const [connectionError, setConnectionError] = useState<string>('');
|
||||
const [isConnecting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
};
|
||||
}, []);
|
||||
|
||||
const reset = async () => {
|
||||
const result = await ApiService.resetPassword(email);
|
||||
|
||||
if (!result) {
|
||||
setConnectionError(result.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
navigate("/login")
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<LoginContainer
|
||||
title='Récupération du compte'
|
||||
error={connectionError}>
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Adresse courriel"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<LoadingButton
|
||||
loading={isConnecting}
|
||||
onClick={reset}
|
||||
variant="contained"
|
||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!email}
|
||||
>
|
||||
Réinitialiser le mot de passe
|
||||
</LoadingButton>
|
||||
|
||||
</LoginContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.provider-btn {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
color: black;
|
||||
margin: 4px 0 4px 0;
|
||||
}
|
||||
|
||||
.provider-btn:hover {
|
||||
background-color: #dbdbdb;
|
||||
border: 1px solid #ccc;
|
||||
color: black;
|
||||
margin: 4px 0 4px 0;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
.login-links {
|
||||
padding-top: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-links a {
|
||||
padding: 4px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
@ -61,25 +61,6 @@
|
|||
align-items: end;
|
||||
}
|
||||
|
||||
.auth-selection-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.auth-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #5271ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.auth-btn:hover {
|
||||
background-color: #5976fa;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.btn-container {
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ENV_VARIABLES } from 'src/constants';
|
|||
|
||||
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
|
||||
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
||||
import webSocketService, {AnswerSubmissionToBackendType} from '../../../services/WebsocketService';
|
||||
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
|
||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
||||
|
||||
import './joinRoom.css';
|
||||
|
|
@ -13,35 +13,18 @@ import { QuestionType } from '../../../Types/QuestionType';
|
|||
import { TextField } from '@mui/material';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
|
||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer';
|
||||
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export type AnswerType = Array<string | number | boolean>;
|
||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
||||
|
||||
const JoinRoom: React.FC = () => {
|
||||
const [roomName, setRoomName] = useState('');
|
||||
const [username, setUsername] = useState(ApiService.getUsername());
|
||||
const [username, setUsername] = useState('');
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [isWaitingForTeacher, setIsWaitingForTeacher] = useState(false);
|
||||
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);
|
||||
const [isQRCodeJoin, setIsQRCodeJoin] = useState(false);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const roomFromUrl = searchParams.get('roomName');
|
||||
if (roomFromUrl) {
|
||||
setRoomName(roomFromUrl);
|
||||
setIsQRCodeJoin(true);
|
||||
console.log('Mode QR Code détecté, salle:', roomFromUrl);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
handleCreateSocket();
|
||||
|
|
@ -50,41 +33,23 @@ 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);
|
||||
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
|
||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
|
||||
socket.on('join-success', (roomJoinedName) => {
|
||||
socket.on('join-success', () => {
|
||||
setIsWaitingForTeacher(true);
|
||||
setIsConnecting(false);
|
||||
console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`);
|
||||
console.log('Successfully joined the room.');
|
||||
});
|
||||
socket.on('next-question', (question: QuestionType) => {
|
||||
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]);
|
||||
});
|
||||
|
|
@ -113,7 +78,6 @@ const JoinRoom: React.FC = () => {
|
|||
};
|
||||
|
||||
const disconnect = () => {
|
||||
// localStorage.clear();
|
||||
webSocketService.disconnect();
|
||||
setSocket(null);
|
||||
setQuestion(undefined);
|
||||
|
|
@ -132,35 +96,19 @@ const JoinRoom: React.FC = () => {
|
|||
}
|
||||
|
||||
if (username && roomName) {
|
||||
console.log(`Tentative de rejoindre : ${roomName}, utilisateur : ${username}`);
|
||||
|
||||
webSocketService.joinRoom(roomName, username);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => {
|
||||
console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`);
|
||||
const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => {
|
||||
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);
|
||||
};
|
||||
|
||||
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && username && roomName) {
|
||||
handleSocket();
|
||||
}
|
||||
webSocketService.submitAnswer(answerData);
|
||||
};
|
||||
|
||||
if (isWaitingForTeacher) {
|
||||
|
|
@ -191,7 +139,6 @@ const JoinRoom: React.FC = () => {
|
|||
return (
|
||||
<StudentModeQuiz
|
||||
questions={questions}
|
||||
answers={answers}
|
||||
submitAnswer={handleOnSubmitAnswer}
|
||||
disconnectWebSocket={disconnect}
|
||||
/>
|
||||
|
|
@ -201,7 +148,6 @@ const JoinRoom: React.FC = () => {
|
|||
question && (
|
||||
<TeacherModeQuiz
|
||||
questionInfos={question}
|
||||
answers={answers}
|
||||
submitAnswer={handleOnSubmitAnswer}
|
||||
disconnectWebSocket={disconnect}
|
||||
/>
|
||||
|
|
@ -210,25 +156,20 @@ const JoinRoom: React.FC = () => {
|
|||
default:
|
||||
return (
|
||||
<LoginContainer
|
||||
title={isQRCodeJoin ? `Rejoindre la salle ${roomName}` : 'Rejoindre une salle'}
|
||||
error={connectionError}
|
||||
>
|
||||
{/* Afficher champ salle SEULEMENT si pas de QR code */}
|
||||
{!isQRCodeJoin && (
|
||||
<TextField
|
||||
type="text"
|
||||
label="Nom de la salle"
|
||||
variant="outlined"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value.toUpperCase())}
|
||||
placeholder="Nom de la salle"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth={true}
|
||||
onKeyDown={handleReturnKey}
|
||||
/>
|
||||
)}
|
||||
title='Rejoindre une salle'
|
||||
error={connectionError}>
|
||||
|
||||
<TextField
|
||||
type="number"
|
||||
label="Numéro de la salle"
|
||||
variant="outlined"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value)}
|
||||
placeholder="Numéro de la salle"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{/* Champ username toujours visible */}
|
||||
<TextField
|
||||
label="Nom d'utilisateur"
|
||||
variant="outlined"
|
||||
|
|
@ -236,8 +177,7 @@ const JoinRoom: React.FC = () => {
|
|||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Nom d'utilisateur"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth={true}
|
||||
onKeyDown={handleReturnKey}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<LoadingButton
|
||||
|
|
@ -245,10 +185,9 @@ const JoinRoom: React.FC = () => {
|
|||
onClick={handleSocket}
|
||||
variant="contained"
|
||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!username || (isQRCodeJoin && !roomName)}
|
||||
>
|
||||
{isQRCodeJoin ? 'Rejoindre avec QR Code' : 'Rejoindre'}
|
||||
</LoadingButton>
|
||||
disabled={!username || !roomName}
|
||||
>Rejoindre</LoadingButton>
|
||||
|
||||
</LoginContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,8 @@ import ApiService from '../../../services/ApiService';
|
|||
import './dashboard.css';
|
||||
import ImportModal from 'src/components/ImportModal/ImportModal';
|
||||
//import axios from 'axios';
|
||||
import { RoomType } from 'src/Types/RoomType';
|
||||
// import { useRooms } from '../ManageRoom/RoomContext';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
|
|
@ -28,7 +23,6 @@ import {
|
|||
NativeSelect,
|
||||
CardContent,
|
||||
styled,
|
||||
DialogContentText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search,
|
||||
|
|
@ -37,10 +31,11 @@ import {
|
|||
Upload,
|
||||
FolderCopy,
|
||||
ContentCopy,
|
||||
Edit
|
||||
Edit,
|
||||
Share,
|
||||
// DriveFileMove
|
||||
} from '@mui/icons-material';
|
||||
import DownloadQuizModal from 'src/components/DownloadQuizModal/DownloadQuizModal';
|
||||
import ShareQuizModal from 'src/components/ShareQuizModal/ShareQuizModal';
|
||||
|
||||
// Create a custom-styled Card component
|
||||
const CustomCard = styled(Card)({
|
||||
|
|
@ -48,7 +43,7 @@ const CustomCard = styled(Card)({
|
|||
position: 'relative',
|
||||
margin: '40px 0 20px 0', // Add top margin to make space for the tab
|
||||
borderRadius: '8px',
|
||||
paddingTop: '20px' // Ensure content inside the card doesn't overlap with the tab
|
||||
paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
|
||||
});
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
|
|
@ -58,14 +53,6 @@ const Dashboard: React.FC = () => {
|
|||
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
|
||||
const [rooms, setRooms] = useState<RoomType[]>([]);
|
||||
const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false);
|
||||
const [newRoomTitle, setNewRoomTitle] = useState('');
|
||||
// const { selectedRoom, selectRoom, createRoom } = useRooms();
|
||||
const [selectedRoom, selectRoom] = useState<RoomType>(); // menu
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||
|
||||
// Filter quizzes based on search term
|
||||
// const filteredQuizzes = quizzes.filter(quiz =>
|
||||
|
|
@ -78,6 +65,7 @@ const Dashboard: React.FC = () => {
|
|||
);
|
||||
}, [quizzes, searchTerm]);
|
||||
|
||||
|
||||
// Group quizzes by folder
|
||||
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
||||
if (!acc[quiz.folderName]) {
|
||||
|
|
@ -89,83 +77,28 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const isLoggedIn = await ApiService.isLoggedIn();
|
||||
console.log(`Dashboard: isLoggedIn: ${isLoggedIn}`);
|
||||
if (!isLoggedIn) {
|
||||
navigate('/teacher/login');
|
||||
if (!ApiService.isLoggedIn()) {
|
||||
navigate("/teacher/login");
|
||||
return;
|
||||
} else {
|
||||
const userRooms = await ApiService.getUserRooms();
|
||||
setRooms(userRooms as RoomType[]);
|
||||
|
||||
}
|
||||
else {
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
|
||||
setFolders(userFolders as FolderType[]);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rooms.length > 0 && !selectedRoom) {
|
||||
selectRoom(rooms[rooms.length - 1]);
|
||||
localStorage.setItem('selectedRoomId', rooms[rooms.length - 1]._id);
|
||||
}
|
||||
}, [rooms, selectedRoom]);
|
||||
|
||||
const handleSelectRoom = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (event.target.value === 'add-room') {
|
||||
setOpenAddRoomDialog(true);
|
||||
} else {
|
||||
selectRoomByName(event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearchVisibility = () => {
|
||||
setIsSearchVisible(!isSearchVisible);
|
||||
};
|
||||
|
||||
// Créer une salle
|
||||
const createRoom = async (title: string) => {
|
||||
// Créer la salle et récupérer l'objet complet
|
||||
const newRoom = await ApiService.createRoom(title);
|
||||
|
||||
// Mettre à jour la liste des salles
|
||||
const updatedRooms = await ApiService.getUserRooms();
|
||||
setRooms(updatedRooms as RoomType[]);
|
||||
|
||||
// Sélectionner la nouvelle salle avec son ID
|
||||
selectRoomByName(newRoom); // Utiliser l'ID de l'objet retourné
|
||||
};
|
||||
|
||||
// Sélectionner une salle
|
||||
const selectRoomByName = (roomId: string) => {
|
||||
const room = rooms.find((r) => r._id === roomId);
|
||||
selectRoom(room);
|
||||
localStorage.setItem('selectedRoomId', roomId);
|
||||
};
|
||||
|
||||
const handleCreateRoom = async () => {
|
||||
if (newRoomTitle.trim()) {
|
||||
try {
|
||||
await createRoom(newRoomTitle);
|
||||
const userRooms = await ApiService.getUserRooms();
|
||||
setRooms(userRooms as RoomType[]);
|
||||
setOpenAddRoomDialog(false);
|
||||
setNewRoomTitle('');
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
setShowErrorDialog(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedFolderId(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQuizzesForFolder = async () => {
|
||||
|
||||
if (selectedFolderId == '') {
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
//console.log("show all quizzes")
|
||||
|
|
@ -176,29 +109,33 @@ const Dashboard: React.FC = () => {
|
|||
//console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
||||
// add the folder.title to the QuizType if the folderQuizzes is an array
|
||||
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
||||
}
|
||||
|
||||
setQuizzes(quizzes as QuizType[]);
|
||||
} else {
|
||||
console.log('show some quizzes');
|
||||
}
|
||||
else {
|
||||
console.log("show some quizzes")
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||
console.log('folderQuizzes: ', folderQuizzes);
|
||||
console.log("folderQuizzes: ", folderQuizzes);
|
||||
// get the folder title from its id
|
||||
const folderTitle =
|
||||
folders.find((folder) => folder._id === selectedFolderId)?.title || '';
|
||||
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
|
||||
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
|
||||
setQuizzes(folderQuizzes as QuizType[]);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
fetchQuizzesForFolder();
|
||||
}, [selectedFolderId]);
|
||||
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveQuiz = async (quiz: QuizType) => {
|
||||
try {
|
||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce quiz?');
|
||||
|
|
@ -212,27 +149,30 @@ const Dashboard: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
||||
try {
|
||||
await ApiService.duplicateQuiz(quiz._id);
|
||||
if (selectedFolderId == '') {
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
console.log('show all quizzes');
|
||||
console.log("show all quizzes")
|
||||
let quizzes: QuizType[] = [];
|
||||
|
||||
for (const folder of folders as FolderType[]) {
|
||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||
console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
|
||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
||||
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||
}
|
||||
|
||||
setQuizzes(quizzes as QuizType[]);
|
||||
} else {
|
||||
console.log('show some quizzes');
|
||||
}
|
||||
else {
|
||||
console.log("show some quizzes")
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
|
||||
setQuizzes(folderQuizzes as QuizType[]);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error duplicating quiz:', error);
|
||||
|
|
@ -241,6 +181,7 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
const handleOnImport = () => {
|
||||
setShowImportModal(true);
|
||||
|
||||
};
|
||||
|
||||
const validateQuiz = (questions: string[]) => {
|
||||
|
|
@ -252,10 +193,11 @@ const Dashboard: React.FC = () => {
|
|||
// Otherwise the quiz is invalid
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
try {
|
||||
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
|
||||
const parsedItem = parse(questions[i]);
|
||||
Template(parsedItem[0]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
console.error('Error parsing question:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -272,6 +214,7 @@ const Dashboard: React.FC = () => {
|
|||
setFolders(userFolders as FolderType[]);
|
||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||
setSelectedFolderId(newlyCreatedFolder._id);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating folder:', error);
|
||||
|
|
@ -279,6 +222,7 @@ const Dashboard: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleDeleteFolder = async () => {
|
||||
|
||||
try {
|
||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
||||
if (confirmed) {
|
||||
|
|
@ -288,17 +232,18 @@ const Dashboard: React.FC = () => {
|
|||
}
|
||||
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
console.log('show all quizzes');
|
||||
console.log("show all quizzes")
|
||||
let quizzes: QuizType[] = [];
|
||||
|
||||
for (const folder of folders as FolderType[]) {
|
||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||
console.log('folder: ', folder.title, ' quiz: ', folderQuizzes);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
||||
}
|
||||
|
||||
setQuizzes(quizzes as QuizType[]);
|
||||
setSelectedFolderId('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting folder:', error);
|
||||
}
|
||||
|
|
@ -308,10 +253,7 @@ const Dashboard: React.FC = () => {
|
|||
try {
|
||||
// folderId: string GET THIS FROM CURRENT FOLDER
|
||||
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
||||
const newTitle = prompt(
|
||||
'Entrée le nouveau nom du fichier',
|
||||
folders.find((folder) => folder._id === selectedFolderId)?.title
|
||||
);
|
||||
const newTitle = prompt('Entrée le nouveau nom du fichier', folders.find((folder) => folder._id === selectedFolderId)?.title);
|
||||
if (newTitle) {
|
||||
const renamedFolderId = selectedFolderId;
|
||||
const result = await ApiService.renameFolder(selectedFolderId, newTitle);
|
||||
|
|
@ -348,296 +290,152 @@ const Dashboard: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleCreateQuiz = () => {
|
||||
navigate('/teacher/editor-quiz/new');
|
||||
};
|
||||
navigate("/teacher/editor-quiz/new");
|
||||
}
|
||||
|
||||
const handleEditQuiz = (quiz: QuizType) => {
|
||||
navigate(`/teacher/editor-quiz/${quiz._id}`);
|
||||
};
|
||||
}
|
||||
|
||||
const handleLancerQuiz = (quiz: QuizType) => {
|
||||
if (selectedRoom) {
|
||||
navigate(`/teacher/manage-room/${quiz._id}/${selectedRoom.title}`);
|
||||
} else {
|
||||
const randomSixDigit = Math.floor(100000 + Math.random() * 900000);
|
||||
navigate(`/teacher/manage-room/${quiz._id}/${randomSixDigit}`);
|
||||
navigate(`/teacher/manage-room/${quiz._id}`);
|
||||
}
|
||||
|
||||
const handleShareQuiz = async (quiz: QuizType) => {
|
||||
try {
|
||||
const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, "");
|
||||
|
||||
if (email) {
|
||||
const result = await ApiService.ShareQuiz(quiz._id, email);
|
||||
|
||||
if (!result) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(`Quiz partagé avec succès!`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du partage du quiz:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
{/* Conteneur pour le titre et le sélecteur de salle */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
{/* Titre tableau de bord */}
|
||||
<div className="title" style={{ fontSize: '30px', fontWeight: 'bold' }}>
|
||||
Tableau de bord
|
||||
</div>
|
||||
|
||||
{/* Sélecteur de salle */}
|
||||
<div
|
||||
className="roomSelection"
|
||||
style={{ display: 'flex', justifyContent: 'flex-end', gap: '15px' }}
|
||||
>
|
||||
<select
|
||||
value={selectedRoom?._id || ''}
|
||||
onChange={(e) => handleSelectRoom(e)}
|
||||
id="room-select"
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
fontSize: '14px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
backgroundColor: '#fff',
|
||||
maxWidth: '200px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Sélectionner une salle
|
||||
</option>
|
||||
{rooms.map((room) => (
|
||||
<option key={room._id} value={room._id}>
|
||||
{room.title}
|
||||
</option>
|
||||
))}
|
||||
<option
|
||||
value="add-room"
|
||||
style={{
|
||||
color: 'black',
|
||||
backgroundColor: '#f0f0f0',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
Ajouter une salle
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="dashboard">
|
||||
|
||||
<div className="title">Tableau de bord</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<TextField
|
||||
onChange={handleSearch}
|
||||
value={searchTerm}
|
||||
placeholder="Rechercher un quiz par son titre"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton>
|
||||
<Search />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dialog pour créer une salle */}
|
||||
<Dialog open={openAddRoomDialog} onClose={() => setOpenAddRoomDialog(false)}>
|
||||
<DialogTitle>Créer une nouvelle salle</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
value={newRoomTitle}
|
||||
onChange={(e) => setNewRoomTitle(e.target.value.toUpperCase())}
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenAddRoomDialog(false)}>Annuler</Button>
|
||||
<Button onClick={handleCreateRoom}>Créer</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Dialog open={showErrorDialog} onClose={() => setShowErrorDialog(false)}>
|
||||
<DialogTitle>Erreur</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{errorMessage}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowErrorDialog(false)}>Fermer</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
gap: '20px'
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Conteneur principal avec les actions et la liste des quiz */}
|
||||
<div className="folder">
|
||||
<div className="select">
|
||||
<div className='folder'>
|
||||
<div className='select'>
|
||||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolderId}
|
||||
onChange={handleSelectFolder}
|
||||
sx={{
|
||||
padding: '6px 12px',
|
||||
maxWidth: '180px',
|
||||
borderRadius: '8px',
|
||||
borderColor: '#e0e0e0',
|
||||
'&:hover': { borderColor: '#5271FF' }
|
||||
}}
|
||||
>
|
||||
<option value="">Tous les dossiers...</option>
|
||||
{folders.map((folder) => (
|
||||
<option value={folder._id} key={folder._id}>
|
||||
{folder.title}
|
||||
</option>
|
||||
<option value=""> Tous les dossiers... </option>
|
||||
|
||||
{folders.map((folder: FolderType) => (
|
||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<div className='actions'>
|
||||
<Tooltip title="Ajouter dossier" placement="top">
|
||||
<IconButton color="primary" onClick={handleCreateFolder}>
|
||||
{' '}
|
||||
<Add />{' '}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleCreateFolder}
|
||||
> <Add /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Renommer dossier" placement="top">
|
||||
<div>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleRenameFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
>
|
||||
{' '}
|
||||
<Edit />{' '}
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleRenameFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <Edit /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Dupliquer dossier" placement="top">
|
||||
<div>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDuplicateFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
>
|
||||
{' '}
|
||||
<FolderCopy />{' '}
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDuplicateFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <FolderCopy /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Supprimer dossier" placement="top">
|
||||
<div>
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={handleDeleteFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
>
|
||||
{' '}
|
||||
<DeleteOutline />{' '}
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={handleDeleteFolder}
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <DeleteOutline /> </IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="search-bar"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '20px',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
{!isSearchVisible ? (
|
||||
<IconButton
|
||||
onClick={toggleSearchVisibility}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fff',
|
||||
color: '#5271FF'
|
||||
}}
|
||||
>
|
||||
<Search />
|
||||
</IconButton>
|
||||
) : (
|
||||
<TextField
|
||||
onChange={handleSearch}
|
||||
value={searchTerm}
|
||||
placeholder="Rechercher un quiz"
|
||||
fullWidth
|
||||
autoFocus
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fff',
|
||||
fontWeight: 500,
|
||||
width: '100%',
|
||||
maxWidth: '1000px'
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={toggleSearchVisibility}
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
backgroundColor: '#fff',
|
||||
color: '#5271FF'
|
||||
}}
|
||||
>
|
||||
<Search />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='ajouter'>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<Add />}
|
||||
onClick={handleCreateQuiz}
|
||||
>
|
||||
Ajouter un nouveau quiz
|
||||
</Button>
|
||||
|
||||
{/* À droite : les boutons */}
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<Add />}
|
||||
onClick={handleCreateQuiz}
|
||||
sx={{ borderRadius: '8px', minWidth: 'auto', padding: '4px 12px' }}
|
||||
>
|
||||
Nouveau quiz
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<Upload />}
|
||||
onClick={handleOnImport}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<Upload />}
|
||||
onClick={handleOnImport}
|
||||
>
|
||||
Importer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="list">
|
||||
{Object.keys(quizzesByFolder).map((folderName) => (
|
||||
<CustomCard key={folderName} className="folder-card">
|
||||
<div className="folder-tab">{folderName}</div>
|
||||
<div className='list'>
|
||||
{Object.keys(quizzesByFolder).map(folderName => (
|
||||
<CustomCard key={folderName} className='folder-card'>
|
||||
<div className='folder-tab'>{folderName}</div>
|
||||
<CardContent>
|
||||
{quizzesByFolder[folderName].map((quiz: QuizType) => (
|
||||
<div className="quiz" key={quiz._id}>
|
||||
<div className="title">
|
||||
<Tooltip title="Démarrer" placement="top">
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => handleLancerQuiz(quiz)}
|
||||
disabled={!validateQuiz(quiz.content)}
|
||||
>
|
||||
{`${quiz.title} (${
|
||||
quiz.content.length
|
||||
} question${
|
||||
quiz.content.length > 1 ? 's' : ''
|
||||
})`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='quiz' key={quiz._id}>
|
||||
<div className='title'>
|
||||
<Tooltip title="Lancer quiz" placement="top">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => handleLancerQuiz(quiz)}
|
||||
disabled={!validateQuiz(quiz.content)}
|
||||
>
|
||||
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
|
|
@ -646,38 +444,33 @@ const Dashboard: React.FC = () => {
|
|||
<DownloadQuizModal quiz={quiz} />
|
||||
</div>
|
||||
|
||||
<Tooltip title="Modifier" placement="top">
|
||||
<Tooltip title="Modifier quiz" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleEditQuiz(quiz)}
|
||||
>
|
||||
{' '}
|
||||
<Edit />{' '}
|
||||
</IconButton>
|
||||
> <Edit /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Dupliquer" placement="top">
|
||||
<Tooltip title="Dupliquer quiz" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleDuplicateQuiz(quiz)}
|
||||
>
|
||||
{' '}
|
||||
<ContentCopy />{' '}
|
||||
</IconButton>
|
||||
> <ContentCopy /> </IconButton>
|
||||
</Tooltip>
|
||||
<div className="quiz-share">
|
||||
<ShareQuizModal quiz={quiz} />
|
||||
</div>
|
||||
|
||||
<Tooltip title="Supprimer" placement="top">
|
||||
<Tooltip title="Supprimer quiz" placement="top">
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
color="error"
|
||||
color="primary"
|
||||
onClick={() => handleRemoveQuiz(quiz)}
|
||||
>
|
||||
{' '}
|
||||
<DeleteOutline />{' '}
|
||||
</IconButton>
|
||||
> <DeleteOutline /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Partager quiz" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleShareQuiz(quiz)}
|
||||
> <Share /> </IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -692,6 +485,7 @@ const Dashboard: React.FC = () => {
|
|||
handleOnImport={handleOnImport}
|
||||
selectedFolder={selectedFolderId}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -704,3 +498,4 @@ function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName:
|
|||
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// EditorQuiz.tsx
|
||||
import React, { useState, useEffect, CSSProperties } from 'react';
|
||||
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { FolderType } from '../../../Types/FolderType';
|
||||
|
|
@ -9,15 +9,14 @@ import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
|||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
||||
|
||||
import { QuizType } from '../../../Types/QuizType';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
import './editorQuiz.css';
|
||||
import { Button, TextField, NativeSelect, Divider } from '@mui/material';
|
||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||
import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal';
|
||||
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import { escapeForGIFT } from '../../../utils/giftUtils';
|
||||
import { ENV_VARIABLES } from 'src/constants';
|
||||
import { Upload } from '@mui/icons-material';
|
||||
|
||||
interface EditQuizParams {
|
||||
id: string;
|
||||
|
|
@ -39,6 +38,8 @@ const QuizForm: React.FC = () => {
|
|||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedFolder(event.target.value);
|
||||
};
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
|
||||
const scrollToTop = () => {
|
||||
|
|
@ -117,11 +118,7 @@ const QuizForm: React.FC = () => {
|
|||
setValue(value);
|
||||
}
|
||||
|
||||
// split value when there is at least one blank line
|
||||
const linesArray = value.split(/\n{2,}/);
|
||||
|
||||
// if the first item in linesArray is blank, remove it
|
||||
if (linesArray[0] === '') linesArray.shift();
|
||||
const linesArray = value.split(/(?<=^|[^\\]}.*)[\n]+/);
|
||||
|
||||
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
|
||||
|
||||
|
|
@ -165,75 +162,87 @@ const QuizForm: React.FC = () => {
|
|||
return <div>Chargement...</div>;
|
||||
}
|
||||
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
||||
|
||||
if (!inputElement?.files || inputElement.files.length === 0) {
|
||||
setDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inputElement.files || inputElement.files.length === 0) {
|
||||
window.alert("Veuillez d'abord choisir une image à téléverser.")
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = await ApiService.uploadImage(inputElement.files[0]);
|
||||
|
||||
// Check for errors
|
||||
if(imageUrl.indexOf("ERROR") >= 0) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
return;
|
||||
}
|
||||
|
||||
setImageLinks(prevLinks => [...prevLinks, imageUrl]);
|
||||
|
||||
// Reset the file input element
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = async (link: string) => {
|
||||
navigator.clipboard.writeText(link);
|
||||
}
|
||||
|
||||
const handleCopyImage = (id: string) => {
|
||||
const escLink = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`;
|
||||
setImageLinks(prevLinks => [...prevLinks, escLink]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="quizEditor">
|
||||
<div
|
||||
className="editHeader"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '32px'
|
||||
}}
|
||||
>
|
||||
<div className='quizEditor'>
|
||||
|
||||
<div className='editHeader'>
|
||||
<ReturnButton
|
||||
askConfirm
|
||||
message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleQuizSave}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<SaveIcon sx={{ fontSize: 20 }} />
|
||||
Enregistrer
|
||||
</Button>
|
||||
</div>
|
||||
<div className='title'>Éditeur de quiz</div>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: '30px' }}>
|
||||
<div className="title">Éditeur de quiz</div>
|
||||
<div className='dumb'></div>
|
||||
</div>
|
||||
|
||||
{/* <h2 className="subtitle">Éditeur</h2> */}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<TextField
|
||||
onChange={handleQuizTitleChange}
|
||||
value={quizTitle}
|
||||
color="primary"
|
||||
placeholder="Titre du quiz"
|
||||
label="Titre du quiz"
|
||||
sx={{ width: '200px', marginTop: '50px' }}
|
||||
/>
|
||||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolder}
|
||||
onChange={handleSelectFolder}
|
||||
disabled={!isNewQuiz}
|
||||
style={{ marginBottom: '16px', width: '200px', marginTop: '10px' }}
|
||||
<TextField
|
||||
onChange={handleQuizTitleChange}
|
||||
value={quizTitle}
|
||||
placeholder="Titre du quiz"
|
||||
label="Titre du quiz"
|
||||
fullWidth
|
||||
/>
|
||||
<label>Choisir un dossier:
|
||||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolder}
|
||||
onChange={handleSelectFolder}
|
||||
disabled={!isNewQuiz}
|
||||
style={{ marginBottom: '16px' }} // Ajout de marge en bas
|
||||
>
|
||||
<option disabled value="">
|
||||
Choisir un dossier...
|
||||
</option>
|
||||
{folders.map((folder: FolderType) => (
|
||||
<option value={folder._id} key={folder._id}>
|
||||
{folder.title}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<option disabled value=""> Choisir un dossier... </option>
|
||||
|
||||
{folders.map((folder: FolderType) => (
|
||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
||||
))}
|
||||
</NativeSelect></label>
|
||||
|
||||
<Button variant="contained" onClick={handleQuizSave}>
|
||||
Enregistrer
|
||||
</Button>
|
||||
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
|
||||
|
|
@ -246,11 +255,37 @@ const QuizForm: React.FC = () => {
|
|||
onEditorChange={handleUpdatePreview} />
|
||||
|
||||
<div className='images'>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<h4>Mes images :</h4>
|
||||
<ImageGalleryModal handleCopy={handleCopyImage} />
|
||||
<div className='upload'>
|
||||
<label className="dropArea">
|
||||
<input type="file" id="file-input" className="file-input"
|
||||
accept="image/jpeg, image/png"
|
||||
multiple
|
||||
ref={fileInputRef} />
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label='Téléverser'
|
||||
onClick={handleSaveImage}>
|
||||
Téléverser <Upload />
|
||||
</Button>
|
||||
|
||||
</label>
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)} >
|
||||
<DialogTitle>Erreur</DialogTitle>
|
||||
<DialogContent>
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)} color="primary">
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<h4>Mes images :</h4>
|
||||
<div>
|
||||
<div>
|
||||
<div style={{ display: "inline" }}>(Voir section </div>
|
||||
|
|
@ -264,7 +299,7 @@ const QuizForm: React.FC = () => {
|
|||
</div>
|
||||
<ul>
|
||||
{imageLinks.map((link, index) => {
|
||||
const imgTag = `[markdown]} "texte de l'infobulle") {T}`;
|
||||
const imgTag = `} "texte de l'infobulle")`;
|
||||
return (
|
||||
<li key={index}>
|
||||
<code
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
// JoinRoom.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import './Login.css';
|
||||
|
|
@ -36,11 +38,6 @@ const Login: React.FC = () => {
|
|||
|
||||
};
|
||||
|
||||
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && email && password) {
|
||||
login();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginContainer
|
||||
|
|
@ -54,8 +51,7 @@ const Login: React.FC = () => {
|
|||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Adresse courriel"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth={true}
|
||||
onKeyDown={handleReturnKey} // Add this line as well
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
|
|
@ -66,8 +62,7 @@ const Login: React.FC = () => {
|
|||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mot de passe"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth={true}
|
||||
onKeyDown={handleReturnKey} // Add this line as well
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<LoadingButton
|
||||
|
|
|
|||
|
|
@ -1,150 +1,69 @@
|
|||
// ManageRoom.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { BaseQuestion, parse, Question } from 'gift-pegjs';
|
||||
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
|
||||
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
|
||||
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
|
||||
import webSocketService, {
|
||||
AnswerReceptionFromBackendType
|
||||
} from '../../../services/WebsocketService';
|
||||
// import { QuestionService } from '../../../services/QuestionService';
|
||||
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
|
||||
import { QuizType } from '../../../Types/QuizType';
|
||||
import GroupIcon from '@mui/icons-material/Group';
|
||||
|
||||
import './manageRoom.css';
|
||||
import QRCodeIcon from '@mui/icons-material/QrCode';
|
||||
import { ENV_VARIABLES } from 'src/constants';
|
||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||
import { Button } from '@mui/material';
|
||||
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
||||
import { Refresh, Error } from '@mui/icons-material';
|
||||
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
|
||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
||||
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
|
||||
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions
|
||||
} from '@mui/material';
|
||||
import { checkIfIsCorrect } from './useRooms';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
|
||||
const ManageRoom: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [roomName, setRoomName] = useState<string>('');
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [students, setStudents] = useState<StudentType[]>([]);
|
||||
const { quizId = '', roomName = '' } = useParams<{ quizId: string; roomName: string }>();
|
||||
const quizId = useParams<{ id: string }>();
|
||||
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
|
||||
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
||||
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
||||
const [connectingError, setConnectingError] = useState<string>('');
|
||||
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
||||
const [quizStarted, setQuizStarted] = useState<boolean>(false);
|
||||
const [formattedRoomName, setFormattedRoomName] = useState('');
|
||||
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null);
|
||||
const roomUrl = `${window.location.origin}/student/join-room?roomName=${roomName}`;
|
||||
const [showQrModal, setShowQrModal] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(roomUrl).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// 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 () => {
|
||||
if (!ApiService.isLoggedIn()) {
|
||||
navigate('/teacher/login');
|
||||
return;
|
||||
}
|
||||
};
|
||||
if (quizId.id) {
|
||||
const fetchquiz = async () => {
|
||||
|
||||
verifyLogin();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomName) {
|
||||
console.error('Room name is missing!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Joining room: ${roomName}`);
|
||||
}, [roomName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomName || !quizId) {
|
||||
window.alert(
|
||||
`Une erreur est survenue.\n La salle ou le quiz n'a pas été spécifié.\nVeuillez réessayer plus tard.`
|
||||
);
|
||||
console.error(`Room "${roomName}" or Quiz "${quizId}" not found.`);
|
||||
navigate('/teacher/dashboard');
|
||||
}
|
||||
if (roomName && !socket) {
|
||||
createWebSocketRoom();
|
||||
}
|
||||
return () => {
|
||||
disconnectWebSocket();
|
||||
};
|
||||
}, [roomName, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (quizId) {
|
||||
const fetchQuiz = async () => {
|
||||
const quiz = await ApiService.getQuiz(quizId);
|
||||
const quiz = await ApiService.getQuiz(quizId.id as string);
|
||||
|
||||
if (!quiz) {
|
||||
window.alert(
|
||||
`Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
|
||||
);
|
||||
console.error('Quiz not found for id:', quizId);
|
||||
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
||||
console.error('Quiz not found for id:', quizId.id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setQuiz(quiz as QuizType);
|
||||
|
||||
if (!socket) {
|
||||
console.log(`no socket in ManageRoom, creating one.`);
|
||||
createWebSocketRoom();
|
||||
}
|
||||
|
||||
// return () => {
|
||||
// webSocketService.disconnect();
|
||||
// };
|
||||
};
|
||||
|
||||
fetchQuiz();
|
||||
fetchquiz();
|
||||
|
||||
} else {
|
||||
window.alert(
|
||||
`Une erreur est survenue.\n Le quiz ${quizId} n'a pas été trouvé\nVeuillez réessayer plus tard`
|
||||
);
|
||||
console.error('Quiz not found for id:', quizId);
|
||||
window.alert(`Une erreur est survenue.\n Le quiz ${quizId.id} n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
||||
console.error('Quiz not found for id:', quizId.id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
|
@ -152,69 +71,76 @@ const ManageRoom: React.FC = () => {
|
|||
|
||||
const disconnectWebSocket = () => {
|
||||
if (socket) {
|
||||
webSocketService.endQuiz(formattedRoomName);
|
||||
webSocketService.endQuiz(roomName);
|
||||
webSocketService.disconnect();
|
||||
setSocket(null);
|
||||
setQuizQuestions(undefined);
|
||||
setCurrentQuestion(undefined);
|
||||
setStudents(new Array<StudentType>());
|
||||
setRoomName('');
|
||||
}
|
||||
};
|
||||
|
||||
const createWebSocketRoom = () => {
|
||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||
const roomNameUpper = roomName.toUpperCase();
|
||||
setFormattedRoomName(roomNameUpper);
|
||||
console.log(`Creating WebSocket room named ${roomNameUpper}`);
|
||||
console.log('Creating WebSocket room...');
|
||||
setConnectingError('');
|
||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
webSocketService.createRoom();
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
||||
console.error('ManageRoom: WebSocket connection error:', error);
|
||||
});
|
||||
|
||||
socket.on('create-success', (createdRoomName: string) => {
|
||||
console.log(`Room created: ${createdRoomName}`);
|
||||
socket.on('create-success', (roomName: string) => {
|
||||
setRoomName(roomName);
|
||||
});
|
||||
socket.on('create-failure', () => {
|
||||
console.log('Error creating room.');
|
||||
});
|
||||
|
||||
socket.on('user-joined', (student: StudentType) => {
|
||||
setNewlyConnectedUser(student);
|
||||
});
|
||||
console.log(`Student joined: name = ${student.name}, id = ${student.id}`);
|
||||
|
||||
setStudents((prevStudents) => [...prevStudents, student]);
|
||||
|
||||
if (quizMode === 'teacher') {
|
||||
webSocketService.nextQuestion(roomName, currentQuestion);
|
||||
} else if (quizMode === 'student') {
|
||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
||||
}
|
||||
});
|
||||
socket.on('join-failure', (message) => {
|
||||
setConnectingError(message);
|
||||
setSocket(null);
|
||||
});
|
||||
|
||||
socket.on('user-disconnected', (userId: string) => {
|
||||
console.log(`Student left: id = ${userId}`);
|
||||
setStudents((prevUsers) => prevUsers.filter((user) => user.id !== userId));
|
||||
});
|
||||
|
||||
setSocket(socket);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// This is here to make sure the correct value is sent when user join
|
||||
if (socket) {
|
||||
console.log(`Listening for submit-answer-room in room ${formattedRoomName}`);
|
||||
console.log(`Listening for user-joined in room ${roomName}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
socket.on('user-joined', (_student: StudentType) => {
|
||||
if (quizMode === 'teacher') {
|
||||
webSocketService.nextQuestion(roomName, currentQuestion);
|
||||
} else if (quizMode === 'student') {
|
||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
// handle the case where user submits an answer
|
||||
console.log(`Listening for submit-answer-room in room ${roomName}`);
|
||||
socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => {
|
||||
const { answer, idQuestion, idUser, username } = answerData;
|
||||
console.log(
|
||||
`Received answer from ${username} for question ${idQuestion}: ${answer}`
|
||||
);
|
||||
console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
|
||||
if (!quizQuestions) {
|
||||
console.log('Quiz questions not found (cannot update answers without them).');
|
||||
return;
|
||||
|
|
@ -222,6 +148,7 @@ const ManageRoom: React.FC = () => {
|
|||
|
||||
// Update the students state using the functional form of setStudents
|
||||
setStudents((prevStudents) => {
|
||||
// print the list of current student names
|
||||
console.log('Current students:');
|
||||
prevStudents.forEach((student) => {
|
||||
console.log(student.name);
|
||||
|
|
@ -232,35 +159,21 @@ const ManageRoom: React.FC = () => {
|
|||
console.log(`Comparing ${student.id} to ${idUser}`);
|
||||
if (student.id === idUser) {
|
||||
foundStudent = true;
|
||||
const existingAnswer = student.answers.find(
|
||||
(ans) => ans.idQuestion === idQuestion
|
||||
);
|
||||
const existingAnswer = student.answers.find((ans) => ans.idQuestion === idQuestion);
|
||||
let updatedAnswers: Answer[] = [];
|
||||
if (existingAnswer) {
|
||||
// Update the existing answer
|
||||
updatedAnswers = student.answers.map((ans) => {
|
||||
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
|
||||
return ans.idQuestion === idQuestion
|
||||
? {
|
||||
...ans,
|
||||
answer,
|
||||
isCorrect: checkIfIsCorrect(
|
||||
answer,
|
||||
idQuestion,
|
||||
quizQuestions!
|
||||
)
|
||||
}
|
||||
: ans;
|
||||
return (ans.idQuestion === idQuestion ? { ...ans, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) } : ans);
|
||||
});
|
||||
} else {
|
||||
const newAnswer = {
|
||||
idQuestion,
|
||||
answer,
|
||||
isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!)
|
||||
};
|
||||
// Add a new answer
|
||||
const newAnswer = { idQuestion, answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!) };
|
||||
updatedAnswers = [...student.answers, newAnswer];
|
||||
}
|
||||
return { ...student, answers: updatedAnswers };
|
||||
}
|
||||
}
|
||||
return student;
|
||||
});
|
||||
if (!foundStudent) {
|
||||
|
|
@ -271,8 +184,73 @@ const ManageRoom: React.FC = () => {
|
|||
});
|
||||
setSocket(socket);
|
||||
}
|
||||
|
||||
}, [socket, currentQuestion, quizQuestions]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (socket) {
|
||||
// const submitAnswerHandler = (answerData: answerSubmissionType) => {
|
||||
// const { answer, idQuestion, username } = answerData;
|
||||
// console.log(`Received answer from ${username} for question ${idQuestion}: ${answer}`);
|
||||
|
||||
// // print the list of current student names
|
||||
// console.log('Current students:');
|
||||
// students.forEach((student) => {
|
||||
// console.log(student.name);
|
||||
// });
|
||||
|
||||
// // Update the students state using the functional form of setStudents
|
||||
// setStudents((prevStudents) => {
|
||||
// let foundStudent = false;
|
||||
// const updatedStudents = prevStudents.map((student) => {
|
||||
// if (student.id === username) {
|
||||
// foundStudent = true;
|
||||
// const updatedAnswers = student.answers.map((ans) => {
|
||||
// const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion, quizQuestions!), idQuestion };
|
||||
// console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`);
|
||||
// return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans);
|
||||
// }
|
||||
// );
|
||||
// return { ...student, answers: updatedAnswers };
|
||||
// }
|
||||
// return student;
|
||||
// });
|
||||
// if (!foundStudent) {
|
||||
// console.log(`Student ${username} not found in the list of students in LiveResults`);
|
||||
// }
|
||||
// return updatedStudents;
|
||||
// });
|
||||
|
||||
|
||||
// // make a copy of the students array so we can update it
|
||||
// // const updatedStudents = [...students];
|
||||
|
||||
// // const student = updatedStudents.find((student) => student.id === idUser);
|
||||
// // if (!student) {
|
||||
// // // this is a bad thing if an answer was submitted but the student isn't in the list
|
||||
// // console.log(`Student ${idUser} not found in the list of students in LiveResults`);
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
// // const isCorrect = checkIfIsCorrect(answer, idQuestion);
|
||||
// // const newAnswer: Answer = { answer, isCorrect, idQuestion };
|
||||
// // student.answers.push(newAnswer);
|
||||
// // // print list of answers
|
||||
// // console.log('Answers:');
|
||||
// // student.answers.forEach((answer) => {
|
||||
// // console.log(answer.answer);
|
||||
// // });
|
||||
// // setStudents(updatedStudents); // update the state
|
||||
// };
|
||||
|
||||
// socket.on('submit-answer', submitAnswerHandler);
|
||||
// return () => {
|
||||
// socket.off('submit-answer');
|
||||
// };
|
||||
// }
|
||||
// }, [socket]);
|
||||
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (!quizQuestions || !currentQuestion || !quiz?.content) return;
|
||||
|
||||
|
|
@ -281,12 +259,7 @@ const ManageRoom: React.FC = () => {
|
|||
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
|
||||
|
||||
setCurrentQuestion(quizQuestions[nextQuestionIndex]);
|
||||
webSocketService.nextQuestion({
|
||||
roomName: formattedRoomName,
|
||||
questions: quizQuestions,
|
||||
questionIndex: nextQuestionIndex,
|
||||
isLaunch: false
|
||||
});
|
||||
webSocketService.nextQuestion(roomName, quizQuestions[nextQuestionIndex]);
|
||||
};
|
||||
|
||||
const previousQuestion = () => {
|
||||
|
|
@ -296,12 +269,7 @@ const ManageRoom: React.FC = () => {
|
|||
|
||||
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
|
||||
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
|
||||
webSocketService.nextQuestion({
|
||||
roomName: formattedRoomName,
|
||||
questions: quizQuestions,
|
||||
questionIndex: prevQuestionIndex,
|
||||
isLaunch: false
|
||||
});
|
||||
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
|
||||
};
|
||||
|
||||
const initializeQuizQuestion = () => {
|
||||
|
|
@ -329,12 +297,7 @@ const ManageRoom: React.FC = () => {
|
|||
}
|
||||
|
||||
setCurrentQuestion(quizQuestions[0]);
|
||||
webSocketService.nextQuestion({
|
||||
roomName: formattedRoomName,
|
||||
questions: quizQuestions,
|
||||
questionIndex: 0,
|
||||
isLaunch: true
|
||||
});
|
||||
webSocketService.nextQuestion(roomName, quizQuestions[0]);
|
||||
};
|
||||
|
||||
const launchStudentMode = () => {
|
||||
|
|
@ -346,19 +309,15 @@ const ManageRoom: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
setQuizQuestions(quizQuestions);
|
||||
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
|
||||
webSocketService.launchStudentModeQuiz(roomName, quizQuestions);
|
||||
};
|
||||
|
||||
const launchQuiz = () => {
|
||||
setQuizStarted(true);
|
||||
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
|
||||
if (!socket || !roomName || !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}`
|
||||
);
|
||||
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
|
||||
return;
|
||||
}
|
||||
console.log(`Launching quiz in ${quizMode} mode...`);
|
||||
switch (quizMode) {
|
||||
case 'student':
|
||||
return launchStudentMode();
|
||||
|
|
@ -370,28 +329,74 @@ const ManageRoom: React.FC = () => {
|
|||
const showSelectedQuestion = (questionIndex: number) => {
|
||||
if (quiz?.content && quizQuestions) {
|
||||
setCurrentQuestion(quizQuestions[questionIndex]);
|
||||
|
||||
if (quizMode === 'teacher') {
|
||||
webSocketService.nextQuestion({
|
||||
roomName: formattedRoomName,
|
||||
questions: quizQuestions,
|
||||
questionIndex,
|
||||
isLaunch: false
|
||||
});
|
||||
webSocketService.nextQuestion(roomName, quizQuestions[questionIndex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const finishQuiz = () => {
|
||||
disconnectWebSocket();
|
||||
navigate('/teacher/dashboard');
|
||||
};
|
||||
|
||||
const handleReturn = () => {
|
||||
disconnectWebSocket();
|
||||
navigate('/teacher/dashboard');
|
||||
};
|
||||
|
||||
if (!formattedRoomName) {
|
||||
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number, questions: QuestionType[]): boolean {
|
||||
const questionInfo = questions.find((q) =>
|
||||
q.question.id ? q.question.id === idQuestion.toString() : false
|
||||
) as QuestionType | undefined;
|
||||
|
||||
const answerText = answer.toString();
|
||||
if (questionInfo) {
|
||||
const question = questionInfo.question as ParsedGIFTQuestion;
|
||||
if (question.type === 'TF') {
|
||||
return (
|
||||
(question.isTrue && answerText == 'true') ||
|
||||
(!question.isTrue && answerText == 'false')
|
||||
);
|
||||
} else if (question.type === 'MC') {
|
||||
return question.choices.some(
|
||||
(choice) => choice.isCorrect && choice.formattedText.text === answerText
|
||||
);
|
||||
} else if (question.type === 'Numerical') {
|
||||
if (isHighLowNumericalAnswer(question.choices[0])) {
|
||||
const choice = question.choices[0];
|
||||
const answerNumber = parseFloat(answerText);
|
||||
if (!isNaN(answerNumber)) {
|
||||
return (
|
||||
answerNumber <= choice.numberHigh &&
|
||||
answerNumber >= choice.numberLow
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isRangeNumericalAnswer(question.choices[0])) {
|
||||
const answerNumber = parseFloat(answerText);
|
||||
const range = question.choices[0].range;
|
||||
const correctAnswer = question.choices[0].number;
|
||||
if (!isNaN(answerNumber)) {
|
||||
return (
|
||||
answerNumber <= correctAnswer + range &&
|
||||
answerNumber >= correctAnswer - range
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isSimpleNumericalAnswer(question.choices[0])) {
|
||||
const answerNumber = parseFloat(answerText);
|
||||
if (!isNaN(answerNumber)) {
|
||||
return answerNumber === question.choices[0].number;
|
||||
}
|
||||
}
|
||||
} else if (question.type === 'Short') {
|
||||
return question.choices.some(
|
||||
(choice) => choice.text.toUpperCase() === answerText.toUpperCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!roomName) {
|
||||
return (
|
||||
<div className="center">
|
||||
{!connectingError ? (
|
||||
|
|
@ -414,114 +419,33 @@ const ManageRoom: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="room">
|
||||
{/* En-tête avec bouton Disconnect à gauche et QR code à droite */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<div className='room'>
|
||||
<div className='roomHeader'>
|
||||
|
||||
<DisconnectButton
|
||||
onReturn={handleReturn}
|
||||
askConfirm
|
||||
message={`Êtes-vous sûr de vouloir quitter?`}
|
||||
/>
|
||||
message={`Êtes-vous sûr de vouloir quitter?`} />
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setShowQrModal(true)}
|
||||
startIcon={<QRCodeIcon />}
|
||||
>
|
||||
Lien de participation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={showQrModal}
|
||||
onClose={() => setShowQrModal(false)}
|
||||
aria-labelledby="qr-modal-title"
|
||||
>
|
||||
<DialogTitle id="qr-modal-title">
|
||||
Rejoindre la salle: {formattedRoomName}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Scannez ce QR code ou partagez le lien ci-dessous pour rejoindre la salle :
|
||||
</DialogContentText>
|
||||
|
||||
<div style={{ textAlign: 'center', margin: '20px 0' }}>
|
||||
<QRCodeCanvas value={roomUrl} size={256} />
|
||||
</div>
|
||||
|
||||
<div style={{ wordBreak: 'break-all', textAlign: 'center' }}>
|
||||
<h3>URL de participation :</h3>
|
||||
<p>{roomUrl}</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ContentCopyIcon />}
|
||||
onClick={handleCopy}
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
{copied ? 'Copié !' : 'Copier le lien'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowQrModal(false)} color="primary">
|
||||
Fermer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<div className="roomHeader">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center' }}>
|
||||
Salle : {formattedRoomName}
|
||||
<div
|
||||
className="userCount subtitle"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginLeft: '20px',
|
||||
marginBottom: '0px'
|
||||
}}
|
||||
>
|
||||
<GroupIcon style={{ marginRight: '5px', verticalAlign: 'middle' }} />{' '}
|
||||
{students.length}/60
|
||||
</div>
|
||||
</h1>
|
||||
<div className='centerTitle'>
|
||||
<div className='title'>Salle: {roomName}</div>
|
||||
<div className='userCount subtitle'>Utilisateurs: {students.length}/60</div>
|
||||
</div>
|
||||
|
||||
<div className="dumb"></div>
|
||||
</div>
|
||||
<div className='dumb'></div>
|
||||
|
||||
</div>
|
||||
{/* the following breaks the css (if 'room' classes are nested) */}
|
||||
<div className="">
|
||||
<div className=''>
|
||||
|
||||
{quizQuestions ? (
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
|
||||
<div className="title center-h-align mb-2">{quiz?.title}</div>
|
||||
{!isNaN(Number(currentQuestion?.question.id)) && (
|
||||
<strong className="number of questions">
|
||||
Question {Number(currentQuestion?.question.id)}/
|
||||
{quizQuestions?.length}
|
||||
</strong>
|
||||
)}
|
||||
|
||||
{quizMode === 'teacher' && (
|
||||
|
||||
<div className="mb-1">
|
||||
{/* <QuestionNavigation
|
||||
currentQuestionId={Number(currentQuestion?.question.id)}
|
||||
|
|
@ -530,10 +454,12 @@ const ManageRoom: React.FC = () => {
|
|||
nextQuestion={nextQuestion}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
<div className="mb-2 flex-column-wrapper">
|
||||
<div className="preview-and-result-container">
|
||||
|
||||
{currentQuestion && (
|
||||
<QuestionDisplay
|
||||
showAnswer={false}
|
||||
|
|
@ -548,51 +474,42 @@ const ManageRoom: React.FC = () => {
|
|||
showSelectedQuestion={showSelectedQuestion}
|
||||
students={students}
|
||||
></LiveResultsComponent>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{quizMode === 'teacher' && (
|
||||
<div
|
||||
className="questionNavigationButtons"
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<div className="previousQuestionButton">
|
||||
<Button
|
||||
onClick={previousQuestion}
|
||||
variant="contained"
|
||||
disabled={Number(currentQuestion?.question.id) <= 1}
|
||||
>
|
||||
Question précédente
|
||||
</Button>
|
||||
</div>
|
||||
<div className="nextQuestionButton">
|
||||
<Button
|
||||
onClick={nextQuestion}
|
||||
variant="contained"
|
||||
disabled={
|
||||
Number(currentQuestion?.question.id) >=
|
||||
quizQuestions.length
|
||||
}
|
||||
>
|
||||
Prochaine question
|
||||
</Button>
|
||||
</div>
|
||||
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<div className="previousQuestionButton">
|
||||
<Button onClick={previousQuestion}
|
||||
variant="contained"
|
||||
disabled={Number(currentQuestion?.question.id) <= 1}>
|
||||
Question précédente
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="finishQuizButton">
|
||||
<Button onClick={finishQuiz} variant="contained">
|
||||
Terminer le quiz
|
||||
</Button>
|
||||
</div>
|
||||
<div className="nextQuestionButton">
|
||||
<Button onClick={nextQuestion}
|
||||
variant="contained"
|
||||
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
|
||||
>
|
||||
Prochaine question
|
||||
</Button>
|
||||
</div>
|
||||
</div> )}
|
||||
|
||||
</div>
|
||||
|
||||
) : (
|
||||
|
||||
<StudentWaitPage
|
||||
students={students}
|
||||
launchQuiz={launchQuiz}
|
||||
setQuizMode={setQuizMode}
|
||||
/>
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import { RoomType } from 'src/Types/RoomType';
|
||||
import React from "react";
|
||||
import { RoomContext } from './useRooms';
|
||||
|
||||
export const RoomProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [rooms, setRooms] = useState<RoomType[]>([]);
|
||||
const [selectedRoom, setSelectedRoom] = useState<RoomType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRooms = async () => {
|
||||
const userRooms = await ApiService.getUserRooms();
|
||||
const roomsList = userRooms as RoomType[];
|
||||
setRooms(roomsList);
|
||||
|
||||
const savedRoomId = localStorage.getItem('selectedRoomId');
|
||||
if (savedRoomId) {
|
||||
const savedRoom = roomsList.find(r => r._id === savedRoomId);
|
||||
if (savedRoom) {
|
||||
setSelectedRoom(savedRoom);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (roomsList.length > 0) {
|
||||
setSelectedRoom(roomsList[0]);
|
||||
localStorage.setItem('selectedRoomId', roomsList[0]._id);
|
||||
}
|
||||
};
|
||||
|
||||
loadRooms();
|
||||
}, []);
|
||||
|
||||
// Sélectionner une salle
|
||||
const selectRoom = (roomId: string) => {
|
||||
const room = rooms.find(r => r._id === roomId) || null;
|
||||
setSelectedRoom(room);
|
||||
localStorage.setItem('selectedRoomId', roomId);
|
||||
};
|
||||
|
||||
// Créer une salle
|
||||
const createRoom = async (title: string) => {
|
||||
// Créer la salle et récupérer l'objet complet
|
||||
const newRoom = await ApiService.createRoom(title);
|
||||
|
||||
// Mettre à jour la liste des salles
|
||||
const updatedRooms = await ApiService.getUserRooms();
|
||||
setRooms(updatedRooms as RoomType[]);
|
||||
|
||||
// Sélectionner la nouvelle salle avec son ID
|
||||
selectRoom(newRoom); // Utiliser l'ID de l'objet retourné
|
||||
};
|
||||
return (
|
||||
<RoomContext.Provider value={{ rooms, selectedRoom, selectRoom, createRoom }}>
|
||||
{children}
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,31 +2,25 @@
|
|||
.room .roomHeader {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-content: stretch
|
||||
}
|
||||
.room .roomHeader .returnButton {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
flex-basis: 10%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room .roomHeader .centerTitle {
|
||||
flex-basis: auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.room .roomHeader .headerContent {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 60px;
|
||||
|
||||
}
|
||||
|
||||
.room .roomHeader .dumb {
|
||||
|
|
@ -40,16 +34,152 @@
|
|||
|
||||
overflow: auto;
|
||||
justify-content: center;
|
||||
/* align-items: center; */
|
||||
}
|
||||
|
||||
.room .finishQuizButton {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* .create-room-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.manage-room-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.room h1 {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
.quiz-setup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.quiz-mode-selection {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.users-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: 2vh;
|
||||
}
|
||||
|
||||
.launch-quiz-btn {
|
||||
width: 20vw;
|
||||
height: 11vh;
|
||||
margin-top: 2vh;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mode-choice {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20vw;
|
||||
margin-top: 2vh;
|
||||
}
|
||||
|
||||
.user {
|
||||
background-color: #e7dad1;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.bottom-btn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2vh;
|
||||
}
|
||||
|
||||
.room-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 60vw;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 768px) {
|
||||
.room-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.room-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-name-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
}
|
||||
.user-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-column-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-and-result-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nextQuestionButton {
|
||||
align-self: flex-end;
|
||||
margin-bottom: 5rem !important;
|
||||
}
|
||||
|
||||
.top-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-height: 4000px) {
|
||||
.flex-column-wrapper {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-device-height: 1079px) {
|
||||
.flex-column-wrapper {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-device-height: 741px) {
|
||||
.flex-column-wrapper {
|
||||
height: 40vh;
|
||||
}
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
import { useContext } from 'react';
|
||||
import { RoomType } from 'src/Types/RoomType';
|
||||
import { createContext } from 'react';
|
||||
import { MultipleNumericalAnswer, NumericalAnswer, ParsedGIFTQuestion } from 'gift-pegjs';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
import {
|
||||
isSimpleNumericalAnswer,
|
||||
isRangeNumericalAnswer,
|
||||
isHighLowNumericalAnswer,
|
||||
isMultipleNumericalAnswer
|
||||
} from 'gift-pegjs/typeGuards';
|
||||
|
||||
type RoomContextType = {
|
||||
rooms: RoomType[];
|
||||
selectedRoom: RoomType | null;
|
||||
selectRoom: (roomId: string) => void;
|
||||
createRoom: (title: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const RoomContext = createContext<RoomContextType | undefined>(undefined);
|
||||
|
||||
export const useRooms = () => {
|
||||
const context = useContext(RoomContext);
|
||||
if (!context) throw new Error('useRooms must be used within a RoomProvider');
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the answer is correct - logic varies by type of question!
|
||||
* True/False: answer must match the isTrue property
|
||||
* Multiple Choice: answer must match the correct choice(s)
|
||||
* Numerical: answer must be within the range or equal to the number (for each type of correct answer)
|
||||
* Short Answer: answer must match the correct choice(s) (case-insensitive)
|
||||
* @param answer
|
||||
* @param idQuestion
|
||||
* @param questions
|
||||
* @returns
|
||||
*/
|
||||
export function checkIfIsCorrect(
|
||||
answer: AnswerType,
|
||||
idQuestion: number,
|
||||
questions: QuestionType[]
|
||||
): boolean {
|
||||
const questionInfo = questions.find((q) =>
|
||||
q.question.id ? q.question.id === idQuestion.toString() : false
|
||||
) as QuestionType | undefined;
|
||||
|
||||
const simpleAnswerText = answer.toString();
|
||||
if (questionInfo) {
|
||||
const question = questionInfo.question as ParsedGIFTQuestion;
|
||||
if (question.type === 'TF') {
|
||||
return (
|
||||
(question.isTrue && simpleAnswerText == 'true') ||
|
||||
(!question.isTrue && simpleAnswerText == 'false')
|
||||
);
|
||||
} else if (question.type === 'MC') {
|
||||
const correctChoices = question.choices.filter((choice) => choice.isCorrect
|
||||
/* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
|
||||
);
|
||||
const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
|
||||
if (correctChoices.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// check if all (and only) correct choices are in the multipleAnswers array
|
||||
return correctChoices.length === multipleAnswers.length && correctChoices.every(
|
||||
(choice) => multipleAnswers.includes(choice.formattedText.text)
|
||||
);
|
||||
} else if (question.type === 'Numerical') {
|
||||
if (isMultipleNumericalAnswer(question.choices[0])) { // Multiple numerical answers
|
||||
// check to see if answer[0] is a match for any of the choices that isCorrect
|
||||
const correctChoices = question.choices.filter((choice) => isMultipleNumericalAnswer(choice) && choice.isCorrect);
|
||||
if (correctChoices.length === 0) { // weird case where there are multiple numerical answers but none are correct
|
||||
return false;
|
||||
}
|
||||
return correctChoices.some((choice) => {
|
||||
// narrow choice to MultipleNumericalAnswer type
|
||||
const multipleNumericalChoice = choice as MultipleNumericalAnswer;
|
||||
return isCorrectNumericalAnswer(multipleNumericalChoice.answer, simpleAnswerText);
|
||||
});
|
||||
}
|
||||
if (isHighLowNumericalAnswer(question.choices[0])) {
|
||||
// const choice = question.choices[0];
|
||||
// const answerNumber = parseFloat(simpleAnswerText);
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return (
|
||||
// answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
|
||||
// );
|
||||
// }
|
||||
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
|
||||
}
|
||||
if (isRangeNumericalAnswer(question.choices[0])) {
|
||||
// const answerNumber = parseFloat(simpleAnswerText);
|
||||
// const range = question.choices[0].range;
|
||||
// const correctAnswer = question.choices[0].number;
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return (
|
||||
// answerNumber <= correctAnswer + range &&
|
||||
// answerNumber >= correctAnswer - range
|
||||
// );
|
||||
// }
|
||||
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
|
||||
}
|
||||
if (isSimpleNumericalAnswer(question.choices[0])) {
|
||||
// const answerNumber = parseFloat(simpleAnswerText);
|
||||
// if (!isNaN(answerNumber)) {
|
||||
// return answerNumber === question.choices[0].number;
|
||||
// }
|
||||
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
|
||||
}
|
||||
} else if (question.type === 'Short') {
|
||||
return question.choices.some(
|
||||
(choice) => choice.text.toUpperCase() === simpleAnswerText.toUpperCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a numerical answer is correct based on the type of numerical answer.
|
||||
* @param correctAnswer The correct answer (of type NumericalAnswer).
|
||||
* @param userAnswer The user's answer (as a string or number).
|
||||
* @returns True if the user's answer is correct, false otherwise.
|
||||
*/
|
||||
export function isCorrectNumericalAnswer(
|
||||
correctAnswer: NumericalAnswer,
|
||||
userAnswer: string | number
|
||||
): boolean {
|
||||
const answerNumber = typeof userAnswer === 'string' ? parseFloat(userAnswer) : userAnswer;
|
||||
|
||||
if (isNaN(answerNumber)) {
|
||||
return false; // User's answer is not a valid number
|
||||
}
|
||||
|
||||
if (isSimpleNumericalAnswer(correctAnswer)) {
|
||||
// Exact match for simple numerical answers
|
||||
return answerNumber === correctAnswer.number;
|
||||
}
|
||||
|
||||
if (isRangeNumericalAnswer(correctAnswer)) {
|
||||
// Check if the user's answer is within the range
|
||||
const { number, range } = correctAnswer;
|
||||
return answerNumber >= number - range && answerNumber <= number + range;
|
||||
}
|
||||
|
||||
if (isHighLowNumericalAnswer(correctAnswer)) {
|
||||
// Check if the user's answer is within the high-low range
|
||||
const { numberLow, numberHigh } = correctAnswer;
|
||||
return answerNumber >= numberLow && answerNumber <= numberHigh;
|
||||
}
|
||||
|
||||
// if (isMultipleNumericalAnswer(correctAnswer)) {
|
||||
// // Check if the user's answer matches any of the multiple numerical answers
|
||||
// return correctAnswer.answer.some((choice) =>
|
||||
// isCorrectNumericalAnswer(choice, answerNumber)
|
||||
// );
|
||||
// }
|
||||
|
||||
return false; // Default to false if the answer type is not recognized
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// JoinRoom.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import '../css/simpleLogin.css';
|
||||
import { TextField } from '@mui/material';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
|
||||
import LoginContainer from '../../../../components/LoginContainer/LoginContainer'
|
||||
import ApiService from '../../../../services/ApiService';
|
||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
||||
import ApiService from '../../../services/ApiService';
|
||||
|
||||
const SimpleLogin: React.FC = () => {
|
||||
const Register: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
|
@ -24,19 +25,21 @@ const SimpleLogin: React.FC = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const login = async () => {
|
||||
console.log(`SimpleLogin: login: email: ${email}, password: ${password}`);
|
||||
const result = await ApiService.login(email, password);
|
||||
if (result !== true) {
|
||||
const register = async () => {
|
||||
const result = await ApiService.register(email, password);
|
||||
|
||||
if (typeof result === 'string') {
|
||||
setConnectionError(result);
|
||||
return;
|
||||
}
|
||||
|
||||
navigate("/teacher/login")
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<LoginContainer
|
||||
title=''
|
||||
title='Créer un compte'
|
||||
error={connectionError}>
|
||||
|
||||
<TextField
|
||||
|
|
@ -44,6 +47,7 @@ const SimpleLogin: React.FC = () => {
|
|||
variant="outlined"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Adresse courriel"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
|
@ -51,38 +55,27 @@ const SimpleLogin: React.FC = () => {
|
|||
<TextField
|
||||
label="Mot de passe"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mot de passe"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<LoadingButton
|
||||
loading={isConnecting}
|
||||
onClick={login}
|
||||
onClick={register}
|
||||
variant="contained"
|
||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!email || !password}
|
||||
>
|
||||
Login
|
||||
S'inscrire
|
||||
</LoadingButton>
|
||||
|
||||
<div className="login-links">
|
||||
|
||||
|
||||
{/* <Link to="/resetPassword"> */}
|
||||
<del>Réinitialiser le mot de passe</del>
|
||||
{/* </Link> */}
|
||||
|
||||
<Link to="/register">
|
||||
Créer un compte
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
</LoginContainer>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleLogin;
|
||||
export default Register;
|
||||
|
|
@ -1,72 +1,66 @@
|
|||
// EditorQuiz.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { FolderType } from '../../../Types/FolderType';
|
||||
|
||||
|
||||
import './share.css';
|
||||
import { Button, NativeSelect, Typography, Box } from '@mui/material';
|
||||
import { Button, NativeSelect } from '@mui/material';
|
||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||
|
||||
import ApiService from '../../../services/ApiService';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
|
||||
const Share: React.FC = () => {
|
||||
console.log('Component rendered');
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<string>();
|
||||
|
||||
const [quizTitle, setQuizTitle] = useState('');
|
||||
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
||||
|
||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||
const [quizExists, setQuizExists] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
if (!id) {
|
||||
console.error('Quiz not found for id:', id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ApiService.isLoggedIn()) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const quizIds = await ApiService.getAllQuizIds();
|
||||
|
||||
if (quizIds.includes(id)) {
|
||||
setQuizExists(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
|
||||
if (userFolders.length == 0) {
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setFolders(userFolders as FolderType[]);
|
||||
|
||||
const title = await ApiService.getSharedQuiz(id);
|
||||
|
||||
if (!title) {
|
||||
console.error('Quiz not found for id:', id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setQuizTitle(title);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
setLoading(false);
|
||||
console.log("QUIZID : " + id)
|
||||
if (!id) {
|
||||
window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
||||
console.error('Quiz not found for id:', id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ApiService.isLoggedIn()) {
|
||||
window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`);
|
||||
navigate("/teacher/login");
|
||||
return;
|
||||
}
|
||||
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
|
||||
if (userFolders.length == 0) {
|
||||
window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`)
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setFolders(userFolders as FolderType[]);
|
||||
|
||||
const title = await ApiService.getSharedQuiz(id);
|
||||
|
||||
if (!title) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
console.error('Quiz not found for id:', id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
setQuizTitle(title);
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [id, navigate]);
|
||||
}, []);
|
||||
|
||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedFolder(event.target.value);
|
||||
|
|
@ -74,12 +68,14 @@ const Share: React.FC = () => {
|
|||
|
||||
const handleQuizSave = async () => {
|
||||
try {
|
||||
|
||||
if (selectedFolder == '') {
|
||||
alert("Veuillez choisir un dossier");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`)
|
||||
console.error('Quiz not found for id:', id);
|
||||
navigate('/teacher/dashboard');
|
||||
return;
|
||||
|
|
@ -89,90 +85,47 @@ const Share: React.FC = () => {
|
|||
navigate('/teacher/dashboard');
|
||||
|
||||
} catch (error) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
console.log(error)
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className='quizImport'>Chargement...</div>;
|
||||
}
|
||||
|
||||
if (quizExists) {
|
||||
return (
|
||||
<div className='quizImport'>
|
||||
<div className='importHeader'>
|
||||
<ReturnButton />
|
||||
<div className='titleContainer'>
|
||||
<div className='mainTitle'>Quiz déjà existant</div>
|
||||
</div>
|
||||
<div className='dumb'></div>
|
||||
</div>
|
||||
|
||||
<div className='editSection'>
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
padding: 3,
|
||||
maxWidth: 600,
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Le quiz que vous essayez d'importer existe déjà sur votre compte.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate('/teacher/dashboard')}
|
||||
sx={{ mt: 3, mb: 1 }}
|
||||
fullWidth
|
||||
>
|
||||
Retour au tableau de bord
|
||||
</Button>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Si vous souhaitiez créer une copie de ce quiz,
|
||||
vous pouvez utiliser la fonction "Dupliquer" disponible
|
||||
dans votre tableau de bord.
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='quizImport'>
|
||||
|
||||
<div className='importHeader'>
|
||||
<ReturnButton />
|
||||
<div className='titleContainer'>
|
||||
<div className='mainTitle'>Importation du Quiz: {quizTitle}</div>
|
||||
<div className='subTitle'>
|
||||
Vous êtes sur le point d'importer le quiz <strong>{quizTitle}</strong>, choisissez un dossier dans lequel enregistrer ce nouveau quiz.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='title'>Importer quiz: {quizTitle}</div>
|
||||
|
||||
<div className='dumb'></div>
|
||||
</div>
|
||||
|
||||
<div className='editSection'>
|
||||
<div className='formContainer'>
|
||||
|
||||
<div>
|
||||
|
||||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolder}
|
||||
onChange={handleSelectFolder}
|
||||
className="folderSelect"
|
||||
>
|
||||
<option disabled value=""> Choisir un dossier... </option>
|
||||
|
||||
{folders.map((folder: FolderType) => (
|
||||
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
|
||||
<Button variant="contained" onClick={handleQuizSave} className="saveButton">
|
||||
{<SaveIcon sx={{ fontSize: 20, marginRight: '8px' }} />}
|
||||
<Button variant="contained" onClick={handleQuizSave}>
|
||||
Enregistrer
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,58 +3,19 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-content: stretch;
|
||||
align-content: stretch
|
||||
}
|
||||
|
||||
.quizImport .importHeader .returnButton {
|
||||
flex-basis: 10%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quizImport .importHeader .titleContainer {
|
||||
.quizImport .importHeader .title {
|
||||
flex-basis: auto;
|
||||
text-align: center; /* Center the title and subtitle */
|
||||
}
|
||||
|
||||
.quizImport .importHeader .mainTitle {
|
||||
font-size: 44px; /* Larger font size for the main title */
|
||||
font-weight: bold; /* Remove bold */
|
||||
color: #333; /* Slightly paler color */
|
||||
}
|
||||
|
||||
.quizImport .importHeader .subTitle {
|
||||
font-size: 14px; /* Smaller font size for the subtitle */
|
||||
color: #666; /* Pale gray color */
|
||||
margin-top: 8px; /* Add some space between the title and subtitle */
|
||||
}
|
||||
|
||||
.quizImport .importHeader .dumb {
|
||||
flex-basis: 10%;
|
||||
}
|
||||
|
||||
.quizImport .editSection {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.quizImport .editSection .formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px; /* Adds space between the select and button */
|
||||
width: 100%;
|
||||
max-width: 400px; /* Limits the width of the form container */
|
||||
}
|
||||
|
||||
.quizImport .editSection .folderSelect {
|
||||
width: 100%; /* Ensures the select element takes the full width of its container */
|
||||
}
|
||||
|
||||
.quizImport .editSection .saveButton {
|
||||
width: 100%; /* Makes the button take the full width of its container */
|
||||
padding: 10px 20px; /* Increases the button's padding for a larger appearance */
|
||||
font-size: 16px; /* Increases the font size for better visibility */
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { ENV_VARIABLES } from '../constants';
|
||||
|
||||
import { FolderType } from 'src/Types/FolderType';
|
||||
import { ImagesResponse, ImagesParams } from '../Types/Images';
|
||||
import { QuizType } from 'src/Types/QuizType';
|
||||
import { RoomType } from 'src/Types/RoomType';
|
||||
import { ENV_VARIABLES } from 'src/constants';
|
||||
|
||||
type ApiResponse = boolean | string;
|
||||
|
||||
|
|
@ -37,7 +34,7 @@ class ApiService {
|
|||
}
|
||||
|
||||
// Helpers
|
||||
public saveToken(token: string): void {
|
||||
private saveToken(token: string): void {
|
||||
const now = new Date();
|
||||
|
||||
const object = {
|
||||
|
|
@ -81,93 +78,7 @@ class ApiService {
|
|||
return true;
|
||||
}
|
||||
|
||||
public isLoggedInTeacher(): boolean {
|
||||
const token = this.getToken();
|
||||
|
||||
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedToken = jwtDecode(token) as { roles: string[] };
|
||||
|
||||
/////// REMOVE BELOW
|
||||
// automatically add teacher role if not present
|
||||
if (!decodedToken.roles.includes('teacher')) {
|
||||
decodedToken.roles.push('teacher');
|
||||
}
|
||||
////// REMOVE ABOVE
|
||||
const userRoles = decodedToken.roles;
|
||||
const requiredRole = 'teacher';
|
||||
|
||||
if (!userRoles || !userRoles.includes(requiredRole)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update token expiry
|
||||
this.saveToken(token);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error decoding token:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public saveUsername(username: string): void {
|
||||
if (!username || username.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const object = {
|
||||
username: username
|
||||
}
|
||||
|
||||
localStorage.setItem("username", JSON.stringify(object));
|
||||
}
|
||||
|
||||
public getUsername(): string {
|
||||
const objectStr = localStorage.getItem("username");
|
||||
|
||||
if (!objectStr) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const object = JSON.parse(objectStr)
|
||||
|
||||
return object.username;
|
||||
}
|
||||
|
||||
public getUserID(): string {
|
||||
const token = localStorage.getItem("jwt");
|
||||
|
||||
if (!token) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const jsonObj = jwtDecode(token) as { userId: string };
|
||||
|
||||
if (!jsonObj.userId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return jsonObj.userId;
|
||||
}
|
||||
|
||||
// Route to know if rooms need authentication to join
|
||||
public async getRoomsRequireAuth(): Promise<any> {
|
||||
const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`);
|
||||
const result: AxiosResponse = await axios.get(url);
|
||||
|
||||
if (result.status == 200) {
|
||||
return result.data.roomsRequireAuth;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
localStorage.removeItem("username");
|
||||
return localStorage.removeItem("jwt");
|
||||
}
|
||||
|
||||
|
|
@ -177,31 +88,27 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async register(name: string, email: string, password: string, roles: string[]): Promise<any> {
|
||||
console.log(`ApiService.register: name: ${name}, email: ${email}, password: ${password}, roles: ${roles}`);
|
||||
public async register(email: string, password: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
if (!email || !password) {
|
||||
throw new Error(`L'email et le mot de passe sont requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/auth/simple-auth/register`);
|
||||
const url: string = this.constructRequestUrl(`/user/register`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { name, email, password, roles };
|
||||
const body = { email, password };
|
||||
|
||||
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
||||
|
||||
if (result.status == 200) {
|
||||
//window.location.href = result.request.responseURL;
|
||||
window.location.href = '/login';
|
||||
}
|
||||
else {
|
||||
throw new Error(`La connexion a échoué. Status: ${result.status}`);
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'enregistrement a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
|
|
@ -213,58 +120,48 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if successful
|
||||
* @returns An error string if unsuccessful
|
||||
*/
|
||||
public async login(email: string, password: string): Promise<any> {
|
||||
console.log(`login: email: ${email}, password: ${password}`);
|
||||
try {
|
||||
if (!email || !password) {
|
||||
throw new Error("L'email et le mot de passe sont requis.");
|
||||
}
|
||||
/**
|
||||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async login(email: string, password: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
const url: string = this.constructRequestUrl(`/auth/simple-auth/login`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { email, password };
|
||||
|
||||
console.log(`login: POST ${url} body: ${JSON.stringify(body)}`);
|
||||
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
||||
console.log(`login: result: ${result.status}, ${result.data}`);
|
||||
|
||||
// If login is successful, redirect the user
|
||||
if (result.status === 200) {
|
||||
//window.location.href = result.request.responseURL;
|
||||
this.saveToken(result.data.token);
|
||||
this.saveUsername(result.data.username);
|
||||
window.location.href = '/teacher/dashboard';
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`La connexion a échoué. Statut: ${result.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error details:", error);
|
||||
|
||||
// Handle Axios-specific errors
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const responseData = err.response?.data as { message?: string } | undefined;
|
||||
|
||||
// If there is a message field in the response, print it
|
||||
if (responseData?.message) {
|
||||
console.log("Backend error message:", responseData.message);
|
||||
return responseData.message;
|
||||
if (!email || !password) {
|
||||
throw new Error(`L'email et le mot de passe sont requis.`);
|
||||
}
|
||||
|
||||
// If no message is found, return a fallback message
|
||||
return "Erreur serveur inconnue lors de la requête.";
|
||||
const url: string = this.constructRequestUrl(`/user/login`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { email, password };
|
||||
|
||||
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`La connexion a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
this.saveToken(result.data.token);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
if (err.status === 401) {
|
||||
return 'Email ou mot de passe incorrect.';
|
||||
}
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
}
|
||||
|
||||
return `Une erreur inattendue s'est produite.`
|
||||
}
|
||||
|
||||
// Handle other non-Axios errors
|
||||
return "Une erreur inattendue s'est produite.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @returns true if successful
|
||||
|
|
@ -277,7 +174,7 @@ public async login(email: string, password: string): Promise<any> {
|
|||
throw new Error(`L'email est requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/auth/simple-auth/reset-password`);
|
||||
const url: string = this.constructRequestUrl(`/user/reset-password`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { email };
|
||||
|
||||
|
|
@ -313,7 +210,7 @@ public async login(email: string, password: string): Promise<any> {
|
|||
throw new Error(`L'email, l'ancien et le nouveau mot de passe sont requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/auth/simple-auth/change-password`);
|
||||
const url: string = this.constructRequestUrl(`/user/change-password`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { email, oldPassword, newPassword };
|
||||
|
||||
|
|
@ -564,6 +461,7 @@ public async login(email: string, password: string): Promise<any> {
|
|||
const headers = this.constructRequestHeaders();
|
||||
const body = { folderId };
|
||||
|
||||
console.log(headers);
|
||||
const result: AxiosResponse = await axios.post(url, body, { headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
|
|
@ -853,6 +751,36 @@ public async login(email: string, password: string): Promise<any> {
|
|||
}
|
||||
}
|
||||
|
||||
async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
|
||||
try {
|
||||
if (!quizId || !email) {
|
||||
throw new Error(`quizId and email are required.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/quiz/Share`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { quizId, email };
|
||||
|
||||
const result: AxiosResponse = await axios.put(url, body, { headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`Update and share quiz failed. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Unknown server error during request.';
|
||||
}
|
||||
|
||||
return `An unexpected error occurred.`;
|
||||
}
|
||||
}
|
||||
|
||||
async getSharedQuiz(quizId: string): Promise<string> {
|
||||
try {
|
||||
if (!quizId) {
|
||||
|
|
@ -912,195 +840,6 @@ public async login(email: string, password: string): Promise<any> {
|
|||
}
|
||||
}
|
||||
|
||||
//ROOM routes
|
||||
|
||||
public async getUserRooms(): Promise<RoomType[] | string> {
|
||||
try {
|
||||
const url: string = this.constructRequestUrl(`/room/getUserRooms`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
|
||||
const result: AxiosResponse = await axios.get(url, { headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'obtention des salles utilisateur a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return result.data.data.map((room: RoomType) => ({ _id: room._id, title: room.title }));
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
const url = err.config?.url || 'URL inconnue';
|
||||
return data?.error || `Erreur serveur inconnue lors de la requête (${url}).`;
|
||||
}
|
||||
|
||||
return `Une erreur inattendue s'est produite.`
|
||||
}
|
||||
}
|
||||
|
||||
public async getRoomContent(roomId: string): Promise<RoomType> {
|
||||
try {
|
||||
const url = this.constructRequestUrl(`/room/${roomId}`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
|
||||
const response = await axios.get<{ data: RoomType }>(url, { headers });
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to get room: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverError = error.response?.data?.error;
|
||||
throw new Error(serverError || 'Erreur serveur inconnue');
|
||||
}
|
||||
throw new Error('Erreur réseau');
|
||||
}
|
||||
}
|
||||
|
||||
public async getRoomTitleByUserId(userId: string): Promise<string[] | string> {
|
||||
try {
|
||||
if (!userId) {
|
||||
throw new Error(`L'ID utilisateur est requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/room/getRoomTitleByUserId/${userId}`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
|
||||
const result: AxiosResponse = await axios.get(url, { headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return result.data.titles;
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
}
|
||||
return `Une erreur inattendue s'est produite.`;
|
||||
}
|
||||
}
|
||||
public async getRoomTitle(roomId: string): Promise<string | string> {
|
||||
try {
|
||||
if (!roomId) {
|
||||
throw new Error(`L'ID de la salle est requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/room/getRoomTitle/${roomId}`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
|
||||
const result: AxiosResponse = await axios.get(url, { headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'obtention du titre de la salle a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return result.data.title;
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
}
|
||||
return `Une erreur inattendue s'est produite.`;
|
||||
}
|
||||
}
|
||||
public async createRoom(title: string): Promise<string> {
|
||||
try {
|
||||
if (!title) {
|
||||
throw new Error("Le titre de la salle est requis.");
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/room/create`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { title };
|
||||
|
||||
const result = await axios.post<{ roomId: string }>(url, body, { headers });
|
||||
return `Salle créée avec succès. ID de la salle: ${result.data.roomId}`;
|
||||
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
|
||||
const serverMessage = (err.response?.data as { message?: string })?.message
|
||||
|| (err.response?.data as { error?: string })?.error
|
||||
|| err.message;
|
||||
|
||||
if (err.response?.status === 409) {
|
||||
throw new Error(serverMessage);
|
||||
}
|
||||
|
||||
throw new Error(serverMessage || "Erreur serveur inconnue");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteRoom(roomId: string): Promise<string | string> {
|
||||
try {
|
||||
if (!roomId) {
|
||||
throw new Error(`L'ID de la salle est requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/room/delete/${roomId}`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
|
||||
const result: AxiosResponse = await axios.delete(url, { headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`La suppression de la salle a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return `Salle supprimée avec succès.`;
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Erreur serveur inconnue lors de la suppression de la salle.';
|
||||
}
|
||||
return `Une erreur inattendue s'est produite.`;
|
||||
}
|
||||
}
|
||||
|
||||
public async renameRoom(roomId: string, newTitle: string): Promise<string | string> {
|
||||
try {
|
||||
if (!roomId || !newTitle) {
|
||||
throw new Error(`L'ID de la salle et le nouveau titre sont requis.`);
|
||||
}
|
||||
|
||||
const url: string = this.constructRequestUrl(`/room/rename`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const body = { roomId, newTitle };
|
||||
|
||||
const result: AxiosResponse = await axios.put(url, body, { headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`La mise à jour du titre de la salle a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return `Titre de la salle mis à jour avec succès.`;
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
return data?.error || 'Erreur serveur inconnue lors de la mise à jour du titre.';
|
||||
}
|
||||
return `Une erreur inattendue s'est produite.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Images Route
|
||||
|
||||
/**
|
||||
|
|
@ -1147,126 +886,7 @@ public async login(email: string, password: string): Promise<any> {
|
|||
return `ERROR : Une erreur inattendue s'est produite.`
|
||||
}
|
||||
}
|
||||
|
||||
public async getImages(page: number, limit: number): Promise<ImagesResponse> {
|
||||
try {
|
||||
const url: string = this.constructRequestUrl(`/image/getImages`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
let params : ImagesParams = { page: page, limit: limit };
|
||||
|
||||
const result: AxiosResponse = await axios.get(url, { params: params, headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'affichage des images a échoué. Status: ${result.status}`);
|
||||
}
|
||||
const images = result.data;
|
||||
|
||||
return images;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
|
||||
}
|
||||
|
||||
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserImages(page: number, limit: number): Promise<ImagesResponse> {
|
||||
try {
|
||||
const url: string = this.constructRequestUrl(`/image/getUserImages`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
let params : ImagesParams = { page: page, limit: limit };
|
||||
|
||||
const uid = this.getUserID();
|
||||
if(uid !== ''){
|
||||
params.uid = uid;
|
||||
}
|
||||
|
||||
const result: AxiosResponse = await axios.get(url, { params: params, headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`L'affichage des images de l'utilisateur a échoué. Status: ${result.status}`);
|
||||
}
|
||||
const images = result.data;
|
||||
|
||||
return images;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
|
||||
}
|
||||
|
||||
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteImage(imgId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
const url: string = this.constructRequestUrl(`/image/delete`);
|
||||
const headers = this.constructRequestHeaders();
|
||||
const uid = this.getUserID();
|
||||
let params = { uid: uid, imgId: imgId };
|
||||
|
||||
const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers });
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
const deleted = result.data.deleted;
|
||||
return deleted;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error details: ", error);
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
const data = err.response?.data as { error: string } | undefined;
|
||||
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||
throw new Error(`L'enregistrement a échoué. Status: ${msg}`);
|
||||
}
|
||||
|
||||
throw new Error(`ERROR : Une erreur inattendue s'est produite.`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllQuizIds(): Promise<string[]> {
|
||||
try {
|
||||
const folders = await this.getUserFolders();
|
||||
|
||||
const allQuizIds: string[] = [];
|
||||
|
||||
if (Array.isArray(folders)) {
|
||||
for (const folder of folders) {
|
||||
const folderQuizzes = await this.getFolderContent(folder._id);
|
||||
|
||||
if (Array.isArray(folderQuizzes)) {
|
||||
allQuizIds.push(...folderQuizzes.map(quiz => quiz._id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to get user folders:', folders);
|
||||
}
|
||||
|
||||
return allQuizIds;
|
||||
} catch (error) {
|
||||
console.error('Failed to get all quiz ids:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// NOTE : Get Image pas necessaire
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue