Compare commits

..

No commits in common. "da3e810a32f198f4f1e65c4ff22f7a084dd0f342" and "e5e7f61b7159aca7aaaa2bdf1b09891c7474bd56" have entirely different histories.

63 changed files with 252 additions and 3049 deletions

View file

@ -21,13 +21,9 @@ jobs:
with:
node-version: '18'
- name: Install Dependencies, lint and Run Tests
- name: Install Dependencies and Run Tests
run: |
echo "Installing dependencies..."
npm ci
echo "Running ESLint..."
npx eslint .
echo "Running tests..."
npm test
working-directory: ${{ matrix.directory }}

18
client/.eslintrc.cjs Normal file
View 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 },
],
},
}

View file

@ -1,4 +1,3 @@
/* eslint-disable no-undef */
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
};

View file

@ -1,29 +0,0 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: globals.browser,
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
settings: {
react: {
version: "detect", // Automatically detect the React version
},
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];

View file

@ -1,4 +1,3 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {

View file

@ -1,3 +1,2 @@
/* eslint-disable no-undef */
process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';

2098
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,6 @@
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@eslint/js": "^9.18.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
@ -55,16 +54,13 @@
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^9.18.0",
"eslint-plugin-react": "^7.37.3",
"eslint": "^9.10.0",
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.5",
"vite-plugin-environment": "^1.1.3"
}

View file

@ -1,4 +1,3 @@
import React from 'react';
// App.tsx
import { Routes, Route } from 'react-router-dom';

View file

@ -1,5 +1,4 @@
// Modal.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';

View file

@ -1,5 +1,4 @@
// Editor.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Editor from '../../../components/Editor/Editor';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';

View file

@ -32,7 +32,7 @@ describe('TextType', () => {
// Hint -- if the output changes because of a change in the code or library, you can update
// by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct)
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
});
@ -42,17 +42,19 @@ describe('TextType', () => {
format: 'plain'
};
// hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
});
it('should format text with a katex matrix correctly', () => {
const input: TextFormat = {
// eslint-disable-next-line no-useless-escape
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`,
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}
a&b \\\\
c&d
\\end\{pmatrix\}`,
format: 'plain'
};
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow></mrow><annotation encoding="application/x-tex"></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow></mrow><annotation encoding=\"application/x-tex\"></annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
expect(textType({ text: input })).toContain(expectedOutput);
});

View file

@ -1,5 +1,4 @@
//styles.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
@ -28,7 +27,6 @@ function convertStylesToObject(styles: string): React.CSSProperties {
styles.split(';').forEach((style) => {
const [property, value] = style.split(':');
if (property && value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(styleObject as any)[property.trim()] = value.trim();
}
});

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import DragAndDrop from '../../../components/ImportModal/ImportModal';
@ -70,4 +69,4 @@ describe('DragAndDrop Component', () => {
target: { files: [file] },
});
});
});
});

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';

View file

@ -1,5 +1,4 @@
// NumericalQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion';

View file

@ -1,5 +1,4 @@
// Question.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Questions from '../../../components/Questions/Question';

View file

