import react inside class for pipeline, not IDE

This commit is contained in:
JubaAzul 2025-01-15 10:27:28 -05:00
commit 15805e2e7e
79 changed files with 1303 additions and 292 deletions

View file

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

View file

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -18,4 +19,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.

View file

@ -1,2 +0,0 @@
VITE_BACKEND_URL=http://localhost:4400
VITE_AZURE_BACKEND_URL=http://localhost:4400

2
client/.env.development Normal file
View file

@ -0,0 +1,2 @@
VITE_BACKEND_URL=http://localhost:4400
VITE_BACKEND_SOCKET_URL=http://localhost:4400

View file

@ -1,18 +0,0 @@
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,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = { module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'] presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
}; };

29
client/eslint.config.js Normal file
View file

@ -0,0 +1,29 @@
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,3 +1,4 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */ /** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = { module.exports = {
@ -11,7 +12,8 @@ module.exports = {
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], //moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./jest.setup.cjs'], setupFiles: ['./jest.setup.cjs'],
moduleNameMapper: { moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy' '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'^src/constants$': '<rootDir>/src/__mocks__/constantsMock.tsx',
}, },
transformIgnorePatterns: ['node_modules/(?!nanoid/)'], transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
}; };

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
export interface QuizType { export interface QuizType {
_id: string; _id: string;
folderId: string; folderId: string;
folderName: string;
userId: string; userId: string;
title: string; title: string;
content: string[]; content: string[];

View file

@ -0,0 +1,13 @@
console.log('constantsMock.tsx is loaded');
// constants.tsx
const ENV_VARIABLES = {
MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
VITE_BACKEND_SOCKET_URL: process.env.VITE_BACKEND_SOCKET_URL || "",
};
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES };

View file

@ -9,6 +9,7 @@ describe('isQuizValid function', () => {
const validQuiz: QuizType = { const validQuiz: QuizType = {
_id: '1', _id: '1',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),
updated_at: new Date('2021-10-02'), updated_at: new Date('2021-10-02'),
@ -24,6 +25,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = { const invalidQuiz: QuizType = {
_id: '2', _id: '2',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
title: '', title: '',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),
@ -39,6 +41,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = { const invalidQuiz: QuizType = {
_id: '2', _id: '2',
folderId: 'test', folderId: 'test',
folderName: 'test',
userId: 'user', userId: 'user',
title: 'sample', title: 'sample',
created_at: new Date('2021-10-01'), created_at: new Date('2021-10-01'),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,8 +31,8 @@ Object.defineProperty(window, 'localStorage', {
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database) // NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
describe.skip('QuizService', () => { describe.skip('QuizService', () => {
const mockQuizzes: QuizType[] = [ const mockQuizzes: QuizType[] = [
{ folderId: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') }, { folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
{ folderId: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') }, { folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
]; ];
beforeEach(() => { beforeEach(() => {

View file

@ -1,16 +1,10 @@
//WebsocketService.test.tsx //WebsocketService.test.tsx
import WebsocketService from '../../services/WebsocketService'; import WebsocketService from '../../services/WebsocketService';
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../constants'; import { ENV_VARIABLES } from 'src/constants';
jest.mock('socket.io-client'); jest.mock('socket.io-client');
// jest.mock('../../constants', () => ({
// ENV_VARIABLES: {
// VITE_BACKEND_SOCKET_URL: 'https://ets-glitch-backend.glitch.me/'
// }
// }));
describe('WebSocketService', () => { describe('WebSocketService', () => {
let mockSocket: Partial<Socket>; let mockSocket: Partial<Socket>;

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')} (choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
{formatLatex(choice.text.text)} <div dangerouslySetInnerHTML={{ __html: formatLatex(choice.text.text) }} />
</div> </div>
</Button> </Button>
{choice.feedback && showAnswer && ( {choice.feedback && showAnswer && (

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
// constants.tsx // constants.tsx
const ENV_VARIABLES = { const ENV_VARIABLES = {
MODE: 'production', MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
VITE_BACKEND_SOCKET_URL: process.env.VITE_BACKEND_SOCKET_URL || "", VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
}; };
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`); console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);

View file

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

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from 'src/constants';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';

View file

@ -18,8 +18,11 @@ import {
IconButton, IconButton,
InputAdornment, InputAdornment,
Button, Button,
Card,
Tooltip, Tooltip,
NativeSelect NativeSelect,
CardContent,
styled,
} from '@mui/material'; } from '@mui/material';
import { import {
Search, Search,
@ -34,13 +37,43 @@ import {
// DriveFileMove // DriveFileMove
} from '@mui/icons-material'; } from '@mui/icons-material';
// Create a custom-styled Card component
const CustomCard = styled(Card)({
overflow: 'visible', // Override the overflow property
position: 'relative',
margin: '40px 0 20px 0', // Add top margin to make space for the tab
borderRadius: '8px',
paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
});
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [quizzes, setQuizzes] = useState<QuizType[]>([]); const [quizzes, setQuizzes] = useState<QuizType[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showImportModal, setShowImportModal] = useState<boolean>(false); const [showImportModal, setShowImportModal] = useState<boolean>(false);
const [folders, setFolders] = useState<FolderType[]>([]); const [folders, setFolders] = useState<FolderType[]>([]);
const [selectedFolder, setSelectedFolder] = useState<string>(''); // Selected folder const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
// Filter quizzes based on search term
// const filteredQuizzes = quizzes.filter(quiz =>
// quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
// );
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
// Group quizzes by folder
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
if (!acc[quiz.folderName]) {
acc[quiz.folderName] = [];
}
acc[quiz.folderName].push(quiz);
return acc;
}, {} as Record<string, QuizType[]>);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@ -49,7 +82,7 @@ const Dashboard: React.FC = () => {
return; return;
} }
else { else {
let userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
} }
@ -59,40 +92,23 @@ const Dashboard: React.FC = () => {
fetchData(); fetchData();
}, []); }, []);
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedFolder(event.target.value); setSelectedFolderId(event.target.value);
}; };
useEffect(() => { useEffect(() => {
const fetchQuizzesForFolder = async () => { const fetchQuizzesForFolder = async () => {
if (selectedFolder == '') { if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes); console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
// add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]) quizzes = quizzes.concat(folderQuizzes as QuizType[])
} }
@ -100,17 +116,19 @@ const Dashboard: React.FC = () => {
} }
else { else {
console.log("show some quizzes") console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder); const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
console.log("folderQuizzes: ", folderQuizzes); console.log("folderQuizzes: ", folderQuizzes);
// get the folder title from its id
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
setQuizzes(folderQuizzes as QuizType[]); setQuizzes(folderQuizzes as QuizType[]);
} }
}; };
fetchQuizzesForFolder(); fetchQuizzesForFolder();
}, [selectedFolder]); }, [selectedFolderId]);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -135,22 +153,24 @@ const Dashboard: React.FC = () => {
const handleDuplicateQuiz = async (quiz: QuizType) => { const handleDuplicateQuiz = async (quiz: QuizType) => {
try { try {
await ApiService.duplicateQuiz(quiz._id); await ApiService.duplicateQuiz(quiz._id);
if (selectedFolder == '') { if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizzes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes); console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
quizzes = quizzes.concat(folderQuizzes as QuizType[]) addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
} }
setQuizzes(quizzes as QuizType[]); setQuizzes(quizzes as QuizType[]);
} }
else { else {
console.log("show some quizzes") console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder); const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
setQuizzes(folderQuizzes as QuizType[]); setQuizzes(folderQuizzes as QuizType[]);
} }
@ -159,13 +179,6 @@ const Dashboard: React.FC = () => {
} }
}; };
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
const handleOnImport = () => { const handleOnImport = () => {
setShowImportModal(true); setShowImportModal(true);
@ -183,6 +196,7 @@ const Dashboard: React.FC = () => {
// questions[i] = QuestionService.ignoreImgTags(questions[i]); // questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]); const parsedItem = parse(questions[i]);
Template(parsedItem[0]); Template(parsedItem[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) { } catch (error) {
return false; return false;
} }
@ -191,30 +205,6 @@ const Dashboard: React.FC = () => {
return true; return true;
}; };
// const handleMoveQuiz = async (quiz: QuizType, newFolderId: string) => {
// try {
// await ApiService.moveQuiz(quiz._id, newFolderId);
// if (selectedFolder == '') {
// const folders = await ApiService.getUserFolders();
// var quizzes: QuizType[] = [];
// for (const folder of folders as FolderType[]) {
// const folderQuizzes = await ApiService.getFolderContent(folder._id);
// quizzes = quizzes.concat(folderQuizzes as QuizType[])
// }
// setQuizzes(quizzes as QuizType[]);
// }
// else {
// const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
// setQuizzes(folderQuizzes as QuizType[]);
// }
// } catch (error) {
// console.error('Error moving quiz:', error);
// }
// };
const downloadTxtFile = async (quiz: QuizType) => { const downloadTxtFile = async (quiz: QuizType) => {
try { try {
@ -227,7 +217,7 @@ const Dashboard: React.FC = () => {
//const { title, content } = selectedQuiz; //const { title, content } = selectedQuiz;
let quizContent = ""; let quizContent = "";
let title = selectedQuiz.title; const title = selectedQuiz.title;
console.log(selectedQuiz.content); console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => { selectedQuiz.content.forEach((question, qIndex) => {
const formattedQuestion = question.trim(); const formattedQuestion = question.trim();
@ -264,7 +254,7 @@ const Dashboard: React.FC = () => {
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType; const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id); setSelectedFolderId(newlyCreatedFolder._id);
} }
} catch (error) { } catch (error) {
@ -274,18 +264,17 @@ const Dashboard: React.FC = () => {
const handleDeleteFolder = async () => { const handleDeleteFolder = async () => {
try { try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?'); const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
if (confirmed) { if (confirmed) {
await ApiService.deleteFolder(selectedFolder); await ApiService.deleteFolder(selectedFolderId);
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
} }
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes") console.log("show all quizzes")
var quizzes: QuizType[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -294,19 +283,20 @@ const Dashboard: React.FC = () => {
} }
setQuizzes(quizzes as QuizType[]); setQuizzes(quizzes as QuizType[]);
setSelectedFolder(''); setSelectedFolderId('');
} catch (error) { } catch (error) {
console.error('Error deleting folder:', error); console.error('Error deleting folder:', error);
} }
}; };
const handleRenameFolder = async () => { const handleRenameFolder = async () => {
try { try {
// folderId: string GET THIS FROM CURRENT FOLDER // folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: string GET THIS FROM CURRENT FOLDER // currentTitle: string GET THIS FROM CURRENT FOLDER
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier"); const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
if (newTitle) { if (newTitle) {
await ApiService.renameFolder(selectedFolder, newTitle); await ApiService.renameFolder(selectedFolderId, newTitle);
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
@ -315,15 +305,16 @@ const Dashboard: React.FC = () => {
console.error('Error renaming folder:', error); console.error('Error renaming folder:', error);
} }
}; };
const handleDuplicateFolder = async () => { const handleDuplicateFolder = async () => {
try { try {
// folderId: string GET THIS FROM CURRENT FOLDER // folderId: string GET THIS FROM CURRENT FOLDER
await ApiService.duplicateFolder(selectedFolder); await ApiService.duplicateFolder(selectedFolderId);
// TODO set the selected folder to be the duplicated folder // TODO set the selected folder to be the duplicated folder
const userFolders = await ApiService.getUserFolders(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType; const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id); setSelectedFolderId(newlyCreatedFolder._id);
} catch (error) { } catch (error) {
console.error('Error duplicating folder:', error); console.error('Error duplicating folder:', error);
} }
@ -393,7 +384,7 @@ const Dashboard: React.FC = () => {
<NativeSelect <NativeSelect
id="select-folder" id="select-folder"
color="primary" color="primary"
value={selectedFolder} value={selectedFolderId}
onChange={handleSelectFolder} onChange={handleSelectFolder}
> >
<option value=""> Tous les dossiers... </option> <option value=""> Tous les dossiers... </option>
@ -416,7 +407,7 @@ const Dashboard: React.FC = () => {
<IconButton <IconButton
color="primary" color="primary"
onClick={handleRenameFolder} onClick={handleRenameFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <Edit /> </IconButton> > <Edit /> </IconButton>
</Tooltip> </Tooltip>
@ -424,7 +415,7 @@ const Dashboard: React.FC = () => {
<IconButton <IconButton
color="primary" color="primary"
onClick={handleDuplicateFolder} onClick={handleDuplicateFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <FolderCopy /> </IconButton> > <FolderCopy /> </IconButton>
</Tooltip> </Tooltip>
@ -433,7 +424,7 @@ const Dashboard: React.FC = () => {
aria-label="delete" aria-label="delete"
color="primary" color="primary"
onClick={handleDeleteFolder} onClick={handleDeleteFolder}
disabled={selectedFolder == ''} // cannot action on all disabled={selectedFolderId == ''} // cannot action on all
> <DeleteOutline /> </IconButton> > <DeleteOutline /> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
@ -461,74 +452,72 @@ const Dashboard: React.FC = () => {
</div> </div>
<div className='list'> <div className='list'>
{Object.keys(quizzesByFolder).map(folderName => (
<CustomCard key={folderName} className='folder-card'>
<div className='folder-tab'>{folderName}</div>
<CardContent>
{quizzesByFolder[folderName].map((quiz: QuizType) => (
<div className='quiz' key={quiz._id}>
<div className='title'>
<Tooltip title="Lancer quiz" placement="top">
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
</Button>
</Tooltip>
</div>
{filteredQuizzes.map((quiz: QuizType) => ( <div className='actions'>
<div className='quiz'> <Tooltip title="Télécharger quiz" placement="top">
<div className='title'> <IconButton
<Tooltip title="Lancer quiz" placement="top"> color="primary"
<Button onClick={() => downloadTxtFile(quiz)}
variant="outlined" > <FileDownload /> </IconButton>
onClick={() => handleLancerQuiz(quiz)} </Tooltip>
disabled={!validateQuiz(quiz.content)}
>
{quiz.title}
</Button>
</Tooltip>
</div>
<div className='actions'> <Tooltip title="Modifier quiz" placement="top">
<Tooltip title="Télécharger quiz" placement="top"> <IconButton
<IconButton color="primary"
color="primary" onClick={() => handleEditQuiz(quiz)}
onClick={() => downloadTxtFile(quiz)} > <Edit /> </IconButton>
> <FileDownload /> </IconButton> </Tooltip>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top"> <Tooltip title="Dupliquer quiz" placement="top">
<IconButton <IconButton
color="primary" color="primary"
onClick={() => handleEditQuiz(quiz)} onClick={() => handleDuplicateQuiz(quiz)}
> <Edit /> </IconButton> > <ContentCopy /> </IconButton>
</Tooltip> </Tooltip>
{/* <Tooltip title="Bouger quiz" placement="top"> <Tooltip title="Supprimer quiz" placement="top">
<IconButton <IconButton
color="primary" aria-label="delete"
onClick={() => handleMoveQuiz(quiz)} color="primary"
> <DriveFileMove /> </IconButton> onClick={() => handleRemoveQuiz(quiz)}
</Tooltip> */} > <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top"> <Tooltip title="Partager quiz" placement="top">
<IconButton <IconButton
color="primary" color="primary"
onClick={() => handleDuplicateQuiz(quiz)} onClick={() => handleShareQuiz(quiz)}
> <ContentCopy /> </IconButton> > <Share /> </IconButton>
</Tooltip> </Tooltip>
</div>
<Tooltip title="Supprimer quiz" placement="top"> </div>
<IconButton ))}
aria-label="delete" </CardContent>
color="primary" </CustomCard>
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
</Tooltip>
</div>
</div>
))} ))}
</div> </div>
<ImportModal <ImportModal
open={showImportModal} open={showImportModal}
handleOnClose={() => setShowImportModal(false)} handleOnClose={() => setShowImportModal(false)}
handleOnImport={handleOnImport} handleOnImport={handleOnImport}
selectedFolder={selectedFolder} selectedFolder={selectedFolderId}
/> />
</div> </div>
@ -536,3 +525,11 @@ const Dashboard: React.FC = () => {
}; };
export default Dashboard; export default Dashboard;
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
if (Array.isArray(folderQuizzes))
folderQuizzes.forEach((quiz) => {
quiz.folderName = folderName;
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
});
}

