diff --git a/api/controllers/admin.js b/api/controllers/admin.js index b369b16..cb8cc9d 100644 --- a/api/controllers/admin.js +++ b/api/controllers/admin.js @@ -4,6 +4,50 @@ const { validationResult } = require('express-validator'); const errorHandling = require('../assets/utils/errorHandling'); const { serverError } = require('../assets/config/errors'); const Functions = require('../models/functions'); +const Categories = require('../models/categories'); +const helperQueryNumber = require('../assets/utils/helperQueryNumber'); +const Sequelize = require('sequelize'); + +exports.getFunctions = (req, res, next) => { + const page = helperQueryNumber(req.query.page, 1); + const limit = helperQueryNumber(req.query.limit, 10); + const categoryId = helperQueryNumber(req.query.categoryId, 0); + let search = req.query.search; + try { search = search.toLowerCase(); } catch {} + const offset = (page - 1) * limit; + Functions.findAndCountAll({ + limit, + offset, + where: { + // Trie par catégorie + ... (categoryId !== 0) && { categorieId: categoryId }, + // Recherche + ... (search != undefined) && { + [Sequelize.Op.or]: [ + { title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', `%${search}%`) }, + { slug: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('slug')), 'LIKE', `%${search}%`) }, + { description: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('description')), 'LIKE', `%${search}%`) } + ] + } + }, + include: [ + { model: Categories, attributes: ["name", "color"] } + ], + attributes: { + exclude: ["updatedAt", "utilizationForm", "article", "isOnline"] + }, + order: [['createdAt', 'DESC']] + }) + .then((result) => { + const { count, rows } = result; + const hasMore = (page * limit) < count; + return res.status(200).json({ totalItems: count, hasMore, rows }); + }) + .catch((error) => { + console.log(error); + return errorHandling(next, serverError); + }); +} exports.postFunction = (req, res, next) => { const { title, slug, description, type, categorieId } = req.body; @@ -19,7 +63,9 @@ exports.postFunction = (req, res, next) => { )) { return errorHandling(next, { message:"La fonction doit avoir une image valide.", statusCode: 400 }); } - const imageName = slug + image.name; + const splitedImageName = image.name.split('.'); + if (splitedImageName.length !== 2) return errorHandling(next, serverError); + const imageName = slug + '.' + splitedImageName[1]; image.mv(path.join(__dirname, '..', 'assets', 'images', 'functions') + '/' + imageName, async (error) => { if (error) return errorHandling(next, serverError); try { diff --git a/api/middlewares/isAdmin.js b/api/middlewares/isAdmin.js index 5802249..f0ac9e0 100644 --- a/api/middlewares/isAdmin.js +++ b/api/middlewares/isAdmin.js @@ -18,6 +18,6 @@ module.exports = (req, _res, next) => { }) .catch((error) => { console.log(error); - errorHandling(next, serverError); + return errorHandling(next, serverError); }); } \ No newline at end of file diff --git a/api/routes/admin.js b/api/routes/admin.js index 823fcf5..7134029 100644 --- a/api/routes/admin.js +++ b/api/routes/admin.js @@ -9,70 +9,95 @@ const Categories = require('../models/categories'); const AdminRouter = Router(); -// Permet de créé une fonction -AdminRouter.post('/functions', isAuth, isAdmin, -fileUpload({ - useTempFiles: true, - safeFileNames: true, - preserveExtension: Number, - limits: { fileSize: 5 * 1024 * 1024 }, // 5mb, - parseNested: true -}), -[ - body('title') - .not() - .isEmpty() - .withMessage("La fonction doit avoir un titre.") - .isLength({ max: 100 }) - .withMessage("Le titre est trop long."), - body('slug') - .not() - .isEmpty() - .withMessage("La fonction doit avoir un slug.") - .isLength({ max: 100 }) - .withMessage("Le slug est trop long.") - .custom((async (slug) => { - try { - const FunctionSlug = await Functions.findOne({ where: { slug } }); - if (FunctionSlug) { - return Promise.reject("Le slug existe déjà..."); - } - } catch (error) { - console.log(error); - } - return true; - })), - body('description') - .not() - .isEmpty() - .withMessage("La fonction doit avoir une description.") - .isLength({ max: 255 }) - .withMessage("La description est trop longue."), - body('categorieId') - .not() - .isEmpty() - .withMessage("La fonction doit avoir une catégorie.") - .custom(async (categorieId) => { - try { - const categorieFound = await Categories.findOne({ where: { id: categorieId } }); - if (!categorieFound) { - return Promise.reject("La catégorie n'existe pas!"); - } - } catch (error) { - console.log(error); - } - return true; - }), - body('type') - .custom((type) => { - if (!(type === 'article' || type === 'form' || type === 'page')) { - return Promise.reject('Le type de la fonction peut être : article, form ou page.'); - } - return true; - }) -], adminController.postFunction); +AdminRouter.route('/functions') -// Supprime une fonction avec son id -AdminRouter.delete('/functions/:id', isAuth, isAdmin, adminController.deleteFunction); + // Récupère les fonctions + .get(isAuth, isAdmin, adminController.getFunctions) + + // Permet de créé une fonction + .post(isAuth, isAdmin, + fileUpload({ + useTempFiles: true, + safeFileNames: true, + preserveExtension: Number, + limits: { fileSize: 5 * 1024 * 1024 }, // 5mb, + parseNested: true + }), + [ + body('title') + .not() + .isEmpty() + .withMessage("La fonction doit avoir un titre.") + .isLength({ max: 100 }) + .withMessage("Le titre est trop long.") + .custom(((title) => { + if (title === 'undefined') { + return Promise.reject("La fonction doit avoir un titre."); + } + return true; + })), + body('slug') + .not() + .isEmpty() + .withMessage("La fonction doit avoir un slug.") + .isLength({ max: 100 }) + .withMessage("Le slug est trop long.") + .custom(((slug) => { + if (slug === 'undefined') { + return Promise.reject("La fonction doit avoir un slug."); + } + return true; + })) + .custom((async (slug) => { + try { + const FunctionSlug = await Functions.findOne({ where: { slug } }); + if (FunctionSlug) { + return Promise.reject("Le slug existe déjà..."); + } + } catch (error) { + console.log(error); + } + return true; + })), + body('description') + .not() + .isEmpty() + .withMessage("La fonction doit avoir une description.") + .isLength({ max: 255, min: 1 }) + .withMessage("La description est trop longue.") + .custom(((description) => { + if (description === 'undefined') { + return Promise.reject("La fonction doit avoir une description."); + } + return true; + })), + body('categorieId') + .not() + .isEmpty() + .withMessage("La fonction doit avoir une catégorie.") + .custom(async (categorieId) => { + try { + const categorieFound = await Categories.findOne({ where: { id: parseInt(categorieId) } }); + if (!categorieFound) { + return Promise.reject("La catégorie n'existe pas!"); + } + } catch (error) { + console.log(error); + } + return true; + }), + body('type') + .custom((type) => { + if (!(type === 'article' || type === 'form' || type === 'page')) { + return Promise.reject('Le type de la fonction peut être : article, form ou page.'); + } + return true; + }) + ], adminController.postFunction); + +AdminRouter.route('/functions/:id') + + // Supprime une fonction avec son id + .delete(isAuth, isAdmin, adminController.deleteFunction); module.exports = AdminRouter; \ No newline at end of file diff --git a/website/components/FunctionsList/FunctionsList.js b/website/components/FunctionsList/FunctionsList.js index faaa532..abdef26 100644 --- a/website/components/FunctionsList/FunctionsList.js +++ b/website/components/FunctionsList/FunctionsList.js @@ -55,7 +55,12 @@ const FunctionsList = (props) => { const getFunctionsData = () => { setLoadingFunctions(true); return new Promise(async (next) => { - const result = await api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`); + const URL = `${(props.isAdmin) ? "/admin/functions" : "/functions"}?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`; + const result = await api.get(URL, { + headers: { + ...(props.isAdmin && props.token != undefined) && { 'Authorization': props.token } + } + }); setLoadingFunctions(false); next(result.data); }); diff --git a/website/pages/admin/addFunction.js b/website/pages/admin/addFunction.js deleted file mode 100644 index e70e1a5..0000000 --- a/website/pages/admin/addFunction.js +++ /dev/null @@ -1,18 +0,0 @@ -import Cookies from "universal-cookie"; - -const addFunction = (props) => { - return ( -

Crée une nouvelle fonction

- ); -} - -export async function getServerSideProps({ req }) { - const cookies = new Cookies(req.headers.cookie); - return { - props: { - user: { ...cookies.get('user') } - } - }; -} - -export default addFunction; \ No newline at end of file diff --git a/website/pages/admin/index.js b/website/pages/admin/index.js index 89ec17a..cc22b3b 100644 --- a/website/pages/admin/index.js +++ b/website/pages/admin/index.js @@ -1,12 +1,66 @@ -import { Fragment } from 'react'; -import Link from 'next/link'; +import { Fragment, useState, useEffect } from 'react'; import Cookies from "universal-cookie"; import HeadTag from '../../components/HeadTag'; import FunctionsList from '../../components/FunctionsList/FunctionsList'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import Modal from '../../components/Modal'; import redirect from '../../utils/redirect'; +import htmlParser from 'html-react-parser'; +import Loader from '../../components/Loader'; +import useAPI from '../../hooks/useAPI'; +import '../../public/css/pages/admin.css'; +import api from '../../utils/api'; const Admin = (props) => { + const [, categories] = useAPI('/categories'); + const [isOpen, setIsOpen] = useState(false); + const [inputState, setInputState] = useState({ type: 'form' }); + const [message, setMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (categories.length > 0) { + handleChange({ + target: { + name: "categorieId", + value: categories[0].id + } + }); + } + }, [categories]); + + const toggleModal = () => setIsOpen(!isOpen); + + const handleChange = (event, isTypeCheck = false) => { + const inputStateNew = { ...inputState }; + inputStateNew[event.target.name] = (event.target.files != undefined) ? event.target.files[0] : (isTypeCheck) ? event.target.checked : event.target.value; + setInputState(inputStateNew); + } + + const handleSubmit = (event) => { + event.preventDefault(); + setIsLoading(true); + const formData = new FormData(); + formData.append('type', inputState.type); + formData.append('categorieId', inputState.categorieId); + formData.append('title', inputState.title); + formData.append('slug', inputState.slug); + formData.append('description', inputState.description); + formData.append('image', inputState.image); + + api.post('/admin/functions', formData, { headers: { 'Authorization': props.user.token } }) + .then((response) => { + setMessage(`

Succès: ${response.data.message}

`); + setIsLoading(false); + }) + .catch((error) => { + setMessage(`

Erreur: ${error.response.data.message}

`); + setIsLoading(false); + }); + } + if (!props.user.isAdmin && typeof window != 'undefined') { return redirect({}, '/404'); } @@ -15,14 +69,89 @@ const Admin = (props) => { - -
-

Administration

- - - -
-
+ {/* Création d'une fonction */} + {(isOpen) ? + +
+
+
+
+
+ + + +

Crée une nouvelle fonction

+
+
+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+ { + (isLoading) ? + + : + htmlParser(message) + } +
+
+
+
+
+ + : + + +
+

Administration

+ +
+
+ }
); } diff --git a/website/pages/profile/[name].js b/website/pages/profile/[name].js index 1c08cf0..96211aa 100644 --- a/website/pages/profile/[name].js +++ b/website/pages/profile/[name].js @@ -70,137 +70,140 @@ const Profile = (props) => { {/* Édition du profil */} - {(isOpen) && - -
-
-
-
-
- - - -

Éditer le profil

-

(Vous devrez vous reconnecter après la sauvegarde)
Si vous changez votre adresse email, vous devrez la confirmer comme à l'inscription (vérifier vos emails).

+ {(isOpen) ? + +
+
+
+
+
+ + + +

Éditer le profil

+

(Vous devrez vous reconnecter après la sauvegarde)
Si vous changez votre adresse email, vous devrez la confirmer comme à l'inscription (vérifier vos emails).

+
+
+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ handleChange(event, true)} type="checkbox" name="isPublicEmail" checked={inputState.isPublicEmail} className="custom-control-input" id="isPublicEmail" /> + +
+ +
+ + +
+ +
+ +

Si aucun fichier est choisi, le logo ne sera pas modifié.

+ +
+ +
+ +
+
+
+ { + (isLoading) ? + + : + htmlParser(message) + }
+
+
-
-
-
- - + : + +
+
+
+
+

Profil de {props.name}

+
+
+ +
+ {props.name}
-
- - +
+ {(props.biography != undefined) &&

{props.biography}

} + {(props.email != undefined) &&

Email : {props.email}

} +

Date de création : {publicationDate}

-
- handleChange(event, true)} type="checkbox" name="isPublicEmail" checked={inputState.isPublicEmail} className="custom-control-input" id="isPublicEmail" /> - -
- -
- - -
- -
- -

Si aucun fichier est choisi, le logo ne sera pas modifié.

- -
- -
- -
- -
- { - (isLoading) ? - - : - htmlParser(message) + {(isAuth && user.name === props.name) && + }
-
- - } - -
-
-
-
-

Profil de {props.name}

-
-
- -
- {props.name} -
- -
- {(props.biography != undefined) &&

{props.biography}

} - {(props.email != undefined) &&

Email : {props.email}

} -

Date de création : {publicationDate}

-
- - {(isAuth && user.name === props.name) && - - } -
-
-
- - {(props.favoritesArray.length > 0) && -
-
-

Fonctions en favoris :

-
-
+ + {(props.favoritesArray.length > 0) &&
- {props.favoritesArray.map((favorite) => { - return ( - - ); - })} -
-
-
- } - - {(props.commentsArray.length > 0) && -
-
-

Derniers commentaires :

-
-
- {props.commentsArray.map((comment) => ( -
-
-

- Posté sur la fonction  - - {comment.function.title} - -  le {date.format(new Date(comment.createdAt), 'DD/MM/YYYY à HH:mm', true)} -

-

"{comment.message}"

+
+

Fonctions en favoris :

+
+
+
+ {props.favoritesArray.map((favorite) => { + return ( + + ); + })}
- ))} -
+
+ } + + {(props.commentsArray.length > 0) && +
+
+

Derniers commentaires :

+
+
+ {props.commentsArray.map((comment) => ( +
+
+

+ Posté sur la fonction  + + {comment.function.title} + +  le {date.format(new Date(comment.createdAt), 'DD/MM/YYYY à HH:mm', true)} +

+

"{comment.message}"

+
+
+ ))} +
+
+ }
- } -
+ } + ); } diff --git a/website/public/css/pages/admin.css b/website/public/css/pages/admin.css new file mode 100644 index 0000000..fb9aa9b --- /dev/null +++ b/website/public/css/pages/admin.css @@ -0,0 +1,22 @@ +.Admin__Modal__container { + display: flex; + flex-direction: column; + justify-content: center; + margin: 30px 0 0 0; +} +.Admin__Modal__row { + display: flex; + align-items: center; + flex-direction: column; + word-wrap: break-word; + box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25); + border: 1px solid black; + border-radius: 1rem; + margin-bottom: 50px; +} +.Admin__Modal-top-container { + margin: 20px 0; +} +.Admin__Modal-select-option { + color: rgb(221, 220, 220); +} \ No newline at end of file