diff --git a/src/API.ts b/src/API.ts index e8e2df1f30029fe1cea997483e6ef2cce44ba3c4..876787995bf1fe1292eb9bc59836c35d1bb617eb 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1,4 +1,4 @@ -import axios from 'axios' +import axios, { AxiosRequestConfig } from 'axios' import { useQuery } from 'react-query' import { toast } from 'react-toastify' import { User } from './models/user.model' @@ -24,3 +24,16 @@ export const useWhoAmI = () => { export const fetchLogout = async () => { return await axios.get('/Logout') } + +export const fetchEcogestureImages = async ( + axiosHeaders: AxiosRequestConfig +) => { + const { data: imageNames } = await axios.get<string[]>( + `/api/animator/imageNames`, + axiosHeaders + ) + if (imageNames && imageNames !== null) { + return imageNames.map(image => `/assets/ecogesture/${image}`) + } + return [] +} diff --git a/src/components/ImagePicker/ImagePicker.tsx b/src/components/ImagePicker/ImagePicker.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55baa2e3ac4eb557172f45fe64437fcf236dcb34 --- /dev/null +++ b/src/components/ImagePicker/ImagePicker.tsx @@ -0,0 +1,117 @@ +import { Button, Dialog, Pagination } from '@mui/material' +import React, { useState } from 'react' +import { useQuery } from 'react-query' +import { fetchEcogestureImages, useWhoAmI } from '../../API' +import { getAxiosXSRFHeader } from '../../axios.config' +import SingleImage from './SingleImage' + +const IMAGES_PER_PAGE = 15 + +interface ImagePickerProps { + imageURL: string + handleChange: (value: string, type: 'image') => void + defaultIcon?: string +} + +const ImagePicker: React.FC<ImagePickerProps> = ({ + imageURL, + handleChange, + defaultIcon, +}) => { + const { data: user } = useWhoAmI() + const { data: imagesNames } = useQuery({ + queryKey: ['imageNames'], + queryFn: () => + fetchEcogestureImages(getAxiosXSRFHeader(user?.xsrftoken ?? '')), + }) + + // Default icon must be the first of the list if it exists + if (defaultIcon && imagesNames) { + const indexOfDefault = imagesNames.indexOf(defaultIcon) + if (indexOfDefault > 0) { + imagesNames.slice(indexOfDefault, 1) + imagesNames.unshift(defaultIcon) + } + } + const pageCount = Math.ceil((imagesNames?.length ?? 1) / IMAGES_PER_PAGE) + const imageIndex = imagesNames?.indexOf(imageURL ?? '') ?? 1 + + const [selectedImageURL, setSelectedImageURL] = useState(imageURL ?? '') + const [openModal, setOpenModal] = useState(false) + const [currentPage, setCurrentPage] = useState( + Math.floor((imageIndex > 0 ? imageIndex : 1) / IMAGES_PER_PAGE + 1) + ) + const [preSelectImage, setPreSelectImage] = useState(imageURL ?? '') + + const toggleModal = () => { + setOpenModal(prev => !prev) + } + + const handleValidateImage = () => { + const newImage = + defaultIcon && preSelectImage === '' ? defaultIcon : preSelectImage + setPreSelectImage(newImage) + setSelectedImageURL(newImage) + handleChange(newImage, 'image') + setOpenModal(false) + } + + const startIndex = (currentPage - 1) * IMAGES_PER_PAGE + const stopIndex = startIndex + IMAGES_PER_PAGE + + return ( + <> + {selectedImageURL === '' || !selectedImageURL ? ( + <> + <p>Pas d'image sélectionnée</p> + <br /> + <Button onClick={toggleModal}>Choisir une image</Button> + </> + ) : ( + <> + <img + src={selectedImageURL} + width="120" + height="120" + className="ecogesture-image" + alt="selected" + /> + <Button onClick={toggleModal}>Modifier l'image</Button> + </> + )} + + <Dialog open={openModal} className="modal-large"> + <div className="image-picker"> + {imagesNames + ?.slice(startIndex, stopIndex) + ?.map(image => ( + <SingleImage + imageURL={image} + key={image} + selectedImage={preSelectImage} + setSelectedImageURL={setPreSelectImage} + /> + ))} + </div> + <Pagination + count={pageCount} + page={currentPage} + onChange={(_e, page) => setCurrentPage(page)} + style={{ + display: 'flex', + justifyContent: 'center', + marginTop: '1rem', + }} + /> + <div className="buttons"> + <Button variant="outlined" onClick={() => setOpenModal(false)}> + Annuler + </Button> + <Button onClick={handleValidateImage}>Valider</Button> + </div> + </Dialog> + </> + ) +} + +export default ImagePicker diff --git a/src/components/Newsletter/ImagePicker/SingleImage.tsx b/src/components/ImagePicker/SingleImage.tsx similarity index 86% rename from src/components/Newsletter/ImagePicker/SingleImage.tsx rename to src/components/ImagePicker/SingleImage.tsx index 9de3d93d39db87f17197b7887524086e27e352ec..1544bfb31d324f6ec71b99f993eaa33b336ab67d 100644 --- a/src/components/Newsletter/ImagePicker/SingleImage.tsx +++ b/src/components/ImagePicker/SingleImage.tsx @@ -12,7 +12,8 @@ const SingleImage: React.FC<SingleImageProps> = ({ setSelectedImageURL, }) => { const selectImage = (targetImage: string) => { - setSelectedImageURL(targetImage) + const newImageUrl = targetImage === selectedImage ? '' : targetImage + setSelectedImageURL(newImageUrl) } return ( <img diff --git a/src/components/Newsletter/ImagePicker/imagePicker.scss b/src/components/ImagePicker/imagePicker.scss similarity index 85% rename from src/components/Newsletter/ImagePicker/imagePicker.scss rename to src/components/ImagePicker/imagePicker.scss index 60c9acff8409e4889f0c08e7a31baddbdb26142a..278be86254f63bd4f86cb5aa91af031850874807 100644 --- a/src/components/Newsletter/ImagePicker/imagePicker.scss +++ b/src/components/ImagePicker/imagePicker.scss @@ -1,5 +1,5 @@ -@import '../../../styles/config/colors'; -@import '../../../styles/config/breakpoints'; +@import '../../styles/config/colors'; +@import '../../styles/config/breakpoints'; .image-picker { display: flex; diff --git a/src/components/Newsletter/ImagePicker/ImagePicker.tsx b/src/components/Newsletter/ImagePicker/ImagePicker.tsx deleted file mode 100644 index 98e236e37af1ae7dbc68875f9d254193d762e985..0000000000000000000000000000000000000000 --- a/src/components/Newsletter/ImagePicker/ImagePicker.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Button, Dialog, Pagination } from '@mui/material' -import React, { useEffect, useState } from 'react' -import { useWhoAmI } from '../../../API' -import { getAxiosXSRFHeader } from '../../../axios.config' -import { NewsletterService } from '../../../services/newsletter.service' -import { EditorType } from '../CustomEditor' -import SingleImage from './SingleImage' - -interface ImagePickerProps { - imageURL: string - handleChange: (value: string, type: EditorType) => void -} - -const ImagePicker: React.FC<ImagePickerProps> = ({ - imageURL, - handleChange, -}) => { - const { data: user } = useWhoAmI() - const [imageNames, setImageNames] = useState<string[][]>([]) - const [selectedImageURL, setSelectedImageURL] = useState<string>( - imageURL && imageURL !== null ? imageURL : '' - ) - const [openModal, setOpenModal] = useState<boolean>(false) - const [currentPage, setCurrentPage] = useState(1) - const [pageCount, setPageCount] = useState<number>(1) - const [preSelectImage, setPreSelectImage] = useState<string>('') - const imagePerPage = 10 - - const toggleModal = () => { - setOpenModal(prev => !prev) - } - const handleChangePage = (page: number) => { - setCurrentPage(page) - } - const handleValidateImage = () => { - setSelectedImageURL(preSelectImage) - handleChange(preSelectImage, 'image') - setOpenModal(false) - } - - useEffect(() => { - let subscribed = true - async function getImageNames() { - if (user) { - const newsletterService = new NewsletterService() - const images = await newsletterService.getEcogestureImages( - getAxiosXSRFHeader(user.xsrftoken) - ) - //Split array depending on page numbers - setPageCount(Math.ceil(images.length / imagePerPage)) - const arraySplitted = [] - while (images.length) { - arraySplitted.push(images.splice(0, imagePerPage)) - } - setImageNames(arraySplitted) - } - } - if (subscribed) { - getImageNames() - } - return () => { - subscribed = false - } - }, [user, imagePerPage]) - - return ( - <> - {selectedImageURL === '' || !selectedImageURL ? ( - <> - <p>Pas d'image sélectionnée</p> - <br /> - <Button onClick={toggleModal}>Choisir une image</Button> - </> - ) : ( - <> - <img - src={selectedImageURL} - width="120" - height="120" - className="ecogesture-image" - alt="selected" - /> - <Button onClick={toggleModal}>Modifier l'image</Button> - </> - )} - - <Dialog open={openModal} className="modal-large"> - <div className="image-picker"> - {imageNames && - imageNames.length !== 0 && - imageNames[currentPage - 1].length !== 0 && - imageNames[currentPage - 1].map(imageName => ( - <SingleImage - imageURL={imageName} - key={imageName} - selectedImage={preSelectImage} - setSelectedImageURL={setPreSelectImage} - /> - ))} - </div> - <Pagination - count={pageCount} - siblingCount={0} - onChange={(_e, page) => handleChangePage(page)} - style={{ - display: 'flex', - justifyContent: 'center', - marginTop: '1rem', - }} - /> - <div className="buttons"> - <Button variant="outlined" onClick={() => setOpenModal(false)}> - Annuler - </Button> - <Button onClick={handleValidateImage}>Valider</Button> - </div> - </Dialog> - </> - ) -} - -export default ImagePicker diff --git a/src/components/Newsletter/MonthlyInfo/MonthlyInfo.tsx b/src/components/Newsletter/MonthlyInfo/MonthlyInfo.tsx index 7b45e657f588ed13fb4cba92a766586433c4d484..384d443bc138d128960e00ac9f1799dba3621409 100644 --- a/src/components/Newsletter/MonthlyInfo/MonthlyInfo.tsx +++ b/src/components/Newsletter/MonthlyInfo/MonthlyInfo.tsx @@ -2,8 +2,8 @@ import { Button } from '@mui/material' import React from 'react' import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' import { convertStringToEditorState } from '../../../utils/editorStateManagement' +import ImagePicker from '../../ImagePicker/ImagePicker' import CustomEditor, { EditorType } from '../CustomEditor' -import ImagePicker from '../ImagePicker/ImagePicker' import { ContentItems } from '../Newsletter' interface MonthlyInfoProps { diff --git a/src/components/Popups/Popups.tsx b/src/components/Popups/Popups.tsx index 426ee34f716d57d021773ccddfbf807b255b633c..1276dc2fc9ced044ed8c7eff25cd24de720f7064 100644 --- a/src/components/Popups/Popups.tsx +++ b/src/components/Popups/Popups.tsx @@ -24,6 +24,8 @@ import { IPartnersInfo } from '../../models/partnersInfo.model' import { CustomPopupService } from '../../services/customPopup.service' import { PartnersInfoService } from '../../services/partnersInfo.service' import { convertStringToEditorState } from '../../utils/editorStateManagement' +import { getFilenameFromPath } from '../../utils/imagesUrlsGetter' +import ImagePicker from '../ImagePicker/ImagePicker' import Loader from '../Loader/Loader' import CustomEditor from '../Newsletter/CustomEditor' import './popups.scss' @@ -45,6 +47,7 @@ const OPTIONS: Option[] = [ const Popups: React.FC = () => { const { data: user } = useWhoAmI() + const defaultIcon = '/assets/ecogesture/bullhorn.png' const [refreshData, setRefreshData] = useState(false) const [isLoading, setIsLoading] = useState(false) @@ -62,6 +65,7 @@ const Popups: React.FC = () => { const [customPopup, setCustomPopup] = useState<ICustomPopup>({ popupEnabled: false, title: '', + image: '', description: '', endDate: DateTime.local().plus({ days: 365 }).toISO(), }) @@ -106,7 +110,10 @@ const Popups: React.FC = () => { } } - const handlePopupChange = (field: 'title' | 'description', value: string) => { + const handlePopupChange = ( + value: string, + field: 'title' | 'image' | 'description' + ) => { setCustomPopup(prev => ({ ...prev, [field]: value, @@ -148,9 +155,13 @@ const Popups: React.FC = () => { const isOutdated = isPopupOutdated(previousPopup.endDate) /** If outdated, set value to false, otherwise, set it to its value */ const isEnabled = isOutdated ? false : previousPopup.popupEnabled + const popupImage = + previousPopup.image === '' + ? defaultIcon + : `/assets/ecogesture/${previousPopup.image}.png` setCustomPopup({ - title: previousPopup.title, - description: previousPopup.description, + ...previousPopup, + image: popupImage, endDate: customPopup.endDate, popupEnabled: isEnabled, }) @@ -168,6 +179,7 @@ const Popups: React.FC = () => { subscribed = false setRefreshData(false) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [user, refreshData, setPartnersInfo, setCustomPopup, resetFields]) const handleSave = async (): Promise<void> => { @@ -185,7 +197,10 @@ const Popups: React.FC = () => { getAxiosXSRFHeader(user.xsrftoken) ) await customPopupService.saveCustomPopup( - customPopup, + { + ...customPopup, + image: getFilenameFromPath(customPopup.image), + }, getAxiosXSRFHeader(user.xsrftoken) ) setPreviousEndDate(customPopup.endDate) @@ -375,6 +390,15 @@ const Popups: React.FC = () => { </div> </div> + <h4>Image</h4> + <div> + <ImagePicker + imageURL={customPopup.image} + handleChange={handlePopupChange} + defaultIcon={defaultIcon} + /> + </div> + <h4>Contenu</h4> <div className="popupTitle"> <TextField @@ -384,7 +408,7 @@ const Popups: React.FC = () => { label="Titre" value={customPopup.title} onChange={event => - handlePopupChange('title', event.target.value) + handlePopupChange(event.target.value, 'title') } /> </div> @@ -395,7 +419,7 @@ const Popups: React.FC = () => { customPopup.description )} handleChange={value => - handlePopupChange('description', value) + handlePopupChange(value, 'description') } type="custom_popup" /> diff --git a/src/components/SideBar/sidebar.scss b/src/components/SideBar/sidebar.scss index 1b2189ed7cc9407f80bb4f597ec5c33660d2532d..35e62288761b63720788f8b0bf717724c72bd0ca 100644 --- a/src/components/SideBar/sidebar.scss +++ b/src/components/SideBar/sidebar.scss @@ -2,7 +2,7 @@ @import '../../styles/config/breakpoints'; .menu { - z-index: 1500; + z-index: 1000; display: flex; flex-direction: column; padding: 1.5rem; diff --git a/src/models/customPopup.model.ts b/src/models/customPopup.model.ts index 0241e3a1660e48af6e662391664b921d2df7aca6..74127e5c6d63f2eeea2a30c21e81e3086885dab7 100644 --- a/src/models/customPopup.model.ts +++ b/src/models/customPopup.model.ts @@ -2,6 +2,7 @@ import { durationType } from './durationOptions.model' export interface ICustomPopup { description: string + image: string popupEnabled: boolean endDate: string title: string diff --git a/src/services/newsletter.service.ts b/src/services/newsletter.service.ts index 525ca8762ce9e4f758c78520d2774d9b92aa581c..fcedb6b50caf723ca7bd3243fb6e578ded0f4bbd 100644 --- a/src/services/newsletter.service.ts +++ b/src/services/newsletter.service.ts @@ -335,25 +335,4 @@ export class NewsletterService { console.error(e) } } - - /** - * Gets the ecogesture images URLs - */ - public getEcogestureImages = async ( - axiosHeaders: AxiosRequestConfig - ): Promise<string[]> => { - try { - const { data: imageNames } = await axios.get( - `/api/animator/imageNames`, - axiosHeaders - ) - if (imageNames && imageNames !== null) { - return imageNames.map((image: string) => `/assets/ecogesture/${image}`) - } - return [] - } catch (e) { - console.error('error', e) - return [] - } - } } diff --git a/src/utils/imagesUrlsGetter.ts b/src/utils/imagesUrlsGetter.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bc24b330a482c5afbf98bab55a0e1dd738e9dc4 --- /dev/null +++ b/src/utils/imagesUrlsGetter.ts @@ -0,0 +1,35 @@ +import axios, { AxiosRequestConfig } from 'axios' + +/** + * Returns the filename from a path (without extension) + * Example: 'path/to/file.svg' or 'file.png' become 'file' + */ +export const getFilenameFromPath = (inputPath: string): string => { + // Use a regular expression to match the full path and capture the filename without extension + const regex = /(?:.*[\\/])?(.+?)(?:\.[^.]*$|$)/ + const match = RegExp(regex).exec(inputPath) + + // Check if there is a match, else case returns an empty string + return match?.[1] ?? '' +} + +/** + * Gets the ecogesture images URLs + */ +export const getEcogestureImages = async ( + axiosHeaders: AxiosRequestConfig +): Promise<string[]> => { + try { + const { data: imageNames } = await axios.get<string[]>( + `/api/animator/imageNames`, + axiosHeaders + ) + if (imageNames && imageNames !== null) { + return imageNames.map(image => `/assets/ecogesture/${image}`) + } + return [] + } catch (e) { + console.error('error', e) + return [] + } +}