mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
question focus from editor to preview
This commit is contained in:
parent
c18b1a8759
commit
6e91ab311d
3 changed files with 202 additions and 108 deletions
|
|
@ -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)}
|
||||||
|
|
@ -60,8 +77,6 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange }) => {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
style={{ overflow: 'auto' }}
|
style={{ overflow: 'auto' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'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 = `} "texte de l'infobulle")`;
|
const imgTag = `} "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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue