frontend: Édition profil + corrections backend

This commit is contained in:
Divlo 2020-04-08 00:47:24 +02:00
parent 3ed605af1b
commit 5d048f3010
11 changed files with 268 additions and 18 deletions

View File

@ -9,7 +9,7 @@ function deleteFilesNameStartWith(pattern, dirPath = __dirname) {
// Iterate through the found file names // Iterate through the found file names
for (const name of fileNames) { for (const name of fileNames) {
// If file name matches the pattern // If file name matches the pattern
if (name.startsWith(pattern)) { if (name.startsWith(pattern) && name !== 'default.png') {
console.log(name) console.log(name)
fs.unlinkSync(path.join(dirPath, name)); fs.unlinkSync(path.join(dirPath, name));
} }

View File

@ -19,14 +19,27 @@ const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameS
async function handleEditUser(res, { name, email, biography, isPublicEmail }, userId, logoName) { async function handleEditUser(res, { name, email, biography, isPublicEmail }, userId, logoName) {
const user = await Users.findOne({ where: { id: userId } }); const user = await Users.findOne({ where: { id: userId } });
user.name = name; user.name = name;
user.email = email; if (user.email !== email) {
user.biography = biography; const tempToken = uuid.v4();
user.email = email;
user.isConfirmed = false;
user.tempToken = tempToken;
await transporter.sendMail({
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.")
});
}
if (biography != undefined) {
user.biography = biography;
}
user.isPublicEmail = isPublicEmail; user.isPublicEmail = isPublicEmail;
if (logoName != undefined) { if (logoName != undefined) {
user.logo = `/images/users/${logoName}`; user.logo = `/images/users/${logoName}`;
} }
await user.save(); await user.save();
return res.status(200).json({ message: "Le profil a bien été modifié!" }); return res.status(200).json({ id: user.id, name: user.name, email: user.email, biography: user.biography, logo: user.logo, isPublicEmail: user.isPublicEmail, isAdmin: user.isAdmin, createdAt: user.createdAt });
} }
exports.putUser = (req, res, next) => { exports.putUser = (req, res, next) => {
@ -115,7 +128,7 @@ exports.login = async (req, res, next) => {
} }
const token = jwt.sign({ const token = jwt.sign({
email: user.email, userId: user.id email: user.email, userId: user.id
}, JWT_SECRET, { expiresIn: '3h' }); }, JWT_SECRET, { expiresIn: '6h' });
return res.status(200).json({ token, id: user.id, name: user.name, email: user.email, biography: user.biography, logo: user.logo, isPublicEmail: user.isPublicEmail, isAdmin: user.isAdmin, createdAt: user.createdAt }); return res.status(200).json({ token, id: user.id, name: user.name, email: user.email, biography: user.biography, logo: user.logo, isPublicEmail: user.isPublicEmail, isAdmin: user.isAdmin, createdAt: user.createdAt });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -198,7 +211,7 @@ exports.getUserInfo = async (req, res, next) => {
const { name } = req.params; const { name } = req.params;
try { try {
const user = await Users.findOne({ const user = await Users.findOne({
where: { name }, where: { name, isConfirmed: true },
attributes: { attributes: {
exclude: ["updatedAt", "isAdmin", "isConfirmed", "password", "tempToken", "tempExpirationToken"] exclude: ["updatedAt", "isAdmin", "isConfirmed", "password", "tempToken", "tempExpirationToken"]
}, },
@ -232,6 +245,7 @@ exports.getUserInfo = async (req, res, next) => {
const userObject = { const userObject = {
// Si Public Email // Si Public Email
... (user.isPublicEmail) && { email: user.email }, ... (user.isPublicEmail) && { email: user.email },
isPublicEmail: user.isPublicEmail,
name: user.name, name: user.name,
biography: user.biography, biography: user.biography,
logo: user.logo, logo: user.logo,

View File

@ -0,0 +1,9 @@
const Modal = (props) => (
<div className="Modal container-fluid">
<div className="Modal__content">
{props.children}
</div>
</div>
);
export default Modal;

View File

@ -26,8 +26,7 @@ function UserContextProvider(props) {
api.post('/users/login', { email, password }) api.post('/users/login', { email, password })
.then((response) => { .then((response) => {
const user = response.data; const user = response.data;
cookies.set('user', user); changeUserValue(user);
setUser(user);
setIsAuth(true); setIsAuth(true);
setMessageLogin('<p class="form-success"><b>Succès:</b> Connexion réussi!</p>'); setMessageLogin('<p class="form-success"><b>Succès:</b> Connexion réussi!</p>');
setLoginLoading(false); setLoginLoading(false);
@ -46,8 +45,14 @@ function UserContextProvider(props) {
setIsAuth(false); setIsAuth(false);
} }
const changeUserValue = (user) => {
cookies.remove('user');
cookies.set('user', user);
setUser(user);
}
return ( return (
<UserContext.Provider value={{ user, loginUser, logoutUser, loginLoading, messageLogin, isAuth }}> <UserContext.Provider value={{ user, loginUser, logoutUser, loginLoading, messageLogin, isAuth, changeUserValue }}>
{props.children} {props.children}
</UserContext.Provider> </UserContext.Provider>
); );

View File

@ -1395,6 +1395,35 @@
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw=="
}, },
"@fortawesome/fontawesome-common-types": {
"version": "0.2.28",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz",
"integrity": "sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.28",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz",
"integrity": "sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.28"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz",
"integrity": "sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.28"
}
},
"@fortawesome/react-fontawesome": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.9.tgz",
"integrity": "sha512-49V3WNysLZU5fZ3sqSuys4nGRytsrxJktbv3vuaXkEoxv22C6T7TEG0TW6+nqVjMnkfCQd5xOnmJoZHMF78tOw==",
"requires": {
"prop-types": "^15.7.2"
}
},
"@next/polyfill-nomodule": { "@next/polyfill-nomodule": {
"version": "9.3.2", "version": "9.3.2",
"resolved": "https://registry.npmjs.org/@next/polyfill-nomodule/-/polyfill-nomodule-9.3.2.tgz", "resolved": "https://registry.npmjs.org/@next/polyfill-nomodule/-/polyfill-nomodule-9.3.2.tgz",

View File

@ -8,6 +8,9 @@
"start": "cross-env NODE_ENV=production node server" "start": "cross-env NODE_ENV=production node server"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@zeit/next-css": "^1.0.1", "@zeit/next-css": "^1.0.1",
"axios": "^0.19.2", "axios": "^0.19.2",
"date-and-time": "^0.13.1", "date-and-time": "^0.13.1",

View File

@ -13,7 +13,7 @@ const Login = () => {
const [inputState, setInputState] = useState({}); const [inputState, setInputState] = useState({});
const { loginUser, messageLogin, loginLoading } = useContext(UserContext); const { loginUser, messageLogin, loginLoading } = useContext(UserContext);
const handleChange = () => { const handleChange = (event) => {
const inputStateNew = { ...inputState }; const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = event.target.value; inputStateNew[event.target.name] = event.target.value;
setInputState(inputStateNew); setInputState(inputStateNew);
@ -55,7 +55,8 @@ const Login = () => {
</div> </div>
</form> </form>
<div className="form-result text-center"> <div className="form-result text-center">
{(router.query.isConfirmed !== undefined) && <p className="form-success"><b>Succès:</b> Votre compte a bien été confirmé, vous pouvez maintenant vous connectez!</p>} {(router.query.isConfirmed !== undefined && messageLogin === "") && <p className="form-success"><b>Succès:</b> Votre compte a bien été confirmé, vous pouvez maintenant vous connectez!</p>}
{(router.query.isSuccessEdit !== undefined && messageLogin === "") && <p className="form-success"><b>Succès:</b> Votre profil a bien été modifié, vous pouvez maintenant vous connectez!</p>}
{ {
(loginLoading) ? (loginLoading) ?
<Loader /> <Loader />

View File

@ -1,9 +1,15 @@
import Link from 'next/link'; import Link from 'next/link';
import { Fragment } from 'react'; import { Fragment, useContext, useState } from 'react';
import { UserContext } from '../../contexts/UserContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faTimes } from '@fortawesome/free-solid-svg-icons';
import date from 'date-and-time'; import date from 'date-and-time';
import HeadTag from '../../components/HeadTag'; import HeadTag from '../../components/HeadTag';
import FunctionCard from '../../components/FunctionCard/FunctionCard'; import FunctionCard from '../../components/FunctionCard/FunctionCard';
import Modal from '../../components/Modal';
import redirect from '../../utils/redirect'; import redirect from '../../utils/redirect';
import htmlParser from 'html-react-parser';
import Loader from '../../components/Loader';
import api from '../../utils/api'; import api from '../../utils/api';
import { API_URL } from '../../utils/config'; import { API_URL } from '../../utils/config';
import '../../public/css/pages/profile.css'; import '../../public/css/pages/profile.css';
@ -14,25 +20,142 @@ const Profile = (props) => {
const createdAt = new Date(props.createdAt); const createdAt = new Date(props.createdAt);
const publicationDate = `${('0'+createdAt.getDate()).slice(-2)}/${('0'+(createdAt.getMonth()+1)).slice(-2)}/${createdAt.getFullYear()}`; const publicationDate = `${('0'+createdAt.getDate()).slice(-2)}/${('0'+(createdAt.getMonth()+1)).slice(-2)}/${createdAt.getFullYear()}`;
const { isAuth, user, logoutUser } = useContext(UserContext);
const [isOpen, setIsOpen] = useState(false);
let defaultInputState = {};
if (isAuth) {
defaultInputState = { name: user.name, email: user.email, biography: user.biography, isPublicEmail: user.isPublicEmail };
}
const [inputState, setInputState] = useState(defaultInputState);
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const toggleModal = () => setIsOpen(!isOpen);
const handleChange = (event, isTypeCheck = false) => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = (event.target.files != undefined) ? event.target.files[0] : (isTypeCheck) ? event.target.checked : event.target.value;
setInputState(inputStateNew);
}
const handleSubmit = (event) => {
event.preventDefault();
const token = user.token;
if (isAuth && token != undefined) {
setIsLoading(true);
const formData = new FormData();
formData.append('name', inputState.name);
formData.append('email', inputState.email);
formData.append('biography', inputState.biography);
formData.append('isPublicEmail', inputState.isPublicEmail);
formData.append('logo', inputState.logo);
api.put('/users/', formData, { headers: { 'Authorization': user.token } })
.then(() => {
setIsLoading(false);
logoutUser();
redirect({}, '/login?isSuccessEdit=true');
})
.catch((error) => {
setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`);
setIsLoading(false);
});
}
}
return ( return (
<Fragment> <Fragment>
<HeadTag title={`${props.name} - FunctionProject`} description={`Profil utilisateur de ${props.name}. ${(props.biography != undefined) ? props.biography : ""}`} /> <HeadTag title={`${props.name} - FunctionProject`} description={`Profil utilisateur de ${props.name}. ${(props.biography != undefined) ? props.biography : ""}`} />
<div className="container-fluid Profile__container"> {/* Édition du profil */}
{(isOpen) &&
<Modal toggleModal={toggleModal}>
<div className="container-fluid Profile__container">
<div className="row Profile__row">
<div className="col-24">
<div className="Profile__Modal-top-container row">
<div className="col-24">
<span onClick={toggleModal} style={{ cursor: 'pointer', position: 'absolute', left: 0 }}>
<FontAwesomeIcon icon={faTimes} style={{ width: '1.5rem', color: 'red' }} />
</span>
<h2 className="text-center">Éditer le profil</h2>
<p className="text-center"><em>(Vous devrez vous reconnecter après la sauvegarde) <br/> Si vous changez votre adresse email, vous devrez la confirmer (vérifier vos emails).</em></p>
</div>
</div>
</div>
<div className="col-24">
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label" htmlFor="name">Nom :</label>
<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="email">Email :</label>
<input value={inputState.email} onChange={handleChange} type="email" name="email" id="email" className="form-control" placeholder="email@gmail.com" />
</div>
<div className="form-group custom-control custom-switch">
<input onChange={(event) => handleChange(event, true)} type="checkbox" name="isPublicEmail" checked={inputState.isPublicEmail} className="custom-control-input" id="isPublicEmail" />
<label className="custom-control-label" htmlFor="isPublicEmail">Email Public</label>
</div>
<div className="form-group">
<label className="form-label" htmlFor="biography">Biographie :</label>
<textarea style={{ height: 'auto' }} value={inputState.biography} onChange={handleChange} name="biography" id="biography" className="form-control" rows="5"></textarea>
</div>
<div className="form-group">
<label className="form-label" htmlFor="logo">Logo <em>(400x400 recommandé)</em> :</label>
<br/>
<input onChange={handleChange} accept="image/jpeg,image/jpg,image/png,image/gif" type="file" name="logo" id="logo" />
</div>
<div className="form-group text-center">
<button type="submit" className="btn btn-dark">Sauvegarder</button>
</div>
</form>
<div className="form-result text-center">
{
(isLoading) ?
<Loader />
:
htmlParser(message)
}
</div>
</div>
</div>
</div>
</Modal>
}
<div className={`container-fluid Profile__container ${(isOpen) ? "d-none" : ""}`}>
<div className="row Profile__row"> <div className="row Profile__row">
<div className="col-20"> <div className="col-20">
<div className="text-center"> <div className="text-center">
<h1>Profil de <span className="important">{props.name}</span></h1> <h1>Profil de <span className="important">{props.name}</span></h1>
</div> </div>
<div className="row justify-content-center"> <div className="row justify-content-center">
<div className="col-24 text-center"> <div className="col-24 text-center">
<img className="Profile__logo" src={API_URL + props.logo} alt={props.name} /> <img className="Profile__logo" src={API_URL + props.logo} alt={props.name} />
</div> </div>
<div className="col-24 text-center"> <div className="col-24 text-center">
{(props.biography != undefined) && <p>{props.biography}</p>} {(props.biography != undefined) && <p>{props.biography}</p>}
{(props.email != undefined) && <p><span className="important">Email :</span> {props.email}</p>} {(props.email != undefined) && <p><span className="important">Email :</span> {props.email}</p>}
<p><span className="important">Date de création :</span> {publicationDate}</p> <p><span className="important">Date de création :</span> {publicationDate}</p>
</div> </div>
{(isAuth && user.name === props.name) &&
<button onClick={toggleModal} style={{ marginBottom: '25px' }} className="btn btn-dark">
<FontAwesomeIcon icon={faPen} style={{cursor: 'pointer', width: '1rem'}} />
&nbsp; Éditez le profil
</button>
}
</div> </div>
</div> </div>
</div> </div>
@ -59,13 +182,13 @@ const Profile = (props) => {
<div className="col-24 text-center"> <div className="col-24 text-center">
<h2>Derniers <span className="important">commentaires :</span></h2> <h2>Derniers <span className="important">commentaires :</span></h2>
</div> </div>
<div className="col-18 text-center"> <div className="col-24 text-center">
{props.commentsArray.map((comment) => ( {props.commentsArray.map((comment) => (
<div key={comment.id} className="row Profile__row Profile__comment"> <div key={comment.id} className="row Profile__row Profile__comment">
<div className="col-20"> <div className="col-20">
<p> <p>
Posté sur la fonction&nbsp; Posté sur la fonction&nbsp;
<Link href={(comment.function.type === 'form' || comment.function.type === 'article') ? "/functions/[slug]" : `/functions/${props.slug}`} as={`/functions/${comment.function.slug}`}> <Link href={(comment.function.type === 'form' || comment.function.type === 'article') ? "/functions/[slug]" : `/functions/${comment.function.slug}`} as={`/functions/${comment.function.slug}`}>
<a>{comment.function.title}</a> <a>{comment.function.title}</a>
</Link> </Link>
&nbsp;le {date.format(new Date(comment.createdAt), 'DD/MM/YYYY à HH:mm', true)} &nbsp;le {date.format(new Date(comment.createdAt), 'DD/MM/YYYY à HH:mm', true)}

View File

@ -11,7 +11,7 @@ const Register = () => {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleChange = () => { const handleChange = (event) => {
const inputStateNew = { ...inputState }; const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = event.target.value; inputStateNew[event.target.name] = event.target.value;
setInputState(inputStateNew); setInputState(inputStateNew);

View File

@ -22,4 +22,70 @@
} }
.Profile__comment { .Profile__comment {
margin: 0 0 50px 0; margin: 0 0 50px 0;
}
.Profile__Modal-top-container {
margin: 20px 0;
}
.custom-control {
display: flex;
justify-content: center;
}
.custom-control-input {
position: absolute;
z-index: -1;
opacity: 0;
}
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-input:checked~.custom-control-label::before {
color: #fff;
border-color: #007bff;
background-color: #007bff;
}
.custom-switch .custom-control-label::before {
left: -2.25rem;
width: 1.75rem;
pointer-events: all;
border-radius: .5rem;
}
.custom-control-label::before {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #adb5bd solid 1px;
}
.custom-switch .custom-control-input:checked~.custom-control-label::after {
background-color: #fff;
-webkit-transform: translateX(.75rem);
transform: translateX(.75rem);
}
.custom-switch .custom-control-label::after {
top: calc(.25rem + 2px);
left: calc(-2.25rem + 2px);
width: calc(1rem - 4px);
height: calc(1rem - 4px);
background-color: #adb5bd;
border-radius: .5rem;
transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
}
.custom-control-label::after {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50%/50% 50%;
} }

View File

@ -1,9 +1,9 @@
function redirect(ctx, path) { function redirect(ctx, path) {
if (ctx.res) { if (ctx.res != undefined) {
ctx.res.writeHead(302, { Location: path }); ctx.res.writeHead(302, { Location: path });
ctx.res.end(); ctx.res.end();
} else { } else {
document.location.pathname = path; document.location.href = path;
} }
} }