From aa74227981103fb87044d1c236d891a16aee5afd Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:43:11 -0400 Subject: [PATCH 1/4] editor question per question, delete and add question works --- client/src/components/Editor/Editor.tsx | 86 +++++++++++++------ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 41 ++++----- 2 files changed, 81 insertions(+), 46 deletions(-) diff --git a/client/src/components/Editor/Editor.tsx b/client/src/components/Editor/Editor.tsx index 540cb4a..47d9c70 100644 --- a/client/src/components/Editor/Editor.tsx +++ b/client/src/components/Editor/Editor.tsx @@ -1,37 +1,71 @@ -// Editor.tsx -import React, { useState, useRef } from 'react'; -import './editor.css'; -import { TextareaAutosize } from '@mui/material'; +import React from 'react'; +import { TextField, Typography, IconButton, Box } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; // Import delete icon interface EditorProps { label: string; - initialValue: string; - onEditorChange: (value: string) => void; + values: string[]; + onValuesChange: (values: string[]) => void; } -const Editor: React.FC = ({ initialValue, onEditorChange, label }) => { - const [value, setValue] = useState(initialValue); - const editorRef = useRef(null); +const Editor: React.FC = ({ label, values, onValuesChange }) => { + const handleChange = (index: number) => (event: React.ChangeEvent) => { + const newValues = [...values]; + newValues[index] = event.target.value; + onValuesChange(newValues); + }; - function handleEditorChange(event: React.ChangeEvent) { - const text = event.target.value; - setValue(text); - onEditorChange(text || ''); - } + const handleDeleteQuestion = (index: number) => () => { + const newValues = values.filter((_, i) => i !== index); // Remove the question at the specified index + onValuesChange(newValues); + }; return ( - +
+ {/* Label with increased margin */} + + {label} + + + {/* Map through each question */} + {values.map((value, index) => ( + + {/* Bold "Question #" title */} + + + Question {index + 1} + + + {/* Delete button */} + + + + + {/* TextField for the question */} + + + + + ))} +
); }; -export default Editor; +export default Editor; \ No newline at end of file diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 89f822a..65d6c1f 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -29,7 +29,7 @@ const QuizForm: React.FC = () => { const [filteredValue, setFilteredValue] = useState([]); const { id } = useParams(); - const [value, setValue] = useState(''); + const [values, setValues] = useState([]); const [isNewQuiz, setNewQuiz] = useState(false); const [quiz, setQuiz] = useState(null); const navigate = useNavigate(); @@ -101,7 +101,7 @@ const QuizForm: React.FC = () => { setQuizTitle(title); setSelectedFolder(folderId); setFilteredValue(content); - setValue(quiz.content.join('\n\n')); + setValues(content); } catch (error) { window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) @@ -113,21 +113,17 @@ const QuizForm: React.FC = () => { fetchData(); }, [id]); - function handleUpdatePreview(value: string) { - if (value !== '') { - setValue(value); - } + const handleAddQuestion = () => { + console.log("Adding question"); + console.log("Current values:", values); // Log current state + setValues([...values, '']); + console.log("Updated values:", [...values, '']); // Log new state + }; - // 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 handleUpdatePreview = (newValues: string[]) => { + setValues(newValues); + setFilteredValue(newValues.filter(value => value.trim() !== '')); + }; const handleQuizTitleChange = (event: React.ChangeEvent) => { setQuizTitle(event.target.value); @@ -204,6 +200,8 @@ const QuizForm: React.FC = () => { navigator.clipboard.writeText(link); } + + return (
@@ -213,7 +211,7 @@ const QuizForm: React.FC = () => { message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`} /> -
Éditeur de quiz
+
Éditeur de Quiz
@@ -253,9 +251,12 @@ const QuizForm: React.FC = () => {
+ label="Contenu GIFT de chaque question:" + values={values} + onValuesChange={handleUpdatePreview} /> +
From c18b1a87597adc302f83bd91ef7e31a8c704377b Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Sun, 16 Mar 2025 13:25:51 -0400 Subject: [PATCH 2/4] new test for editor components --- .../components/Editor/Editor.test.tsx | 90 +++++++------------ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 1 - 2 files changed, 30 insertions(+), 61 deletions(-) diff --git a/client/src/__tests__/components/Editor/Editor.test.tsx b/client/src/__tests__/components/Editor/Editor.test.tsx index 7548aac..878fedd 100644 --- a/client/src/__tests__/components/Editor/Editor.test.tsx +++ b/client/src/__tests__/components/Editor/Editor.test.tsx @@ -1,80 +1,50 @@ -// Editor.test.tsx import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import Editor from 'src/components/Editor/Editor'; +import Editor from '../../../components/Editor/Editor'; describe('Editor Component', () => { - const mockOnEditorChange = jest.fn(); + const mockOnValuesChange = jest.fn(); const sampleProps = { label: 'Sample Label', - initialValue: 'Sample Initial Value', - onEditorChange: mockOnEditorChange + values: ['Question 1', 'Question 2'], + onValuesChange: mockOnValuesChange }; beforeEach(() => { + mockOnValuesChange.mockClear(); + }); + + test('renders the label correctly', () => { render(); + const label = screen.getByText('Sample Label'); + expect(label).toBeInTheDocument(); }); - it('renders correctly with initial value', () => { - const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement; - expect(editorTextarea).toBeInTheDocument(); - expect(editorTextarea.value).toBe('Sample Initial Value'); + test('renders the correct number of questions', () => { + render(); + const questions = screen.getAllByRole('textbox'); + expect(questions.length).toBe(2); }); - it('calls onEditorChange callback when editor value changes', () => { - const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement; - fireEvent.change(editorTextarea, { target: { value: 'Updated Value' } }); - expect(mockOnEditorChange).toHaveBeenCalledWith('Updated Value'); + test('calls onValuesChange with updated values when a question is changed', () => { + render(); + const questionInput = screen.getAllByRole('textbox')[0]; + fireEvent.change(questionInput, { target: { value: 'Updated Question 1' } }); + expect(mockOnValuesChange).toHaveBeenCalledWith(['Updated Question 1', 'Question 2']); }); - it('updates editor value when initialValue prop changes', () => { - const updatedProps = { - label: 'Updated Label', - initialValue: 'Updated Initial Value', - onEditorChange: mockOnEditorChange - }; - - render(); - - const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[]; - const editorTextarea = editorTextareas[1]; - - expect(editorTextarea.value).toBe('Updated Initial Value'); + test('calls onValuesChange with updated values when a question is deleted', () => { + render(); + const deleteButton = screen.getAllByLabelText('delete')[0]; + fireEvent.click(deleteButton); + expect(mockOnValuesChange).toHaveBeenCalledWith(['Question 2']); }); - test('should call change text with the correct value on textarea change', () => { - const updatedProps = { - label: 'Updated Label', - initialValue: 'Updated Initial Value', - onEditorChange: mockOnEditorChange - }; - - render(); - - const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[]; - const editorTextarea = editorTextareas[1]; - fireEvent.change(editorTextarea, { target: { value: 'New value' } }); - - expect(editorTextarea.value).toBe('New value'); + test('renders delete buttons for each question', () => { + render(); + const deleteButtons = screen.getAllByLabelText('delete'); + expect(deleteButtons.length).toBe(2); }); - - test('should call onEditorChange with an empty string if textarea value is falsy', () => { - const updatedProps = { - label: 'Updated Label', - initialValue: 'Updated Initial Value', - onEditorChange: mockOnEditorChange - }; - - render(); - - const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[]; - const editorTextarea = editorTextareas[1]; - fireEvent.change(editorTextarea, { target: { value: '' } }); - - expect(editorTextarea.value).toBe(''); - }); - - -}); +}); \ No newline at end of file diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 65d6c1f..8387be3 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -131,7 +131,6 @@ const QuizForm: React.FC = () => { const handleQuizSave = async () => { try { - // check if everything is there if (quizTitle == '') { alert("Veuillez choisir un titre"); return; From 6e91ab311d2694332125eb9b679dbbef2314d3b1 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:52:09 -0400 Subject: [PATCH 3/4] question focus from editor to preview --- client/src/components/Editor/Editor.tsx | 67 +++--- .../GiftTemplate/GIFTTemplatePreview.tsx | 45 ++-- .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 198 ++++++++++++------ 3 files changed, 202 insertions(+), 108 deletions(-) diff --git a/client/src/components/Editor/Editor.tsx b/client/src/components/Editor/Editor.tsx index 47d9c70..17d5237 100644 --- a/client/src/components/Editor/Editor.tsx +++ b/client/src/components/Editor/Editor.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { TextField, Typography, IconButton, Box } from '@mui/material'; -import DeleteIcon from '@mui/icons-material/Delete'; // Import delete icon +import DeleteIcon from '@mui/icons-material/Delete'; +import VisibilityIcon from '@mui/icons-material/Visibility'; interface EditorProps { label: string; values: string[]; onValuesChange: (values: string[]) => void; + onFocusQuestion?: (index: number) => void; } -const Editor: React.FC = ({ label, values, onValuesChange }) => { +const Editor: React.FC = ({ label, values, onValuesChange, onFocusQuestion }) => { const handleChange = (index: number) => (event: React.ChangeEvent) => { const newValues = [...values]; newValues[index] = event.target.value; @@ -20,36 +22,51 @@ const Editor: React.FC = ({ label, values, onValuesChange }) => { onValuesChange(newValues); }; - return ( + const handleFocusQuestion = (index: number) => () => { + if (onFocusQuestion) { + onFocusQuestion(index); // Call the focus function if provided + } + } + + + return (
- {/* Label with increased margin */} {label} - {/* Map through each question */} {values.map((value, index) => ( - {/* Bold "Question #" title */} - - Question {index + 1} - - - {/* Delete button */} - - - + + Question {index + 1} + + + {/* Focus (Eye) Button */} + + + + {/* Delete Button */} + + + + - {/* TextField for the question */} = ({ label, values, onValuesChange }) => { minRows={4} maxRows={Infinity} variant="outlined" - style={{ overflow: 'auto'}} + style={{ overflow: 'auto' }} /> - - ))}
diff --git a/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx b/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx index 9da21c8..d834343 100644 --- a/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx +++ b/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx @@ -12,43 +12,47 @@ interface GIFTTemplatePreviewProps { const GIFTTemplatePreview: React.FC = ({ questions, - hideAnswers = false + hideAnswers = false, }) => { const [error, setError] = useState(''); const [isPreviewReady, setIsPreviewReady] = useState(false); - const [items, setItems] = useState(''); + const [questionItems, setQuestionItems] = useState([]); // Array of HTML strings for each question useEffect(() => { try { - let previewHTML = ''; - questions.forEach((giftQuestion) => { + const previewItems: string[] = []; + questions.forEach((giftQuestion, index) => { try { const question = parse(giftQuestion); - previewHTML += Template(question[0], { + const html = Template(question[0], { preview: true, - theme: 'light' + theme: 'light', }); + previewItems.push(html); } catch (error) { let errorMsg: string; if (error instanceof UnsupportedQuestionTypeError) { errorMsg = ErrorTemplate(giftQuestion, `Erreur: ${error.message}`); } else if (error instanceof Error) { - errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`); + errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`); } else { - errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue'); + errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue'); } - previewHTML += `
${errorMsg}
`; + previewItems.push(`
${errorMsg}
`); } }); if (hideAnswers) { - const svgRegex = /]*>([\s\S]*?)<\/svg>/gi; - previewHTML = previewHTML.replace(svgRegex, ''); - const placeholderRegex = /(placeholder=")[^"]*(")/gi; - previewHTML = previewHTML.replace(placeholderRegex, '$1$2'); + previewItems.forEach((item, index) => { + const svgRegex = /]*>([\s\S]*?)<\/svg>/gi; + const placeholderRegex = /(placeholder=")[^"]*(")/gi; + previewItems[index] = item + .replace(svgRegex, '') + .replace(placeholderRegex, '$1$2'); + }); } - setItems(previewHTML); + setQuestionItems(previewItems); setIsPreviewReady(true); } catch (error: unknown) { if (error instanceof Error) { @@ -57,7 +61,7 @@ const GIFTTemplatePreview: React.FC = ({ setError('Une erreur est survenue durant le chargement de la prévisualisation.'); } } - }, [questions]); + }, [questions, hideAnswers]); const PreviewComponent = () => ( @@ -65,8 +69,13 @@ const GIFTTemplatePreview: React.FC = ({
{error}
) : isPreviewReady ? (
- -
+ {questionItems.map((item, index) => ( +
+ ))}
) : (
Chargement de la prévisualisation...
@@ -77,4 +86,4 @@ const GIFTTemplatePreview: React.FC = ({ return ; }; -export default GIFTTemplatePreview; +export default GIFTTemplatePreview; \ No newline at end of file diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 8387be3..f92a8a0 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -11,7 +11,7 @@ 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 { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent, Snackbar } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; import ApiService from '../../../services/ApiService'; @@ -42,6 +42,12 @@ const QuizForm: React.FC = () => { const [dialogOpen, setDialogOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); + const [isImagesCollapsed, setIsImagesCollapsed] = useState(true); + const [isCheatSheetCollapsed, setIsCheatSheetCollapsed] = useState(true); + const [isUploadCollapsed, setIsUploadCollapsed] = useState(true); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -149,7 +155,8 @@ const QuizForm: React.FC = () => { } } - navigate('/teacher/dashboard'); + setSnackbarMessage('Quiz enregistré avec succès!'); + setSnackbarOpen(true); } catch (error) { window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) console.log(error) @@ -161,6 +168,10 @@ const QuizForm: React.FC = () => { return
Chargement...
; } + const handleSnackbarClose = () => { + setSnackbarOpen(false); + }; + const handleSaveImage = async () => { try { const inputElement = document.getElementById('file-input') as HTMLInputElement; @@ -199,7 +210,15 @@ const QuizForm: React.FC = () => { navigator.clipboard.writeText(link); } - + const handleFocusQuestion = (index: number) => { + const previewElement = document.querySelector('.preview-column'); + if (previewElement) { + const questionElements = previewElement.querySelectorAll('.question-item'); + if (questionElements[index]) { + questionElements[index].scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + }; return (
@@ -252,75 +271,119 @@ const QuizForm: React.FC = () => { + onValuesChange={handleUpdatePreview} + onFocusQuestion={handleFocusQuestion} /> - -
-
- - setDialogOpen(false)} > - Erreur - - Veuillez d'abord choisir une image à téléverser. - - - - - + onClick={() => setIsUploadCollapsed(!isUploadCollapsed)} + style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }} + > + {isUploadCollapsed ? 'Afficher Téléverser image' : 'Masquer Téléverser image'} + + {!isUploadCollapsed && ( +
+ + setDialogOpen(false)}> + Erreur + + Veuillez d'abord choisir une image à téléverser. + + + + + +
+ )}
-

Mes images :

-
+ {/* Collapsible Images Section */} +
+ + {!isImagesCollapsed && (
-
(Voir section
- -

9. Images

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

Mes images :

+
+
+
(Voir section
+ + + +

9. Images

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

Prévisualisation

@@ -328,7 +391,6 @@ const QuizForm: React.FC = () => {
-
{showScrollButton && ( @@ -342,9 +404,17 @@ const QuizForm: React.FC = () => { ↑ )} + +
); -}; +}; const scrollToTopButtonStyle: CSSProperties = { position: 'fixed', From 2695716e18dbd171eb9422085a76fa39e06fe91f Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:11:16 -0400 Subject: [PATCH 4/4] change behavior of return button --- .../components/ReturnButton/ReturnButton.tsx | 64 ++++++++++--------- .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 7 +- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/client/src/components/ReturnButton/ReturnButton.tsx b/client/src/components/ReturnButton/ReturnButton.tsx index a184356..8ec0bb1 100644 --- a/client/src/components/ReturnButton/ReturnButton.tsx +++ b/client/src/components/ReturnButton/ReturnButton.tsx @@ -1,65 +1,69 @@ -// GoBackButton.tsx -import React from 'react'; -import { useState } from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import ConfirmDialog from '../ConfirmDialog/ConfirmDialog'; import { Button } from '@mui/material'; import { ChevronLeft } from '@mui/icons-material'; +import ApiService from '../../services/ApiService'; // Assuming this is where save logic lives interface Props { onReturn?: () => void; - askConfirm?: boolean; - message?: string; + quizTitle?: string; // Quiz title to save + quizContent?: string[]; // Quiz content to save + quizFolder?: string; // Folder ID to save + quizId?: string; // Quiz ID for updates (optional) + isNewQuiz?: boolean; // Flag to determine create or update } const ReturnButton: React.FC = ({ - askConfirm = false, - message = 'Êtes-vous sûr de vouloir quitter la page ?', - onReturn + onReturn, + quizTitle = '', + quizContent = [], + quizFolder = '', + quizId, + isNewQuiz = false, }) => { const navigate = useNavigate(); - const [showDialog, setShowDialog] = useState(false); + const [isSaving, setIsSaving] = useState(false); // Optional: to show saving state - const handleOnReturnButtonClick = () => { - if (askConfirm) { - setShowDialog(true); - } else { + const handleOnReturnButtonClick = async () => { + setIsSaving(true); + try { + // Automatically save the quiz + if (isNewQuiz && quizTitle && quizFolder) { + await ApiService.createQuiz(quizTitle, quizContent, quizFolder); + } else if (quizId && quizTitle) { + await ApiService.updateQuiz(quizId, quizTitle, quizContent); + } + // If no title/folder, proceed without saving (optional behavior) handleOnReturn(); + } catch (error) { + console.error('Error saving quiz on return:', error); + // Still navigate even if save fails, to avoid trapping the user + handleOnReturn(); + } finally { + setIsSaving(false); } }; - const handleConfirm = () => { - setShowDialog(false); - handleOnReturn(); - }; - const handleOnReturn = () => { if (onReturn) { onReturn(); } else { - navigate(-1); + navigate('/teacher/dashboard'); // Navigate to dashboard instead of -1 } }; return ( -
+
- setShowDialog(false)} - buttonOrderType="warning" - />
); }; diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index f92a8a0..76de8a3 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -225,8 +225,11 @@ const QuizForm: React.FC = () => {
Éditeur de Quiz