diff --git a/.gitignore b/.gitignore index 1ae1622..b39bc1b 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,5 @@ launch.json .yarn/build-state.yml .yarn/install-state.gz .pnp.* -db-backup/ \ No newline at end of file +db-backup/ +/.vs diff --git a/client/package-lock.json b/client/package-lock.json index e2c6890..1cd42d5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -17,8 +17,11 @@ "@mui/icons-material": "^6.4.6", "@mui/lab": "^5.0.0-alpha.153", "@mui/material": "^6.4.6", + "@types/bootstrap": "^5.2.10", "@types/uuid": "^9.0.7", "axios": "^1.8.1", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "dompurify": "^3.2.3", "esbuild": "^0.25.0", "gift-pegjs": "^2.0.0-beta.1", @@ -4521,6 +4524,14 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bootstrap": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", + "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5536,6 +5547,39 @@ "devOptional": true, "license": "MIT" }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", diff --git a/client/package.json b/client/package.json index b6d62e4..0a1b056 100644 --- a/client/package.json +++ b/client/package.json @@ -21,8 +21,11 @@ "@mui/icons-material": "^6.4.6", "@mui/lab": "^5.0.0-alpha.153", "@mui/material": "^6.4.6", + "@types/bootstrap": "^5.2.10", "@types/uuid": "^9.0.7", "axios": "^1.8.1", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "dompurify": "^3.2.3", "esbuild": "^0.25.0", "gift-pegjs": "^2.0.0-beta.1", diff --git a/client/src/components/Footer/Footer.tsx b/client/src/components/Footer/Footer.tsx index 331d8ec..a7d7c1d 100644 --- a/client/src/components/Footer/Footer.tsx +++ b/client/src/components/Footer/Footer.tsx @@ -1,21 +1,33 @@ import * as React from 'react'; -import './footer.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; // Add Bootstrap CSS import -type FooterProps = object; //empty object +type FooterProps = object; const Footer: React.FC = () => { return ( -
-
- Réalisé avec ❤ à Montréal par des finissant•e•s de l'ETS +
+ ); }; -export default Footer; +export default Footer; \ No newline at end of file diff --git a/client/src/components/Footer/footer.css b/client/src/components/Footer/footer.css deleted file mode 100644 index 907065c..0000000 --- a/client/src/components/Footer/footer.css +++ /dev/null @@ -1,23 +0,0 @@ -.footer { - flex-shrink: 0; - padding: 20px; - text-align: center; -} - -.footer-content { - margin-bottom: 10px; -} - -.footer-links a { - color: #333; - text-decoration: none; -} - -.footer-links a:hover { - text-decoration: underline; -} - -.divider { - margin: 0 10px; - color: #666; -} \ No newline at end of file diff --git a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx index 036f2d0..6beb559 100644 --- a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx +++ b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx @@ -1,6 +1,8 @@ // GiftCheatSheet.tsx import React, { useState } from 'react'; -import './giftCheatSheet.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import FileCopyIcon from '@mui/icons-material/FileCopy'; +import { Button } from '@mui/material'; const GiftCheatSheet: React.FC = () => { const [copySuccess, setCopySuccess] = useState(false); @@ -8,197 +10,242 @@ const GiftCheatSheet: React.FC = () => { const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text) .then(() => { - setCopySuccess(true); // Afficher le message de succès - console.log(copySuccess); - // Masquer le message de succès après quelques secondes + setCopySuccess(true); setTimeout(() => { setCopySuccess(false); - }, 3000); // 3 secondes + }, 3000); }) .catch((error) => { console.error('Erreur lors de la copie dans le presse-papiers : ', error); }); }; - const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}."; const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)"; const QuestionChoixMulMany = "::Villes canadiennes:: \n Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse."; - const QuestionCourte ="::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte."; - const QuestionNum ="::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}"; + const QuestionCourte = "::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte."; + const QuestionNum = "::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}"; + return ( -
-

Informations pratiques sur l'éditeur

- +
+ + {/* Add feedback alert at the top */} + {copySuccess && ( +
+ Texte copié dans le presse-papiers! + +
+ )} + +

Informations pratiques sur l'éditeur

+

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 : - -

-

1. Questions Vrai/Faux

-
-                    
-                        {QuestionVraiFaux}
-                    
+            

-
- -
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((section) => ( +
+ {section === 1 && ( + <> +

1. Questions Vrai/Faux

+
+                                
+                                    {QuestionVraiFaux}
+                                
+                            
+ + + )} -
-

2. Questions à choix multiple

-
-                    
-                        {
-                            QuestionChoixMul
-                        }
-                    
-                
- -
-
-

3. Questions à choix multiple avec plusieurs réponses

-
-                    
-                        {
-                            QuestionChoixMulMany
-                        }
-                    
-                
- -
+ {section === 2 && ( + <> +

2. Questions à choix multiple

+
+                                
+                                    {QuestionChoixMul}
+                                
+                            
+ + + )} -
-

4. Questions à réponse courte

-
-                    
-                        {QuestionCourte}
-                    
-                
- -
+ {section === 3 && ( + <> +

3. Questions à choix multiple avec plusieurs réponses

+
+                                
+                                    {QuestionChoixMulMany}
+                                
+                            
+ + + )} -
-

5. Questions numériques

-
-                    
-                        {
-                            QuestionNum
-                        }
-                                        
-                
- -
+ {section === 4 && ( + <> +

4. Questions à réponse courte

+
+                                
+                                    {QuestionCourte}
+                                
+                            
+ + + )} -
-

6. Paramètres optionnels

-
-                    
-                        {'::Titre:: '}
-                        
-                            {' // Ajoute un titre à une question'}
-                        
-                        
- {'# Feedback '} - - {' // Feedback pour UNE réponse'} - -
- {'// Commentaire '} - - {' // Commentaire non apparent'} - -
- {'#### Feedback général '} - - {' // Feedback général pour une question'} - -
- {'%50% '} - - {" // Poids d'une réponse (peut être négatif)"} - -
-
-
+ {section === 5 && ( + <> +

5. Questions numériques

+
+                                
+                                    {QuestionNum}
+                                
+                            
+ + + )} -
-

7. Caractères spéciaux

-

- Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés, - réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \ - devant: -

-
-                    
-                        {'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
-                    
-                
-
+ {section === 6 && ( + <> +

6. Paramètres optionnels

+
+                                
+                                    {'::Titre:: '}
+                                    
+                                        {' // Ajoute un titre à une question'}
+                                    
+                                    
+ {'# Feedback '} + + {' // Feedback pour UNE réponse'} + +
+ {'// Commentaire '} + + {' // Commentaire non apparent'} + +
+ {'#### Feedback général '} + + {' // Feedback général pour une question'} + +
+ {'%50% '} + + {" // Poids d'une réponse (peut être négatif)"} + +
+
+ + )} -
-

8. LaTeX et Markdown

-

- 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. -

-

Exemple d'équation:

-
-                    {'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}
-                    {'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}
-                
-

Exemple de texte Markdown:

-
-                    {'[markdown]Grâce à la balise markdown, Il est possible d\'insérer du texte *italique*, **gras**, du `code` et bien plus.'}
-                
-
+ {section === 7 && ( + <> +

7. Caractères spéciaux

+

+ Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés, + réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \ + devant: +

+
+                                
+                                    {'\\~ \n\\= \n\\# \n\\{ \n\\} \n\\:'}
+                                
+                            
+ + )} -
-

9. Images

-

Il est possible d'insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D'abord, le format de l'élément doit être [markdown]. Ensuite utilisez la syntaxe suivante :

-
-                    
-                        {'!['}
-                        {`text alternatif`}
-                        {']('}
-                        {`URL-de-l'image`}
-                        {' "'}
-                        {`texte de l'infobulle`}
-                        {'")'}
-                    
-                
-

Exemple d'une question Vrai/Faux avec l'image d'un chat:

-
-                    
-                        {'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
-                    
-                
-

Exemple d'une question à choix multiple avec l'image d'un chat dans une rétroaction :

-
-                    
-                        {`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! ![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px")
-                        ~EPFL#Non...}`}
-                    
-                
-

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.

-

- Attention: l'ancienne fonctionnalité avec les balises {''} n'est plus - supportée. -

+ {section === 8 && ( + <> +

8. LaTeX et Markdown

+

+ 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. +

+

Exemple d'équation:

+
+                                {'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}
+                                {'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}
+                            
+

Exemple de texte Markdown:

+
+                                {'[markdown]Grâce à la balise markdown, Il est possible d\'insérer du texte *italique*, **gras**, du `code` et bien plus.'}
+                            
+ + )} + + {section === 9 && ( + <> +

9. Images

+

Il est possible d'insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D'abord, le format de l'élément doit être [markdown]. Ensuite utilisez la syntaxe suivante :

+
+                                
+                                    {'!['}
+                                    {`text alternatif`}
+                                    {']('}
+                                    {`URL-de-l'image`}
+                                    {' "'}
+                                    {`texte de l'infobulle`}
+                                    {'")'}
+                                
+                            
+

Exemple d'une question Vrai/Faux avec l'image d'un chat:

+
+                                
+                                    {'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
+                                
+                            
+

Exemple d'une question à choix multiple avec l'image d'un chat dans une rétroaction :

+
+                                
+                                    {`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! ![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px")
+                                    ~EPFL#Non...}`}
+                                
+                            
+

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.

+

+ Attention: l'ancienne fonctionnalité avec les balises {''} n'est plus + supportée. +

+ + )} + + {section === 10 && ( + <> +

10. Informations supplémentaires

+

+ GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette + application. +

+

Vous pouvez retrouver la Documentation de GIFT (en anglais):

+ + Documentation de GIFT + + + )}
-
-

10. Informations supplémentaires

-

- GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette - application. -

-

Vous pouvez retrouver la Documentation de GIFT (en anglais):

- - Documentation de GIFT - -
+ ))}
); -}; +}; -export default GiftCheatSheet; +export default GiftCheatSheet; \ No newline at end of file diff --git a/client/src/components/GIFTCheatSheet/giftCheatSheet.css b/client/src/components/GIFTCheatSheet/giftCheatSheet.css deleted file mode 100644 index 5fc7777..0000000 --- a/client/src/components/GIFTCheatSheet/giftCheatSheet.css +++ /dev/null @@ -1,37 +0,0 @@ -.gift-cheat-sheet { - /* width: 30vw; */ - height: 100%; -} -.subtitle { - color: #3a3a3a; - margin-bottom: 2vh; -} - -.question-type { - margin-bottom: 20; -} -.question-code-block, -.code-comment { - white-space: pre-line; -} - -code { - font-family: 'Courier New', Courier, monospace; - padding: 2px 4px; - border-radius: 4px; -} - -pre { - background-color: #ffffffbd; - padding: 10px; - border: 1px solid #000; - border-radius: 4px; - overflow-x: auto; -} -.code-comment { - color: green; -} - -.question-type h4 { - margin-top: 20px; -} diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 016d23e..03fce1b 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,7 +1,9 @@ import { Link, useNavigate } from 'react-router-dom'; import * as React from 'react'; -import './header.css'; import { Button } from '@mui/material'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import LogoutIcon from '@mui/icons-material/Logout'; +import LoginIcon from '@mui/icons-material/Login'; interface HeaderProps { isLoggedIn: boolean; @@ -12,36 +14,37 @@ const Header: React.FC = ({ isLoggedIn, handleLogout }) => { const navigate = useNavigate(); return ( -
+
Logo navigate('/')} /> - {isLoggedIn && ( - - )} - - {!isLoggedIn && ( -
- - - -
- )} -
+
+ {isLoggedIn ? ( + + ) : ( + + + + )} +
+ ); }; -export default Header; +export default Header; \ No newline at end of file diff --git a/client/src/components/Header/header.css b/client/src/components/Header/header.css deleted file mode 100644 index 379a60d..0000000 --- a/client/src/components/Header/header.css +++ /dev/null @@ -1,14 +0,0 @@ - -.header { - flex-shrink: 0; - padding: 15px; - overflow: hidden; - - display: flex; - justify-content: space-between; - align-items: center; -} - -.header img { - cursor: pointer; -} \ No newline at end of file diff --git a/client/src/components/ImportModal/ImportModal.tsx b/client/src/components/ImportModal/ImportModal.tsx index 41ab6ca..c8bac59 100644 --- a/client/src/components/ImportModal/ImportModal.tsx +++ b/client/src/components/ImportModal/ImportModal.tsx @@ -1,6 +1,4 @@ import React, { useState, DragEvent, useRef, useEffect } from 'react'; -import './importModal.css'; - import { Button, Dialog, @@ -12,7 +10,7 @@ import { } from '@mui/material'; import { Clear, Download } from '@mui/icons-material'; import ApiService from '../../services/ApiService'; - +import 'bootstrap/dist/css/bootstrap.min.css'; type DroppedFile = { id: number; @@ -28,7 +26,7 @@ interface Props { selectedFolder: string; } -const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, selectedFolder }) => { +const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, selectedFolder }) => { const [droppedFiles, setDroppedFiles] = useState([]); const fileInputRef = useRef(null); @@ -48,7 +46,6 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel const handleDrop = (e: DragEvent) => { e.preventDefault(); - const files = e.dataTransfer.files; handleFiles(files); }; @@ -66,8 +63,6 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel setDroppedFiles((prevFiles) => [...prevFiles, ...newDroppedFiles]); }; - - const handleOnSave = async () => { const storedQuizzes = JSON.parse(localStorage.getItem('quizzes') || '[]'); const quizzesToImportPromises = droppedFiles.map((droppedFile) => { @@ -77,23 +72,16 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel reader.onload = async (event) => { if (event.target && event.target.result) { const fileContent = event.target.result as string; - //console.log(fileContent); if (fileContent.trim() === '') { resolve(null); } - const questions = fileContent.split(/}/) + const questions = fileContent.split(/}/) .map(question => { - // Remove trailing and leading spaces - - return question.trim()+"}"; + return question.trim() + "}"; }) - .filter(question => question.trim() !== '').slice(0, -1); // Filter out lines with only whitespace characters - - try { - // const folders = await ApiService.getUserFolders(); + .filter(question => question.trim() !== '').slice(0, -1); - // Assuming you want to use the first folder - // const selectedFolder = folders.length > 0 ? folders[0]._id : null; + try { await ApiService.createQuiz(droppedFile.name.slice(0, -4) || 'Untitled quiz', questions, selectedFolder); resolve('success'); } catch (error) { @@ -105,8 +93,6 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel }); }); - - Promise.all(quizzesToImportPromises).then((quizzesToImport) => { const verifiedQuizzesToImport = quizzesToImport.filter((quiz) => { return quiz !== null; @@ -118,17 +104,10 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel setDroppedFiles([]); handleOnImport(); handleOnClose(); - window.location.reload(); }); }; - - - - - - const handleRemoveFile = (id: number) => { setDroppedFiles((prevFiles) => prevFiles.filter((file) => file.id !== id)); }; @@ -158,14 +137,15 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel {'Importation de quiz'} -
- +
+ Déposer des fichiers ici ou
cliquez pour ouvrir l'explorateur des fichiers @@ -175,7 +155,7 @@ const DragAndDrop: React.FC = ({ handleOnClose, handleOnImport, open, sel {droppedFiles.map((file) => ( -
+
{file.icon} {file.name} = ({ handleOnClose, handleOnImport, open, sel ); }; -export default DragAndDrop; +export default DragAndDrop; \ No newline at end of file diff --git a/client/src/components/ImportModal/importModal.css b/client/src/components/ImportModal/importModal.css deleted file mode 100644 index 0bcca96..0000000 --- a/client/src/components/ImportModal/importModal.css +++ /dev/null @@ -1,20 +0,0 @@ -.import-container { - border-style: dashed; - border-width: thin; - border-color: rgba(128, 128, 128, 0.5); - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; - height: 20vh; - cursor: pointer; - box-sizing: border-box; - margin: 0 20px 0 20px; -} - -.file-container { - gap: 10px; - display: flex; - align-items: center; - padding: 4px; -} diff --git a/client/src/components/LoadingCircle/LoadingCircle.tsx b/client/src/components/LoadingCircle/LoadingCircle.tsx index bb0b56a..c7b9809 100644 --- a/client/src/components/LoadingCircle/LoadingCircle.tsx +++ b/client/src/components/LoadingCircle/LoadingCircle.tsx @@ -1,6 +1,6 @@ import { CircularProgress } from '@mui/material'; import React from 'react'; -import './loadingCircle.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; interface Props { text: string; @@ -8,11 +8,11 @@ interface Props { const LoadingCircle: React.FC = ({ text }) => { return ( -
-
{text}
+
+
{text}
); }; -export default LoadingCircle; +export default LoadingCircle; \ No newline at end of file diff --git a/client/src/components/LoadingCircle/loadingCircle.css b/client/src/components/LoadingCircle/loadingCircle.css deleted file mode 100644 index 8cedca3..0000000 --- a/client/src/components/LoadingCircle/loadingCircle.css +++ /dev/null @@ -1,5 +0,0 @@ -.loading-circle { - display: flex; - flex-direction: column; - align-items: center; -} diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index 192c0b2..6a976e8 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -1,7 +1,6 @@ // StudentModeQuiz.tsx import React, { useEffect, useState } from 'react'; import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; -import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; import { Button } from '@mui/material'; //import QuestionNavigation from '../QuestionNavigation/QuestionNavigation'; diff --git a/client/src/components/StudentWaitPage/StudentWaitPage.tsx b/client/src/components/StudentWaitPage/StudentWaitPage.tsx index c5de4f2..e09d16f 100644 --- a/client/src/components/StudentWaitPage/StudentWaitPage.tsx +++ b/client/src/components/StudentWaitPage/StudentWaitPage.tsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Box, Button, Chip } from '@mui/material'; import { StudentType } from '../../Types/StudentType'; import { PlayArrow } from '@mui/icons-material'; import LaunchQuizDialog from '../LaunchQuizDialog/LaunchQuizDialog'; -import { useState } from 'react'; -import './studentWaitPage.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; interface Props { students: StudentType[]; @@ -20,31 +19,31 @@ const StudentWaitPage: React.FC = ({ students, launchQuiz, setQuizMode }) }; return ( -
-
+
+
+ Lancer +
-
- +
- {students.map((student, index) => ( - + ))} - -
= ({ students, launchQuiz, setQuizMode }) launchQuiz={launchQuiz} setQuizMode={setQuizMode} /> -
); }; -export default StudentWaitPage; +export default StudentWaitPage; \ No newline at end of file diff --git a/client/src/components/StudentWaitPage/studentWaitPage.css b/client/src/components/StudentWaitPage/studentWaitPage.css deleted file mode 100644 index 6ae1f3b..0000000 --- a/client/src/components/StudentWaitPage/studentWaitPage.css +++ /dev/null @@ -1,23 +0,0 @@ -.wait { - width: 100%; - - display: flex; - flex-direction: column; -} - -.wait .button { - padding: 10px; - display: flex; - - justify-content: center; - align-items: center; -} - -.wait .students { - width: 100%; - - padding: 10px; - box-sizing: border-box; - - overflow: auto; -} \ No newline at end of file diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 8925c09..0103fbb 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -1,7 +1,6 @@ // TeacherModeQuiz.tsx import React, { useEffect, useState } from 'react'; import QuestionComponent from '../QuestionsDisplay/QuestionDisplay'; -import '../../pages/Student/JoinRoom/joinRoom.css'; import { QuestionType } from '../../Types/QuestionType'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; diff --git a/client/src/main.tsx b/client/src/main.tsx index e73c979..a04f569 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from 'react-router-dom'; import { ThemeProvider, createTheme } from '@mui/material'; import '@fortawesome/fontawesome-free/css/all.min.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; import './cssReset.css'; import './index.css'; diff --git a/client/src/pages/AuthManager/AuthDrawer.tsx b/client/src/pages/AuthManager/AuthDrawer.tsx index 093b7aa..bd16513 100644 --- a/client/src/pages/AuthManager/AuthDrawer.tsx +++ b/client/src/pages/AuthManager/AuthDrawer.tsx @@ -1,61 +1,64 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import './authDrawer.css'; import SimpleLogin from './providers/SimpleLogin/Login'; import authService from '../../services/AuthService'; import { ENV_VARIABLES } from '../../constants'; import ButtonAuth from './providers/OAuth-Oidc/ButtonAuth'; +import 'bootstrap/dist/css/bootstrap.min.css'; const AuthSelection: React.FC = () => { - const [authData, setAuthData] = useState(null); // Stocke les données d'auth + const [authData, setAuthData] = useState(null); const navigate = useNavigate(); ENV_VARIABLES.VITE_BACKEND_URL; - // Récupérer les données d'authentification depuis l'API + useEffect(() => { const fetchData = async () => { const data = await authService.fetchAuthData(); setAuthData(data); }; - fetchData(); }, []); return ( -
-

Connexion

+
+

Connexion

- {/* Formulaire de connexion Simple Login */} + {/* Simple Login Form - Responsive width */} {authData && authData['simpleauth'] && ( -
+
)} - {/* Conteneur OAuth/OIDC */} - {authData && Object.keys(authData).some(key => authData[key].type === 'oidc' || authData[key].type === 'oauth') && ( -
- {Object.keys(authData).map((providerKey) => { - const providerType = authData[providerKey].type; - if (providerType === 'oidc' || providerType === 'oauth') { - return ( - - ); - } - return null; - })} -
- )} + {/* OAuth/OIDC Providers - Responsive width */} + {authData && Object.keys(authData).some(key => + authData[key].type === 'oidc' || authData[key].type === 'oauth') && ( +
+ {Object.keys(authData).map((providerKey) => { + const providerType = authData[providerKey].type; + if (providerType === 'oidc' || providerType === 'oauth') { + return ( + + ); + } + return null; + })} +
+ )} -
- -
+
); }; -export default AuthSelection; +export default AuthSelection; \ No newline at end of file diff --git a/client/src/pages/AuthManager/authDrawer.css b/client/src/pages/AuthManager/authDrawer.css deleted file mode 100644 index b0d5263..0000000 --- a/client/src/pages/AuthManager/authDrawer.css +++ /dev/null @@ -1,49 +0,0 @@ -.auth-selection-page { - display: flex; - flex-direction: column; - align-items: center; - padding: 20px; -} -h1 { - margin-bottom: 20px; -} -.form-container { - border: 1px solid #ccc; - border-radius: 8px; - padding: 15px; - margin: 10px 0; - width: 400px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - text-align: center; -} -form { - display: flex; - flex-direction: column; -} -input { - margin: 5px 0; - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; -} -button { - padding: 10px; - border: none; - border-radius: 4px; - background-color: #5271ff; - color: white; - cursor: pointer; -} -/* This hover was affecting the entire App */ -/* button:hover { - background-color: #5271ff; - } */ -.home-button-container { - background: none; - color: black; -} -.home-button-container:hover { - background: none; - color: black; - text-decoration: underline; -} diff --git a/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx b/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx index c8f4efc..a07e315 100644 --- a/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx +++ b/client/src/pages/AuthManager/providers/OAuth-Oidc/ButtonAuth.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ENV_VARIABLES } from '../../../../constants'; -import '../css/buttonAuth.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; interface ButtonAuthContainerProps { providerName: string; @@ -13,14 +13,16 @@ const handleAuthLogin = (provider: string) => { const ButtonAuth: React.FC = ({ providerName, providerType }) => { return ( - <> -
-

Se connecter avec {providerType.toUpperCase()}

- -
- +
+

Se connecter avec {providerType.toUpperCase()}

+ +
); }; diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx index ecc9a1c..6cca2b4 100644 --- a/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx +++ b/client/src/pages/AuthManager/providers/SimpleLogin/Login.tsx @@ -1,90 +1,83 @@ +import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; - -// JoinRoom.tsx -import React, { useEffect, useState } from 'react'; - -import '../css/simpleLogin.css'; -import { TextField } from '@mui/material'; -import LoadingButton from '@mui/lab/LoadingButton'; - -import LoginContainer from '../../../../components/LoginContainer/LoginContainer' +import { TextField, Button, CircularProgress } from '@mui/material'; +import LoginContainer from '../../../../components/LoginContainer/LoginContainer'; import ApiService from '../../../../services/ApiService'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import LoginIcon from '@mui/icons-material/Login'; const SimpleLogin: React.FC = () => { - const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [connectionError, setConnectionError] = useState(''); - const [isConnecting] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); useEffect(() => { return () => { - + // Cleanup if needed }; }, []); const login = async () => { - console.log(`SimpleLogin: login: email: ${email}, password: ${password}`); + setIsConnecting(true); const result = await ApiService.login(email, password); + setIsConnecting(false); + if (result !== true) { setConnectionError(result); return; } }; - return ( - - + + {/* Email Input */} setEmail(e.target.value)} placeholder="Nom d'utilisateur" - sx={{ marginBottom: '1rem' }} - fullWidth + fullWidth // Material-UI fullWidth /> + {/* Password Input */} setPassword(e.target.value)} - placeholder="Nom de la salle" - sx={{ marginBottom: '1rem' }} + placeholder="Mot de passe" fullWidth /> - : } + size="large" > - Login - + Se connecter + -
- - - {/* */} - Réinitialiser le mot de passe - {/* */} - - + {/* Links Section */} +
+ Réinitialiser le mot de passe + Créer un compte -
- ); }; -export default SimpleLogin; +export default SimpleLogin; \ No newline at end of file diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx index 46a8c85..396f451 100644 --- a/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx +++ b/client/src/pages/AuthManager/providers/SimpleLogin/Register.tsx @@ -1,32 +1,35 @@ -// JoinRoom.tsx -import React, { useEffect, useState } from 'react'; - -import { TextField, FormLabel, RadioGroup, FormControlLabel, Radio, Box } from '@mui/material'; -import LoadingButton from '@mui/lab/LoadingButton'; - +import React, { useState, useEffect } from 'react'; +import { + TextField, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, + Box, + Button, + CircularProgress +} from '@mui/material'; import LoginContainer from '../../../../components/LoginContainer/LoginContainer'; import ApiService from '../../../../services/ApiService'; +import 'bootstrap/dist/css/bootstrap.min.css'; const Register: React.FC = () => { - - const [name, setName] = useState(''); // State for name + const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [roles, setRoles] = useState(['teacher']); // Set 'student' as the default role - + const [roles, setRoles] = useState(['teacher']); const [connectionError, setConnectionError] = useState(''); - const [isConnecting] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); useEffect(() => { return () => { }; }, []); const handleRoleChange = (role: string) => { - setRoles([role]); // Update the roles array to contain the selected role + setRoles([role]); }; const isValidEmail = (email: string) => { - // Basic email format validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; @@ -37,7 +40,9 @@ const Register: React.FC = () => { return; } + setIsConnecting(true); const result = await ApiService.register(name, email, password, roles); + setIsConnecting(false); if (result !== true) { setConnectionError(result); @@ -46,46 +51,49 @@ const Register: React.FC = () => { }; return ( - + + {/* Name Field */} setName(e.target.value)} placeholder="Votre nom" - sx={{ marginBottom: '1rem' }} fullWidth /> + {/* Email Field */} setEmail(e.target.value)} placeholder="Adresse courriel" - sx={{ marginBottom: '1rem' }} fullWidth - type="email" - error={!!connectionError && !isValidEmail(email)} + type="email" + error={!!connectionError && !isValidEmail(email)} helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""} /> + {/* Password Field */} setPassword(e.target.value)} placeholder="Mot de passe" - sx={{ marginBottom: '1rem' }} fullWidth /> - - Choisir votre rôle + {/* Role Selection */} + + + Choisir votre rôle + { - : null} + size="large" > S'inscrire - + ); }; -export default Register; +export default Register; \ No newline at end of file diff --git a/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx b/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx index c33c9fa..526c487 100644 --- a/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx +++ b/client/src/pages/AuthManager/providers/SimpleLogin/ResetPassword.tsx @@ -1,68 +1,72 @@ - +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; - -// JoinRoom.tsx -import React, { useEffect, useState } from 'react'; - -import { TextField } from '@mui/material'; -import LoadingButton from '@mui/lab/LoadingButton'; - -import LoginContainer from '../../../../components/LoginContainer/LoginContainer' +import { TextField, Button, CircularProgress } from '@mui/material'; +import LoginContainer from '../../../../components/LoginContainer/LoginContainer'; import ApiService from '../../../../services/ApiService'; +import 'bootstrap/dist/css/bootstrap.min.css'; const ResetPassword: React.FC = () => { const navigate = useNavigate(); - const [email, setEmail] = useState(''); - const [connectionError, setConnectionError] = useState(''); - const [isConnecting] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); useEffect(() => { return () => { - + // Cleanup if needed }; }, []); const reset = async () => { - const result = await ApiService.resetPassword(email); + setIsConnecting(true); + try { + const result = await ApiService.resetPassword(email); - if (!result) { - setConnectionError(result.toString()); - return; + if (!result) { + setConnectionError(result.toString()); + return; + } + + navigate("/login"); + } finally { + setIsConnecting(false); } - - navigate("/login") }; + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; return ( - - + + {/* Email Field */} setEmail(e.target.value)} placeholder="Adresse courriel" - sx={{ marginBottom: '1rem' }} fullWidth + type="email" + error={!!connectionError && !isValidEmail(email)} + helperText={connectionError && !isValidEmail(email) ? "Adresse email invalide." : ""} /> - : null} + size="large" > Réinitialiser le mot de passe - - + ); }; -export default ResetPassword; +export default ResetPassword; \ No newline at end of file diff --git a/client/src/pages/AuthManager/providers/css/buttonAuth.css b/client/src/pages/AuthManager/providers/css/buttonAuth.css deleted file mode 100644 index 98476ec..0000000 --- a/client/src/pages/AuthManager/providers/css/buttonAuth.css +++ /dev/null @@ -1,23 +0,0 @@ -.provider-btn { - background-color: #ffffff; - border: 1px solid #ccc; - color: black; - margin: 4px 0 4px 0; -} - -.provider-btn:hover { - background-color: #dbdbdb; - border: 1px solid #ccc; - color: black; - margin: 4px 0 4px 0; -} - -.button-container { - border: 1px solid #ccc; - border-radius: 8px; - padding: 15px; - margin: 10px 0; - width: 400px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - text-align: center; -} \ No newline at end of file diff --git a/client/src/pages/AuthManager/providers/css/simpleLogin.css b/client/src/pages/AuthManager/providers/css/simpleLogin.css deleted file mode 100644 index ddbebdb..0000000 --- a/client/src/pages/AuthManager/providers/css/simpleLogin.css +++ /dev/null @@ -1,17 +0,0 @@ -.login-links { - padding-top: 10px; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -.login-links a { - padding: 4px; - color: #333; - text-decoration: none; -} - -.login-links a:hover { - text-decoration: underline; -} diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index a5ee1ff..d450f8d 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -1,21 +1,15 @@ import React, { useEffect, useState } from 'react'; - import { Socket } from 'socket.io-client'; import { ENV_VARIABLES } from 'src/constants'; - import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; - -import './joinRoom.css'; import { QuestionType } from '../../../Types/QuestionType'; -import { TextField } from '@mui/material'; -import LoadingButton from '@mui/lab/LoadingButton'; - -import LoginContainer from 'src/components/LoginContainer/LoginContainer' - -import ApiService from '../../../services/ApiService' +import { TextField, Button, CircularProgress } from '@mui/material'; +import LoginContainer from 'src/components/LoginContainer/LoginContainer'; +import ApiService from '../../../services/ApiService'; +import 'bootstrap/dist/css/bootstrap.min.css'; export type AnswerType = Array; @@ -39,69 +33,63 @@ const JoinRoom: React.FC = () => { }, []); useEffect(() => { - console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []); }, [questions]); - const handleCreateSocket = () => { - console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); - socket.on('join-success', (roomJoinedName) => { + socket.on('join-success', () => { setIsWaitingForTeacher(true); setIsConnecting(false); - console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`); }); + socket.on('next-question', (question: QuestionType) => { - console.log('JoinRoom: on(next-question): Received next-question:', question); setQuizMode('teacher'); setIsWaitingForTeacher(false); setQuestion(question); }); + socket.on('launch-teacher-mode', (questions: QuestionType[]) => { - console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); setQuizMode('teacher'); setIsWaitingForTeacher(true); - setQuestions([]); // clear out from last time (in case quiz is repeated) + setQuestions([]); setQuestions(questions); - // wait for next-question }); - socket.on('launch-student-mode', (questions: QuestionType[]) => { - console.log('on(launch-student-mode): Received launch-student-mode:', questions); + socket.on('launch-student-mode', (questions: QuestionType[]) => { setQuizMode('student'); setIsWaitingForTeacher(false); - setQuestions([]); // clear out from last time (in case quiz is repeated) + setQuestions([]); setQuestions(questions); setQuestion(questions[0]); }); + socket.on('end-quiz', () => { disconnect(); }); + socket.on('join-failure', (message) => { - console.log('Failed to join the room.'); setConnectionError(`Erreur de connexion : ${message}`); setIsConnecting(false); }); + socket.on('connect_error', (error) => { switch (error.message) { case 'timeout': - setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible"); + setConnectionError("Le serveur n'est pas disponible"); break; case 'websocket error': - setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible"); + setConnectionError("Le serveur n'est pas disponible"); break; } setIsConnecting(false); - console.log('Connection Error:', error.message); }); setSocket(socket); }; const disconnect = () => { -// localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -120,28 +108,22 @@ const JoinRoom: React.FC = () => { } if (username && roomName) { - console.log(`Tentative de rejoindre : ${roomName}, utilisateur : ${username}`); - webSocketService.joinRoom(roomName, username); } }; const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => { - console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`); const answerData: AnswerSubmissionToBackendType = { roomName: roomName, answer: answer, username: username, idQuestion: idQuestion }; - // localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); setAnswers((prevAnswers) => { - console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`); - const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array - newAnswers[idQuestion - 1] = answerData; // Update the specific answer - return newAnswers; // Return the new array + const newAnswers = [...prevAnswers]; + newAnswers[idQuestion - 1] = answerData; + return newAnswers; }); - console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`); webSocketService.submitAnswer(answerData); }; @@ -153,22 +135,20 @@ const JoinRoom: React.FC = () => { if (isWaitingForTeacher) { return ( -
-
- +
+
-
-
Salle: {roomName}
-
+
+

