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
for (const name of fileNames) {
// If file name matches the pattern
if (name.startsWith(pattern)) {
if (name.startsWith(pattern) && name !== 'default.png') {
console.log(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) {
const user = await Users.findOne({ where: { id: userId } });
user.name = name;
if (user.email !== email) {
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;
if (logoName != undefined) {
user.logo = `/images/users/${logoName}`;
}
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) => {
@ -115,7 +128,7 @@ exports.login = async (req, res, next) => {
}
const token = jwt.sign({
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 });
} catch (error) {
console.log(error);
@ -198,7 +211,7 @@ exports.getUserInfo = async (req, res, next) => {
const { name } = req.params;
try {
const user = await Users.findOne({
where: { name },
where: { name, isConfirmed: true },
attributes: {
exclude: ["updatedAt", "isAdmin", "isConfirmed", "password", "tempToken", "tempExpirationToken"]
},
@ -232,6 +245,7 @@ exports.getUserInfo = async (req, res, next) => {
const userObject = {
// Si Public Email
... (user.isPublicEmail) && { email: user.email },
isPublicEmail: user.isPublicEmail,
name: user.name,
biography: user.biography,
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 })
.then((response) => {
const user = response.data;
cookies.set('user', user);
setUser(user);
changeUserValue(user);
setIsAuth(true);
setMessageLogin('<p class="form-success"><b>Succès:</b> Connexion réussi!</p>');
setLoginLoading(false);
@ -46,8 +45,14 @@ function UserContextProvider(props) {
setIsAuth(false);
}
const changeUserValue = (user) => {
cookies.remove('user');
cookies.set('user', user);
setUser(user);
}
return (
<UserContext.Provider value={{ user, loginUser, logoutUser, loginLoading, messageLogin, isAuth }}>
<UserContext.Provider value={{ user, loginUser, logoutUser, loginLoading, messageLogin, isAuth, changeUserValue }}>
{props.children}
</UserContext.Provider>
);

View File

@ -1395,6 +1395,35 @@
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
"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": {
"version": "9.3.2",
"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"
},
"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",
"axios": "^0.19.2",
"date-and-time": "^0.13.1",

View File

@ -13,7 +13,7 @@ const Login = () => {
const [inputState, setInputState] = useState({});
const { loginUser, messageLogin, loginLoading } = useContext(UserContext);
const handleChange = () => {
const handleChange = (event) => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = event.target.value;
setInputState(inputStateNew);
@ -55,7 +55,8 @@ const Login = () => {
</div>
</form>
<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) ?
<Loader />

View File

@ -1,9 +1,15 @@
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 HeadTag from '../../components/HeadTag';
import FunctionCard from '../../components/FunctionCard/FunctionCard';
import Modal from '../../components/Modal';
import redirect from '../../utils/redirect';
import htmlParser from 'html-react-parser';
import Loader from '../../components/Loader';
import api from '../../utils/api';
import { API_URL } from '../../utils/config';
import '../../public/css/pages/profile.css';
@ -14,25 +20,142 @@ const Profile = (props) => {
const createdAt = new Date(props.createdAt);
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 (
<Fragment>
<HeadTag title={`${props.name} - FunctionProject`} description={`Profil utilisateur de ${props.name}. ${(props.biography != undefined) ? props.biography : ""}`} />
{/* É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="col-20">
<div className="text-center">
<h1>Profil de <span className="important">{props.name}</span></h1>
</div>
<div className="row justify-content-center">
<div className="col-24 text-center">
<img className="Profile__logo" src={API_URL + props.logo} alt={props.name} />
</div>
<div className="col-24 text-center">
{(props.biography != undefined) && <p>{props.biography}</p>}
{(props.email != undefined) && <p><span className="important">Email :</span> {props.email}</p>}
<p><span className="important">Date de création :</span> {publicationDate}</p>
</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>
@ -59,13 +182,13 @@ const Profile = (props) => {
<div className="col-24 text-center">
<h2>Derniers <span className="important">commentaires :</span></h2>
</div>
<div className="col-18 text-center">
<div className="col-24 text-center">
{props.commentsArray.map((comment) => (
<div key={comment.id} className="row Profile__row Profile__comment">
<div className="col-20">
<p>
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>
</Link>
&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 [isLoading, setIsLoading] = useState(false);
const handleChange = () => {
const handleChange = (event) => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = event.target.value;
setInputState(inputStateNew);

View File

@ -23,3 +23,69 @@
.Profile__comment {
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) {
if (ctx.res) {
if (ctx.res != undefined) {
ctx.res.writeHead(302, { Location: path });
ctx.res.end();
} else {
document.location.pathname = path;
document.location.href = path;
}
}