diff --git a/client-side/public/images/notification.svg b/client-side/public/images/notification.svg new file mode 100644 index 000000000..79c5feb78 --- /dev/null +++ b/client-side/public/images/notification.svg @@ -0,0 +1 @@ +notification-bell \ No newline at end of file diff --git a/client-side/public/images/pencil.svg b/client-side/public/images/pencil.svg new file mode 100644 index 000000000..a63cf6de5 --- /dev/null +++ b/client-side/public/images/pencil.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file 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..25328e434 --- /dev/null +++ b/client-side/src/components/signUp/signUp.jsx @@ -0,0 +1,166 @@ +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,TOAST_MESSAGES } 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 resetFormValues = () => { + formik.resetForm({ + values: { + name: user.name || '', + email: user.email || '', + password: '' + } + }); +}; + + 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)); + enqueueSnackbar(); + navigate('/home'); + } catch (error) { + if (error.response && error.response.status === 401) { + enqueueSnackbar(); + }else{ + enqueueSnackbar(); + } + resetFormValues(); + } + }; + 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..ff0d09f4e 100644 --- a/client-side/src/constants.js +++ b/client-side/src/constants.js @@ -13,4 +13,49 @@ export const MESSAGES = { EMAIL_LABEL: 'Email', EMAIL_PLACEHOLDER: 'example@example.com', PASSWORD_LABEL: 'Password', - }; \ No newline at end of file + }; + 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' + } + }; + export const HEADER = { + edit_user: 'edit user', + manage_notifications:'manage notifications' + }; + 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/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..05720974c 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 { @@ -22,7 +22,7 @@ .MuiOutlinedInput-notchedOutline { border-width: 2px; border-color:$secondary-color; - + border-radius: 8px; legend { width: auto; } 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; } diff --git a/client-side/src/stories/header/header.jsx b/client-side/src/stories/header/header.jsx index b17391d15..0b281f312 100644 --- a/client-side/src/stories/header/header.jsx +++ b/client-side/src/stories/header/header.jsx @@ -4,23 +4,42 @@ 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'; +import { HEADER } from '../../constants.js'; 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) { @@ -85,36 +104,36 @@ function ResponsiveAppBar() { /> - + { {getAvatarLetter()} - - - - + } + {user && user.name && isSelectOpen && ( + +