Salle: {roomName}

+

En attente que le professeur lance le questionnaire... -

+

-
- +
{/* Spacer for balance */}
); @@ -197,44 +177,42 @@ const JoinRoom: React.FC = () => { ); default: return ( - - + setRoomName(e.target.value.toUpperCase())} placeholder="Nom de la salle" - sx={{ marginBottom: '1rem' }} - fullWidth={true} + fullWidth onKeyDown={handleReturnKey} /> setUsername(e.target.value)} placeholder="Nom d'utilisateur" - sx={{ marginBottom: '1rem' }} - fullWidth={true} + fullWidth onKeyDown={handleReturnKey} /> - Rejoindre - + className="w-100" + onClick={handleSocket} + disabled={!username || !roomName || isConnecting} + startIcon={isConnecting ? : null} + > + Rejoindre + ); } }; -export default JoinRoom; +export default JoinRoom; \ No newline at end of file diff --git a/client/src/pages/Student/JoinRoom/joinRoom.css b/client/src/pages/Student/JoinRoom/joinRoom.css deleted file mode 100644 index 5cb6b01..0000000 --- a/client/src/pages/Student/JoinRoom/joinRoom.css +++ /dev/null @@ -1,50 +0,0 @@ - - -/* .join-room-container { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - height: 85%; -} - -.waiting-text { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 85%; - text-align: center; -} - -.login-container { - display: flex; - flex-direction: column; - align-items: center; - margin: 2rem 4rem 2rem 4rem; - width: 25vw; -} - -.login-avatar { - margin-bottom: 2rem; -} -.question-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; -} - -.question-component-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -@media only screen and (max-device-width: 768px) { - .login-container { - width: inherit; - } -} */ diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index cdae619..fb8ef6a 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -1,19 +1,12 @@ -// Dashboard.tsx import { useNavigate } from 'react-router-dom'; import React, { useState, useEffect, useMemo } from 'react'; import { parse } from 'gift-pegjs'; - import Template from 'src/components/GiftTemplate/templates'; import { QuizType } from '../../../Types/QuizType'; import { FolderType } from '../../../Types/FolderType'; -// import { QuestionService } from '../../../services/QuestionService'; import ApiService from '../../../services/ApiService'; - -import './dashboard.css'; import ImportModal from 'src/components/ImportModal/ImportModal'; -//import axios from 'axios'; import { RoomType } from 'src/Types/RoomType'; -// import { useRooms } from '../ManageRoom/RoomContext'; import { Dialog, DialogActions, @@ -40,17 +33,31 @@ import { ContentCopy, Edit, Share - // DriveFileMove } from '@mui/icons-material'; +import 'bootstrap/dist/css/bootstrap.min.css'; -// Create a custom-styled Card component -const CustomCard = styled(Card)({ - overflow: 'visible', // Override the overflow property +const CustomCard = styled(Card)(({ theme }) => ({ + overflow: 'visible', position: 'relative', - margin: '40px 0 20px 0', // Add top margin to make space for the tab + margin: '40px 0 20px 0', borderRadius: '8px', - paddingTop: '20px' // Ensure content inside the card doesn't overlap with the tab -}); + paddingTop: '20px', + border: `2px solid ${theme.palette.divider}`, + '& .folder-tab': { + position: 'absolute', + top: '-33px', + left: '9px', + padding: '5px 10px', + borderRadius: '8px 8px 0 0', + fontWeight: 'bold', + whiteSpace: 'nowrap', + display: 'inline-block', + border: `2px solid ${theme.palette.divider}`, + borderBottom: 'none', + backgroundColor: theme.palette.background.paper, + color: theme.palette.primary.main + } +})); const Dashboard: React.FC = () => { const navigate = useNavigate(); @@ -58,19 +65,14 @@ const Dashboard: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const [showImportModal, setShowImportModal] = useState(false); const [folders, setFolders] = useState([]); - const [selectedFolderId, setSelectedFolderId] = useState(''); // Selected folder + const [selectedFolderId, setSelectedFolderId] = useState(''); const [rooms, setRooms] = useState([]); const [openAddRoomDialog, setOpenAddRoomDialog] = useState(false); const [newRoomTitle, setNewRoomTitle] = useState(''); - // const { selectedRoom, selectRoom, createRoom } = useRooms(); - const [selectedRoom, selectRoom] = useState(); // menu + const [selectedRoom, selectRoom] = useState(); const [errorMessage, setErrorMessage] = useState(''); const [showErrorDialog, setShowErrorDialog] = useState(false); - // Filter quizzes based on search term - // const filteredQuizzes = quizzes.filter(quiz => - // quiz.title.toLowerCase().includes(searchTerm.toLowerCase()) - // ); const filteredQuizzes = useMemo(() => { return quizzes.filter( (quiz) => @@ -78,7 +80,6 @@ const Dashboard: React.FC = () => { ); }, [quizzes, searchTerm]); - // Group quizzes by folder const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => { if (!acc[quiz.folderName]) { acc[quiz.folderName] = []; @@ -90,28 +91,18 @@ const Dashboard: React.FC = () => { useEffect(() => { const fetchData = async () => { const isLoggedIn = await ApiService.isLoggedIn(); - console.log(`Dashboard: isLoggedIn: ${isLoggedIn}`); if (!isLoggedIn) { navigate('/teacher/login'); return; } else { const userRooms = await ApiService.getUserRooms(); setRooms(userRooms as RoomType[]); - const userFolders = await ApiService.getUserFolders(); setFolders(userFolders as FolderType[]); } }; - fetchData(); - }, []); - - useEffect(() => { - if (rooms.length > 0 && !selectedRoom) { - selectRoom(rooms[rooms.length - 1]); - localStorage.setItem('selectedRoomId', rooms[rooms.length - 1]._id); - } - }, [rooms, selectedRoom]); + }, [navigate]); const handleSelectRoom = (event: React.ChangeEvent) => { if (event.target.value === 'add-room') { @@ -424,31 +415,38 @@ const Dashboard: React.FC = () => { }; return ( -
-
Tableau de bord
- -
- - +
+