View file

@ -77,4 +77,43 @@ div:has(> #select-folder) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.dashboard .list .quiz .actions {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.folder-card {
position: relative;
/* margin: 40px 0 20px 0; /* Add top margin to make space for the tab */
border-radius: 8px;
color: #f9f9f9;
--outline-color: #e1e1e1;
border: 2px solid var(--outline-color);
}
.folder-tab {
position: absolute;
top: -33px;
left: 9px;
padding: 5px 10px;
border-radius: 8px 8px 0 0;
font-weight: bold;
white-space: nowrap; /* Prevent text from wrapping */
display: inline-block; /* Ensure the tab width is based on content */
border: 2px solid var(--outline-color);
border-bottom-style: none;
background-color: white; /* Optional: background color to match the card */
color: #3f51b5; /* Text color to match the outline */
}
/* .folder-card:nth-child(odd) {
background-color: #f9f9f9;
}
.folder-card:nth-child(even) {
background-color: #e0e0e0;
} */

View file

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

View file

@ -22,7 +22,6 @@
.quizEditor .editSection { .quizEditor .editSection {
width: 100%; width: 100%;
height: 78vh;
display: flex; display: flex;
} }

View file

@ -28,7 +28,7 @@ const Login: React.FC = () => {
const login = async () => { const login = async () => {
const result = await ApiService.login(email, password); const result = await ApiService.login(email, password);
if (result != true) { if (typeof result === "string") {
setConnectionError(result); setConnectionError(result);
return; return;
} }
@ -49,7 +49,7 @@ const Login: React.FC = () => {
variant="outlined" variant="outlined"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="Nom d'utilisateur" placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth
/> />
@ -60,7 +60,7 @@ const Login: React.FC = () => {
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Nom de la salle" placeholder="Mot de passe"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth
/> />

View file

@ -10,7 +10,7 @@ import webSocketService, { AnswerReceptionFromBackendType } from '../../../servi
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants'; import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType'; import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle'; import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
@ -124,8 +124,8 @@ const ManageRoom: React.FC = () => {
// This is here to make sure the correct value is sent when user join // This is here to make sure the correct value is sent when user join
if (socket) { if (socket) {
console.log(`Listening for user-joined in room ${roomName}`); console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => { socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion); webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') { } else if (quizMode === 'student') {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["src/*"]
},
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
@ -12,7 +16,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react",
/* Linting */ /* Linting */
"strict": true, "strict": true,

View file

@ -20,6 +20,11 @@ export default defineConfig({
pluginChecker({ typescript: true }), pluginChecker({ typescript: true }),
EnvironmentPlugin(filteredEnv), EnvironmentPlugin(filteredEnv),
], ],
resolve: {
alias: {
'src': '/src'
}
},
preview: { preview: {
port: 5173, port: 5173,
strictPort: true strictPort: true

View file

@ -3,12 +3,13 @@ services:
frontend: frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest image: fuhrmanator/evaluetonsavoir-frontend:latest
container_name: frontend container_name: frontend
environment:
# Define empty VITE_BACKEND_URL because it's production
- VITE_BACKEND_URL=
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
- VITE_BACKEND_SOCKET_URL=
ports: ports:
- "5173:5173" - "5173:5173"
environment:
VITE_BACKEND_URL: "http://localhost:4400"
# don't define VITE_BACKEND_SOCKET_URL so it will default to window.location.host
# VITE_BACKEND_SOCKET_URL: ""
restart: always restart: always
backend: backend:

10
export-collections.bash Normal file
View file

@ -0,0 +1,10 @@
#!/bin/bash
DB_NAME="evaluetonsavoir"
OUTPUT_DIR="/data/db"
collections=$(mongosh $DB_NAME --quiet --eval "db.getCollectionNames().join(' ')")
for collection in $collections; do
mongoexport --db=$DB_NAME --collection=$collection --out=$OUTPUT_DIR/$collection.json --jsonArray
done

View file

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

View file

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

View file

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

View file

@ -43,6 +43,7 @@ const imagesRouter = require('./routers/images.js');
// Setup environment // Setup environment
dotenv.config(); dotenv.config();
const isDev = process.env.NODE_ENV === 'development';
const errorHandler = require("./middleware/errorHandler.js"); const errorHandler = require("./middleware/errorHandler.js");
// Start app // Start app
@ -51,6 +52,7 @@ const cors = require("cors");
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const configureServer = (httpServer, isDev) => { const configureServer = (httpServer, isDev) => {
console.log(`Configuring server with isDev: ${isDev}`);
return new Server(httpServer, { return new Server(httpServer, {
path: "/socket.io", path: "/socket.io",
cors: { cors: {
@ -63,13 +65,12 @@ const configureServer = (httpServer, isDev) => {
}; };
// Start sockets (depending on the dev or prod environment) // Start sockets (depending on the dev or prod environment)
let server = http.createServer(app); const server = http.createServer(app);
let isDev = process.env.NODE_ENV === 'development';
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`); console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
const io = configureServer(server); const io = configureServer(server, isDev);
console.log(`server.io configured: ${io.secure ? 'secure' : 'not secure'}`); console.log(`Server configured with cors.origin: ${io.opts.cors.origin} and secure: ${io.opts.secure}`);
setupWebsocket(io); setupWebsocket(io);
console.log(`Websocket setup with on() listeners.`); console.log(`Websocket setup with on() listeners.`);

View file

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

View file

@ -1,6 +1,6 @@
//controller //controller
const AppError = require('../middleware/AppError.js'); const AppError = require('../middleware/AppError.js');
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'); 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');
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class FoldersController { class FoldersController {

View file

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

32
server/eslint.config.mjs Normal file
View file

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

View file

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

871
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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