From c50cd3e6e7a624df277f1e0c160f64f425d0c8b0 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 20 Mar 2025 21:11:46 -0400 Subject: [PATCH] =?UTF-8?q?FIX=20-=20Changement=20de=20table=20en=20galler?= =?UTF-8?q?y=20-=20Ajout=20option=20de=20r=C3=A9solution=20-=20Ajout=20war?= =?UTF-8?q?ning=20-=20Int=C3=A9gration=20de=20upload=20-=20Ajout=20de=20sn?= =?UTF-8?q?ackbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Types/ImageType.tsx | 17 + .../components/ImageGallery/ImageGallery.tsx | 359 ++++++++++++------ .../ImageGalleryModal/ImageGalleryModal.tsx | 54 +++ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 100 +---- 4 files changed, 320 insertions(+), 210 deletions(-) create mode 100644 client/src/Types/ImageType.tsx create mode 100644 client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx diff --git a/client/src/Types/ImageType.tsx b/client/src/Types/ImageType.tsx new file mode 100644 index 0000000..d0a1541 --- /dev/null +++ b/client/src/Types/ImageType.tsx @@ -0,0 +1,17 @@ +export interface ImageType { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: ImageType[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 85d80ed..dfb63d8 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -1,169 +1,280 @@ import React, { useState, useEffect } from "react"; import { + Box, + CircularProgress, + Button, + IconButton, + Card, + CardContent, Dialog, - DialogTitle, DialogContent, DialogActions, - Button, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - IconButton, - Paper, - Box, - CircularProgress + DialogTitle, + DialogContentText, + Tabs, + Tab, + TextField, Snackbar, + Alert } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { Images } from "../../Types/Images"; -import ApiService from '../../services/ApiService'; -import { ENV_VARIABLES } from '../../constants'; +import { ImageType } from "../../Types/ImageType"; +import ApiService from "../../services/ApiService"; +import { Upload } from "@mui/icons-material"; -type Props = { - galleryOpen: boolean; - setDialogOpen: React.Dispatch>; - setImageLinks: React.Dispatch>; -}; +interface ImagesProps { + handleCopy?: (id: string) => void; +} -const ImageDialog: React.FC = ({ galleryOpen, setDialogOpen, setImageLinks }) => { - const [copiedId, setCopiedId] = useState(null); - const [images, setImages] = useState([]); +const ImageGallery: React.FC = ({ handleCopy }) => { + const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); - const [imgLimit] = useState(3); + const [imgLimit] = useState(6); const [loading, setLoading] = useState(false); - const [deleteConfirm, setDeleteConfirm] = useState<{ id: string | null; linked: boolean }>({ id: null, linked: false }); + const [selectedImage, setSelectedImage] = useState(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [imageToDelete, setImageToDelete] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [importedImage, setImportedImage] = useState(null); + const [preview, setPreview] = useState(null); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success"); - const fetchImages = async (page: number, limit: number) => { - const data = await ApiService.getImages(page, limit); + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getImages(imgPage, imgLimit); setImages(data.images); setTotalImg(data.total); + setLoading(false); }; useEffect(() => { - fetchImages(imgPage, imgLimit); + fetchImages(); }, [imgPage]); - const onCopy = (id: string) => { - const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - setCopiedId(id); - setImageLinks(prevLinks => [...prevLinks, escLink]); - }; - - const handleDelete = async (id: string) => { - setLoading(true); - const isDeleted = await ApiService.deleteImage(id); - setLoading(false); - if (!isDeleted) { - setDeleteConfirm({ id, linked: true }); - } else { - setImages(images.filter(image => image.id !== id)); - setDeleteConfirm({ id: null, linked: false }); - } - }; - - const confirmDelete = async () => { - if (deleteConfirm.id) { + const handleDelete = async () => { + if (imageToDelete) { setLoading(true); - await ApiService.deleteImage(deleteConfirm.id); - setImages(images.filter(image => image.id !== deleteConfirm.id)); - setDeleteConfirm({ id: null, linked: false }); + const isDeleted = await ApiService.deleteImage(imageToDelete.id); setLoading(false); + + if (isDeleted) { + setImages(images.filter((image) => image.id !== imageToDelete.id)); + setSnackbarMessage("Image supprimée avec succès !"); + setSnackbarSeverity("success"); + } else { + setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer."); + setSnackbarSeverity("error"); + } + + setSnackbarOpen(true); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); } }; - const handleNextPage = () => { - if ((imgPage * imgLimit) < totalImg) { - setImgPage(prev => prev + 1); + const defaultHandleCopy = (id: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(id); } }; - const handlePrevPage = () => { - if (imgPage > 1) { - setImgPage(prev => prev - 1); + const handleCopyFunction = handleCopy || defaultHandleCopy; + + const handleImageUpload = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setImportedImage(file); + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }; + + const handleSaveImage = async () => { + try { + if (!importedImage) { + setSnackbarMessage("Veuillez d'abord choisir une image à téléverser."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + + const imageUrl = await ApiService.uploadImage(importedImage); + + if (imageUrl.includes("ERROR")) { + setSnackbarMessage("Une erreur est survenue. Veuillez réessayer plus tard."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + fetchImages(); + + setSnackbarMessage("Téléversée avec succès !"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + + // Reset the input field and preview after successful upload + setImportedImage(null); + setPreview(null); + } catch (error) { + setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); + setSnackbarSeverity("error"); + setSnackbarOpen(true); } }; return ( - setDialogOpen(false)} maxWidth="xl"> - - Images disponibles - setDialogOpen(false)} - style={{ position: "absolute", right: 8, top: 8 }} - > - - - - - {loading ? ( - - - - ) : ( - - - - {images.map((obj: Images) => ( - - + + setTabValue(newValue)}> + + + + {tabValue === 0 && ( + <> + {loading ? ( + + + + ) : ( + <> + + {images.map((obj) => ( + setSelectedImage(obj)}> + {`Image - - {obj.file_name} - - {obj.id} - onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> - + + + { + e.stopPropagation(); + handleCopyFunction(obj.id); + }} + color="primary" > + + - handleDelete(obj.id)} size="small" color="primary" data-testid={`delete-button-${obj.id}`}> - + + { + e.stopPropagation(); + setImageToDelete(obj); + setOpenDeleteDialog(true); + }} + color="error" > + + - {copiedId === obj.id && Copié!} - - + + ))} - -
-
- )} -
- {deleteConfirm.linked && ( - setDeleteConfirm({ id: null, linked: false })}> - Confirmer la suppression - - Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ? - - - - - - + + + + + + + )} + )} - - - - + {tabValue === 1 && ( + + {/* Image Preview at the top */} + {preview && ( + + Preview + + )} + + + + - -
+ )} + setSelectedImage(null)} maxWidth="md"> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> + + + + {selectedImage && ( + Enlarged view + )} + + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)}> + Supprimer + + Voulez-vous supprimer cette image? + + + + + + + + setSnackbarOpen(false)}> + setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: "100%" }}> + {snackbarMessage} + + + ); }; -export default ImageDialog; +export default ImageGallery; diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx new file mode 100644 index 0000000..7ea2404 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { + Button, + IconButton, + Dialog, + DialogContent, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import ImageGallery from "../ImageGallery"; +import { ImageSearch } from "@mui/icons-material"; + + +interface ImageGalleryModalProps { + handleCopy?: (id: string) => void; +} + + +const ImageGalleryModal: React.FC = ({ handleCopy }) => { + const [open, setOpen] = useState(false); + + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default ImageGalleryModal; diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 6ce512d..d3989a3 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -1,5 +1,5 @@ // EditorQuiz.tsx -import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import React, { useState, useEffect, CSSProperties } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { FolderType } from '../../../Types/FolderType'; @@ -11,13 +11,13 @@ 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 } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; -import ImageGallery from 'src/components/ImageGallery/ImageGallery'; +import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; -import { Upload, ImageSearch } from '@mui/icons-material'; +import { ENV_VARIABLES } from '../../../constants'; interface EditQuizParams { id: string; @@ -39,9 +39,6 @@ const QuizForm: React.FC = () => { const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); }; - const fileInputRef = useRef(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [galleryOpen, setGalleryOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); const scrollToTop = () => { @@ -168,44 +165,16 @@ const QuizForm: React.FC = () => { return
Chargement...
; } - 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 une image à téléverser.") - 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]); - - // Reset the file input element - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } catch (error) { - window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) - - } - }; - const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } + const handleCopyImage = (id: string) => { + const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + navigator.clipboard.writeText(id); + setImageLinks(prevLinks => [...prevLinks, escLink]); + } + return (
@@ -260,51 +229,10 @@ const QuizForm: React.FC = () => { onEditorChange={handleUpdatePreview} />
-
- - setDialogOpen(false)} > - Erreur - - Veuillez d'abord choisir une image à téléverser. - - - - - +
+

Mes images :

+
- -

Mes images :

- - - - -
@@ -319,7 +247,7 @@ const QuizForm: React.FC = () => {
    {imageLinks.map((link, index) => { - const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; return (