@ -1,5 +1,4 @@
// ShortAnswerQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
@ -12,7 +11,6 @@ describe('ShortAnswerQuestion Component', () => {
questionTitle: 'Sample Question',
choices: [
{
id: '1',
feedback: {
format: 'text',
text: 'Correct answer feedback'
@ -24,7 +22,6 @@ describe('ShortAnswerQuestion Component', () => {
}
},
{
id: '2',
feedback: null,
isCorrect: false,
text: {
@ -61,7 +58,7 @@ describe('ShortAnswerQuestion Component', () => {
expect(submitButton).toBeDisabled();
});
it('not submitted answer if nothing is entered', () => {
it('not submited answer if nothing is entered', () => {
const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton);

View file

@ -1,5 +1,4 @@
// TrueFalseQuestion.test.tsx
import React from 'react';
import { render, fireEvent, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';

View file

@ -1,5 +1,4 @@
// Importez le type UserType s'il n'est pas déjà importé
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BrowserRouter } from 'react-router-dom';
@ -33,4 +32,4 @@ describe('Home', () => {
fireEvent.click(teacherButton);
expect(window.location.pathname).toBe('/teacher/dashboard');
});
});
});

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';

View file

@ -1,5 +1,4 @@
//TeacherModeQuiz.test.tsx
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';

View file

@ -1,5 +1,4 @@
// GoBackButton.tsx
import React from 'react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -34,7 +33,7 @@ const DisconnectButton: React.FC<Props> = ({
};
const handleOnReturn = () => {
if (onReturn) {
if (!!onReturn) {
onReturn();
} else {
navigate(-1);

View file

@ -1,16 +1,20 @@
import * as React from 'react';
import './footer.css';
type FooterProps = object; //empty object
interface FooterProps {
const Footer: React.FC<FooterProps> = () => {
}
const Footer: React.FC<FooterProps> = ({ }) => {
return (
<div className="footer">
<div className="footer-content">
Réalisé avec à Montréal par des finissantes de l&apos;ETS
Réalisé avec à Montréal par des finissantes de l'ETS
</div>
<div className="footer-links">
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
</div>

View file

@ -28,9 +28,9 @@ const GiftCheatSheet: React.FC = () => {
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} // OU \nQuestion {#=PetitNombre..GrandNombre\n}\n// La tolérance est un pourcentage.\n// La réponse doit être comprise entre PetitNombre et GrandNombre";
return (
<div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
<span>
L&apos;éditeur utilise le format GIFT (General Import Format Template) créé pour la
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
syntaxe pour chaque type de question&nbsp;:
</span>
@ -126,7 +126,7 @@ const GiftCheatSheet: React.FC = () => {
<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 \
réponses ou feedback, vous devez 'échapper' ces derniers en ajoutant un \
devant:
</p>
<pre>
@ -140,9 +140,9 @@ const GiftCheatSheet: React.FC = () => {
<h4> 8. LaTeX et Markdown</h4>
<p>
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
à «échapper» les caractères spéciaux mentionnés plus haut.
à 'échapper' les caractères spéciaux mentionnés plus haut.
</p>
<p>Exemple d&apos;équation:</p>
<p>Exemple d'équation:</p>
<pre>
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
@ -167,16 +167,16 @@ const GiftCheatSheet: React.FC = () => {
{'")'}
</code>
</pre>
<p>Exemple d&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p>
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
<pre>
<code className="question-code-block">
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code>
</pre>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l&apos;URL de l&apos;image.</p>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
<p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
<p style={{ color: 'red' }}>
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
supportée.
</p>
</div>
@ -184,7 +184,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type">
<h4> 10. Informations supplémentaires </h4>
<p>
GIFT supporte d&apos;autres formats de questions que nous ne gérons pas sur cette
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
application.
</p>
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>

View file

@ -30,7 +30,7 @@ export function formatLatex(text: string): string {
*/
export default function textType({ text }: TextTypeOptions) {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = '';
switch (text.format) {
case 'moodle':
case 'plain':
@ -40,7 +40,7 @@ export default function textType({ text }: TextTypeOptions) {
// Strip outer paragraph tags (not a great approach with regex)
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
case 'markdown':
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
const parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
default:
throw new Error(`Unsupported text format: ${text.format}`);

View file

@ -168,7 +168,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
<DialogContentText sx={{ textAlign: 'center' }}>
Déposer des fichiers ici ou
<br />
cliquez pour ouvrir l&apos;explorateur des fichiers
cliquez pour ouvrir l'explorateur des fichiers
</DialogContentText>
</div>
<Download color="primary" />

View file

@ -1,4 +1,3 @@
import React from 'react';
import {
Button,
Dialog,

View file

@ -300,7 +300,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
<div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell

View file

@ -1,4 +1,3 @@
import React from 'react';
import { IconButton } from '@mui/material';
import { ChevronLeft, ChevronRight } from '@mui/icons-material';

View file

@ -42,7 +42,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = (
<MultipleChoiceQuestion
questionStem={question.stem}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
choices={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
@ -78,7 +78,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = (
<ShortAnswerQuestion
questionContent={question.stem}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
choices={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}

View file

@ -10,7 +10,6 @@ type Choices = {
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
id: string;
};
interface Props {
@ -34,9 +33,7 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
<>
<div className="correct-answer-text mb-1">
{choices.map((choice) => (
<div key={choice.id} className="mb-1">
{choice.text.text}
</div>
<div className="mb-1">{choice.text.text}</div>
))}
</div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}

View file

@ -1,5 +1,4 @@
// GoBackButton.tsx
import React from 'react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -34,7 +33,7 @@ const ReturnButton: React.FC<Props> = ({
};
const handleOnReturn = () => {
if (onReturn) {
if (!!onReturn) {
onReturn();
} else {
navigate(-1);

View file

@ -1,4 +1,3 @@
import React from 'react';
import { Box, Button, Chip } from '@mui/material';
import { StudentType } from '../../Types/StudentType';
import { PlayArrow } from '@mui/icons-material';

View file

@ -1,4 +1,3 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
@ -28,16 +27,10 @@ const theme = createTheme({
}
});
const rootElement = document.getElementById('root');
if (rootElement) {
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
);
} else {
console.error('Root element not found');
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
);

View file

@ -82,7 +82,7 @@ const Dashboard: React.FC = () => {
return;
}
else {
const userFolders = await ApiService.getUserFolders();
let userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
}
@ -102,7 +102,7 @@ const Dashboard: React.FC = () => {
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
let quizzes: QuizType[] = [];
var quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -155,8 +155,8 @@ const Dashboard: React.FC = () => {
await ApiService.duplicateQuiz(quiz._id);
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizzes")
let quizzes: QuizType[] = [];
console.log("show all quizes")
var quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -196,7 +196,6 @@ const Dashboard: React.FC = () => {
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]);
Template(parsedItem[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return false;
}
@ -217,7 +216,7 @@ const Dashboard: React.FC = () => {
//const { title, content } = selectedQuiz;
let quizContent = "";
const title = selectedQuiz.title;
let title = selectedQuiz.title;
console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => {
const formattedQuestion = question.trim();
@ -274,7 +273,7 @@ const Dashboard: React.FC = () => {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizzes")
let quizzes: QuizType[] = [];
var quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);

View file

@ -162,10 +162,8 @@ const QuizForm: React.FC = () => {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
}
};
@ -247,7 +245,7 @@ const QuizForm: React.FC = () => {
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d&apos;abord choisir une image à téléverser.
Veuillez d'abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">

View file

@ -28,7 +28,7 @@ const Login: React.FC = () => {
const login = async () => {
const result = await ApiService.login(email, password);
if (typeof result === "string") {
if (result != true) {
setConnectionError(result);
return;
}

View file

@ -124,8 +124,8 @@ const ManageRoom: React.FC = () => {
// This is here to make sure the correct value is sent when user join
if (socket) {
console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') {

View file

@ -28,7 +28,7 @@ const Register: React.FC = () => {
const register = async () => {
const result = await ApiService.register(email, password);
if (typeof result === 'string') {
if (result != true) {
setConnectionError(result);
return;
}
@ -70,7 +70,7 @@ const Register: React.FC = () => {
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password}
>
S&apos;inscrire
S'inscrire
</LoadingButton>
</LoginContainer>

View file

@ -27,7 +27,7 @@ const ResetPassword: React.FC = () => {
const reset = async () => {
const result = await ApiService.resetPassword(email);
if (typeof result === 'string') {
if (result != true) {
setConnectionError(result);
return;
}

View file

@ -4,8 +4,6 @@ import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType';
import { ENV_VARIABLES } from 'src/constants';
type ApiResponse = boolean | string;
class ApiService {
private BASE_URL: string;
private TTL: number;
@ -19,7 +17,7 @@ class ApiService {
return `${this.BASE_URL}/api${endpoint}`;
}
private constructRequestHeaders() {
private constructRequestHeaders(): any {
if (this.isLoggedIn()) {
return {
Authorization: `Bearer ${this.getToken()}`,
@ -88,7 +86,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async register(email: string, password: string): Promise<ApiResponse> {
public async register(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -124,7 +122,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async login(email: string, password: string): Promise<ApiResponse> {
public async login(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -148,13 +146,8 @@ class ApiService {
} catch (error) {
console.log("Error details: ", error);
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
if (err.status === 401) {
return 'Email ou mot de passe incorrect.';
}
const data = err.response?.data as { error: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.';
}
@ -164,10 +157,10 @@ class ApiService {
}
/**
* @returns true if successful
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async resetPassword(email: string): Promise<ApiResponse> {
public async resetPassword(email: string): Promise<any> {
try {
if (!email) {
@ -203,7 +196,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<ApiResponse> {
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<any> {
try {
if (!email || !oldPassword || !newPassword) {
@ -239,7 +232,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteUser(email: string, password: string): Promise<ApiResponse> {
public async deleteUser(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -277,7 +270,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createFolder(title: string): Promise<ApiResponse> {
public async createFolder(title: string): Promise<any> {
try {
if (!title) {
@ -382,7 +375,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteFolder(folderId: string): Promise<ApiResponse> {
public async deleteFolder(folderId: string): Promise<any> {
try {
if (!folderId) {
@ -417,7 +410,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
public async renameFolder(folderId: string, newTitle: string): Promise<any> {
try {
if (!folderId || !newTitle) {
@ -448,7 +441,7 @@ class ApiService {
}
}
public async duplicateFolder(folderId: string): Promise<ApiResponse> {
public async duplicateFolder(folderId: string): Promise<any> {
try {
if (!folderId) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -480,7 +473,7 @@ class ApiService {
}
}
public async copyFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
public async copyFolder(folderId: string, newTitle: string): Promise<any> {
try {
if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -517,7 +510,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createQuiz(title: string, content: string[], folderId: string): Promise<ApiResponse> {
public async createQuiz(title: string, content: string[], folderId: string): Promise<any> {
try {
if (!title || !content || !folderId) {
@ -588,7 +581,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteQuiz(quizId: string): Promise<ApiResponse> {
public async deleteQuiz(quizId: string): Promise<any> {
try {
if (!quizId) {
@ -623,7 +616,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<ApiResponse> {
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<any> {
try {
if (!quizId || !newTitle || !newContent) {
@ -659,7 +652,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async moveQuiz(quizId: string, newFolderId: string): Promise<ApiResponse> {
public async moveQuiz(quizId: string, newFolderId: string): Promise<any> {
try {
if (!quizId || !newFolderId) {
@ -696,7 +689,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async duplicateQuiz(quizId: string): Promise<ApiResponse> {
public async duplicateQuiz(quizId: string): Promise<any> {
const url: string = this.constructRequestUrl(`/quiz/duplicate`);
@ -710,7 +703,7 @@ class ApiService {
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
}
return result.status === 200;
return result;
} catch (error) {
console.error("Error details: ", error);
@ -730,9 +723,9 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<ApiResponse> {
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<any> {
try {
console.log(quizId, newTitle, folderId);
console.log(quizId, newTitle), folderId;
return "Route not implemented yet!";
} catch (error) {
@ -748,7 +741,7 @@ class ApiService {
}
}
async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
async ShareQuiz(quizId: string, email: string): Promise<any> {
try {
if (!quizId || !email) {
throw new Error(`quizId and email are required.`);
@ -807,7 +800,7 @@ class ApiService {
}
}
async receiveSharedQuiz(quizId: string, folderId: string): Promise<ApiResponse> {
async receiveSharedQuiz(quizId: string, folderId: string): Promise<any> {
try {
if (!quizId || !folderId) {
throw new Error(`quizId and folderId are required.`);
@ -876,8 +869,7 @@ class ApiService {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
return `ERROR : ${msg}`;
return `ERROR : ${data?.error}` || 'ERROR : Erreur serveur inconnue lors de la requête.';
}
return `ERROR : Une erreur inattendue s'est produite.`

View file

@ -1,4 +1,4 @@
export function escapeForGIFT(link: string): string {
const specialChars = /[{}#~=<>\\:]/g;
const specialChars = /[{}#~=<>\:]/g;
return link.replace(specialChars, (match) => `\\${match}`);
}

View file

@ -16,7 +16,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"jsx": "react-jsx",
/* Linting */
"strict": true,

View file

@ -1,3 +1,4 @@
const { create } = require('../middleware/jwtToken');
const Folders = require('../models/folders');
const ObjectId = require('mongodb').ObjectId;
const Quizzes = require('../models/quiz');

View file

@ -1,5 +1,3 @@
/* eslint-disable */
// const request = require('supertest');
// const app = require('../app.js');
// // const app = require('../routers/images.js');

View file

@ -2,6 +2,7 @@ const Users = require('../models/users');
const bcrypt = require('bcrypt');
const Quizzes = require('../models/quiz');
const Folders = require('../models/folders');
const AppError = require('../middleware/AppError');
const { ObjectId } = require('mongodb');
jest.mock('bcrypt');

View file

@ -18,7 +18,7 @@ exports.USER_ALREADY_EXISTS = {
}
exports.LOGIN_CREDENTIALS_ERROR = {
message: 'L\'email et le mot de passe ne correspondent pas.',
code: 401
code: 400
}
exports.GENERATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
@ -130,4 +130,4 @@ exports.NOT_IMPLEMENTED = {
// static badRequest(res, message) {400
// static unauthorized(res, message) {401
// static notFound(res, message) {404
// static serverError(res, message) {505
// static serverError(res, message) {505

View file

@ -1,6 +1,6 @@
//controller
const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes');
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class FoldersController {

View file

@ -1,13 +1,13 @@
const emailer = require('../config/email.js');
const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR } = require('../constants/errorCodes');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes');
class QuizController {
constructor(quizModel, foldersModel) {
this.quizzes = quizModel;
this.folders = foldersModel;
this.quizzes = quizModel;
}
create = async (req, res, next) => {
@ -165,7 +165,7 @@ class QuizController {
}
};
copy = async (req, _res, _next) => {
copy = async (req, res, next) => {
const { quizId, newTitle, folderId } = req.body;
if (!quizId || !newTitle || !folderId) {
@ -207,7 +207,7 @@ class QuizController {
}
// Call the method from the Quiz model to delete quizzes by folder ID
await this.quizzes.deleteQuizzesByFolderId(folderId);
await Quiz.deleteQuizzesByFolderId(folderId);
return res.status(200).json({
message: 'Quizzes deleted successfully.'
@ -232,7 +232,7 @@ class QuizController {
try {
const existingFile = await this.quizzes.quizExists(title, userId);
return existingFile !== null;
} catch (_error) {
} catch (error) {
throw new AppError(GETTING_QUIZ_ERROR);
}
};

View file

@ -1,32 +0,0 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
...globals.jest, // Add Jest globals
},
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrors": "all", // Ignore all catch clause parameters
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
},
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
pluginJs.configs.recommended,
];

View file

@ -1,7 +1,7 @@
const AppError = require("./AppError");
const fs = require('fs');
const errorHandler = (error, req, res) => {
const errorHandler = (error, req, res, next) => {
console.log("ERROR", error);
if (error instanceof AppError) {

View file

@ -1,3 +1,4 @@
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class Images {
@ -7,8 +8,8 @@ class Images {
}
async upload(file, userId) {
await this.db.connect()
const conn = this.db.getConnection();
await db.connect()
const conn = db.getConnection();
const imagesCollection = conn.collection('images');
@ -26,8 +27,8 @@ class Images {
}
async get(id) {
await this.db.connect()
const conn = this.db.getConnection();
await db.connect()
const conn = db.getConnection();
const imagesCollection = conn.collection('images');

871
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,10 +25,7 @@
"socket.io-client": "^4.7.2"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"cross-env": "^7.0.3",
"eslint": "^9.18.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
"jest-mock": "^29.7.0",
"nodemon": "^3.0.1",