From fefe278d7954243fa605e10b1470438e32de0b60 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Tue, 17 Sep 2024 18:56:13 -0400 Subject: [PATCH] Actualiser l'aide pour exemples avec markdown, images, etc. Fixes #102 --- .../components/Editor/Editor.test.tsx | 4 + .../Teacher/EditorQuiz/EditorQuiz.test.tsx | 2 +- client/src/components/Editor/Editor.tsx | 15 +- .../GIFTCheatSheet/GiftCheatSheet.tsx | 45 +++-- .../GIFTCheatSheet/giftCheatSheet.css | 2 +- client/src/index.css | 187 +++--------------- .../src/pages/Teacher/Dashboard/Dashboard.tsx | 2 +- .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 141 ++++++++----- .../pages/Teacher/EditorQuiz/editorQuiz.css | 14 +- client/src/utils/giftUtils.ts | 4 + 10 files changed, 182 insertions(+), 234 deletions(-) create mode 100644 client/src/utils/giftUtils.ts diff --git a/client/src/__tests__/components/Editor/Editor.test.tsx b/client/src/__tests__/components/Editor/Editor.test.tsx index 9a08e06..d5d6921 100644 --- a/client/src/__tests__/components/Editor/Editor.test.tsx +++ b/client/src/__tests__/components/Editor/Editor.test.tsx @@ -7,6 +7,7 @@ describe('Editor Component', () => { const mockOnEditorChange = jest.fn(); const sampleProps = { + label: 'Sample Label', initialValue: 'Sample Initial Value', onEditorChange: mockOnEditorChange }; @@ -29,6 +30,7 @@ describe('Editor Component', () => { it('updates editor value when initialValue prop changes', () => { const updatedProps = { + label: 'Updated Label', initialValue: 'Updated Initial Value', onEditorChange: mockOnEditorChange }; @@ -43,6 +45,7 @@ describe('Editor Component', () => { test('should call change text with the correct value on textarea change', () => { const updatedProps = { + label: 'Updated Label', initialValue: 'Updated Initial Value', onEditorChange: mockOnEditorChange }; @@ -58,6 +61,7 @@ describe('Editor Component', () => { test('should call onEditorChange with an empty string if textarea value is falsy', () => { const updatedProps = { + label: 'Updated Label', initialValue: 'Updated Initial Value', onEditorChange: mockOnEditorChange }; diff --git a/client/src/__tests__/pages/Teacher/EditorQuiz/EditorQuiz.test.tsx b/client/src/__tests__/pages/Teacher/EditorQuiz/EditorQuiz.test.tsx index bc18f6e..d75c9ee 100644 --- a/client/src/__tests__/pages/Teacher/EditorQuiz/EditorQuiz.test.tsx +++ b/client/src/__tests__/pages/Teacher/EditorQuiz/EditorQuiz.test.tsx @@ -28,7 +28,7 @@ describe('QuizForm Component', () => { ); expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument(); - expect(screen.queryByText('Éditeur')).toBeInTheDocument(); + // expect(screen.queryByText('Éditeur')).toBeInTheDocument(); expect(screen.queryByText('Prévisualisation')).toBeInTheDocument(); }); diff --git a/client/src/components/Editor/Editor.tsx b/client/src/components/Editor/Editor.tsx index 46d6ec0..540cb4a 100644 --- a/client/src/components/Editor/Editor.tsx +++ b/client/src/components/Editor/Editor.tsx @@ -1,13 +1,15 @@ // Editor.tsx import React, { useState, useRef } from 'react'; import './editor.css'; +import { TextareaAutosize } from '@mui/material'; interface EditorProps { + label: string; initialValue: string; onEditorChange: (value: string) => void; } -const Editor: React.FC = ({ initialValue, onEditorChange }) => { +const Editor: React.FC = ({ initialValue, onEditorChange, label }) => { const [value, setValue] = useState(initialValue); const editorRef = useRef(null); @@ -18,14 +20,17 @@ const Editor: React.FC = ({ initialValue, onEditorChange }) => { } return ( -
- -
+ minRows={5} + /> + ); }; diff --git a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx index 4dc9f1e..6e13395 100644 --- a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx +++ b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx @@ -21,18 +21,18 @@ const GiftCheatSheet: React.FC = () => { }; - const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Vous pouvez utiliser les valeurs {T}, {F}, {TRUE} et {FALSE}"; + const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}"; const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}// La bonne réponse est Ottawa"; - const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n~ %33.3% Ottawa \n~ %33.3% Vancouver \n~ %-100% New York \n~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n} //On utilise le signe ~ pour toutes les réponses. On doit indiquer le pourcentage de chaque réponse"; - const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}// Permet de fournir plusieurs bonnes réponses. Note: Les majuscules ne sont pas prises en compte."; - const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} //OU \nQuestion {#=PetitNombre..GrandNombre\n} // La tolérance est un pourcentage. La réponse doit être comprise entre PetitNombre et GrandNombre"; + const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez le signe ~ pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse."; + const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte."; + 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 (

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 quizs. Ci-dessous vous pouvez retrouver la - syntaxe pour chaque type de question ainsi que les champs optionnels : + 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

