question focus from editor to preview

This commit is contained in:
Philippe 2025-03-26 14:52:09 -04:00
parent c18b1a8759
commit 6e91ab311d
3 changed files with 202 additions and 108 deletions

View file

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import { TextField, Typography, IconButton, Box } from '@mui/material'; 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 { interface EditorProps {
label: string; label: string;
values: string[]; values: string[];
onValuesChange: (values: string[]) => void; onValuesChange: (values: string[]) => void;
onFocusQuestion?: (index: number) => void;
} }
const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange }) => { const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQuestion }) => {
const handleChange = (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
const newValues = [...values]; const newValues = [...values];
newValues[index] = event.target.value; newValues[index] = event.target.value;
@ -20,36 +22,51 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange }) => {
onValuesChange(newValues); onValuesChange(newValues);
}; };
const handleFocusQuestion = (index: number) => () => {
if (onFocusQuestion) {
onFocusQuestion(index); // Call the focus function if provided
}
}
return ( return (
<div> <div>
{/* Label with increased margin */}
<Typography variant="h6" fontWeight="bold" style={{ marginBottom: '24px' }}> <Typography variant="h6" fontWeight="bold" style={{ marginBottom: '24px' }}>
{label} {label}
</Typography> </Typography>
{/* Map through each question */}
{values.map((value, index) => ( {values.map((value, index) => (
<Box key={index} style={{ marginBottom: '24px' }}> <Box key={index} style={{ marginBottom: '24px' }}>
{/* Bold "Question #" title */}
<Box display="flex" alignItems="center" justifyContent="space-between"> <Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="subtitle1" fontWeight="bold" style={{ marginBottom: '8px' }}> <Typography variant="subtitle1" fontWeight="bold" style={{ marginBottom: '8px' }}>
Question {index + 1} Question {index + 1}
</Typography> </Typography>
<Box>
{/* Delete button */} {/* Focus (Eye) Button */}
<IconButton
onClick={handleFocusQuestion(index)}
aria-label="focus question"
sx={{
color: 'gray',
'&:hover': { color: 'blue' },
marginRight: '8px', // Space between eye and delete
}}
>
<VisibilityIcon />
</IconButton>
{/* Delete Button */}
<IconButton <IconButton
onClick={handleDeleteQuestion(index)} onClick={handleDeleteQuestion(index)}
aria-label="delete" aria-label="delete"
sx={{ color: 'light-gray', sx={{
'&:hover': { color: 'light-gray',
color: 'red' '&:hover': { color: 'red' },
},
}} }}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</Box> </Box>
{/* TextField for the question */} </Box>
<TextField <TextField
value={value} value={value}
onChange={handleChange(index)} onChange={handleChange(index)}
@ -58,10 +75,8 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange }) => {
minRows={4} minRows={4}
maxRows={Infinity} maxRows={Infinity}
variant="outlined" variant="outlined"
style={{ overflow: 'auto'}} style={{ overflow: 'auto' }}
/> />
</Box> </Box>
))} ))}
</div> </div>

View file

@ -12,22 +12,23 @@ interface GIFTTemplatePreviewProps {
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
questions, questions,
hideAnswers = false hideAnswers = false,
}) => { }) => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isPreviewReady, setIsPreviewReady] = useState(false); const [isPreviewReady, setIsPreviewReady] = useState(false);
const [items, setItems] = useState(''); const [questionItems, setQuestionItems] = useState<string[]>([]); // Array of HTML strings for each question
useEffect(() => { useEffect(() => {
try { try {
let previewHTML = ''; const previewItems: string[] = [];
questions.forEach((giftQuestion) => { questions.forEach((giftQuestion, index) => {
try { try {
const question = parse(giftQuestion); const question = parse(giftQuestion);
previewHTML += Template(question[0], { const html = Template(question[0], {
preview: true, preview: true,
theme: 'light' theme: 'light',
}); });
previewItems.push(html);
} catch (error) { } catch (error) {
let errorMsg: string; let errorMsg: string;
if (error instanceof UnsupportedQuestionTypeError) { if (error instanceof UnsupportedQuestionTypeError) {
@ -37,18 +38,21 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
} else { } else {
errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue'); errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
} }
previewHTML += `<div label="error-message">${errorMsg}</div>`; previewItems.push(`<div label="error-message">${errorMsg}</div>`);
} }
}); });
if (hideAnswers) { if (hideAnswers) {
previewItems.forEach((item, index) => {
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi; const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
previewHTML = previewHTML.replace(svgRegex, '');
const placeholderRegex = /(placeholder=")[^"]*(")/gi; const placeholderRegex = /(placeholder=")[^"]*(")/gi;
previewHTML = previewHTML.replace(placeholderRegex, '$1$2'); previewItems[index] = item
.replace(svgRegex, '')
.replace(placeholderRegex, '$1$2');
});
} }
setItems(previewHTML); setQuestionItems(previewItems);
setIsPreviewReady(true); setIsPreviewReady(true);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -57,7 +61,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
setError('Une erreur est survenue durant le chargement de la prévisualisation.'); setError('Une erreur est survenue durant le chargement de la prévisualisation.');
} }
} }
}, [questions]); }, [questions, hideAnswers]);
const PreviewComponent = () => ( const PreviewComponent = () => (
<React.Fragment> <React.Fragment>
@ -65,8 +69,13 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div> <div className="error">{error}</div>
) : isPreviewReady ? ( ) : isPreviewReady ? (
<div data-testid="preview-container"> <div data-testid="preview-container">
{questionItems.map((item, index) => (
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div> <div
key={index}
className="question-item"
dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: item }) }}
/>
))}
</div> </div>
) : ( ) : (
<div className="loading">Chargement de la prévisualisation...</div> <div className="loading">Chargement de la prévisualisation...</div>

View file

@ -11,7 +11,7 @@ import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import './editorQuiz.css'; 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 ReturnButton from 'src/components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
@ -42,6 +42,12 @@ const QuizForm: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [showScrollButton, setShowScrollButton] = 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 = () => { const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' }); 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) { } catch (error) {
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
console.log(error) console.log(error)
@ -161,6 +168,10 @@ const QuizForm: React.FC = () => {
return <div>Chargement...</div>; return <div>Chargement...</div>;
} }
const handleSnackbarClose = () => {
setSnackbarOpen(false);
};
const handleSaveImage = async () => { const handleSaveImage = async () => {
try { try {
const inputElement = document.getElementById('file-input') as HTMLInputElement; const inputElement = document.getElementById('file-input') as HTMLInputElement;
@ -199,7 +210,15 @@ const QuizForm: React.FC = () => {
navigator.clipboard.writeText(link); 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 ( return (
<div className='quizEditor'> <div className='quizEditor'>
@ -252,33 +271,45 @@ const QuizForm: React.FC = () => {
<Editor <Editor
label="Contenu GIFT de chaque question:" label="Contenu GIFT de chaque question:"
values={values} values={values}
onValuesChange={handleUpdatePreview} /> onValuesChange={handleUpdatePreview}
onFocusQuestion={handleFocusQuestion} />
<Button variant="contained" onClick={handleAddQuestion}> <Button variant="contained" onClick={handleAddQuestion}>
Ajouter une question Ajouter une question
</Button> </Button>
<div className='images'> <div className="images">
<div className='upload'> {/* Collapsible Upload Section */}
<label className="dropArea"> <div style={{ marginTop: '8px' }}>
<input type="file" id="file-input" className="file-input"
accept="image/jpeg, image/png"
multiple
ref={fileInputRef} />
<Button <Button
variant="outlined" variant="outlined"
aria-label='Téléverser' onClick={() => setIsUploadCollapsed(!isUploadCollapsed)}
onClick={handleSaveImage}> style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}
>
{isUploadCollapsed ? 'Afficher Téléverser image' : 'Masquer Téléverser image'}
</Button>
{!isUploadCollapsed && (
<div className="upload">
<label className="dropArea">
<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 /> Téléverser <Upload />
</Button> </Button>
</label> </label>
<Dialog <Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
open={dialogOpen}
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle> <DialogTitle>Erreur</DialogTitle>
<DialogContent> <DialogContent>
Veuillez d&apos;abord choisir une image à téléverser. Veuillez d'abord choisir une image à téléverser.
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary"> <Button onClick={() => setDialogOpen(false)} color="primary">
@ -287,16 +318,37 @@ const QuizForm: React.FC = () => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</div> </div>
)}
</div>
{/* Collapsible Images Section */}
<div style={{ marginTop: '2px' }}>
<Button
variant="outlined"
onClick={() => setIsImagesCollapsed(!isImagesCollapsed)}
style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}
>
{isImagesCollapsed ? 'Afficher Mes images' : 'Masquer Mes images'}
</Button>
{!isImagesCollapsed && (
<div>
<h4>Mes images :</h4> <h4>Mes images :</h4>
<div> <div>
<div> <div>
<div style={{ display: "inline" }}>(Voir section </div> <div style={{ display: 'inline' }}>(Voir section </div>
<a href="#images-section"style={{ textDecoration: "none" }} onClick={scrollToImagesSection}> <a
<u><em><h4 style={{ display: "inline" }}> 9. Images </h4></em></u> href="#images-section"
style={{ textDecoration: 'none' }}
onClick={scrollToImagesSection}
>
<u>
<em>
<h4 style={{ display: 'inline' }}> 9. Images </h4>
</em>
</u>
</a> </a>
<div style={{ display: "inline" }}> ci-dessous</div> <div style={{ display: 'inline' }}> ci-dessous</div>
<div style={{ display: "inline" }}>)</div> <div style={{ display: 'inline' }}>)</div>
<br /> <br />
<em> - Cliquez sur un lien pour le copier</em> <em> - Cliquez sur un lien pour le copier</em>
</div> </div>
@ -305,8 +357,7 @@ const QuizForm: React.FC = () => {
const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`;
return ( return (
<li key={index}> <li key={index}>
<code <code onClick={() => handleCopyToClipboard(imgTag)}>
onClick={() => handleCopyToClipboard(imgTag)}>
{imgTag} {imgTag}
</code> </code>
</li> </li>
@ -315,12 +366,24 @@ const QuizForm: React.FC = () => {
</ul> </ul>
</div> </div>
</div> </div>
)}
<GiftCheatSheet />
</div> </div>
<div className='preview'> {/* Collapsible CheatSheet Section */}
<div style={{ marginTop: '2px' }}>
<Button
variant="outlined"
onClick={() => setIsCheatSheetCollapsed(!isCheatSheetCollapsed)}
style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}
>
{isCheatSheetCollapsed ? 'Afficher CheatSheet' : 'Masquer CheatSheet'}
</Button>
{!isCheatSheetCollapsed && <GiftCheatSheet />}
</div>
</div>
</div>
<div className="preview">
<div className="preview-column"> <div className="preview-column">
<h4>Prévisualisation</h4> <h4>Prévisualisation</h4>
<div> <div>
@ -328,7 +391,6 @@ const QuizForm: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{showScrollButton && ( {showScrollButton && (
@ -342,6 +404,14 @@ const QuizForm: React.FC = () => {
</Button> </Button>
)} )}
<Snackbar
open={snackbarOpen}
autoHideDuration={3000} // Hide after 3 seconds
onClose={handleSnackbarClose}
message={snackbarMessage}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} // Lower-right corner
/>
</div> </div>
); );
}; };