📦 NEW: randomQuote page personnalisée

This commit is contained in:
Divlo 2020-04-22 20:44:06 +02:00
parent f2df72fe29
commit 43add978b6
14 changed files with 635 additions and 34 deletions

View File

@ -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> <center>
<table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818"> <table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818">
<tbody> <tbody>

View File

@ -14,6 +14,9 @@ module.exports = randomQuote = async ({ res, next }, _argsObject) => {
attributes: { attributes: {
exclude: ["isValidated"] exclude: ["isValidated"]
}, },
where: {
isValidated: 1,
}
}); });
return res.status(200).json(quote); return res.status(200).json(quote);
} catch (error) { } catch (error) {

View File

@ -1,15 +1,18 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { validationResult } = require('express-validator'); const { validationResult } = require('express-validator');
const errorHandling = require('../assets/utils/errorHandling'); const errorHandling = require('../assets/utils/errorHandling');
const { serverError } = require('../assets/config/errors'); const { serverError } = require('../assets/config/errors');
const Functions = require('../models/functions'); const Functions = require('../models/functions');
const Categories = require('../models/categories'); const Categories = require('../models/categories');
const Quotes = require('../models/quotes'); const Quotes = require('../models/quotes');
const Users = require('../models/users'); const Users = require('../models/users');
const helperQueryNumber = require('../assets/utils/helperQueryNumber'); const helperQueryNumber = require('../assets/utils/helperQueryNumber');
const Sequelize = require('sequelize'); const Sequelize = require('sequelize');
const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameStartWith'); 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) => { const handleEditFunction = async (res, resultFunction, { title, slug, description, type, categorieId, isOnline }, imageName = false) => {
resultFunction.title = title; resultFunction.title = title;
@ -305,18 +308,35 @@ exports.putQuote = async (req, res, next) => {
if (typeof isValid !== 'boolean') { if (typeof isValid !== 'boolean') {
return errorHandling(next, { message: "isValid doit être un booléen.", statusCode: 400 }); 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) { if (!quote) {
return errorHandling(next, { message: "La citation n'existe pas (ou est déjà validé).", statusCode: 404 }); 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) { if (isValid) {
quote.isValidated = true; quote.isValidated = true;
const result = await quote.save(); await quote.save();
return res.status(200).json({ isValidated: true, message: "La citation a bien été validée!", result }); return res.status(200).json({ message: "La citation a bien été validée!" });
} } else {
await quote.destroy();
await quote.destroy(); return res.status(200).json({ imessage: "La citation a bien été supprimée!" });
return res.status(200).json({ isValidated: false, message: "La citation a bien été supprimée!" }); }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorHandling(next, serverError); return errorHandling(next, serverError);

View File

@ -1,8 +1,8 @@
const errorHandling = require('../assets/utils/errorHandling'); const errorHandling = require('../assets/utils/errorHandling');
const { serverError } = require('../assets/config/errors'); const { serverError, requiredFields } = require('../assets/config/errors');
const Quotes = require('../models/quotes'); const Quotes = require('../models/quotes');
const Users = require('../models/users'); const Users = require('../models/users');
const helperQueryNumber = require('../assets/utils/helperQueryNumber'); const helperQueryNumber = require('../assets/utils/helperQueryNumber');
exports.getQuotes = (req, res, next) => { exports.getQuotes = (req, res, next) => {
const page = helperQueryNumber(req.query.page, 1); const page = helperQueryNumber(req.query.page, 1);
@ -35,6 +35,10 @@ exports.getQuotes = (req, res, next) => {
exports.postQuote = (req, res, next) => { exports.postQuote = (req, res, next) => {
const { quote, author } = req.body; 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 }) Quotes.create({ quote, author, userId: req.userId })
.then((_result) => { .then((_result) => {
return res.status(200).json({ message: "La citation a bien été ajoutée, elle est en attente de confirmation d'un administrateur." }); return res.status(200).json({ message: "La citation a bien été ajoutée, elle est en attente de confirmation d'un administrateur." });

View File

@ -8,7 +8,7 @@ const errorHandling = require('..
const { serverError, generalError } = require('../assets/config/errors'); const { serverError, generalError } = require('../assets/config/errors');
const { JWT_SECRET, FRONT_END_HOST, EMAIL_INFO, HOST, TOKEN_LIFE } = require('../assets/config/config'); const { JWT_SECRET, FRONT_END_HOST, EMAIL_INFO, HOST, TOKEN_LIFE } = require('../assets/config/config');
const transporter = require('../assets/config/transporter'); const transporter = require('../assets/config/transporter');
const { emailTemplate } = require('../assets/config/emails'); const { emailUserTemplate } = require('../assets/config/emails');
const Users = require('../models/users'); const Users = require('../models/users');
const Favorites = require('../models/favorites'); const Favorites = require('../models/favorites');
const Functions = require('../models/functions'); const Functions = require('../models/functions');
@ -28,7 +28,7 @@ async function handleEditUser(res, { name, email, biography, isPublicEmail }, us
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`, from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email, to: email,
subject: "FunctionProject - Confirmer l'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) { if (biography != undefined) {
@ -97,7 +97,7 @@ exports.register = async (req, res, next) => {
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`, from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email, to: email,
subject: "FunctionProject - Confirmer l'inscription", 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." }); return res.status(201).json({ result: "Vous y êtes presque, veuillez vérifier vos emails pour confirmer l'inscription." });
} catch (error) { } catch (error) {
@ -173,7 +173,7 @@ exports.resetPassword = async (req, res, next) => {
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`, from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email, to: email,
subject: "FunctionProject - Réinitialisation du mot de passe", 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!" }); return res.status(200).json({ result: "Demande de réinitialisation du mot de passe réussi, veuillez vérifier vos emails!" });
} catch (error) { } catch (error) {

View File

@ -1408,6 +1408,14 @@
"@fortawesome/fontawesome-common-types": "^0.2.28" "@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": { "@fortawesome/free-regular-svg-icons": {
"version": "5.13.0", "version": "5.13.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz",

View File

@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28", "@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-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0", "@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9", "@fortawesome/react-fontawesome": "^0.1.9",

View File

@ -56,6 +56,9 @@ const Admin = (props) => {
<Link href={"/admin/manageCategories"}> <Link href={"/admin/manageCategories"}>
<button style={{ margin: '0 0 0 20px' }} className="btn btn-dark">Gérer les catégories</button> <button style={{ margin: '0 0 0 20px' }} className="btn btn-dark">Gérer les catégories</button>
</Link> </Link>
<Link href={"/admin/manageQuotes"}>
<button style={{ margin: '0 0 0 20px' }} className="btn btn-dark">Gérer les citations</button>
</Link>
</div> </div>
</FunctionsList> </FunctionsList>
} }

View 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;

View 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;

View File

@ -8,7 +8,7 @@ import '../public/css/pages/register-login.css';
const Register = () => { const Register = () => {
const [inputState, setInputState] = useState({}); const [inputState, setInputState] = useState({ name: "", email: "", password: "" });
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -23,9 +23,9 @@ const Register = () => {
event.preventDefault(); event.preventDefault();
api.post('/users/register', inputState) api.post('/users/register', inputState)
.then(({ data }) => { .then(({ data }) => {
setInputState({ name: "", email: "", password: "" });
setMessage(`<p class="form-success"><b>Succès:</b> ${data.result}</p>`); setMessage(`<p class="form-success"><b>Succès:</b> ${data.result}</p>`);
setIsLoading(false); setIsLoading(false);
setInputState({});
}) })
.catch((error) => { .catch((error) => {
setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`); setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`);
@ -46,17 +46,17 @@ const Register = () => {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="name">Nom :</label> <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>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="name">Email :</label> <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>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="name">Mot de passe :</label> <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>
<div className="form-group text-center"> <div className="form-group text-center">

View File

@ -129,6 +129,12 @@ a, .important {
border-radius: .25rem; 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; 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 { .btn-dark:hover {
color: #fff; color: #fff;
background-color: #23272b; background-color: #23272b;
@ -139,6 +145,16 @@ a, .important {
background-color: #343a40; background-color: #343a40;
border-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 { .custom-control {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -37,4 +37,14 @@
border-radius: 10px; border-radius: 10px;
overflow: auto; overflow: auto;
white-space: normal !important; white-space: normal !important;
}
.table-column {
display: grid;
}
.table, th, td {
border: 1px solid var(--text-color);
border-collapse: collapse;
}
.table-row {
padding: 15px;
} }

View 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;