From 6803cc160a57fc9ff37922937948c98853ee99fe Mon Sep 17 00:00:00 2001 From: Chani-Pielet Date: Thu, 5 Sep 2024 18:08:41 +0300 Subject: [PATCH 1/4] update user --- .../components/signUp/PasswordStrength.jsx | 43 +++++ .../components/signUp/PasswordStrength.scss | 35 ++++ client-side/src/components/signUp/signUp.jsx | 149 ++++++++++++++++++ client-side/src/components/signUp/signUp.scss | 62 ++++++++ client-side/src/constants.js | 36 +++++ client-side/src/redux/user/user.slice.js | 9 +- client-side/src/router/router.jsx | 15 +- client-side/src/services/userService.js | 35 ++++ client-side/src/stories/Select/select.scss | 2 +- client-side/src/stories/header/header.jsx | 62 +++++--- .../controllers/preference.controller.js | 4 - server-side/controllers/user.controller.js | 36 ++++- server-side/middleware/auth.js | 16 ++ server-side/models/user.model.js | 6 + server-side/router/preference.router.js | 10 -- server-side/router/profile.router.js | 2 +- server-side/router/user.router.js | 4 +- 17 files changed, 478 insertions(+), 48 deletions(-) create mode 100644 client-side/src/components/signUp/PasswordStrength.jsx create mode 100644 client-side/src/components/signUp/PasswordStrength.scss create mode 100644 client-side/src/components/signUp/signUp.jsx create mode 100644 client-side/src/components/signUp/signUp.scss create mode 100644 client-side/src/services/userService.js create mode 100644 server-side/middleware/auth.js diff --git a/client-side/src/components/signUp/PasswordStrength.jsx b/client-side/src/components/signUp/PasswordStrength.jsx new file mode 100644 index 000000000..799314b60 --- /dev/null +++ b/client-side/src/components/signUp/PasswordStrength.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Box, LinearProgress } from '@mui/material'; +import './PasswordStrength.scss'; +import { PASSWORD_STRENGTH } from '../../constants'; + +const PasswordStrengthMeter = ({ password }) => { + + const colors = { + passwordStrength: { + empty: '#f00', + weak: '#ffa500', + medium: '#008000', + } +}; + const calculatePasswordStrength = (password) => { + if (!password) return PASSWORD_STRENGTH.EMPTY; + if (password.length < 4) return PASSWORD_STRENGTH.WEAK; + if (/[A-Za-z]/.test(password) && /[0-9]/.test(password) && password.length > 4) return PASSWORD_STRENGTH.MEDIUM; + return PASSWORD_STRENGTH.WEAK; + }; + const strength = calculatePasswordStrength(password); + + const { color, message } = strength; + const strengthColor = colors.passwordStrength[color]; + const strengthClass = { + [PASSWORD_STRENGTH.EMPTY.key]: 'weak', + [PASSWORD_STRENGTH.WEAK.key]: 'medium', + [PASSWORD_STRENGTH.MEDIUM.key]: 'strong', + }[strength.key]; + return ( + + + + {message} + + +); +}; +export default PasswordStrengthMeter; diff --git a/client-side/src/components/signUp/PasswordStrength.scss b/client-side/src/components/signUp/PasswordStrength.scss new file mode 100644 index 000000000..d1e20eaff --- /dev/null +++ b/client-side/src/components/signUp/PasswordStrength.scss @@ -0,0 +1,35 @@ +.centered-container { + width: 50%; + margin-left: 2%; + } + + .password-strength-meter { + height: 10px; + border-radius: 5px; + right: 3%; + background-color: #D3D3D3; + + &.weak { + & .MuiLinearProgress-bar { + background-color: red; + } + } + + &.medium { + & .MuiLinearProgress-bar { + background-color: orange; + } + } + + &.strong { + & .MuiLinearProgress-bar { + background-color: green; + } + } + } + + .password-strength-message { + margin-top: 1px; + margin-right: 34%; + font-size: small; + } \ No newline at end of file diff --git a/client-side/src/components/signUp/signUp.jsx b/client-side/src/components/signUp/signUp.jsx new file mode 100644 index 000000000..7f09b807b --- /dev/null +++ b/client-side/src/components/signUp/signUp.jsx @@ -0,0 +1,149 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +// import ReCAPTCHA from 'react-google-recaptcha'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { useNavigate } from 'react-router-dom'; +import PasswordStrengthMeter from '../signUp/PasswordStrength'; +import GenericButton from '../../stories/Button/GenericButton'; +import GenericInput from '../../stories/GenericInput/genericInput'; +import { MessagesSignUp } from '../../constants'; +import { createUser, updateUser } from '../../services/userService'; +import { addUser, updateUserDetails } from '../../redux/user/user.slice'; +import './signUp.scss'; + +const SignUpSchema = Yup.object().shape({ + name: Yup.string() + .matches(/^[a-zA-Z\u0590-\u05FF\s]+$/, MessagesSignUp.name.matches) + .required(MessagesSignUp.name.required), + email: Yup.string() + .email(MessagesSignUp.email.invalid) + .required(MessagesSignUp.email.required), + password: Yup.string() + .required(MessagesSignUp.password.required) + .matches(/[A-Za-z]/, MessagesSignUp.password.matches.letters) + .matches(/\d/, MessagesSignUp.password.matches.digits) + .min(4, MessagesSignUp.password.min) +}); +function SignUp({ isEditMode = false, onSave }) { + const [password, setPassword] = useState(''); +// const [robotPass, setRobotPass] = useState(null) + const url = process.env.REACT_APP_SITEKEY; + const dispatch = useDispatch(); + const navigate=useNavigate(); +const user = useSelector(state => state.user.currentUser|| {}); +const userId =user.id || null; +const formik = useFormik({ + initialValues:{ name: user.name || '', email: user.email || '', password: '' }, + validationSchema: SignUpSchema, + onSubmit: (values) => { + const user = { + name: values.name, + email: values.email, + password: values.password, + }; + if (isEditMode) { + editUser(user); + } else { + userSignUp(user); + } + }, + }); +useEffect(() => { + if (isEditMode) { + formik.setValues({ name: user.name || '', email: user.email || '', password: '' }); + } +}, [isEditMode]); + + const userSignUp = async (user) => { + try { + await createUser(user); + dispatch(addUser(user)); + navigate('/home'); + } catch (error) { + console.error("The user is not included in the system"); + } + }; + const editUser = async (user) => { + try { + await updateUser(user,userId); + dispatch(updateUserDetails(user)); + navigate('/home'); + } catch (error) { + console.error("Failed to update user details"); + } + }; + return ( +
+

{isEditMode ? 'Edit User' : 'Sign Up'}

+
+
+ + {formik.touched.name && formik.errors.name ? ( +
{formik.errors.name}
+ ) : null} +
+
+ + {formik.touched.email && formik.errors.email ? ( +
{formik.errors.email}
+ ) : null} +
+
+ { + formik.handleChange(e); + setPassword(e.target.value); + }} + + width="100%" + size="medium" + /> + {formik.touched.password && formik.errors.password ? ( +
{formik.errors.password}
+ ) : null} + +
+ {/* setRobotPass(val)} + /> */} + + +
+ ); +} +export default SignUp; + diff --git a/client-side/src/components/signUp/signUp.scss b/client-side/src/components/signUp/signUp.scss new file mode 100644 index 000000000..b9467e817 --- /dev/null +++ b/client-side/src/components/signUp/signUp.scss @@ -0,0 +1,62 @@ + +$primary-color: #ccc; +$border-radius: 1%; +$direction: rtl; +.signup-container { + max-width: 25%; + margin: 0 auto; + padding: 1%; + border: 1px solid $primary-color; + border-radius: $border-radius; + box-shadow: 0 0 1% rgba(0, 0, 0, 0.1); + text-align: $direction; +} +h2 { + margin-bottom: 2%; + direction: ltr; + margin-right: calc(100% - 78%); +} +.form-group { + margin-bottom: 3%; +} +@mixin form-element { + display: block; + margin-bottom: 2%; + + & + & { + margin-top: 1%; + } +} +label { + @include form-element; +} + +input { + @include form-element; + + width: 100%; + padding: 1%; + box-sizing: border-box; + border: 1px solid $primary-color; + border-radius: $border-radius; + direction: $direction; + margin-bottom: 16px; +} +button { + width: 100%; + padding: 2%; + color: white; + border: none; + border-radius: $border-radius; + cursor: pointer; + font-size: 5%; +} +.error { + color: red; + font-size: small; + margin-top: 1%; + margin-right: calc(100% - 54%); +} +.css-eglki6-MuiLinearProgress-root{ + margin-top: 12px; +} \ No newline at end of file diff --git a/client-side/src/constants.js b/client-side/src/constants.js index 59c25eb6f..763b9b358 100644 --- a/client-side/src/constants.js +++ b/client-side/src/constants.js @@ -13,4 +13,40 @@ export const MESSAGES = { EMAIL_LABEL: 'Email', EMAIL_PLACEHOLDER: 'example@example.com', PASSWORD_LABEL: 'Password', + }; + export const MessagesSignUp = { + name: { + required: 'required field', + matches: 'The field must contain only letters', + }, + email: { + required: 'required field', + invalid: 'invalid email', + }, + password: { + required: 'required field', + matches: { + letters: 'field must contain english letter', + digits: 'field must contain one digit', + }, + min: 'password must contain at least 4 characters', + max: 'password must be up to 20 characters', + }, + }; + export const PASSWORD_STRENGTH = { + EMPTY: { + key: 1, + message: 'Weak password', + color: 'red' + }, + WEAK: { + key: 2, + message: 'Medium password', + color: 'orange' + }, + MEDIUM: { + key: 3, + message: 'Strong password', + color: 'green' + } }; \ No newline at end of file diff --git a/client-side/src/redux/user/user.slice.js b/client-side/src/redux/user/user.slice.js index 093863443..e658ee992 100644 --- a/client-side/src/redux/user/user.slice.js +++ b/client-side/src/redux/user/user.slice.js @@ -41,6 +41,13 @@ const userSlice = createSlice({ * @param {UserStateType} state * @param {PayloadAction} action */ + updateUserDetails: (state, action) => { + state.currentUser = { ...state.currentUser, ...action.payload }; + }, + /** + * @param {UserStateType} state + * @param {PayloadAction} action + */ deleteUser: (state, action) => { const index = state.users.findIndex(user => user.id === action.payload); if (index !== -1) { @@ -50,5 +57,5 @@ const userSlice = createSlice({ } }); -export const { setUser, addUser, updateUser, deleteUser } = userSlice.actions; +export const { setUser, addUser, updateUser, deleteUser,updateUserDetails } = userSlice.actions; export default userSlice.reducer; diff --git a/client-side/src/router/router.jsx b/client-side/src/router/router.jsx index 7bfa68907..d993b1010 100644 --- a/client-side/src/router/router.jsx +++ b/client-side/src/router/router.jsx @@ -1,8 +1,9 @@ import React from "react"; import {createBrowserRouter } from "react-router-dom"; -import ProfileList from "../components/profileComponent.jsx"; +// import ProfileList from "../components/profileComponent.jsx"; import Layout from "./layout.jsx"; import Login from "../login/Login.jsx"; +import SignUp from "../components/signUp/signUp.jsx"; export const router = createBrowserRouter([ { path: '', @@ -16,13 +17,17 @@ export const router = createBrowserRouter([ path: '/home', element:

