ajout merge

This commit is contained in:
Eddi3_As 2025-04-10 16:56:18 -04:00
parent 10169b93f1
commit e0897ff536
4 changed files with 145 additions and 49 deletions

View file

@ -149,10 +149,10 @@ const AdminTable: React.FC<AdminTableProps> = ({
/> />
<Dialog open={openDialog} onClose={handleCloseDialog}> <Dialog open={openDialog} onClose={handleCloseDialog}>
<DialogTitle>Confirm Deletion</DialogTitle> <DialogTitle>Confirmation</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
Are you sure you want to delete this record? Voulez-vous vraiment supprimer?
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View file

@ -13,7 +13,8 @@ import {
DialogContentText, DialogContentText,
Tabs, Tabs,
Tab, Tab,
TextField TextField, Snackbar,
Alert
} from "@mui/material"; } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import ContentCopyIcon from "@mui/icons-material/ContentCopy";
@ -21,12 +22,15 @@ import CloseIcon from "@mui/icons-material/Close";
import { ImageType } from "../../Types/ImageType"; import { ImageType } from "../../Types/ImageType";
import ApiService from "../../services/ApiService"; import ApiService from "../../services/ApiService";
import { Upload } from "@mui/icons-material"; import { Upload } from "@mui/icons-material";
import { ENV_VARIABLES } from '../../constants';
import { escapeForGIFT } from "src/utils/giftUtils";
interface ImagesProps { interface ImagesProps {
handleCopy?: (id: string) => void; handleCopy?: (id: string) => void;
handleDelete?: (id: string) => void;
} }
const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => { const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
const [images, setImages] = useState<ImageType[]>([]); const [images, setImages] = useState<ImageType[]>([]);
const [totalImg, setTotalImg] = useState(0); const [totalImg, setTotalImg] = useState(0);
const [imgPage, setImgPage] = useState(1); const [imgPage, setImgPage] = useState(1);
@ -38,8 +42,10 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [importedImage, setImportedImage] = useState<File | null>(null); const [importedImage, setImportedImage] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null); const [preview, setPreview] = useState<string | null>(null);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success");
useEffect(() => {
const fetchImages = async () => { const fetchImages = async () => {
setLoading(true); setLoading(true);
const data = await ApiService.getImages(imgPage, imgLimit); const data = await ApiService.getImages(imgPage, imgLimit);
@ -47,30 +53,49 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
setTotalImg(data.total); setTotalImg(data.total);
setLoading(false); setLoading(false);
}; };
useEffect(() => {
fetchImages(); fetchImages();
}, [imgPage]); }, [imgPage]);
const handleDelete = async () => { const defaultHandleDelete = async (id: string) => {
if (imageToDelete) { if (imageToDelete) {
setLoading(true); setLoading(true);
const isDeleted = await ApiService.deleteImage(imageToDelete.id); const isDeleted = await ApiService.deleteImage(id);
setLoading(false); setLoading(false);
if (isDeleted) { if (isDeleted) {
setImages(images.filter((image) => image.id !== imageToDelete.id)); setImgPage(1);
fetchImages();
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); setSelectedImage(null);
setImageToDelete(null); setImageToDelete(null);
setOpenDeleteDialog(false); setOpenDeleteDialog(false);
} }
}
}; };
const defaultHandleCopy = (id: string) => { const defaultHandleCopy = (id: string) => {
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(id); const link = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`;
const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`;
setSnackbarMessage("Le lien Markdown de limage a été copié dans le presse-papiers");
setSnackbarSeverity("success");
setSnackbarOpen(true);
navigator.clipboard.writeText(imgTag);
}
if(handleCopy) {
handleCopy(id);
} }
}; };
const handleCopyFunction = handleCopy || defaultHandleCopy; const handleDeleteFunction = handleDelete || defaultHandleDelete;
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files ? event.target.files[0] : null; const file = event.target.files ? event.target.files[0] : null;
@ -81,6 +106,44 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
} }
}; };
const handleSaveImage = async () => {
try {
if (!importedImage) {
setSnackbarMessage("Veuillez 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);
setImportedImage(null);
setPreview(null);
setTabValue(0);
} catch (error) {
setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`);
setSnackbarSeverity("error");
setSnackbarOpen(true);
}
};
const handleCloseSnackbar = () => {
setSnackbarOpen(false);
};
return ( return (
<Box p={3}> <Box p={3}>
<Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}> <Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}>
@ -97,7 +160,7 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
<> <>
<Box display="grid" gridTemplateColumns="repeat(3, 1fr)" gridTemplateRows="repeat(2, 1fr)" gap={2} maxWidth="900px" margin="auto"> <Box display="grid" gridTemplateColumns="repeat(3, 1fr)" gridTemplateRows="repeat(2, 1fr)" gap={2} maxWidth="900px" margin="auto">
{images.map((obj) => ( {images.map((obj) => (
<Card key={obj.id} sx={{ cursor: "pointer" }} onClick={() => setSelectedImage(obj)}> <Card key={obj.id} sx={{ cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center" }} onClick={() => setSelectedImage(obj)}>
<CardContent sx={{ p: 0 }}> <CardContent sx={{ p: 0 }}>
<img <img
src={`data:${obj.mime_type};base64,${obj.file_content}`} src={`data:${obj.mime_type};base64,${obj.file_content}`}
@ -105,6 +168,29 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
style={{ width: "100%", height: 250, objectFit: "cover", borderRadius: 8 }} style={{ width: "100%", height: 250, objectFit: "cover", borderRadius: 8 }}
/> />
</CardContent> </CardContent>
<Box display="flex" justifyContent="center" mt={1}>
<IconButton onClick={(e) => {
e.stopPropagation();
defaultHandleCopy(obj.id);
}}
color="primary"
data-testid={`gallery-tab-copy-${obj.id}`} >
<ContentCopyIcon sx={{ fontSize: 18 }} />
</IconButton>
<IconButton
onClick={(e) => {
e.stopPropagation();
setImageToDelete(obj);
setOpenDeleteDialog(true);
}}
color="error"
data-testid={`gallery-tab-delete-${obj.id}`} >
<DeleteIcon sx={{ fontSize: 18 }} />
</IconButton>
</Box>
</Card> </Card>
))} ))}
</Box> </Box>
@ -146,16 +232,16 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
maxHeight: "600px", maxHeight: "600px",
}} }}
/> />
</Box> </Box>
)} )}
<Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}> <Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}>
<TextField <TextField
type="file" type="file"
data-testid="file-input"
onChange={handleImageUpload} onChange={handleImageUpload}
slotProps={{ slotProps={{
htmlInput: { htmlInput: {
"data-testid": "file-input",
accept: "image/*", accept: "image/*",
}, },
}} }}
@ -164,7 +250,7 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
<Button <Button
variant="outlined" variant="outlined"
aria-label="Téléverser" aria-label="Téléverser"
onClick={() => { console.log("save..."); }} onClick={() => { handleSaveImage() }}
sx={{ ml: 2, height: "100%" }} sx={{ ml: 2, height: "100%" }}
> >
Téléverser <Upload sx={{ ml: 1 }} /> Téléverser <Upload sx={{ ml: 1 }} />
@ -173,7 +259,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
</Box> </Box>
)} )}
<Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md"> <Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md">
<IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> <IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}
data-testid="close-button">
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<DialogContent> <DialogContent>
@ -185,39 +272,36 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
/> />
)} )}
</DialogContent> </DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
{selectedImage && (
<IconButton onClick={() => handleCopyFunction(selectedImage.id)} color="primary">
<ContentCopyIcon />
</IconButton>
)}
<IconButton
onClick={() => {
setImageToDelete(selectedImage);
setOpenDeleteDialog(true);
}}
color="error"
>
<DeleteIcon />
</IconButton>
</DialogActions>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)}> <Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)}>
<DialogTitle>Confirm Deletion</DialogTitle> <DialogTitle>Supprimer</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>Are you sure you want to delete this image?</DialogContentText> <DialogContentText>Voulez-vous supprimer cette image?</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setOpenDeleteDialog(false)} color="primary"> <Button onClick={() => setOpenDeleteDialog(false)} color="primary">
Cancel Annuler
</Button> </Button>
<Button onClick={handleDelete} color="error"> <Button onClick={() => imageToDelete && handleDeleteFunction(imageToDelete.id)} color="error">
Delete Delete
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Snackbar
open={snackbarOpen}
autoHideDuration={4000}
onClose={handleCloseSnackbar}
>
<Alert
onClose={handleCloseSnackbar}
severity={snackbarSeverity}
sx={{ width: "100%" }}>
{snackbarMessage}
</Alert>
</Snackbar>
</Box> </Box>
); );
}; };

View file

@ -7,8 +7,15 @@ import {
} from "@mui/material"; } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import ImageGallery from "../ImageGallery"; import ImageGallery from "../ImageGallery";
import { ImageSearch } from "@mui/icons-material";
const ImageGalleryModal: React.FC = () => {
interface ImageGalleryModalProps {
handleCopy?: (id: string) => void;
}
const ImageGalleryModal: React.FC<ImageGalleryModalProps> = ({ handleCopy }) => {
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const handleOpen = () => setOpen(true); const handleOpen = () => setOpen(true);
@ -16,14 +23,18 @@ const ImageGalleryModal: React.FC = () => {
return ( return (
<> <>
<Button variant="contained" color="primary" onClick={handleOpen}> <Button
Open Image Manager variant="outlined"
aria-label='images-open'
onClick={() => handleOpen()}>
Images <ImageSearch />
</Button> </Button>
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth> <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogContent sx={{ display: "flex", flexDirection: "column", alignItems: "center", py: 3 }}> <DialogContent sx={{ display: "flex", flexDirection: "column", alignItems: "center", py: 3 }}>
<IconButton <IconButton
onClick={handleClose} onClick={handleClose}
color="primary" color="primary"
aria-label="close"
sx={{ sx={{
position: "absolute", position: "absolute",
right: 8, right: 8,
@ -34,7 +45,7 @@ const ImageGalleryModal: React.FC = () => {
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<ImageGallery /> <ImageGallery handleCopy={handleCopy}/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </>

View file

@ -2,6 +2,7 @@
const ENV_VARIABLES = { const ENV_VARIABLES = {
MODE: process.env.MODE || "production", MODE: process.env.MODE || "production",
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "",
IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.VITE_IMG_URL,
BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '',
FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : ''
}; };