2024-03-29 20:08:34 -04:00
// EditorQuiz.tsx
2025-01-14 17:37:29 -05:00
import React , { useState , useEffect , useRef , CSSProperties } from 'react' ;
2024-03-29 20:08:34 -04:00
import { useParams , useNavigate } from 'react-router-dom' ;
import { FolderType } from '../../../Types/FolderType' ;
2025-01-16 12:37:07 -05:00
import Editor from 'src/components/Editor/Editor' ;
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet' ;
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview' ;
2024-03-29 20:08:34 -04:00
import { QuizType } from '../../../Types/QuizType' ;
import './editorQuiz.css' ;
2025-03-28 15:24:58 -04:00
import { Button , TextField , NativeSelect , Divider , Dialog , DialogTitle , DialogActions , DialogContent , MenuItem , Select , Snackbar } from '@mui/material' ;
2025-01-16 12:37:07 -05:00
import ReturnButton from 'src/components/ReturnButton/ReturnButton' ;
2024-03-29 20:08:34 -04:00
import ApiService from '../../../services/ApiService' ;
2024-09-17 18:56:13 -04:00
import { escapeForGIFT } from '../../../utils/giftUtils' ;
import { Upload } from '@mui/icons-material' ;
2024-03-29 20:08:34 -04:00
interface EditQuizParams {
id : string ;
[ key : string ] : string | undefined ;
}
const QuizForm : React.FC = ( ) = > {
const [ quizTitle , setQuizTitle ] = useState ( '' ) ;
const [ selectedFolder , setSelectedFolder ] = useState < string > ( '' ) ;
const [ filteredValue , setFilteredValue ] = useState < string [ ] > ( [ ] ) ;
const { id } = useParams < EditQuizParams > ( ) ;
const [ value , setValue ] = useState ( '' ) ;
const [ isNewQuiz , setNewQuiz ] = useState ( false ) ;
const [ quiz , setQuiz ] = useState < QuizType | null > ( null ) ;
const navigate = useNavigate ( ) ;
const [ folders , setFolders ] = useState < FolderType [ ] > ( [ ] ) ;
const [ imageLinks , setImageLinks ] = useState < string [ ] > ( [ ] ) ;
const handleSelectFolder = ( event : React.ChangeEvent < HTMLSelectElement > ) = > {
setSelectedFolder ( event . target . value ) ;
} ;
2024-09-17 18:56:13 -04:00
const fileInputRef = useRef < HTMLInputElement > ( null ) ;
const [ dialogOpen , setDialogOpen ] = useState ( false ) ;
2025-01-14 17:37:29 -05:00
const [ showScrollButton , setShowScrollButton ] = useState ( false ) ;
2025-03-28 15:24:58 -04:00
const [ copySuccess , setCopySuccess ] = useState < string | null > ( null ) ;
2025-01-14 17:37:29 -05:00
const scrollToTop = ( ) = > {
window . scrollTo ( { top : 0 , behavior : 'smooth' } ) ;
} ;
2025-03-28 15:24:58 -04:00
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 templates = [
{ label : 'Vrai/Faux' , value : QuestionVraiFaux } ,
{ label : 'Choix multiples R1' , value : QuestionChoixMul } ,
{ label : 'Choix multiples R2+' , value : QuestionChoixMulMany } ,
{ label : 'Réponse courte' , value : QuestionCourte } ,
{ label : 'Numérique' , value : QuestionNum } ,
] ;
2025-01-14 17:37:29 -05:00
useEffect ( ( ) = > {
const handleScroll = ( ) = > {
if ( window . scrollY > 300 ) {
setShowScrollButton ( true ) ;
} else {
setShowScrollButton ( false ) ;
}
} ;
window . addEventListener ( 'scroll' , handleScroll ) ;
return ( ) = > {
window . removeEventListener ( 'scroll' , handleScroll ) ;
} ;
} , [ ] ) ;
2024-03-29 20:08:34 -04:00
2025-01-14 16:21:32 -05:00
const scrollToImagesSection = ( event : { preventDefault : ( ) = > void ; } ) = > {
event . preventDefault ( ) ;
const section = document . getElementById ( 'images-section' ) ;
if ( section ) {
section . scrollIntoView ( { behavior : 'smooth' } ) ;
}
} ;
2024-03-29 20:08:34 -04:00
useEffect ( ( ) = > {
const fetchData = async ( ) = > {
const userFolders = await ApiService . getUserFolders ( ) ;
setFolders ( userFolders as FolderType [ ] ) ;
} ;
fetchData ( ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
const fetchData = async ( ) = > {
try {
if ( ! id || id === 'new' ) {
setNewQuiz ( true ) ;
return ;
}
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 ) ;
navigate ( '/teacher/dashboard' ) ;
return ;
}
setQuiz ( quiz as QuizType ) ;
const { title , content , folderId } = quiz ;
setQuizTitle ( title ) ;
setSelectedFolder ( folderId ) ;
setFilteredValue ( 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 ) ;
navigate ( '/teacher/dashboard' ) ;
}
} ;
fetchData ( ) ;
} , [ id ] ) ;
function handleUpdatePreview ( value : string ) {
if ( value !== '' ) {
setValue ( value ) ;
}
2025-02-14 21:28:08 -05:00
// 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 ( ) ;
2024-03-29 20:08:34 -04:00
if ( linesArray [ linesArray . length - 1 ] === '' ) linesArray . pop ( ) ;
setFilteredValue ( linesArray ) ;
}
const handleQuizTitleChange = ( event : React.ChangeEvent < HTMLInputElement > ) = > {
setQuizTitle ( event . target . value ) ;
} ;
const handleQuizSave = async ( ) = > {
try {
// check if everything is there
if ( quizTitle == '' ) {
alert ( "Veuillez choisir un titre" ) ;
return ;
}
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 ) ;
}
}
navigate ( '/teacher/dashboard' ) ;
} catch ( error ) {
window . alert ( ` Une erreur est survenue. \ n Veuillez réessayer plus tard ` )
console . log ( error )
}
} ;
// I do not know what this does but do not remove
if ( ! isNewQuiz && ! quiz ) {
return < div > Chargement . . . < / div > ;
}
const handleSaveImage = async ( ) = > {
try {
const inputElement = document . getElementById ( 'file-input' ) as HTMLInputElement ;
2024-09-17 18:56:13 -04:00
if ( ! inputElement ? . files || inputElement . files . length === 0 ) {
setDialogOpen ( true ) ;
return ;
}
2024-03-29 20:08:34 -04:00
if ( ! inputElement . files || inputElement . files . length === 0 ) {
2024-09-17 18:56:13 -04:00
window . alert ( "Veuillez d'abord choisir une image à téléverser." )
2024-03-29 20:08:34 -04:00
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 ` )
return ;
}
setImageLinks ( prevLinks = > [ . . . prevLinks , imageUrl ] ) ;
2024-09-17 18:56:13 -04:00
// Reset the file input element
if ( fileInputRef . current ) {
fileInputRef . current . value = '' ;
}
2025-02-02 12:54:25 -05:00
} catch ( error ) {
2025-02-21 14:48:21 -05:00
window . alert ( ` Une erreur est survenue. \ n ${ error } \ nVeuillez réessayer plus tard. ` )
2025-01-11 02:22:14 -05:00
2024-03-29 20:08:34 -04:00
}
} ;
const handleCopyToClipboard = async ( link : string ) = > {
navigator . clipboard . writeText ( link ) ;
}
2025-03-28 15:24:58 -04:00
const copyToClipboard = ( text : string , label : string ) = > {
2025-03-16 13:18:30 -04:00
navigator . clipboard . writeText ( text )
. then ( ( ) = > {
2025-03-28 15:24:58 -04:00
setCopySuccess ( ` Copié dans le presse-papier: ${ label } ` ) ;
2025-03-16 13:18:30 -04:00
} )
2025-03-28 15:24:58 -04:00
. catch ( ( error ) = > console . error ( 'Clipboard error:' , error ) ) ;
2025-03-16 13:18:30 -04:00
} ;
2025-03-28 15:24:58 -04:00
const handleSelectChange = ( value : string , label : string ) = > {
copyToClipboard ( value , label ) ;
} ;
2025-03-16 13:18:30 -04:00
2024-03-29 20:08:34 -04:00
return (
< div className = 'quizEditor' >
< div className = 'editHeader' >
< ReturnButton
askConfirm
message = { ` Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire? ` }
/ >
< div className = 'title' > É diteur de quiz < / div >
< div className = 'dumb' > < / div >
< / div >
2024-09-17 18:56:13 -04:00
{ /* <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 >
2025-03-28 15:24:58 -04:00
< div className = 'sticky-buttons' >
< Select
value = ""
displayEmpty
onChange = { ( e ) = > handleSelectChange ( e . target . value , templates . find ( t = > t . value === e . target . value ) ? . label || '' ) }
style = { { width : '210px' } }
>
< MenuItem value = "" disabled > Modèles de questions < / MenuItem >
{ templates . map ( ( template , index ) = > (
< MenuItem key = { index } value = { template . value } > { template . label } < / MenuItem >
) ) }
< / Select >
< Button variant = "contained" onClick = { handleQuizSave } > Enregistrer < / Button >
2025-03-16 13:18:30 -04:00
< / div >
2024-09-17 18:56:13 -04:00
2025-03-28 15:24:58 -04:00
< Snackbar
open = { ! ! copySuccess }
autoHideDuration = { 3000 }
onClose = { ( ) = > setCopySuccess ( null ) }
message = { copySuccess }
anchorOrigin = { { vertical : 'bottom' , horizontal : 'center' } }
key = { copySuccess ? 'open' : 'close' }
/ >
2024-09-17 18:56:13 -04:00
< Divider style = { { margin : '16px 0' } } / >
2024-03-29 20:08:34 -04:00
< div className = 'editSection' >
< div className = 'edit' >
2024-09-17 18:56:13 -04:00
< Editor
label = "Contenu GIFT du quiz:"
initialValue = { value }
onEditorChange = { handleUpdatePreview } / >
2024-03-29 20:08:34 -04:00
< div className = 'images' >
< div className = 'upload' >
< label className = "dropArea" >
2024-09-17 18:56:13 -04:00
< 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 >
2024-03-29 20:08:34 -04:00
2024-09-17 18:56:13 -04:00
< / label >
< Dialog
open = { dialogOpen }
onClose = { ( ) = > setDialogOpen ( false ) } >
< DialogTitle > Erreur < / DialogTitle >
< DialogContent >
2025-01-11 02:22:14 -05:00
Veuillez d & apos ; abord choisir une image à téléverser .
2024-09-17 18:56:13 -04:00
< / DialogContent >
< DialogActions >
< Button onClick = { ( ) = > setDialogOpen ( false ) } color = "primary" >
OK
< / Button >
< / DialogActions >
< / Dialog >
2024-03-29 20:08:34 -04:00
< / div >
2024-09-17 18:56:13 -04:00
< h4 > Mes images : < / h4 >
2024-03-29 20:08:34 -04:00
< div >
2025-01-14 15:06:41 -05:00
< div >
2025-01-14 16:21:32 -05:00
< div style = { { display : "inline" } } > ( Voir section < / div >
< a href = "#images-section" style = { { textDecoration : "none" } } onClick = { scrollToImagesSection } >
< u > < em > < h4 style = { { display : "inline" } } > 9 . Images < / h4 > < / em > < / u >
< / a >
< div style = { { display : "inline" } } > ci - dessous < / div >
2025-01-14 15:06:41 -05:00
< div style = { { display : "inline" } } > ) < / div >
< br / >
< em > - Cliquez sur un lien pour le copier < / em >
< / div >
< ul >
2024-09-17 18:56:13 -04:00
{ imageLinks . map ( ( link , index ) = > {
const imgTag = `  } "texte de l'infobulle") ` ;
return (
< li key = { index } >
< code
onClick = { ( ) = > handleCopyToClipboard ( imgTag ) } >
{ imgTag }
< / code >
< / li >
) ;
} ) }
2024-03-29 20:08:34 -04:00
< / ul >
< / div >
< / div >
< GiftCheatSheet / >
< / div >
< div className = 'preview' >
< div className = "preview-column" >
2024-09-17 18:56:13 -04:00
< h4 > Prévisualisation < / h4 >
2024-03-29 20:08:34 -04:00
< div >
< GIFTTemplatePreview questions = { filteredValue } / >
< / div >
< / div >
< / div >
< / div >
2025-01-14 17:37:29 -05:00
{ showScrollButton && (
< Button
onClick = { scrollToTop }
variant = "contained"
color = "primary"
style = { scrollToTopButtonStyle }
title = "Scroll to top"
>
↑
< / Button >
) }
2024-03-29 20:08:34 -04:00
< / div >
) ;
} ;
2025-01-14 17:37:29 -05:00
const scrollToTopButtonStyle : CSSProperties = {
position : 'fixed' ,
bottom : '40px' ,
right : '50px' ,
padding : '10px' ,
fontSize : '16px' ,
color : 'white' ,
backgroundColor : '#5271ff' ,
border : 'none' ,
cursor : 'pointer' ,
zIndex : 1000 ,
} ;
2024-03-29 20:08:34 -04:00
export default QuizForm ;