diff --git a/.env.example b/.env.example deleted file mode 100644 index e67d4e53..00000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -REACT_APP_API_URL="http://localhost:4000" \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 639c6972..8be254a5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,5 +5,5 @@ "singleQuote": true, "trailingComma": "none", "jsxBracketSameLine": false, - "endOfLine": "lf" + "endOfLine": "auto" } diff --git a/package-lock.json b/package-lock.json index 6891fbef..4bbf17e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react-modal": "^3.16.1", "react-router-dom": "^6.8.0", "react-scripts": "5.0.1", + "validator": "^13.12.0", "web-vitals": "^3.1.1" }, "devDependencies": { @@ -17116,6 +17117,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 249b6b05..6045acce 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "react-modal": "^3.16.1", "react-router-dom": "^6.8.0", "react-scripts": "5.0.1", + "validator": "^13.12.0", "web-vitals": "^3.1.1" }, "scripts": { diff --git a/src/App.css b/src/App.css index ec62bd1a..a103714e 100644 --- a/src/App.css +++ b/src/App.css @@ -14,3 +14,7 @@ .ReactModal__Html--open { overflow: hidden; } + +.profile-button-group > section { + padding-left: 400px; +} diff --git a/src/App.js b/src/App.js index 136c3a15..cb2d1e06 100644 --- a/src/App.js +++ b/src/App.js @@ -8,7 +8,8 @@ import Verification from './pages/verification'; import { AuthProvider, ProtectedRoute } from './context/auth'; import { ModalProvider } from './context/modal'; import Welcome from './pages/welcome'; - +import ViewProfile from './pages/profileView'; +import EditProfile from './pages/profileEdit'; const App = () => { return ( <> @@ -28,6 +29,24 @@ const App = () => { } /> + + + + } + /> + + + + + } + /> + { +const TextInput = ({ + value, + onChange, + errorResponse = '', + name, + label, + icon, + require = false, + type = 'text', + readOnly = false +}) => { const [input, setInput] = useState(''); const [showpassword, setShowpassword] = useState(false); + // form is valid if has a text value or field is optional + // const [isValid, setIsValid] = useState(!!value || !require); if (type === 'password') { return (
@@ -11,21 +23,33 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { type={type} name={name} value={value} + required={require} + readOnly={readOnly} onChange={(e) => { onChange(e); setInput(e.target.value); }} /> - {showpassword && } + {showpassword && ( + + )} + {errorResponse && {errorResponse}}
); } else { @@ -36,10 +60,13 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { type={type} name={name} value={value} + required={require} + readOnly={readOnly} onChange={onChange} className={icon && 'input-has-icon'} /> {icon && {icon}} + {errorResponse && {errorResponse}} ); } diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js index b31393a8..1f788a47 100644 --- a/src/components/navigation/index.js +++ b/src/components/navigation/index.js @@ -4,6 +4,7 @@ import HomeIcon from '../../assets/icons/homeIcon'; import ProfileIcon from '../../assets/icons/profileIcon'; import useAuth from '../../hooks/useAuth'; import './style.css'; +import { getId } from '../../service/tokenService'; const Navigation = () => { const { token } = useAuth(); @@ -22,7 +23,7 @@ const Navigation = () => {
  • - +

    Profile

    diff --git a/src/components/profile/index.js b/src/components/profile/index.js new file mode 100644 index 00000000..9b92ea1e --- /dev/null +++ b/src/components/profile/index.js @@ -0,0 +1,177 @@ +import './profile.css'; +import { useContext } from 'react'; +import Form from '../form'; +import TextInput from '../form/textInput'; + +const Profile = ({ readOnly, UserContext }) => { + if (!UserContext) { + throw new Error('UserContext is required'); + } + + const context = useContext(UserContext); + const { user, updatedUser, onChange, submit } = context; + const initials = user ? `${user.firstName[0]}${user.lastName[0]}` : ''; + + if (!user) { + return
    Loading...
    ; + } + + return ( +
    +

    {readOnly ? 'View Profile' : 'Edit Profile'}

    +
    +
    +
    +

    {initials}

    +
    + +
    + +
    +
    +
    +
    +

    Basic Info

    +
    +

    {initials}

    +
    + + + + +
    + +
    +

    {user.role === 'STUDENT' ? 'Training Info' : 'Professional Info'}

    + + + {user.role === 'STUDENT' ? ( + <> + + + + ) : ( + + )} +
    +
    +

    Contact Info

    + + + +
    +
    +

    Bio

    + +

    *Required

    +
    + + + ); +}; + +export default StepFour; diff --git a/src/pages/welcome/stepOne/index.js b/src/pages/welcome/stepOne/index.js index 317940f8..b36b30e4 100644 --- a/src/pages/welcome/stepOne/index.js +++ b/src/pages/welcome/stepOne/index.js @@ -1,31 +1,78 @@ +import { useEffect, useState } from 'react'; import ProfileIcon from '../../../assets/icons/profileIcon'; import Form from '../../../components/form'; import TextInput from '../../../components/form/textInput'; +import { validFirstName, validLastName } from '../../../service/inputValidationService'; +import useModal from '../../../hooks/useModal'; +import UploadPhotoModal from '../../../components/uploadPhotoModal'; -const StepOne = ({ data, setData }) => { +const StepOne = ({ data, setData, inputIsValid, setInputIsValid }) => { + const [firstNameResponse, setFirstNameResponse] = useState(''); + const [lastNameResponse, setLastNameResponse] = useState(''); + const { openModal, setModal } = useModal(); + + // Create a function to run on user interaction + const showModal = () => { + // Use setModal to set the header of the modal and the component the modal should render + setModal('Upload photo', ); // CreatePostModal is just a standard React component, nothing special + + // Open the modal! + openModal(); + }; + useEffect(() => { + if (data.firstName !== '' && data.lastName !== '') { + setInputIsValid(true); + } + if (data.lastName === '') { + setLastNameResponse(validLastName(data.lastName).message); + setInputIsValid(false); + } + if (data.lastName.length > 1) { + setLastNameResponse(''); + } + if (data.firstName === '') { + setFirstNameResponse(validFirstName(data.firstName).message); + setInputIsValid(false); + } + if (data.firstName.length > 1) { + setFirstNameResponse(''); + } + }, [data.firstName, data.lastName, inputIsValid, firstNameResponse, lastNameResponse]); return ( <>

    Basic info

    -
    + console.log('next')}>

    Photo

    -

    Add headshot

    +

    + Add headshot +

    Please upload a valid image file

    + - { + return ( + <> +
    +

    Training Info

    +
    + +
    + + + + + +

    *Required

    +
    + + + ); +}; + +export default StepThree; diff --git a/src/pages/welcome/stepTwo/index.js b/src/pages/welcome/stepTwo/index.js index f40dad3e..5e515fb6 100644 --- a/src/pages/welcome/stepTwo/index.js +++ b/src/pages/welcome/stepTwo/index.js @@ -1,14 +1,17 @@ import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; const StepTwo = ({ data, setData }) => { return ( <>
    -

    Bio

    +

    Contact info

    - + + +

    *Required

    diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..b1683aac 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -5,8 +5,8 @@ async function login(email, password) { } async function register(email, password) { - await post('users', { email, password }, false); - return await login(email, password); + return post('users', { email, password }, false); + // return await login(email, password); } async function createProfile(userId, firstName, lastName, githubUrl, bio) { @@ -18,6 +18,14 @@ async function getPosts() { return res.data.posts; } +async function getUser(id) { + return await get(`users/${id}`); +} + +async function updateUser(id, data) { + return await patch(`users/${id}`, data); +} + async function post(endpoint, data, auth = true) { return await request('POST', endpoint, data, auth); } @@ -52,4 +60,4 @@ async function request(method, endpoint, data, auth = true) { return response.json(); } -export { login, getPosts, register, createProfile }; +export { login, getPosts, register, createProfile, getUser, updateUser }; diff --git a/src/service/inputValidationService.js b/src/service/inputValidationService.js new file mode 100644 index 00000000..c577c622 --- /dev/null +++ b/src/service/inputValidationService.js @@ -0,0 +1,144 @@ +import validator from 'validator'; + +export const validEmail = (email) => { + if (validator.isEmail(email)) { + return { + isValid: true, + message: '' + }; + } else { + return { + isValid: false, + message: 'Invalid email format' + }; + } +}; + +export const validPassword = (password) => { + if (password.length < 8) { + return { + isValid: false, + message: 'Password must be at least 8 characters long' + }; + } + if (!/[A-Za-z]/.test(password)) { + return { + isValid: false, + message: 'Password must contain at least one letter' + }; + } + if (!/[A-Z]/.test(password)) { + return { + isValid: false, + message: 'Password must contain at least one uppercase letter' + }; + } + if (!/\d/.test(password)) { + return { + isValid: false, + message: 'Password must contain at least one number' + }; + } + if (!/[@$!%*?&]/.test(password)) { + return { + isValid: false, + message: 'Password must contain at least one special character' + }; + } + + return { + isValid: true, + message: 'Success' + }; +}; + +export const validateUpdateUser = (toUpdate) => { + const validationPatterns = { + name: /^[a-zA-Z\s-']{2,50}$/, + email: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, + password: /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/, + biography: /^[\s\S]{0,500}$/, + githubUrl: /^https:\/\/github\.com\/[a-zA-Z0-9-]+$/, + role: /^(STUDENT|TEACHER)$/, + mobile: /^\+?[1-9]\d{1,14}$/ + }; + + const validateField = (field, value, pattern) => { + if (value === undefined || value === null || value === '') return true; + return pattern.test(value); + }; + // Validate input fields + const validationErrors = {}; + + if ( + toUpdate.firstName && + !validateField('firstName', toUpdate.firstName, validationPatterns.name) + ) { + validationErrors.firstName = 'Invalid first name format'; + } + if (toUpdate.lastName && !validateField('lastName', toUpdate.lastName, validationPatterns.name)) { + validationErrors.lastName = 'Invalid last name format'; + } + if (toUpdate.email && !validateField('email', toUpdate.email, validationPatterns.email)) { + validationErrors.email = 'Invalid email format'; + } + if ( + toUpdate.biography && + !validateField('biography', toUpdate.biography, validationPatterns.biography) + ) { + validationErrors.biography = 'Biography must not exceed 500 characters'; + } + if ( + toUpdate.githubUrl && + !validateField('githubUrl', toUpdate.githubUrl, validationPatterns.githubUrl) + ) { + validationErrors.githubUrl = 'Invalid GitHub URL format'; + } + if ( + toUpdate.password && + !validateField('password', toUpdate.password, validationPatterns.password) + ) { + validationErrors.password = + 'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character (@$!%*#?&)'; + } + if (toUpdate.role && !validateField('role', toUpdate.role, validationPatterns.role)) { + validationErrors.role = 'Invalid role'; + } + if (toUpdate.mobile && !validateField('mobile', toUpdate.mobile, validationPatterns.mobile)) { + validationErrors.mobile = 'Invalid mobile number format'; + } + + // Check for validation errors + if (Object.keys(validationErrors).length > 0) { + let errormsg = ''; + Object.keys(validationErrors).forEach( + (key) => (errormsg = errormsg + validationErrors[key] + '\n') + ); + return { + isValid: false, + message: errormsg + }; + } + + return { + isValid: true, + message: '' + }; +}; + +export const validFirstName = (firstName) => { + if (firstName.length < 1) { + return { + isValid: false, + message: 'First name is required' + }; + } +}; +export const validLastName = (lastName) => { + if (lastName.length < 1) { + return { + isValid: false, + message: 'last name is required' + }; + } +}; diff --git a/src/service/tokenService.js b/src/service/tokenService.js new file mode 100644 index 00000000..590a5d4c --- /dev/null +++ b/src/service/tokenService.js @@ -0,0 +1,10 @@ +import jwtDecode from 'jwt-decode'; + +export const getId = () => { + const token = localStorage.getItem('token'); + if (token) { + const decoded = jwtDecode(token); + return decoded.userId; + } + return -1; +}; diff --git a/src/styles/_form.css b/src/styles/_form.css index 419cfb63..837ceb7c 100644 --- a/src/styles/_form.css +++ b/src/styles/_form.css @@ -31,6 +31,14 @@ form label { padding-right: 10px; } +.input-description{ + color: var(--color-blue2); +} + +.input-invalid { + color: var(--color-red) +} + .showpasswordbutton { position: absolute; bottom: 28px; diff --git a/src/styles/_globals.css b/src/styles/_globals.css index eb9772c6..6d4d1b3b 100644 --- a/src/styles/_globals.css +++ b/src/styles/_globals.css @@ -8,6 +8,7 @@ --color-green: #64dc78; --color-offwhite: #f0f5fa; --color-lightgrey: #f5f5f5; + --color-red: #F00000; } /* global settings here, resetting a lot of defaults */