home

}, - { - path: '/profiles', - element: - }, + // { + // path: '/profiles', + // element: + // }, { path: '/login', element: + }, + { + path: '/signup', + element: } ] }, diff --git a/client-side/src/services/userService.js b/client-side/src/services/userService.js new file mode 100644 index 000000000..612f950df --- /dev/null +++ b/client-side/src/services/userService.js @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { handlePost, handlePut } from '../axios/middleware'; +const baseURL = process.env.REACT_APP_BASE_URL; +export const createUser = async (userData) => { + try { + const response = await handlePost('/users', userData); + return response.data; + } catch (err) { + console.error('Error creating user:', err); + throw err; + } +}; + +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + } + + export const updateUser = async (user, userId) => { + try { + const token = getCookie('token'); + const response = await axios.put(`${baseURL}/users/${userId}`, + user, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + return response.data; + } catch (err) { + console.error('Error updating user:', err); + throw err; + } + }; diff --git a/client-side/src/stories/Select/select.scss b/client-side/src/stories/Select/select.scss index 9bcb03f95..903e12a14 100644 --- a/client-side/src/stories/Select/select.scss +++ b/client-side/src/stories/Select/select.scss @@ -4,7 +4,7 @@ .genericSelect { &.primary { - background-color: $background-class; + // background-color: $background-class; } &.secondary { diff --git a/client-side/src/stories/header/header.jsx b/client-side/src/stories/header/header.jsx index b17391d15..6895490c6 100644 --- a/client-side/src/stories/header/header.jsx +++ b/client-side/src/stories/header/header.jsx @@ -4,23 +4,41 @@ import { useSelector } from 'react-redux'; import {AppBar,Box,Toolbar,IconButton,Typography,Menu,AdbIcon,MenuItem,Tooltip,Button,Avatar,Container} from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import LabTabs from '../tabs/tabs'; +import Select from '../Select/Select.jsx'; import './header.scss'; import { selectAuth } from '../../redux/auth/auth.selector'; +import SignUp from '../../components/signUp/signUp.jsx'; function ResponsiveAppBar() { const [anchorElNav, setAnchorElNav] = useState(null); const [anchorElUser, setAnchorElUser] = useState(null); - const { user } = useSelector(selectAuth); + const { user } = useSelector(selectAuth); + const [isSelectOpen, setIsSelectOpen] = useState(false); + const [isSignupVisible, setIsSignupVisible] = useState(false); const handleOpenNavMenu = (event) => { setAnchorElNav(event.currentTarget); }; const handleOpenUserMenu = (event) => { setAnchorElUser(event.currentTarget); + setIsSelectOpen((prev) => !prev); }; const handleCloseNavMenu = () => { setAnchorElNav(null); }; const handleCloseUserMenu = () => { setAnchorElUser(null); + }; + const handleSave = () => { + setIsSignupVisible(false); + }; + + + const selectFunctions = (selectedValue) => { + console.log('Selected Value:', selectedValue); + if (selectedValue == 1) { + setIsSignupVisible(true); + } else if (selectedValue == 2) { + + } }; const getAvatarLetter = () => { if (user && user.name) { @@ -90,31 +108,31 @@ function ResponsiveAppBar() { {getAvatarLetter()} - - - + {isSelectOpen && ( + + selectFunctions(selectedValue)} // Directly pass the value + onChange={(selectedValue) => selectFunctions(selectedValue)} className="primary" options={[ - { value: 1, text: "edit user profile", iconSrc: '/images/pencil.svg' }, - { value: 2, text: "manage notifications", iconSrc: 'images/notification.svg' }, + { value: 1, text: HEADER.edit_user, iconSrc: '/images/pencil.svg' }, + { value: 2, text: HEADER.manage_notifications, iconSrc: 'images/notification.svg' }, ]} title="edit user" size="small" diff --git a/server-side/controllers/user.controller.js b/server-side/controllers/user.controller.js index e13586005..fa9bd74d8 100644 --- a/server-side/controllers/user.controller.js +++ b/server-side/controllers/user.controller.js @@ -103,7 +103,6 @@ export const signIn = async (req, res, next) => { if (same) { user.password = "****"; const token=generateToken(user); - // res.cookie('token', token, { httpOnly: false, secure: true }); return res.status(200).send({ user }); } else { diff --git a/server-side/middleware/auth.js b/server-side/middleware/auth.js index a500ce501..c1ca7526a 100644 --- a/server-side/middleware/auth.js +++ b/server-side/middleware/auth.js @@ -2,8 +2,6 @@ import jwt from 'jsonwebtoken'; export const auth = (req, res, next) => { try { - console.log('got to midleware'); - const { authorization } = req.headers; const [, token] = authorization.split(' '); const privateKey = process.env.JWT_SECRET || 'JWT_SECRET'; From a38493c649d6f66153e45a443acf7fe0cdbfe533 Mon Sep 17 00:00:00 2001 From: Chani-Pielet Date: Sun, 8 Sep 2024 12:25:15 +0300 Subject: [PATCH 4/4] add toast messages --- client-side/src/components/signUp/signUp.jsx | 21 +++++++++++++++++-- client-side/src/constants.js | 7 ++++++- .../src/stories/Toast/ToastMessage.scss | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/client-side/src/components/signUp/signUp.jsx b/client-side/src/components/signUp/signUp.jsx index 7f09b807b..25328e434 100644 --- a/client-side/src/components/signUp/signUp.jsx +++ b/client-side/src/components/signUp/signUp.jsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { enqueueSnackbar, useSnackbar } from 'notistack'; // import ReCAPTCHA from 'react-google-recaptcha'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import { useNavigate } from 'react-router-dom'; import PasswordStrengthMeter from '../signUp/PasswordStrength'; import GenericButton from '../../stories/Button/GenericButton'; +import ToastMessage from '../../stories/Toast/ToastMessage.jsx'; import GenericInput from '../../stories/GenericInput/genericInput'; -import { MessagesSignUp } from '../../constants'; +import { MessagesSignUp,TOAST_MESSAGES } from '../../constants'; import { createUser, updateUser } from '../../services/userService'; import { addUser, updateUserDetails } from '../../redux/user/user.slice'; import './signUp.scss'; @@ -54,6 +56,15 @@ useEffect(() => { formik.setValues({ name: user.name || '', email: user.email || '', password: '' }); } }, [isEditMode]); +const resetFormValues = () => { + formik.resetForm({ + values: { + name: user.name || '', + email: user.email || '', + password: '' + } + }); +}; const userSignUp = async (user) => { try { @@ -68,9 +79,15 @@ useEffect(() => { try { await updateUser(user,userId); dispatch(updateUserDetails(user)); + enqueueSnackbar(); navigate('/home'); } catch (error) { - console.error("Failed to update user details"); + if (error.response && error.response.status === 401) { + enqueueSnackbar(); + }else{ + enqueueSnackbar(); + } + resetFormValues(); } }; return ( diff --git a/client-side/src/constants.js b/client-side/src/constants.js index a91291e64..ff0d09f4e 100644 --- a/client-side/src/constants.js +++ b/client-side/src/constants.js @@ -53,4 +53,9 @@ export const MESSAGES = { export const HEADER = { edit_user: 'edit user', manage_notifications:'manage notifications' - }; \ No newline at end of file + }; + export const TOAST_MESSAGES = { + USER_UPDATED_SUCCESS: 'User updated successfully!', + USER_UPDATED_ERROR: 'Error updating user!', + USER_UPDATED_ERROR_UNAUTHORIZED: 'Error updating user, Connect to the site and try again', + } \ No newline at end of file diff --git a/client-side/src/stories/Toast/ToastMessage.scss b/client-side/src/stories/Toast/ToastMessage.scss index 5f87734f7..4b9809d4f 100644 --- a/client-side/src/stories/Toast/ToastMessage.scss +++ b/client-side/src/stories/Toast/ToastMessage.scss @@ -1,4 +1,4 @@ -@import './variables.scss'; +@import '../variables.scss'; .MuiAlert-root { font-family: 'Montserrat', sans-serif; }