mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
commit
e2c8a09494
192 changed files with 29125 additions and 3 deletions
26
.github/workflows/backend-deploy.yml
vendored
Normal file
26
.github/workflows/backend-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Backend
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_BACKEND_REPO }}:latest
|
||||||
26
.github/workflows/deploy.yml
vendored
Normal file
26
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Nginx Router
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_nginx:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Router
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_ROUTER_REPO }}:latest
|
||||||
26
.github/workflows/dev_backend-deploy.yml
vendored
Normal file
26
.github/workflows/dev_backend-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Backend
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./server
|
||||||
|
file: ./server/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/dev_${{ secrets.DOCKERHUB_BACKEND_REPO }}:latest
|
||||||
26
.github/workflows/dev_deploy.yml
vendored
Normal file
26
.github/workflows/dev_deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Nginx Router
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_nginx:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Router
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/dev_${{ secrets.DOCKERHUB_ROUTER_REPO }}:latest
|
||||||
26
.github/workflows/dev_frontend-deploy.yml
vendored
Normal file
26
.github/workflows/dev_frontend-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Frontend
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./client
|
||||||
|
file: ./client/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/dev_${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest
|
||||||
26
.github/workflows/frontend-deploy.yml
vendored
Normal file
26
.github/workflows/frontend-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: CI/CD Pipeline for Frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_push_frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check Out Repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker image for Frontend
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: ./client
|
||||||
|
file: ./client/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest
|
||||||
4
LICENSE
4
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 louis-antoine-etsmtl
|
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -18,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.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# EvalueTonSavoir
|
# EvalueTonSavoir
|
||||||
|
|
||||||
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle.
|
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle.
|
||||||
|
|
||||||
|
|
|
||||||
2
client/.dockerignore
Normal file
2
client/.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/node_modules
|
||||||
|
.env
|
||||||
2
client/.env.example
Normal file
2
client/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_BACKEND_URL=http://localhost:4400
|
||||||
|
VITE_AZURE_BACKEND_URL=http://localhost:4400
|
||||||
18
client/.eslintrc.cjs
Normal file
18
client/.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
7
client/.prettierrc.json
Normal file
7
client/.prettierrc.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
17
client/Dockerfile
Normal file
17
client/Dockerfile
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# client
|
||||||
|
|
||||||
|
FROM node:18 AS build
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/client
|
||||||
|
|
||||||
|
COPY ./package*.json ./
|
||||||
|
|
||||||
|
COPY ./ .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
CMD [ "npm", "run", "preview" ]
|
||||||
3
client/babel.config.cjs
Normal file
3
client/babel.config.cjs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
||||||
|
};
|
||||||
19
client/index.html
Normal file
19
client/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/katex@latest/dist/katex.min.css"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<title>Évalue Ton Savoir</title>
|
||||||
|
<base href="/" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
client/jest.config.cjs
Normal file
17
client/jest.config.cjs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
roots: ['<rootDir>/src'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||||
|
'^.+\\.(js|jsx)$': 'babel-jest'
|
||||||
|
},
|
||||||
|
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
setupFiles: ['./jest.setup.cjs'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: ['node_modules/(?!nanoid/)']
|
||||||
|
};
|
||||||
7
client/jest.setup.cjs
Normal file
7
client/jest.setup.cjs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
global.import = {
|
||||||
|
meta: {
|
||||||
|
env: {
|
||||||
|
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
11129
client/package-lock.json
generated
Normal file
11129
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
66
client/package.json
Normal file
66
client/package.json
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"name": "pfe004-evaluetonsavoir",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.3",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@mui/icons-material": "^5.14.18",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.153",
|
||||||
|
"@mui/material": "^5.15.11",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
|
"gift-pegjs": "^0.2.1",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"katex": "^0.16.9",
|
||||||
|
"marked": "^9.1.2",
|
||||||
|
"nanoid": "^5.0.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-latex": "^2.0.0",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
|
"react-router-dom": "^6.16.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
|
"socket.io-client": "^4.7.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vite-plugin-checker": "^0.6.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/preset-env": "^7.23.3",
|
||||||
|
"@babel/preset-react": "^7.23.3",
|
||||||
|
"@babel/preset-typescript": "^7.23.3",
|
||||||
|
"@testing-library/jest-dom": "^6.1.4",
|
||||||
|
"@testing-library/react": "^14.1.0",
|
||||||
|
"@types/jest": "^29.5.8",
|
||||||
|
"@types/node": "^20.8.8",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/react-latex": "^2.0.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.1",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-plugin-rewrite-all": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
client/public/Logo.svg
Normal file
6
client/public/Logo.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="244" height="225" fill="none" viewBox="0 0 244 225">
|
||||||
|
<path stroke="#636363" stroke-width="4" d="M79.06 48.494 122 6.787l88.131 85.601L122 177.99l-42.94-41.707c-24.958-24.242-24.958-63.547 0-87.79Z"/>
|
||||||
|
<path stroke="#636363" stroke-width="4" d="M123 32.777c34.318 0 62 26.713 62 59.5 0 32.786-27.682 59.5-62 59.5-34.317 0-62-26.714-62-59.5 0-32.787 27.683-59.5 62-59.5Z"/>
|
||||||
|
<path stroke="#636363" stroke-width="4" d="M74 128.777v-73h96v73H74Zm53 1v-74m1 44h42m-26 30v-29m-15 12h16m-6 1v-13m2 6h4"/>
|
||||||
|
<path fill="#636363" d="M111.412 190.336c0 1.2-.576 2.268-1.728 3.204-1.128.936-2.436 1.62-3.924 2.052-1.464.432-2.88.648-4.248.648-.36 0-.624-.012-.792-.036v-1.224c.048 0 .744-.108 2.088-.324 1.344-.24 2.472-.648 3.384-1.224.936-.576 1.404-1.224 1.404-1.944 0-.864-.648-1.296-1.944-1.296-1.752 0-3.408.6-4.968 1.8-1.56 1.2-2.34 2.46-2.34 3.78 0 1.32.816 2.172 2.448 2.556.36.096.6.192.72.288.12.096.18.264.18.504s-.12.444-.36.612c-.216.144-.732.36-1.548.648-4.416 1.512-6.624 3.864-6.624 7.056 0 .648.12 1.2.36 1.656 1.464-1.872 3.36-3.396 5.688-4.572 2.328-1.176 4.572-1.764 6.732-1.764 1.128 0 2.028.252 2.7.756.672.48 1.008 1.152 1.008 2.016 0 1.344-.624 2.688-1.872 4.032-1.248 1.344-2.868 2.436-4.86 3.276-1.968.864-3.936 1.296-5.904 1.296-1.944 0-3.48-.36-4.608-1.08-.024.024-.132.204-.324.54-.48.96-.888 1.632-1.224 2.016l-1.188-.504c.168-.672.66-1.692 1.476-3.06-1.104-1.248-1.656-2.64-1.656-4.176 0-1.56.624-3.036 1.872-4.428 1.272-1.416 3.036-2.58 5.292-3.492-1.128-.936-1.692-1.908-1.692-2.916 0-1.368.72-2.76 2.16-4.176 1.44-1.44 3.192-2.604 5.256-3.492 2.088-.888 4.044-1.332 5.868-1.332.96 0 1.728.216 2.304.648.576.408.864.96.864 1.656Zm-7.56 14.58c-1.416 0-3.072.54-4.968 1.62-1.872 1.08-3.396 2.364-4.572 3.852.96 1.08 2.316 1.62 4.068 1.62 1.776 0 3.504-.564 5.184-1.692 1.704-1.152 2.556-2.388 2.556-3.708 0-.528-.204-.936-.612-1.224-.408-.312-.96-.468-1.656-.468Zm24.802-13.896-5.148-.108c-5.256 0-7.884 1.152-7.884 3.456 0 .6.216 1.092.648 1.476.456.36 1.128.54 2.016.54.888 0 2.052-.408 3.492-1.224l.54.936a8.62 8.62 0 0 1-2.52 1.872c-.936.456-1.872.684-2.808.684-.936 0-1.74-.312-2.412-.936-.672-.624-1.008-1.404-1.008-2.34 0-1.896 1.02-3.552 3.06-4.968 2.064-1.416 4.848-2.124 8.352-2.124.288 0 1.32.048 3.096.144 1.776.096 3.18.144 4.212.144 1.032 0 1.98-.156 2.844-.468.336-.096.54-.144.612-.144.336 0 .504.216.504.648 0 .624-.552 1.188-1.656 1.692-1.08.504-2.328.756-3.744.756-1.584 1.536-2.976 4.224-4.176 8.064-1.176 3.816-2.592 6.732-4.248 8.748-1.632 1.992-3.576 2.988-5.832 2.988-.888 0-1.728-.288-2.52-.864l.828-1.368c.288.144.612.276.972.396l.504.072c.12.024.3.036.54.036 1.2 0 2.268-.576 3.204-1.728.96-1.152 1.956-3.324 2.988-6.516 1.056-3.216 1.872-5.34 2.448-6.372.6-1.056 1.632-2.22 3.096-3.492Zm23.303 13.464 2.556-.036v1.476a6.202 6.202 0 0 0-1.008-.072c-.384 0-.816.024-1.296.072 0 1.848-.888 3.72-2.664 5.616-1.776 1.896-3.924 3.432-6.444 4.608-2.496 1.2-4.824 1.8-6.984 1.8-1.128 0-2.052-.288-2.772-.864-.696-.576-1.044-1.332-1.044-2.268 0-1.392.924-2.844 2.772-4.356 1.848-1.512 4.008-2.76 6.48-3.744 2.472-.984 4.704-1.56 6.696-1.728-.24-.36-.612-.672-1.116-.936-.504-.264-1.536-.672-3.096-1.224-1.56-.576-2.664-1.2-3.312-1.872-.648-.696-.972-1.668-.972-2.916s.672-2.676 2.016-4.284a18.287 18.287 0 0 1 4.752-4.032c1.848-1.104 3.468-1.656 4.86-1.656.624 0 1.14.228 1.548.684.408.432.612.972.612 1.62 0 1.056-.636 2.388-1.908 3.996-1.272 1.584-2.688 2.796-4.248 3.636l-.864-1.224c.936-.48 1.752-1.224 2.448-2.232.72-1.032 1.08-1.92 1.08-2.664 0-.744-.396-1.116-1.188-1.116-1.128 0-2.424.708-3.888 2.124-1.44 1.392-2.16 2.7-2.16 3.924 0 .84.216 1.5.648 1.98.432.48 1.524 1.08 3.276 1.8 1.752.696 3 1.32 3.744 1.872.744.528 1.236 1.2 1.476 2.016Zm-16.056 9.396c0 1.152.936 1.728 2.808 1.728 1.032 0 2.364-.384 3.996-1.152a13.126 13.126 0 0 0 4.356-3.168c1.272-1.344 1.908-2.724 1.908-4.14 0-.168-.036-.42-.108-.756-3.288.312-6.276 1.236-8.964 2.772-2.664 1.536-3.996 3.108-3.996 4.716Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
client/public/logo.png
Normal file
BIN
client/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
4
client/public/people.svg
Normal file
4
client/public/people.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M23.313 26.102l-6.296-3.488c2.34-1.841 2.976-5.459 2.976-7.488v-4.223c0-2.796-3.715-5.91-7.447-5.91-3.73 0-7.544 3.114-7.544 5.91v4.223c0 1.845 0.78 5.576 3.144 7.472l-6.458 3.503s-1.688 0.752-1.688 1.689v2.534c0 0.933 0.757 1.689 1.688 1.689h21.625c0.931 0 1.688-0.757 1.688-1.689v-2.534c0-0.994-1.689-1.689-1.689-1.689zM23.001 30.015h-21.001v-1.788c0.143-0.105 0.344-0.226 0.502-0.298 0.047-0.021 0.094-0.044 0.139-0.070l6.459-3.503c0.589-0.32 0.979-0.912 1.039-1.579s-0.219-1.32-0.741-1.739c-1.677-1.345-2.396-4.322-2.396-5.911v-4.223c0-1.437 2.708-3.91 5.544-3.91 2.889 0 5.447 2.44 5.447 3.91v4.223c0 1.566-0.486 4.557-2.212 5.915-0.528 0.416-0.813 1.070-0.757 1.739s0.446 1.267 1.035 1.589l6.296 3.488c0.055 0.030 0.126 0.063 0.184 0.089 0.148 0.063 0.329 0.167 0.462 0.259v1.809zM30.312 21.123l-6.39-3.488c2.34-1.841 3.070-5.459 3.070-7.488v-4.223c0-2.796-3.808-5.941-7.54-5.941-2.425 0-4.904 1.319-6.347 3.007 0.823 0.051 1.73 0.052 2.514 0.302 1.054-0.821 2.386-1.308 3.833-1.308 2.889 0 5.54 2.47 5.54 3.941v4.223c0 1.566-0.58 4.557-2.305 5.915-0.529 0.416-0.813 1.070-0.757 1.739 0.056 0.67 0.445 1.267 1.035 1.589l6.39 3.488c0.055 0.030 0.126 0.063 0.184 0.089 0.148 0.063 0.329 0.167 0.462 0.259v1.779h-4.037c0.61 0.46 0.794 1.118 1.031 2h3.319c0.931 0 1.688-0.757 1.688-1.689v-2.503c-0.001-0.995-1.689-1.691-1.689-1.691z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
11
client/public/student.svg
Normal file
11
client/public/student.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M23.3 8.40007L21.82 6.40008C21.7248 6.27314 21.6008 6.17066 21.4583 6.10111C21.3157 6.03156 21.1586 5.99693 21 6.00008H11.2C11.0555 6.00007 10.9128 6.03135 10.7816 6.09177C10.6504 6.15219 10.5339 6.24031 10.44 6.35007L8.71998 8.35008C8.57227 8.53401 8.49435 8.76424 8.49998 9.00008V16.2901C8.50262 18.0317 9.19567 19.7013 10.4272 20.9328C11.6588 22.1644 13.3283 22.8574 15.07 22.8601H16.93C18.6716 22.8574 20.3412 22.1644 21.5728 20.9328C22.8043 19.7013 23.4973 18.0317 23.5 16.2901V9.00008C23.5 8.7837 23.4298 8.57317 23.3 8.40007Z" fill="#FFCC80"/>
|
||||||
|
<path d="M29.78 28.38L25.78 23.38C25.664 23.2321 25.5086 23.1198 25.3318 23.0562C25.1549 22.9925 24.9637 22.98 24.78 23.02L16 25L7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H29C29.1885 29.9995 29.373 29.9457 29.5322 29.8448C29.6914 29.744 29.8189 29.6002 29.9 29.43C29.98 29.2613 30.0112 29.0734 29.99 28.8879C29.9688 28.7023 29.8961 28.5264 29.78 28.38Z" fill="#01579B"/>
|
||||||
|
<path d="M29.29 6.00003L16.29 2.00003C16.0999 1.95002 15.9001 1.95002 15.71 2.00003L2.71 6.00003C2.49742 6.06422 2.31226 6.19735 2.1837 6.37841C2.05515 6.55947 1.99052 6.77817 2 7.00003C1.9917 7.22447 2.0592 7.44518 2.19163 7.62659C2.32405 7.80799 2.5137 7.93954 2.73 8.00003L15.73 11.6C15.906 11.6534 16.094 11.6534 16.27 11.6L29.27 8.00003C29.4863 7.93954 29.6759 7.80799 29.8084 7.62659C29.9408 7.44518 30.0083 7.22447 30 7.00003C30.0095 6.77817 29.9448 6.55947 29.8163 6.37841C29.6877 6.19735 29.5026 6.06422 29.29 6.00003Z" fill="#01579B"/>
|
||||||
|
<path d="M11.22 6C11.0756 5.99999 10.9328 6.03127 10.8016 6.09169C10.6704 6.15211 10.5539 6.24023 10.46 6.35L8.74 8.35C8.58509 8.53114 8.49998 8.76166 8.5 9V16.29C8.50264 18.0317 9.19569 19.7012 10.4272 20.9328C11.6588 22.1643 13.3283 22.8574 15.07 22.86H16V6H11.22Z" fill="#FFE0B2"/>
|
||||||
|
<path d="M7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H16V25L7.21999 23Z" fill="#0277BD"/>
|
||||||
|
<path d="M15.71 2.00002L2.71 6.00002C2.49742 6.06422 2.31226 6.19734 2.1837 6.3784C2.05515 6.55947 1.99052 6.77817 2 7.00002C1.9917 7.22447 2.0592 7.44518 2.19163 7.62658C2.32405 7.80799 2.5137 7.93954 2.73 8.00002L15.73 11.6C15.8194 11.6146 15.9106 11.6146 16 11.6V2.00002C15.9039 1.98469 15.8061 1.98469 15.71 2.00002Z" fill="#0277BD"/>
|
||||||
|
<path d="M2.73 8.00003L8.5 9.56003V16.29C8.50264 18.0317 9.19569 19.7013 10.4272 20.9328C11.6588 22.1643 13.3283 22.8574 15.07 22.86H16.93C18.6717 22.8574 20.3412 22.1643 21.5728 20.9328C22.8043 19.7013 23.4974 18.0317 23.5 16.29V9.56003L29.27 8.00003C29.4863 7.93954 29.6759 7.80799 29.8084 7.62659C29.9408 7.44518 30.0083 7.22447 30 7.00003C30.0095 6.77817 29.9448 6.55947 29.8163 6.37841C29.6877 6.19735 29.5026 6.06422 29.29 6.00003L16.29 2.00003C16.0999 1.95002 15.9001 1.95002 15.71 2.00003L2.71 6.00003C2.49742 6.06422 2.31226 6.19735 2.1837 6.37841C2.05515 6.55947 1.99052 6.77817 2 7.00003C1.9917 7.22447 2.0592 7.44518 2.19163 7.62659C2.32405 7.80799 2.5137 7.93954 2.73 8.00003ZM21.5 16.29C21.4974 17.5013 21.015 18.6621 20.1586 19.5186C19.3021 20.3751 18.1412 20.8574 16.93 20.86H15.07C13.8588 20.8574 12.6979 20.3751 11.8414 19.5186C10.985 18.6621 10.5026 17.5013 10.5 16.29V10.11L15.73 11.56C15.906 11.6134 16.094 11.6134 16.27 11.56L21.5 10.11V16.29ZM16 4.05003L25.44 7.00003L16 9.56003L6.56 7.00003L16 4.05003Z" fill="#263238"/>
|
||||||
|
<path d="M25.78 23.38C25.664 23.2321 25.5086 23.1198 25.3318 23.0562C25.1549 22.9925 24.9637 22.98 24.78 23.02L16 25L7.21999 23C7.03632 22.96 6.84507 22.9725 6.6682 23.0362C6.49133 23.0998 6.33598 23.2121 6.21999 23.36L2.21999 28.36C2.10392 28.5064 2.03116 28.6823 2.00995 28.8679C1.98874 29.0534 2.01993 29.2413 2.09999 29.41C2.17815 29.5839 2.3044 29.7319 2.46385 29.8364C2.62331 29.9409 2.80933 29.9977 2.99999 30H29C29.1885 29.9995 29.373 29.9457 29.5322 29.8448C29.6914 29.744 29.8189 29.6002 29.9 29.43C29.98 29.2613 30.0112 29.0734 29.99 28.8879C29.9688 28.7023 29.8961 28.5264 29.78 28.38L25.78 23.38ZM5.07999 28L7.38999 25.11L15.78 27C15.9251 27.0299 16.0748 27.0299 16.22 27L24.61 25.13L26.92 28H5.07999Z" fill="#263238"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
238
client/public/teacher.svg
Normal file
238
client/public/teacher.svg
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--noto" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20895" gradientUnits="userSpaceOnUse" x1="63.999" y1="116.605" x2="63.999" y2="39.511" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#26a69a">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#00796b">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#IconifyId17ecdb2904d178eab20895)" d="M6.36 10.9h115.29v77.52H6.36z">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20896" gradientUnits="userSpaceOnUse" x1="63.999" y1="119.455" x2="63.999" y2="37.224" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#8d6e63">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".779" stop-color="#795548">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M119.29 13.26v72.81H8.71V13.26h110.58M124 8.55H4v82.23h120V8.55z" fill="url(#IconifyId17ecdb2904d178eab20896)">
|
||||||
|
</path>
|
||||||
|
<path d="M98.9 79.85c-1.25-2.27.34-4.58 3.06-7.44c4.31-4.54 9-15.07 4.64-25.76c.03-.06-.86-1.86-.83-1.92l-1.79-.09c-.57-.08-20.26-.12-39.97-.12s-39.4.04-39.97.12c0 0-2.65 1.95-2.63 2.01c-4.35 10.69.33 21.21 4.64 25.76c2.71 2.86 4.3 5.17 3.06 7.44c-1.21 2.21-4.81 2.53-4.81 2.53s.83 2.26 2.83 3.48c1.85 1.13 4.13 1.39 5.7 1.43c0 0 6.15 8.51 22.23 8.51h17.9c16.08 0 22.23-8.51 22.23-8.51c1.57-.04 3.85-.3 5.7-1.43c2-1.22 2.83-3.48 2.83-3.48s-3.61-.32-4.82-2.53z" fill="#232020">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20897" cx="99.638" cy="45.85" r="23.419" gradientTransform="matrix(1 0 0 .4912 -21.055 59.629)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".728" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M63.99 95.79v-9.44l28.57-2.26l2.6 3.2s-6.15 8.51-22.23 8.51l-8.94-.01z" fill="url(#IconifyId17ecdb2904d178eab20897)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20898" cx="76.573" cy="49.332" r="6.921" gradientTransform="matrix(-.9057 .4238 -.3144 -.6719 186.513 79.36)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".663" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M95.1 83.16c-4.28-6.5 5.21-8.93 5.21-8.93l.01.01c-1.65 2.05-2.4 3.84-1.43 5.61c1.21 2.21 4.81 2.53 4.81 2.53s-4.91 4.36-8.6.78z" fill="url(#IconifyId17ecdb2904d178eab20898)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20899" cx="94.509" cy="68.91" r="30.399" gradientTransform="matrix(-.0746 -.9972 .8311 -.0622 33.494 157.622)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".725" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M106.62 46.65c4.25 10.35-.22 21.01-4.41 25.51c-.57.62-3.01 3.01-3.57 4.92c0 0-9.54-13.31-12.39-21.13c-.57-1.58-1.1-3.2-1.17-4.88c-.05-1.26.14-2.76.87-3.83c.89-1.31 20.16-1.7 20.16-1.7c0 .01.51 1.11.51 1.11z" fill="url(#IconifyId17ecdb2904d178eab20899)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20900" cx="44.31" cy="68.91" r="30.399" gradientTransform="matrix(.0746 -.9972 -.8311 -.0622 98.274 107.563)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".725" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M21.4 46.65c-4.24 10.35.23 21.01 4.41 25.5c.58.62 3.01 3.01 3.57 4.92c0 0 9.54-13.31 12.39-21.13c.58-1.58 1.1-3.2 1.17-4.88c.05-1.26-.14-2.76-.87-3.83c-.89-1.31-1.93-.96-3.44-.96c-2.88 0-15.49-.74-16.47-.74c.01.02-.76 1.12-.76 1.12z" fill="url(#IconifyId17ecdb2904d178eab20900)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20901" cx="49.439" cy="45.85" r="23.419" gradientTransform="matrix(-1 0 0 .4912 98.878 59.629)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".728" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M64.03 95.79v-9.44l-28.57-2.26l-2.6 3.2s6.15 8.51 22.23 8.51l8.94-.01z" fill="url(#IconifyId17ecdb2904d178eab20901)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20902" cx="26.374" cy="49.332" r="6.921" gradientTransform="matrix(.9057 .4238 .3144 -.6719 -13.024 100.635)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".663" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M32.92 83.16c4.28-6.5-5.21-8.93-5.21-8.93l-.01.01c1.65 2.05 2.4 3.84 1.43 5.61c-1.21 2.21-4.81 2.53-4.81 2.53s4.91 4.36 8.6.78z" fill="url(#IconifyId17ecdb2904d178eab20902)">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20903" gradientUnits="userSpaceOnUse" x1="64" y1="25.908" x2="64" y2="10.938" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#e1f5fe">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#81d4fa">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M114.5 120.75c0-15.47-25.34-23.56-50.36-23.56H64c-25.14.03-50.5 7.32-50.5 23.56V124h101v-3.25z" fill="url(#IconifyId17ecdb2904d178eab20903)">
|
||||||
|
</path>
|
||||||
|
<g>
|
||||||
|
<path fill="#3c2b24" d="M64 92.33h-9.08v9.98l9.06 2.38l9.1-2.38v-9.98z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20904" gradientUnits="userSpaceOnUse" x1="29.113" y1="29.156" x2="29.113" y2="4.97" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#ffa000">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".341" stop-color="#ff9300">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".972" stop-color="#ff7100">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#ff6f00">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M12 120.75V124h32.89l1.33-27.04C27.52 99.72 12 107.15 12 120.75z" fill="url(#IconifyId17ecdb2904d178eab20904)">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20905" gradientUnits="userSpaceOnUse" x1="98.888" y1="29.435" x2="98.888" y2="4.807" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#ffa000">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".341" stop-color="#ff9300">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".972" stop-color="#ff7100">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#ff6f00">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M81.78 96.96L83.1 124H116v-3.25c0-13.6-15.52-21.03-34.22-23.79z" fill="url(#IconifyId17ecdb2904d178eab20905)">
|
||||||
|
</path>
|
||||||
|
<g>
|
||||||
|
<path fill="#66c0e8" d="M54.03 92.12l9.99 12.82l-16.24 6.64l-2.41-14.54z">
|
||||||
|
</path>
|
||||||
|
<path fill="#66c0e8" d="M73.97 92.12l-9.99 12.82l16.24 6.64l2.41-14.54z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M48.88 95s-1.14 2.72-1.94 6c-1.59 6.52-1.69 15.8-1.69 15.8s-6.89-2.34-9.04-8.05c-2.54-6.75 1.75-10.46 1.75-10.46s.9-.38 4.68-2.46s6.24-.83 6.24-.83z" fill="#af5214">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M79.12 95s1.14 2.72 1.94 6c1.59 6.52 1.69 15.8 1.69 15.8s6.89-2.34 9.04-8.05c2.54-6.75-1.75-10.46-1.75-10.46s-.9-.38-4.68-2.46s-6.24-.83-6.24-.83z" fill="#af5214">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M91.12 50.43H36.47c-5.89 0-10.71 5.14-10.71 11.41s4.82 11.41 10.71 11.41H91.12c5.89 0 10.71-5.14 10.71-11.41s-4.82-11.41-10.71-11.41z" fill="#3c2b24">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M63.79 11.07c-17.4 0-33.52 18.61-33.52 45.4c0 26.64 16.61 39.81 33.52 39.81S97.31 83.1 97.31 56.46c0-26.78-16.11-45.39-33.52-45.39z" fill="#70534a">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g fill="#1a1717">
|
||||||
|
<ellipse cx="47.98" cy="58.81" rx="4.93" ry="5.1">
|
||||||
|
</ellipse>
|
||||||
|
<ellipse cx="79.13" cy="58.81" rx="4.93" ry="5.1">
|
||||||
|
</ellipse>
|
||||||
|
</g>
|
||||||
|
<g fill="#1a1717">
|
||||||
|
<path d="M55.37 49.82c-.93-1.23-3.07-3.01-7.23-3.01s-6.31 1.79-7.23 3.01c-.41.54-.31 1.17-.02 1.55c.26.35 1.04.68 1.9.39s2.54-1.16 5.35-1.18c2.81.02 4.49.89 5.35 1.18c.86.29 1.64-.03 1.9-.39c.28-.38.39-1.01-.02-1.55z">
|
||||||
|
</path>
|
||||||
|
<path d="M86.36 49.82c-.93-1.23-3.07-3.01-7.23-3.01s-6.31 1.79-7.23 3.01c-.41.54-.31 1.17-.02 1.55c.26.35 1.04.68 1.9.39s2.54-1.16 5.35-1.18c2.81.02 4.49.89 5.35 1.18c.86.29 1.64-.03 1.9-.39c.29-.38.39-1.01-.02-1.55z">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<path d="M67.65 68.06c-.11-.04-.21-.07-.32-.08h-7.08c-.11.01-.22.04-.32.08c-.64.26-.99.92-.69 1.63c.3.71 1.71 2.69 4.55 2.69s4.25-1.99 4.55-2.69c.31-.71-.05-1.37-.69-1.63z" fill="#33251f">
|
||||||
|
</path>
|
||||||
|
<path d="M72.32 76.14c-3.18 1.89-13.63 1.89-16.81 0c-1.83-1.09-3.7.58-2.94 2.24c.75 1.63 6.44 5.42 11.37 5.42s10.55-3.79 11.3-5.42c.76-1.66-1.09-3.33-2.92-2.24z" fill="#1a1717">
|
||||||
|
</path>
|
||||||
|
<path d="M93.83 52.93c-.07-1.19-.12-1.31-1.69-1.81c-1.23-.39-7.95-.94-13.01-.66c-.36.02-.71.04-1.04.07c-4.59.39-10.1 2.24-14.24 2.34c-1.76.04-9.01-1.86-14.14-2.26c-.33-.02-.66-.05-1-.06c-5.07-.26-11.82.33-13.05.73c-1.57.51-1.62.63-1.68 1.82c-.07 1.19.13 2.2 1.06 2.51c1.27.42 1.28 2 2.13 6.54c.77 4.14 2.62 7.41 10.57 7.98c.34.02.66.04.98.04c7.03.1 9.45-4.53 10.25-6.07c1.49-2.86 1.02-6.8 4.96-6.81c3.93-.01 3.56 3.86 5.07 6.71c.81 1.53 3.17 6.18 10.14 6.08c.34 0 .69-.02 1.05-.05c7.94-.62 9.78-3.9 10.52-8.04c.82-4.55.83-6.14 2.09-6.56c.91-.3 1.11-1.31 1.03-2.5zM53.28 68.17c-1.22.57-2.85.86-4.57.86c-3.59-.01-7.57-1.27-9.01-3.81c-2.04-3.62-2.57-10.94.03-12.47c1.14-.67 4.99-1.13 8.97-.96c4.13.18 8.4 1.04 9.94 3.06c2.55 3.33-1.5 11.5-5.36 13.32zm34.9-3.1c-1.43 2.56-5.44 3.85-9.05 3.86c-1.7.01-3.31-.27-4.51-.83c-3.87-1.8-7.97-9.94-5.45-13.29c1.53-2.04 5.82-2.92 9.96-3.12c3.97-.19 7.81.25 8.94.91c2.61 1.52 2.13 8.84.11 12.47z" fill="#212121" stroke="#212121" stroke-width=".55" stroke-miterlimit="10">
|
||||||
|
</path>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20906" gradientUnits="userSpaceOnUse" x1="79.569" y1="22.713" x2="76.946" y2="11.668" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset=".002" stop-color="#212121" stop-opacity=".2">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#212121" stop-opacity=".6">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M101.67 121.61l.57-2.2l.01-.05l1.93-7.6l-6.9-1.98l-34.92-10.03c-.05-.01-.09-.01-.13-.03a6.177 6.177 0 0 0-7.51 4.27L48.97 124h52.02l.68-2.39z" opacity=".67" fill="url(#IconifyId17ecdb2904d178eab20906)">
|
||||||
|
</path>
|
||||||
|
<path d="M105.75 111.88c.29-1.01-.29-2.06-1.3-2.34l-38.69-11.1a6.19 6.19 0 0 0-7.65 4.24L52 124h50.28l3.47-12.12z" fill="#424242">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20907" gradientUnits="userSpaceOnUse" x1="81.84" y1="17.098" x2="79.869" y2="10.486" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#ef5350">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#e53935">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M105.08 120.31c.35-1.22-.38-2.5-1.62-2.85l-41.52-11.9c-4.53-1.3-5.32 2.35-6.59 6.78L52 124h52.02l1.06-3.69z" fill="url(#IconifyId17ecdb2904d178eab20907)">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20908" gradientUnits="userSpaceOnUse" x1="58.405" y1="19.113" x2="60.268" y2="24.969" gradientTransform="matrix(1 0 0 -1 0 128)">
|
||||||
|
<stop offset="0" stop-color="#212121">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#424242">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M63.26 98.24a6.172 6.172 0 0 0-5.14 4.42L52 124h3.87l7.39-25.76z" fill="url(#IconifyId17ecdb2904d178eab20908)">
|
||||||
|
</path>
|
||||||
|
<path d="M64.33 101.57c.18 0 .38.02.59.07l37.25 10.7l-.31 1.08c-11.79-3.29-34.29-9.62-38.94-11.16c.24-.33.71-.69 1.41-.69m0-3.33c-4.52 0-6.78 5.57-3.12 6.94c4.03 1.5 42.93 12.32 42.93 12.32l1.58-5.52c.31-1.06-.19-2.14-1.11-2.4L65.77 98.42c-.5-.12-.98-.18-1.44-.18z" fill="#424242" opacity=".2">
|
||||||
|
</path>
|
||||||
|
<linearGradient id="IconifyId17ecdb2904d178eab20909" gradientUnits="userSpaceOnUse" x1="-117.44" y1="-972.312" x2="-73.995" y2="-972.312" gradientTransform="matrix(.9612 .2758 -.3192 1.1123 -136.555 1216.41)">
|
||||||
|
<stop offset=".01" stop-color="#bdbdbd">
|
||||||
|
</stop>
|
||||||
|
<stop offset=".987" stop-color="#f8f8f7">
|
||||||
|
</stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path d="M103.37 112.12l-39.8-11.42c-1.08-.31-2.26.46-2.62 1.71l-.06.22c-.36 1.25.23 2.53 1.31 2.84l39.8 11.42s-.34-.83.07-2.3c.41-1.48 1.3-2.47 1.3-2.47z" fill="url(#IconifyId17ecdb2904d178eab20909)">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M104.07 25.11c-2.44-3.69-7.91-8.64-12.82-8.97c-.79-4.72-5.84-8.72-10.73-10.27c-13.23-4.19-21.84.51-26.46 3.03c-.96.52-7.17 3.97-11.51 1.5c-2.72-1.55-2.67-5.74-2.67-5.74s-8.52 3.25-5.61 12.3c-2.93.12-6.77 1.36-8.8 5.47c-2.42 4.9-1.56 8.99-.86 10.95c-2.52 2.14-5.69 6.69-3.52 12.6c1.64 4.45 8.17 6.5 8.17 6.5c-.46 8.01 1.03 12.94 1.82 14.93c.14.35.63.32.72-.04c.99-3.97 4.37-17.8 4.03-20.21c0 0 11.35-2.25 22.17-10.22c2.2-1.62 4.59-3 7.13-4.01c13.59-5.41 16.43 3.82 16.43 3.82s9.42-1.81 12.26 11.27c1.07 4.9 1.79 12.75 2.4 18.24c.04.39.57.47.72.11c.95-2.18 2.85-6.5 3.3-10.91c.16-1.55 4.34-3.6 6.14-10.26c2.41-8.88-.54-17.42-2.31-20.09z" fill="#232020">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20910" cx="82.019" cy="84.946" r="35.633" gradientTransform="matrix(.3076 .9515 .706 -.2282 -3.184 -15.605)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".699" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M100.22 55.5c.16-1.55 4.34-3.6 6.14-10.26c.19-.71.35-1.43.5-2.15c1.46-8.09-1.16-15.52-2.79-17.98c-2.26-3.41-7.1-7.89-11.69-8.81c-.4-.05-.79-.1-1.16-.12c0 0 .33 2.15-.54 3.86c-1.12 2.22-3.41 2.75-3.41 2.75c11.97 11.98 11.12 22 12.95 32.71z" fill="url(#IconifyId17ecdb2904d178eab20910)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20911" cx="47.28" cy="123.8" r="9.343" gradientTransform="matrix(.8813 .4726 .5603 -1.045 -63.752 111.228)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".58" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M56.95 7.39c-1.1.53-2.06 1.06-2.9 1.51c-.96.52-7.17 3.97-11.51 1.5c-2.67-1.52-2.67-5.58-2.67-5.72c-1.23 1.57-4.95 12.78 5.93 13.53c4.69.32 7.58-3.77 9.3-7.23c.62-1.26 1.59-3.1 1.85-3.59z" fill="url(#IconifyId17ecdb2904d178eab20911)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20912" cx="159.055" cy="62.862" r="28.721" gradientTransform="matrix(-.9378 -.3944 -.2182 .5285 231.04 50.678)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".699" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M79.16 5.47c7.32 1.98 10.89 5.71 12.08 10.68c.35 1.46.77 15.08-25.23-.4c-9.67-5.76-7.03-9.36-5.9-9.77c4.42-1.6 10.85-2.73 19.05-.51z" fill="url(#IconifyId17ecdb2904d178eab20912)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20913" cx="43.529" cy="115.276" r="8.575" gradientTransform="matrix(1 0 0 -1.2233 0 153.742)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".702" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M39.84 4.68c-.01.01-.03.01-.06.03h-.01c-.93.39-8.24 3.78-5.51 12.25l7.78 1.25c-6.89-6.98-2.17-13.55-2.17-13.55s-.02.01-.03.02z" fill="url(#IconifyId17ecdb2904d178eab20913)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20914" cx="42.349" cy="100.139" r="16.083" gradientTransform="matrix(-.9657 -.2598 -.2432 .9037 107.598 -51.632)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".66" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M39.07 17.73l-4.81-.77c-.19 0-.83.06-1.18.11c-2.71.38-5.9 1.78-7.63 5.36c-1.86 3.86-1.81 7.17-1.3 9.38c.15.74.45 1.58.45 1.58s2.38-2.26 8.05-2.41l6.42-13.25z" fill="url(#IconifyId17ecdb2904d178eab20914)">
|
||||||
|
</path>
|
||||||
|
<radialGradient id="IconifyId17ecdb2904d178eab20915" cx="38.533" cy="84.609" r="16.886" gradientTransform="matrix(.9907 .1363 .1915 -1.3921 -15.841 155.923)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".598" stop-color="#444140" stop-opacity="0">
|
||||||
|
</stop>
|
||||||
|
<stop offset="1" stop-color="#444140">
|
||||||
|
</stop>
|
||||||
|
</radialGradient>
|
||||||
|
<path d="M24.37 33.58c-2.37 2.1-5.56 6.79-3.21 12.61c1.77 4.39 8.09 6.29 8.09 6.29c0 .02 1.26.4 1.91.4l1.48-21.9c-3.03 0-5.94.91-7.82 2.22c.03.03-.46.35-.45.38z" fill="url(#IconifyId17ecdb2904d178eab20915)">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
1
client/public/vite.svg
Normal file
1
client/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
13
client/pull_request_template.md
Normal file
13
client/pull_request_template.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
## PULL REQUEST
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
## Link to the ticket
|
||||||
|
|
||||||
|
## Checklist before requesting a review
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] If it is a core feature, I have added thorough tests.
|
||||||
|
- [ ] Do we need to implement analytics?
|
||||||
|
- [ ] Will this be part of a product update? If yes, please write one phrase about this update.
|
||||||
|
|
||||||
|
|
||||||
66
client/src/App.tsx
Normal file
66
client/src/App.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// App.tsx
|
||||||
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Page main
|
||||||
|
import Home from './pages/Home/Home';
|
||||||
|
|
||||||
|
// Pages espace enseignant
|
||||||
|
import Dashboard from './pages/Teacher/Dashboard/Dashboard';
|
||||||
|
import Share from './pages/Teacher/Share/Share';
|
||||||
|
import Login from './pages/Teacher/Login/Login';
|
||||||
|
import Register from './pages/Teacher/Register/Register';
|
||||||
|
import ResetPassword from './pages/Teacher/ResetPassword/ResetPassword';
|
||||||
|
import ManageRoom from './pages/Teacher/ManageRoom/ManageRoom';
|
||||||
|
import QuizForm from './pages/Teacher/EditorQuiz/EditorQuiz';
|
||||||
|
|
||||||
|
// Pages espace étudiant
|
||||||
|
import JoinRoom from './pages/Student/JoinRoom/JoinRoom';
|
||||||
|
|
||||||
|
// Header/Footer import
|
||||||
|
import Header from './components/Header/Header';
|
||||||
|
import Footer from './components/Footer/Footer';
|
||||||
|
|
||||||
|
import ApiService from './services/ApiService';
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
ApiService.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoggedIn = () => {
|
||||||
|
return ApiService.isLogedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
|
||||||
|
<Header
|
||||||
|
isLoggedIn={isLoggedIn}
|
||||||
|
handleLogout={handleLogout}/>
|
||||||
|
|
||||||
|
<div className="app">
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
{/* Page main */}
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
|
||||||
|
{/* Pages espace enseignant */}
|
||||||
|
<Route path="/teacher/login" element={<Login />} />
|
||||||
|
<Route path="/teacher/register" element={<Register />} />
|
||||||
|
<Route path="/teacher/resetPassword" element={<ResetPassword />} />
|
||||||
|
<Route path="/teacher/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/teacher/share/:id" element={<Share />} />
|
||||||
|
<Route path="/teacher/editor-quiz/:id" element={<QuizForm />} />
|
||||||
|
<Route path="/teacher/manage-room/:id" element={<ManageRoom />} />
|
||||||
|
|
||||||
|
{/* Pages espace étudiant */}
|
||||||
|
<Route path="/student/join-room" element={<JoinRoom />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
6
client/src/Types/FolderType.tsx
Normal file
6
client/src/Types/FolderType.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface FolderType {
|
||||||
|
_id: string;
|
||||||
|
userId: string;
|
||||||
|
title: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
6
client/src/Types/QuestionType.tsx
Normal file
6
client/src/Types/QuestionType.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
|
||||||
|
export interface QuestionType {
|
||||||
|
question: GIFTQuestion;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
10
client/src/Types/QuizType.tsx
Normal file
10
client/src/Types/QuizType.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// QuizType.tsx
|
||||||
|
export interface QuizType {
|
||||||
|
_id: string;
|
||||||
|
folderId: string;
|
||||||
|
userId: string;
|
||||||
|
title: string;
|
||||||
|
content: string[];
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
4
client/src/Types/UserType.tsx
Normal file
4
client/src/Types/UserType.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface UserType {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
43
client/src/__tests__/Types/QuestionType.test.tsx
Normal file
43
client/src/__tests__/Types/QuestionType.test.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
//QuestionType.test.tsx
|
||||||
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
|
|
||||||
|
const mockQuestion: GIFTQuestion = {
|
||||||
|
id: '1',
|
||||||
|
type: 'MC',
|
||||||
|
stem: { format: 'plain', text: 'Sample Question' },
|
||||||
|
title: 'Sample Question',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||||
|
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockQuestionType: QuestionType = {
|
||||||
|
question: mockQuestion,
|
||||||
|
image: 'sample-image-url',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('QuestionType', () => {
|
||||||
|
test('has the expected structure', () => {
|
||||||
|
expect(mockQuestionType).toEqual(expect.objectContaining({
|
||||||
|
question: expect.any(Object),
|
||||||
|
image: expect.any(String),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(mockQuestionType.question).toEqual(expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
type: expect.any(String),
|
||||||
|
stem: expect.objectContaining({
|
||||||
|
format: expect.any(String),
|
||||||
|
text: expect.any(String),
|
||||||
|
}),
|
||||||
|
title: expect.any(String),
|
||||||
|
hasEmbeddedAnswers: expect.any(Boolean),
|
||||||
|
globalFeedback: expect.any(Object),
|
||||||
|
choices: expect.any(Array),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
40
client/src/__tests__/Types/QuizType.test.tsx
Normal file
40
client/src/__tests__/Types/QuizType.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*//QuizType.test.tsx
|
||||||
|
import { QuizType } from "../../Types/QuizType";
|
||||||
|
export function isQuizValid(quiz: QuizType): boolean {
|
||||||
|
return quiz.title.length > 0 && quiz.content.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('isQuizValid function', () => {
|
||||||
|
it('returns true for a valid quiz', () => {
|
||||||
|
const validQuiz: QuizType = {
|
||||||
|
_id: '1',
|
||||||
|
title: 'Sample Quiz',
|
||||||
|
content: ['Question 1', 'Question 2'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = isQuizValid(validQuiz);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for an invalid quiz with an empty title', () => {
|
||||||
|
const invalidQuiz: QuizType = {
|
||||||
|
_id: '2',
|
||||||
|
title: '',
|
||||||
|
content: ['Question 1', 'Question 2'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = isQuizValid(invalidQuiz);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for an invalid quiz with no questions', () => {
|
||||||
|
const invalidQuiz: QuizType = {
|
||||||
|
_id: '3',
|
||||||
|
title: 'Sample Quiz',
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = isQuizValid(invalidQuiz);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});*/
|
||||||
15
client/src/__tests__/Types/UserType.test.tsx
Normal file
15
client/src/__tests__/Types/UserType.test.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
//UserTyper.test.tsx
|
||||||
|
import { UserType } from "../../Types/UserType";
|
||||||
|
|
||||||
|
const user : UserType = {
|
||||||
|
name: 'Student',
|
||||||
|
id: '123'
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UserType', () => {
|
||||||
|
test('creates a user with name and id', () => {
|
||||||
|
|
||||||
|
expect(user.name).toBe('Student');
|
||||||
|
expect(user.id).toBe('123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Modal.test.tsx
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';
|
||||||
|
|
||||||
|
describe('ConfirmDialog Component', () => {
|
||||||
|
const mockOnConfirm = jest.fn();
|
||||||
|
const mockOnCancel = jest.fn();
|
||||||
|
const mockOnOptionalInputChange = jest.fn();
|
||||||
|
|
||||||
|
const sampleProps = {
|
||||||
|
open: true,
|
||||||
|
title: 'Sample Modal Title',
|
||||||
|
message: 'Sample Modal Message',
|
||||||
|
onConfirm: mockOnConfirm,
|
||||||
|
onCancel: mockOnCancel,
|
||||||
|
hasOptionalInput: true,
|
||||||
|
optionalInputValue: 'Optional Input Value',
|
||||||
|
onOptionalInputChange: mockOnOptionalInputChange
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<ConfirmDialog {...sampleProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
expect(screen.getByText('Sample Modal Title')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Sample Modal Message')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const optionalInput = screen.getByTestId('optional-input') as HTMLInputElement;
|
||||||
|
expect(optionalInput).toBeInTheDocument();
|
||||||
|
expect(optionalInput.value).toBe('Optional Input Value');
|
||||||
|
|
||||||
|
expect(screen.getByText('Confirmer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Annuler')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onConfirm callback when "Confirmer" button is clicked', () => {
|
||||||
|
const confirmButton = screen.getByText('Confirmer');
|
||||||
|
fireEvent.click(confirmButton);
|
||||||
|
expect(mockOnConfirm).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onCancel callback when "Annuler" button is clicked', () => {
|
||||||
|
const cancelButton = screen.getByText('Annuler');
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
expect(mockOnCancel).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onOptionalInputChange callback when optional input changes', () => {
|
||||||
|
const optionalInput = screen.getByTestId('optional-input') as HTMLInputElement;
|
||||||
|
fireEvent.change(optionalInput, { target: { value: 'Updated Value' } });
|
||||||
|
expect(mockOnOptionalInputChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
75
client/src/__tests__/components/Editor/Editor.test.tsx
Normal file
75
client/src/__tests__/components/Editor/Editor.test.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Editor.test.tsx
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import Editor from '../../../components/Editor/Editor';
|
||||||
|
|
||||||
|
describe('Editor Component', () => {
|
||||||
|
const mockOnEditorChange = jest.fn();
|
||||||
|
|
||||||
|
const sampleProps = {
|
||||||
|
initialValue: 'Sample Initial Value',
|
||||||
|
onEditorChange: mockOnEditorChange
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<Editor {...sampleProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with initial value', () => {
|
||||||
|
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||||
|
expect(editorTextarea).toBeInTheDocument();
|
||||||
|
expect(editorTextarea.value).toBe('Sample Initial Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onEditorChange callback when editor value changes', () => {
|
||||||
|
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||||
|
fireEvent.change(editorTextarea, { target: { value: 'Updated Value' } });
|
||||||
|
expect(mockOnEditorChange).toHaveBeenCalledWith('Updated Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates editor value when initialValue prop changes', () => {
|
||||||
|
const updatedProps = {
|
||||||
|
initialValue: 'Updated Initial Value',
|
||||||
|
onEditorChange: mockOnEditorChange
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Editor {...updatedProps} />);
|
||||||
|
|
||||||
|
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||||
|
const editorTextarea = editorTextareas[1];
|
||||||
|
|
||||||
|
expect(editorTextarea.value).toBe('Updated Initial Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call change text with the correct value on textarea change', () => {
|
||||||
|
const updatedProps = {
|
||||||
|
initialValue: 'Updated Initial Value',
|
||||||
|
onEditorChange: mockOnEditorChange
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Editor {...updatedProps} />);
|
||||||
|
|
||||||
|
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||||
|
const editorTextarea = editorTextareas[1];
|
||||||
|
fireEvent.change(editorTextarea, { target: { value: 'New value' } });
|
||||||
|
|
||||||
|
expect(editorTextarea.value).toBe('New value');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call onEditorChange with an empty string if textarea value is falsy', () => {
|
||||||
|
const updatedProps = {
|
||||||
|
initialValue: 'Updated Initial Value',
|
||||||
|
onEditorChange: mockOnEditorChange
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Editor {...updatedProps} />);
|
||||||
|
|
||||||
|
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||||
|
const editorTextarea = editorTextareas[1];
|
||||||
|
fireEvent.change(editorTextarea, { target: { value: '' } });
|
||||||
|
|
||||||
|
expect(editorTextarea.value).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
|
||||||
|
|
||||||
|
describe('GIFTTemplatePreview Component', () => {
|
||||||
|
test('renders error message when questions contain invalid syntax', () => {
|
||||||
|
render(<GIFTTemplatePreview questions={['Invalid GIFT syntax']} />);
|
||||||
|
const errorMessage = screen.findByText(/Erreur inconnue/i, {}, { timeout: 5000 });
|
||||||
|
expect(errorMessage).resolves.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
test('renders preview when valid questions are provided', () => {
|
||||||
|
const questions = [
|
||||||
|
'Question 1 { A | B | C }',
|
||||||
|
'Question 2 { D | E | F }',
|
||||||
|
];
|
||||||
|
render(<GIFTTemplatePreview questions={questions} />);
|
||||||
|
const previewContainer = screen.getByTestId('preview-container');
|
||||||
|
expect(previewContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
test('hides answers when hideAnswers prop is true', () => {
|
||||||
|
const questions = [
|
||||||
|
'Question 1 { A | B | C }',
|
||||||
|
'Question 2 { D | E | F }',
|
||||||
|
];
|
||||||
|
render(<GIFTTemplatePreview questions={questions} hideAnswers />);
|
||||||
|
const previewContainer = screen.getByTestId('preview-container');
|
||||||
|
expect(previewContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('renders images correctly', () => {
|
||||||
|
const questions = [
|
||||||
|
'Question 1',
|
||||||
|
'<img src="image1.jpg" alt="Image 1">',
|
||||||
|
'Question 2',
|
||||||
|
'<img src="image2.jpg" alt="Image 2">',
|
||||||
|
];
|
||||||
|
const { getByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||||
|
const image1 = getByAltText('Image 1');
|
||||||
|
const image2 = getByAltText('Image 2');
|
||||||
|
expect(image1).toBeInTheDocument();
|
||||||
|
expect(image2).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('renders non-images correctly', () => {
|
||||||
|
const questions = ['Question 1', 'Question 2'];
|
||||||
|
const { queryByAltText } = render(<GIFTTemplatePreview questions={questions} />);
|
||||||
|
const image1 = queryByAltText('Image 1');
|
||||||
|
const image2 = queryByAltText('Image 2');
|
||||||
|
expect(image1).toBeNull();
|
||||||
|
expect(image2).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
//color.test.tsx
|
||||||
|
import { colors } from "../../../../components/GiftTemplate/constants";
|
||||||
|
|
||||||
|
describe('Colors object', () => {
|
||||||
|
test('All colors are defined', () => {
|
||||||
|
expect(colors.red100).toBeDefined();
|
||||||
|
expect(colors.red300).toBeDefined();
|
||||||
|
expect(colors.red700).toBeDefined();
|
||||||
|
expect(colors.redGray800).toBeDefined();
|
||||||
|
expect(colors.beige100).toBeDefined();
|
||||||
|
expect(colors.beige300).toBeDefined();
|
||||||
|
expect(colors.beige400).toBeDefined();
|
||||||
|
expect(colors.beige500).toBeDefined();
|
||||||
|
expect(colors.beige600).toBeDefined();
|
||||||
|
expect(colors.beige900).toBeDefined();
|
||||||
|
expect(colors.beigeGray800).toBeDefined();
|
||||||
|
expect(colors.green100).toBeDefined();
|
||||||
|
expect(colors.green300).toBeDefined();
|
||||||
|
expect(colors.green400).toBeDefined();
|
||||||
|
expect(colors.green500).toBeDefined();
|
||||||
|
expect(colors.green600).toBeDefined();
|
||||||
|
expect(colors.green700).toBeDefined();
|
||||||
|
expect(colors.greenGray500).toBeDefined();
|
||||||
|
expect(colors.greenGray600).toBeDefined();
|
||||||
|
expect(colors.greenGray700).toBeDefined();
|
||||||
|
expect(colors.teal400).toBeDefined();
|
||||||
|
expect(colors.teal500).toBeDefined();
|
||||||
|
expect(colors.teal600).toBeDefined();
|
||||||
|
expect(colors.teal700).toBeDefined();
|
||||||
|
expect(colors.blue).toBe('#5271FF');
|
||||||
|
expect(colors.success).toBe('hsl(120, 39%, 54%)');
|
||||||
|
expect(colors.danger).toBe('hsl(2, 64%, 58%)');
|
||||||
|
expect(colors.white).toBe('hsl(0, 0%, 100%)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
//styles.test.tsx
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants';
|
||||||
|
|
||||||
|
describe('ParagraphStyle', () => {
|
||||||
|
test('applies styles correctly', () => {
|
||||||
|
const theme = 'light';
|
||||||
|
const paragraphText = 'Test paragraph';
|
||||||
|
|
||||||
|
const styles = ParagraphStyle(theme);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<p style={convertStylesToObject(styles)}>{paragraphText}</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
const paragraphElement = container.firstChild;
|
||||||
|
|
||||||
|
expect(paragraphElement).toHaveStyle(`color: rgb(0, 0, 0);`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function convertStylesToObject(styles: string): React.CSSProperties {
|
||||||
|
const styleObject: React.CSSProperties = {};
|
||||||
|
styles.split(';').forEach((style) => {
|
||||||
|
const [property, value] = style.split(':');
|
||||||
|
if (property && value) {
|
||||||
|
(styleObject as any)[property.trim()] = value.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return styleObject;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { theme } from '../../../../components/GiftTemplate/constants/theme';
|
||||||
|
import { colors } from '../../../../components/GiftTemplate/constants/colors';
|
||||||
|
|
||||||
|
describe('Theme', () => {
|
||||||
|
test('returns correct light color', () => {
|
||||||
|
const lightColor = theme('light', 'gray500', 'black500');
|
||||||
|
expect(lightColor).toBe(colors.gray500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns correct dark color', () => {
|
||||||
|
const darkColor = theme('dark', 'gray500', 'black500');
|
||||||
|
expect(darkColor).toBe(colors.black500);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
|
||||||
|
|
||||||
|
describe('AnswerIcon', () => {
|
||||||
|
test('renders correct icon when correct is true', () => {
|
||||||
|
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />);
|
||||||
|
const svgElement = container.querySelector('svg');
|
||||||
|
|
||||||
|
expect(svgElement).toBeInTheDocument();
|
||||||
|
expect(svgElement).toHaveStyle(`
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
width: 1em;
|
||||||
|
color: rgb(92, 92, 92);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders incorrect icon when correct is false', () => {
|
||||||
|
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />);
|
||||||
|
const svgElement = container.querySelector('svg');
|
||||||
|
|
||||||
|
expect(svgElement).toBeInTheDocument();
|
||||||
|
expect(svgElement).toHaveStyle(`
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
width: 0.75em;
|
||||||
|
color: rgb(79, 216, 79);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import DragAndDrop from '../../../components/ImportModal/ImportModal';
|
||||||
|
|
||||||
|
describe('DragAndDrop Component', () => {
|
||||||
|
|
||||||
|
it('renders without errors', () => {
|
||||||
|
const handleOnClose = jest.fn();
|
||||||
|
const handleOnImport = jest.fn();
|
||||||
|
const open = true;
|
||||||
|
render(
|
||||||
|
<DragAndDrop
|
||||||
|
handleOnClose={handleOnClose}
|
||||||
|
handleOnImport={handleOnImport}
|
||||||
|
open={open}
|
||||||
|
selectedFolder="selectedFolder"/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Importation de quiz')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test('handles drag and drop', () => {
|
||||||
|
const handleOnCloseMock = jest.fn();
|
||||||
|
const handleOnImportMock = jest.fn();
|
||||||
|
render(
|
||||||
|
<DragAndDrop
|
||||||
|
handleOnClose={handleOnCloseMock}
|
||||||
|
handleOnImport={handleOnImportMock}
|
||||||
|
open={true}
|
||||||
|
selectedFolder="selectedFolder"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const dropZone = screen.getByText(/Déposer des fichiers ici/i);
|
||||||
|
fireEvent.dragEnter(dropZone);
|
||||||
|
fireEvent.dragOver(dropZone);
|
||||||
|
fireEvent.drop(dropZone, { dataTransfer: { files: [new File([''], 'sample.txt')] } });
|
||||||
|
expect(screen.getByText('📄')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('sample.txt')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('handles cancel button correctly', () => {
|
||||||
|
const handleOnClose = jest.fn();
|
||||||
|
const handleOnImport = jest.fn();
|
||||||
|
const open = true;
|
||||||
|
const { container } = render(
|
||||||
|
<DragAndDrop handleOnClose={handleOnClose} handleOnImport={handleOnImport} open={open}
|
||||||
|
selectedFolder="selectedFolder" />
|
||||||
|
);
|
||||||
|
const file = new File(['file content'], 'example.txt', { type: 'text/plain' });
|
||||||
|
fireEvent.change(screen.getByText( /cliquez pour ouvrir l'explorateur des fichiers/i), {
|
||||||
|
target: { files: [file] },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText('Annuler'));
|
||||||
|
expect(container.querySelector('.file-container')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('handles import correctly', async () => {
|
||||||
|
const handleOnCloseMock = jest.fn();
|
||||||
|
const handleOnImportMock = jest.fn();
|
||||||
|
render(
|
||||||
|
<DragAndDrop
|
||||||
|
handleOnClose={handleOnCloseMock}
|
||||||
|
handleOnImport={handleOnImportMock}
|
||||||
|
open={true}
|
||||||
|
selectedFolder="selectedFolder"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const file = new File(['file content'], 'example.txt', { type: 'text/plain' });
|
||||||
|
fireEvent.change(screen.getByText( /Importer/i), {
|
||||||
|
target: { files: [file] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';
|
||||||
|
|
||||||
|
// Mock the functions passed as props
|
||||||
|
const mockHandleOnClose = jest.fn();
|
||||||
|
const mockLaunchQuiz = jest.fn();
|
||||||
|
const mockSetQuizMode = jest.fn();
|
||||||
|
|
||||||
|
const renderComponent = (open: boolean) => {
|
||||||
|
render(
|
||||||
|
<LaunchQuizDialog
|
||||||
|
open={open}
|
||||||
|
handleOnClose={mockHandleOnClose}
|
||||||
|
launchQuiz={mockLaunchQuiz}
|
||||||
|
setQuizMode={mockSetQuizMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LaunchQuizDialog', () => {
|
||||||
|
it('renders with correct title', () => {
|
||||||
|
renderComponent(true);
|
||||||
|
expect(screen.getByText('Options de lancement du quiz')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders radio buttons for teacher and student modes', () => {
|
||||||
|
renderComponent(true);
|
||||||
|
expect(screen.getByLabelText('Rythme du professeur')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Rythme de l\'étudiant')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls handleOnClose when "Annuler" button is clicked', () => {
|
||||||
|
renderComponent(true);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Annuler'));
|
||||||
|
|
||||||
|
expect(mockHandleOnClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls launchQuiz when "Lancer" button is clicked', () => {
|
||||||
|
renderComponent(true);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Lancer'));
|
||||||
|
|
||||||
|
expect(mockLaunchQuiz).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render when open is false', () => {
|
||||||
|
renderComponent(false);
|
||||||
|
expect(screen.queryByText('Options de lancement du quiz')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||||
|
|
||||||
|
describe('LoadingCircle', () => {
|
||||||
|
it('displays the provided text correctly', () => {
|
||||||
|
const text = 'Veuillez attendre la connexion au serveur...';
|
||||||
|
render(<LoadingCircle text={text} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(text)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
|
||||||
|
|
||||||
|
describe('MultipleChoiceQuestion', () => {
|
||||||
|
const mockHandleOnSubmitAnswer = jest.fn();
|
||||||
|
const choices = [
|
||||||
|
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' } },
|
||||||
|
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' } }
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(
|
||||||
|
<MultipleChoiceQuestion
|
||||||
|
globalFeedback="feedback"
|
||||||
|
questionTitle="Test Question"
|
||||||
|
choices={choices}
|
||||||
|
handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the question and choices', () => {
|
||||||
|
expect(screen.getByText('Test Question')).toBeInTheDocument();
|
||||||
|
choices.forEach((choice) => {
|
||||||
|
expect(screen.getByText(choice.text.text)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not submit when no answer is selected', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submits the selected answer', () => {
|
||||||
|
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||||
|
if (!choiceButton) throw new Error('Choice button not found');
|
||||||
|
fireEvent.click(choiceButton);
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// NumericalQuestion.test.tsx
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion';
|
||||||
|
|
||||||
|
describe('NumericalQuestion Component', () => {
|
||||||
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
||||||
|
const sampleProps = {
|
||||||
|
questionTitle: 'Sample Question',
|
||||||
|
correctAnswers: {
|
||||||
|
numberHigh: 10,
|
||||||
|
numberLow: 5,
|
||||||
|
type: 'high-low'
|
||||||
|
},
|
||||||
|
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||||
|
showAnswer: false
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<NumericalQuestion {...sampleProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('number-input')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input change correctly', () => {
|
||||||
|
const inputElement = screen.getByTestId('number-input') as HTMLInputElement;
|
||||||
|
|
||||||
|
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||||
|
|
||||||
|
expect(inputElement.value).toBe('7');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Submit button should be disable if nothing is entered', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not submited answer if nothing is entered', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits answer correctly', () => {
|
||||||
|
const inputElement = screen.getByTestId('number-input');
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
140
client/src/__tests__/components/Questions/Question.test.tsx
Normal file
140
client/src/__tests__/components/Questions/Question.test.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
// Question.test.tsx
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import Questions from '../../../components/Questions/Question';
|
||||||
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
|
||||||
|
//
|
||||||
|
describe('Questions Component', () => {
|
||||||
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
||||||
|
const sampleTrueFalseQuestion: GIFTQuestion = {
|
||||||
|
type: 'TF',
|
||||||
|
stem: { format: 'plain', text: 'Sample True/False Question' },
|
||||||
|
isTrue: true,
|
||||||
|
incorrectFeedback: null,
|
||||||
|
correctFeedback: null,
|
||||||
|
title: 'True/False Question',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleMultipleChoiceQuestion: GIFTQuestion = {
|
||||||
|
type: 'MC',
|
||||||
|
stem: { format: 'plain', text: 'Sample Multiple Choice Question' },
|
||||||
|
title: 'Multiple Choice Question',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' }, weight: 1 },
|
||||||
|
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' }, weight: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleNumericalQuestion: GIFTQuestion = {
|
||||||
|
type: 'Numerical',
|
||||||
|
stem: { format: 'plain', text: 'Sample Numerical Question' },
|
||||||
|
title: 'Numerical Question',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: { numberHigh: 10, numberLow: 5, type: 'high-low' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleShortAnswerQuestion: GIFTQuestion = {
|
||||||
|
type: 'Short',
|
||||||
|
stem: { format: 'plain', text: 'Sample short answer question' },
|
||||||
|
title: 'Short Answer Question Title',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
feedback: { format: 'html', text: 'Correct answer feedback' },
|
||||||
|
isCorrect: true,
|
||||||
|
text: { format: 'html', text: 'Correct Answer' },
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feedback: { format: 'html', text: 'Incorrect answer feedback' },
|
||||||
|
isCorrect: false,
|
||||||
|
text: { format: 'html', text: 'Incorrect Answer' },
|
||||||
|
weight: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (question: GIFTQuestion) => {
|
||||||
|
render(<Questions question={question} handleOnSubmitAnswer={mockHandleSubmitAnswer} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders correctly for True/False question', () => {
|
||||||
|
renderComponent(sampleTrueFalseQuestion);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample True/False Question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Vrai')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Faux')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly for Multiple Choice question', () => {
|
||||||
|
renderComponent(sampleMultipleChoiceQuestion);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Multiple Choice Question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Choice 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Choice 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles selection and submission for Multiple Choice question', () => {
|
||||||
|
renderComponent(sampleMultipleChoiceQuestion);
|
||||||
|
|
||||||
|
const choiceButton = screen.getByText('Choice 1').closest('button')!;
|
||||||
|
fireEvent.click(choiceButton);
|
||||||
|
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly for Numerical question', () => {
|
||||||
|
renderComponent(sampleNumericalQuestion);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Numerical Question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('number-input')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input and submission for Numerical question', () => {
|
||||||
|
renderComponent(sampleNumericalQuestion);
|
||||||
|
|
||||||
|
const inputElement = screen.getByTestId('number-input') as HTMLInputElement;
|
||||||
|
fireEvent.change(inputElement, { target: { value: '7' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly for Short Answer question', () => {
|
||||||
|
renderComponent(sampleShortAnswerQuestion);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample short answer question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('text-input')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input and submission for Short Answer question', () => {
|
||||||
|
renderComponent(sampleShortAnswerQuestion);
|
||||||
|
|
||||||
|
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||||
|
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// ShortAnswerQuestion.test.tsx
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
|
||||||
|
|
||||||
|
describe('ShortAnswerQuestion Component', () => {
|
||||||
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
||||||
|
const sampleProps = {
|
||||||
|
questionTitle: 'Sample Question',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
feedback: {
|
||||||
|
format: 'text',
|
||||||
|
text: 'Correct answer feedback'
|
||||||
|
},
|
||||||
|
isCorrect: true,
|
||||||
|
text: {
|
||||||
|
format: 'text',
|
||||||
|
text: 'Correct Answer'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feedback: null,
|
||||||
|
isCorrect: false,
|
||||||
|
text: {
|
||||||
|
format: 'text',
|
||||||
|
text: 'Incorrect Answer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||||
|
showAnswer: false
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<ShortAnswerQuestion {...sampleProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('text-input')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input change correctly', () => {
|
||||||
|
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||||
|
|
||||||
|
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||||
|
|
||||||
|
expect(inputElement.value).toBe('User Input');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Submit button should be disable if nothing is entered', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not submited answer if nothing is entered', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits answer correctly', () => {
|
||||||
|
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.change(inputElement, { target: { value: 'User Input' } });
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// TrueFalseQuestion.test.tsx
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';
|
||||||
|
|
||||||
|
describe('TrueFalseQuestion Component', () => {
|
||||||
|
const mockHandleSubmitAnswer = jest.fn();
|
||||||
|
|
||||||
|
const sampleProps = {
|
||||||
|
questionTitle: 'Sample True/False Question',
|
||||||
|
correctAnswer: true,
|
||||||
|
handleOnSubmitAnswer: mockHandleSubmitAnswer,
|
||||||
|
showAnswer: false
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<TrueFalseQuestion {...sampleProps} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
expect(screen.getByText('Sample True/False Question')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('Vrai')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Faux')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Submit button should be disabled if no option is selected', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not submit answer if no option is selected', () => {
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits answer correctly for True', () => {
|
||||||
|
const trueButton = screen.getByText('Vrai');
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.click(trueButton);
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits answer correctly for False', () => {
|
||||||
|
const falseButton = screen.getByText('Faux');
|
||||||
|
const submitButton = screen.getByText('Répondre');
|
||||||
|
|
||||||
|
fireEvent.click(falseButton);
|
||||||
|
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ReturnButton', () => {
|
||||||
|
test('navigates back when askConfirm is false', () => {
|
||||||
|
const navigateMock = jest.fn();
|
||||||
|
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||||
|
render(<ReturnButton askConfirm={false} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows confirmation modal when askConfirm is true', () => {
|
||||||
|
render(<ReturnButton askConfirm={true} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||||
|
expect(confirmButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*test('navigates back after confirming in the modal', () => {
|
||||||
|
const navigateMock = jest.fn();
|
||||||
|
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||||
|
render(<ReturnButton askConfirm={true} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||||
|
fireEvent.click(confirmButton);
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||||
|
});*/
|
||||||
|
|
||||||
|
test('cancels navigation when canceling in the modal', () => {
|
||||||
|
const navigateMock = jest.fn();
|
||||||
|
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||||
|
render(<ReturnButton askConfirm={true} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
fireEvent.click(screen.getByText('Annuler'));
|
||||||
|
expect(navigateMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Importez le type UserType s'il n'est pas déjà importé
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import UserWaitPage from '../../../components/UserWaitPage/UserWaitPage';
|
||||||
|
|
||||||
|
describe('UserWaitPage Component', () => {
|
||||||
|
const mockUsers = [
|
||||||
|
{ id: '1', name: 'User1' },
|
||||||
|
{ id: '2', name: 'User2' },
|
||||||
|
{ id: '3', name: 'User3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockProps = {
|
||||||
|
users: mockUsers,
|
||||||
|
launchQuiz: jest.fn(),
|
||||||
|
roomName: 'Test Room',
|
||||||
|
setQuizMode: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
test('renders UserWaitPage with correct content', () => {
|
||||||
|
render(<UserWaitPage {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Salle: Test Room/)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const launchButton = screen.getByRole('button', { name: /Lancer/i });
|
||||||
|
expect(launchButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
mockUsers.forEach((user) => {
|
||||||
|
expect(screen.getByText(user.name)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking on "Lancer" button opens LaunchQuizDialog', () => {
|
||||||
|
render(<UserWaitPage {...mockProps} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /Lancer/i }));
|
||||||
|
|
||||||
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
35
client/src/__tests__/pages/Home/Home.test.tsx
Normal file
35
client/src/__tests__/pages/Home/Home.test.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import Home from '../../../pages/Home/Home';
|
||||||
|
|
||||||
|
describe('Home', () => {
|
||||||
|
it('renders Home component with page title and buttons', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<Home />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Espace\s*étudiant/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Espace\s*enseignant/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigates to the correct routes when student and teacher buttons are clicked', () => {
|
||||||
|
render(
|
||||||
|
<BrowserRouter>
|
||||||
|
<Home />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
const studentButton = screen.getByText(/Espace\s*étudiant/);
|
||||||
|
expect(studentButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(studentButton);
|
||||||
|
expect(window.location.pathname).toBe('/student/join-room');
|
||||||
|
|
||||||
|
const teacherButton = screen.getByText(/Espace\s*enseignant/);
|
||||||
|
expect(teacherButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(teacherButton);
|
||||||
|
expect(window.location.pathname).toBe('/teacher/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { QuestionType } from '../../../../Types/QuestionType';
|
||||||
|
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||||
|
|
||||||
|
describe('StudentModeQuiz', () => {
|
||||||
|
const mockQuestions: QuestionType[] = [
|
||||||
|
{
|
||||||
|
question: {
|
||||||
|
id: '1',
|
||||||
|
type: 'MC',
|
||||||
|
stem: { format: 'plain', text: 'Sample Question 1' },
|
||||||
|
title: 'Sample Question 1',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||||
|
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
image: '<img src="sample-image-url" alt="Sample Image" />',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: {
|
||||||
|
id: '2',
|
||||||
|
type: 'TF',
|
||||||
|
stem: { format: 'plain', text: 'Sample Question 2' },
|
||||||
|
isTrue: true,
|
||||||
|
incorrectFeedback: null,
|
||||||
|
correctFeedback: null,
|
||||||
|
title: 'Question 2',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
},
|
||||||
|
image: 'sample-image-url-2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockSubmitAnswer = jest.fn();
|
||||||
|
const mockDisconnectWebSocket = jest.fn();
|
||||||
|
|
||||||
|
|
||||||
|
test('renders the initial question', async () => {
|
||||||
|
render(
|
||||||
|
<StudentModeQuiz
|
||||||
|
questions={mockQuestions}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Déconnexion')).toBeInTheDocument();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles answer submission text', () => {
|
||||||
|
|
||||||
|
render(
|
||||||
|
<StudentModeQuiz
|
||||||
|
questions={mockQuestions}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Option A'));
|
||||||
|
fireEvent.click(screen.getByText('Répondre'));
|
||||||
|
|
||||||
|
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles disconnect button click', () => {
|
||||||
|
render(
|
||||||
|
<StudentModeQuiz
|
||||||
|
questions={mockQuestions}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByText('Déconnexion'));
|
||||||
|
|
||||||
|
expect(mockDisconnectWebSocket).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to the next question', () => {
|
||||||
|
render(
|
||||||
|
<StudentModeQuiz
|
||||||
|
questions={mockQuestions}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Option A'));
|
||||||
|
fireEvent.click(screen.getByText('Répondre'));
|
||||||
|
fireEvent.click(screen.getByText('Question suivante'));
|
||||||
|
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('T')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates to the previous question', () => {
|
||||||
|
|
||||||
|
render(
|
||||||
|
<StudentModeQuiz
|
||||||
|
questions={mockQuestions}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Option A'));
|
||||||
|
fireEvent.click(screen.getByText('Répondre'));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Question précédente'));
|
||||||
|
|
||||||
|
|
||||||
|
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
//TeacherModeQuiz.test.tsx
|
||||||
|
import { render, screen, fireEvent} from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
|
||||||
|
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||||
|
|
||||||
|
describe('TeacherModeQuiz', () => {
|
||||||
|
const mockQuestion: GIFTQuestion = {
|
||||||
|
id: '1',
|
||||||
|
type: 'MC',
|
||||||
|
stem: { format: 'plain', text: 'Sample Question' },
|
||||||
|
title: 'Sample Question',
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
|
||||||
|
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSubmitAnswer = jest.fn();
|
||||||
|
const mockDisconnectWebSocket = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(
|
||||||
|
<TeacherModeQuiz
|
||||||
|
questionInfos={{ question: mockQuestion, image: 'sample-image-url' }}
|
||||||
|
submitAnswer={mockSubmitAnswer}
|
||||||
|
disconnectWebSocket={mockDisconnectWebSocket}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the initial question', () => {
|
||||||
|
expect(screen.getByText('Question 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Sample Question')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Option B')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Déconnexion')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles answer submission and displays wait text', () => {
|
||||||
|
fireEvent.click(screen.getByText('Option A'));
|
||||||
|
fireEvent.click(screen.getByText('Répondre'));
|
||||||
|
|
||||||
|
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
|
||||||
|
expect(screen.getByText('En attente pour la prochaine question...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles disconnect button click', () => {
|
||||||
|
fireEvent.click(screen.getByText('Déconnexion'));
|
||||||
|
|
||||||
|
expect(mockDisconnectWebSocket).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import Dashboard from '../../../../pages/Teacher/Dashboard/Dashboard';
|
||||||
|
|
||||||
|
const localStorageMock = (() => {
|
||||||
|
let store: Record<string, string> = {};
|
||||||
|
return {
|
||||||
|
getItem: (key: string) => store[key] || null,
|
||||||
|
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
||||||
|
clear: () => (store = {}),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
describe('Dashboard Component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.setItem('quizzes', JSON.stringify([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders Dashboard with default state', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Dashboard />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Tableau de bord/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds a quiz and checks if it is displayed', () => {
|
||||||
|
const mockQuizzes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Sample Quiz',
|
||||||
|
questions: ['Question 1?', 'Question 2?'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
localStorage.setItem('quizzes', JSON.stringify(mockQuizzes));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Dashboard />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Sample Quiz/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens ImportModal when "Importer" button is clicked', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<Dashboard />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText(/Importer/i));
|
||||||
|
|
||||||
|
expect(screen.getByText(/Importation de quiz/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import QuizForm from '../../../../pages/Teacher/EditorQuiz/EditorQuiz';
|
||||||
|
import { waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
const localStorageMock = (() => {
|
||||||
|
let store: Record<string, string> = {};
|
||||||
|
return {
|
||||||
|
getItem: (key: string) => store[key] || null,
|
||||||
|
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
||||||
|
clear: () => (store = {}),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('QuizForm Component', () => {
|
||||||
|
test('renders QuizForm with default state for a new quiz', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/teacher/editor-quiz/new']}>
|
||||||
|
<QuizForm />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Éditeur')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Prévisualisation')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders QuizForm for a new quiz', async () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/teacher/editor-quiz']}>
|
||||||
|
<QuizForm />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Éditeur de quiz/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editorTextArea = screen.getByRole('textbox');
|
||||||
|
fireEvent.change(editorTextArea, { target: { value: 'Sample question?' } });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const sampleQuestionElements = screen.queryAllByText(/Sample question\?/i);
|
||||||
|
expect(sampleQuestionElements.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveButton = screen.getByText(/Enregistrer/i);
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Sauvegarder le questionnaire/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
39
client/src/__tests__/services/QuestionService.test.tsx
Normal file
39
client/src/__tests__/services/QuestionService.test.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { QuestionService } from "../../services/QuestionService";
|
||||||
|
|
||||||
|
describe('QuestionService', () => {
|
||||||
|
describe('getImage', () => {
|
||||||
|
it('should return empty string for text without image tag', () => {
|
||||||
|
const text = 'This is a sample text without an image tag.';
|
||||||
|
const imageUrl = QuestionService.getImage(text);
|
||||||
|
expect(imageUrl).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the image tag from the text', () => {
|
||||||
|
const text = 'This is a sample text with an <img src="image.jpg" alt="Sample Image" /> tag.';
|
||||||
|
const imageUrl = QuestionService.getImage(text);
|
||||||
|
expect(imageUrl).toBe('<img src="image.jpg" alt="Sample Image" />');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getImageSource', () => {
|
||||||
|
it('should return the image source from the image tag in the text', () => {
|
||||||
|
const text = '<img src="image.jpg" alt="Sample Image" />';
|
||||||
|
const imageUrl = QuestionService.getImageSource(text);
|
||||||
|
expect(imageUrl).toBe('src="image.jpg" alt="Sample Image" /');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ignoreImgTags', () => {
|
||||||
|
it('should return the same text if it does not contain an image tag', () => {
|
||||||
|
const text = 'This is a sample text without an image tag.';
|
||||||
|
const result = QuestionService.ignoreImgTags(text);
|
||||||
|
expect(result).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the image tag from the text', () => {
|
||||||
|
const text = 'This is a sample text with an <img src="image.jpg" alt="Sample Image" /> tag.';
|
||||||
|
const result = QuestionService.ignoreImgTags(text);
|
||||||
|
expect(result).toBe('This is a sample text with an tag.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
65
client/src/__tests__/services/QuizService.test.tsx
Normal file
65
client/src/__tests__/services/QuizService.test.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*import { QuizService } from '../../services/QuizService';
|
||||||
|
import { QuizType } from '../../Types/QuizType';
|
||||||
|
|
||||||
|
// we need to mock localStorage for this test
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
global.window = {} as Window & typeof globalThis;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localStorageMock = (() => {
|
||||||
|
let store: { [key: string]: string } = {};
|
||||||
|
return {
|
||||||
|
getItem(key: string) {
|
||||||
|
return store[key] || null;
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
store[key] = value.toString();
|
||||||
|
},
|
||||||
|
removeItem(key: string) {
|
||||||
|
delete store[key];
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
store = {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'localStorage', {
|
||||||
|
value: localStorageMock
|
||||||
|
});
|
||||||
|
|
||||||
|
/*describe('QuizService', () => {
|
||||||
|
const mockQuizzes: QuizType[] = [
|
||||||
|
{ _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'] },
|
||||||
|
{ _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'] }
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageMock.setItem('quizzes', JSON.stringify(mockQuizzes));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorageMock.removeItem('quizzes');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return quiz for valid id', () => {
|
||||||
|
const quiz = QuizService.getQuizById('quiz1', localStorageMock);
|
||||||
|
expect(quiz).toEqual(mockQuizzes[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined for invalid id', () => {
|
||||||
|
const quiz = QuizService.getQuizById('nonexistent', localStorageMock);
|
||||||
|
expect(quiz).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined for undefined id', () => {
|
||||||
|
const quiz = QuizService.getQuizById(undefined, localStorageMock);
|
||||||
|
expect(quiz).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty localStorage', () => {
|
||||||
|
localStorageMock.removeItem('quizzes');
|
||||||
|
const quiz = QuizService.getQuizById('quiz1', localStorageMock);
|
||||||
|
expect(quiz).toBeUndefined();
|
||||||
|
});
|
||||||
|
});*/
|
||||||
88
client/src/__tests__/services/WebsocketService.test.tsx
Normal file
88
client/src/__tests__/services/WebsocketService.test.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
//WebsocketService.test.tsx
|
||||||
|
import WebsocketService from '../../services/WebsocketService';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import { ENV_VARIABLES } from '../../constants';
|
||||||
|
|
||||||
|
jest.mock('socket.io-client');
|
||||||
|
|
||||||
|
jest.mock('../../constants', () => ({
|
||||||
|
ENV_VARIABLES: {
|
||||||
|
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('WebSocketService', () => {
|
||||||
|
let mockSocket: Partial<Socket>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSocket = {
|
||||||
|
emit: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
connect: jest.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
(io as jest.Mock).mockReturnValue(mockSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('connect should initialize socket connection', () => {
|
||||||
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
expect(io).toHaveBeenCalled();
|
||||||
|
expect(WebsocketService['socket']).toBe(mockSocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disconnect should terminate socket connection', () => {
|
||||||
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
expect(WebsocketService['socket']).toBeTruthy();
|
||||||
|
WebsocketService.disconnect();
|
||||||
|
expect(mockSocket.disconnect).toHaveBeenCalled();
|
||||||
|
expect(WebsocketService['socket']).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createRoom should emit create-room event', () => {
|
||||||
|
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
WebsocketService.createRoom();
|
||||||
|
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nextQuestion should emit next-question event with correct parameters', () => {
|
||||||
|
const roomName = 'testRoom';
|
||||||
|
const question = { id: 1, text: 'Sample Question' };
|
||||||
|
|
||||||
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
WebsocketService.nextQuestion(roomName, question);
|
||||||
|
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('launchStudentModeQuiz should emit launch-student-mode event with correct parameters', () => {
|
||||||
|
const roomName = 'testRoom';
|
||||||
|
const questions = [{ id: 1, text: 'Sample Question' }];
|
||||||
|
|
||||||
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
WebsocketService.launchStudentModeQuiz(roomName, questions);
|
||||||
|
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
|
||||||
|
roomName,
|
||||||
|
questions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('endQuiz should emit end-quiz event with correct parameters', () => {
|
||||||
|
const roomName = 'testRoom';
|
||||||
|
|
||||||
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
WebsocketService.endQuiz(roomName);
|
||||||
|
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('joinRoom should emit join-room event with correct parameters', () => {
|
||||||
|
const enteredRoomName = 'testRoom';
|
||||||
|
const username = 'testUser';
|
||||||
|
|
||||||
|
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
|
||||||
|
WebsocketService.joinRoom(enteredRoomName, username);
|
||||||
|
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
|
||||||
|
});
|
||||||
|
});
|
||||||
77
client/src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
77
client/src/components/ConfirmDialog/ConfirmDialog.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Modal.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
hasOptionalInput?: boolean;
|
||||||
|
optionalInputValue?: string;
|
||||||
|
onOptionalInputChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
buttonOrderType?: 'normal' | 'warning';
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfirmDialog: React.FC<Props> = ({
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
hasOptionalInput,
|
||||||
|
optionalInputValue,
|
||||||
|
onOptionalInputChange,
|
||||||
|
buttonOrderType = 'normal'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel}>
|
||||||
|
<DialogTitle sx={{ fontWeight: 'bold', fontSize: 24 }}>{title}</DialogTitle>
|
||||||
|
<DialogContentText sx={{ padding: '0 1.5rem 0.5rem 1.5rem' }}>
|
||||||
|
{message}
|
||||||
|
</DialogContentText>
|
||||||
|
{hasOptionalInput && (
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
id="optional-input"
|
||||||
|
inputProps={{ 'data-testid': 'optional-input' }}
|
||||||
|
focused
|
||||||
|
fullWidth
|
||||||
|
value={optionalInputValue || ''}
|
||||||
|
onChange={onOptionalInputChange}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
)}
|
||||||
|
<DialogActions>
|
||||||
|
{buttonOrderType === 'normal' && (
|
||||||
|
<Button variant="outlined" onClick={onCancel}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant={buttonOrderType === 'normal' ? 'contained' : 'outlined'}
|
||||||
|
onClick={onConfirm}
|
||||||
|
data-testid="modal-confirm-button"
|
||||||
|
>
|
||||||
|
Confirmer
|
||||||
|
</Button>
|
||||||
|
{buttonOrderType === 'warning' && (
|
||||||
|
<Button variant="contained" onClick={onCancel}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmDialog;
|
||||||
66
client/src/components/DisconnectButton/DisconnectButton.tsx
Normal file
66
client/src/components/DisconnectButton/DisconnectButton.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// GoBackButton.tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { ChevronLeft } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onReturn?: () => void;
|
||||||
|
askConfirm?: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisconnectButton: React.FC<Props> = ({
|
||||||
|
askConfirm = false,
|
||||||
|
message = 'Êtes-vous sûr de vouloir quitter la page ?',
|
||||||
|
onReturn
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleOnReturnButtonClick = () => {
|
||||||
|
if (askConfirm) {
|
||||||
|
setShowDialog(true);
|
||||||
|
} else {
|
||||||
|
handleOnReturn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setShowDialog(false);
|
||||||
|
handleOnReturn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnReturn = () => {
|
||||||
|
if (!!onReturn) {
|
||||||
|
onReturn();
|
||||||
|
} else {
|
||||||
|
navigate(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='returnButton'>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
startIcon={<ChevronLeft />}
|
||||||
|
onClick={handleOnReturnButtonClick}
|
||||||
|
color="primary"
|
||||||
|
sx={{ marginLeft: '-0.5rem', fontSize: 16 }}
|
||||||
|
>
|
||||||
|
Quitter
|
||||||
|
</Button>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={showDialog}
|
||||||
|
title="Confirmer"
|
||||||
|
message={message}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onCancel={() => setShowDialog(false)}
|
||||||
|
buttonOrderType="warning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisconnectButton;
|
||||||
32
client/src/components/Editor/Editor.tsx
Normal file
32
client/src/components/Editor/Editor.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Editor.tsx
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import './editor.css';
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
initialValue: string;
|
||||||
|
onEditorChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange }) => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
|
function handleEditorChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
|
const text = event.target.value;
|
||||||
|
setValue(text);
|
||||||
|
onEditorChange(text || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
ref={editorRef}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
value={value}
|
||||||
|
className="editor"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Editor;
|
||||||
9
client/src/components/Editor/editor.css
Normal file
9
client/src/components/Editor/editor.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 50vh;
|
||||||
|
background-color: #f8f9ff;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: medium;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
25
client/src/components/Footer/Footer.tsx
Normal file
25
client/src/components/Footer/Footer.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import './footer.css';
|
||||||
|
|
||||||
|
interface FooterProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Footer: React.FC<FooterProps> = ({ }) => {
|
||||||
|
return (
|
||||||
|
<div className="footer">
|
||||||
|
<div className="footer-content">
|
||||||
|
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
||||||
|
</div>
|
||||||
|
<div className="footer-links">
|
||||||
|
<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>
|
||||||
|
<a href="https://github.com/louis-antoine-etsmtl/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
23
client/src/components/Footer/footer.css
Normal file
23
client/src/components/Footer/footer.css
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
.footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
153
client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
Normal file
153
client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// GiftCheatSheet.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import './giftCheatSheet.css';
|
||||||
|
|
||||||
|
const GiftCheatSheet: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="gift-cheat-sheet">
|
||||||
|
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||||
|
<span>
|
||||||
|
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||||
|
plateforme Moodle afin de générer les quizs. Ci-dessous vous pouvez retrouver la
|
||||||
|
syntaxe pour chaque type de question ainsi que les champs optionnels :
|
||||||
|
</span>
|
||||||
|
<div className="question-type">
|
||||||
|
<h4>1. Questions Vrai/Faux</h4>
|
||||||
|
<pre>
|
||||||
|
<code className="selectable-text">
|
||||||
|
{'2+2 \\= 4 ? {T\n}// Vous pouvez utiliser les valeurs {T}, {F}, {TRUE} et {FALSE}'}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4>2. Questions à choix multiple</h4>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{
|
||||||
|
'Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}// La bonne réponse est Ottawa'
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="question-type">
|
||||||
|
<h4>3. Questions à choix multiple avec plusieurs réponses</h4>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{
|
||||||
|
'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} //On utilise le signe ~ pour toutes les réponses. On doit indiquer le pourcentage de chaque réponse'
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4>4. Questions à reponse courte</h4>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{'Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}// Permet de fournir plusieurs bonnes réponses. Note: Les majuscules ne sont pas prises en compte.'}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 5. Questions numériques </h4>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{
|
||||||
|
'Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} //OU \nQuestion {#=PetitNombre..GrandNombre\n} // La tolérance est un pourcentage. La réponse doit être comprise entre PetitNombre et GrandNombre'
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 6. Paramètres optionnels </h4>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{'::Titre:: '}
|
||||||
|
<span className="code-comment selectable-text">
|
||||||
|
{' // Ajoute un titre à une question'}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
{'# Feedback '}
|
||||||
|
<span className="code-comment selectable-text">
|
||||||
|
{' // Feedback pour UNE réponse'}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
{'// Commentaire '}
|
||||||
|
<span className="code-comment selectable-text">
|
||||||
|
{' // Commentaire non apparent'}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
{'#### Feedback général '}
|
||||||
|
<span className="code-comment selectable-text">
|
||||||
|
{' // Feedback général pour une question'}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
{'%50% '}
|
||||||
|
<span className="code-comment selectable-text">
|
||||||
|
{" // Poids d'une réponse (peut être négatif)"}
|
||||||
|
</span>
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 7. Paramètres optionnels </h4>
|
||||||
|
<p>
|
||||||
|
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 \
|
||||||
|
devant:
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block selectable-text">
|
||||||
|
{'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 8. LaTeX </h4>
|
||||||
|
<p>
|
||||||
|
Le format LaTeX est supporté dans cette application. Vous devez cependant penser
|
||||||
|
à 'échapper' les caractères spéciaux mentionnés plus haut.
|
||||||
|
</p>
|
||||||
|
<p>Exemple d'équation:</p>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 9. inserer une image </h4>
|
||||||
|
<p>Pour insérer une image, vous devez utiliser la syntaxe suivante:</p>
|
||||||
|
<pre>
|
||||||
|
<code className="question-code-block">
|
||||||
|
{'<img '}
|
||||||
|
<span className="code-comment">{`un_URL_d_image`}</span>
|
||||||
|
{' >'}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<p style={{ color: 'red' }}>
|
||||||
|
Attention nous ne supportons pas encore les images en tant que réponses à une
|
||||||
|
question
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="question-type">
|
||||||
|
<h4> 10. Informations supplémentaires </h4>
|
||||||
|
<p>
|
||||||
|
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
||||||
|
application.
|
||||||
|
</p>
|
||||||
|
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>
|
||||||
|
<a href="https://ethan-ou.github.io/vscode-gift-docs/docs/questions">
|
||||||
|
Documentation de GIFT
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GiftCheatSheet;
|
||||||
37
client/src/components/GIFTCheatSheet/giftCheatSheet.css
Normal file
37
client/src/components/GIFTCheatSheet/giftCheatSheet.css
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
.gift-cheat-sheet {
|
||||||
|
width: 30vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #3a3a3a;
|
||||||
|
margin-bottom: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-type {
|
||||||
|
margin-bottom: 20;
|
||||||
|
}
|
||||||
|
.question-code-block,
|
||||||
|
.code-comment {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #ffffffbd;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.code-comment {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-type h4 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
86
client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
Normal file
86
client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// GIFTTemplatePreview.tsx
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Template, { ErrorTemplate } from './templates';
|
||||||
|
import { parse } from 'gift-pegjs';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
interface GIFTTemplatePreviewProps {
|
||||||
|
questions: string[];
|
||||||
|
hideAnswers?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
|
questions,
|
||||||
|
hideAnswers = false
|
||||||
|
}) => {
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [isPreviewReady, setIsPreviewReady] = useState(false);
|
||||||
|
const [items, setItems] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
let previewHTML = '';
|
||||||
|
questions.forEach((item) => {
|
||||||
|
const isImage = item.includes('<img');
|
||||||
|
if (isImage) {
|
||||||
|
const imageUrlMatch = item.match(/<img[^>]+>/i);
|
||||||
|
if (imageUrlMatch) {
|
||||||
|
let imageUrl = imageUrlMatch[0];
|
||||||
|
imageUrl = imageUrl.replace('img', 'img style="width:10vw;" src=');
|
||||||
|
item = item.replace(imageUrlMatch[0], '');
|
||||||
|
previewHTML += `${imageUrl}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedItem = parse(item);
|
||||||
|
previewHTML += Template(parsedItem[0], {
|
||||||
|
preview: true,
|
||||||
|
theme: 'light'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
previewHTML += ErrorTemplate(item + '\n' + error.message);
|
||||||
|
} else {
|
||||||
|
previewHTML += ErrorTemplate(item + '\n' + 'Erreur inconnue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previewHTML += '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hideAnswers) {
|
||||||
|
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
|
||||||
|
previewHTML = previewHTML.replace(svgRegex, '');
|
||||||
|
const placeholderRegex = /(placeholder=")[^"]*(")/gi;
|
||||||
|
previewHTML = previewHTML.replace(placeholderRegex, '$1$2');
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems(previewHTML);
|
||||||
|
setIsPreviewReady(true);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error.message);
|
||||||
|
} else {
|
||||||
|
setError('Une erreur est survenue durant le chargement de la prévisualisation.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [questions]);
|
||||||
|
|
||||||
|
const PreviewComponent = () => (
|
||||||
|
<React.Fragment>
|
||||||
|
{error ? (
|
||||||
|
<div className="error">{error}</div>
|
||||||
|
) : isPreviewReady ? (
|
||||||
|
<div data-testid="preview-container">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: items }}></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="loading">Chargement de la prévisualisation...</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <PreviewComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GIFTTemplatePreview;
|
||||||
52
client/src/components/GiftTemplate/constants/colors.ts
Normal file
52
client/src/components/GiftTemplate/constants/colors.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
export const colors = {
|
||||||
|
red100: 'hsl(0, 100%, 97%)',
|
||||||
|
red300: 'hsl(0, 53%, 90%)',
|
||||||
|
red700: 'hsl(0, 45%, 25%)',
|
||||||
|
redGray800: 'hsl(0, 23%, 18%)',
|
||||||
|
beige100: 'hsl(43, 100%, 94%)',
|
||||||
|
beige300: 'hsl(36, 84%, 93%)',
|
||||||
|
beige400: 'hsl(36, 39%, 75%)',
|
||||||
|
beige500: 'hsl(36, 50%, 50%)',
|
||||||
|
beige600: 'hsl(35, 51%, 33%)',
|
||||||
|
beige900: 'hsl(43, 95%, 9%)',
|
||||||
|
beigeGray800: 'hsl(43, 23%, 33%)',
|
||||||
|
green100: 'hsl(134, 68%, 95%)',
|
||||||
|
green300: 'hsl(134, 31%, 82%)',
|
||||||
|
green400: 'hsl(134, 31%, 75%)',
|
||||||
|
green500: 'hsl(134, 31%, 66%)',
|
||||||
|
green600: 'hsl(134, 31%, 44%)',
|
||||||
|
green700: 'hsl(134, 31%, 32%)',
|
||||||
|
greenGray500: 'hsl(134, 18%, 50%)',
|
||||||
|
greenGray600: 'hsl(134, 21%, 44%)',
|
||||||
|
greenGray700: 'hsl(134, 23%, 33%)',
|
||||||
|
teal400: 'hsl(180, 35%, 89%)',
|
||||||
|
teal500: 'hsl(180, 35%, 84%)',
|
||||||
|
teal600: 'hsl(180, 24%, 60%)',
|
||||||
|
teal700: 'hsl(180, 15%, 41%)',
|
||||||
|
cyan100: 'hsl(194, 55%, 98%)',
|
||||||
|
cyan200: 'hsl(194, 60%, 96%)',
|
||||||
|
cyan300: 'hsl(194, 65%, 92%)',
|
||||||
|
navy600: 'hsl(218, 17%, 35%)',
|
||||||
|
gray100: 'hsl(0, 0%, 95%)',
|
||||||
|
gray200: 'hsl(0, 0%, 88%)',
|
||||||
|
gray300: 'hsl(0, 0%, 81%)',
|
||||||
|
gray400: 'hsl(0, 0%, 74%)',
|
||||||
|
gray500: 'hsl(0, 0%, 67%)',
|
||||||
|
gray600: 'hsl(0, 0%, 60%)',
|
||||||
|
gray700: 'hsl(0, 0%, 53%)',
|
||||||
|
gray800: 'hsl(0, 0%, 46%)',
|
||||||
|
gray900: 'hsl(0, 0%, 39%)',
|
||||||
|
black100: 'hsl(0, 0%, 32%)',
|
||||||
|
black200: 'hsl(0, 0%, 28%)',
|
||||||
|
black300: 'hsl(0, 0%, 24%)',
|
||||||
|
black400: 'hsl(0, 0%, 20%)',
|
||||||
|
black500: 'hsl(0, 0%, 16%)',
|
||||||
|
black600: 'hsl(0, 0%, 12%)',
|
||||||
|
black700: 'hsl(0, 0%, 8%)',
|
||||||
|
black800: 'hsl(0, 0%, 4%)',
|
||||||
|
black900: 'hsl(0, 0%, 0%)',
|
||||||
|
blue: '#5271FF',
|
||||||
|
success: 'hsl(120, 39%, 54%)',
|
||||||
|
danger: 'hsl(2, 64%, 58%)',
|
||||||
|
white: 'hsl(0, 0%, 100%)'
|
||||||
|
};
|
||||||
3
client/src/components/GiftTemplate/constants/index.ts
Normal file
3
client/src/components/GiftTemplate/constants/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { colors } from './colors';
|
||||||
|
export { theme } from './theme';
|
||||||
|
export * from './styles';
|
||||||
58
client/src/components/GiftTemplate/constants/styles.ts
Normal file
58
client/src/components/GiftTemplate/constants/styles.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { ThemeType } from '../templates/types';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
export const ParagraphStyle = (t: ThemeType) => `
|
||||||
|
color: ${theme(t, 'black900', 'gray200')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TextAreaStyle = (t: ThemeType) => `
|
||||||
|
width: 100%;
|
||||||
|
height: 7rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${theme(t, 'black500', 'gray200')};
|
||||||
|
background-color: ${theme(t, 'white', 'black300')};
|
||||||
|
border: ${t === 'light' ? 1 : 1.5}px solid ${theme(t, 'gray300', 'gray900')};
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectStyle = (t: ThemeType) => `
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
white-space: pre;
|
||||||
|
cursor: default;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
text-transform: none;
|
||||||
|
min-width: 8rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${theme(t, 'black500', 'gray200')};
|
||||||
|
background-color: ${theme(t, 'white', 'black300')};
|
||||||
|
border: ${t === 'light' ? 1 : 1.5}px solid ${theme(t, 'gray300', 'gray900')};
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InputStyle = (t: ThemeType) => `
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${theme(t, 'black500', 'gray200')};
|
||||||
|
background-color: ${theme(t, 'white', 'black300')};
|
||||||
|
border: ${t === 'light' ? 1 : 2}px solid ${theme(t, 'gray300', 'gray900')};
|
||||||
|
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%;
|
||||||
|
`;
|
||||||
11
client/src/components/GiftTemplate/constants/theme.ts
Normal file
11
client/src/components/GiftTemplate/constants/theme.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { colors } from './colors';
|
||||||
|
|
||||||
|
type Color = keyof typeof colors;
|
||||||
|
|
||||||
|
export const theme = (theme: 'light' | 'dark', light: Color, dark: Color) => {
|
||||||
|
if (theme === 'light') {
|
||||||
|
return colors[light];
|
||||||
|
} else {
|
||||||
|
return colors[dark];
|
||||||
|
}
|
||||||
|
};
|
||||||
263
client/src/components/GiftTemplate/index.ts
Normal file
263
client/src/components/GiftTemplate/index.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import Template, { ErrorTemplate } from './templates';
|
||||||
|
import { GIFTQuestion } from './templates/types';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const multiple: GIFTQuestion[] = [
|
||||||
|
{
|
||||||
|
type: 'MC',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'markdown', text: "Who's buried in Grant's \r\n tomb?" },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'Not sure? There are many answers for this question so do not fret. Not sure? There are many answers for this question so do not fret.'
|
||||||
|
},
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: -50,
|
||||||
|
text: { format: 'moodle', text: 'Grant' },
|
||||||
|
feedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: 50,
|
||||||
|
text: { format: 'moodle', text: 'Jefferson' },
|
||||||
|
feedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: 50,
|
||||||
|
text: { format: 'moodle', text: 'no one' },
|
||||||
|
feedback: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'MC',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
|
||||||
|
hasEmbeddedAnswers: true,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'buried' },
|
||||||
|
feedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'entombed' },
|
||||||
|
feedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'living' },
|
||||||
|
feedback: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'TF',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'moodle', text: "Grant is buried in Grant's tomb." },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
isTrue: false,
|
||||||
|
incorrectFeedback: null,
|
||||||
|
correctFeedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Short',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'moodle', text: "Who's buried in Grant's tomb?" },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'no one " has got me' },
|
||||||
|
feedback: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'nobody' },
|
||||||
|
feedback: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Numerical',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: {
|
||||||
|
type: 'range',
|
||||||
|
number: 1822,
|
||||||
|
range: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Matching',
|
||||||
|
title: null,
|
||||||
|
stem: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'Match the following countries with their corresponding capitals.'
|
||||||
|
},
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
matchPairs: [
|
||||||
|
{
|
||||||
|
subquestion: { format: 'moodle', text: 'Canada' },
|
||||||
|
subanswer: 'Ottawa'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subquestion: { format: 'moodle', text: 'Italy' },
|
||||||
|
subanswer: 'Rome'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subquestion: { format: 'moodle', text: 'Japan' },
|
||||||
|
subanswer: 'Tokyo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'MC',
|
||||||
|
title: "Grant's Tomb",
|
||||||
|
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
|
||||||
|
hasEmbeddedAnswers: true,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'buried' },
|
||||||
|
feedback: { format: 'moodle', text: 'No one is buried there.' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'entombed' },
|
||||||
|
feedback: { format: 'moodle', text: 'Right answer!' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'living' },
|
||||||
|
feedback: { format: 'moodle', text: 'We hope not!' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'MC',
|
||||||
|
title: null,
|
||||||
|
stem: { format: 'moodle', text: 'Difficult multiple choice question.' },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'wrong answer' },
|
||||||
|
feedback: { format: 'moodle', text: 'comment on wrong answer' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: false,
|
||||||
|
weight: 50,
|
||||||
|
text: { format: 'moodle', text: 'half credit answer' },
|
||||||
|
feedback: { format: 'moodle', text: 'comment on answer' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'full credit answer' },
|
||||||
|
feedback: { format: 'moodle', text: 'well done!' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Short',
|
||||||
|
title: "Jesus' hometown (Short answer ex.)",
|
||||||
|
stem: { format: 'moodle', text: 'Jesus Christ was from _____ .' },
|
||||||
|
hasEmbeddedAnswers: true,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: { format: 'moodle', text: 'Nazareth' },
|
||||||
|
feedback: { format: 'moodle', text: "Yes! That's right!" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: 75,
|
||||||
|
text: { format: 'moodle', text: 'Nazereth' },
|
||||||
|
feedback: { format: 'moodle', text: 'Right, but misspelled.' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: 25,
|
||||||
|
text: { format: 'moodle', text: 'Bethlehem' },
|
||||||
|
feedback: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'He was born here, but not raised here.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Numerical',
|
||||||
|
title: 'Numerical example',
|
||||||
|
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: null,
|
||||||
|
text: {
|
||||||
|
type: 'range',
|
||||||
|
number: 1822,
|
||||||
|
range: 0
|
||||||
|
},
|
||||||
|
feedback: { format: 'moodle', text: 'Correct! 100% credit' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCorrect: true,
|
||||||
|
weight: 50,
|
||||||
|
text: {
|
||||||
|
type: 'range',
|
||||||
|
number: 1822,
|
||||||
|
range: 2
|
||||||
|
},
|
||||||
|
feedback: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'He was born in 1822. You get 50% credit for being close.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Essay',
|
||||||
|
title: 'Essay Example',
|
||||||
|
stem: { format: 'moodle', text: 'This is an essay.' },
|
||||||
|
hasEmbeddedAnswers: false,
|
||||||
|
globalFeedback: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
|
||||||
|
const errorItemDark = ErrorTemplate('Hello');
|
||||||
|
|
||||||
|
const lightItems = multiple.map((item) => Template(item, { theme: 'light' })).join('');
|
||||||
|
|
||||||
|
const errorItem = ErrorTemplate('Hello');
|
||||||
|
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
if (app) app.innerHTML = items + errorItemDark + lightItems + errorItem;
|
||||||
86
client/src/components/GiftTemplate/styles.css
Normal file
86
client/src/components/GiftTemplate/styles.css
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* :root {
|
||||||
|
--correct: hsla(134, 31%, 32%, 1);
|
||||||
|
--correct-light: hsla(134, 68%, 95%, 1);
|
||||||
|
|
||||||
|
--light-gray: rgb(202, 202, 202);
|
||||||
|
--moodle-blue-lighter: hsla(194, 55%, 98%, 1);
|
||||||
|
--moodle-blue-light: hsla(194, 60%, 96%, 1);
|
||||||
|
--moodle-blue: #def2f8;
|
||||||
|
--moodle-blue-dark: #c7e4e4;
|
||||||
|
--moodle-blue-darker: #81b1b1;
|
||||||
|
--moodle-blue-darkest: rgb(87, 119, 119);
|
||||||
|
--moodle-alt-light: #fff8e2;
|
||||||
|
--moodle-alt: #fcefdc;
|
||||||
|
--moodle-alt-dark: #b8945e;
|
||||||
|
--moodle-alt-darker: #7d5a29;
|
||||||
|
--moodle-alt-darkest: #2b2101;
|
||||||
|
--moodle-error-light: #fff0f0;
|
||||||
|
--moodle-error: #f3d8d8;
|
||||||
|
--default-box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||||
|
--form-input-color: rgb(74, 85, 104);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* body {
|
||||||
|
background-color: #121212;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* :root {
|
||||||
|
--correct: hsla(134, 31%, 32%, 1);
|
||||||
|
--correct-light: hsla(134, 68%, 95%, 1);
|
||||||
|
|
||||||
|
--light-gray: rgb(202, 202, 202);
|
||||||
|
--moodle-blue-lighter: hsla(194, 55%, 98%, 1);
|
||||||
|
--moodle-blue-light: hsla(194, 60%, 96%, 1);
|
||||||
|
--moodle-blue: #def2f8;
|
||||||
|
--moodle-blue-dark: #c7e4e4;
|
||||||
|
--moodle-blue-darker: #81b1b1;
|
||||||
|
--moodle-blue-darkest: rgb(87, 119, 119);
|
||||||
|
--moodle-alt-light: #fff8e2;
|
||||||
|
--moodle-alt: #fcefdc;
|
||||||
|
--moodle-alt-dark: #b8945e;
|
||||||
|
--moodle-alt-darker: #7d5a29;
|
||||||
|
--moodle-alt-darkest: #2b2101;
|
||||||
|
--moodle-error-light: #fff0f0;
|
||||||
|
--moodle-error: #f3d8d8;
|
||||||
|
--default-box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||||
|
--form-input-color: rgb(74, 85, 104);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||||
|
Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||||
|
Segoe UI Symbol;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
} */
|
||||||
|
/*
|
||||||
|
.gift-textarea::-webkit-input-placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.gift-textarea::-moz-placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.gift-textarea:-ms-input-placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.gift-textarea:-moz-placeholder {
|
||||||
|
color: #999;
|
||||||
|
} */
|
||||||
|
.present-question-title {
|
||||||
|
margin-top: 8vh;
|
||||||
|
margin-bottom: 2vh;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.preview-container {
|
||||||
|
margin-bottom: 2vh;
|
||||||
|
width: 60vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.multiple-choice-answers-container {
|
||||||
|
margin: 1% 0% 1% 0%;
|
||||||
|
}
|
||||||
|
.multiple-choice-answers-container > * {
|
||||||
|
display: inline;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
36
client/src/components/GiftTemplate/templates/AnswerIcon.ts
Normal file
36
client/src/components/GiftTemplate/templates/AnswerIcon.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { TemplateOptions } from './types';
|
||||||
|
import { theme } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
interface AnswerIconOptions extends TemplateOptions {
|
||||||
|
correct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnswerIcon({ correct }: AnswerIconOptions): string {
|
||||||
|
const Icon = `
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Correct = `
|
||||||
|
width: 1em;
|
||||||
|
color: ${theme(state.theme, 'success', 'success')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Incorrect = `
|
||||||
|
width: 0.75em;
|
||||||
|
color: ${theme(state.theme, 'danger', 'danger')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CorrectIcon = (): string => {
|
||||||
|
return `<svg style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IncorrectIcon = (): string => {
|
||||||
|
return `<svg style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return correct ? CorrectIcon() : IncorrectIcon();
|
||||||
|
}
|
||||||
14
client/src/components/GiftTemplate/templates/Category.ts
Normal file
14
client/src/components/GiftTemplate/templates/Category.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { TemplateOptions, Category as CategoryType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
|
||||||
|
type CategoryOptions = TemplateOptions & CategoryType;
|
||||||
|
|
||||||
|
export default function Category({ title }: CategoryOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: Title({
|
||||||
|
type: 'Catégorie',
|
||||||
|
title: title
|
||||||
|
})
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
22
client/src/components/GiftTemplate/templates/Description.ts
Normal file
22
client/src/components/GiftTemplate/templates/Description.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { TemplateOptions, Description as DescriptionType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import { ParagraphStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type DescriptionOptions = TemplateOptions & DescriptionType;
|
||||||
|
|
||||||
|
export default function Description({ title, stem }: DescriptionOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Description',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
59
client/src/components/GiftTemplate/templates/Error.ts
Normal file
59
client/src/components/GiftTemplate/templates/Error.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { theme, ParagraphStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
export default function (text: string): string {
|
||||||
|
const Container = `
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background-color: ${theme(state.theme, 'red100', 'redGray800')};
|
||||||
|
border: solid ${theme(state.theme, 'red300', 'red700')} 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/);
|
||||||
|
return document[0] !== ``
|
||||||
|
? `<section style="${Container}">${document
|
||||||
|
.map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`)
|
||||||
|
.join('')}</section>`
|
||||||
|
: ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentRegex(text: string): string {
|
||||||
|
const newText = text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, ''))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g;
|
||||||
|
const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g;
|
||||||
|
const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g;
|
||||||
|
|
||||||
|
return newText
|
||||||
|
.replace(newLineAnswer, `\n$2`)
|
||||||
|
.replace(correctAnswer, `$1<li>$4</li>`)
|
||||||
|
.replace(incorrectAnswer, `$1<li>$4</li>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineRegex(text: string): string {
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((category) =>
|
||||||
|
category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`)
|
||||||
|
)
|
||||||
|
.map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`))
|
||||||
|
.map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`))
|
||||||
|
.map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeBackslash(text: string): string {
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((colon) => colon.replace(/[\\]:/g, ':'))
|
||||||
|
.map((openBracket) => openBracket.replace(/[\\]{/g, '{'))
|
||||||
|
.map((closeBracket) => closeBracket.replace(/[\\]}/g, '}'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
27
client/src/components/GiftTemplate/templates/Essay.ts
Normal file
27
client/src/components/GiftTemplate/templates/Essay.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { TemplateOptions, Essay as EssayType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import { ParagraphStyle, TextAreaStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type EssayOptions = TemplateOptions & EssayType;
|
||||||
|
|
||||||
|
export default function Essay({ title, stem, globalFeedback }: EssayOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Développement',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
`<textarea class="gift-textarea" style="${TextAreaStyle(
|
||||||
|
state.theme
|
||||||
|
)}" placeholder="Entrez votre réponse ici..."></textarea>`,
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { TemplateOptions, Question } from './types';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import { state } from '.';
|
||||||
|
import { theme } from '../constants';
|
||||||
|
|
||||||
|
interface GlobalFeedbackOptions extends TemplateOptions {
|
||||||
|
feedback: Question['globalFeedback'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): string {
|
||||||
|
const Container = `
|
||||||
|
position: relative;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
background-color: ${theme(state.theme, 'beige100', 'black400')};
|
||||||
|
color: ${theme(state.theme, 'beige900', 'gray200')};
|
||||||
|
border: ${theme(state.theme, 'beige300', 'black300')} ${state.theme === 'light' ? 1 : 2}px solid;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 2px 5px ${theme(state.theme, 'gray400', 'black800')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return feedback !== null
|
||||||
|
? `<div style="${Container}">
|
||||||
|
<p>${TextType({ text: feedback })}</p>
|
||||||
|
</div>`
|
||||||
|
: ``;
|
||||||
|
}
|
||||||
87
client/src/components/GiftTemplate/templates/Matching.ts
Normal file
87
client/src/components/GiftTemplate/templates/Matching.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { TemplateOptions, Matching as MatchingType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import { ParagraphStyle, SelectStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type MatchingOptions = TemplateOptions & MatchingType;
|
||||||
|
|
||||||
|
interface MatchAnswerOptions extends TemplateOptions {
|
||||||
|
choices: MatchingType['matchPairs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Matching({
|
||||||
|
title,
|
||||||
|
stem,
|
||||||
|
matchPairs,
|
||||||
|
globalFeedback
|
||||||
|
}: MatchingOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Appariement',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
MatchAnswers({ choices: matchPairs }),
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchAnswers({ choices }: MatchAnswerOptions): string {
|
||||||
|
const Layout = `
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: fit-content(50%) fit-content(50%);
|
||||||
|
grid-gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Dropdown = `
|
||||||
|
padding: 0.375rem 1.75rem 0.375rem 0.75rem;
|
||||||
|
background-image: url(data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23${
|
||||||
|
state.theme === 'light' ? '000' : 'ccc'
|
||||||
|
}%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E);
|
||||||
|
background-size: 0.6em;
|
||||||
|
background-position: calc(100% - 0.5em) center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: auto;
|
||||||
|
vertical-align: baseline;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionTable = `
|
||||||
|
padding-right: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uniqueMatchOptions = Array.from(new Set(choices.map(({ subanswer }) => subanswer)));
|
||||||
|
|
||||||
|
const result = choices
|
||||||
|
.map(({ subquestion }) => {
|
||||||
|
return `
|
||||||
|
<div style="${OptionTable} ${ParagraphStyle(state.theme)}">
|
||||||
|
${TextType({ text: subquestion })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}">
|
||||||
|
<option value="" disabled selected hidden>Choisir...</option>
|
||||||
|
${uniqueMatchOptions.map((subanswer) => `<option>${subanswer}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="${Layout}">
|
||||||
|
${result}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
|
||||||
|
import { ParagraphStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type MultipleChoiceOptions = TemplateOptions & MultipleChoiceType;
|
||||||
|
|
||||||
|
export default function MultipleChoice({
|
||||||
|
title,
|
||||||
|
stem,
|
||||||
|
choices,
|
||||||
|
globalFeedback
|
||||||
|
}: MultipleChoiceOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Choix multiple',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
MultipleChoiceAnswers({ choices: choices }),
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { TemplateOptions, TextFormat, Choice, MultipleChoice as MultipleChoiceType } from './types';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import AnswerIcon from './AnswerIcon';
|
||||||
|
import { state } from '.';
|
||||||
|
import { ParagraphStyle, theme } from '../constants';
|
||||||
|
|
||||||
|
type MultipleChoiceAnswerOptions = TemplateOptions & Pick<MultipleChoiceType, 'choices'>;
|
||||||
|
|
||||||
|
type AnswerFeedbackOptions = TemplateOptions & Pick<Choice, 'feedback'>;
|
||||||
|
|
||||||
|
interface AnswerWeightOptions extends TemplateOptions {
|
||||||
|
weight: Choice['weight'];
|
||||||
|
correct: Choice['isCorrect'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MultipleChoiceAnswers({ choices }: MultipleChoiceAnswerOptions) {
|
||||||
|
const id = `id${nanoid(8)}`;
|
||||||
|
|
||||||
|
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0;
|
||||||
|
|
||||||
|
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
|
||||||
|
isMultipleAnswer ? ` ou plusieurs` : ``
|
||||||
|
}:</span>`;
|
||||||
|
const result = choices
|
||||||
|
.map(({ weight, isCorrect, text, feedback }) => {
|
||||||
|
const CustomLabel = `
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0 0.2em 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const inputId = `id${nanoid(6)}`;
|
||||||
|
|
||||||
|
const isPositiveWeight = weight !== null && weight > 0;
|
||||||
|
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class='multiple-choice-answers-container'>
|
||||||
|
<input class="gift-input" type="${
|
||||||
|
isMultipleAnswer ? 'checkbox' : 'radio'
|
||||||
|
}" id="${inputId}" name="${id}">
|
||||||
|
${AnswerWeight({ correct: isCorrectOption, weight: weight })}
|
||||||
|
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
|
||||||
|
${TextType({ text: text as TextFormat })}
|
||||||
|
</label>
|
||||||
|
${AnswerIcon({ correct: isCorrectOption })}
|
||||||
|
${AnswerFeedback({ feedback: feedback })}
|
||||||
|
</input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `${prompt}${result}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnswerWeight({ weight, correct }: AnswerWeightOptions): string {
|
||||||
|
const Container = `
|
||||||
|
box-shadow: 0px 1px 1px ${theme(state.theme, 'gray400', 'black900')};
|
||||||
|
border-radius: 3px;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
padding-top: 0.05rem;
|
||||||
|
padding-bottom: 0.05rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CorrectWeight = `
|
||||||
|
color: ${theme(state.theme, 'green700', 'green100')};
|
||||||
|
background-color: ${theme(state.theme, 'green100', 'greenGray700')};
|
||||||
|
`;
|
||||||
|
const IncorrectWeight = `
|
||||||
|
color: ${theme(state.theme, 'beige600', 'beige100')};
|
||||||
|
background-color: ${theme(state.theme, 'beige300', 'beigeGray800')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return weight
|
||||||
|
? `<span style="${Container} ${
|
||||||
|
correct ? `${CorrectWeight}` : `${IncorrectWeight}`
|
||||||
|
}">${weight}%</span>`
|
||||||
|
: ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnswerFeedback({ feedback }: AnswerFeedbackOptions): string {
|
||||||
|
const Container = `
|
||||||
|
color: ${theme(state.theme, 'teal700', 'gray700')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return feedback ? `<span style="${Container}">${TextType({ text: feedback })}</span>` : ``;
|
||||||
|
}
|
||||||
60
client/src/components/GiftTemplate/templates/Numerical.ts
Normal file
60
client/src/components/GiftTemplate/templates/Numerical.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { TemplateOptions, Numerical as NumericalType, NumericalFormat } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import { ParagraphStyle, InputStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type NumericalOptions = TemplateOptions & NumericalType;
|
||||||
|
type NumericalAnswerOptions = TemplateOptions & Pick<NumericalType, 'choices'>;
|
||||||
|
|
||||||
|
export default function Numerical({
|
||||||
|
title,
|
||||||
|
stem,
|
||||||
|
choices,
|
||||||
|
globalFeedback
|
||||||
|
}: NumericalOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Numérique',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
NumericalAnswers({ choices: choices }),
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumericalAnswers({ choices }: NumericalAnswerOptions): string {
|
||||||
|
const placeholder = Array.isArray(choices)
|
||||||
|
? choices.map(({ text }) => Answer(text)).join(', ')
|
||||||
|
: Answer(choices);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<span style="${ParagraphStyle(
|
||||||
|
state.theme
|
||||||
|
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
|
||||||
|
state.theme
|
||||||
|
)}" placeholder="${placeholder}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Answer({ type, number, range, numberLow, numberHigh }: NumericalFormat): string {
|
||||||
|
switch (type) {
|
||||||
|
case 'simple':
|
||||||
|
return `${number}`;
|
||||||
|
case 'range':
|
||||||
|
return `${number} ± ${range}`;
|
||||||
|
case 'high-low':
|
||||||
|
return `${numberLow} - ${numberHigh}`;
|
||||||
|
default:
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { TemplateOptions } from './types';
|
||||||
|
import { state } from './index';
|
||||||
|
import { theme } from '../constants';
|
||||||
|
|
||||||
|
export default function QuestionContainer({ children }: TemplateOptions): string {
|
||||||
|
const Container = `
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background-color: ${theme(state.theme, 'white', 'black600')};
|
||||||
|
border: solid ${theme(state.theme, 'white', 'black500')} 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `<section style="${Container}">${
|
||||||
|
Array.isArray(children) ? children.join('') : children
|
||||||
|
}</section>`;
|
||||||
|
}
|
||||||
46
client/src/components/GiftTemplate/templates/ShortAnswer.ts
Normal file
46
client/src/components/GiftTemplate/templates/ShortAnswer.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { TemplateOptions, ShortAnswer as ShortAnswerType, TextFormat } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import Title from './Title';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import { ParagraphStyle, InputStyle } from '../constants';
|
||||||
|
import { state } from './index';
|
||||||
|
|
||||||
|
type ShortAnswerOptions = TemplateOptions & ShortAnswerType;
|
||||||
|
type AnswerOptions = TemplateOptions & Pick<ShortAnswerType, 'choices'>;
|
||||||
|
|
||||||
|
export default function ShortAnswer({
|
||||||
|
title,
|
||||||
|
stem,
|
||||||
|
choices,
|
||||||
|
globalFeedback
|
||||||
|
}: ShortAnswerOptions): string {
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Réponse courte',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
Answers({ choices: choices }),
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Answers({ choices }: AnswerOptions): string {
|
||||||
|
const placeholder = choices
|
||||||
|
.map(({ text }) => TextType({ text: text as TextFormat }))
|
||||||
|
.join(', ');
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<span style="${ParagraphStyle(
|
||||||
|
state.theme
|
||||||
|
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
|
||||||
|
state.theme
|
||||||
|
)}" placeholder="${placeholder}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
46
client/src/components/GiftTemplate/templates/TextType.ts
Normal file
46
client/src/components/GiftTemplate/templates/TextType.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import katex from 'katex';
|
||||||
|
import { TemplateOptions, TextFormat } from './types';
|
||||||
|
|
||||||
|
interface TextTypeOptions extends TemplateOptions {
|
||||||
|
text: TextFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLatex(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||||
|
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
|
||||||
|
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
|
||||||
|
katex.renderToString(inner, { displayMode: false })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TextType({ text }: TextTypeOptions): string {
|
||||||
|
const formatText = formatLatex(escapeHTML(text.text.trim()));
|
||||||
|
|
||||||
|
switch (text.format) {
|
||||||
|
case 'moodle':
|
||||||
|
case 'plain':
|
||||||
|
return formatText.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||||
|
case 'html':
|
||||||
|
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
|
||||||
|
case 'markdown':
|
||||||
|
return (
|
||||||
|
marked
|
||||||
|
.parse(formatText, { breaks: true }) // call marked.parse instead of marked
|
||||||
|
// Strip outer paragraph tags
|
||||||
|
.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2')
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
client/src/components/GiftTemplate/templates/Title.ts
Normal file
56
client/src/components/GiftTemplate/templates/Title.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { TemplateOptions, Question } from './types';
|
||||||
|
import { state } from '.';
|
||||||
|
import { theme } from '../constants';
|
||||||
|
|
||||||
|
// Type is string to allow for custom question type text (e,g, "Multiple Choice")
|
||||||
|
interface TitleOptions extends TemplateOptions {
|
||||||
|
type: string;
|
||||||
|
title: Question['title'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Title({ type, title }: TitleOptions): string {
|
||||||
|
const Container = `
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QuestionTitle = `
|
||||||
|
color: ${theme(state.theme, 'blue', 'gray200')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionalTitle = `
|
||||||
|
color: ${theme(state.theme, 'blue', 'gray900')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QuestionTypeContainer = `
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
flex: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QuestionType = `
|
||||||
|
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black700')};
|
||||||
|
padding-left: 0.7rem;
|
||||||
|
padding-right: 0.7rem;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: ${theme(state.theme, 'white', 'black400')};
|
||||||
|
color: ${theme(state.theme, 'teal700', 'gray300')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="${Container}">
|
||||||
|
<span>
|
||||||
|
${
|
||||||
|
title !== null
|
||||||
|
? `<span style="${QuestionTitle}">${title}</span>`
|
||||||
|
: `<span style="${OptionalTitle}">Titre optionnel...</span>`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span style="${QuestionTypeContainer} margin-bottom: 10px;">
|
||||||
|
<span style="${QuestionType}">${type}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
54
client/src/components/GiftTemplate/templates/TrueFalse.ts
Normal file
54
client/src/components/GiftTemplate/templates/TrueFalse.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { TemplateOptions, TextChoice, TrueFalse as TrueFalseType } from './types';
|
||||||
|
import QuestionContainer from './QuestionContainer';
|
||||||
|
import TextType from './TextType';
|
||||||
|
import GlobalFeedback from './GlobalFeedback';
|
||||||
|
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
|
||||||
|
import Title from './Title';
|
||||||
|
import { ParagraphStyle } from '../constants';
|
||||||
|
import { state } from '.';
|
||||||
|
|
||||||
|
type TrueFalseOptions = TemplateOptions & TrueFalseType;
|
||||||
|
|
||||||
|
export default function TrueFalse({
|
||||||
|
title,
|
||||||
|
isTrue,
|
||||||
|
stem,
|
||||||
|
correctFeedback,
|
||||||
|
incorrectFeedback,
|
||||||
|
globalFeedback
|
||||||
|
}: TrueFalseOptions): string {
|
||||||
|
const choices: TextChoice[] = [
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'Vrai'
|
||||||
|
},
|
||||||
|
isCorrect: isTrue,
|
||||||
|
weight: null,
|
||||||
|
feedback: isTrue ? correctFeedback : incorrectFeedback
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: {
|
||||||
|
format: 'moodle',
|
||||||
|
text: 'Faux'
|
||||||
|
},
|
||||||
|
isCorrect: !isTrue,
|
||||||
|
weight: null,
|
||||||
|
feedback: !isTrue ? correctFeedback : incorrectFeedback
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return `${QuestionContainer({
|
||||||
|
children: [
|
||||||
|
Title({
|
||||||
|
type: 'Vrai/Faux',
|
||||||
|
title: title
|
||||||
|
}),
|
||||||
|
`<p style="${ParagraphStyle(state.theme)}">${TextType({
|
||||||
|
text: stem
|
||||||
|
})}</p>`,
|
||||||
|
MultipleChoiceAnswers({ choices: choices }),
|
||||||
|
GlobalFeedback({ feedback: globalFeedback })
|
||||||
|
]
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
75
client/src/components/GiftTemplate/templates/index.ts
Normal file
75
client/src/components/GiftTemplate/templates/index.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import Category from './Category';
|
||||||
|
import Description from './Description';
|
||||||
|
import Essay from './Essay';
|
||||||
|
import Matching from './Matching';
|
||||||
|
import MultipleChoice from './MultipleChoice';
|
||||||
|
import Numerical from './Numerical';
|
||||||
|
import ShortAnswer from './ShortAnswer';
|
||||||
|
import TrueFalse from './TrueFalse';
|
||||||
|
import Error from './Error';
|
||||||
|
import {
|
||||||
|
GIFTQuestion,
|
||||||
|
Category as CategoryType,
|
||||||
|
Description as DescriptionType,
|
||||||
|
MultipleChoice as MultipleChoiceType,
|
||||||
|
Numerical as NumericalType,
|
||||||
|
ShortAnswer as ShortAnswerType,
|
||||||
|
Essay as EssayType,
|
||||||
|
TrueFalse as TrueFalseType,
|
||||||
|
Matching as MatchingType,
|
||||||
|
DisplayOptions
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const state: DisplayOptions = { preview: true, theme: 'light' };
|
||||||
|
|
||||||
|
export default function Template(
|
||||||
|
{ type, ...keys }: GIFTQuestion,
|
||||||
|
options?: Partial<DisplayOptions>
|
||||||
|
): string {
|
||||||
|
Object.assign(state, options);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'Category':
|
||||||
|
return Category({ ...(keys as CategoryType) });
|
||||||
|
case 'Description':
|
||||||
|
return Description({
|
||||||
|
...(keys as DescriptionType)
|
||||||
|
});
|
||||||
|
case 'MC':
|
||||||
|
return MultipleChoice({
|
||||||
|
...(keys as MultipleChoiceType)
|
||||||
|
});
|
||||||
|
case 'Numerical':
|
||||||
|
return Numerical({ ...(keys as NumericalType) });
|
||||||
|
case 'Short':
|
||||||
|
return ShortAnswer({
|
||||||
|
...(keys as ShortAnswerType)
|
||||||
|
});
|
||||||
|
case 'Essay':
|
||||||
|
return Essay({ ...(keys as EssayType) });
|
||||||
|
case 'TF':
|
||||||
|
return TrueFalse({ ...(keys as TrueFalseType) });
|
||||||
|
case 'Matching':
|
||||||
|
return Matching({ ...(keys as MatchingType) });
|
||||||
|
default:
|
||||||
|
return ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>): string {
|
||||||
|
Object.assign(state, options);
|
||||||
|
|
||||||
|
return Error(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Category,
|
||||||
|
Description,
|
||||||
|
Essay,
|
||||||
|
Matching,
|
||||||
|
MultipleChoice,
|
||||||
|
Numerical,
|
||||||
|
ShortAnswer,
|
||||||
|
TrueFalse,
|
||||||
|
Error
|
||||||
|
};
|
||||||
120
client/src/components/GiftTemplate/templates/types.d.ts
vendored
Normal file
120
client/src/components/GiftTemplate/templates/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
export type Template = (options: TemplateOptions) => string;
|
||||||
|
|
||||||
|
export interface TemplateOptions {
|
||||||
|
children?: Template | string | Array<Template | string>;
|
||||||
|
options?: DisplayOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeType = 'light' | 'dark';
|
||||||
|
|
||||||
|
export interface DisplayOptions {
|
||||||
|
theme: ThemeType;
|
||||||
|
preview: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionType =
|
||||||
|
| 'Description'
|
||||||
|
| 'Category'
|
||||||
|
| 'MC'
|
||||||
|
| 'Numerical'
|
||||||
|
| 'Short'
|
||||||
|
| 'Essay'
|
||||||
|
| 'TF'
|
||||||
|
| 'Matching';
|
||||||
|
|
||||||
|
export type FormatType = 'moodle' | 'html' | 'markdown' | 'plain';
|
||||||
|
export type NumericalType = 'simple' | 'range' | 'high-low';
|
||||||
|
|
||||||
|
export interface TextFormat {
|
||||||
|
format: FormatType;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumericalFormat {
|
||||||
|
type: NumericalType;
|
||||||
|
number?: number;
|
||||||
|
range?: number;
|
||||||
|
numberHigh?: number;
|
||||||
|
numberLow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Choice {
|
||||||
|
isCorrect: boolean;
|
||||||
|
weight: number | null;
|
||||||
|
text: TextFormat | NumericalFormat;
|
||||||
|
feedback: TextFormat | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextChoice extends Choice {
|
||||||
|
text: TextFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumericalChoice extends Choice {
|
||||||
|
text: NumericalFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
|
type: QuestionType;
|
||||||
|
title: string | null;
|
||||||
|
stem: TextFormat;
|
||||||
|
hasEmbeddedAnswers: boolean;
|
||||||
|
globalFeedback: TextFormat | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Description {
|
||||||
|
type: Extract<QuestionType, 'Description'>;
|
||||||
|
title: string | null;
|
||||||
|
stem: TextFormat;
|
||||||
|
hasEmbeddedAnswers: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
type: Extract<QuestionType, 'Category'>;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultipleChoice extends Question {
|
||||||
|
type: Extract<QuestionType, 'MC'>;
|
||||||
|
choices: TextChoice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortAnswer extends Question {
|
||||||
|
type: Extract<QuestionType, 'Short'>;
|
||||||
|
choices: TextChoice[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Numerical extends Question {
|
||||||
|
type: Extract<QuestionType, 'Numerical'>;
|
||||||
|
choices: NumericalChoice[] | NumericalFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Essay extends Question {
|
||||||
|
type: Extract<QuestionType, 'Essay'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrueFalse extends Question {
|
||||||
|
type: Extract<QuestionType, 'TF'>;
|
||||||
|
isTrue: boolean;
|
||||||
|
incorrectFeedback: TextFormat | null;
|
||||||
|
correctFeedback: TextFormat | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Matching extends Question {
|
||||||
|
type: Extract<QuestionType, 'Matching'>;
|
||||||
|
matchPairs: Match[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Match {
|
||||||
|
subquestion: TextFormat;
|
||||||
|
subanswer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GIFTQuestion =
|
||||||
|
| Description
|
||||||
|
| Category
|
||||||
|
| MultipleChoice
|
||||||
|
| ShortAnswer
|
||||||
|
| Numerical
|
||||||
|
| Essay
|
||||||
|
| TrueFalse
|
||||||
|
| Matching;
|
||||||
39
client/src/components/Header/Header.tsx
Normal file
39
client/src/components/Header/Header.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import * as React from 'react';
|
||||||
|
import './header.css';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
isLoggedIn: () => boolean;
|
||||||
|
handleLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="header">
|
||||||
|
<img
|
||||||
|
className="logo"
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoggedIn() && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
handleLogout();
|
||||||
|
navigate('/');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
14
client/src/components/Header/header.css
Normal file
14
client/src/components/Header/header.css
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
210
client/src/components/ImportModal/ImportModal.tsx
Normal file
210
client/src/components/ImportModal/ImportModal.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import React, { useState, DragEvent, useRef, useEffect } from 'react';
|
||||||
|
import './importModal.css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Clear, Download } from '@mui/icons-material';
|
||||||
|
import ApiService from '../../services/ApiService';
|
||||||
|
|
||||||
|
|
||||||
|
type DroppedFile = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
handleOnClose: () => void;
|
||||||
|
handleOnImport: () => void;
|
||||||
|
open: boolean;
|
||||||
|
selectedFolder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, selectedFolder }) => {
|
||||||
|
const [droppedFiles, setDroppedFiles] = useState<DroppedFile[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setDroppedFiles([]);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
handleFiles(files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFiles = (files: FileList) => {
|
||||||
|
const newDroppedFiles = Array.from(files)
|
||||||
|
.filter((file) => file.name.endsWith('.txt'))
|
||||||
|
.map((file, index) => ({
|
||||||
|
id: index,
|
||||||
|
name: file.name,
|
||||||
|
icon: '📄',
|
||||||
|
file
|
||||||
|
}));
|
||||||
|
|
||||||
|
setDroppedFiles((prevFiles) => [...prevFiles, ...newDroppedFiles]);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleOnSave = async () => {
|
||||||
|
const storedQuizzes = JSON.parse(localStorage.getItem('quizzes') || '[]');
|
||||||
|
const quizzesToImportPromises = droppedFiles.map((droppedFile) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
if (event.target && event.target.result) {
|
||||||
|
const fileContent = event.target.result as string;
|
||||||
|
//console.log(fileContent);
|
||||||
|
if (fileContent.trim() === '') {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
const questions = fileContent.split(/}/)
|
||||||
|
.map(question => {
|
||||||
|
// Remove trailing and leading spaces
|
||||||
|
|
||||||
|
return question.trim()+"}";
|
||||||
|
})
|
||||||
|
.filter(question => question.trim() !== '').slice(0, -1); // Filter out lines with only whitespace characters
|
||||||
|
|
||||||
|
try {
|
||||||
|
// const folders = await ApiService.getUserFolders();
|
||||||
|
|
||||||
|
// Assuming you want to use the first folder
|
||||||
|
// const selectedFolder = folders.length > 0 ? folders[0]._id : null;
|
||||||
|
await ApiService.createQuiz(droppedFile.name.slice(0, -4) || 'Untitled quiz', questions, selectedFolder);
|
||||||
|
resolve('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving quiz:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(droppedFile.file);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Promise.all(quizzesToImportPromises).then((quizzesToImport) => {
|
||||||
|
const verifiedQuizzesToImport = quizzesToImport.filter((quiz) => {
|
||||||
|
return quiz !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedQuizzes = [...storedQuizzes, ...verifiedQuizzesToImport];
|
||||||
|
localStorage.setItem('quizzes', JSON.stringify(updatedQuizzes));
|
||||||
|
|
||||||
|
setDroppedFiles([]);
|
||||||
|
handleOnImport();
|
||||||
|
handleOnClose();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleRemoveFile = (id: number) => {
|
||||||
|
setDroppedFiles((prevFiles) => prevFiles.filter((file) => file.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files) {
|
||||||
|
handleFiles(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBrowseButtonClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnCancel = () => {
|
||||||
|
setDroppedFiles([]);
|
||||||
|
handleOnClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onClose={handleOnCancel} fullWidth>
|
||||||
|
<DialogTitle sx={{ fontWeight: 'bold', fontSize: 24 }}>
|
||||||
|
{'Importation de quiz'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent
|
||||||
|
className="import-container"
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleBrowseButtonClick}
|
||||||
|
>
|
||||||
|
<div className="mb-1">
|
||||||
|
<DialogContentText sx={{ textAlign: 'center' }}>
|
||||||
|
Déposer des fichiers ici ou
|
||||||
|
<br />
|
||||||
|
cliquez pour ouvrir l'explorateur des fichiers
|
||||||
|
</DialogContentText>
|
||||||
|
</div>
|
||||||
|
<Download color="primary" />
|
||||||
|
</DialogContent>
|
||||||
|
<DialogContent>
|
||||||
|
{droppedFiles.map((file) => (
|
||||||
|
<div key={file.id + file.name} className="file-container">
|
||||||
|
<span>{file.icon}</span>
|
||||||
|
<span>{file.name}</span>
|
||||||
|
<IconButton
|
||||||
|
sx={{ padding: 0 }}
|
||||||
|
onClick={() => handleRemoveFile(file.id)}
|
||||||
|
>
|
||||||
|
<Clear />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={handleOnCancel}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleOnSave}>
|
||||||
|
Importer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragAndDrop;
|
||||||
20
client/src/components/ImportModal/importModal.css
Normal file
20
client/src/components/ImportModal/importModal.css
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
.import-container {
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: thin;
|
||||||
|
border-color: rgba(128, 128, 128, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 20vh;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-container {
|
||||||
|
gap: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
59
client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx
Normal file
59
client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormLabel,
|
||||||
|
Radio,
|
||||||
|
RadioGroup
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
handleOnClose: () => void;
|
||||||
|
launchQuiz: () => void;
|
||||||
|
setQuizMode: (mode: 'teacher' | 'student') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LaunchQuizDialog: React.FC<Props> = ({ open, handleOnClose, launchQuiz, setQuizMode }) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={handleOnClose}>
|
||||||
|
<DialogTitle sx={{ fontWeight: 'bold', fontSize: 24 }}>
|
||||||
|
Options de lancement du quiz
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Rythme du quiz</FormLabel>
|
||||||
|
<RadioGroup defaultValue="teacher" name="radio-buttons-group">
|
||||||
|
<FormControlLabel
|
||||||
|
value="teacher"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Rythme du professeur"
|
||||||
|
onChange={() => setQuizMode('teacher')}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="student"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Rythme de l'étudiant"
|
||||||
|
onChange={() => setQuizMode('student')}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={handleOnClose}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={launchQuiz}>
|
||||||
|
Lancer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LaunchQuizDialog;
|
||||||
390
client/src/components/LiveResults/LiveResults.tsx
Normal file
390
client/src/components/LiveResults/LiveResults.tsx
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
// LiveResults.tsx
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { GIFTQuestion } from 'gift-pegjs';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { QuestionType } from '../../Types/QuestionType';
|
||||||
|
|
||||||
|
import './liveResult.css';
|
||||||
|
import {
|
||||||
|
FormControlLabel,
|
||||||
|
FormGroup,
|
||||||
|
Paper,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow
|
||||||
|
} from '@mui/material';
|
||||||
|
import Latex from 'react-latex';
|
||||||
|
import { UserType } from '../../Types/UserType';
|
||||||
|
|
||||||
|
interface LiveResultsProps {
|
||||||
|
socket: Socket | null;
|
||||||
|
questions: QuestionType[];
|
||||||
|
showSelectedQuestion: (index: number) => void;
|
||||||
|
quizMode: 'teacher' | 'student';
|
||||||
|
students: UserType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Answer {
|
||||||
|
answer: string | number | boolean;
|
||||||
|
isCorrect: boolean;
|
||||||
|
idQuestion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StudentResult {
|
||||||
|
username: string;
|
||||||
|
idUser: string;
|
||||||
|
answers: Answer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelectedQuestion, students }) => {
|
||||||
|
const [showUsernames, setShowUsernames] = useState<boolean>(false);
|
||||||
|
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
|
||||||
|
const [studentResults, setStudentResults] = useState<StudentResult[]>([]);
|
||||||
|
|
||||||
|
const maxQuestions = questions.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Set student list before starting
|
||||||
|
let newStudents:StudentResult[] = [];
|
||||||
|
|
||||||
|
for (const student of students as UserType[]) {
|
||||||
|
newStudents.push( { username: student.name, idUser: student.id, answers: [] } )
|
||||||
|
}
|
||||||
|
|
||||||
|
setStudentResults(newStudents);
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
const submitAnswerHandler = ({
|
||||||
|
idUser,
|
||||||
|
username,
|
||||||
|
answer,
|
||||||
|
idQuestion
|
||||||
|
}: {
|
||||||
|
idUser: string;
|
||||||
|
username: string;
|
||||||
|
answer: string | number | boolean;
|
||||||
|
idQuestion: number;
|
||||||
|
}) => {
|
||||||
|
setStudentResults((currentResults) => {
|
||||||
|
const userIndex = currentResults.findIndex(
|
||||||
|
(result) => result.idUser === idUser
|
||||||
|
);
|
||||||
|
const isCorrect = checkIfIsCorrect(answer, idQuestion);
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
const newResults = [...currentResults];
|
||||||
|
newResults[userIndex].answers.push({ answer, isCorrect, idQuestion });
|
||||||
|
return newResults;
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
...currentResults,
|
||||||
|
{ idUser, username, answers: [{ answer, isCorrect, idQuestion }] }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('submit-answer', submitAnswerHandler);
|
||||||
|
return () => {
|
||||||
|
socket.off('submit-answer');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const getStudentGrade = (student: StudentResult): number => {
|
||||||
|
if (student.answers.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueQuestions = new Set();
|
||||||
|
let correctAnswers = 0;
|
||||||
|
|
||||||
|
for (const answer of student.answers) {
|
||||||
|
const { idQuestion, isCorrect } = answer;
|
||||||
|
|
||||||
|
if (!uniqueQuestions.has(idQuestion)) {
|
||||||
|
uniqueQuestions.add(idQuestion);
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
correctAnswers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (correctAnswers / questions.length) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const classAverage: number = useMemo(() => {
|
||||||
|
let classTotal = 0;
|
||||||
|
studentResults.forEach((student) => {
|
||||||
|
classTotal += getStudentGrade(student);
|
||||||
|
});
|
||||||
|
|
||||||
|
return classTotal / studentResults.length;
|
||||||
|
}, [studentResults]);
|
||||||
|
|
||||||
|
const getCorrectAnswersPerQuestion = (index: number): number => {
|
||||||
|
return (
|
||||||
|
(studentResults.filter((student) =>
|
||||||
|
student.answers.some(
|
||||||
|
(answer) =>
|
||||||
|
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
|
||||||
|
)
|
||||||
|
).length /
|
||||||
|
studentResults.length) *
|
||||||
|
100
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean {
|
||||||
|
const questionInfo = questions.find((q) =>
|
||||||
|
q.question.id ? q.question.id === idQuestion.toString() : false
|
||||||
|
) as QuestionType | undefined;
|
||||||
|
|
||||||
|
const answerText = answer.toString();
|
||||||
|
if (questionInfo) {
|
||||||
|
const question = questionInfo.question as GIFTQuestion;
|
||||||
|
if (question.type === 'TF') {
|
||||||
|
return (
|
||||||
|
(question.isTrue && answerText == 'true') ||
|
||||||
|
(!question.isTrue && answerText == 'false')
|
||||||
|
);
|
||||||
|
} else if (question.type === 'MC') {
|
||||||
|
return question.choices.some(
|
||||||
|
(choice) => choice.isCorrect && choice.text.text === answerText
|
||||||
|
);
|
||||||
|
} else if (question.type === 'Numerical') {
|
||||||
|
if (question.choices && !Array.isArray(question.choices)) {
|
||||||
|
if (
|
||||||
|
question.choices.type === 'high-low' &&
|
||||||
|
question.choices.numberHigh &&
|
||||||
|
question.choices.numberLow
|
||||||
|
) {
|
||||||
|
const answerNumber = parseFloat(answerText);
|
||||||
|
if (!isNaN(answerNumber)) {
|
||||||
|
return (
|
||||||
|
answerNumber <= question.choices.numberHigh &&
|
||||||
|
answerNumber >= question.choices.numberLow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.choices && Array.isArray(question.choices)) {
|
||||||
|
if (
|
||||||
|
question.choices[0].text.type === 'range' &&
|
||||||
|
question.choices[0].text.number &&
|
||||||
|
question.choices[0].text.range
|
||||||
|
) {
|
||||||
|
const answerNumber = parseFloat(answerText);
|
||||||
|
const range = question.choices[0].text.range;
|
||||||
|
const correctAnswer = question.choices[0].text.number;
|
||||||
|
if (!isNaN(answerNumber)) {
|
||||||
|
return (
|
||||||
|
answerNumber <= correctAnswer + range &&
|
||||||
|
answerNumber >= correctAnswer - range
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
question.choices[0].text.type === 'simple' &&
|
||||||
|
question.choices[0].text.number
|
||||||
|
) {
|
||||||
|
const answerNumber = parseFloat(answerText);
|
||||||
|
if (!isNaN(answerNumber)) {
|
||||||
|
return answerNumber === question.choices[0].text.number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (question.type === 'Short') {
|
||||||
|
return question.choices.some(
|
||||||
|
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="action-bar mb-1">
|
||||||
|
<div className="text-2xl text-bold">Résultats du quiz</div>
|
||||||
|
<FormGroup row>
|
||||||
|
<FormControlLabel
|
||||||
|
label={<div className="text-sm">Afficher les noms</div>}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
value={showUsernames}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setShowUsernames(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
label={<div className="text-sm">Afficher les réponses</div>}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
value={showCorrectAnswers}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setShowCorrectAnswers(e.target.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table size="small" stickyHeader component={Paper}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-base text-bold">Nom d'utilisateur</div>
|
||||||
|
</TableCell>
|
||||||
|
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: `pointer`,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)'
|
||||||
|
}}
|
||||||
|
onClick={() => showSelectedQuestion(index)}
|
||||||
|
>
|
||||||
|
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-base text-bold">% réussite</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{studentResults.map((student) => (
|
||||||
|
<TableRow key={student.idUser}>
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-base">
|
||||||
|
{showUsernames ? student.username : '******'}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{Array.from({ length: maxQuestions }, (_, index) => {
|
||||||
|
const answer = student.answers.find(
|
||||||
|
(answer) => parseInt(answer.idQuestion.toString()) === index + 1
|
||||||
|
);
|
||||||
|
const answerText = answer ? answer.answer.toString() : '';
|
||||||
|
const isCorrect = answer ? answer.isCorrect : false;
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)'
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
answerText === ''
|
||||||
|
? ''
|
||||||
|
: isCorrect
|
||||||
|
? 'correct-answer'
|
||||||
|
: 'incorrect-answer'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showCorrectAnswers ? (
|
||||||
|
<Latex>{answerText}</Latex>
|
||||||
|
) : isCorrect ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
) : (
|
||||||
|
answerText !== '' && (
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'rgba(0, 0, 0)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getStudentGrade(student).toFixed()} %
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
|
||||||
|
<TableCell sx={{ color: 'black' }}>
|
||||||
|
<div className="text-base text-bold">% réussite</div>
|
||||||
|
</TableCell>
|
||||||
|
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||||
|
<TableCell
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'rgba(0, 0, 0)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{studentResults.length > 0
|
||||||
|
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
|
||||||
|
: '-'}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(224, 224, 224, 1)',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'rgba(0, 0, 0)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{studentResults.length > 0 ? `${classAverage.toFixed()} %` : '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LiveResults;
|
||||||
14
client/src/components/LiveResults/liveResult.css
Normal file
14
client/src/components/LiveResults/liveResult.css
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
.correct-answer {
|
||||||
|
background-color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect-answer {
|
||||||
|
background-color: lightcoral;
|
||||||
|
}
|
||||||
|
|
||||||
|
.present-results-title {
|
||||||
|
margin-top: 8vh;
|
||||||
|
margin-bottom: 2vh;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
18
client/src/components/LoadingCircle/LoadingCircle.tsx
Normal file
18
client/src/components/LoadingCircle/LoadingCircle.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { CircularProgress } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import './loadingCircle.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingCircle: React.FC<Props> = ({ text }) => {
|
||||||
|
return (
|
||||||
|
<div className="loading-circle">
|
||||||
|
<div className="text-base">{text}</div>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingCircle;
|
||||||
5
client/src/components/LoadingCircle/loadingCircle.css
Normal file
5
client/src/components/LoadingCircle/loadingCircle.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.loading-circle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue