📦 NEW: randomQuote page personnalisée
This commit is contained in:
		| @@ -1,4 +1,65 @@ | ||||
| exports.emailTemplate = (subtitle, buttonText, url, footerText) => ` | ||||
| exports.emailQuoteTemplate = (isValid, quote, frontendLink) => ` | ||||
| <center> | ||||
|     <table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818"> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td align="center" valign="top"> | ||||
|                     <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px"> | ||||
|                         <tbody> | ||||
|                             <tr> | ||||
|                                 <td align="center" valign="top"> | ||||
|                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px"> | ||||
|                                         <tbody> | ||||
|                                             <tr> | ||||
|                                                 <td> | ||||
|                                                     <h1 style="font-family:Arial, Helvetica, sans-serif;color:#ffd800;font-size:28px;line-height:110%;margin-bottom:30px;margin-top:0;padding:0">FunctionProject</h1> | ||||
|                                                 </td> | ||||
|                                             </tr> | ||||
|                                         </tbody> | ||||
|                                     </table> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                             <tr> | ||||
|                                 <td align="center" valign="top"> | ||||
|                                     <table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px;"> | ||||
|                                         <tbody> | ||||
|                                             <tr> | ||||
|                                                 <td align="left" valign="top" style="line-height:150%;font-family:Helvetica;font-size:14px;color:rgb(222, 222, 222);padding:30px;box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25);border: 1px solid black;border-radius: 1rem;"> | ||||
|                                                     <h2 style="font-size:22px;line-height:28px;margin:0 0 12px 0;"> | ||||
|                                                         La citation que vous avez proposée a été ${(isValid) ? "validée" : "supprimée"}. | ||||
|                                                     </h2> | ||||
|                                                     <p style="margin: 0 0 12px 0;"> | ||||
|                                                         <a style="color: #ffd800;" href="${frontendLink}/functions/randomQuote">Lien vers la fonction randomQuote de FunctionProject.</a> | ||||
|                                                     </p> | ||||
|                                                     ${(!isValid) ? ` | ||||
|                                                         <p style="margin: 0 0 12px 0;"> | ||||
|                                                             Si votre citation a été supprimée et vous pensez que c'est une erreur, contactez-moi à cette adresse email : <a style="color: #ffd800;" href="mailto:contact@divlo.fr">contact@divlo.fr</a>. | ||||
|                                                         </p> | ||||
|                                                     ` : ""} | ||||
|                                                     <div> | ||||
|                                                         <p style="padding:0 0 10px 0"> | ||||
|                                                             La citation en question : <br/> | ||||
|                                                             " ${quote.quote} " | ||||
|                                                             <br/> | ||||
|                                                             - ${quote.author} | ||||
|                                                         </p> | ||||
|                                                     </div> | ||||
|                                                 </td> | ||||
|                                             </tr> | ||||
|                                         </tbody> | ||||
|                                     </table> | ||||
|                                 </td> | ||||
|                             </tr> | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                 </td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
| </center> | ||||
| `; | ||||
|  | ||||
| exports.emailUserTemplate = (subtitle, buttonText, url, footerText) => ` | ||||
| <center> | ||||
|     <table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818"> | ||||
|         <tbody> | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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);    | ||||
|   | ||||
| @@ -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." }); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
							
								
								
									
										8
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								website/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -56,6 +56,9 @@ const Admin = (props) => { | ||||
|                             <Link href={"/admin/manageCategories"}> | ||||
|                                 <button style={{ margin: '0 0 0 20px' }} className="btn btn-dark">Gérer les catégories</button> | ||||
|                             </Link> | ||||
|                             <Link href={"/admin/manageQuotes"}> | ||||
|                                 <button style={{ margin: '0 0 0 20px' }} className="btn btn-dark">Gérer les citations</button> | ||||
|                             </Link> | ||||
|                         </div> | ||||
|                     </FunctionsList> | ||||
|             } | ||||
|   | ||||
							
								
								
									
										124
									
								
								website/pages/admin/manageQuotes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								website/pages/admin/manageQuotes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|         <Fragment> | ||||