@@ -69,7 +69,7 @@ const GiftCheatSheet: React.FC = () => {
-

4. Questions à reponse courte

+

4. Questions à réponse courte

                     
                         {QuestionCourte}
@@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
             
-

5. Questions numériques

+

5. Question numérique

                     
                         {
@@ -139,7 +139,7 @@ const GiftCheatSheet: React.FC = () => {
             

8. LaTeX et Markdown

- Les format LaTeX et markdown sont supportés dans cette application. Vous devez cependant penser + Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser à 'échapper' les caractères spéciaux mentionnés plus haut.

Exemple d'équation:

@@ -154,18 +154,30 @@ const GiftCheatSheet: React.FC = () => {
-

9. inserer une image

-

Pour insérer une image, vous devez utiliser la syntaxe suivante:

+

9. Images

+

Pour insérer une image dans une question ou dans une réponse, vous devez utiliser la syntaxe suivante:

                     
-                        {'{`un_URL_d_image`}
-                        {' >'}
+                        {'!['}
+                        {`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}'}
+                    
+                
+

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.

+

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

- Attention nous ne supportons pas encore les images en tant que réponses à une - question + Attention: l'ancienne fonctionnalité avec les balises {''} n'est plus + supportée.

@@ -184,5 +196,4 @@ const GiftCheatSheet: React.FC = () => { ); }; - export default GiftCheatSheet; diff --git a/client/src/components/GIFTCheatSheet/giftCheatSheet.css b/client/src/components/GIFTCheatSheet/giftCheatSheet.css index c338ff3..5fc7777 100644 --- a/client/src/components/GIFTCheatSheet/giftCheatSheet.css +++ b/client/src/components/GIFTCheatSheet/giftCheatSheet.css @@ -1,5 +1,5 @@ .gift-cheat-sheet { - width: 30vw; + /* width: 30vw; */ height: 100%; } .subtitle { diff --git a/client/src/index.css b/client/src/index.css index b873748..291aa8f 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -25,12 +25,39 @@ body { } .content { - max-width: 1000px; margin: auto; - height: 100%; display: flex; flex-direction: column; + max-width: 100%; +} + +/* Media query for phones in portrait mode (max-width: 767px) */ +@media (max-width: 767px) { + .content { + max-width: 100%; /* Full width for small screens */ + } +} + +/* Media query for tablets (min-width: 768px) */ +@media (min-width: 768px) { + .content { + max-width: 750px; + } +} + +/* Media query for small desktops (min-width: 992px) */ +@media (min-width: 992px) { + .content { + max-width: 970px; + } +} + +/* Media query for large desktops (min-width: 1200px) */ +@media (min-width: 1800px) { + .content { + max-width: 1770px; + } } .app { @@ -55,159 +82,3 @@ main { padding: 2rem 2rem; } -/* - - -main { - height: 85%; - width: 100%; - display: flex; - flex-direction: column; - margin-top: 5rem; -} -.wrapper { - height: 100%; -} - -#root { - height: 100%; - overflow: hidden; -} - -.app { - height: 100%; - padding: 1rem; -} - -.logo { - position: absolute; - cursor: pointer; - left: 0.5rem; - top: 0.5rem; -} - -.center { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.title { - font-size: xx-large; - font-weight: 600; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-2xl { - font-size: 1.5rem; - line-height: 1.75rem; -} - -.center-v-align { - display: flex; - flex-direction: column; - align-items: center; -} - -.center-v-align > * { - padding: 10px; -} - -.center-h-align { - display: flex; - justify-content: center; - align-items: center; -} - -.center-h-align > * { - padding: 10px; -} - -.text-bold { - font-weight: bold; -} - -.end-h-align { - display: flex; - align-items: center; - justify-content: space-between; -} - -.w-full { - width: 100%; -} - -.h-full { - height: 100%; -} - -.error-text { - color: red; -} - -.page-title { - font-size: 36pt; - font-weight: 600; - text-align: center; -} -.quit-btn { - position: absolute; - right: 1rem; - top: 1rem; -} - -.overflow-auto { - overflow: auto; -} - -.mt-1\/2 { - margin-top: 0.5rem; -} -.mb-1 { - margin-bottom: 1rem; -} - -.mb-2 { - margin-bottom: 2rem; -} - -.mb-3 { - margin-bottom: 3rem; -} - -.mb-4 { - margin-bottom: 4rem; -} - -.mb-5 { - margin-bottom: 5rem; -} - -.text-center { - text-align: center; -} - -.blue { - color: #5271ff; -} - -.w-12 { - width: 13rem; -} */ \ No newline at end of file diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index 17b7112..7c0bff5 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -370,7 +370,7 @@ const Dashboard: React.FC = () => { { const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); }; + const fileInputRef = useRef(null); + const [dialogOpen, setDialogOpen] = useState(false); useEffect(() => { const fetchData = async () => { @@ -134,9 +137,14 @@ const QuizForm: React.FC = () => { 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 un fichier à télécharger") + window.alert("Veuillez d'abord choisir une image à téléverser.") return; } @@ -149,6 +157,11 @@ const QuizForm: React.FC = () => { } setImageLinks(prevLinks => [...prevLinks, imageUrl]); + + // Reset the file input element + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } } catch (error) { window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) } @@ -172,73 +185,103 @@ const QuizForm: React.FC = () => {
+ {/*

Éditeur

*/} + + + + + + + +
-

Éditeur

- - - - - - {folders.map((folder: FolderType) => ( - - ))} - - - +
- - - + setDialogOpen(false)} > + Erreur + + Veuillez d'abord choisir une image à téléverser. + + + + +
-

Mes images :

+

Mes images :

+
(Cliquez sur un lien pour le copier)
    - {imageLinks.map((link, index) => ( -
  • - handleCopyToClipboard(``)}> - {``} - -
  • - ))} + {imageLinks.map((link, index) => { + const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + return ( +
  • + handleCopyToClipboard(imgTag)}> + {imgTag} + +
  • + ); + })}
- -
-

Prévisualisation

+

Prévisualisation

diff --git a/client/src/pages/Teacher/EditorQuiz/editorQuiz.css b/client/src/pages/Teacher/EditorQuiz/editorQuiz.css index 2d59f3d..79157ff 100644 --- a/client/src/pages/Teacher/EditorQuiz/editorQuiz.css +++ b/client/src/pages/Teacher/EditorQuiz/editorQuiz.css @@ -44,10 +44,20 @@ .quizEditor .editSection .edit .upload { display: flex; width: 100%; - - align-items: center; + flex-direction: row; + align-items: right; justify-content: center; + gap: 8px; + flex-wrap: wrap; } + +@media (max-width: 600px) { + .upload .dropArea { + flex-direction: column; + /* align-items: stretch; */ + } +} + input[type="file"] { height: 100%; width: 100%; diff --git a/client/src/utils/giftUtils.ts b/client/src/utils/giftUtils.ts new file mode 100644 index 0000000..f81bda8 --- /dev/null +++ b/client/src/utils/giftUtils.ts @@ -0,0 +1,4 @@ +export function escapeForGIFT(link: string): string { + const specialChars = /[{}#~=<>\:]/g; + return link.replace(specialChars, (match) => `\\${match}`); +}