Tableau de bord

+ {/* Room Selection */} +
+
+
+ + +
+
{selectedRoom && ( -
+

Salle sélectionnée: {selectedRoom.title}

)} + {/* Dialogs */} setOpenAddRoomDialog(false)}> Créer une nouvelle salle @@ -463,6 +461,7 @@ const Dashboard: React.FC = () => { + setShowErrorDialog(false)}> Erreur @@ -473,193 +472,189 @@ const Dashboard: React.FC = () => { -
- - - - - - ) - }} - /> + {/* Search Bar */} +
+
+ + + + + + ) + }} + /> +
-
-
+ {/* Folder Selection and Actions */} +
+
- - + {folders.map((folder: FolderType) => ( ))}
- -
- - - {' '} - {' '} - - - - -
- - {' '} - {' '} - -
-
- - -
- - {' '} - {' '} - -
-
- - -
- - {' '} - {' '} - -
-
+
+
+ + + + + + + + + + + + + + + + + + + + +
-
- - - + {/* Add Quiz and Import Buttons */} +
+
+ +
+
+ +
-
+ + {/* Quiz List */} +
{Object.keys(quizzesByFolder).map((folderName) => ( - -
{folderName}
- - {quizzesByFolder[folderName].map((quiz: QuizType) => ( -
-
- -
- -
-
+ +
+
+ + downloadTxtFile(quiz)} + className="border" + > + + + + + handleEditQuiz(quiz)} + className="border" + > + + + + + handleDuplicateQuiz(quiz)} + className="border" + > + + + + + handleRemoveQuiz(quiz)} + className="border" + > + + + + + handleShareQuiz(quiz)} + className="border" + > + + + +
- -
- - downloadTxtFile(quiz)} - > - {' '} - {' '} - - - - - handleEditQuiz(quiz)} - > - {' '} - {' '} - - - - - handleDuplicateQuiz(quiz)} - > - {' '} - {' '} - - - - - handleRemoveQuiz(quiz)} - > - {' '} - {' '} - - - - - handleShareQuiz(quiz)} - > - {' '} - {' '} - - -
-
- ))} - - + ))} + + +
))}
+ setShowImportModal(false)} @@ -670,11 +665,13 @@ const Dashboard: React.FC = () => { ); }; -export default Dashboard; +// Helper function function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) { - if (Array.isArray(folderQuizzes)) + if (Array.isArray(folderQuizzes)) { folderQuizzes.forEach((quiz) => { quiz.folderName = folderName; - console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`); }); + } } + +export default Dashboard; \ No newline at end of file diff --git a/client/src/pages/Teacher/Dashboard/dashboard.css b/client/src/pages/Teacher/Dashboard/dashboard.css deleted file mode 100644 index a17f46a..0000000 --- a/client/src/pages/Teacher/Dashboard/dashboard.css +++ /dev/null @@ -1,119 +0,0 @@ -.dashboard { - display: flex; - flex-direction: column; - max-width: 100%; - gap: 30px; - -} - -.dashboard .folder { - display: flex; - flex-direction: row; -} - -.dashboard .folder .select { - flex-grow: 8; - - display: flex; - align-items: center; -} - -/* Select the selector to make it 100% width */ -div:has(> #select-folder) { - width: 100%; -} - -.dashboard .folder .actions { - flex-shrink: 0; - display: flex; - flex-direction: row; - align-items: center; -} - -.dashboard .ajouter { - display: flex; - flex-direction: row; - gap: 10px; -} - -.dashboard .ajouter button:first-child { - width: 100%; -} - -.dashboard .list { - display: flex; - flex-direction: column; -} - -.dashboard .list .quiz { - display: flex; - flex-direction: row; - - margin-bottom: 10px; - box-sizing: content-box; -} - -.dashboard .list .quiz .title { - flex-grow: 8; - - display: flex; - align-items: center; - - /* reset title css */ - font-size: large; - margin: 0; - font-weight: 100; - overflow: hidden; -} -.dashboard .list .quiz .title button { - overflow: hidden; - white-space: nowrap; - display: block; - text-overflow: ellipsis; -} - -.dashboard .list .quiz .actions { - flex-shrink: 0; - display: flex; - 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; -} */ diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 89f822a..c005735 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -1,22 +1,17 @@ -// EditorQuiz.tsx -import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; - import { FolderType } from '../../../Types/FolderType'; - import Editor from 'src/components/Editor/Editor'; import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet'; import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview'; - import { QuizType } from '../../../Types/QuizType'; - -import './editorQuiz.css'; import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; - import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; import { Upload } from '@mui/icons-material'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import SaveIcon from '@mui/icons-material/Save'; interface EditQuizParams { id: string; @@ -27,7 +22,6 @@ const QuizForm: React.FC = () => { const [quizTitle, setQuizTitle] = useState(''); const [selectedFolder, setSelectedFolder] = useState(''); const [filteredValue, setFilteredValue] = useState([]); - const { id } = useParams(); const [value, setValue] = useState(''); const [isNewQuiz, setNewQuiz] = useState(false); @@ -35,9 +29,6 @@ const QuizForm: React.FC = () => { const navigate = useNavigate(); const [folders, setFolders] = useState([]); const [imageLinks, setImageLinks] = useState([]); - const handleSelectFolder = (event: React.ChangeEvent) => { - setSelectedFolder(event.target.value); - }; const fileInputRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); @@ -48,25 +39,15 @@ const QuizForm: React.FC = () => { useEffect(() => { const handleScroll = () => { - if (window.scrollY > 300) { - setShowScrollButton(true); - } else { - setShowScrollButton(false); - } + setShowScrollButton(window.scrollY > 300); }; - window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; + return () => window.removeEventListener('scroll', handleScroll); }, []); const scrollToImagesSection = (event: { preventDefault: () => void; }) => { event.preventDefault(); - const section = document.getElementById('images-section'); - if (section) { - section.scrollIntoView({ behavior: 'smooth' }); - } + document.getElementById('images-section')?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { @@ -74,7 +55,6 @@ const QuizForm: React.FC = () => { const userFolders = await ApiService.getUserFolders(); setFolders(userFolders as FolderType[]); }; - fetchData(); }, []); @@ -87,116 +67,83 @@ const QuizForm: React.FC = () => { } const quiz = await ApiService.getQuiz(id) as QuizType; - if (!quiz) { - window.alert(`Une erreur est survenue.\n Le quiz ${id} n'a pas été trouvé\nVeuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); + window.alert(`Une erreur est survenue.\n Le quiz ${id} n'a pas été trouvé\nVeuillez réessayer plus tard`); navigate('/teacher/dashboard'); return; } - setQuiz(quiz as QuizType); - const { title, content, folderId } = quiz; - - setQuizTitle(title); - setSelectedFolder(folderId); - setFilteredValue(content); + setQuiz(quiz); + setQuizTitle(quiz.title); + setSelectedFolder(quiz.folderId); + setFilteredValue(quiz.content); setValue(quiz.content.join('\n\n')); - } catch (error) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - console.error('Error fetching quiz:', error); + window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); navigate('/teacher/dashboard'); } }; - fetchData(); - }, [id]); + }, [id, navigate]); function handleUpdatePreview(value: string) { if (value !== '') { setValue(value); + const linesArray = value.split(/\n{2,}/); + if (linesArray[0] === '') linesArray.shift(); + if (linesArray[linesArray.length - 1] === '') linesArray.pop(); + setFilteredValue(linesArray); } - - // split value when there is at least one blank line - const linesArray = value.split(/\n{2,}/); - - // if the first item in linesArray is blank, remove it - if (linesArray[0] === '') linesArray.shift(); - - if (linesArray[linesArray.length - 1] === '') linesArray.pop(); - - setFilteredValue(linesArray); } const handleQuizTitleChange = (event: React.ChangeEvent) => { setQuizTitle(event.target.value); }; + const handleSelectFolder = (event: React.ChangeEvent) => { + setSelectedFolder(event.target.value); + }; + const handleQuizSave = async () => { try { - // check if everything is there - if (quizTitle == '') { + if (quizTitle === '') { alert("Veuillez choisir un titre"); return; } - - if (selectedFolder == '') { + if (selectedFolder === '') { alert("Veuillez choisir un dossier"); return; } if (isNewQuiz) { await ApiService.createQuiz(quizTitle, filteredValue, selectedFolder); - } else { - if (quiz) { - await ApiService.updateQuiz(quiz._id, quizTitle, filteredValue); - } + } else if (quiz) { + await ApiService.updateQuiz(quiz._id, quizTitle, filteredValue); } - navigate('/teacher/dashboard'); } catch (error) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - console.log(error) + window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); } }; - // I do not know what this does but do not remove - if (!isNewQuiz && !quiz) { - return
Chargement...
; - } - const handleSaveImage = async () => { try { const inputElement = document.getElementById('file-input') as HTMLInputElement; - if (!inputElement?.files || inputElement.files.length === 0) { setDialogOpen(true); return; } - if (!inputElement.files || inputElement.files.length === 0) { - window.alert("Veuillez d'abord choisir une image à téléverser.") - return; - } - const imageUrl = await ApiService.uploadImage(inputElement.files[0]); - - // Check for errors - if(imageUrl.indexOf("ERROR") >= 0) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) + if (imageUrl.indexOf("ERROR") >= 0) { + window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); return; } setImageLinks(prevLinks => [...prevLinks, imageUrl]); - - // Reset the file input element - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } + if (fileInputRef.current) fileInputRef.current.value = ''; } catch (error) { - window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) - + window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); } }; @@ -204,109 +151,125 @@ const QuizForm: React.FC = () => { navigator.clipboard.writeText(link); } + if (!isNewQuiz && !quiz) { + return
Chargement...
; + } + return ( -
- -
- - -
Éditeur de quiz
- -
+
+ {/* Header */} +
+
+ +
+

Éditeur de quiz

+
{/* Spacer for balance */}
- {/*

Éditeur

*/} + {/* Quiz Info */} +
+
+ +
+
+
+ + + + {folders.map((folder: FolderType) => ( + + ))} + +
+
+
- - - - - + -
- -
+ {/* Editor Section */} +
+ {/* Editor Column */} +
+ onEditorChange={handleUpdatePreview} + /> -
-
-