|             <HeadTag title="Admin - FunctionProject" description="Page d'administration de FunctionProject. Gérer les citations." /> | ||||
|  | ||||
|             <div className="container-fluid"> | ||||
|                 <div className="row justify-content-center"> | ||||
|                     <div className="col-24 text-center"> | ||||
|                         <h2>Liste des citations (non validées) : </h2> | ||||
|                         <p style={{ marginTop: '5px' }}>Total de {quotesData.totalItems} citations.</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div className="row" style={{ marginBottom: '30px' }}> | ||||
|                     <div className="col-24 Admin__table-column"> | ||||
|                         <table className="Admin__table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th className="Admin__table-row" scope="col">Citation/Proverbe</th> | ||||
|                                     <th className="Admin__table-row" scope="col">Auteur</th> | ||||
|                                     <th className="Admin__table-row" scope="col">Proposée par</th> | ||||
|                                     <th className="Admin__table-row" scope="col">Valider</th> | ||||
|                                     <th className="Admin__table-row" scope="col">Supprimer</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 {quotesData.rows.map((currentQuote, index) => { | ||||
|                                     const quoteJSX = ( | ||||
|                                         <Fragment> | ||||
|                                                 <td className="Admin__table-row text-center">{currentQuote.quote}</td> | ||||
|                                                 <td className="Admin__table-row text-center">{currentQuote.author}</td> | ||||
|                                                 <td className="Admin__table-row text-center"> | ||||
|                                                     <Link href={"/profile/[name]"} as={`/profile/${currentQuote.user.name}`}> | ||||
|                                                         <a>{currentQuote.user.name}</a> | ||||
|                                                     </Link> | ||||
|                                                 </td> | ||||
|                                                 <td onClick={() => handleValidationQuote(currentQuote.id, true)} className="Admin__table-row text-center" style={{ cursor: 'pointer' }}> | ||||
|                                                     <FontAwesomeIcon icon={faCheck} style={{ width: '1.5rem' }} /> | ||||
|                                                 </td> | ||||
|                                                 <td onClick={() => handleValidationQuote(currentQuote.id, false)} className="Admin__table-row text-center" style={{ cursor: 'pointer' }}> | ||||
|                                                     <FontAwesomeIcon icon={faTrash} style={{ width: '1.5rem' }} /> | ||||
|                                                 </td> | ||||
|                                         </Fragment> | ||||
|                                     ); | ||||
|                                     // Si c'est le dernier élément | ||||
|                                     if (quotesData.rows.length === index + 1) { | ||||
|                                         return <tr key={index} ref={lastQuoteRef}>{quoteJSX}</tr> | ||||
|                                     } | ||||
|                                     return <tr key={index}>{quoteJSX}</tr> | ||||
|                                 })} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </Fragment> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export async function getServerSideProps({ req }) { | ||||
|     const cookies = new Cookies(req.headers.cookie); | ||||
|     return { | ||||
|         props: {  | ||||
|             user: { ...cookies.get('user') } | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default manageQuotes; | ||||
							
								
								
									
										341
									
								
								website/pages/functions/randomQuote.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								website/pages/functions/randomQuote.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|         <div className="container"> | ||||
|             <div className="row justify-content-center"> | ||||
|                 <ul className="FunctionTabs__nav"> | ||||
|                     <li className="FunctionTabs__nav-item"> | ||||
|                         <a  | ||||
|                             className={`FunctionTabs__nav-link ${(props.slideIndex === 0) ? "FunctionTabs__nav-link-active" : ""}`}  | ||||
|                             onClick={() => props.setSlideIndex(0)} | ||||
|                         > | ||||
|                             ⚙️ Utilisation | ||||
|                         </a> | ||||
|                     </li> | ||||
|                     <li className="FunctionTabs__nav-item"> | ||||
|                         <a  | ||||
|                             className={`FunctionTabs__nav-link ${(props.slideIndex === 1) ? "FunctionTabs__nav-link-active" : ""}`}  | ||||
|                             onClick={() => props.setSlideIndex(1)} | ||||
|                         > | ||||
|                             📜 Liste | ||||
|                         </a> | ||||
|                     </li> | ||||
|                     <li className="FunctionTabs__nav-item"> | ||||
|                         <a  | ||||
|                             className={`FunctionTabs__nav-link ${(props.slideIndex === 2) ? "FunctionTabs__nav-link-active" : ""}`}  | ||||
|                             onClick={() => props.setSlideIndex(2)} | ||||
|                         > | ||||
|                             ✒️ Proposer | ||||
|                         </a> | ||||
|                     </li> | ||||
|                     <li className="FunctionTabs__nav-item"> | ||||
|                         <a  | ||||
|                             className={`FunctionTabs__nav-link ${(props.slideIndex === 3) ? "FunctionTabs__nav-link-active" : ""}`}  | ||||
|                             onClick={() => props.setSlideIndex(3)} | ||||
|                         > | ||||
|                             📝 Article | ||||
|                         </a> | ||||
|                     </li> | ||||
|                     <li className="FunctionTabs__nav-item"> | ||||
|                         <a  | ||||
|                             className={`FunctionTabs__nav-link ${(props.slideIndex === 4) ? "FunctionTabs__nav-link-active" : ""}`}  | ||||
|                             onClick={() => props.setSlideIndex(4)} | ||||
|                         > | ||||
|                             📬 Commentaires | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| const FunctionTabs = (props) => { | ||||
|     return ( | ||||
|         <div className="container-fluid"> | ||||
|             <SwipeableViews onChangeIndex={(index) => props.setSlideIndex(index)} index={props.slideIndex} enableMouseEvents> | ||||
|                 {props.children} | ||||
|             </SwipeableViews> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| 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 ( | ||||
|         <div className="container-fluid"> | ||||
|             <div className="row justify-content-center"> | ||||
|                 <div className="col-24 text-center"> | ||||
|                     <button onClick={getRandomQuote} className="btn btn-dark">Générer une nouvelle citation</button> | ||||
|                     <button style={{ marginLeft: '15px' }} onClick={handleCopyQuote} className="btn btn-dark">Copier la citation</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div style={{ marginTop: '20px' }} className="row justify-content-center"> | ||||
|                 <div className="col-24 text-center"> | ||||
|                     <p>" {quote.quote} "</p> | ||||
|                     <p>- {quote.author}</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div style={{ marginBottom: '20px' }} className="row justify-content-center"> | ||||
|                 <a  | ||||
|                     target="_blank" | ||||
|                     rel="noopener noreferrer" | ||||
|                     href={`https://twitter.com/intent/tweet?text="${quote.quote}" - ${quote.author}&via=Divlo_FR&hashtags=citation,FunctionProject&url=https://function.divlo.fr/functions/randomQuote`} | ||||
|                     className="btn btn-lg btn-primary" | ||||
|                 > | ||||
|                     <FontAwesomeIcon icon={faTwitter} style={{ width: '1em' }} /> Twitter | ||||
|                 </a> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| 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 ( | ||||
|         <div className="container-fluid"> | ||||
|             <div className="row justify-content-center"> | ||||
|                 <div className="col-24 text-center"> | ||||
|                     <h2 style={{ margin: 0 }}>Liste des citations : </h2> | ||||
|                     <p style={{ marginTop: '5px' }}>Total de {quotesData.totalItems} citations.</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div className="row" style={{ marginBottom: '30px' }}> | ||||
|                 <div className="col-24 table-column"> | ||||
|                     <table> | ||||
|                         <thead> | ||||
|                             <tr> | ||||
|                                 <th className="table-row" scope="col">Citation/Proverbe</th> | ||||
|                                 <th className="table-row" scope="col">Auteur</th> | ||||
|                                 <th className="table-row" scope="col">Proposée par</th> | ||||
|                             </tr> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
|                             {quotesData.rows.map((currentQuote, index) => { | ||||
|                                 const quoteJSX = ( | ||||
|                                     <Fragment> | ||||
|                                             <td className="table-row text-center">{currentQuote.quote}</td> | ||||
|                                             <td className="table-row text-center">{currentQuote.author}</td> | ||||
|                                             <td className="table-row text-center"> | ||||
|                                                 <Link href={"/profile/[name]"} as={`/profile/${currentQuote.user.name}`}> | ||||
|                                                     <a>{currentQuote.user.name}</a> | ||||
|                                                 </Link> | ||||
|                                             </td> | ||||
|                                     </Fragment> | ||||
|                                 ); | ||||
|                                 // Si c'est le dernier élément | ||||
|                                 if (quotesData.rows.length === index + 1) { | ||||
|                                     return <tr key={index} ref={lastQuoteRef}>{quoteJSX}</tr> | ||||
|                                 } | ||||
|                                 return <tr key={index}>{quoteJSX}</tr> | ||||
|                             })} | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| 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(`<p class="form-success"><b>Succès:</b> ${data.message}</p>`); | ||||
|                     setIsLoading(false); | ||||
|                 }) | ||||
|                 .catch((error) => { | ||||
|                     setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`); | ||||
|                     setIsLoading(false); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className="container-fluid"> | ||||
|             { | ||||
|                 (isAuth) ? | ||||
|                     <Fragment>                         | ||||
|                         <div className="row justify-content-center"> | ||||
|                             <div className="col-24 text-center"> | ||||
|                                 <h2 style={{ margin: 0 }}>Proposer une citation : </h2> | ||||
|                                 <p style={{ marginTop: '5px' }}>Vous pouvez proposer des citations, et une fois validé elles seront rajoutés à la liste des citations.</p> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div style={{ marginBottom: '40px' }} className="row"> | ||||
|                             <div className="col-24"> | ||||
|                                 <form onSubmit={handleSubmit}> | ||||
|                                     <div className="form-group"> | ||||
|                                         <label htmlFor="quote" className="form-label">Citation :</label> | ||||
|                                         <textarea value={inputState.quote} onChange={handleChange} style={{ height: 'auto' }} id="quote" name="quote" type="text" className="form-control" rows="4" placeholder="La citation..." /> | ||||
|                                     </div> | ||||
|                                      | ||||
|                                     <div className="form-group"> | ||||
|                                         <label htmlFor="author" className="form-label">Auteur :</label> | ||||
|                                         <input value={inputState.author} onChange={handleChange} name="author" id="author" type="text" className="form-control" placeholder="L'auteur de la citation..." /> | ||||
|                                     </div> | ||||
|  | ||||
|                                     <div className="form-group text-center"> | ||||
|                                         <button type="submit" className="btn btn-dark">Envoyer</button> | ||||
|                                     </div> | ||||
|                                 </form> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div className="form-result text-center"> | ||||
|                             { | ||||
|                                 (isLoading) ?  | ||||
|                                     <Loader /> | ||||
|                                 : | ||||
|                                     htmlParser(message) | ||||
|                             } | ||||
|                         </div> | ||||
|                     </Fragment> | ||||
|                 : | ||||
|                 <p className="text-center"> | ||||
|                     Vous devez être <Link href={'/login'}><a>connecté</a></Link> pour proposer une citation. | ||||
|                 </p> | ||||
|             } | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| const FunctionTabManager = (props) => { | ||||
|     return ( | ||||
|         <FunctionTabs setSlideIndex={props.setSlideIndex} slideIndex={props.slideIndex}> | ||||
|             <div className="FunctionComponent__slide"> | ||||
|                 <GenerateQuote /> | ||||
|             </div> | ||||
|             <div className="FunctionComponent__slide"> | ||||
|                 <QuoteList /> | ||||
|             </div> | ||||
|             <div className="FunctionComponent__slide"> | ||||
|                 <SuggestQuote /> | ||||
|             </div> | ||||
|             <div className="FunctionComponent__slide"> | ||||
|                 <FunctionArticle article={props.article} /> | ||||
|             </div> | ||||
|             <div className="FunctionComponent__slide"> | ||||
|                 <FunctionComments functionId={props.id} /> | ||||
|             </div> | ||||
|         </FunctionTabs> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| const randomQuote = (props) => { | ||||
|  | ||||
|     const [slideIndex, setSlideIndex] = useState(0); | ||||
|  | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <HeadTag title={props.title} description={props.description} image={API_URL + props.image} /> | ||||
|  | ||||
|             <div className="container-fluid"> | ||||
|                 <FunctionTabsTop slideIndex={slideIndex} setSlideIndex={setSlideIndex} /> | ||||
|                 <FunctionComponentTop { ...props } /> | ||||
|                 <FunctionTabManager { ...props } slideIndex={slideIndex} setSlideIndex={setSlideIndex} /> | ||||
|             </div> | ||||
|         </Fragment> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export async function getServerSideProps(context) { | ||||
|     return api.get(`/functions/randomQuote`) | ||||
|         .then((response) => ({ props: response.data })) | ||||
|         .catch(() => redirect(context, '/404')); | ||||
| } | ||||
|  | ||||
| export default randomQuote; | ||||
| @@ -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(`<p class="form-success"><b>Succès:</b> ${data.result}</p>`); | ||||
|                 setIsLoading(false); | ||||
|                 setInputState({}); | ||||
|             }) | ||||
|             .catch((error) => { | ||||
|                 setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`); | ||||
| @@ -46,17 +46,17 @@ const Register = () => { | ||||
|                         <form onSubmit={handleSubmit}> | ||||
|                             <div className="form-group"> | ||||
|                                 <label className="form-label" htmlFor="name">Nom :</label> | ||||
|                                 <input onChange={handleChange} type="text" name="name" id="name" className="form-control" placeholder="Divlo" /> | ||||
|                                 <input value={inputState.name} onChange={handleChange} type="text" name="name" id="name" className="form-control" placeholder="Divlo" /> | ||||
|                             </div> | ||||
|  | ||||
|                             <div className="form-group"> | ||||
|                                 <label className="form-label" htmlFor="name">Email :</label> | ||||
|                                 <input onChange={handleChange} type="email" name="email" id="email" className="form-control" placeholder="email@gmail.com" /> | ||||
|                                 <input value={inputState.email} onChange={handleChange} type="email" name="email" id="email" className="form-control" placeholder="email@gmail.com" /> | ||||
|                             </div> | ||||
|  | ||||
|                             <div className="form-group"> | ||||
|                                 <label className="form-label" htmlFor="name">Mot de passe :</label> | ||||
|                                 <input onChange={handleChange} type="password" name="password" id="password" className="form-control" placeholder="******" /> | ||||
|                                 <input value={inputState.password} onChange={handleChange} type="password" name="password" id="password" className="form-control" placeholder="******" /> | ||||
|                             </div> | ||||
|  | ||||
|                             <div className="form-group text-center"> | ||||
|   | ||||
| @@ -129,6 +129,12 @@ a, .important { | ||||
|     border-radius: .25rem; | ||||
|     transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; | ||||
| } | ||||
| .btn-lg { | ||||
|     padding: .5rem 1rem; | ||||
|     font-size: 1.25rem; | ||||
|     line-height: 1.5; | ||||
|     border-radius: .3rem; | ||||
| } | ||||
| .btn-dark:hover { | ||||
|     color: #fff; | ||||
|     background-color: #23272b; | ||||
| @@ -139,6 +145,16 @@ a, .important { | ||||
|     background-color: #343a40; | ||||
|     border-color: #343a40; | ||||
| } | ||||
| .btn-primary:hover { | ||||
|     color: #fff; | ||||
|     background-color: #0069d9; | ||||
|     border-color: #0062cc; | ||||
| } | ||||
| .btn-primary { | ||||
|     color: #fff; | ||||
|     background-color: #007bff; | ||||
|     border-color: #007bff; | ||||
| } | ||||
| .custom-control { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|   | ||||
| @@ -37,4 +37,14 @@ | ||||
|     border-radius: 10px; | ||||
|     overflow: auto; | ||||
|     white-space: normal !important; | ||||
| } | ||||
| .table-column { | ||||
|     display: grid; | ||||
| } | ||||
| .table, th, td { | ||||
|     border: 1px solid var(--text-color); | ||||
|     border-collapse: collapse; | ||||
| } | ||||
| .table-row { | ||||
|     padding: 15px; | ||||
| } | ||||
							
								
								
									
										10
									
								
								website/utils/copyToClipboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								website/utils/copyToClipboard.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| function copyToClipboard(text) { | ||||
|     const elem = document.createElement('textarea'); | ||||
|     elem.value = text; | ||||
|     document.body.appendChild(elem); | ||||
|     elem.select(); | ||||
|     document.execCommand('copy'); | ||||
|     document.body.removeChild(elem); | ||||
| } | ||||
|  | ||||
| export default copyToClipboard; | ||||
		Reference in New Issue
	
	Block a user