diff --git a/api/assets/functions/main/randomQuote.js b/api/assets/functions/main/randomQuote.js
index 66c3027..789c3b0 100644
--- a/api/assets/functions/main/randomQuote.js
+++ b/api/assets/functions/main/randomQuote.js
@@ -14,6 +14,9 @@ module.exports = randomQuote = async ({ res, next }, _argsObject) => {
attributes: {
exclude: ["isValidated"]
},
+ where: {
+ isValidated: 1,
+ }
});
return res.status(200).json(quote);
} catch (error) {
diff --git a/api/controllers/admin.js b/api/controllers/admin.js
index 1495f6e..ad4e47c 100644
--- a/api/controllers/admin.js
+++ b/api/controllers/admin.js
@@ -1,15 +1,18 @@
-const path = require('path');
-const fs = require('fs');
-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 Quotes = require('../models/quotes');
-const Users = require('../models/users');
-const helperQueryNumber = require('../assets/utils/helperQueryNumber');
-const Sequelize = require('sequelize');
-const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameStartWith');
+const path = require('path');
+const fs = require('fs');
+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 Quotes = require('../models/quotes');
+const Users = require('../models/users');
+const helperQueryNumber = require('../assets/utils/helperQueryNumber');
+const Sequelize = require('sequelize');
+const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameStartWith');
+const { EMAIL_INFO, FRONT_END_HOST } = require('../assets/config/config');
+const transporter = require('../assets/config/transporter');
+const { emailQuoteTemplate } = require('../assets/config/emails');
const handleEditFunction = async (res, resultFunction, { title, slug, description, type, categorieId, isOnline }, imageName = false) => {
resultFunction.title = title;
@@ -305,18 +308,35 @@ exports.putQuote = async (req, res, next) => {
if (typeof isValid !== 'boolean') {
return errorHandling(next, { message: "isValid doit être un booléen.", statusCode: 400 });
}
- const quote = await Quotes.findOne({ where: { id, isValidated: 0 } });
+ const quote = await Quotes.findOne({
+ where: {
+ id,
+ isValidated: 0
+ },
+ include: [
+ { model: Users, attributes: ["name", "email"] }
+ ]
+ });
if (!quote) {
return errorHandling(next, { message: "La citation n'existe pas (ou est déjà validé).", statusCode: 404 });
}
+
+ await transporter.sendMail({
+ from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
+ to: quote.user.email,
+ subject: "FunctionProject - Citation proposée",
+ html: emailQuoteTemplate(isValid, quote, FRONT_END_HOST)
+ });
+
if (isValid) {
quote.isValidated = true;
- const result = await quote.save();
- return res.status(200).json({ isValidated: true, message: "La citation a bien été validée!", result });
- }
-
- await quote.destroy();
- return res.status(200).json({ isValidated: false, message: "La citation a bien été supprimée!" });
+ await quote.save();
+ return res.status(200).json({ message: "La citation a bien été validée!" });
+ } else {
+ await quote.destroy();
+ return res.status(200).json({ imessage: "La citation a bien été supprimée!" });
+ }
+
} catch (error) {
console.log(error);
return errorHandling(next, serverError);
diff --git a/api/controllers/quotes.js b/api/controllers/quotes.js
index dd88681..7cc6e40 100644
--- a/api/controllers/quotes.js
+++ b/api/controllers/quotes.js
@@ -1,8 +1,8 @@
-const errorHandling = require('../assets/utils/errorHandling');
-const { serverError } = require('../assets/config/errors');
-const Quotes = require('../models/quotes');
-const Users = require('../models/users');
-const helperQueryNumber = require('../assets/utils/helperQueryNumber');
+const errorHandling = require('../assets/utils/errorHandling');
+const { serverError, requiredFields } = require('../assets/config/errors');
+const Quotes = require('../models/quotes');
+const Users = require('../models/users');
+const helperQueryNumber = require('../assets/utils/helperQueryNumber');
exports.getQuotes = (req, res, next) => {
const page = helperQueryNumber(req.query.page, 1);
@@ -35,6 +35,10 @@ exports.getQuotes = (req, res, next) => {
exports.postQuote = (req, res, next) => {
const { quote, author } = req.body;
+ // S'il n'y a pas les champs obligatoire
+ if (!(quote && author)) {
+ return errorHandling(next, requiredFields);
+ }
Quotes.create({ quote, author, userId: req.userId })
.then((_result) => {
return res.status(200).json({ message: "La citation a bien été ajoutée, elle est en attente de confirmation d'un administrateur." });
diff --git a/api/controllers/users.js b/api/controllers/users.js
index 8b6e8c9..80b8747 100644
--- a/api/controllers/users.js
+++ b/api/controllers/users.js
@@ -8,7 +8,7 @@ const errorHandling = require('..
const { serverError, generalError } = require('../assets/config/errors');
const { JWT_SECRET, FRONT_END_HOST, EMAIL_INFO, HOST, TOKEN_LIFE } = require('../assets/config/config');
const transporter = require('../assets/config/transporter');
-const { emailTemplate } = require('../assets/config/emails');
+const { emailUserTemplate } = require('../assets/config/emails');
const Users = require('../models/users');
const Favorites = require('../models/favorites');
const Functions = require('../models/functions');
@@ -28,7 +28,7 @@ async function handleEditUser(res, { name, email, biography, isPublicEmail }, us
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: "FunctionProject - Confirmer l'email",
- html: emailTemplate("Veuillez confirmer l'email", "Oui, je confirme.", `${HOST}/users/confirm-email/${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre email ne serez pas confirmé si vous ne cliquez pas sur le lien de confirmation ci-dessus.")
+ html: emailUserTemplate("Veuillez confirmer l'email", "Oui, je confirme.", `${HOST}/users/confirm-email/${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre email ne serez pas confirmé si vous ne cliquez pas sur le lien de confirmation ci-dessus.")
});
}
if (biography != undefined) {
@@ -97,7 +97,7 @@ exports.register = async (req, res, next) => {
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: "FunctionProject - Confirmer l'inscription",
- html: emailTemplate("Veuillez confirmer l'inscription", "Oui, je m'inscris.", `${HOST}/users/confirm-email/${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Vous ne serez pas inscrit si vous ne cliquez pas sur le lien de confirmation ci-dessus.")
+ html: emailUserTemplate("Veuillez confirmer l'inscription", "Oui, je m'inscris.", `${HOST}/users/confirm-email/${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Vous ne serez pas inscrit si vous ne cliquez pas sur le lien de confirmation ci-dessus.")
});
return res.status(201).json({ result: "Vous y êtes presque, veuillez vérifier vos emails pour confirmer l'inscription." });
} catch (error) {
@@ -173,7 +173,7 @@ exports.resetPassword = async (req, res, next) => {
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: "FunctionProject - Réinitialisation du mot de passe",
- html: emailTemplate("Veuillez confirmer la réinitialisation du mot de passe", "Oui, je change mon mot de passe.", `${FRONT_END_HOST}/newPassword?token=${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre mot de passe ne sera pas réinitialiser si vous ne cliquez pas sur le lien ci-dessus. Par ailleurs, pour la sécurité de votre compte, la réinitialisation du mot de passe est disponible pendant un délai de 1 heure, passez ce temps, la réinitialisation ne sera plus valide.")
+ html: emailUserTemplate("Veuillez confirmer la réinitialisation du mot de passe", "Oui, je change mon mot de passe.", `${FRONT_END_HOST}/newPassword?token=${tempToken}`, "Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre mot de passe ne sera pas réinitialiser si vous ne cliquez pas sur le lien ci-dessus. Par ailleurs, pour la sécurité de votre compte, la réinitialisation du mot de passe est disponible pendant un délai de 1 heure, passez ce temps, la réinitialisation ne sera plus valide.")
});
return res.status(200).json({ result: "Demande de réinitialisation du mot de passe réussi, veuillez vérifier vos emails!" });
} catch (error) {
diff --git a/website/package-lock.json b/website/package-lock.json
index 46d7fa5..5f72ad4 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -1408,6 +1408,14 @@
"@fortawesome/fontawesome-common-types": "^0.2.28"
}
},
+ "@fortawesome/free-brands-svg-icons": {
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.13.0.tgz",
+ "integrity": "sha512-/6xXiJFCMEQxqxXbL0FPJpwq5Cv6MRrjsbJEmH/t5vOvB4dILDpnY0f7zZSlA8+TG7jwlt12miF/yZpZkykucA==",
+ "requires": {
+ "@fortawesome/fontawesome-common-types": "^0.2.28"
+ }
+ },
"@fortawesome/free-regular-svg-icons": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz",
diff --git a/website/package.json b/website/package.json
index d49b381..8e1c6f8 100644
--- a/website/package.json
+++ b/website/package.json
@@ -9,6 +9,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
+ "@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
diff --git a/website/pages/admin/index.js b/website/pages/admin/index.js
index 2835443..a157c0f 100644
--- a/website/pages/admin/index.js
+++ b/website/pages/admin/index.js
@@ -56,6 +56,9 @@ const Admin = (props) => {
+
+
+
}
diff --git a/website/pages/admin/manageQuotes.js b/website/pages/admin/manageQuotes.js
new file mode 100644
index 0000000..997998d
--- /dev/null
+++ b/website/pages/admin/manageQuotes.js
@@ -0,0 +1,124 @@
+import { Fragment, useState, useEffect, useRef, useCallback } from 'react';
+import Cookies from "universal-cookie";
+import Link from 'next/link';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCheck, faTrash } from '@fortawesome/free-solid-svg-icons';
+import redirect from '../../utils/redirect';
+import HeadTag from '../../components/HeadTag';
+import api from '../../utils/api';
+import '../../public/css/pages/admin.css';
+
+const manageQuotes = (props) => {
+
+ const [quotesData, setQuotesData] = useState({ hasMore: true, rows: [], totalItems: 0 });
+ const [isLoadingQuotes, setLoadingQuotes] = useState(true);
+ const [pageQuotes, setPageQuotes] = useState(1);
+
+ // Récupère les citations si la page change
+ useEffect(() => {
+ getQuotesData();
+ }, [pageQuotes]);
+
+ const getQuotesData = async () => {
+ setLoadingQuotes(true);
+ const { data } = await api.get(`/admin/quotes?limit=20page=${pageQuotes}`, { headers: { 'Authorization': props.user.token } });
+ setQuotesData({
+ hasMore: data.hasMore,
+ rows: [...quotesData.rows, ...data.rows],
+ totalItems: data.totalItems
+ });
+ setLoadingQuotes(false);
+ }
+
+ // Permet la pagination au scroll
+ const observer = useRef();
+ const lastQuoteRef = useCallback((node) => {
+ if (isLoadingQuotes) return;
+ if (observer.current) observer.current.disconnect();
+ observer.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && quotesData.hasMore) {
+ setPageQuotes(pageQuotes + 1);
+ }
+ }, { threshold: 1 });
+ if (node) observer.current.observe(node);
+ }, [isLoadingQuotes, quotesData.hasMore]);
+
+ if (!props.user.isAdmin && typeof window != 'undefined') {
+ return redirect({}, '/404');
+ }
+
+ const handleValidationQuote = async (id, isValid) => {
+ try {
+ await api.put(`/admin/quotes/${id}`, { isValid }, { headers: { 'Authorization': props.user.token } });
+ window.location.reload(true);
+ } catch {}
+ }
+
+ return (
+
+
+
+
+
+
+
Liste des citations (non validées) :
+
Total de {quotesData.totalItems} citations.
+
+
+
+
+
+
+
+
+ Citation/Proverbe |
+ Auteur |
+ Proposée par |
+ Valider |
+ Supprimer |
+
+
+
+ {quotesData.rows.map((currentQuote, index) => {
+ const quoteJSX = (
+
+ {currentQuote.quote} |
+ {currentQuote.author} |
+
+
+ {currentQuote.user.name}
+
+ |
+ handleValidationQuote(currentQuote.id, true)} className="Admin__table-row text-center" style={{ cursor: 'pointer' }}>
+
+ |
+ handleValidationQuote(currentQuote.id, false)} className="Admin__table-row text-center" style={{ cursor: 'pointer' }}>
+
+ |
+
+ );
+ // Si c'est le dernier élément
+ if (quotesData.rows.length === index + 1) {
+ return {quoteJSX}
+ }
+ return {quoteJSX}
+ })}
+
+
+
+
+
+
+ );
+}
+
+export async function getServerSideProps({ req }) {
+ const cookies = new Cookies(req.headers.cookie);
+ return {
+ props: {
+ user: { ...cookies.get('user') }
+ }
+ };
+}
+
+export default manageQuotes;
\ No newline at end of file
diff --git a/website/pages/functions/randomQuote.js b/website/pages/functions/randomQuote.js
new file mode 100644
index 0000000..e6543cd
--- /dev/null
+++ b/website/pages/functions/randomQuote.js
@@ -0,0 +1,341 @@
+import { Fragment, useState, useEffect, useContext, useRef, useCallback } from 'react';
+import HeadTag from '../../components/HeadTag';
+import Link from 'next/link';
+import { UserContext } from '../../contexts/UserContext';
+import FunctionComponentTop from '../../components/FunctionComponentTop';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTwitter } from '@fortawesome/free-brands-svg-icons';
+import SwipeableViews from 'react-swipeable-views';
+import redirect from '../../utils/redirect';
+import FunctionArticle from '../../components/FunctionArticle';
+import FunctionComments from '../../components/FunctionComments/FunctionComments';
+import htmlParser from 'html-react-parser';
+import Loader from '../../components/Loader';
+import api from '../../utils/api';
+import copyToClipboard from '../../utils/copyToClipboard';
+import { API_URL } from '../../utils/config';
+import '../../public/css/pages/FunctionComponent.css';
+import '../../components/FunctionTabs/FunctionTabs.css';
+import '../../components/FunctionCard/FunctionCard.css';
+import 'notyf/notyf.min.css';
+
+const FunctionTabsTop = (props) => {
+ return (
+
+ );
+}
+
+const FunctionTabs = (props) => {
+ return (
+
+ props.setSlideIndex(index)} index={props.slideIndex} enableMouseEvents>
+ {props.children}
+
+
+ );
+}
+
+const GenerateQuote = () => {
+
+ const [quote, setQuote] = useState({ quote: "", author: "" });
+
+ useEffect(() => {
+ getRandomQuote();
+ }, []);
+
+ const getRandomQuote = async () => {
+ const { data } = await api.post("/functions/randomQuote");
+ setQuote(data);
+ }
+
+ const handleCopyQuote = () => {
+ let Notyf;
+ if (typeof window != 'undefined') {
+ Notyf = require('notyf');
+ }
+ const notyf = new Notyf.Notyf({
+ duration: 5000
+ });
+ copyToClipboard(`"${quote.quote}" - ${quote.author}`);
+ notyf.success('Citation copiée dans le presse-papier!');
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
" {quote.quote} "
+
- {quote.author}
+
+
+
+
+ );
+}
+
+const QuoteList = () => {
+
+ const [quotesData, setQuotesData] = useState({ hasMore: true, rows: [], totalItems: 0 });
+ const [isLoadingQuotes, setLoadingQuotes] = useState(true);
+ const [pageQuotes, setPageQuotes] = useState(1);
+
+ // Récupère les citations si la page change
+ useEffect(() => {
+ getQuotesData();
+ }, [pageQuotes]);
+
+ const getQuotesData = async () => {
+ setLoadingQuotes(true);
+ const { data } = await api.get(`/quotes?limit=20page=${pageQuotes}`);
+ setQuotesData({
+ hasMore: data.hasMore,
+ rows: [...quotesData.rows, ...data.rows],
+ totalItems: data.totalItems
+ });
+ setLoadingQuotes(false);
+ }
+
+ // Permet la pagination au scroll
+ const observer = useRef();
+ const lastQuoteRef = useCallback((node) => {
+ if (isLoadingQuotes) return;
+ if (observer.current) observer.current.disconnect();
+ observer.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && quotesData.hasMore) {
+ setPageQuotes(pageQuotes + 1);
+ }
+ }, { threshold: 1 });
+ if (node) observer.current.observe(node);
+ }, [isLoadingQuotes, quotesData.hasMore]);
+
+ return (
+
+
+
+
Liste des citations :
+
Total de {quotesData.totalItems} citations.
+
+
+
+
+
+
+
+
+ Citation/Proverbe |
+ Auteur |
+ Proposée par |
+
+
+
+ {quotesData.rows.map((currentQuote, index) => {
+ const quoteJSX = (
+
+ {currentQuote.quote} |
+ {currentQuote.author} |
+
+
+ {currentQuote.user.name}
+
+ |
+
+ );
+ // Si c'est le dernier élément
+ if (quotesData.rows.length === index + 1) {
+ return {quoteJSX}
+ }
+ return {quoteJSX}
+ })}
+
+
+
+
+
+ );
+}
+
+const SuggestQuote = () => {
+
+ const { isAuth, user } = useContext(UserContext);
+ const [inputState, setInputState] = useState({ quote: "", author: "" });
+ const [message, setMessage] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleChange = (event) => {
+ const inputStateNew = { ...inputState };
+ inputStateNew[event.target.name] = event.target.value;
+ setInputState(inputStateNew);
+ }
+
+ const handleSubmit = (event) => {
+ setIsLoading(true);
+ event.preventDefault();
+ const token = user.token;
+ if (isAuth && token != undefined) {
+ api.post('/quotes', inputState, { headers: { 'Authorization': token } })
+ .then(({ data }) => {
+ setInputState({ quote: "", author: "" });
+ setMessage(`Succès: ${data.message}
`);
+ setIsLoading(false);
+ })
+ .catch((error) => {
+ setMessage(`Erreur: ${error.response.data.message}
`);
+ setIsLoading(false);
+ });
+ }
+ }
+
+ return (
+
+ {
+ (isAuth) ?
+
+
+
+
Proposer une citation :
+
Vous pouvez proposer des citations, et une fois validé elles seront rajoutés à la liste des citations.
+
+
+
+
+ {
+ (isLoading) ?
+
+ :
+ htmlParser(message)
+ }
+
+
+ :
+
+ Vous devez être connecté pour proposer une citation.
+
+ }
+
+ );
+}
+
+const FunctionTabManager = (props) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const randomQuote = (props) => {
+
+ const [slideIndex, setSlideIndex] = useState(0);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export async function getServerSideProps(context) {
+ return api.get(`/functions/randomQuote`)
+ .then((response) => ({ props: response.data }))
+ .catch(() => redirect(context, '/404'));
+}
+
+export default randomQuote;
\ No newline at end of file
diff --git a/website/pages/register.js b/website/pages/register.js
index 5f30656..6509a96 100644
--- a/website/pages/register.js
+++ b/website/pages/register.js
@@ -8,7 +8,7 @@ import '../public/css/pages/register-login.css';
const Register = () => {
- const [inputState, setInputState] = useState({});
+ const [inputState, setInputState] = useState({ name: "", email: "", password: "" });
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -23,9 +23,9 @@ const Register = () => {
event.preventDefault();
api.post('/users/register', inputState)
.then(({ data }) => {
+ setInputState({ name: "", email: "", password: "" });
setMessage(`Succès: ${data.result}
`);
setIsLoading(false);
- setInputState({});
})
.catch((error) => {
setMessage(`Erreur: ${error.response.data.message}
`);
@@ -46,17 +46,17 @@ const Register = () => {