diff --git a/.gitignore b/.gitignore index 70d1ca62476e1a2596e2b9648a838958d0c9a60f..06f1ac1a877018d63171f40e5be78b2d0c9a68d8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ /coverage # production /build - +/image-lib # misc .DS_Store /.env diff --git a/.vscode/settings.json b/.vscode/settings.json index dce69f3f9103eb3972f0ff533a6a9738c41b49f3..2a18744cb31bd103c38d9ca494c666d51d4981ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,28 @@ { + "workbench.colorCustomizations": { + "activityBar.background": "#37cc4e", + "activityBar.activeBorder": "#7867d8", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#7867d8", + "activityBarBadge.foreground": "#e7e7e7", + "titleBar.activeBackground": "#2aa63d", + "titleBar.inactiveBackground": "#2aa63d99", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveForeground": "#e7e7e799", + "statusBar.background": "#2aa63d", + "statusBarItem.hoverBackground": "#37cc4e", + "statusBar.foreground": "#e7e7e7", + "activityBar.activeBackground": "#37cc4e", + "sash.hoverBorder": "#37cc4e", + "statusBarItem.remoteBackground": "#2aa63d", + "statusBarItem.remoteForeground": "#e7e7e7" + }, "editor.formatOnSave": true, "eslint.format.enable": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "peacock.color": "#2aa63d" } diff --git a/docker-compose.local.yml b/docker-compose.local.yml index bb3f992e2f9fd94563396ca2db5909074611ee26..14115d60a1bdcbb33531e9d135610bc82b4c861e 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -2,26 +2,20 @@ version: '3.7' services: nginx: image: nginx:1.16 - depends_on: - - front - - backend volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/site.conf:/etc/nginx/conf.d/default.conf - ./cert.pem:/etc/nginx/cert.pem - ./key.pem:/etc/nginx/key.pem - - ./../${IMAGE_FOLDER}:/usr/share/nginx/html/lib/${IMAGE_FOLDER} ports: - 443:443 depends_on: - backend - environment: - - IMAGE_FOLDER=${IMAGE_FOLDER} # For linux users # extra_hosts: - # - "host.docker.internal:host-gateway" + # - 'host.docker.internal:host-gateway' - database: + database-agent: image: mysql:5 ports: - 3306:3306 @@ -37,7 +31,7 @@ services: backend: image: registry.forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server:dev depends_on: - database: + database-agent: condition: service_healthy restart: unless-stopped volumes: @@ -45,7 +39,7 @@ services: - ./configs:/app/configs - ./letsencrypt_cache:/app/letsencrypt_cache - ./data:/app/data - - ./../${IMAGE_FOLDER}:/app/${IMAGE_FOLDER} + - ./${IMAGE_FOLDER}:/app/${IMAGE_FOLDER} ports: - ${HTTPS_PORT}:${HTTPS_PORT} - 8090:8090 @@ -63,6 +57,6 @@ services: - DATABASE_USER=${DATABASE_USER} - DATABASE_NAME=${DATABASE_NAME} - DATABASE_PASSWORD=${DATABASE_PASSWORD} - - DATABASE_HOST=database + - DATABASE_HOST=database-agent - MOCK_OAUTH2=${MOCK_OAUTH2} - IMAGE_FOLDER=${IMAGE_FOLDER} diff --git a/docker-compose.yml b/docker-compose.yml index 86d3e525d7a0b8a145eb851ae58bc7194ad22ec6..0ef136a42b02ab4f8ba8cf852e11f78e6f58d281 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: - ./configs:/app/configs - ./letsencrypt_cache:/app/letsencrypt_cache - ./data:/app/data - - ./image-lib/${IMAGE_FOLDER}:/app/${IMAGE_FOLDER} + - ./${IMAGE_FOLDER}:/app/${IMAGE_FOLDER} ports: - ${HTTPS_PORT}:${HTTPS_PORT} - 8190:8090 diff --git a/nginx/site.conf b/nginx/site.conf index 2ab117ce14c72d39f31bf5a03a0e34e40575d7aa..8304b5b033888634c53fb8b4489b9057cc955bfe 100644 --- a/nginx/site.conf +++ b/nginx/site.conf @@ -24,10 +24,7 @@ server { location /swagger { proxy_pass https://backend:1443/swagger; } - location ~ ^/assets/(.+\.(?:gif|jpe?g|svg))$ { - alias /usr/share/nginx/html/lib/$1; - gzip_static on; - expires max; - add_header Cache-Control public; + location /assets { + proxy_pass https://backend:1443/assets; } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index f7cd167053b03ced42bf2abb6ee99c15e304f5cc..ca83182efde7e14126ee348156ff8607c9bbb9d8 100644 --- a/public/index.html +++ b/public/index.html @@ -5,10 +5,7 @@ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> - <meta - name="description" - content="Web site created using create-react-app" - /> + <meta name="description" content="Le backoffice de l'application Ecolyo."> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <!-- manifest.json provides metadata used when your web app is installed on a @@ -24,7 +21,18 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>Backoffice Ecolyo</title> + <title>Ecolyo - Agent</title> + <meta property="og:url" content="%PUBLIC_URL%"> + <meta property="og:type" content="website"> + <meta property="og:title" content="Ecolyo - Agent"> + <meta property="og:description" content="Le backoffice de l'application Ecolyo."> + <meta property="og:image:secure_url" content="https://ecolyo-agent.grandlyon.com/og-icon.png"> + <meta property="og:image" content="https://ecolyo-agent.grandlyon.com/og-icon.png"> + <meta property="og:image:type" content="image/png" /> + <meta property="og:image:width" content="1200" /> + <meta property="og:image:height" content="627" /> + <meta property="og:image:alt" content="Ecolyo Logo" /> + </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/public/logo192.png b/public/logo192.png index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..62e287da17b3a66f7d9745a726e35b6b7a79de88 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index a4e47a6545bc15971f8f63fba70e4013df88a664..d94e3e561d3c694f3d7627931c16b609fed77898 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/public/og-icon.png b/public/og-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ab931e75cc8477bbcb634499d985bb490ca8a91e Binary files /dev/null and b/public/og-icon.png differ diff --git a/scripts/import-convert-assets.sh b/scripts/import-convert-assets.sh new file mode 100755 index 0000000000000000000000000000000000000000..73615174a91ed9745ace41a50ab05c3a66ea2d68 --- /dev/null +++ b/scripts/import-convert-assets.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Get env variables +. ../.env + +REGISTRY_ID=409 +EMAIL_ASSETS_PATH="src/assets/icons/email" +ECOGESTURE_ASSETS_PATH="src/assets/icons/visu/ecogesture" +pwd=$(pwd) + +# Fetch and convert email assets +curl "https://forge.grandlyon.com/api/v4/projects/${REGISTRY_ID}/repository/archive?path=${EMAIL_ASSETS_PATH}" --output email.tar.gz +tar -xf email.tar.gz +cd *-email/$EMAIL_ASSETS_PATH && for file in *.svg; do inkscape -h 200 --export-type="png" $file; done && rm *.svg + +cd $pwd +# Fetch and convert ecogesture assets +curl "https://forge.grandlyon.com/api/v4/projects/${REGISTRY_ID}/repository/archive?path=${ECOGESTURE_ASSETS_PATH}" --output ecogesture.tar.gz +tar -xf ecogesture.tar.gz +cd *-ecogesture/$ECOGESTURE_ASSETS_PATH && for file in *.svg; do inkscape -h 200 --export-type="png" $file; done && rm *.svg + +# Cleanup +cd $pwd +rm -rf ../${IMAGE_FOLDER}/* + +# Copy assets in IMAGE_FOLDER +mv *-email/$EMAIL_ASSETS_PATH/* ../${IMAGE_FOLDER} +mv *-ecogesture/$ECOGESTURE_ASSETS_PATH ../${IMAGE_FOLDER}/ecogesture +rm -rf email.tar.gz ecogesture.tar.gz *-email *-ecogesture diff --git a/src/components/Editing/Editing.tsx b/src/components/Editing/Editing.tsx index 90158488aeafb84ee5759ae8e9b6386dd4a94747..f0ef6d39daf6af08842a3c7e0a512855c97c80b0 100644 --- a/src/components/Editing/Editing.tsx +++ b/src/components/Editing/Editing.tsx @@ -1,281 +1,270 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' -import DateSelector from '../DateSelector/DateSelector' -import { NewsletterService } from '../../services/newsletter.service' -import { UserContext, UserContextProps } from '../../hooks/userContext' -import { IMonthlyNews } from '../../models/monthlyNews.model' -import { IMonthlyInfo } from '../../models/monthlyInfo.model' -import { IPoll } from '../../models/poll.model' -import Poll from '../Poll/Poll' -import MonthlyInfo from '../MonthlyInfo/MonthlyInfo' -import MonthlyNews from '../MonthlyNews/MonthlyNews' -import Loader from '../Loader/Loader' -import Modal from '../Modal/Modal' -import './editing.scss' - -export type ContentItems = 'monthlyInfo' | 'monthlyNews' | 'poll' | '' - -const Editing: React.FC = () => { - const [date, setDate] = useState<Date>(new Date()) - const [info, setInfo] = useState<string>('') - const [title, setTitle] = useState<string>('') - const [imageURL, setImageURL] = useState<string>('') - const [content, setContent] = useState<string>('') - const [question, setQuestion] = useState<string>('') - const [link, setLink] = useState<string>('') - const [isTouched, setIsTouched] = useState<boolean>(false) - const [refreshData, setRefreshData] = useState(false) - const [isLoading, setisLoading] = useState<boolean>(false) - const [warningModal, setwarningModal] = useState<boolean>(false) - const [toDelete, settoDelete] = useState<ContentItems>('') - const { user }: Partial<UserContextProps> = useContext(UserContext) - const newsletterService = new NewsletterService() - - const handleSaveMonthlyInfo = async (): Promise<void> => { - if (user) { - const newsletterService = new NewsletterService() - await newsletterService.saveMonthlyInfo( - date, - info, - imageURL, - user.xsrftoken - ) - setIsTouched(false) - } - } - - const handleSaveMonthlyNews = async (): Promise<void> => { - if (user) { - const newsletterService = new NewsletterService() - await newsletterService.saveMonthlyNews( - date, - title, - content, - user.xsrftoken - ) - setIsTouched(false) - } - } - const handleSavePoll = async (): Promise<void> => { - if (user) { - await newsletterService.savePoll(date, question, link, user.xsrftoken) - setIsTouched(false) - } - } - const handleCancel = useCallback(() => { - setRefreshData(true) - }, []) - - const handleDeleteMonthlyInfo = async (): Promise<void> => { - if (user) { - await newsletterService.deleteMonthlyInfo( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - setRefreshData(true) - } - } - const handleDeleteMonthlyNews = async (): Promise<void> => { - if (user) { - await newsletterService.deleteMonthlyNews( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - setRefreshData(true) - } - } - const handleDeletePoll = async (): Promise<void> => { - if (user) { - await newsletterService.deletePoll( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - setRefreshData(true) - } - } - const handleOpenDeleteModal = (target: ContentItems) => { - settoDelete(target) - setwarningModal(true) - } - const handleConfirmAlert = () => { - if (toDelete === 'monthlyInfo') { - handleDeleteMonthlyInfo() - } - if (toDelete === 'monthlyNews') { - handleDeleteMonthlyNews() - } - if (toDelete === 'poll') { - handleDeletePoll() - } - setwarningModal(false) - } - - const isEmpty = (): boolean => { - if ( - (info !== '' || - title !== '' || - content !== '' || - question !== '' || - imageURL !== '' || - link !== '') && - isTouched - ) { - return false - } else return true - } - - const handleEditorChange = ( - value: string, - type: 'info' | 'title' | 'content' | 'question' | 'link' | 'image' - ): void => { - setIsTouched(true) - if (type === 'info') { - setInfo(value) - } - if (type === 'title') { - setTitle(value) - } - if (type === 'content') { - setContent(value) - } - if (type === 'question') { - setQuestion(value) - } - if (type === 'link') { - setLink(value) - } - if (type === 'image') { - setImageURL(value) - } - } - const resetFields = useCallback(() => { - setImageURL('') - setInfo('') - setTitle('') - setContent('') - setLink('') - setQuestion('') - }, []) - - useEffect(() => { - let subscribed = true - resetFields() - setisLoading(true) - async function getCurrentMonthlyNews() { - if (user) { - const newsletterService = new NewsletterService() - const montlhyInfo: IMonthlyInfo | null = - await newsletterService.getSingleMonthlyInfo( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - const montlhyNews: IMonthlyNews | null = - await newsletterService.getSingleMonthlyNews( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - const poll: IPoll | null = await newsletterService.getSinglePoll( - date.getFullYear(), - date.getMonth(), - user.xsrftoken - ) - if (montlhyInfo) { - setInfo(montlhyInfo.info) - setImageURL(montlhyInfo.image) - setIsTouched(false) - } - if (montlhyNews) { - setTitle(montlhyNews.title) - setContent(montlhyNews.content) - setIsTouched(false) - } - if (poll) { - setLink(poll.link) - setQuestion(poll.question) - setIsTouched(false) - } - } - setisLoading(false) - } - if (subscribed) { - getCurrentMonthlyNews() - } - return () => { - subscribed = false - setRefreshData(false) - } - }, [date, user, refreshData, resetFields]) - - return ( - <> - <div className="header"> - <p className="title pagetitle">Édition de la newsletter</p> - <DateSelector date={date} setDate={setDate} isEmpty={isEmpty} /> - </div> - {isLoading ? ( - <Loader /> - ) : ( - <div className="content"> - <MonthlyInfo - info={info} - onSave={handleSaveMonthlyInfo} - onCancel={handleCancel} - handleChange={handleEditorChange} - onDelete={handleOpenDeleteModal} - imageURL={imageURL} - /> - <hr /> - <MonthlyNews - title={title} - content={content} - onSave={handleSaveMonthlyNews} - onCancel={handleCancel} - handleChange={handleEditorChange} - onDelete={handleOpenDeleteModal} - /> - <hr /> - <Poll - question={question} - link={link} - onSave={handleSavePoll} - onCancel={handleCancel} - handleChange={handleEditorChange} - onDelete={handleOpenDeleteModal} - /> - </div> - )} - {warningModal && ( - <Modal> - <> - <div className="modal-text"> - Etes-vous sûr de vouloir supprimer{' '} - {toDelete === 'monthlyInfo' - ? 'cette info mensuelle ' - : toDelete === 'monthlyNews' - ? 'cette news mensuelle' - : 'ce sondage'}{' '} - ? - </div> - <div className="buttons"> - <button - className="btnCancel" - onClick={() => setwarningModal(false)} - > - Annuler - </button> - <button className="btnValid" onClick={handleConfirmAlert}> - Continuer - </button> - </div> - </> - </Modal> - )} - </> - ) -} - -export default Editing +import React, { useCallback, useContext, useEffect, useState } from 'react' +import DateSelector from '../DateSelector/DateSelector' +import { NewsletterService } from '../../services/newsletter.service' +import { UserContext, UserContextProps } from '../../hooks/userContext' +import { IMonthlyNews } from '../../models/monthlyNews.model' +import { IMonthlyInfo } from '../../models/monthlyInfo.model' +import { IPoll } from '../../models/poll.model' +import Poll from '../Poll/Poll' +import MonthlyInfo from '../MonthlyInfo/MonthlyInfo' +import MonthlyNews from '../MonthlyNews/MonthlyNews' +import Loader from '../Loader/Loader' +import Modal from '../Modal/Modal' +import './editing.scss' + +export type ContentItems = 'monthlyInfo' | 'monthlyNews' | 'poll' | '' + +const Editing: React.FC = () => { + // Fonctional rule : + // Display next month after the 3rd of the current month + const getCurrentNewsletterDate = (): Date => { + let newsletterDate = new Date() + if (newsletterDate.getDate() >= 3) { + newsletterDate.setMonth(newsletterDate.getMonth() + 1) + } + return newsletterDate + } + + const [date, setDate] = useState<Date>(getCurrentNewsletterDate()) + const [info, setInfo] = useState<string>('') + const [title, setTitle] = useState<string>('') + const [imageURL, setImageURL] = useState<string>('') + const [content, setContent] = useState<string>('') + const [question, setQuestion] = useState<string>('') + const [link, setLink] = useState<string>('') + const [isTouched, setIsTouched] = useState<boolean>(false) + const [refreshData, setRefreshData] = useState(false) + const [isLoading, setisLoading] = useState<boolean>(false) + const [warningModal, setwarningModal] = useState<boolean>(false) + const [toDelete, settoDelete] = useState<ContentItems>('') + const { user }: Partial<UserContextProps> = useContext(UserContext) + const newsletterService = new NewsletterService() + + const handleSaveMonthlyInfo = async (): Promise<void> => { + if (user) { + const newsletterService = new NewsletterService() + await newsletterService.saveMonthlyInfo( + date, + info, + imageURL, + user.xsrftoken + ) + setIsTouched(false) + } + } + + const handleSaveMonthlyNews = async (): Promise<void> => { + if (user) { + const newsletterService = new NewsletterService() + await newsletterService.saveMonthlyNews( + date, + title, + content, + user.xsrftoken + ) + setIsTouched(false) + } + } + const handleSavePoll = async (): Promise<void> => { + if (user) { + await newsletterService.savePoll(date, question, link, user.xsrftoken) + setIsTouched(false) + } + } + const handleCancel = useCallback(() => { + setRefreshData(true) + }, []) + + const handleDeleteMonthlyInfo = async (): Promise<void> => { + if (user) { + await newsletterService.deleteMonthlyInfo(date, user.xsrftoken) + setRefreshData(true) + } + } + const handleDeleteMonthlyNews = async (): Promise<void> => { + if (user) { + await newsletterService.deleteMonthlyNews(date, user.xsrftoken) + setRefreshData(true) + } + } + const handleDeletePoll = async (): Promise<void> => { + if (user) { + await newsletterService.deletePoll(date, user.xsrftoken) + setRefreshData(true) + } + } + const handleOpenDeleteModal = (target: ContentItems) => { + settoDelete(target) + setwarningModal(true) + } + const handleConfirmAlert = () => { + if (toDelete === 'monthlyInfo') { + handleDeleteMonthlyInfo() + } + if (toDelete === 'monthlyNews') { + handleDeleteMonthlyNews() + } + if (toDelete === 'poll') { + handleDeletePoll() + } + setwarningModal(false) + } + + const isEmpty = (): boolean => { + if ( + (info !== '' || + title !== '' || + content !== '' || + question !== '' || + imageURL !== '' || + link !== '') && + isTouched + ) { + return false + } else return true + } + + const handleEditorChange = ( + value: string, + type: 'info' | 'title' | 'content' | 'question' | 'link' | 'image' + ): void => { + setIsTouched(true) + if (type === 'info') { + setInfo(value) + } + if (type === 'title') { + setTitle(value) + } + if (type === 'content') { + setContent(value) + } + if (type === 'question') { + setQuestion(value) + } + if (type === 'link') { + setLink(value) + } + if (type === 'image') { + setImageURL(value) + } + } + const resetFields = useCallback(() => { + setImageURL('') + setInfo('') + setTitle('') + setContent('') + setLink('') + setQuestion('') + }, []) + + useEffect(() => { + let subscribed = true + resetFields() + setisLoading(true) + async function getCurrentMonthlyNews() { + if (user) { + const newsletterService = new NewsletterService() + const montlhyInfo: IMonthlyInfo | null = + await newsletterService.getSingleMonthlyInfo(date, user.xsrftoken) + const montlhyNews: IMonthlyNews | null = + await newsletterService.getSingleMonthlyNews(date, user.xsrftoken) + const poll: IPoll | null = await newsletterService.getSinglePoll( + date, + user.xsrftoken + ) + if (montlhyInfo) { + setInfo(montlhyInfo.info) + setImageURL(montlhyInfo.image) + setIsTouched(false) + } + if (montlhyNews) { + setTitle(montlhyNews.title) + setContent(montlhyNews.content) + setIsTouched(false) + } + if (poll) { + setLink(poll.link) + setQuestion(poll.question) + setIsTouched(false) + } + } + setisLoading(false) + } + if (subscribed) { + getCurrentMonthlyNews() + } + return () => { + subscribed = false + setRefreshData(false) + } + }, [date, user, refreshData, resetFields]) + + return ( + <> + <div className="header"> + <p className="title pagetitle">Édition de la newsletter</p> + <DateSelector date={date} setDate={setDate} isEmpty={isEmpty} /> + </div> + {isLoading ? ( + <Loader /> + ) : ( + <div className="content"> + <MonthlyInfo + info={info} + onSave={handleSaveMonthlyInfo} + onCancel={handleCancel} + handleChange={handleEditorChange} + onDelete={handleOpenDeleteModal} + imageURL={imageURL} + /> + <hr /> + <MonthlyNews + title={title} + content={content} + onSave={handleSaveMonthlyNews} + onCancel={handleCancel} + handleChange={handleEditorChange} + onDelete={handleOpenDeleteModal} + /> + <hr /> + <Poll + question={question} + link={link} + onSave={handleSavePoll} + onCancel={handleCancel} + handleChange={handleEditorChange} + onDelete={handleOpenDeleteModal} + /> + </div> + )} + {warningModal && ( + <Modal> + <> + <div className="modal-text"> + Etes-vous sûr de vouloir supprimer{' '} + {toDelete === 'monthlyInfo' + ? 'cette info mensuelle ' + : toDelete === 'monthlyNews' + ? 'cette news mensuelle' + : 'ce sondage'}{' '} + ? + </div> + <div className="buttons"> + <button + className="btnCancel" + onClick={() => setwarningModal(false)} + > + Annuler + </button> + <button className="btnValid" onClick={handleConfirmAlert}> + Continuer + </button> + </div> + </> + </Modal> + )} + </> + ) +} + +export default Editing diff --git a/src/components/ImagePicker/ImagePicker.tsx b/src/components/ImagePicker/ImagePicker.tsx index fa93461a1958d0ec3f5cfd95f76d2481ab81f82b..7362ea594db5fb9fc4a644a76872ff192ba735c8 100644 --- a/src/components/ImagePicker/ImagePicker.tsx +++ b/src/components/ImagePicker/ImagePicker.tsx @@ -49,7 +49,7 @@ const ImagePicker: React.FC<ImagePickerProps> = ({ user.xsrftoken ) //Split array depending on page numbers - setpageCount(Math.ceil(images.length / imagePerPage) - 1) + setpageCount(Math.ceil(images.length / imagePerPage)) const arraySplitted = [] while (images.length) { arraySplitted.push(images.splice(0, imagePerPage)) @@ -95,8 +95,8 @@ const ImagePicker: React.FC<ImagePickerProps> = ({ <div className="image-picker"> {imageNames && imageNames !== [] && - imageNames[currentPage] !== [] && - imageNames[currentPage].map((imageURL) => ( + imageNames[currentPage - 1] !== [] && + imageNames[currentPage - 1].map((imageURL) => ( <SingleImage imageURL={imageURL} key={imageURL} diff --git a/src/components/PartnersInfo/PartnersInfo.tsx b/src/components/PartnersInfo/PartnersInfo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0988b3d7f27f1e8e3f2f68ad3b4dad51540e7c6d --- /dev/null +++ b/src/components/PartnersInfo/PartnersInfo.tsx @@ -0,0 +1,187 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { IPartnersInfo } from '../../models/partnersInfo.model' +import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css' +import './partnersInfo.scss' +import { PartnersInfoService } from '../../services/partnersInfo.service' +import { UserContext, UserContextProps } from '../../hooks/userContext' +import Loader from '../Loader/Loader' +import { CheckboxType } from '../../enum/checkboxType.enum' + +const PartnersInfo: React.FC = () => { + const [refreshData, setRefreshData] = useState(false) + const [isLoading, setIsLoading] = useState<boolean>(false) + const [partnersInfo, setPartnersInfo] = useState<IPartnersInfo>({ + grdf_failure: false, + enedis_failure: false, + egl_failure: false, + notification_activated: false, + }) + const { user }: Partial<UserContextProps> = useContext(UserContext) + + const handleCheckboxChange = (value: boolean, type: CheckboxType): void => { + switch (type) { + case CheckboxType.GRDF: + setPartnersInfo((prevPartnersInfo) => ({ + ...prevPartnersInfo, + grdf_failure: value, + })) + break + case CheckboxType.ENEDIS: + setPartnersInfo((prevPartnersInfo) => ({ + ...prevPartnersInfo, + enedis_failure: value, + })) + break + case CheckboxType.EGL: + setPartnersInfo((prevPartnersInfo) => ({ + ...prevPartnersInfo, + egl_failure: value, + })) + break + case CheckboxType.NOTIFICATION: + setPartnersInfo((prevPartnersInfo) => ({ + ...prevPartnersInfo, + notification_activated: value, + })) + break + default: + throw new Error('Unknown checkbox type') + } + } + + const handleCancel = useCallback(() => { + setRefreshData(true) + }, [setRefreshData]) + + const resetFields = useCallback(() => { + setPartnersInfo({ + grdf_failure: false, + enedis_failure: false, + egl_failure: false, + notification_activated: false, + }) + }, [setPartnersInfo]) + + useEffect(() => { + let subscribed = true + resetFields() + setIsLoading(true) + + async function getPartnersInfo() { + if (user) { + const partnersInfoService = new PartnersInfoService() + const partnersInfoResp: IPartnersInfo | null = + await partnersInfoService.getPartnersInfo() + if (partnersInfoResp) { + setPartnersInfo({ + grdf_failure: partnersInfoResp.grdf_failure, + enedis_failure: partnersInfoResp.enedis_failure, + egl_failure: partnersInfoResp.egl_failure, + notification_activated: partnersInfoResp.notification_activated, + }) + } + } + setIsLoading(false) + } + if (subscribed) { + getPartnersInfo() + } + return () => { + subscribed = false + setRefreshData(false) + } + }, [user, refreshData, setPartnersInfo, resetFields]) + + const handleSave = async (): Promise<void> => { + if (user) { + const partnersInfoService = new PartnersInfoService() + await partnersInfoService.savePartnersInfo(partnersInfo, user.xsrftoken) + } + } + + return ( + <> + {isLoading ? ( + <Loader /> + ) : ( + <div className="partnersInfo"> + <h2>État des services des partenaires</h2> + <div> + <p className="title">Affichage de la pop-up dans Ecolyo</p> + <div className="switch_div"> + Pop-up active + <input + type="checkbox" + id="switch_notification" + onChange={(event) => { + handleCheckboxChange( + event.currentTarget.checked, + CheckboxType.NOTIFICATION + ) + }} + checked={partnersInfo.notification_activated} + /> + <label htmlFor="switch_notification"></label> + </div> + <p className="title">Services concernés</p> + <div className="switch_div"> + Panne Enedis + <input + type="checkbox" + id="switch_enedis" + onChange={(event) => { + handleCheckboxChange( + event.currentTarget.checked, + CheckboxType.ENEDIS + ) + }} + checked={partnersInfo.enedis_failure} + /> + <label htmlFor="switch_enedis"></label> + </div> + <div className="switch_div"> + Panne EGL + <input + type="checkbox" + id="switch_egl" + onChange={(event) => { + handleCheckboxChange( + event.currentTarget.checked, + CheckboxType.EGL + ) + }} + checked={partnersInfo.egl_failure} + /> + <label htmlFor="switch_egl"></label> + </div> + <div className="switch_div"> + Panne GRDF + <input + type="checkbox" + id="switch_grdf" + onChange={(event) => { + handleCheckboxChange( + event.currentTarget.checked, + CheckboxType.GRDF + ) + }} + checked={partnersInfo.grdf_failure} + /> + <label htmlFor="switch_grdf"></label> + </div> + <div className="buttons"> + <button className="btnCancel" onClick={handleCancel}> + Annuler + </button> + <button className="btnValid" onClick={handleSave}> + Sauvegarder + </button> + </div> + </div> + </div> + )} + </> + ) +} + +export default PartnersInfo diff --git a/src/components/PartnersInfo/partnersInfo.scss b/src/components/PartnersInfo/partnersInfo.scss new file mode 100644 index 0000000000000000000000000000000000000000..27de6e115e2ddca455b1fe3003e36429588a5e3f --- /dev/null +++ b/src/components/PartnersInfo/partnersInfo.scss @@ -0,0 +1,59 @@ +.partnersInfo { + margin: 2rem 0; + .title { + margin: 1rem 0; + } + h2 { + margin-bottom: 1rem; + } + + .switch_div { + display: inline-block; + padding: 1rem 1rem; + min-width: 135px; + } + + input[type='checkbox'] { + width: 0; + height: 0; + visibility: hidden; + margin-bottom: 15px; + } + + label { + display: block; + width: 50px; + height: 20px; + background-color: grey; + border-radius: 15px; + position: relative; + cursor: pointer; + transition: 0.5s; + box-shadow: 0 0 20px #80808050; + } + + label::after { + content: ''; + width: 17px; + height: 17px; + background-color: #e8f5f7; + position: absolute; + border-radius: 13px; + top: 2px; + left: 2px; + transition: 0.5s; + } + + input:checked + label:after { + left: calc(100% - 3px); + transform: translateX(-100%); + } + + input:checked + label { + background-color: #e3b82a; + } + + label:active:after { + width: 34px; + } +} diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 30cfca082884b42997bbed1cb1c6ced7907a8719..444d3842e42251c5dce59589252026ddcbfeb168 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -1,7 +1,17 @@ import React from 'react' +import PartnersInfo from '../PartnersInfo/PartnersInfo' const Settings: React.FC = () => { - return <div>A venir</div> -} + return ( + <> + <div className="header"> + <p className="title pagetitle">Paramètres de l'appli</p> + </div> + <div className="content"> + <PartnersInfo /> + </div> + </> + ) +} export default Settings diff --git a/src/enum/checkboxType.enum.ts b/src/enum/checkboxType.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..6363fcefdaaa845a8bb343bc6bd363a032f9ae1d --- /dev/null +++ b/src/enum/checkboxType.enum.ts @@ -0,0 +1,6 @@ +export enum CheckboxType { + NOTIFICATION = 0, + GRDF = 1, + ENEDIS = 2, + EGL = 3, +} diff --git a/src/hooks/useFindUser.ts b/src/hooks/useFindUser.ts index 3c853bca6acc2f327dc1c1fc2dd94210a3042bb0..b76d2a1145652a49d9bf0abdbfd4ead40172a872 100644 --- a/src/hooks/useFindUser.ts +++ b/src/hooks/useFindUser.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import axios from 'axios' import { User } from '../models/user.model' +import { toast } from 'react-toastify' const useFindUser = () => { const [user, setUser] = useState<User | null>(null) @@ -8,10 +9,14 @@ const useFindUser = () => { useEffect(() => { async function findUser() { - const { data } = await axios.get(`/api/common/WhoAmI`) - if (data) { - setUser(data) - setLoading(false) + try { + const { data } = await axios.get(`/api/common/WhoAmI`) + if (data) { + setUser(data) + setLoading(false) + } + } catch (error) { + toast.error('Access denied, please login') } } findUser() diff --git a/src/models/partnersInfo.model.ts b/src/models/partnersInfo.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d073d3e2035fe03482157b5cdb48ab1b3b02d70 --- /dev/null +++ b/src/models/partnersInfo.model.ts @@ -0,0 +1,6 @@ +export interface IPartnersInfo { + grdf_failure: boolean + enedis_failure: boolean + egl_failure: boolean + notification_activated: boolean +} diff --git a/src/services/newsletter.service.ts b/src/services/newsletter.service.ts index d0f4515aca3fecec6af17b512a6b4ae5a3a24bb6..7a27ccca1665cc7fe6e2ddcf29df11ea89f3b18c 100644 --- a/src/services/newsletter.service.ts +++ b/src/services/newsletter.service.ts @@ -19,7 +19,7 @@ export class NewsletterService { await axios.put( `/api/admin/monthlyInfo`, { - month: date.getMonth(), + month: date.getMonth() + 1, year: date.getFullYear(), info: info, image: image, @@ -31,23 +31,30 @@ export class NewsletterService { } ) toast.success('Monthly info succesfully saved !') - } catch (e) { - toast.error('Failed to create monthly info') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to create monthly info') + } console.error(e) } } /** * Gets the information for selected month + * @param date + * @param token */ public getSingleMonthlyInfo = async ( - year: number, - month: number, + date: Date, token: string ): Promise<IMonthlyInfo | null> => { try { const { data } = await axios.get( - `/api/admin/monthlyInfo/${year}/${month}`, + `/api/admin/monthlyInfo/${date.getFullYear()}/${date.getMonth() + 1}`, { headers: { 'XSRF-TOKEN': token, @@ -55,7 +62,7 @@ export class NewsletterService { } ) return data as IMonthlyInfo - } catch (e) { + } catch (e: any) { console.error('error', e) return null } @@ -63,24 +70,31 @@ export class NewsletterService { /** * Deletes a Monthly Info for selected month - * @param year - * @param month + * @param date * @param token */ public deleteMonthlyInfo = async ( - year: number, - month: number, + date: Date, token: string ): Promise<void> => { try { - await axios.delete(`/api/admin/monthlyInfo/${year}/${month}`, { - headers: { - 'XSRF-TOKEN': token, - }, - }) + await axios.delete( + `/api/admin/monthlyInfo/${date.getFullYear()}/${date.getMonth() + 1}`, + { + headers: { + 'XSRF-TOKEN': token, + }, + } + ) toast.success('Monthly info succesfully deleted !') - } catch (e) { - toast.error('Failed to delete monthly info') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to delete monthly info') + } console.error(e) } } @@ -101,7 +115,7 @@ export class NewsletterService { await axios.put( `/api/admin/monthlyNews`, { - month: date.getMonth(), + month: date.getMonth() + 1, year: date.getFullYear(), title: title, content: content, @@ -113,23 +127,30 @@ export class NewsletterService { } ) toast.success('Monthly news succesfully saved !') - } catch (e) { - toast.error('Failed to create monthly news') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to save monthly news') + } console.error(e) } } /** * Gets a news title and content for selected month + * @param date + * @param token */ public getSingleMonthlyNews = async ( - year: number, - month: number, + date: Date, token: string ): Promise<IMonthlyNews | null> => { try { const { data } = await axios.get( - `/api/admin/monthlyNews/${year}/${month}`, + `/api/admin/monthlyNews/${date.getFullYear()}/${date.getMonth() + 1}`, { headers: { 'XSRF-TOKEN': token, @@ -145,24 +166,31 @@ export class NewsletterService { /** * Deletes a Monthly News for selected month - * @param year - * @param month + * @param date * @param token */ public deleteMonthlyNews = async ( - year: number, - month: number, + date: Date, token: string ): Promise<void> => { try { - await axios.delete(`/api/admin/monthlyNews/${year}/${month}`, { - headers: { - 'XSRF-TOKEN': token, - }, - }) + await axios.delete( + `/api/admin/monthlyNews/${date.getFullYear()}/${date.getMonth() + 1}`, + { + headers: { + 'XSRF-TOKEN': token, + }, + } + ) toast.success('Monthly news succesfully deleted !') - } catch (e) { - toast.error('Failed to delete monthly news') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to delete monthly news') + } console.error(e) } } @@ -183,7 +211,7 @@ export class NewsletterService { await axios.put( `/api/admin/poll`, { - month: date.getMonth(), + month: date.getMonth() + 1, year: date.getFullYear(), link: link, question: question, @@ -195,26 +223,36 @@ export class NewsletterService { } ) toast.success('Poll successfully saved !') - } catch (e) { - toast.error('Failed to create poll') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to create poll') + } console.error(e) } } /** * Gets a poll with question and link for selected month + * @param date + * @param token */ public getSinglePoll = async ( - year: number, - month: number, + date: Date, token: string ): Promise<IPoll | null> => { try { - const { data } = await axios.get(`/api/admin/poll/${year}/${month}`, { - headers: { - 'XSRF-TOKEN': token, - }, - }) + const { data } = await axios.get( + `/api/admin/poll/${date.getFullYear()}/${date.getMonth() + 1}`, + { + headers: { + 'XSRF-TOKEN': token, + }, + } + ) return data as IPoll } catch (e) { console.error('error', e) @@ -224,24 +262,28 @@ export class NewsletterService { /** * Deletes a poll for selected month - * @param month - * @param year + * @param date * @param token */ - public deletePoll = async ( - year: number, - month: number, - token: string - ): Promise<void> => { + public deletePoll = async (date: Date, token: string): Promise<void> => { try { - await axios.delete(`/api/admin/poll/${year}/${month}`, { - headers: { - 'XSRF-TOKEN': token, - }, - }) + await axios.delete( + `/api/admin/poll/${date.getFullYear()}/${date.getMonth() + 1}`, + { + headers: { + 'XSRF-TOKEN': token, + }, + } + ) toast.success('Poll succesfully deleted !') - } catch (e) { - toast.error('Failed to delete poll') + } catch (e: any) { + if (e.response.status === 403) { + toast.error( + "Unauthorized : You don't have the rights to do this operation" + ) + } else { + toast.error('Failed to delete poll') + } console.error(e) } } @@ -258,7 +300,7 @@ export class NewsletterService { }) if (imageNames && imageNames !== null) { const imageURLs = imageNames.map((image: string) => { - return `/assets/ecogeste/${image}` + return `/assets/ecogesture/${image}` }) return imageURLs } diff --git a/src/services/partnersInfo.service.ts b/src/services/partnersInfo.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b930759c5f95db71a73fdee1911480391af9cb9 --- /dev/null +++ b/src/services/partnersInfo.service.ts @@ -0,0 +1,48 @@ +import axios from 'axios' +import { IPartnersInfo } from '../models/partnersInfo.model' +import { toast } from 'react-toastify' +export class PartnersInfoService { + /** + * Save the partnersInfo + * @param partnersInfo + * @param token + */ + public savePartnersInfo = async ( + partnersInfo: IPartnersInfo, + token: string + ): Promise<void> => { + try { + await axios.put( + `/api/admin/partnersInfo`, + { + grdf_failure: partnersInfo.grdf_failure, + enedis_failure: partnersInfo.enedis_failure, + egl_failure: partnersInfo.egl_failure, + notification_activated: partnersInfo.notification_activated, + }, + { + headers: { + 'XSRF-TOKEN': token, + }, + } + ) + toast.success('Partners info succesfully saved !') + } catch (e) { + toast.error('Failed to save partners info') + console.error(e) + } + } + + /** + * Gets the partners information + */ + public getPartnersInfo = async (): Promise<IPartnersInfo | null> => { + try { + const { data } = await axios.get(`/api/common/partnersInfo`) + return data as IPartnersInfo + } catch (e) { + console.error('error', e) + return null + } + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss index b8ed46d63473931a98210a9ad2f6f5f76f895873..42a35b1f11cd5cd3def79d3f3d1b1b765eb7365d 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -2,7 +2,6 @@ @import 'config/colors'; @import 'config/typography'; @import 'config/layout'; -@import 'config/layout'; @import 'toast'; * {