mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Compare commits
77 commits
6f270b5436
...
27efcda1d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 27efcda1d8 | |||
| d91cb0e245 | |||
|
|
04ccca91d6 | ||
| dd0f5f9534 | |||
| a2d83f4f77 | |||
| b97454d9a9 | |||
|
|
75e669b8b4 | ||
|
|
3ef37f6dc4 | ||
|
|
b60961acea | ||
|
|
e7eede36be | ||
|
|
81186b6a35 | ||
|
|
fff5830afd | ||
| 63cdd03c14 | |||
| dabdfafd35 | |||
| 32a41d93aa | |||
|
|
bb4ef54db9 | ||
| b3d65e0a1e | |||
| 567a765f94 | |||
| 5c75347887 | |||
| 8eab2d3a05 | |||
|
|
1a7be0ad79 | ||
|
|
1e67762b5e | ||
| d3199b9f3f | |||
| d9e5d6a91f | |||
| d4e13b8c36 | |||
|
|
18d3ded4fa | ||
|
|
3744bf4347 | ||
|
|
ec0cc48ae7 | ||
|
|
20fe4e673a | ||
|
|
6d988c347f | ||
|
|
80610c3a6e | ||
|
|
58a55ed176 | ||
|
|
5a3f965c58 | ||
|
|
ec15909d55 | ||
| 71353669ca | |||
| 15144244ad | |||
|
|
0af9b099fd | ||
| 4f72dc7b9b | |||
|
|
49fbdb1ffd | ||
| fa95b9003f | |||
| 2176edf7d0 | |||
| 6883774ed4 | |||
|
|
11222c70bd | ||
| c45a674c72 | |||
|
|
5c24ae56a9 | ||
|
|
f835c733a1 | ||
|
|
5c21b6a15f | ||
|
|
b608793ac3 | ||
| 878fd302a4 | |||
|
|
0b2552bdff | ||
| 706308d54f | |||
|
|
977d1c9700 | ||
| 35d6724d87 | |||
| 806935e48c | |||
| 3c2bcb4ed4 | |||
|
|
2c7fd9c828 | ||
|
|
db6fa947d7 | ||
|
|
d37e6c540a | ||
|
|
b744284472 | ||
|
|
cca9a2c99a | ||
| 2df750b6f7 | |||
| d563459aa6 | |||
| 678d1c2250 | |||
| 80115f050c | |||
| c26708a609 | |||
|
|
bbc0359ead | ||
|
|
e08d1477ec | ||
|
|
85bd93792c | ||
|
|
3fd562c144 | ||
| f2597f5491 | |||
| 4cca066751 | |||
|
|
cc420b3a9c | ||
|
|
ff7f0da964 | ||
|
|
900ccd847f | ||
|
|
3db53c5cc4 | ||
|
|
93e16f8d0b | ||
| 32bcb67f33 |
191 changed files with 13541 additions and 9315 deletions
31
.github/workflows/create-branch-images.yml
vendored
31
.github/workflows/create-branch-images.yml
vendored
|
|
@ -103,4 +103,35 @@ jobs:
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-quizroom:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Extract metadata for Quizroom Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}-quizroom
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
- name: Build and push Quizroom Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./quizRoom
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
33
.github/workflows/create-docs.yml
vendored
Normal file
33
.github/workflows/create-docs.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
name: Creates docs and deploy to gh-pages
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Deploy docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
PUMLURL: "https://www.plantuml.com/plantuml/"
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./documentation
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Build docs
|
||||||
|
working-directory: ./documentation
|
||||||
|
run: mkdocs build --verbose --clean
|
||||||
|
|
||||||
|
- name: Push docs to gh-pages
|
||||||
|
working-directory: ./documentation
|
||||||
|
run: python deploy.py
|
||||||
2
.github/workflows/frontend-deploy.yml
vendored
2
.github/workflows/frontend-deploy.yml
vendored
|
|
@ -23,4 +23,4 @@ jobs:
|
||||||
context: ./client
|
context: ./client
|
||||||
file: ./client/Dockerfile
|
file: ./client/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest
|
||||||
|
|
|
||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
|
@ -21,13 +21,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Install Dependencies, lint and Run Tests
|
- name: Install Dependencies and Run Tests
|
||||||
run: |
|
run: |
|
||||||
echo "Installing dependencies..."
|
|
||||||
npm ci
|
npm ci
|
||||||
echo "Running ESLint..."
|
|
||||||
npx eslint .
|
|
||||||
echo "Running tests..."
|
|
||||||
npm test
|
npm test
|
||||||
working-directory: ${{ matrix.directory }}
|
working-directory: ${{ matrix.directory }}
|
||||||
|
|
||||||
|
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -129,3 +129,16 @@ dist
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
db-backup/
|
db-backup/
|
||||||
|
|
||||||
|
**/.env
|
||||||
|
.venv
|
||||||
|
deployments
|
||||||
|
/test/stressTest/output
|
||||||
|
|
||||||
|
# Opentofu state
|
||||||
|
opentofu/*/.terraform
|
||||||
|
opentofu/*/.terraform.lock*
|
||||||
|
opentofu/*/terraform.tfstate*
|
||||||
|
opentofu/*/terraform.tfvars
|
||||||
|
# Opentofu auth config
|
||||||
|
opentofu/auth_config.json
|
||||||
|
|
|
||||||
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug backend",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/server/app.js",
|
||||||
|
"cwd":"${workspaceFolder}/server/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "msedge",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug frontend",
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}/client/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Docker: Attach to Node",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"restart": true,
|
||||||
|
"port": 9229,
|
||||||
|
"address": "localhost",
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "/app",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
LICENSE
3
LICENSE
|
|
@ -1,7 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
|
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
|
||||||
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
@ -19,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
10
ansible/Dockerfile
Normal file
10
ansible/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Installer Ansible
|
||||||
|
RUN pip install ansible
|
||||||
|
|
||||||
|
# Définir le répertoire de travail
|
||||||
|
WORKDIR /ansible
|
||||||
|
|
||||||
|
# Copier les fichiers nécessaires
|
||||||
|
COPY inventory.ini deploy.yml ./
|
||||||
40
ansible/README.md
Normal file
40
ansible/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Déploiement de Services avec Ansible et Docker Compose
|
||||||
|
|
||||||
|
Ce guide explique comment utiliser Ansible pour configurer et déployer des services Docker avec `docker-compose`.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
1. **Ansible** : Assurez-vous qu'Ansible est installé sur votre système.
|
||||||
|
- [Guide d'installation d'Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
|
||||||
|
|
||||||
|
2. **Docker et Docker Compose** : Docker doit être installé et configuré pour fonctionner avec Ansible.
|
||||||
|
- Installez Docker : [Documentation Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- Docker Compose est inclus comme plugin Docker dans les versions récentes de Docker.
|
||||||
|
|
||||||
|
3. **WSL (pour Windows)** : Si vous utilisez Windows, assurez-vous d'avoir configuré WSL et un environnement Ubuntu.
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
Le fichier `deploy.yml` contient les tâches Ansible nécessaires pour télécharger, configurer, et démarrer les services Docker en utilisant Docker Compose.
|
||||||
|
|
||||||
|
## Installation et de déploiement
|
||||||
|
|
||||||
|
### Lancer le déploiement avec Ansible
|
||||||
|
|
||||||
|
Pour exécuter le playbook Ansible `deploy.yml`, utilisez la commande suivante depuis le répertoire racine du projet :
|
||||||
|
|
||||||
|
`ansible-playbook -i inventory.ini deploy.yml`
|
||||||
|
|
||||||
|
### Vérification du déploiement
|
||||||
|
|
||||||
|
Une fois le playbook exécuté, Ansible télécharge Docker et Docker Compose, télécharge le fichier `docker-compose.yaml`, démarre Docker et lance les conteneurs spécifiés.
|
||||||
|
|
||||||
|
### Configuration et contenu du Playbook (deploy.yml)
|
||||||
|
Le playbook deploy.yml exécute les étapes suivantes :
|
||||||
|
|
||||||
|
1. Télécharge Docker Compose si ce dernier n'est pas encore présent.
|
||||||
|
2. Vérifie l'installation de Docker Compose pour s'assurer qu'il est opérationnel.
|
||||||
|
3. Démarre le service Docker si ce n'est pas déjà le cas.
|
||||||
|
4. Télécharge le fichier docker-compose.yaml depuis le dépôt Git spécifié.
|
||||||
|
5. Lance Docker Compose pour déployer les conteneurs définis dans docker-compose.yaml.
|
||||||
|
6. Vérifie l'état des conteneurs et affiche les conteneurs en cours d'exécution.
|
||||||
38
ansible/deploy.yml
Normal file
38
ansible/deploy.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
- name: Déployer des services avec Docker Compose
|
||||||
|
hosts: local
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
- name: Télécharger Docker
|
||||||
|
ansible.builtin.package:
|
||||||
|
name: docker-compose
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Vérifier l'installation de Docker Compose plugin
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: docker compose version
|
||||||
|
|
||||||
|
- name: Commencer le service docker
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: docker
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
|
||||||
|
- name: Telecharger le fichier docker-compose
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: https://raw.githubusercontent.com/ets-cfuhrman-pfe/EvalueTonSavoir/refs/heads/main/docker-compose.yaml
|
||||||
|
dest: ./docker-compose.yaml
|
||||||
|
|
||||||
|
- name: Lancer Docker Compose
|
||||||
|
ansible.builtin.shell:
|
||||||
|
docker-compose up -d
|
||||||
|
become: true
|
||||||
|
|
||||||
|
- name: Vérification des services Docker
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: docker ps
|
||||||
|
register: docker_ps_output
|
||||||
|
|
||||||
|
- name: Afficher l'état des conteneurs Docker
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ docker_ps_output.stdout }}"
|
||||||
70
ansible/docker-compose.yaml
Normal file
70
ansible/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
services:
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: fuhrmanator/evaluetonsavoir-frontend:latest
|
||||||
|
container_name: frontend
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
environment:
|
||||||
|
VITE_BACKEND_URL: "http://localhost:4400"
|
||||||
|
# don't define VITE_BACKEND_SOCKET_URL so it will default to window.location.host
|
||||||
|
# VITE_BACKEND_SOCKET_URL: ""
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: fuhrmanator/evaluetonsavoir-backend:latest
|
||||||
|
container_name: backend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
|
||||||
|
MONGO_DATABASE: evaluetonsavoir
|
||||||
|
EMAIL_SERVICE: gmail
|
||||||
|
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
|
||||||
|
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
|
||||||
|
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
|
||||||
|
FRONTEND_URL: "http://localhost:5173"
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
|
||||||
|
nginx:
|
||||||
|
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
||||||
|
container_name: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- frontend
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# Ce conteneur est la base de données principale pour l'application
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
container_name: mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- TZ=America/Montreal
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_DEBUG=true
|
||||||
|
- WATCHTOWER_INCLUDE_RESTARTING=true
|
||||||
|
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
external: false
|
||||||
9
ansible/inventory.ini
Normal file
9
ansible/inventory.ini
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Spécifier les serveurs où vous souhaitez déployer votre application.
|
||||||
|
# Remplacez votre_ip_serveur par l’adresse IP de votre serveur, et votre_utilisateur_ssh par le nom d’utilisateur SSH.
|
||||||
|
|
||||||
|
# Pour les serveurs
|
||||||
|
# [app_servers]
|
||||||
|
# votre_ip_serveur ansible_user=votre_utilisateur_ssh
|
||||||
|
|
||||||
|
[local]
|
||||||
|
localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
.env
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
VITE_BACKEND_URL=http://localhost:4400
|
|
||||||
VITE_BACKEND_SOCKET_URL=http://localhost:4400
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: { browser: true, es2020: true },
|
env: { browser: true, es2020: true },
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ RUN npm install
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
EXPOSE 5173
|
ENV PORT=5173
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:${PORT} || exit 1
|
||||||
|
|
||||||
CMD [ "npm", "run", "preview" ]
|
CMD [ "npm", "run", "preview" ]
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-undef */
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import globals from "globals";
|
|
||||||
import pluginJs from "@eslint/js";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import pluginReact from "eslint-plugin-react";
|
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config[]} */
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
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,3 @@
|
||||||
/* eslint-disable no-undef */
|
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
@ -12,11 +11,7 @@ module.exports = {
|
||||||
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
setupFiles: ['./jest.setup.cjs'],
|
setupFiles: ['./jest.setup.cjs'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||||
// Permet de mocker les constantes pour les tests avec un chemin absolue (ex: import { ENV_VARIABLES } from 'src/constants';). Voir les "paths" dans tsconfig.json.
|
|
||||||
'^src/constants$': '<rootDir>/src/__mocks__/constantsMock.tsx',
|
|
||||||
// Dû au fait que tous les imports de "src/" sont normalisés, Jest doit comprendre le chemin réel. TODO: Trouver une solution pour que Jest se fie à tsconfig.json.
|
|
||||||
'^src/(.*)$': '<rootDir>/src/$1',
|
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
|
transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
/* eslint-disable no-undef */
|
global.import = {
|
||||||
process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
|
meta: {
|
||||||
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';
|
env: {
|
||||||
|
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
7451
client/package-lock.json
generated
7451
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,7 @@
|
||||||
"@mui/material": "^6.1.0",
|
"@mui/material": "^6.1.0",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dompurify": "^3.2.3",
|
"dockerode": "^4.0.2",
|
||||||
"esbuild": "^0.23.1",
|
"esbuild": "^0.23.1",
|
||||||
"gift-pegjs": "^1.0.2",
|
"gift-pegjs": "^1.0.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
"@babel/preset-env": "^7.23.3",
|
"@babel/preset-env": "^7.23.3",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.23.3",
|
||||||
"@babel/preset-typescript": "^7.23.3",
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
"@eslint/js": "^9.18.0",
|
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.5.0",
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.0.1",
|
||||||
|
|
@ -56,17 +55,14 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||||
"@typescript-eslint/parser": "^8.5.0",
|
"@typescript-eslint/parser": "^8.5.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.10.0",
|
||||||
"eslint-plugin-react": "^7.37.3",
|
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
|
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
|
||||||
"eslint-plugin-react-refresh": "^0.4.12",
|
"eslint-plugin-react-refresh": "^0.4.12",
|
||||||
"globals": "^15.14.0",
|
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"typescript-eslint": "^8.19.1",
|
|
||||||
"vite": "^5.4.5",
|
"vite": "^5.4.5",
|
||||||
"vite-plugin-environment": "^1.1.3"
|
"vite-plugin-environment": "^1.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
// App.tsx
|
// App.tsx
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
export interface QuizType {
|
export interface QuizType {
|
||||||
_id: string;
|
_id: string;
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderName: string;
|
|
||||||
userId: string;
|
userId: string;
|
||||||
title: string;
|
title: string;
|
||||||
content: string[];
|
content: string[];
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
console.log('constantsMock.tsx is loaded');
|
|
||||||
|
|
||||||
// constants.tsx
|
|
||||||
const ENV_VARIABLES = {
|
|
||||||
MODE: 'production',
|
|
||||||
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
|
|
||||||
VITE_BACKEND_SOCKET_URL: process.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 };
|
|
||||||
|
|
@ -9,7 +9,6 @@ describe('isQuizValid function', () => {
|
||||||
const validQuiz: QuizType = {
|
const validQuiz: QuizType = {
|
||||||
_id: '1',
|
_id: '1',
|
||||||
folderId: 'test',
|
folderId: 'test',
|
||||||
folderName: 'test',
|
|
||||||
userId: 'user',
|
userId: 'user',
|
||||||
created_at: new Date('2021-10-01'),
|
created_at: new Date('2021-10-01'),
|
||||||
updated_at: new Date('2021-10-02'),
|
updated_at: new Date('2021-10-02'),
|
||||||
|
|
@ -25,7 +24,6 @@ describe('isQuizValid function', () => {
|
||||||
const invalidQuiz: QuizType = {
|
const invalidQuiz: QuizType = {
|
||||||
_id: '2',
|
_id: '2',
|
||||||
folderId: 'test',
|
folderId: 'test',
|
||||||
folderName: 'test',
|
|
||||||
userId: 'user',
|
userId: 'user',
|
||||||
title: '',
|
title: '',
|
||||||
created_at: new Date('2021-10-01'),
|
created_at: new Date('2021-10-01'),
|
||||||
|
|
@ -41,7 +39,6 @@ describe('isQuizValid function', () => {
|
||||||
const invalidQuiz: QuizType = {
|
const invalidQuiz: QuizType = {
|
||||||
_id: '2',
|
_id: '2',
|
||||||
folderId: 'test',
|
folderId: 'test',
|
||||||
folderName: 'test',
|
|
||||||
userId: 'user',
|
userId: 'user',
|
||||||
title: 'sample',
|
title: 'sample',
|
||||||
created_at: new Date('2021-10-01'),
|
created_at: new Date('2021-10-01'),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Modal.test.tsx
|
// Modal.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent, screen } from '@testing-library/react';
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import ConfirmDialog from 'src/components/ConfirmDialog/ConfirmDialog';
|
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';
|
||||||
|
|
||||||
describe('ConfirmDialog Component', () => {
|
describe('ConfirmDialog Component', () => {
|
||||||
const mockOnConfirm = jest.fn();
|
const mockOnConfirm = jest.fn();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Editor.test.tsx
|
// Editor.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent, screen } from '@testing-library/react';
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import Editor from 'src/components/Editor/Editor';
|
import Editor from '../../../components/Editor/Editor';
|
||||||
|
|
||||||
describe('Editor Component', () => {
|
describe('Editor Component', () => {
|
||||||
const mockOnEditorChange = jest.fn();
|
const mockOnEditorChange = jest.fn();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
|
||||||
|
|
||||||
describe('GIFTTemplatePreview Component', () => {
|
describe('GIFTTemplatePreview Component', () => {
|
||||||
test('renders error message when questions contain invalid syntax', () => {
|
test('renders error message when questions contain invalid syntax', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// TextType.test.ts
|
// TextType.test.ts
|
||||||
|
|
||||||
import { TextFormat } from "gift-pegjs";
|
import { TextFormat } from "gift-pegjs";
|
||||||
import textType from "src/components/GiftTemplate/templates/TextType";
|
import textType from "../../../components/GiftTemplate/templates/TextType";
|
||||||
|
|
||||||
describe('TextType', () => {
|
describe('TextType', () => {
|
||||||
it('should format text with basic characters correctly', () => {
|
it('should format text with basic characters correctly', () => {
|
||||||
|
|
@ -32,7 +32,7 @@ describe('TextType', () => {
|
||||||
// Hint -- if the output changes because of a change in the code or library, you can update
|
// Hint -- if the output changes because of a change in the code or library, you can update
|
||||||
// by running the test and copying the "Received string:" in jest output
|
// by running the test and copying the "Received string:" in jest output
|
||||||
// when it fails (assuming the output is correct)
|
// when it fails (assuming the output is correct)
|
||||||
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
|
const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
|
||||||
expect(textType({ text: input })).toContain(expectedOutput);
|
expect(textType({ text: input })).toContain(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -42,17 +42,19 @@ describe('TextType', () => {
|
||||||
format: 'plain'
|
format: 'plain'
|
||||||
};
|
};
|
||||||
// hint: katex-display is the class that indicates a separate equation
|
// hint: katex-display is the class that indicates a separate equation
|
||||||
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
|
const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
|
||||||
expect(textType({ text: input })).toContain(expectedOutput);
|
expect(textType({ text: input })).toContain(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format text with a katex matrix correctly', () => {
|
it('should format text with a katex matrix correctly', () => {
|
||||||
const input: TextFormat = {
|
const input: TextFormat = {
|
||||||
// eslint-disable-next-line no-useless-escape
|
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}
|
||||||
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`,
|
a&b \\\\
|
||||||
|
c&d
|
||||||
|
\\end\{pmatrix\}`,
|
||||||
format: 'plain'
|
format: 'plain'
|
||||||
};
|
};
|
||||||
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow></mrow><annotation encoding="application/x-tex"></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
|
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow></mrow><annotation encoding=\"application/x-tex\"></annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
|
||||||
expect(textType({ text: input })).toContain(expectedOutput);
|
expect(textType({ text: input })).toContain(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//color.test.tsx
|
//color.test.tsx
|
||||||
import { colors } from "src/components/GiftTemplate/constants";
|
import { colors } from "../../../../components/GiftTemplate/constants";
|
||||||
|
|
||||||
describe('Colors object', () => {
|
describe('Colors object', () => {
|
||||||
test('All colors are defined', () => {
|
test('All colors are defined', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
//styles.test.tsx
|
//styles.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
import { ParagraphStyle } from 'src/components/GiftTemplate/constants';
|
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants';
|
||||||
|
|
||||||
describe('ParagraphStyle', () => {
|
describe('ParagraphStyle', () => {
|
||||||
test('applies styles correctly', () => {
|
test('applies styles correctly', () => {
|
||||||
|
|
@ -28,7 +27,6 @@ function convertStylesToObject(styles: string): React.CSSProperties {
|
||||||
styles.split(';').forEach((style) => {
|
styles.split(';').forEach((style) => {
|
||||||
const [property, value] = style.split(':');
|
const [property, value] = style.split(':');
|
||||||
if (property && value) {
|
if (property && value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(styleObject as any)[property.trim()] = value.trim();
|
(styleObject as any)[property.trim()] = value.trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { theme } from 'src/components/GiftTemplate/constants/theme';
|
import { theme } from '../../../../components/GiftTemplate/constants/theme';
|
||||||
import { colors } from 'src/components/GiftTemplate/constants/colors';
|
import { colors } from '../../../../components/GiftTemplate/constants/colors';
|
||||||
|
|
||||||
describe('Theme', () => {
|
describe('Theme', () => {
|
||||||
test('returns correct light color', () => {
|
test('returns correct light color', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import AnswerIcon from 'src/components/GiftTemplate/templates/AnswerIcon';
|
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
describe('AnswerIcon', () => {
|
describe('AnswerIcon', () => {
|
||||||
test('renders correct icon when correct is true', () => {
|
test('renders correct icon when correct is true', () => {
|
||||||
const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: true })) }} />);
|
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />);
|
||||||
const svgElement = container.querySelector('svg');
|
const svgElement = container.querySelector('svg');
|
||||||
|
|
||||||
expect(svgElement).toBeInTheDocument();
|
expect(svgElement).toBeInTheDocument();
|
||||||
|
|
@ -21,7 +19,7 @@ describe('AnswerIcon', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders incorrect icon when correct is false', () => {
|
test('renders incorrect icon when correct is false', () => {
|
||||||
const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: false })) }} />);
|
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />);
|
||||||
const svgElement = container.querySelector('svg');
|
const svgElement = container.querySelector('svg');
|
||||||
|
|
||||||
expect(svgElement).toBeInTheDocument();
|
expect(svgElement).toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import { MultipleChoice } from 'src/components/GiftTemplate/templates';
|
|
||||||
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from 'src/components/GiftTemplate/templates/types';
|
|
||||||
|
|
||||||
// Mock the nanoid function
|
|
||||||
jest.mock('nanoid', () => ({
|
|
||||||
nanoid: jest.fn(() => 'mocked-id')
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockProps: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title',
|
|
||||||
stem: { format: 'plain' , text: 'Sample Stem'},
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const katekMock: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title',
|
|
||||||
stem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageMock: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title with Image',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem with Image' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'plain', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
|
|
||||||
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
|
|
||||||
{ text: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMoodle: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title',
|
|
||||||
stem: { format: 'moodle' , text: 'Sample Stem'},
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const mockHTML: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title',
|
|
||||||
stem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'},
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'html' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'html', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMarkdown: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title with Image',
|
|
||||||
stem: { format: 'markdown', text: 'Sample Stem with Image' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMarkdownTwoImages: TemplateOptions & MultipleChoiceType = {
|
|
||||||
type: 'MC',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Title with Image',
|
|
||||||
stem: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt = "Sample Image"/>' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
|
|
||||||
};
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...mockProps} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with katex', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...katekMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with image', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...imageMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with Moodle text format', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...mockMoodle} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with katex, using html text format', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...mockHTML} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with image using markdown text format', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...mockMarkdown} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MultipleChoice snapshot test with 2 images using markdown text format', () => {
|
|
||||||
const { asFragment } = render(<MultipleChoice {...mockMarkdownTwoImages} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import Numerical from 'src/components/GiftTemplate/templates/Numerical';
|
|
||||||
import { TemplateOptions, Numerical as NumericalType } from 'src/components/GiftTemplate/templates/types';
|
|
||||||
|
|
||||||
// Mock the nanoid function
|
|
||||||
jest.mock('nanoid', () => ({
|
|
||||||
nanoid: jest.fn(() => 'mocked-id')
|
|
||||||
}));
|
|
||||||
|
|
||||||
const plainTextMock: TemplateOptions & NumericalType = {
|
|
||||||
type: 'Numerical',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Numerical Title',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem' },
|
|
||||||
choices: [
|
|
||||||
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
|
|
||||||
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const htmlMock: TemplateOptions & NumericalType = {
|
|
||||||
type: 'Numerical',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Numerical Title',
|
|
||||||
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
|
|
||||||
choices: [
|
|
||||||
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'html', text: 'Correct!' } },
|
|
||||||
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'html', text: 'Incorrect!' } }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const moodleMock: TemplateOptions & NumericalType = {
|
|
||||||
type: 'Numerical',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Numerical Title',
|
|
||||||
stem: { format: 'moodle', text: 'Sample Stem' },
|
|
||||||
choices: [
|
|
||||||
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'moodle', text: 'Correct!' } },
|
|
||||||
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'moodle', text: 'Incorrect!' } }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageMock: TemplateOptions & NumericalType = {
|
|
||||||
type: 'Numerical',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Numerical Title with Image',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem with Image' },
|
|
||||||
choices: [
|
|
||||||
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
|
|
||||||
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } },
|
|
||||||
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 44}, feedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' } }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
|
|
||||||
};
|
|
||||||
|
|
||||||
test('Numerical snapshot test with plain text', () => {
|
|
||||||
const { asFragment } = render(<Numerical {...plainTextMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Numerical snapshot test with html', () => {
|
|
||||||
const { asFragment } = render(<Numerical {...htmlMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Numerical snapshot test with moodle', () => {
|
|
||||||
const { asFragment } = render(<Numerical {...moodleMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Numerical snapshot test with image', () => {
|
|
||||||
const { asFragment } = render(<Numerical {...imageMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswer';
|
|
||||||
import { TemplateOptions, ShortAnswer as ShortAnswerType } from 'src/components/GiftTemplate/templates/types';
|
|
||||||
|
|
||||||
// Mock the nanoid function
|
|
||||||
jest.mock('nanoid', () => ({
|
|
||||||
nanoid: jest.fn(() => 'mocked-id')
|
|
||||||
}));
|
|
||||||
|
|
||||||
const plainTextMock: TemplateOptions & ShortAnswerType = {
|
|
||||||
type: 'Short',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Short Answer Title',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'plain' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'plain' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const katexMock: TemplateOptions & ShortAnswerType = {
|
|
||||||
type: 'Short',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Short Answer Title',
|
|
||||||
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'html' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'html' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'html' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'moodle' , text: 'Correct!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const moodleMock: TemplateOptions & ShortAnswerType = {
|
|
||||||
type: 'Short',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Short Answer Title',
|
|
||||||
stem: { format: 'moodle', text: 'Sample Stem' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'moodle' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
|
|
||||||
{ text: { format: 'moodle' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageMock: TemplateOptions & ShortAnswerType = {
|
|
||||||
type: 'Short',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Short Answer Title with Image',
|
|
||||||
stem: { format: 'markdown', text: 'Sample Stem with Image' },
|
|
||||||
choices: [
|
|
||||||
{ text: { format: 'markdown', text: 'Answer 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: 'Answer 2' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
|
|
||||||
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }
|
|
||||||
],
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
test('ShortAnswer snapshot test with plain text', () => {
|
|
||||||
const { asFragment } = render(<ShortAnswer {...plainTextMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ShortAnswer snapshot test with katex', () => {
|
|
||||||
const { asFragment } = render(<ShortAnswer {...katexMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ShortAnswer snapshot test with moodle', () => {
|
|
||||||
const { asFragment } = render(<ShortAnswer {...moodleMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ShortAnswer snapshot test with image', () => {
|
|
||||||
const { asFragment } = render(<ShortAnswer {...imageMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import TrueFalse from 'src/components/GiftTemplate/templates';
|
|
||||||
import { TemplateOptions, TrueFalse as TrueFalseType } from 'src/components/GiftTemplate/templates/types';
|
|
||||||
|
|
||||||
// Mock the nanoid function
|
|
||||||
jest.mock('nanoid', () => ({
|
|
||||||
nanoid: jest.fn(() => 'mocked-id')
|
|
||||||
}));
|
|
||||||
|
|
||||||
const plainTextMock: TemplateOptions & TrueFalseType = {
|
|
||||||
type: 'TF',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample True/False Title',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem' },
|
|
||||||
isTrue: true,
|
|
||||||
trueFeedback: { format: 'plain', text: 'Correct!' },
|
|
||||||
falseFeedback: { format: 'plain', text: 'Incorrect!' },
|
|
||||||
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const katexMock: TemplateOptions & TrueFalseType = {
|
|
||||||
type: 'TF',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample True/False Title',
|
|
||||||
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
|
|
||||||
isTrue: true,
|
|
||||||
trueFeedback: { format: 'moodle', text: 'Correct!' },
|
|
||||||
falseFeedback: { format: 'html', text: 'Incorrect!' },
|
|
||||||
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const moodleMock: TemplateOptions & TrueFalseType = {
|
|
||||||
type: 'TF',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample True/False Title',
|
|
||||||
stem: { format: 'moodle', text: 'Sample Stem' },
|
|
||||||
isTrue: true,
|
|
||||||
trueFeedback: { format: 'moodle', text: 'Correct!' },
|
|
||||||
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
|
|
||||||
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const imageMock: TemplateOptions & TrueFalseType = {
|
|
||||||
type: 'TF',
|
|
||||||
hasEmbeddedAnswers: false,
|
|
||||||
title: 'Sample Short Answer Title with Image',
|
|
||||||
stem: { format: 'plain', text: 'Sample Stem with Image' },
|
|
||||||
trueFeedback: { format: 'moodle', text: 'Correct!' },
|
|
||||||
isTrue: true,
|
|
||||||
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
|
|
||||||
globalFeedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }
|
|
||||||
};
|
|
||||||
|
|
||||||
test('TrueFalse snapshot test with plain text', () => {
|
|
||||||
const { asFragment } = render(<TrueFalse {...plainTextMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('TrueFalse snapshot test with katex', () => {
|
|
||||||
const { asFragment } = render(<TrueFalse {...katexMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('TrueFalse snapshot test with moodle', () => {
|
|
||||||
const { asFragment } = render(<TrueFalse {...moodleMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('TrueFalse snapshot test with image', () => {
|
|
||||||
const { asFragment } = render(<TrueFalse {...imageMock} />);
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,301 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Numerical snapshot test with html 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Numerical Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Numérique</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mrow><mi>z</mi><mi>z</mi><mi>z</mi></mrow><mrow><mi>y</mi><mi>y</mi><mi>y</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">\\frac{zzz}{yyy}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.988em;vertical-align:-0.8804em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">yyy</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.04398em;">zzz</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.8804em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="42, 43">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Numerical snapshot test with image 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Numerical Title with Image</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Numérique</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem with Image</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="42, 43, 44">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback with Image</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Numerical snapshot test with moodle 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Numerical Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Numérique</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="42, 43">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Numerical snapshot test with plain text 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Numerical Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Numérique</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="42, 43">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`ShortAnswer snapshot test with image 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Short Answer Title with Image</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Réponse courte</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem with Image
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="Answer 1
|
|
||||||
, Answer 2
|
|
||||||
, <img src="https://via.placeholder.com/150" alt="Sample Image" />">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback with Image</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ShortAnswer snapshot test with katex 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Short Answer Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Réponse courte</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mrow><mi>z</mi><mi>z</mi><mi>z</mi></mrow><mrow><mi>y</mi><mi>y</mi><mi>y</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">\\frac{zzz}{yyy}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.988em;vertical-align:-0.8804em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">yyy</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.04398em;">zzz</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.8804em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="Answer 1, Answer 2">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ShortAnswer snapshot test with moodle 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Short Answer Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Réponse courte</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="Answer 1, Answer 2">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ShortAnswer snapshot test with plain text 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Short Answer Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Réponse courte</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p>
|
|
||||||
<div>
|
|
||||||
<span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Réponse: </span><input class="gift-input" type="text" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: hsl(0, 0%, 16%);
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: 1px solid hsl(0, 0%, 81%);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
width: 90%;
|
|
||||||
" placeholder="Answer 1, Answer 2">
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,438 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`TrueFalse snapshot test with image 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample Short Answer Title with Image</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Vrai/Faux</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem with Image</p><span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Choisir une réponse:</span>
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Vrai
|
|
||||||
</label>
|
|
||||||
<svg style="
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
|
|
||||||
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 style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Correct!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Faux
|
|
||||||
</label>
|
|
||||||
<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>
|
|
||||||
<span style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Incorrect!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p><img src="https://via.placeholder.com/150" alt="Sample Image" /></p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TrueFalse snapshot test with katex 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample True/False Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Vrai/Faux</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mrow><mi>z</mi><mi>z</mi><mi>z</mi></mrow><mrow><mi>y</mi><mi>y</mi><mi>y</mi></mrow></mfrac></mrow><annotation encoding="application/x-tex">\\frac{zzz}{yyy}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.988em;vertical-align:-0.8804em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.1076em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.03588em;">yyy</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.04398em;">zzz</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.8804em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p><span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Choisir une réponse:</span>
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Vrai
|
|
||||||
</label>
|
|
||||||
<svg style="
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
|
|
||||||
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 style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Correct!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Faux
|
|
||||||
</label>
|
|
||||||
<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>
|
|
||||||
<span style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Incorrect!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback
|
|
||||||
</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TrueFalse snapshot test with moodle 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample True/False Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Vrai/Faux</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p><span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Choisir une réponse:</span>
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Vrai
|
|
||||||
</label>
|
|
||||||
<svg style="
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
|
|
||||||
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 style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Correct!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Faux
|
|
||||||
</label>
|
|
||||||
<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>
|
|
||||||
<span style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Incorrect!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TrueFalse snapshot test with plain text 1`] = `
|
|
||||||
<DocumentFragment>
|
|
||||||
<section style="
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: relative;
|
|
||||||
padding: 1rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
border: solid hsl(0, 0%, 100%) 2px;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
">
|
|
||||||
<span>
|
|
||||||
<span style="
|
|
||||||
color: #5271FF;
|
|
||||||
">Sample True/False Title</span>
|
|
||||||
</span>
|
|
||||||
<span style="
|
|
||||||
margin-left: auto;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
flex: none;
|
|
||||||
margin-bottom: 10px;">
|
|
||||||
<span style="
|
|
||||||
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
|
|
||||||
padding-left: 0.7rem;
|
|
||||||
padding-right: 0.7rem;
|
|
||||||
padding-top: 0.4rem;
|
|
||||||
padding-bottom: 0.4rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: hsl(0, 0%, 100%);
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Vrai/Faux</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Sample Stem</p><span style="
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
">Choisir une réponse:</span>
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Vrai
|
|
||||||
</label>
|
|
||||||
<svg style="
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
|
|
||||||
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 style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Correct!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='multiple-choice-answers-container'>
|
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
|
||||||
|
|
||||||
<label style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.2em 0 0.2em 0;
|
|
||||||
|
|
||||||
color: hsl(0, 0%, 0%);
|
|
||||||
" for="idmocked-id">
|
|
||||||
Faux
|
|
||||||
</label>
|
|
||||||
<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>
|
|
||||||
<span style="
|
|
||||||
color: hsl(180, 15%, 41%);
|
|
||||||
">Incorrect!</span>
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
<div style="
|
|
||||||
position: relative;
|
|
||||||
margin-top: 1rem;
|
|
||||||
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%);
|
|
||||||
">
|
|
||||||
<p>Sample Global Feedback</p>
|
|
||||||
</div></section>
|
|
||||||
</DocumentFragment>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import DragAndDrop from 'src/components/ImportModal/ImportModal';
|
import DragAndDrop from '../../../components/ImportModal/ImportModal';
|
||||||
|
|
||||||
describe('DragAndDrop Component', () => {
|
describe('DragAndDrop Component', () => {
|
||||||
|
|
||||||
|
|
@ -70,4 +69,4 @@ describe('DragAndDrop Component', () => {
|
||||||
target: { files: [file] },
|
target: { files: [file] },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import LaunchQuizDialog from 'src/components/LaunchQuizDialog/LaunchQuizDialog';
|
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';
|
||||||
|
|
||||||
// Mock the functions passed as props
|
// Mock the functions passed as props
|
||||||
const mockHandleOnClose = jest.fn();
|
const mockHandleOnClose = jest.fn();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||||
|
|
||||||
describe('LoadingCircle', () => {
|
describe('LoadingCircle', () => {
|
||||||
it('displays the provided text correctly', () => {
|
it('displays the provided text correctly', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import MultipleChoiceQuestion from 'src/components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
|
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
|
||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// NumericalQuestion.test.tsx
|
// NumericalQuestion.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import NumericalQuestion from 'src/components/Questions/NumericalQuestion/NumericalQuestion';
|
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion';
|
||||||
|
|
||||||
describe('NumericalQuestion Component', () => {
|
describe('NumericalQuestion Component', () => {
|
||||||
const mockHandleSubmitAnswer = jest.fn();
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Question.test.tsx
|
// Question.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import Questions from 'src/components/Questions/Question';
|
import Questions from '../../../components/Questions/Question';
|
||||||
import { GIFTQuestion } from 'gift-pegjs';
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// ShortAnswerQuestion.test.tsx
|
// ShortAnswerQuestion.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import ShortAnswerQuestion from 'src/components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
|
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
|
||||||
|
|
||||||
describe('ShortAnswerQuestion Component', () => {
|
describe('ShortAnswerQuestion Component', () => {
|
||||||
const mockHandleSubmitAnswer = jest.fn();
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
@ -12,7 +11,6 @@ describe('ShortAnswerQuestion Component', () => {
|
||||||
questionTitle: 'Sample Question',
|
questionTitle: 'Sample Question',
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
id: '1',
|
|
||||||
feedback: {
|
feedback: {
|
||||||
format: 'text',
|
format: 'text',
|
||||||
text: 'Correct answer feedback'
|
text: 'Correct answer feedback'
|
||||||
|
|
@ -24,7 +22,6 @@ describe('ShortAnswerQuestion Component', () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
|
||||||
feedback: null,
|
feedback: null,
|
||||||
isCorrect: false,
|
isCorrect: false,
|
||||||
text: {
|
text: {
|
||||||
|
|
@ -61,7 +58,7 @@ describe('ShortAnswerQuestion Component', () => {
|
||||||
expect(submitButton).toBeDisabled();
|
expect(submitButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not submitted answer if nothing is entered', () => {
|
it('not submited answer if nothing is entered', () => {
|
||||||
const submitButton = screen.getByText('Répondre');
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// TrueFalseQuestion.test.tsx
|
// TrueFalseQuestion.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent, screen, act } from '@testing-library/react';
|
import { render, fireEvent, screen, act } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import TrueFalseQuestion from 'src/components/Questions/TrueFalseQuestion/TrueFalseQuestion';
|
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
describe('TrueFalseQuestion Component', () => {
|
describe('TrueFalseQuestion Component', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// Importez le type UserType s'il n'est pas déjà importé
|
// Importez le type UserType s'il n'est pas déjà importé
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
|
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
|
||||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||||
|
|
||||||
describe('StudentWaitPage Component', () => {
|
describe('StudentWaitPage Component', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent, screen } from '@testing-library/react';
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
@ -33,4 +32,4 @@ describe('Home', () => {
|
||||||
fireEvent.click(teacherButton);
|
fireEvent.click(teacherButton);
|
||||||
expect(window.location.pathname).toBe('/teacher/dashboard');
|
expect(window.location.pathname).toBe('/teacher/dashboard');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { parse } from 'gift-pegjs';
|
import { parse } from 'gift-pegjs';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { QuestionType } from '../../../../Types/QuestionType';
|
import { QuestionType } from '../../../../Types/QuestionType';
|
||||||
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
|
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||||
|
|
||||||
const mockGiftQuestions = parse(
|
const mockGiftQuestions = parse(
|
||||||
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
|
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
|
||||||
|
|
@ -77,5 +76,21 @@ describe('StudentModeQuiz', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('navigates to the previous question', async () => {
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByText('Option A'));
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByText('Répondre'));
|
||||||
|
});
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByText('Question précédente'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
//TeacherModeQuiz.test.tsx
|
//TeacherModeQuiz.test.tsx
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent, act } from '@testing-library/react';
|
import { render, fireEvent, act } from '@testing-library/react';
|
||||||
import { screen } from '@testing-library/dom';
|
import { screen } from '@testing-library/dom';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { parse } from 'gift-pegjs';
|
import { parse } from 'gift-pegjs';
|
||||||
|
|
||||||
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
// import { mock } from 'node:test';
|
// import { mock } from 'node:test';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ Object.defineProperty(window, 'localStorage', {
|
||||||
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
|
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
|
||||||
describe.skip('QuizService', () => {
|
describe.skip('QuizService', () => {
|
||||||
const mockQuizzes: QuizType[] = [
|
const mockQuizzes: QuizType[] = [
|
||||||
{ folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
{ folderId: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
||||||
{ folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
{ folderId: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
//WebsocketService.test.tsx
|
//WebsocketService.test.tsx
|
||||||
import WebsocketService from '../../services/WebsocketService';
|
import WebsocketService from '../../services/WebsocketService';
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
import { ENV_VARIABLES } from '../../constants';
|
||||||
|
|
||||||
jest.mock('socket.io-client');
|
jest.mock('socket.io-client');
|
||||||
|
|
||||||
|
jest.mock('../../constants', () => ({
|
||||||
|
ENV_VARIABLES: {
|
||||||
|
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
describe('WebSocketService', () => {
|
describe('WebSocketService', () => {
|
||||||
let mockSocket: Partial<Socket>;
|
let mockSocket: Partial<Socket>;
|
||||||
|
|
||||||
|
|
@ -23,13 +29,13 @@ describe('WebSocketService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('connect should initialize socket connection', () => {
|
test('connect should initialize socket connection', () => {
|
||||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
expect(io).toHaveBeenCalled();
|
expect(io).toHaveBeenCalled();
|
||||||
expect(WebsocketService['socket']).toBe(mockSocket);
|
expect(WebsocketService['socket']).toBe(mockSocket);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('disconnect should terminate socket connection', () => {
|
test('disconnect should terminate socket connection', () => {
|
||||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
expect(WebsocketService['socket']).toBeTruthy();
|
expect(WebsocketService['socket']).toBeTruthy();
|
||||||
WebsocketService.disconnect();
|
WebsocketService.disconnect();
|
||||||
expect(mockSocket.disconnect).toHaveBeenCalled();
|
expect(mockSocket.disconnect).toHaveBeenCalled();
|
||||||
|
|
@ -37,8 +43,8 @@ describe('WebSocketService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('createRoom should emit create-room event', () => {
|
test('createRoom should emit create-room event', () => {
|
||||||
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.createRoom();
|
WebsocketService.createRoom('test');
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,7 +52,7 @@ describe('WebSocketService', () => {
|
||||||
const roomName = 'testRoom';
|
const roomName = 'testRoom';
|
||||||
const question = { id: 1, text: 'Sample Question' };
|
const question = { id: 1, text: 'Sample Question' };
|
||||||
|
|
||||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.nextQuestion(roomName, question);
|
WebsocketService.nextQuestion(roomName, question);
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
|
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
|
||||||
});
|
});
|
||||||
|
|
@ -55,7 +61,7 @@ describe('WebSocketService', () => {
|
||||||
const roomName = 'testRoom';
|
const roomName = 'testRoom';
|
||||||
const questions = [{ id: 1, text: 'Sample Question' }];
|
const questions = [{ id: 1, text: 'Sample Question' }];
|
||||||
|
|
||||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.launchStudentModeQuiz(roomName, questions);
|
WebsocketService.launchStudentModeQuiz(roomName, questions);
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
|
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
|
||||||
roomName,
|
roomName,
|
||||||
|
|
@ -66,7 +72,7 @@ describe('WebSocketService', () => {
|
||||||
test('endQuiz should emit end-quiz event with correct parameters', () => {
|
test('endQuiz should emit end-quiz event with correct parameters', () => {
|
||||||
const roomName = 'testRoom';
|
const roomName = 'testRoom';
|
||||||
|
|
||||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.endQuiz(roomName);
|
WebsocketService.endQuiz(roomName);
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
|
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
|
||||||
});
|
});
|
||||||
|
|
@ -75,7 +81,7 @@ describe('WebSocketService', () => {
|
||||||
const enteredRoomName = 'testRoom';
|
const enteredRoomName = 'testRoom';
|
||||||
const username = 'testUser';
|
const username = 'testUser';
|
||||||
|
|
||||||
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
WebsocketService.joinRoom(enteredRoomName, username);
|
WebsocketService.joinRoom(enteredRoomName, username);
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
|
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// GoBackButton.tsx
|
// GoBackButton.tsx
|
||||||
import React from 'react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||||
|
|
@ -34,7 +33,7 @@ const DisconnectButton: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnReturn = () => {
|
const handleOnReturn = () => {
|
||||||
if (onReturn) {
|
if (!!onReturn) {
|
||||||
onReturn();
|
onReturn();
|
||||||
} else {
|
} else {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './footer.css';
|
import './footer.css';
|
||||||
|
|
||||||
type FooterProps = object; //empty object
|
interface FooterProps {
|
||||||
|
|
||||||
const Footer: React.FC<FooterProps> = () => {
|
}
|
||||||
|
|
||||||
|
const Footer: React.FC<FooterProps> = ({ }) => {
|
||||||
return (
|
return (
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<div className="footer-content">
|
<div className="footer-content">
|
||||||
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
|
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a>
|
||||||
|
<span className="divider">|</span>
|
||||||
|
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
|
||||||
<span className="divider">|</span>
|
<span className="divider">|</span>
|
||||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,16 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
|
const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Utilisez les valeurs {T}, {F}, {TRUE} et {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 QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\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 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 le signe ~ 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 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}";
|
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} // OU \nQuestion {#=PetitNombre..GrandNombre\n}\n// La tolérance est un pourcentage.\n// La réponse doit être comprise entre PetitNombre et GrandNombre";
|
||||||
return (
|
return (
|
||||||
<div className="gift-cheat-sheet">
|
<div className="gift-cheat-sheet">
|
||||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||||
<span>
|
<span>
|
||||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||||
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
||||||
syntaxe pour chaque type de question :
|
syntaxe pour chaque type de question :
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -126,7 +126,7 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
<h4> 7. Paramètres optionnels </h4>
|
<h4> 7. Paramètres optionnels </h4>
|
||||||
<p>
|
<p>
|
||||||
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
|
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
|
||||||
réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
|
réponses ou feedback, vous devez 'échapper' ces derniers en ajoutant un \
|
||||||
devant:
|
devant:
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<pre>
|
||||||
|
|
@ -140,9 +140,9 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
<h4> 8. LaTeX et Markdown</h4>
|
<h4> 8. LaTeX et Markdown</h4>
|
||||||
<p>
|
<p>
|
||||||
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
|
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
|
||||||
à «échapper» les caractères spéciaux mentionnés plus haut.
|
à 'échapper' les caractères spéciaux mentionnés plus haut.
|
||||||
</p>
|
</p>
|
||||||
<p>Exemple d'équation:</p>
|
<p>Exemple d'équation:</p>
|
||||||
<pre>
|
<pre>
|
||||||
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
||||||
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
|
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
|
||||||
|
|
@ -167,16 +167,16 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
{'")'}
|
{'")'}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
||||||
<pre>
|
<pre>
|
||||||
<code className="question-code-block">
|
<code className="question-code-block">
|
||||||
{'[markdown]Ceci est un chat: \n\n{T}'}
|
{'[markdown]Ceci est un chat: \n\n{T}'}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
||||||
<p>Note : On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
|
<p>Note : On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
|
||||||
<p style={{ color: 'red' }}>
|
<p style={{ color: 'red' }}>
|
||||||
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
||||||
supportée.
|
supportée.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -184,7 +184,7 @@ const GiftCheatSheet: React.FC = () => {
|
||||||
<div className="question-type">
|
<div className="question-type">
|
||||||
<h4> 10. Informations supplémentaires </h4>
|
<h4> 10. Informations supplémentaires </h4>
|
||||||
<p>
|
<p>
|
||||||
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
||||||
application.
|
application.
|
||||||
</p>
|
</p>
|
||||||
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react';
|
||||||
import Template, { ErrorTemplate } from './templates';
|
import Template, { ErrorTemplate } from './templates';
|
||||||
import { parse } from 'gift-pegjs';
|
import { parse } from 'gift-pegjs';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
interface GIFTTemplatePreviewProps {
|
interface GIFTTemplatePreviewProps {
|
||||||
questions: string[];
|
questions: string[];
|
||||||
|
|
@ -74,7 +73,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
<div className="error">{error}</div>
|
<div className="error">{error}</div>
|
||||||
) : isPreviewReady ? (
|
) : isPreviewReady ? (
|
||||||
<div data-testid="preview-container">
|
<div data-testid="preview-container">
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
|
<div dangerouslySetInnerHTML={{ __html: items }}></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="loading">Chargement de la prévisualisation...</div>
|
<div className="loading">Chargement de la prévisualisation...</div>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function formatLatex(text: string): string {
|
||||||
*/
|
*/
|
||||||
export default function textType({ text }: TextTypeOptions) {
|
export default function textType({ text }: TextTypeOptions) {
|
||||||
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
|
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
|
||||||
let parsedText = '';
|
|
||||||
switch (text.format) {
|
switch (text.format) {
|
||||||
case 'moodle':
|
case 'moodle':
|
||||||
case 'plain':
|
case 'plain':
|
||||||
|
|
@ -40,7 +40,7 @@ export default function textType({ text }: TextTypeOptions) {
|
||||||
// Strip outer paragraph tags (not a great approach with regex)
|
// Strip outer paragraph tags (not a great approach with regex)
|
||||||
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
||||||
case 'markdown':
|
case 'markdown':
|
||||||
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
|
const parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
|
||||||
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported text format: ${text.format}`);
|
throw new Error(`Unsupported text format: ${text.format}`);
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
||||||
<DialogContentText sx={{ textAlign: 'center' }}>
|
<DialogContentText sx={{ textAlign: 'center' }}>
|
||||||
Déposer des fichiers ici ou
|
Déposer des fichiers ici ou
|
||||||
<br />
|
<br />
|
||||||
cliquez pour ouvrir l'explorateur des fichiers
|
cliquez pour ouvrir l'explorateur des fichiers
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</div>
|
</div>
|
||||||
<Download color="primary" />
|
<Download color="primary" />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="sticky-column">
|
<TableCell className="sticky-column">
|
||||||
<div className="text-base text-bold">Nom d'utilisateur</div>
|
<div className="text-base text-bold">Nom d'utilisateur</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
|
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import '../questionStyle.css';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
|
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
|
||||||
import { TextFormat } from '../../GiftTemplate/templates/types';
|
import { TextFormat } from '../../GiftTemplate/templates/types';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
// import Latex from 'react-latex';
|
// import Latex from 'react-latex';
|
||||||
|
|
||||||
type Choices = {
|
type Choices = {
|
||||||
|
|
@ -23,7 +22,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
||||||
|
|
||||||
const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
|
const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
|
||||||
const [answer, setAnswer] = useState<string>();
|
const [answer, setAnswer] = useState<string>();
|
||||||
|
|
||||||
|
|
@ -41,7 +39,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="question-container">
|
<div className="question-container">
|
||||||
<div className="question content">
|
<div className="question content">
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
|
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="choices-wrapper mb-1">
|
<div className="choices-wrapper mb-1">
|
||||||
{choices.map((choice, i) => {
|
{choices.map((choice, i) => {
|
||||||
|
|
@ -58,7 +56,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
||||||
(choice.isCorrect ? '✅' : '❌')}
|
(choice.isCorrect ? '✅' : '❌')}
|
||||||
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
||||||
<div className={`answer-text ${selected}`}>
|
<div className={`answer-text ${selected}`}>
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} />
|
{formatLatex(choice.text.text)}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{choice.feedback && showAnswer && (
|
{choice.feedback && showAnswer && (
|
||||||
|
|
@ -74,9 +72,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
||||||
{globalFeedback && showAnswer && (
|
{globalFeedback && showAnswer && (
|
||||||
<div className="global-feedback mb-2">{globalFeedback}</div>
|
<div className="global-feedback mb-2">{globalFeedback}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showAnswer && handleOnSubmitAnswer && (
|
{!showAnswer && handleOnSubmitAnswer && (
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -85,7 +81,6 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
||||||
disabled={answer === undefined}
|
disabled={answer === undefined}
|
||||||
>
|
>
|
||||||
Répondre
|
Répondre
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import '../questionStyle.css';
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
import textType from '../../GiftTemplate/templates/TextType';
|
import textType from '../../GiftTemplate/templates/TextType';
|
||||||
import { TextFormat } from '../../GiftTemplate/templates/types';
|
import { TextFormat } from '../../GiftTemplate/templates/types';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
type CorrectAnswer = {
|
type CorrectAnswer = {
|
||||||
numberHigh?: number;
|
numberHigh?: number;
|
||||||
|
|
@ -35,7 +34,7 @@ const NumericalQuestion: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="question-wrapper">
|
<div className="question-wrapper">
|
||||||
<div>
|
<div>
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
|
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
|
||||||
</div>
|
</div>
|
||||||
{showAnswer ? (
|
{showAnswer ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const Question: React.FC<QuestionProps> = ({
|
||||||
questionTypeComponent = (
|
questionTypeComponent = (
|
||||||
<MultipleChoiceQuestion
|
<MultipleChoiceQuestion
|
||||||
questionStem={question.stem}
|
questionStem={question.stem}
|
||||||
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
|
choices={question.choices}
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
globalFeedback={question.globalFeedback?.text}
|
globalFeedback={question.globalFeedback?.text}
|
||||||
|
|
@ -78,7 +78,7 @@ const Question: React.FC<QuestionProps> = ({
|
||||||
questionTypeComponent = (
|
questionTypeComponent = (
|
||||||
<ShortAnswerQuestion
|
<ShortAnswerQuestion
|
||||||
questionContent={question.stem}
|
questionContent={question.stem}
|
||||||
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
|
choices={question.choices}
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
showAnswer={showAnswer}
|
showAnswer={showAnswer}
|
||||||
globalFeedback={question.globalFeedback?.text}
|
globalFeedback={question.globalFeedback?.text}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ import '../questionStyle.css';
|
||||||
import { Button, TextField } from '@mui/material';
|
import { Button, TextField } from '@mui/material';
|
||||||
import textType from '../../GiftTemplate/templates/TextType';
|
import textType from '../../GiftTemplate/templates/TextType';
|
||||||
import { TextFormat } from '../../GiftTemplate/templates/types';
|
import { TextFormat } from '../../GiftTemplate/templates/types';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
type Choices = {
|
type Choices = {
|
||||||
feedback: { format: string; text: string } | null;
|
feedback: { format: string; text: string } | null;
|
||||||
isCorrect: boolean;
|
isCorrect: boolean;
|
||||||
text: { format: string; text: string };
|
text: { format: string; text: string };
|
||||||
weigth?: number;
|
weigth?: number;
|
||||||
id: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -29,15 +27,13 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="question-wrapper">
|
<div className="question-wrapper">
|
||||||
<div className="question content">
|
<div className="question content">
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
|
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
|
||||||
</div>
|
</div>
|
||||||
{showAnswer ? (
|
{showAnswer ? (
|
||||||
<>
|
<>
|
||||||
<div className="correct-answer-text mb-1">
|
<div className="correct-answer-text mb-1">
|
||||||
{choices.map((choice) => (
|
{choices.map((choice) => (
|
||||||
<div key={choice.id} className="mb-1">
|
<div className="mb-1">{choice.text.text}</div>
|
||||||
{choice.text.text}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
|
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import '../questionStyle.css';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import textType from '../../GiftTemplate/templates/TextType';
|
import textType from '../../GiftTemplate/templates/TextType';
|
||||||
import { TextFormat } from '../../GiftTemplate/templates/types';
|
import { TextFormat } from '../../GiftTemplate/templates/types';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
questionContent: TextFormat;
|
questionContent: TextFormat;
|
||||||
|
|
@ -28,7 +27,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="question-container">
|
<div className="question-container">
|
||||||
<div className="question content">
|
<div className="question content">
|
||||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
|
<div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="choices-wrapper mb-1">
|
<div className="choices-wrapper mb-1">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// GoBackButton.tsx
|
// GoBackButton.tsx
|
||||||
import React from 'react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||||
|
|
@ -34,7 +33,7 @@ const ReturnButton: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnReturn = () => {
|
const handleOnReturn = () => {
|
||||||
if (onReturn) {
|
if (!!onReturn) {
|
||||||
onReturn();
|
onReturn();
|
||||||
} else {
|
} else {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
// StudentModeQuiz.tsx
|
// StudentModeQuiz.tsx
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import QuestionComponent from '../Questions/Question';
|
import QuestionComponent from '../Questions/Question';
|
||||||
|
|
||||||
import '../../pages/Student/JoinRoom/joinRoom.css';
|
import '../../pages/Student/JoinRoom/joinRoom.css';
|
||||||
import { QuestionType } from '../../Types/QuestionType';
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
// import { QuestionService } from '../../services/QuestionService';
|
// import { QuestionService } from '../../services/QuestionService';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
|
import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
|
||||||
//import { ChevronLeft, ChevronRight } from '@mui/icons-material';
|
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
|
||||||
|
|
||||||
interface StudentModeQuizProps {
|
interface StudentModeQuizProps {
|
||||||
questions: QuestionType[];
|
questions: QuestionType[];
|
||||||
|
|
@ -24,10 +25,10 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
||||||
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
|
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
|
||||||
// const [imageUrl, setImageUrl] = useState('');
|
// const [imageUrl, setImageUrl] = useState('');
|
||||||
|
|
||||||
// const previousQuestion = () => {
|
const previousQuestion = () => {
|
||||||
// setQuestion(questions[Number(questionInfos.question?.id) - 2]);
|
setQuestion(questions[Number(questionInfos.question?.id) - 2]);
|
||||||
// setIsAnswerSubmitted(false);
|
setIsAnswerSubmitted(false);
|
||||||
// };
|
};
|
||||||
|
|
||||||
useEffect(() => {}, [questionInfos]);
|
useEffect(() => {}, [questionInfos]);
|
||||||
|
|
||||||
|
|
@ -54,12 +55,12 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<div className="question-component-container">
|
<div className="question-component-container">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
{/* <QuestionNavigation
|
<QuestionNavigation
|
||||||
currentQuestionId={Number(questionInfos.question.id)}
|
currentQuestionId={Number(questionInfos.question.id)}
|
||||||
questionsLength={questions.length}
|
questionsLength={questions.length}
|
||||||
previousQuestion={previousQuestion}
|
previousQuestion={previousQuestion}
|
||||||
nextQuestion={nextQuestion}
|
nextQuestion={nextQuestion}
|
||||||
/> */}
|
/>
|
||||||
</div>
|
</div>
|
||||||
<QuestionComponent
|
<QuestionComponent
|
||||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||||
|
|
@ -68,7 +69,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
||||||
/>
|
/>
|
||||||
<div className="center-h-align mt-1/2">
|
<div className="center-h-align mt-1/2">
|
||||||
<div className="w-12">
|
<div className="w-12">
|
||||||
{/* <Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={previousQuestion}
|
onClick={previousQuestion}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -76,14 +77,14 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
||||||
disabled={Number(questionInfos.question.id) <= 1}
|
disabled={Number(questionInfos.question.id) <= 1}
|
||||||
>
|
>
|
||||||
Question précédente
|
Question précédente
|
||||||
</Button> */}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-12">
|
<div className="w-12">
|
||||||
<Button style={{ display: isAnswerSubmitted ? 'block' : 'none' }}
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={nextQuestion}
|
onClick={nextQuestion}
|
||||||
fullWidth
|
fullWidth
|
||||||
//endIcon={<ChevronRight />}
|
endIcon={<ChevronRight />}
|
||||||
disabled={Number(questionInfos.question.id) >= questions.length}
|
disabled={Number(questionInfos.question.id) >= questions.length}
|
||||||
>
|
>
|
||||||
Question suivante
|
Question suivante
|
||||||
|
|
@ -92,7 +93,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import { Box, Button, Chip } from '@mui/material';
|
import { Box, Button, Chip } from '@mui/material';
|
||||||
import { StudentType } from '../../Types/StudentType';
|
import { StudentType } from '../../Types/StudentType';
|
||||||
import { PlayArrow } from '@mui/icons-material';
|
import { PlayArrow } from '@mui/icons-material';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import QuestionComponent from '../Questions/Question';
|
||||||
import '../../pages/Student/JoinRoom/joinRoom.css';
|
import '../../pages/Student/JoinRoom/joinRoom.css';
|
||||||
import { QuestionType } from '../../Types/QuestionType';
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
// import { QuestionService } from '../../services/QuestionService';
|
// import { QuestionService } from '../../services/QuestionService';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
|
||||||
|
|
||||||
interface TeacherModeQuizProps {
|
interface TeacherModeQuizProps {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
// constants.tsx
|
// constants.tsx
|
||||||
const ENV_VARIABLES = {
|
const ENV_VARIABLES = {
|
||||||
MODE: 'production',
|
MODE: 'production',
|
||||||
VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
|
VITE_BACKEND_URL: process.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 };
|
export { ENV_VARIABLES };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
|
@ -28,16 +27,10 @@ const theme = createTheme({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
if (rootElement) {
|
<BrowserRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
<App />
|
||||||
<BrowserRouter>
|
</ThemeProvider>
|
||||||
<ThemeProvider theme={theme}>
|
</BrowserRouter>
|
||||||
<App />
|
);
|
||||||
</ThemeProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Root element not found');
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
//import { ENV_VARIABLES } from '../../../constants';
|
||||||
|
|
||||||
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
|
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||||
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
|
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||||
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
|
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
|
||||||
|
|
||||||
import './joinRoom.css';
|
import './joinRoom.css';
|
||||||
import { QuestionType } from '../../../Types/QuestionType';
|
import { QuestionType } from '../../../Types/QuestionType';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
|
||||||
|
|
||||||
const JoinRoom: React.FC = () => {
|
const JoinRoom: React.FC = () => {
|
||||||
const [roomName, setRoomName] = useState('');
|
const [roomName, setRoomName] = useState('');
|
||||||
|
|
@ -27,15 +27,14 @@ const JoinRoom: React.FC = () => {
|
||||||
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
const [isConnecting, setIsConnecting] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleCreateSocket();
|
//handleCreateSocket();
|
||||||
return () => {
|
return () => {
|
||||||
disconnect();
|
disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateSocket = () => {
|
const handleCreateSocket = () => {
|
||||||
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
|
const socket = webSocketService.connect(`/api/room/${roomName}/socket`);
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
|
||||||
|
|
||||||
socket.on('join-success', () => {
|
socket.on('join-success', () => {
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
|
|
@ -64,10 +63,10 @@ const JoinRoom: React.FC = () => {
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
switch (error.message) {
|
switch (error.message) {
|
||||||
case 'timeout':
|
case 'timeout':
|
||||||
setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible");
|
setConnectionError("Le serveur n'est pas disponible");
|
||||||
break;
|
break;
|
||||||
case 'websocket error':
|
case 'websocket error':
|
||||||
setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible");
|
setConnectionError("Le serveur n'est pas disponible");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { parse } from 'gift-pegjs';
|
import { parse } from 'gift-pegjs';
|
||||||
|
|
||||||
import Template from 'src/components/GiftTemplate/templates';
|
import Template from '../../../components/GiftTemplate/templates';
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
// import { QuestionService } from '../../../services/QuestionService';
|
// import { QuestionService } from '../../../services/QuestionService';
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
import './dashboard.css';
|
import './dashboard.css';
|
||||||
import ImportModal from 'src/components/ImportModal/ImportModal';
|
import ImportModal from '../../../components/ImportModal/ImportModal';
|
||||||
//import axios from 'axios';
|
//import axios from 'axios';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -18,11 +18,8 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
NativeSelect,
|
NativeSelect
|
||||||
CardContent,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
|
|
@ -30,50 +27,19 @@ import {
|
||||||
FileDownload,
|
FileDownload,
|
||||||
Add,
|
Add,
|
||||||
Upload,
|
Upload,
|
||||||
FolderCopy,
|
|
||||||
ContentCopy,
|
ContentCopy,
|
||||||
Edit,
|
Edit,
|
||||||
Share,
|
Share,
|
||||||
// DriveFileMove
|
// DriveFileMove
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
// Create a custom-styled Card component
|
|
||||||
const CustomCard = styled(Card)({
|
|
||||||
overflow: 'visible', // Override the overflow property
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [quizzes, setQuizzes] = useState<QuizType[]>([]);
|
const [quizzes, setQuizzes] = useState<QuizType[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
||||||
const [folders, setFolders] = useState<FolderType[]>([]);
|
const [folders, setFolders] = useState<FolderType[]>([]);
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
|
const [selectedFolder, setSelectedFolder] = useState<string>(''); // Selected folder
|
||||||
|
|
||||||
// Filter quizzes based on search term
|
|
||||||
// const filteredQuizzes = quizzes.filter(quiz =>
|
|
||||||
// quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
// );
|
|
||||||
const filteredQuizzes = useMemo(() => {
|
|
||||||
return quizzes.filter(
|
|
||||||
(quiz) =>
|
|
||||||
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [quizzes, searchTerm]);
|
|
||||||
|
|
||||||
|
|
||||||
// Group quizzes by folder
|
|
||||||
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
|
|
||||||
if (!acc[quiz.folderName]) {
|
|
||||||
acc[quiz.folderName] = [];
|
|
||||||
}
|
|
||||||
acc[quiz.folderName].push(quiz);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, QuizType[]>);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -82,7 +48,7 @@ const Dashboard: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const userFolders = await ApiService.getUserFolders();
|
let userFolders = await ApiService.getUserFolders();
|
||||||
|
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
}
|
}
|
||||||
|
|
@ -92,23 +58,40 @@ const Dashboard: React.FC = () => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
setSelectedFolderId(event.target.value);
|
setSelectedFolder(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchQuizzesForFolder = async () => {
|
const fetchQuizzesForFolder = async () => {
|
||||||
|
|
||||||
if (selectedFolderId == '') {
|
if (selectedFolder == '') {
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
console.log("show all quizes")
|
console.log("show all quizes")
|
||||||
let quizzes: QuizType[] = [];
|
var quizzes: QuizType[] = [];
|
||||||
|
|
||||||
for (const folder of folders as FolderType[]) {
|
for (const folder of folders as FolderType[]) {
|
||||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
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[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,19 +99,17 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("show some quizzes")
|
console.log("show some quizzes")
|
||||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
|
||||||
console.log("folderQuizzes: ", folderQuizzes);
|
console.log("folderQuizzes: ", folderQuizzes);
|
||||||
// get the folder title from its id
|
|
||||||
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
|
|
||||||
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
|
|
||||||
setQuizzes(folderQuizzes as QuizType[]);
|
setQuizzes(folderQuizzes as QuizType[]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchQuizzesForFolder();
|
fetchQuizzesForFolder();
|
||||||
}, [selectedFolderId]);
|
}, [selectedFolder]);
|
||||||
|
|
||||||
|
|
||||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
@ -153,24 +134,22 @@ const Dashboard: React.FC = () => {
|
||||||
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
||||||
try {
|
try {
|
||||||
await ApiService.duplicateQuiz(quiz._id);
|
await ApiService.duplicateQuiz(quiz._id);
|
||||||
if (selectedFolderId == '') {
|
if (selectedFolder == '') {
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
console.log("show all quizzes")
|
console.log("show all quizes")
|
||||||
let quizzes: QuizType[] = [];
|
var quizzes: QuizType[] = [];
|
||||||
|
|
||||||
for (const folder of folders as FolderType[]) {
|
for (const folder of folders as FolderType[]) {
|
||||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
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[])
|
||||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuizzes(quizzes as QuizType[]);
|
setQuizzes(quizzes as QuizType[]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log("show some quizzes")
|
console.log("show some quizzes")
|
||||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
|
||||||
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
|
|
||||||
setQuizzes(folderQuizzes as QuizType[]);
|
setQuizzes(folderQuizzes as QuizType[]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -179,6 +158,13 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredQuizzes = useMemo(() => {
|
||||||
|
return quizzes.filter(
|
||||||
|
(quiz) =>
|
||||||
|
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [quizzes, searchTerm]);
|
||||||
|
|
||||||
const handleOnImport = () => {
|
const handleOnImport = () => {
|
||||||
setShowImportModal(true);
|
setShowImportModal(true);
|
||||||
|
|
||||||
|
|
@ -196,7 +182,6 @@ const Dashboard: React.FC = () => {
|
||||||
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
|
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
|
||||||
const parsedItem = parse(questions[i]);
|
const parsedItem = parse(questions[i]);
|
||||||
Template(parsedItem[0]);
|
Template(parsedItem[0]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +190,30 @@ const Dashboard: React.FC = () => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const handleMoveQuiz = async (quiz: QuizType, newFolderId: string) => {
|
||||||
|
// try {
|
||||||
|
// await ApiService.moveQuiz(quiz._id, newFolderId);
|
||||||
|
// if (selectedFolder == '') {
|
||||||
|
// const folders = await ApiService.getUserFolders();
|
||||||
|
// var quizzes: QuizType[] = [];
|
||||||
|
|
||||||
|
// for (const folder of folders as FolderType[]) {
|
||||||
|
// const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||||
|
// quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setQuizzes(quizzes as QuizType[]);
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
|
||||||
|
// setQuizzes(folderQuizzes as QuizType[]);
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error moving quiz:', error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
const downloadTxtFile = async (quiz: QuizType) => {
|
const downloadTxtFile = async (quiz: QuizType) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -217,7 +226,7 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
//const { title, content } = selectedQuiz;
|
//const { title, content } = selectedQuiz;
|
||||||
let quizContent = "";
|
let quizContent = "";
|
||||||
const title = selectedQuiz.title;
|
let title = selectedQuiz.title;
|
||||||
console.log(selectedQuiz.content);
|
console.log(selectedQuiz.content);
|
||||||
selectedQuiz.content.forEach((question, qIndex) => {
|
selectedQuiz.content.forEach((question, qIndex) => {
|
||||||
const formattedQuestion = question.trim();
|
const formattedQuestion = question.trim();
|
||||||
|
|
@ -254,7 +263,7 @@ const Dashboard: React.FC = () => {
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||||
setSelectedFolderId(newlyCreatedFolder._id);
|
setSelectedFolder(newlyCreatedFolder._id);
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -264,17 +273,18 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
const handleDeleteFolder = async () => {
|
const handleDeleteFolder = async () => {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await ApiService.deleteFolder(selectedFolderId);
|
await ApiService.deleteFolder(selectedFolder);
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||||
console.log("show all quizzes")
|
console.log("show all quizes")
|
||||||
let quizzes: QuizType[] = [];
|
var quizzes: QuizType[] = [];
|
||||||
|
|
||||||
for (const folder of folders as FolderType[]) {
|
for (const folder of folders as FolderType[]) {
|
||||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||||
|
|
@ -283,20 +293,19 @@ const Dashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuizzes(quizzes as QuizType[]);
|
setQuizzes(quizzes as QuizType[]);
|
||||||
setSelectedFolderId('');
|
setSelectedFolder('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting folder:', error);
|
console.error('Error deleting folder:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRenameFolder = async () => {
|
const handleRenameFolder = async () => {
|
||||||
try {
|
try {
|
||||||
// folderId: string GET THIS FROM CURRENT FOLDER
|
// folderId: string GET THIS FROM CURRENT FOLDER
|
||||||
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
||||||
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
|
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
await ApiService.renameFolder(selectedFolderId, newTitle);
|
await ApiService.renameFolder(selectedFolder, newTitle);
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
|
|
||||||
|
|
@ -305,16 +314,15 @@ const Dashboard: React.FC = () => {
|
||||||
console.error('Error renaming folder:', error);
|
console.error('Error renaming folder:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicateFolder = async () => {
|
const handleDuplicateFolder = async () => {
|
||||||
try {
|
try {
|
||||||
// folderId: string GET THIS FROM CURRENT FOLDER
|
// folderId: string GET THIS FROM CURRENT FOLDER
|
||||||
await ApiService.duplicateFolder(selectedFolderId);
|
await ApiService.duplicateFolder(selectedFolder);
|
||||||
// TODO set the selected folder to be the duplicated folder
|
// TODO set the selected folder to be the duplicated folder
|
||||||
const userFolders = await ApiService.getUserFolders();
|
const userFolders = await ApiService.getUserFolders();
|
||||||
setFolders(userFolders as FolderType[]);
|
setFolders(userFolders as FolderType[]);
|
||||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||||
setSelectedFolderId(newlyCreatedFolder._id);
|
setSelectedFolder(newlyCreatedFolder._id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error duplicating folder:', error);
|
console.error('Error duplicating folder:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -384,7 +392,7 @@ const Dashboard: React.FC = () => {
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
id="select-folder"
|
id="select-folder"
|
||||||
color="primary"
|
color="primary"
|
||||||
value={selectedFolderId}
|
value={selectedFolder}
|
||||||
onChange={handleSelectFolder}
|
onChange={handleSelectFolder}
|
||||||
>
|
>
|
||||||
<option value=""> Tous les dossiers... </option>
|
<option value=""> Tous les dossiers... </option>
|
||||||
|
|
@ -407,7 +415,7 @@ const Dashboard: React.FC = () => {
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleRenameFolder}
|
onClick={handleRenameFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolder == ''} // cannot action on all
|
||||||
> <Edit /> </IconButton>
|
> <Edit /> </IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
@ -415,8 +423,8 @@ const Dashboard: React.FC = () => {
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDuplicateFolder}
|
onClick={handleDuplicateFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolder == ''} // cannot action on all
|
||||||
> <FolderCopy /> </IconButton>
|
> <ContentCopy /> </IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Supprimer dossier" placement="top">
|
<Tooltip title="Supprimer dossier" placement="top">
|
||||||
|
|
@ -424,7 +432,7 @@ const Dashboard: React.FC = () => {
|
||||||
aria-label="delete"
|
aria-label="delete"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDeleteFolder}
|
onClick={handleDeleteFolder}
|
||||||
disabled={selectedFolderId == ''} // cannot action on all
|
disabled={selectedFolder == ''} // cannot action on all
|
||||||
> <DeleteOutline /> </IconButton>
|
> <DeleteOutline /> </IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -452,72 +460,74 @@ const Dashboard: React.FC = () => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className='list'>
|
<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="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>
|
|
||||||
|
|
||||||
<div className='actions'>
|
{filteredQuizzes.map((quiz: QuizType) => (
|
||||||
<Tooltip title="Télécharger quiz" placement="top">
|
<div className='quiz'>
|
||||||
<IconButton
|
<div className='title'>
|
||||||
color="primary"
|
<Tooltip title="Lancer quiz" placement="top">
|
||||||
onClick={() => downloadTxtFile(quiz)}
|
<Button
|
||||||
> <FileDownload /> </IconButton>
|
variant="outlined"
|
||||||
</Tooltip>
|
onClick={() => handleLancerQuiz(quiz)}
|
||||||
|
disabled={!validateQuiz(quiz.content)}
|
||||||
|
>
|
||||||
|
{quiz.title}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tooltip title="Modifier quiz" placement="top">
|
<div className='actions'>
|
||||||
<IconButton
|
<Tooltip title="Télécharger quiz" placement="top">
|
||||||
color="primary"
|
<IconButton
|
||||||
onClick={() => handleEditQuiz(quiz)}
|
color="primary"
|
||||||
> <Edit /> </IconButton>
|
onClick={() => downloadTxtFile(quiz)}
|
||||||
</Tooltip>
|
> <FileDownload /> </IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Dupliquer quiz" placement="top">
|
<Tooltip title="Modifier quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleDuplicateQuiz(quiz)}
|
onClick={() => handleEditQuiz(quiz)}
|
||||||
> <ContentCopy /> </IconButton>
|
> <Edit /> </IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Supprimer quiz" placement="top">
|
{/* <Tooltip title="Bouger quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="delete"
|
color="primary"
|
||||||
color="primary"
|
onClick={() => handleMoveQuiz(quiz)}
|
||||||
onClick={() => handleRemoveQuiz(quiz)}
|
> <DriveFileMove /> </IconButton>
|
||||||
> <DeleteOutline /> </IconButton>
|
</Tooltip> */}
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Partager quiz" placement="top">
|
<Tooltip title="Dupliquer quiz" placement="top">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => handleShareQuiz(quiz)}
|
onClick={() => handleDuplicateQuiz(quiz)}
|
||||||
> <Share /> </IconButton>
|
> <ContentCopy /> </IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
|
||||||
</div>
|
<Tooltip title="Supprimer quiz" placement="top">
|
||||||
))}
|
<IconButton
|
||||||
</CardContent>
|
aria-label="delete"
|
||||||
</CustomCard>
|
color="primary"
|
||||||
|
onClick={() => handleRemoveQuiz(quiz)}
|
||||||
|
> <DeleteOutline /> </IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Partager quiz" placement="top">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => handleShareQuiz(quiz)}
|
||||||
|
> <Share /> </IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImportModal
|
<ImportModal
|
||||||
open={showImportModal}
|
open={showImportModal}
|
||||||
handleOnClose={() => setShowImportModal(false)}
|
handleOnClose={() => setShowImportModal(false)}
|
||||||
handleOnImport={handleOnImport}
|
handleOnImport={handleOnImport}
|
||||||
selectedFolder={selectedFolderId}
|
selectedFolder={selectedFolder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -525,11 +535,3 @@ const Dashboard: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
|
|
||||||
if (Array.isArray(folderQuizzes))
|
|
||||||
folderQuizzes.forEach((quiz) => {
|
|
||||||
quiz.folderName = folderName;
|
|
||||||
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,43 +77,4 @@ div:has(> #select-folder) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard .list .quiz .actions {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-card {
|
|
||||||
position: relative;
|
|
||||||
/* margin: 40px 0 20px 0; /* Add top margin to make space for the tab */
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #f9f9f9;
|
|
||||||
--outline-color: #e1e1e1;
|
|
||||||
border: 2px solid var(--outline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-tab {
|
|
||||||
position: absolute;
|
|
||||||
top: -33px;
|
|
||||||
left: 9px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap; /* Prevent text from wrapping */
|
|
||||||
display: inline-block; /* Ensure the tab width is based on content */
|
|
||||||
border: 2px solid var(--outline-color);
|
|
||||||
border-bottom-style: none;
|
|
||||||
background-color: white; /* Optional: background color to match the card */
|
|
||||||
color: #3f51b5; /* Text color to match the outline */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .folder-card:nth-child(odd) {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder-card:nth-child(even) {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
} */
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
// EditorQuiz.tsx
|
// EditorQuiz.tsx
|
||||||
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
|
|
||||||
import Editor from 'src/components/Editor/Editor';
|
import Editor from '../../../components/Editor/Editor';
|
||||||
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
import GiftCheatSheet from '../../../components/GIFTCheatSheet/GiftCheatSheet';
|
||||||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
|
||||||
|
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
|
|
||||||
import './editorQuiz.css';
|
import './editorQuiz.css';
|
||||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
import { escapeForGIFT } from '../../../utils/giftUtils';
|
import { escapeForGIFT } from '../../../utils/giftUtils';
|
||||||
|
|
@ -40,26 +40,6 @@ const QuizForm: React.FC = () => {
|
||||||
};
|
};
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
||||||
|
|
||||||
const scrollToTop = () => {
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (window.scrollY > 300) {
|
|
||||||
setShowScrollButton(true);
|
|
||||||
} else {
|
|
||||||
setShowScrollButton(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -182,10 +162,8 @@ const QuizForm: React.FC = () => {
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -267,7 +245,7 @@ const QuizForm: React.FC = () => {
|
||||||
onClose={() => setDialogOpen(false)} >
|
onClose={() => setDialogOpen(false)} >
|
||||||
<DialogTitle>Erreur</DialogTitle>
|
<DialogTitle>Erreur</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
Veuillez d'abord choisir une image à téléverser.
|
Veuillez d'abord choisir une image à téléverser.
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setDialogOpen(false)} color="primary">
|
<Button onClick={() => setDialogOpen(false)} color="primary">
|
||||||
|
|
@ -312,32 +290,8 @@ const QuizForm: React.FC = () => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showScrollButton && (
|
|
||||||
<Button
|
|
||||||
onClick={scrollToTop}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
style={scrollToTopButtonStyle}
|
|
||||||
title="Scroll to top"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToTopButtonStyle: CSSProperties = {
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '40px',
|
|
||||||
right: '50px',
|
|
||||||
padding: '10px',
|
|
||||||
fontSize: '16px',
|
|
||||||
color: 'white',
|
|
||||||
backgroundColor: '#5271ff',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
zIndex: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QuizForm;
|
export default QuizForm;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import './Login.css';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
|
|
@ -28,7 +28,7 @@ const Login: React.FC = () => {
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
const result = await ApiService.login(email, password);
|
const result = await ApiService.login(email, password);
|
||||||
|
|
||||||
if (typeof result === "string") {
|
if (result != true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ const Login: React.FC = () => {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Adresse courriel"
|
placeholder="Nom d'utilisateur"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
@ -60,7 +60,7 @@ const Login: React.FC = () => {
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Mot de passe"
|
placeholder="Nom de la salle"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { GIFTQuestion, parse } from 'gift-pegjs';
|
import { GIFTQuestion, parse } from 'gift-pegjs';
|
||||||
import { QuestionType } from '../../../Types/QuestionType';
|
import { QuestionType } from '../../../Types/QuestionType';
|
||||||
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
|
import LiveResultsComponent from '../../../components/LiveResults/LiveResults';
|
||||||
// import { QuestionService } from '../../../services/QuestionService';
|
// import { QuestionService } from '../../../services/QuestionService';
|
||||||
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
|
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
|
|
||||||
import './manageRoom.css';
|
import './manageRoom.css';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
//import { ENV_VARIABLES } from '../../../constants';
|
||||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
|
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||||
import { Refresh, Error } from '@mui/icons-material';
|
import { Refresh, Error } from '@mui/icons-material';
|
||||||
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
|
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
|
||||||
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
|
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
|
||||||
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
|
import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation';
|
||||||
import Question from 'src/components/Questions/Question';
|
import Question from '../../../components/Questions/Question';
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
const ManageRoom: React.FC = () => {
|
const ManageRoom: React.FC = () => {
|
||||||
|
|
@ -49,7 +49,6 @@ const ManageRoom: React.FC = () => {
|
||||||
setQuiz(quiz as QuizType);
|
setQuiz(quiz as QuizType);
|
||||||
|
|
||||||
if (!socket) {
|
if (!socket) {
|
||||||
console.log(`no socket in ManageRoom, creating one.`);
|
|
||||||
createWebSocketRoom();
|
createWebSocketRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,17 +79,22 @@ const ManageRoom: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWebSocketRoom = () => {
|
const createWebSocketRoom = async () => {
|
||||||
console.log('Creating WebSocket room...');
|
|
||||||
setConnectingError('');
|
setConnectingError('');
|
||||||
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
|
const room = await ApiService.createRoom();
|
||||||
|
const socket = webSocketService.connect(`/api/room/${room.id}/socket`);
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
webSocketService.createRoom();
|
webSocketService.createRoom(room.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
console.error("WebSocket server error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('connect_error', (error) => {
|
socket.on('connect_error', (error) => {
|
||||||
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
|
||||||
console.error('ManageRoom: WebSocket connection error:', error);
|
console.error('WebSocket connection error:', error);
|
||||||
});
|
});
|
||||||
socket.on('create-success', (roomName: string) => {
|
socket.on('create-success', (roomName: string) => {
|
||||||
setRoomName(roomName);
|
setRoomName(roomName);
|
||||||
|
|
@ -124,8 +128,8 @@ const ManageRoom: React.FC = () => {
|
||||||
// This is here to make sure the correct value is sent when user join
|
// This is here to make sure the correct value is sent when user join
|
||||||
if (socket) {
|
if (socket) {
|
||||||
console.log(`Listening for user-joined in room ${roomName}`);
|
console.log(`Listening for user-joined in room ${roomName}`);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
socket.on('user-joined', (_student: StudentType) => {
|
socket.on('user-joined', (_student: StudentType) => {
|
||||||
|
|
||||||
if (quizMode === 'teacher') {
|
if (quizMode === 'teacher') {
|
||||||
webSocketService.nextQuestion(roomName, currentQuestion);
|
webSocketService.nextQuestion(roomName, currentQuestion);
|
||||||
} else if (quizMode === 'student') {
|
} else if (quizMode === 'student') {
|
||||||
|
|
@ -144,7 +148,7 @@ const ManageRoom: React.FC = () => {
|
||||||
console.log('Quiz questions not found (cannot update answers without them).');
|
console.log('Quiz questions not found (cannot update answers without them).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the students state using the functional form of setStudents
|
// Update the students state using the functional form of setStudents
|
||||||
setStudents((prevStudents) => {
|
setStudents((prevStudents) => {
|
||||||
// print the list of current student names
|
// print the list of current student names
|
||||||
|
|
@ -152,7 +156,7 @@ const ManageRoom: React.FC = () => {
|
||||||
prevStudents.forEach((student) => {
|
prevStudents.forEach((student) => {
|
||||||
console.log(student.name);
|
console.log(student.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
let foundStudent = false;
|
let foundStudent = false;
|
||||||
const updatedStudents = prevStudents.map((student) => {
|
const updatedStudents = prevStudents.map((student) => {
|
||||||
console.log(`Comparing ${student.id} to ${idUser}`);
|
console.log(`Comparing ${student.id} to ${idUser}`);
|
||||||
|
|
@ -172,7 +176,7 @@ const ManageRoom: React.FC = () => {
|
||||||
updatedAnswers = [...student.answers, newAnswer];
|
updatedAnswers = [...student.answers, newAnswer];
|
||||||
}
|
}
|
||||||
return { ...student, answers: updatedAnswers };
|
return { ...student, answers: updatedAnswers };
|
||||||
}
|
}
|
||||||
return student;
|
return student;
|
||||||
});
|
});
|
||||||
if (!foundStudent) {
|
if (!foundStudent) {
|
||||||
|
|
@ -267,6 +271,7 @@ const ManageRoom: React.FC = () => {
|
||||||
const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1
|
const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1
|
||||||
|
|
||||||
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
|
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
|
||||||
|
|
||||||
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
|
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
|
||||||
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
|
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
|
||||||
};
|
};
|
||||||
|
|
@ -460,12 +465,12 @@ const ManageRoom: React.FC = () => {
|
||||||
{quizMode === 'teacher' && (
|
{quizMode === 'teacher' && (
|
||||||
|
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* <QuestionNavigation
|
<QuestionNavigation
|
||||||
currentQuestionId={Number(currentQuestion?.question.id)}
|
currentQuestionId={Number(currentQuestion?.question.id)}
|
||||||
questionsLength={quizQuestions?.length}
|
questionsLength={quizQuestions?.length}
|
||||||
previousQuestion={previousQuestion}
|
previousQuestion={previousQuestion}
|
||||||
nextQuestion={nextQuestion}
|
nextQuestion={nextQuestion}
|
||||||
/> */}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
@ -492,23 +497,12 @@ const ManageRoom: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{quizMode === 'teacher' && (
|
{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">
|
<div className="nextQuestionButton">
|
||||||
<Button onClick={nextQuestion}
|
<Button onClick={nextQuestion} variant="contained">
|
||||||
variant="contained"
|
|
||||||
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
|
|
||||||
>
|
|
||||||
Prochaine question
|
Prochaine question
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div> )}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
const Register: React.FC = () => {
|
const Register: React.FC = () => {
|
||||||
|
|
@ -28,7 +28,7 @@ const Register: React.FC = () => {
|
||||||
const register = async () => {
|
const register = async () => {
|
||||||
const result = await ApiService.register(email, password);
|
const result = await ApiService.register(email, password);
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
if (result != true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ const Register: React.FC = () => {
|
||||||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||||
disabled={!email || !password}
|
disabled={!email || !password}
|
||||||
>
|
>
|
||||||
S'inscrire
|
S'inscrire
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
||||||
</LoginContainer>
|
</LoginContainer>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
const ResetPassword: React.FC = () => {
|
const ResetPassword: React.FC = () => {
|
||||||
|
|
@ -27,7 +27,7 @@ const ResetPassword: React.FC = () => {
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
const result = await ApiService.resetPassword(email);
|
const result = await ApiService.resetPassword(email);
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
if (result != true) {
|
||||||
setConnectionError(result);
|
setConnectionError(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { FolderType } from '../../../Types/FolderType';
|
||||||
|
|
||||||
import './share.css';
|
import './share.css';
|
||||||
import { Button, NativeSelect } from '@mui/material';
|
import { Button, NativeSelect } from '@mui/material';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { ENV_VARIABLES } from '../constants';
|
||||||
|
|
||||||
import { FolderType } from 'src/Types/FolderType';
|
import { QuizType } from '../Types/QuizType';
|
||||||
import { QuizType } from 'src/Types/QuizType';
|
import { FolderType } from '../Types/FolderType';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
|
||||||
|
|
||||||
type ApiResponse = boolean | string;
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
private BASE_URL: string;
|
private BASE_URL: string;
|
||||||
|
|
@ -19,7 +17,7 @@ class ApiService {
|
||||||
return `${this.BASE_URL}/api${endpoint}`;
|
return `${this.BASE_URL}/api${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructRequestHeaders() {
|
private constructRequestHeaders(): any {
|
||||||
if (this.isLoggedIn()) {
|
if (this.isLoggedIn()) {
|
||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${this.getToken()}`,
|
Authorization: `Bearer ${this.getToken()}`,
|
||||||
|
|
@ -82,13 +80,85 @@ class ApiService {
|
||||||
return localStorage.removeItem("jwt");
|
return localStorage.removeItem("jwt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Socket Route
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new room.
|
||||||
|
* @returns The room object if successful
|
||||||
|
* @returns An error string if unsuccessful
|
||||||
|
*/
|
||||||
|
public async createRoom(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const url: string = this.constructRequestUrl(`/room`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`La création de la salle a échoué. Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await response.json();
|
||||||
|
return room;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a room by its name.
|
||||||
|
* @param roomName - The name of the room to delete.
|
||||||
|
* @returns true if successful
|
||||||
|
* @returns An error string if unsuccessful
|
||||||
|
*/
|
||||||
|
public async deleteRoom(roomName: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
if (!roomName) {
|
||||||
|
throw new Error(`Le nom de la salle est requis.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.constructRequestUrl(`/room/${roomName}`);
|
||||||
|
const headers = this.constructRequestHeaders();
|
||||||
|
fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message || 'Erreur serveur inconnue lors de la requête.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Une erreur inattendue s'est produite.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// User Routes
|
// User Routes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async register(email: string, password: string): Promise<ApiResponse> {
|
public async register(email: string, password: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
|
|
@ -124,7 +194,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async login(email: string, password: string): Promise<ApiResponse> {
|
public async login(email: string, password: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
|
|
@ -148,13 +218,8 @@ class ApiService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error details: ", error);
|
console.log("Error details: ", error);
|
||||||
|
|
||||||
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
|
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const err = error as AxiosError;
|
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;
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
return data?.error || 'Erreur serveur inconnue lors de la requête.';
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +229,10 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async resetPassword(email: string): Promise<ApiResponse> {
|
public async resetPassword(email: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
|
|
@ -203,7 +268,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<ApiResponse> {
|
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email || !oldPassword || !newPassword) {
|
if (!email || !oldPassword || !newPassword) {
|
||||||
|
|
@ -239,7 +304,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async deleteUser(email: string, password: string): Promise<ApiResponse> {
|
public async deleteUser(email: string, password: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
|
|
@ -277,7 +342,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async createFolder(title: string): Promise<ApiResponse> {
|
public async createFolder(title: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
|
|
@ -309,6 +374,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns folder array if successful
|
* @returns folder array if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
|
|
@ -382,7 +448,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async deleteFolder(folderId: string): Promise<ApiResponse> {
|
public async deleteFolder(folderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!folderId) {
|
if (!folderId) {
|
||||||
|
|
@ -417,7 +483,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
|
public async renameFolder(folderId: string, newTitle: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!folderId || !newTitle) {
|
if (!folderId || !newTitle) {
|
||||||
|
|
@ -448,7 +514,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async duplicateFolder(folderId: string): Promise<ApiResponse> {
|
public async duplicateFolder(folderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
if (!folderId) {
|
if (!folderId) {
|
||||||
throw new Error(`Le folderId et le nouveau titre sont requis.`);
|
throw new Error(`Le folderId et le nouveau titre sont requis.`);
|
||||||
|
|
@ -480,7 +546,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async copyFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
|
public async copyFolder(folderId: string, newTitle: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
if (!folderId || !newTitle) {
|
if (!folderId || !newTitle) {
|
||||||
throw new Error(`Le folderId et le nouveau titre sont requis.`);
|
throw new Error(`Le folderId et le nouveau titre sont requis.`);
|
||||||
|
|
@ -517,7 +583,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async createQuiz(title: string, content: string[], folderId: string): Promise<ApiResponse> {
|
public async createQuiz(title: string, content: string[], folderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!title || !content || !folderId) {
|
if (!title || !content || !folderId) {
|
||||||
|
|
@ -588,7 +654,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async deleteQuiz(quizId: string): Promise<ApiResponse> {
|
public async deleteQuiz(quizId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!quizId) {
|
if (!quizId) {
|
||||||
|
|
@ -623,7 +689,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<ApiResponse> {
|
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!quizId || !newTitle || !newContent) {
|
if (!quizId || !newTitle || !newContent) {
|
||||||
|
|
@ -659,7 +725,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async moveQuiz(quizId: string, newFolderId: string): Promise<ApiResponse> {
|
public async moveQuiz(quizId: string, newFolderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!quizId || !newFolderId) {
|
if (!quizId || !newFolderId) {
|
||||||
|
|
@ -696,7 +762,7 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async duplicateQuiz(quizId: string): Promise<ApiResponse> {
|
public async duplicateQuiz(quizId: string): Promise<any> {
|
||||||
|
|
||||||
|
|
||||||
const url: string = this.constructRequestUrl(`/quiz/duplicate`);
|
const url: string = this.constructRequestUrl(`/quiz/duplicate`);
|
||||||
|
|
@ -710,7 +776,7 @@ class ApiService {
|
||||||
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
|
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.status === 200;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error details: ", error);
|
console.error("Error details: ", error);
|
||||||
|
|
||||||
|
|
@ -730,9 +796,9 @@ class ApiService {
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
* @returns A error string if unsuccessful,
|
* @returns A error string if unsuccessful,
|
||||||
*/
|
*/
|
||||||
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<ApiResponse> {
|
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
console.log(quizId, newTitle, folderId);
|
console.log(quizId, newTitle), folderId;
|
||||||
return "Route not implemented yet!";
|
return "Route not implemented yet!";
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -748,7 +814,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
|
async ShareQuiz(quizId: string, email: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
if (!quizId || !email) {
|
if (!quizId || !email) {
|
||||||
throw new Error(`quizId and email are required.`);
|
throw new Error(`quizId and email are required.`);
|
||||||
|
|
@ -807,7 +873,7 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveSharedQuiz(quizId: string, folderId: string): Promise<ApiResponse> {
|
async receiveSharedQuiz(quizId: string, folderId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
if (!quizId || !folderId) {
|
if (!quizId || !folderId) {
|
||||||
throw new Error(`quizId and folderId are required.`);
|
throw new Error(`quizId and folderId are required.`);
|
||||||
|
|
@ -876,8 +942,7 @@ class ApiService {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const err = error as AxiosError;
|
const err = error as AxiosError;
|
||||||
const data = err.response?.data as { error: string } | undefined;
|
const data = err.response?.data as { error: string } | undefined;
|
||||||
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
|
return `ERROR : ${data?.error}` || 'ERROR : Erreur serveur inconnue lors de la requête.';
|
||||||
return `ERROR : ${msg}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `ERROR : Une erreur inattendue s'est produite.`
|
return `ERROR : Une erreur inattendue s'est produite.`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// WebSocketService.tsx
|
// WebSocketService.tsx
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import apiService from './ApiService';
|
||||||
|
|
||||||
// Must (manually) sync these types to server/socket/socket.js
|
// Must (manually) sync these types to server/socket/socket.js
|
||||||
|
|
||||||
|
|
@ -21,23 +22,18 @@ class WebSocketService {
|
||||||
private socket: Socket | null = null;
|
private socket: Socket | null = null;
|
||||||
|
|
||||||
connect(backendUrl: string): Socket {
|
connect(backendUrl: string): Socket {
|
||||||
console.log(`WebSocketService.connect('${backendUrl}')`);
|
this.socket = io( '/',{
|
||||||
|
path: backendUrl,
|
||||||
// // Ensure the URL uses wss: if the URL starts with https:
|
|
||||||
// const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:';
|
|
||||||
// console.log(`WebSocketService.connect: protocol=${protocol}`);
|
|
||||||
// const url = backendUrl.replace(/^http(s):/, protocol);
|
|
||||||
// console.log(`WebSocketService.connect: changed url=${url}`);
|
|
||||||
const url = backendUrl || window.location.host;
|
|
||||||
|
|
||||||
this.socket = io(url, {
|
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
reconnectionAttempts: 1
|
autoConnect: true,
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 10,
|
||||||
|
reconnectionDelay: 10000,
|
||||||
|
timeout: 20000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.socket;
|
return this.socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
|
|
@ -46,9 +42,9 @@ class WebSocketService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoom() {
|
createRoom(roomName: string) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('create-room');
|
this.socket.emit('create-room', roomName || undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +63,8 @@ class WebSocketService {
|
||||||
endQuiz(roomName: string) {
|
endQuiz(roomName: string) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.emit('end-quiz', { roomName });
|
this.socket.emit('end-quiz', { roomName });
|
||||||
|
//Delete room in mongoDb, roomContainer will be deleted in cleanup
|
||||||
|
apiService.deleteRoom(roomName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,15 +81,15 @@ class WebSocketService {
|
||||||
// idQuestion: string
|
// idQuestion: string
|
||||||
) {
|
) {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket?.emit('submit-answer',
|
this.socket?.emit('submit-answer',
|
||||||
// {
|
// {
|
||||||
// answer: answer,
|
// answer: answer,
|
||||||
// roomName: roomName,
|
// roomName: roomName,
|
||||||
// username: username,
|
// username: username,
|
||||||
// idQuestion: idQuestion
|
// idQuestion: idQuestion
|
||||||
// }
|
// }
|
||||||
answerData
|
answerData
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export function escapeForGIFT(link: string): string {
|
export function escapeForGIFT(link: string): string {
|
||||||
const specialChars = /[{}#~=<>\\:]/g;
|
const specialChars = /[{}#~=<>\:]/g;
|
||||||
return link.replace(specialChars, (match) => `\\${match}`);
|
return link.replace(specialChars, (match) => `\\${match}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"src/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ES2020",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|
@ -16,7 +12,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
||||||
|
|
@ -3,44 +3,22 @@ import react from '@vitejs/plugin-react-swc';
|
||||||
import pluginChecker from 'vite-plugin-checker';
|
import pluginChecker from 'vite-plugin-checker';
|
||||||
import EnvironmentPlugin from 'vite-plugin-environment';
|
import EnvironmentPlugin from 'vite-plugin-environment';
|
||||||
|
|
||||||
// Filter out environment variables with invalid identifiers
|
|
||||||
const filteredEnv = Object.keys(process.env).reduce((acc, key) => {
|
|
||||||
// Only include environment variables with valid JavaScript identifiers
|
|
||||||
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
||||||
acc[key] = process.env[key];
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/",
|
base: "/",
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
pluginChecker({ typescript: true }),
|
pluginChecker({ typescript: true }),
|
||||||
EnvironmentPlugin(filteredEnv),
|
EnvironmentPlugin('all'),
|
||||||
],
|
],
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'src': '/src'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preview: {
|
preview: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true
|
strictPort: true
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
host: true,
|
host: true,
|
||||||
origin: "http://0.0.0.0:5173",
|
origin: "http://0.0.0.0:5173",
|
||||||
},
|
|
||||||
build: {
|
|
||||||
sourcemap: true, // Enable source maps
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
sourcemapExcludeSources: true, // Exclude sources from source maps
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
74
create-branch-image.bat
Normal file
74
create-branch-image.bat
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
@echo off
|
||||||
|
setlocal EnableDelayedExpansion
|
||||||
|
|
||||||
|
:: Check if gh is installed
|
||||||
|
where gh >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo GitHub CLI not found. Installing...
|
||||||
|
winget install --id GitHub.cli
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Failed to install GitHub CLI. Exiting...
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo GitHub CLI installed successfully.
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Check if user is authenticated
|
||||||
|
gh auth status >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo GitHub CLI not authenticated. Please authenticate...
|
||||||
|
gh auth login
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Failed to authenticate. Exiting...
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Authentication successful.
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Get the current branch name
|
||||||
|
for /f "tokens=*" %%i in ('git rev-parse --abbrev-ref HEAD') do set BRANCH_NAME=%%i
|
||||||
|
|
||||||
|
:: Run the GitHub workflow with the current branch name
|
||||||
|
echo Running GitHub workflow with branch %BRANCH_NAME%...
|
||||||
|
gh workflow run 119194149 --ref %BRANCH_NAME%
|
||||||
|
|
||||||
|
:: Wait and validate workflow launch
|
||||||
|
set /a attempts=0
|
||||||
|
set /a max_attempts=12
|
||||||
|
echo Waiting for workflow to start...
|
||||||
|
|
||||||
|
:wait_for_workflow
|
||||||
|
timeout /t 15 >nul
|
||||||
|
set /a attempts+=1
|
||||||
|
|
||||||
|
:: Get recent workflow run matching our criteria with in_progress status
|
||||||
|
for /f "tokens=*" %%i in ('gh run list --branch %BRANCH_NAME% --status in_progress --limit 1 --json databaseId --jq ".[0].databaseId"') do set WORKFLOW_RUN_ID=%%i
|
||||||
|
|
||||||
|
if "%WORKFLOW_RUN_ID%"=="" (
|
||||||
|
if !attempts! lss !max_attempts! (
|
||||||
|
echo Attempt !attempts! of !max_attempts!: No running workflow found yet...
|
||||||
|
goto wait_for_workflow
|
||||||
|
) else (
|
||||||
|
echo Timeout waiting for workflow to start running.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Found running workflow ID: %WORKFLOW_RUN_ID%
|
||||||
|
|
||||||
|
:monitor_progress
|
||||||
|
cls
|
||||||
|
echo Workflow Progress:
|
||||||
|
echo ----------------
|
||||||
|
gh run view %WORKFLOW_RUN_ID% --json jobs --jq ".jobs[] | \"Job: \" + .name + \" - Status: \" + .status + if .conclusion != null then \" (\" + .conclusion + \")\" else \"\" end"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Check if workflow is still running
|
||||||
|
for /f "tokens=*" %%i in ('gh run view %WORKFLOW_RUN_ID% --json status --jq ".status"') do set CURRENT_STATUS=%%i
|
||||||
|
if "%CURRENT_STATUS%" == "completed" (
|
||||||
|
echo Workflow completed.
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout /t 5 >nul
|
||||||
|
goto monitor_progress
|
||||||
137
docker-compose.local.yaml
Normal file
137
docker-compose.local.yaml
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
container_name: frontend
|
||||||
|
build:
|
||||||
|
context: ./client
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT} || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 6
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: backend
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
|
||||||
|
MONGO_DATABASE: evaluetonsavoir
|
||||||
|
EMAIL_SERVICE: gmail
|
||||||
|
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
|
||||||
|
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
|
||||||
|
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT}/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 6
|
||||||
|
|
||||||
|
quizroom: # Forces image to update
|
||||||
|
build:
|
||||||
|
context: ./quizRoom
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: quizroom
|
||||||
|
ports:
|
||||||
|
- "4500:4500"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/usr/src/app/healthcheck.sh"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 6
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: nginx
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
frontend:
|
||||||
|
condition: service_healthy
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
#environment:
|
||||||
|
# - PORT=8000
|
||||||
|
# - FRONTEND_HOST=frontend
|
||||||
|
# - FRONTEND_PORT=5173
|
||||||
|
# - BACKEND_HOST=backend
|
||||||
|
# - BACKEND_PORT=3000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --spider http://0.0.0.0:$${PORT}/health || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 5s
|
||||||
|
retries: 6
|
||||||
|
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
container_name: mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
- TZ=America/Montreal
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_DEBUG=true
|
||||||
|
- WATCHTOWER_INCLUDE_RESTARTING=true
|
||||||
|
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
|
||||||
|
networks:
|
||||||
|
- quiz_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
quiz_network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
external: false
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
services:
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: fuhrmanator/evaluetonsavoir-frontend:latest
|
image: fuhrmanator/evaluetonsavoir-frontend:latest
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
environment:
|
|
||||||
# Define empty VITE_BACKEND_URL because it's production
|
|
||||||
- VITE_BACKEND_URL=
|
|
||||||
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
|
|
||||||
- VITE_BACKEND_SOCKET_URL=
|
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
@ -30,6 +27,17 @@ services:
|
||||||
- mongo
|
- mongo
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
quizroom:
|
||||||
|
build:
|
||||||
|
context: ./quizRoom
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: quizroom
|
||||||
|
ports:
|
||||||
|
- "4500:4500"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: always
|
||||||
|
|
||||||
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
|
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
|
||||||
nginx:
|
nginx:
|
||||||
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
image: fuhrmanator/evaluetonsavoir-routeur:latest
|
||||||
|
|
@ -52,7 +60,7 @@ services:
|
||||||
- mongodb_data:/data/db
|
- mongodb_data:/data/db
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
# Ce conteneur cherche des mises à jour à 5h du matin
|
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
|
||||||
watchtower:
|
watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
container_name: watchtower
|
container_name: watchtower
|
||||||
|
|
@ -66,19 +74,6 @@ services:
|
||||||
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
|
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
watchtower-once:
|
|
||||||
image: containrrr/watchtower
|
|
||||||
container_name: watchtower-once
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
command: --run-once
|
|
||||||
environment:
|
|
||||||
- TZ=America/Montreal
|
|
||||||
- WATCHTOWER_CLEANUP=true
|
|
||||||
- WATCHTOWER_DEBUG=true
|
|
||||||
- WATCHTOWER_INCLUDE_RESTARTING=true
|
|
||||||
restart: "no"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongodb_data:
|
mongodb_data:
|
||||||
external: false
|
external: false
|
||||||
|
|
|
||||||
1
documentation/.gitignore
vendored
Normal file
1
documentation/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
site
|
||||||
2
documentation/deploy.py
Normal file
2
documentation/deploy.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from ghp_import import ghp_import
|
||||||
|
ghp_import('site', push=True, force=True)
|
||||||
12
documentation/docs/developpeur/backend/api.md
Normal file
12
documentation/docs/developpeur/backend/api.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## À Propos
|
||||||
|
|
||||||
|
Ce projet utilise Node.js Express pour créer un backend simple pour l'application.
|
||||||
|
|
||||||
|
## Routes API
|
||||||
|
|
||||||
|
Vous pouvez consulter toutes les routes utilisables du backend ici
|
||||||
|
|
||||||
|
* User : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#e942a4f4-321c-465b-bf88-e6c1f1d6f6c8
|
||||||
|
* Quiz : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#732d980b-02fd-4807-b5bc-72725098b9b0
|
||||||
|
* Folders : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#49ecd432-ccfc-4c8a-8390-b3962f0d5fd7
|
||||||
|
* Images : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#58382180-d6f0-492d-80c3-e09de1c368b8
|
||||||
384
documentation/docs/developpeur/backend/auth.md
Normal file
384
documentation/docs/developpeur/backend/auth.md
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
# Authentification
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Le but du module d'authentification est de pouvoir facilement faire des blocs de code permettant une authentification
|
||||||
|
personalisée. Il est possible de le faire grâce à cette architecture. Pour la première version de cette fonctionalité,
|
||||||
|
l'introduction de OIDC et de OAuth sont priorisé ainsi que la migration du module d'authentification simple.
|
||||||
|
|
||||||
|
|
||||||
|
## Déconstruction simple de la structure
|
||||||
|
La structure est la suivante :
|
||||||
|
|
||||||
|
Le AuthManager s'occupe de centraliser les requêtes d'authentification. Ce dernier initialise les autres modules et est
|
||||||
|
la source de vérité dans les aspects liés à l'authentification. Les modules sont automatiquement chargés par
|
||||||
|
l'utilisation de variables d'environment.
|
||||||
|
|
||||||
|
Le module s'occupe de créer les routes nécessaires pour son fonctionnement et de créer les utilisateurs. Ces modules
|
||||||
|
vont appeller le AuthManager afin de confirmer leurs actions avec le login/register de celui-ci.
|
||||||
|
|
||||||
|
Dans le cas de modules plus complexe, tels que le module Passport, la chaine peut être prolongée afin de maintenir
|
||||||
|
les actions centralisée . Chaque connecteur de PassportJs est initialisé par le module de PassportJs.
|
||||||
|
|
||||||
|
|
||||||
|
## Besoins exprimés
|
||||||
|
|
||||||
|
|
||||||
|
Modularité et généricité :
|
||||||
|
|
||||||
|
- Le système d'authentification doit être adaptable à diverses configurations, notamment pour répondre aux exigences
|
||||||
|
spécifiques des différentes universités ou institutions.
|
||||||
|
|
||||||
|
Utilisation de différentes méthodes d'authentification :
|
||||||
|
|
||||||
|
- L'application doit permettre de gérer plusieurs fournisseurs d'authentification (SSO, LDAP, OAuth, etc.) de manière
|
||||||
|
centralisée et flexible.
|
||||||
|
|
||||||
|
Facilité de configuration :
|
||||||
|
|
||||||
|
- Le système doit permettre une configuration simple et flexible, adaptée à différents environnements (développement,
|
||||||
|
production, etc.).
|
||||||
|
|
||||||
|
Gestion des permissions :
|
||||||
|
|
||||||
|
- Il doit être possible de définir et de mapper facilement les permissions et les rôles des utilisateurs pour sécuriser
|
||||||
|
l’accès aux différentes fonctionnalités de l’application.
|
||||||
|
|
||||||
|
Maintien de la connexion :
|
||||||
|
|
||||||
|
- Le système doit garantir la persistance de la connexion pendant toute la durée de l'utilisation de l'application
|
||||||
|
(exemple : quiz), avec la possibilité de se reconnecter sans perte de données en cas de déconnexion temporaire.
|
||||||
|
|
||||||
|
## Recits utilisateurs pris en comptes
|
||||||
|
|
||||||
|
- En tant qu'utilisateur de projet FOSS, je veux que le module d'authentification soit modulaire et générique afin de
|
||||||
|
l'adapter à mes besoins.
|
||||||
|
- En tant qu'administrateur, je veux que les droits des utilisateurs soient inférés par l'authentificateur de l'établissement.
|
||||||
|
- En tant qu'administrateur, je veux que la configuration des authentificateurs soit simple
|
||||||
|
- En tant qu'administrateur, je veux configurer les connexions à partir de variables d'environnement ou fichier de config.
|
||||||
|
- En tant qu'utilisateur, je veux que ma connexion soit stable.
|
||||||
|
- En tant qu'utilisateur, je veux pouvoir me reconnecter à une salle s'il survient un problème de connexion.
|
||||||
|
|
||||||
|
## Diagrammes
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
package Backend {
|
||||||
|
class AuthManager{
|
||||||
|
+IAuthModule[] auths
|
||||||
|
#userInfos
|
||||||
|
|
||||||
|
-load()
|
||||||
|
-registerAuths()
|
||||||
|
+showAuths()
|
||||||
|
|
||||||
|
+authStatus()
|
||||||
|
+logIn(UserInfos)
|
||||||
|
+register(UserInfos)
|
||||||
|
+logOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAuthModule{
|
||||||
|
+registerAuth()
|
||||||
|
+authenticate()
|
||||||
|
+register()
|
||||||
|
+showAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleFormAuthModule{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class PassportAuthModule{
|
||||||
|
IPassportProviderDefinition[] providers
|
||||||
|
}
|
||||||
|
|
||||||
|
Interface IPassportProviderDefinition{
|
||||||
|
+name
|
||||||
|
+type
|
||||||
|
}
|
||||||
|
|
||||||
|
class OAuthPassportProvider{
|
||||||
|
+clientId
|
||||||
|
+clientSecret
|
||||||
|
+configUrl
|
||||||
|
+authorizeUrl
|
||||||
|
+tokenUrl
|
||||||
|
+userinfoUrl
|
||||||
|
+logoutUrl
|
||||||
|
+JWKSUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
IAuthModule <|-- SimpleFormAuthModule
|
||||||
|
IAuthModule <|-- PassportAuthModule
|
||||||
|
IPassportProviderDefinition <|-- OAuthPassportProvider
|
||||||
|
|
||||||
|
AuthManager -> IAuthModule
|
||||||
|
PassportAuthModule -> IPassportProviderDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
package Frontend{
|
||||||
|
class AuthDrawer{
|
||||||
|
+IAuthVisual[] getAuthsVisual()
|
||||||
|
+drawAuths()
|
||||||
|
}
|
||||||
|
|
||||||
|
Interface IAuthVisual{
|
||||||
|
+draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
class FormVisual{
|
||||||
|
+FormInput[] formInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormInput{
|
||||||
|
+name
|
||||||
|
+label
|
||||||
|
+type
|
||||||
|
+value
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthDrawer -> IAuthVisual
|
||||||
|
IAuthVisual <|-- FormVisual
|
||||||
|
FormVisual -> FormInput
|
||||||
|
}
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Explication des communications : Passport Js
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
box "Frontend"
|
||||||
|
participant User
|
||||||
|
Participant App
|
||||||
|
end box
|
||||||
|
|
||||||
|
box "Backend"
|
||||||
|
participant PassportAuthModule
|
||||||
|
participant Db
|
||||||
|
participant AuthManager
|
||||||
|
end box
|
||||||
|
|
||||||
|
box "Auth Server"
|
||||||
|
participant AuthServer
|
||||||
|
end box
|
||||||
|
|
||||||
|
User -> App : Get auth page
|
||||||
|
App -> User : auth page
|
||||||
|
|
||||||
|
User -> App : click OAuth button
|
||||||
|
App -> User : redirect to OAuth
|
||||||
|
|
||||||
|
User -> AuthServer: Login
|
||||||
|
AuthServer -> User: Redirect to Auth endpoint with token
|
||||||
|
|
||||||
|
User -> PassportAuthModule: Authenticate with token
|
||||||
|
|
||||||
|
PassportAuthModule -> AuthServer: get user info
|
||||||
|
AuthServer -> PassportAuthModule: userInfo
|
||||||
|
|
||||||
|
alt login
|
||||||
|
PassportAuthModule -> Db : fetch local userInfo
|
||||||
|
Db->PassportAuthModule: userInfo
|
||||||
|
PassportAuthModule -> PassportAuthModule: Merge userInfo definition
|
||||||
|
PassportAuthModule -> Db : update user profile
|
||||||
|
Db->PassportAuthModule: userInfo
|
||||||
|
end
|
||||||
|
|
||||||
|
alt register
|
||||||
|
PassportAuthModule -> Db : fetch local userInfo
|
||||||
|
Db->PassportAuthModule: null
|
||||||
|
PassportAuthModule -> Db : create user profile
|
||||||
|
Db->PassportAuthModule: userInfo
|
||||||
|
end
|
||||||
|
|
||||||
|
PassportAuthModule -> AuthManager : login(userInfos)
|
||||||
|
|
||||||
|
AuthManager -> User: Give refresh token + Redirect to page
|
||||||
|
User -> App: get /
|
||||||
|
App -> User: Show Authenticated /
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Explication des communications : SimpleAuth
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
box "Frontend"
|
||||||
|
participant User
|
||||||
|
Participant App
|
||||||
|
end box
|
||||||
|
|
||||||
|
box "Backend"
|
||||||
|
participant SimpleAuthModule
|
||||||
|
participant Db
|
||||||
|
participant AuthManager
|
||||||
|
end box
|
||||||
|
|
||||||
|
User -> App : Get auth page
|
||||||
|
App -> User : auth page
|
||||||
|
|
||||||
|
|
||||||
|
alt Login
|
||||||
|
User -> App : Send Login/Pass
|
||||||
|
|
||||||
|
App -> SimpleAuthModule: Send login/pass
|
||||||
|
|
||||||
|
SimpleAuthModule -> Db: get user info
|
||||||
|
Db->SimpleAuthModule: user info
|
||||||
|
SimpleAuthModule -> SimpleAuthModule: Validate Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
alt register
|
||||||
|
User -> App : Send Username + Password + Email
|
||||||
|
|
||||||
|
App -> SimpleAuthModule: Send Username + Password + Email
|
||||||
|
|
||||||
|
SimpleAuthModule -> Db: get user info
|
||||||
|
Db -> SimpleAuthModule : null
|
||||||
|
|
||||||
|
SimpleAuthModule -> Db: put user info
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleAuthModule -> AuthManager: userInfo
|
||||||
|
AuthManager -> User: Give refresh token + Redirect to page
|
||||||
|
User -> App: get /
|
||||||
|
App -> User: Show Authenticated /
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comment les boutons sont affichés
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
|
||||||
|
box "FrontEnd"
|
||||||
|
participant User
|
||||||
|
Participant FrontEnd
|
||||||
|
Participant AuthDrawer
|
||||||
|
end box
|
||||||
|
|
||||||
|
box "BackEnd"
|
||||||
|
participant API
|
||||||
|
participant AuthManager
|
||||||
|
participant Db
|
||||||
|
participant IAuthModule
|
||||||
|
end box
|
||||||
|
|
||||||
|
API -> API : load global configurations
|
||||||
|
|
||||||
|
create AuthManager
|
||||||
|
API -> AuthManager : instanciate with auth configurations
|
||||||
|
|
||||||
|
|
||||||
|
create IAuthModule
|
||||||
|
AuthManager -> IAuthModule : instanciate array
|
||||||
|
|
||||||
|
loop For each auth in auths
|
||||||
|
AuthManager -> IAuthModule : register
|
||||||
|
IAuthModule -> API : register routes
|
||||||
|
API -> IAuthModule : route registration confirmation
|
||||||
|
IAuthModule -> AuthManager : module registration confirmation
|
||||||
|
end
|
||||||
|
|
||||||
|
User -> FrontEnd : get login page
|
||||||
|
|
||||||
|
alt already logged in
|
||||||
|
FrontEnd -> User: redirected to authenticated page
|
||||||
|
end
|
||||||
|
|
||||||
|
FrontEnd -> AuthDrawer : get auth visual
|
||||||
|
AuthDrawer -> API : get auth form data
|
||||||
|
|
||||||
|
API -> AuthManager : get auth form data
|
||||||
|
|
||||||
|
|
||||||
|
loop For each auth in auths
|
||||||
|
AuthManager -> IAuthModule : get form data
|
||||||
|
IAuthModule -> AuthManager : form data
|
||||||
|
end
|
||||||
|
|
||||||
|
AuthManager -> API : auth fom data
|
||||||
|
API -> AuthDrawer : auth form data
|
||||||
|
|
||||||
|
AuthDrawer -> AuthDrawer : make auth html
|
||||||
|
AuthDrawer -> FrontEnd : auth HTML
|
||||||
|
FrontEnd -> User : show auth page
|
||||||
|
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comment les sessions sont conservées
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
box "Frontend"
|
||||||
|
participant User
|
||||||
|
Participant App
|
||||||
|
end box
|
||||||
|
|
||||||
|
box "Backend"
|
||||||
|
participant AuthManager
|
||||||
|
participant IAuthModules
|
||||||
|
end box
|
||||||
|
|
||||||
|
App -> AuthManager : send refresh token
|
||||||
|
|
||||||
|
AuthManager -> IAuthModules: ForEach check if logged
|
||||||
|
IAuthModules -> AuthManager: is authenticated ?
|
||||||
|
|
||||||
|
alt one logged in
|
||||||
|
AuthManager -> App : send new token
|
||||||
|
end
|
||||||
|
|
||||||
|
alt all logged out
|
||||||
|
AuthManager -> App : send error
|
||||||
|
App -> App : destroy token
|
||||||
|
App -> User : redirect to login page
|
||||||
|
end
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration des variables d'environnement
|
||||||
|
|
||||||
|
Example de configuration du fichier : `server/auth_config.json` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"passportjs": // Module
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"gmatte": { // Nom du sous-module Passport
|
||||||
|
"type": "oauth", // type
|
||||||
|
"OAUTH_AUTHORIZATION_URL": "https://auth.gmatte.xyz/application/o/authorize/",
|
||||||
|
"OAUTH_TOKEN_URL": "https://auth.gmatte.xyz/application/o/token/",
|
||||||
|
"OAUTH_USERINFO_URL": "https://auth.gmatte.xyz/application/o/userinfo/",
|
||||||
|
"OAUTH_CLIENT_ID": "--redacted--",
|
||||||
|
"OAUTH_CLIENT_SECRET": "--Redacted--",
|
||||||
|
"OAUTH_ADD_SCOPE": "groups", // scopes supplémentaire nécessaire pour le pivot
|
||||||
|
"OAUTH_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof", // valeur de pivot afin de définir un enseignant
|
||||||
|
"OAUTH_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir" // valeur de pivot afin de définir un étudiant
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"etsmtl":{
|
||||||
|
"type":"oidc",
|
||||||
|
"OIDC_CONFIG_URL":"https://login.microsoftonline.com/70aae3b7-9f3b-484d-8f95-49e8fbb783c0/v2.0/.well-known/openid-configuration",
|
||||||
|
"OIDC_CLIENT_ID": "--redacted--",
|
||||||
|
"OIDC_CLIENT_SECRET": "--redacted--",
|
||||||
|
"OIDC_ADD_SCOPE": "",
|
||||||
|
"OIDC_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof",
|
||||||
|
"OIDC_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"simpleauth":{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue