mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
import react inside class for pipeline, not IDE
This commit is contained in:
commit
15805e2e7e
79 changed files with 1303 additions and 292 deletions
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
|
@ -21,9 +21,13 @@ jobs:
|
|||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install Dependencies and Run Tests
|
||||
- name: Install Dependencies, lint and Run Tests
|
||||
run: |
|
||||
echo "Installing dependencies..."
|
||||
npm ci
|
||||
echo "Running ESLint..."
|
||||
npx eslint .
|
||||
echo "Running tests..."
|
||||
npm test
|
||||
working-directory: ${{ matrix.directory }}
|
||||
|
||||
|
|
|
|||
1
LICENSE
1
LICENSE
|
|
@ -1,6 +1,7 @@
|
|||
MIT License
|
||||
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
VITE_BACKEND_URL=http://localhost:4400
|
||||
VITE_AZURE_BACKEND_URL=http://localhost:4400
|
||||
2
client/.env.development
Normal file
2
client/.env.development
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
VITE_BACKEND_URL=http://localhost:4400
|
||||
VITE_BACKEND_SOCKET_URL=http://localhost:4400
|
||||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
|
||||
};
|
||||
|
|
|
|||
29
client/eslint.config.js
Normal file
29
client/eslint.config.js
Normal 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,
|
||||
];
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-undef */
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -11,7 +12,8 @@ module.exports = {
|
|||
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFiles: ['./jest.setup.cjs'],
|
||||
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/)'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
/* eslint-disable no-undef */
|
||||
process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
|
||||
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"@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",
|
||||
|
|
@ -54,13 +55,16 @@
|
|||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"@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-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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
// App.tsx
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
export interface QuizType {
|
||||
_id: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
content: string[];
|
||||
|
|
|
|||
13
client/src/__mocks__/constantsMock.tsx
Normal file
13
client/src/__mocks__/constantsMock.tsx
Normal 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 };
|
||||
|
|
@ -9,6 +9,7 @@ describe('isQuizValid function', () => {
|
|||
const validQuiz: QuizType = {
|
||||
_id: '1',
|
||||
folderId: 'test',
|
||||
folderName: 'test',
|
||||
userId: 'user',
|
||||
created_at: new Date('2021-10-01'),
|
||||
updated_at: new Date('2021-10-02'),
|
||||
|
|
@ -24,6 +25,7 @@ describe('isQuizValid function', () => {
|
|||
const invalidQuiz: QuizType = {
|
||||
_id: '2',
|
||||
folderId: 'test',
|
||||
folderName: 'test',
|
||||
userId: 'user',
|
||||
title: '',
|
||||
created_at: new Date('2021-10-01'),
|
||||
|
|
@ -39,6 +41,7 @@ describe('isQuizValid function', () => {
|
|||
const invalidQuiz: QuizType = {
|
||||
_id: '2',
|
||||
folderId: 'test',
|
||||
folderName: 'test',
|
||||
userId: 'user',
|
||||
title: 'sample',
|
||||
created_at: new Date('2021-10-01'),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
|
||||
|
|
|
|||
|
|
@ -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,19 +42,17 @@ 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 = {
|
||||
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}
|
||||
a&b \\\\
|
||||
c&d
|
||||
\\end\{pmatrix\}`,
|
||||
// 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\}`,
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//styles.test.tsx
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ function convertStylesToObject(styles: string): React.CSSProperties {
|
|||
styles.split(';').forEach((style) => {
|
||||
const [property, value] = style.split(':');
|
||||
if (property && value) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(styleObject as any)[property.trim()] = value.trim();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import DragAndDrop from '../../../components/ImportModal/ImportModal';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
@ -11,6 +12,7 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
questionTitle: 'Sample Question',
|
||||
choices: [
|
||||
{
|
||||
id: '1',
|
||||
feedback: {
|
||||
format: 'text',
|
||||
text: 'Correct answer feedback'
|
||||
|
|
@ -22,6 +24,7 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
feedback: null,
|
||||
isCorrect: false,
|
||||
text: {
|
||||
|
|
@ -58,7 +61,7 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
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');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// 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';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { parse } from 'gift-pegjs';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//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';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ Object.defineProperty(window, 'localStorage', {
|
|||
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
|
||||
describe.skip('QuizService', () => {
|
||||
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', 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: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
||||
{ folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
//WebsocketService.test.tsx
|
||||
import WebsocketService from '../../services/WebsocketService';
|
||||
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('../../constants', () => ({
|
||||
// ENV_VARIABLES: {
|
||||
// VITE_BACKEND_SOCKET_URL: 'https://ets-glitch-backend.glitch.me/'
|
||||
// }
|
||||
// }));
|
||||
|
||||
describe('WebSocketService', () => {
|
||||
let mockSocket: Partial<Socket>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// GoBackButton.tsx
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||
|
|
@ -33,7 +34,7 @@ const DisconnectButton: React.FC<Props> = ({
|
|||
};
|
||||
|
||||
const handleOnReturn = () => {
|
||||
if (!!onReturn) {
|
||||
if (onReturn) {
|
||||
onReturn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import './footer.css';
|
||||
|
||||
interface FooterProps {
|
||||
type FooterProps = object; //empty object
|
||||
|
||||
}
|
||||
|
||||
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 finissant•e•s de l'ETS
|
||||
Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a>
|
||||
<span className="divider">|</span>
|
||||
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
|
||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
|
||||
<span className="divider">|</span>
|
||||
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'éditeur</h2>
|
||||
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
|
||||
<span>
|
||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
|
||||
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
|
||||
syntaxe pour chaque type de question :
|
||||
</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'é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'une question Vrai/Faux avec l'image d'un chat:</p>
|
||||
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
|
||||
<pre>
|
||||
<code className="question-code-block">
|
||||
{'[markdown]Ceci est un chat: \n\n{T}'}
|
||||
</code>
|
||||
</pre>
|
||||
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
||||
<p>Note : les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
|
||||
<p>Note : On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
|
||||
<p style={{ color: 'red' }}>
|
||||
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
||||
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
|
||||
supportée.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -184,7 +184,7 @@ const GiftCheatSheet: React.FC = () => {
|
|||
<div className="question-type">
|
||||
<h4> 10. Informations supplémentaires </h4>
|
||||
<p>
|
||||
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
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');
|
||||
default:
|
||||
throw new Error(`Unsupported text format: ${text.format}`);
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
|
|||
<DialogContentText sx={{ textAlign: 'center' }}>
|
||||
Déposer des fichiers ici ou
|
||||
<br />
|
||||
cliquez pour ouvrir l'explorateur des fichiers
|
||||
cliquez pour ouvrir l'explorateur des fichiers
|
||||
</DialogContentText>
|
||||
</div>
|
||||
<Download color="primary" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
|
|
|
|||
|
|
@ -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'utilisateur</div>
|
||||
<div className="text-base text-bold">Nom d'utilisateur</div>
|
||||
</TableCell>
|
||||
{Array.from({ length: maxQuestions }, (_, index) => (
|
||||
<TableCell
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
|
|||
(choice.isCorrect ? '✅' : '❌')}
|
||||
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
||||
<div className={`answer-text ${selected}`}>
|
||||
{formatLatex(choice.text.text)}
|
||||
<div dangerouslySetInnerHTML={{ __html: formatLatex(choice.text.text) }} />
|
||||
</div>
|
||||
</Button>
|
||||
{choice.feedback && showAnswer && (
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const Question: React.FC<QuestionProps> = ({
|
|||
questionTypeComponent = (
|
||||
<MultipleChoiceQuestion
|
||||
questionStem={question.stem}
|
||||
choices={question.choices}
|
||||
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
|
||||
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}
|
||||
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
|
||||
handleOnSubmitAnswer={handleOnSubmitAnswer}
|
||||
showAnswer={showAnswer}
|
||||
globalFeedback={question.globalFeedback?.text}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type Choices = {
|
|||
isCorrect: boolean;
|
||||
text: { format: string; text: string };
|
||||
weigth?: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
|
@ -33,7 +34,9 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
|
|||
<>
|
||||
<div className="correct-answer-text mb-1">
|
||||
{choices.map((choice) => (
|
||||
<div className="mb-1">{choice.text.text}</div>
|
||||
<div key={choice.id} className="mb-1">
|
||||
{choice.text.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// GoBackButton.tsx
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||
|
|
@ -33,7 +34,7 @@ const ReturnButton: React.FC<Props> = ({
|
|||
};
|
||||
|
||||
const handleOnReturn = () => {
|
||||
if (!!onReturn) {
|
||||
if (onReturn) {
|
||||
onReturn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Box, Button, Chip } from '@mui/material';
|
||||
import { StudentType } from '../../Types/StudentType';
|
||||
import { PlayArrow } from '@mui/icons-material';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// 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 || "",
|
||||
VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
|
||||
VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
|
||||
};
|
||||
|
||||
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
|
|
@ -27,6 +28,9 @@ const theme = createTheme({
|
|||
}
|
||||
});
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
|
|
@ -34,3 +38,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
} else {
|
||||
console.error('Root element not found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { ENV_VARIABLES } from '../../../constants';
|
||||
import { ENV_VARIABLES } from 'src/constants';
|
||||
|
||||
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
|
||||
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ import {
|
|||
IconButton,
|
||||
InputAdornment,
|
||||
Button,
|
||||
Card,
|
||||
Tooltip,
|
||||
NativeSelect
|
||||
NativeSelect,
|
||||
CardContent,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search,
|
||||
|
|
@ -34,13 +37,43 @@ import {
|
|||
// DriveFileMove
|
||||
} 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 navigate = useNavigate();
|
||||
const [quizzes, setQuizzes] = useState<QuizType[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showImportModal, setShowImportModal] = useState<boolean>(false);
|
||||
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(() => {
|
||||
const fetchData = async () => {
|
||||
|
|
@ -49,7 +82,7 @@ const Dashboard: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
else {
|
||||
let userFolders = await ApiService.getUserFolders();
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
|
||||
setFolders(userFolders as FolderType[]);
|
||||
}
|
||||
|
|
@ -59,40 +92,23 @@ const Dashboard: React.FC = () => {
|
|||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedFolder(event.target.value);
|
||||
setSelectedFolderId(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQuizzesForFolder = async () => {
|
||||
|
||||
if (selectedFolder == '') {
|
||||
if (selectedFolderId == '') {
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
console.log("show all quizes")
|
||||
var quizzes: QuizType[] = [];
|
||||
let quizzes: QuizType[] = [];
|
||||
|
||||
for (const folder of folders as FolderType[]) {
|
||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||
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[])
|
||||
}
|
||||
|
||||
|
|
@ -100,17 +116,19 @@ const Dashboard: React.FC = () => {
|
|||
}
|
||||
else {
|
||||
console.log("show some quizzes")
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||
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[]);
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
fetchQuizzesForFolder();
|
||||
}, [selectedFolder]);
|
||||
}, [selectedFolderId]);
|
||||
|
||||
|
||||
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
|
@ -135,22 +153,24 @@ const Dashboard: React.FC = () => {
|
|||
const handleDuplicateQuiz = async (quiz: QuizType) => {
|
||||
try {
|
||||
await ApiService.duplicateQuiz(quiz._id);
|
||||
if (selectedFolder == '') {
|
||||
if (selectedFolderId == '') {
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
console.log("show all quizes")
|
||||
var quizzes: QuizType[] = [];
|
||||
console.log("show all quizzes")
|
||||
let quizzes: QuizType[] = [];
|
||||
|
||||
for (const folder of folders as FolderType[]) {
|
||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[])
|
||||
addFolderTitleToQuizzes(folderQuizzes, folder.title);
|
||||
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
|
||||
}
|
||||
|
||||
setQuizzes(quizzes as QuizType[]);
|
||||
}
|
||||
else {
|
||||
console.log("show some quizzes")
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
|
||||
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
|
||||
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
|
||||
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 = () => {
|
||||
setShowImportModal(true);
|
||||
|
||||
|
|
@ -183,6 +196,7 @@ 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;
|
||||
}
|
||||
|
|
@ -191,30 +205,6 @@ const Dashboard: React.FC = () => {
|
|||
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) => {
|
||||
|
||||
try {
|
||||
|
|
@ -227,7 +217,7 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
//const { title, content } = selectedQuiz;
|
||||
let quizContent = "";
|
||||
let title = selectedQuiz.title;
|
||||
const title = selectedQuiz.title;
|
||||
console.log(selectedQuiz.content);
|
||||
selectedQuiz.content.forEach((question, qIndex) => {
|
||||
const formattedQuestion = question.trim();
|
||||
|
|
@ -264,7 +254,7 @@ const Dashboard: React.FC = () => {
|
|||
const userFolders = await ApiService.getUserFolders();
|
||||
setFolders(userFolders as FolderType[]);
|
||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||
setSelectedFolder(newlyCreatedFolder._id);
|
||||
setSelectedFolderId(newlyCreatedFolder._id);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -274,18 +264,17 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
const handleDeleteFolder = async () => {
|
||||
|
||||
|
||||
try {
|
||||
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
|
||||
if (confirmed) {
|
||||
await ApiService.deleteFolder(selectedFolder);
|
||||
await ApiService.deleteFolder(selectedFolderId);
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
setFolders(userFolders as FolderType[]);
|
||||
}
|
||||
|
||||
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
|
||||
console.log("show all quizes")
|
||||
var quizzes: QuizType[] = [];
|
||||
console.log("show all quizzes")
|
||||
let quizzes: QuizType[] = [];
|
||||
|
||||
for (const folder of folders as FolderType[]) {
|
||||
const folderQuizzes = await ApiService.getFolderContent(folder._id);
|
||||
|
|
@ -294,19 +283,20 @@ const Dashboard: React.FC = () => {
|
|||
}
|
||||
|
||||
setQuizzes(quizzes as QuizType[]);
|
||||
setSelectedFolder('');
|
||||
setSelectedFolderId('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting folder:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolder = async () => {
|
||||
try {
|
||||
// folderId: string GET THIS FROM CURRENT FOLDER
|
||||
// currentTitle: string GET THIS FROM CURRENT FOLDER
|
||||
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
|
||||
if (newTitle) {
|
||||
await ApiService.renameFolder(selectedFolder, newTitle);
|
||||
await ApiService.renameFolder(selectedFolderId, newTitle);
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
setFolders(userFolders as FolderType[]);
|
||||
|
||||
|
|
@ -315,15 +305,16 @@ const Dashboard: React.FC = () => {
|
|||
console.error('Error renaming folder:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateFolder = async () => {
|
||||
try {
|
||||
// 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
|
||||
const userFolders = await ApiService.getUserFolders();
|
||||
setFolders(userFolders as FolderType[]);
|
||||
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
|
||||
setSelectedFolder(newlyCreatedFolder._id);
|
||||
setSelectedFolderId(newlyCreatedFolder._id);
|
||||
} catch (error) {
|
||||
console.error('Error duplicating folder:', error);
|
||||
}
|
||||
|
|
@ -393,7 +384,7 @@ const Dashboard: React.FC = () => {
|
|||
<NativeSelect
|
||||
id="select-folder"
|
||||
color="primary"
|
||||
value={selectedFolder}
|
||||
value={selectedFolderId}
|
||||
onChange={handleSelectFolder}
|
||||
>
|
||||
<option value=""> Tous les dossiers... </option>
|
||||
|
|
@ -416,7 +407,7 @@ const Dashboard: React.FC = () => {
|
|||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleRenameFolder}
|
||||
disabled={selectedFolder == ''} // cannot action on all
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <Edit /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -424,7 +415,7 @@ const Dashboard: React.FC = () => {
|
|||
<IconButton
|
||||
color="primary"
|
||||
onClick={handleDuplicateFolder}
|
||||
disabled={selectedFolder == ''} // cannot action on all
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <FolderCopy /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -433,7 +424,7 @@ const Dashboard: React.FC = () => {
|
|||
aria-label="delete"
|
||||
color="primary"
|
||||
onClick={handleDeleteFolder}
|
||||
disabled={selectedFolder == ''} // cannot action on all
|
||||
disabled={selectedFolderId == ''} // cannot action on all
|
||||
> <DeleteOutline /> </IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -461,9 +452,12 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
</div>
|
||||
<div className='list'>
|
||||
|
||||
{filteredQuizzes.map((quiz: QuizType) => (
|
||||
<div className='quiz'>
|
||||
{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
|
||||
|
|
@ -471,7 +465,7 @@ const Dashboard: React.FC = () => {
|
|||
onClick={() => handleLancerQuiz(quiz)}
|
||||
disabled={!validateQuiz(quiz.content)}
|
||||
>
|
||||
{quiz.title}
|
||||
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -491,13 +485,6 @@ const Dashboard: React.FC = () => {
|
|||
> <Edit /> </IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{/* <Tooltip title="Bouger quiz" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleMoveQuiz(quiz)}
|
||||
> <DriveFileMove /> </IconButton>
|
||||
</Tooltip> */}
|
||||
|
||||
<Tooltip title="Dupliquer quiz" placement="top">
|
||||
<IconButton
|
||||
color="primary"
|
||||
|
|
@ -522,13 +509,15 @@ const Dashboard: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</CustomCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ImportModal
|
||||
open={showImportModal}
|
||||
handleOnClose={() => setShowImportModal(false)}
|
||||
handleOnImport={handleOnImport}
|
||||
selectedFolder={selectedFolder}
|
||||
selectedFolder={selectedFolderId}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
|
@ -536,3 +525,11 @@ const Dashboard: React.FC = () => {
|
|||
};
|
||||
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,3 +78,42 @@ div:has(> #select-folder) {
|
|||
flex-direction: row;
|
||||
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;
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -162,8 +162,10 @@ 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`)
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -245,7 +247,7 @@ const QuizForm: React.FC = () => {
|
|||
onClose={() => setDialogOpen(false)} >
|
||||
<DialogTitle>Erreur</DialogTitle>
|
||||
<DialogContent>
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)} color="primary">
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
.quizEditor .editSection {
|
||||
width: 100%;
|
||||
height: 78vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const Login: React.FC = () => {
|
|||
const login = async () => {
|
||||
const result = await ApiService.login(email, password);
|
||||
|
||||
if (result != true) {
|
||||
if (typeof result === "string") {
|
||||
setConnectionError(result);
|
||||
return;
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ const Login: React.FC = () => {
|
|||
variant="outlined"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Nom d'utilisateur"
|
||||
placeholder="Adresse courriel"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
|
@ -60,7 +60,7 @@ const Login: React.FC = () => {
|
|||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Nom de la salle"
|
||||
placeholder="Mot de passe"
|
||||
sx={{ marginBottom: '1rem' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import webSocketService, { AnswerReceptionFromBackendType } from '../../../servi
|
|||
import { QuizType } from '../../../Types/QuizType';
|
||||
|
||||
import './manageRoom.css';
|
||||
import { ENV_VARIABLES } from '../../../constants';
|
||||
import { ENV_VARIABLES } from 'src/constants';
|
||||
import { StudentType, Answer } from '../../../Types/StudentType';
|
||||
import { Button } from '@mui/material';
|
||||
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
|
||||
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') {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const Register: React.FC = () => {
|
|||
const register = async () => {
|
||||
const result = await ApiService.register(email, password);
|
||||
|
||||
if (result != true) {
|
||||
if (typeof result === 'string') {
|
||||
setConnectionError(result);
|
||||
return;
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ const Register: React.FC = () => {
|
|||
sx={{ marginBottom: `${connectionError && '2rem'}` }}
|
||||
disabled={!email || !password}
|
||||
>
|
||||
S'inscrire
|
||||
S'inscrire
|
||||
</LoadingButton>
|
||||
|
||||
</LoginContainer>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const ResetPassword: React.FC = () => {
|
|||
const reset = async () => {
|
||||
const result = await ApiService.resetPassword(email);
|
||||
|
||||
if (result != true) {
|
||||
if (typeof result === 'string') {
|
||||
setConnectionError(result);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import axios, { AxiosError, AxiosResponse } from 'axios';
|
||||
import { ENV_VARIABLES } from '../constants';
|
||||
|
||||
import { QuizType } from '../Types/QuizType';
|
||||
import { FolderType } from '../Types/FolderType';
|
||||
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;
|
||||
|
|
@ -17,7 +19,7 @@ class ApiService {
|
|||
return `${this.BASE_URL}/api${endpoint}`;
|
||||
}
|
||||
|
||||
private constructRequestHeaders(): any {
|
||||
private constructRequestHeaders() {
|
||||
if (this.isLoggedIn()) {
|
||||
return {
|
||||
Authorization: `Bearer ${this.getToken()}`,
|
||||
|
|
@ -86,7 +88,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!email || !password) {
|
||||
|
|
@ -122,7 +124,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!email || !password) {
|
||||
|
|
@ -146,8 +148,13 @@ 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.';
|
||||
}
|
||||
|
|
@ -160,7 +167,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async resetPassword(email: string): Promise<any> {
|
||||
public async resetPassword(email: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
if (!email) {
|
||||
|
|
@ -196,7 +203,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!email || !oldPassword || !newPassword) {
|
||||
|
|
@ -232,7 +239,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!email || !password) {
|
||||
|
|
@ -270,7 +277,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async createFolder(title: string): Promise<any> {
|
||||
public async createFolder(title: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
if (!title) {
|
||||
|
|
@ -375,7 +382,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async deleteFolder(folderId: string): Promise<any> {
|
||||
public async deleteFolder(folderId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
if (!folderId) {
|
||||
|
|
@ -410,7 +417,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!folderId || !newTitle) {
|
||||
|
|
@ -441,7 +448,7 @@ class ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
public async duplicateFolder(folderId: string): Promise<any> {
|
||||
public async duplicateFolder(folderId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
if (!folderId) {
|
||||
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 {
|
||||
if (!folderId || !newTitle) {
|
||||
throw new Error(`Le folderId et le nouveau titre sont requis.`);
|
||||
|
|
@ -510,7 +517,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!title || !content || !folderId) {
|
||||
|
|
@ -581,7 +588,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @returns A error string if unsuccessful,
|
||||
*/
|
||||
public async deleteQuiz(quizId: string): Promise<any> {
|
||||
public async deleteQuiz(quizId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
|
||||
if (!quizId) {
|
||||
|
|
@ -616,7 +623,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!quizId || !newTitle || !newContent) {
|
||||
|
|
@ -652,7 +659,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
|
||||
if (!quizId || !newFolderId) {
|
||||
|
|
@ -689,7 +696,7 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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`);
|
||||
|
|
@ -703,7 +710,7 @@ class ApiService {
|
|||
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.status === 200;
|
||||
} catch (error) {
|
||||
console.error("Error details: ", error);
|
||||
|
||||
|
|
@ -723,9 +730,9 @@ class ApiService {
|
|||
* @returns true if successful
|
||||
* @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 {
|
||||
console.log(quizId, newTitle), folderId;
|
||||
console.log(quizId, newTitle, folderId);
|
||||
return "Route not implemented yet!";
|
||||
|
||||
} 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 {
|
||||
if (!quizId || !email) {
|
||||
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 {
|
||||
if (!quizId || !folderId) {
|
||||
throw new Error(`quizId and folderId are required.`);
|
||||
|
|
@ -869,7 +876,8 @@ class ApiService {
|
|||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
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.`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export function escapeForGIFT(link: string): string {
|
||||
const specialChars = /[{}#~=<>\:]/g;
|
||||
const specialChars = /[{}#~=<>\\:]/g;
|
||||
return link.replace(specialChars, (match) => `\\${match}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"src/*": ["src/*"]
|
||||
},
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
|
|
@ -12,7 +16,7 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "react",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ export default defineConfig({
|
|||
pluginChecker({ typescript: true }),
|
||||
EnvironmentPlugin(filteredEnv),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'src': '/src'
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
port: 5173,
|
||||
strictPort: true
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ services:
|
|||
frontend:
|
||||
image: fuhrmanator/evaluetonsavoir-frontend:latest
|
||||
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:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
VITE_BACKEND_URL: "http://localhost:4400"
|
||||
# don't define VITE_BACKEND_SOCKET_URL so it will default to window.location.host
|
||||
# VITE_BACKEND_SOCKET_URL: ""
|
||||
restart: always
|
||||
|
||||
backend:
|
||||
|
|
|
|||
10
export-collections.bash
Normal file
10
export-collections.bash
Normal 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
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
const { create } = require('../middleware/jwtToken');
|
||||
const Folders = require('../models/folders');
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
const Quizzes = require('../models/quiz');
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// const request = require('supertest');
|
||||
// const app = require('../app.js');
|
||||
// // const app = require('../routers/images.js');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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');
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const imagesRouter = require('./routers/images.js');
|
|||
|
||||
// Setup environment
|
||||
dotenv.config();
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const errorHandler = require("./middleware/errorHandler.js");
|
||||
|
||||
// Start app
|
||||
|
|
@ -51,6 +52,7 @@ const cors = require("cors");
|
|||
const bodyParser = require('body-parser');
|
||||
|
||||
const configureServer = (httpServer, isDev) => {
|
||||
console.log(`Configuring server with isDev: ${isDev}`);
|
||||
return new Server(httpServer, {
|
||||
path: "/socket.io",
|
||||
cors: {
|
||||
|
|
@ -63,13 +65,12 @@ const configureServer = (httpServer, isDev) => {
|
|||
};
|
||||
|
||||
// Start sockets (depending on the dev or prod environment)
|
||||
let server = http.createServer(app);
|
||||
let isDev = process.env.NODE_ENV === 'development';
|
||||
const server = http.createServer(app);
|
||||
|
||||
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
|
||||
|
||||
const io = configureServer(server);
|
||||
console.log(`server.io configured: ${io.secure ? 'secure' : 'not secure'}`);
|
||||
const io = configureServer(server, isDev);
|
||||
console.log(`Server configured with cors.origin: ${io.opts.cors.origin} and secure: ${io.opts.secure}`);
|
||||
|
||||
setupWebsocket(io);
|
||||
console.log(`Websocket setup with on() listeners.`);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ exports.USER_ALREADY_EXISTS = {
|
|||
}
|
||||
exports.LOGIN_CREDENTIALS_ERROR = {
|
||||
message: 'L\'email et le mot de passe ne correspondent pas.',
|
||||
code: 400
|
||||
code: 401
|
||||
}
|
||||
exports.GENERATE_PASSWORD_ERROR = {
|
||||
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//controller
|
||||
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
|
||||
class FoldersController {
|
||||
|
|
|
|||
|
|
@ -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, 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 {
|
||||
|
||||
constructor(quizModel, foldersModel) {
|
||||
this.folders = foldersModel;
|
||||
this.quizzes = quizModel;
|
||||
this.folders = foldersModel;
|
||||
}
|
||||
|
||||
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 Quiz.deleteQuizzesByFolderId(folderId);
|
||||
await this.quizzes.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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
32
server/eslint.config.mjs
Normal file
32
server/eslint.config.mjs
Normal 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,
|
||||
];
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
const AppError = require("./AppError");
|
||||
const fs = require('fs');
|
||||
|
||||
const errorHandler = (error, req, res, next) => {
|
||||
const errorHandler = (error, req, res) => {
|
||||
console.log("ERROR", error);
|
||||
|
||||
if (error instanceof AppError) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
//const db = require('../config/db.js')
|
||||
const { ObjectId } = require('mongodb');
|
||||
|
||||
class Images {
|
||||
|
|
@ -8,8 +7,8 @@ class Images {
|
|||
}
|
||||
|
||||
async upload(file, userId) {
|
||||
await db.connect()
|
||||
const conn = db.getConnection();
|
||||
await this.db.connect()
|
||||
const conn = this.db.getConnection();
|
||||
|
||||
const imagesCollection = conn.collection('images');
|
||||
|
||||
|
|
@ -27,8 +26,8 @@ class Images {
|
|||
}
|
||||
|
||||
async get(id) {
|
||||
await db.connect()
|
||||
const conn = db.getConnection();
|
||||
await this.db.connect()
|
||||
const conn = this.db.getConnection();
|
||||
|
||||
const imagesCollection = conn.collection('images');
|
||||
|
||||
|
|
|
|||
871
server/package-lock.json
generated
871
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -25,7 +25,10 @@
|
|||
"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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue