Actualiser l'aide pour exemples avec markdown, images, etc.

Fixes #102
This commit is contained in:
C. Fuhrman 2024-09-17 18:56:13 -04:00
parent 851d04dfac
commit fefe278d79
10 changed files with 182 additions and 234 deletions

View file

@ -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
};

View file

@ -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();
});

View file

@ -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<EditorProps> = ({ initialValue, onEditorChange }) => {
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label }) => {
const [value, setValue] = useState(initialValue);
const editorRef = useRef<HTMLTextAreaElement | null>(null);
@ -18,14 +20,17 @@ const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange }) => {
}
return (
<div>
<textarea
<label>
<h4>{label}</h4>
<TextareaAutosize
id="editor-textarea"
ref={editorRef}
onChange={handleEditorChange}
value={value}
className="editor"
></textarea>
</div>
minRows={5}
/>
</label>
);
};

View file

@ -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 (
<div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
<span>
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&nbsp;:
</span>
<div className="question-type">
<h4>1. Questions Vrai/Faux</h4>
@ -69,7 +69,7 @@ const GiftCheatSheet: React.FC = () => {
</div>
<div className="question-type">
<h4>4. Questions à reponse courte</h4>
<h4>4. Questions à réponse courte</h4>
<pre>
<code className="question-code-block selectable-text">
{QuestionCourte}
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
</div>
<div className="question-type">
<h4> 5. Questions numériques </h4>
<h4> 5. Question numérique </h4>
<pre>
<code className="question-code-block selectable-text">
{
@ -139,7 +139,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type">
<h4> 8. LaTeX et Markdown</h4>
<p>
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.
</p>
<p>Exemple d'équation:</p>
@ -154,18 +154,30 @@ const GiftCheatSheet: React.FC = () => {
</div>
<div className="question-type">
<h4> 9. inserer une image </h4>
<p>Pour insérer une image, vous devez utiliser la syntaxe suivante:</p>
<h4> 9. Images </h4>
<p>Pour insérer une image dans une question ou dans une réponse, vous devez utiliser la syntaxe suivante:</p>
<pre>
<code className="question-code-block">
{'<img '}
<span className="code-comment">{`un_URL_d_image`}</span>
{' >'}
{'!['}
<span className="code-comment">{`text alternatif`}</span>
{']('}
<span className="code-comment">{`URL-de-l'image`}</span>
{' "'}
<span className="code-comment">{`texte de l'infobulle`}</span>
{'")'}
</code>
</pre>
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
<pre>
<code className="question-code-block">
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code>
</pre>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
<p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
<p style={{ color: 'red' }}>
Attention nous ne supportons pas encore les images en tant que réponses à une
question
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
supportée.
</p>
</div>
@ -184,5 +196,4 @@ const GiftCheatSheet: React.FC = () => {
);
};
export default GiftCheatSheet;

View file

@ -1,5 +1,5 @@
.gift-cheat-sheet {
width: 30vw;
/* width: 30vw; */
height: 100%;
}
.subtitle {

View file

@ -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;
} */

View file

@ -370,7 +370,7 @@ const Dashboard: React.FC = () => {
<TextField
onChange={handleSearch}
value={searchTerm}
placeholder="Rechercher un quiz"
placeholder="Rechercher un quiz par son titre"
fullWidth
InputProps={{
endAdornment: (

View file

@ -1,5 +1,5 @@
// EditorQuiz.tsx
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FolderType } from '../../../Types/FolderType';
@ -11,11 +11,12 @@ import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePr
import { QuizType } from '../../../Types/QuizType';
import './editorQuiz.css';
import { Button, TextField, NativeSelect, IconButton } from '@mui/material';
import { Send } from '@mui/icons-material';
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService';
import { escapeForGIFT } from '../../../utils/giftUtils';
import { Upload } from '@mui/icons-material';
interface EditQuizParams {
id: string;
@ -37,6 +38,8 @@ const QuizForm: React.FC = () => {
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedFolder(event.target.value);
};
const fileInputRef = useRef<HTMLInputElement>(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 = () => {
<div className='dumb'></div>
</div>
{/* <h2 className="subtitle">Éditeur</h2> */}
<TextField
onChange={handleQuizTitleChange}
value={quizTitle}
placeholder="Titre du quiz"
label="Titre du quiz"
fullWidth
/>
<label>Choisir un dossier:
<NativeSelect
id="select-folder"
color="primary"
value={selectedFolder}
onChange={handleSelectFolder}
disabled={!isNewQuiz}
style={{ marginBottom: '16px' }} // Ajout de marge en bas
>
<option disabled value=""> Choisir un dossier... </option>
{folders.map((folder: FolderType) => (
<option value={folder._id} key={folder._id}> {folder.title} </option>
))}
</NativeSelect></label>
<Button variant="contained" onClick={handleQuizSave}>
Enregistrer
</Button>
<Divider style={{ margin: '16px 0' }} />
<div className='editSection'>
<div className='edit'>
<h2 className="subtitle">Éditeur</h2>
<TextField
onChange={handleQuizTitleChange}
value={quizTitle}
placeholder="Titre du quiz"
fullWidth
/>
<NativeSelect
id="select-folder"
color="primary"
value={selectedFolder}
onChange={handleSelectFolder}
disabled={!isNewQuiz}
>
<option disabled value=""> Choisir un dossier... </option>
{folders.map((folder: FolderType) => (
<option value={folder._id} key={folder._id}> {folder.title} </option>
))}
</NativeSelect>
<Editor initialValue={value} onEditorChange={handleUpdatePreview} />
<Editor
label="Contenu GIFT du quiz:"
initialValue={value}
onEditorChange={handleUpdatePreview} />
<div className='images'>
<div className='upload'>
<label className="dropArea">
<input type="file" id="file-input" className="file-input" accept="image/jpeg" multiple />
<input type="file" id="file-input" className="file-input"
accept="image/jpeg, image/png"
multiple
ref={fileInputRef} />
<Button
variant="outlined"
aria-label='Téléverser'
onClick={handleSaveImage}>
Téléverser <Upload />
</Button>
</label>
<IconButton
color="primary"
onClick={handleSaveImage}
> <Send /> </IconButton>
<Dialog
open={dialogOpen}
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d'abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">
OK
</Button>
</DialogActions>
</Dialog>
</div>
<h2 className="subtitle">Mes images :</h2>
<h4>Mes images :</h4>
<div>
<div>(Cliquez sur un lien pour le copier)</div>
<ul>
{imageLinks.map((link, index) => (
<li key={index}>
<code
onClick={() => handleCopyToClipboard(`<img ${link} >`)}>
{`<img ${link} >`}
</code>
</li>
))}
{imageLinks.map((link, index) => {
const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`;
return (
<li key={index}>
<code
onClick={() => handleCopyToClipboard(imgTag)}>
{imgTag}
</code>
</li>
);
})}
</ul>
</div>
</div>
<Button variant="contained" onClick={handleQuizSave}>
Enregistrer
</Button>
<GiftCheatSheet />
</div>
<div className='preview'>
<div className="preview-column">
<h2 className="subtitle">Prévisualisation</h2>
<h4>Prévisualisation</h4>
<div>
<GIFTTemplatePreview questions={filteredValue} />
</div>

View file

@ -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%;

View file

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