diff --git a/.eslintrc.json b/.eslintrc.json index 2988e2ba..6b70ab54 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,4 +23,4 @@ "version": "detect" } } -} +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 136c3a15..d92e7c64 100644 --- a/src/App.js +++ b/src/App.js @@ -8,6 +8,9 @@ import Verification from './pages/verification'; import { AuthProvider, ProtectedRoute } from './context/auth'; import { ModalProvider } from './context/modal'; import Welcome from './pages/welcome'; +import UserProfile from './pages/profilePage'; +import Cohort from './pages/cohort'; +import Search from './pages/search'; const App = () => { return ( @@ -20,6 +23,24 @@ const App = () => { } /> } /> + + + + } + /> + + + + + } + /> + { } /> + + + + + } + /> + { } /> + + + + } + /> + + + + } + /> diff --git a/src/assets/icons/arrowLeftIcon.js b/src/assets/icons/arrowLeftIcon.js new file mode 100644 index 00000000..f4b9e9d5 --- /dev/null +++ b/src/assets/icons/arrowLeftIcon.js @@ -0,0 +1,11 @@ +const arrowLeftIcon = () => ( + + + + +); + +export default arrowLeftIcon; diff --git a/src/components/createCohortModal/index.js b/src/components/createCohortModal/index.js new file mode 100644 index 00000000..82b48867 --- /dev/null +++ b/src/components/createCohortModal/index.js @@ -0,0 +1,74 @@ +import useModal from '../../hooks/useModal'; +import { useState } from 'react'; +import Button from '../button'; + +const CreateCohortModal = () => { + const { closeModal } = useModal(); + const [title, setTitle] = useState(''); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + const onChange = (e) => { + const { name, value } = e.target; + if (name === 'title') setTitle(value); + if (name === 'startDate') setStartDate(value); + if (name === 'endDate') setEndDate(value); + }; + + const onSubmit = () => { + console.log('Submit button was clicked! Closing modal in 2 seconds...'); + console.log(title); + console.log(startDate); + console.log(endDate); + setTimeout(() => { + closeModal(); + }, 2000); + }; + + const today = new Date().toISOString().split('T')[0]; + + return ( +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ ); +}; + +export default CreateCohortModal; diff --git a/src/components/createCohortModal/style.css b/src/components/createCohortModal/style.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/errorMessage/_errorMessage.css b/src/components/errorMessage/_errorMessage.css new file mode 100644 index 00000000..0f1bc2c4 --- /dev/null +++ b/src/components/errorMessage/_errorMessage.css @@ -0,0 +1,12 @@ +.simple-error-message { + background: var(--color-offwhite); + padding: 24px; + border-radius: 8px; + width: 100%; + margin-bottom: 25px; + /* + The value for red color below is taken from the style guide in the Figma Design Document. + It could be implemented as a global CSS variable in the _globals.css file instead. + */ + border: 1px #f00000 solid; +} diff --git a/src/components/errorMessage/index.js b/src/components/errorMessage/index.js new file mode 100644 index 00000000..0ff02ec1 --- /dev/null +++ b/src/components/errorMessage/index.js @@ -0,0 +1,7 @@ +import './_errorMessage.css'; + +const ErrorMessage = ({ message }) => { + return
{message}
; +}; + +export default ErrorMessage; diff --git a/src/components/form/textInput/index.js b/src/components/form/textInput/index.js index 39da3cae..b4121a40 100644 --- a/src/components/form/textInput/index.js +++ b/src/components/form/textInput/index.js @@ -1,9 +1,110 @@ -import { useState } from 'react'; +import './textInput.css'; +import { useEffect, useState } from 'react'; -const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { +/** + * validChars: Characters that are allowed in the input, the user can't type characters that are not included in validChars + * - + * pattern: Required pattern/format for email or password. + * patternDescription: Requirements can be passed in via patternDescription, it will be displayed as help messages to the user if the input doesn't match the pattern + */ +const TextInput = ({ + value, + onChange, + name, + label, + icon, + type = 'text', + isRequired = false, + validChars = 'A-Za-z0-9@_-', + pattern = null, + patternDescription = null, + minLength = 0, + maxLength = 50, + isLocked = false +}) => { const [input, setInput] = useState(''); + const [error, setError] = useState(''); const [showpassword, setShowpassword] = useState(false); - if (type === 'password') { + + useEffect(() => { + if (isRequired) { + if (value != null) { + if (value.length === 0) { + setError(`${label.slice(0, -1)} is required`); + } else { + setError(''); + } + } + } + }, [isRequired, value]); + + const validateInput = (value, event) => { + const regex = new RegExp(`^[${validChars}]+$`); + const isValid = regex.test(value) && value.length <= maxLength; + if (!isRequired) { + onChange(event); + } else if (value.length === 0 && isRequired) { + setError(`${label.slice(0, -1)} is required`); + onChange(event); + } else if (!isValid) { + setError( + `Input must be up to ${maxLength} characters long and contain only: ${validChars.split('').join(', ')}` + ); + } else if (pattern && !pattern.test(value)) { + if (patternDescription) { + setError(`${patternDescription}`); + } else { + setError(`Input must match the pattern: ${pattern}`); + } + onChange(event); + } else if (value.length < minLength) { + setError(`Input must be at least ${minLength} characters long`); + onChange(event); + } else { + setError(''); + } + return isValid; + }; + + const handleChange = (event) => { + const { value } = event.target; + if (validateInput(value, event)) { + onChange(event); + } + setInput(value); + }; + + if (isLocked && type === 'password') { + return ( +
+ + + {showpassword && } + + +
+ ); + } else if (isLocked) { + return ( +
+ + + +
+ ); + } else if (type === 'password') { return (
@@ -11,10 +112,8 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { type={type} name={name} value={value} - onChange={(e) => { - onChange(e); - setInput(e.target.value); - }} + onChange={handleChange} + className={error && 'input-error'} /> {showpassword && } + {error && {error}} +
+ ); + } else if (type === 'email') { + return ( +
+ {label && } + + {icon && {icon}} + {error && {error}}
); } else { @@ -36,10 +152,11 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { type={type} name={name} value={value} - onChange={onChange} - className={icon && 'input-has-icon'} + onChange={handleChange} + className={(error && 'input-error') || (icon && 'input-has-icon')} /> {icon && {icon}} + {error && {error}} ); } @@ -56,4 +173,15 @@ const EyeLogo = () => { ); }; +const LockLogo = () => { + return ( + + + + ); +}; + export default TextInput; diff --git a/src/components/form/textInput/textInput.css b/src/components/form/textInput/textInput.css new file mode 100644 index 00000000..ea50f85a --- /dev/null +++ b/src/components/form/textInput/textInput.css @@ -0,0 +1,3 @@ +.error-message { + font-size: 0.8rem; +} \ No newline at end of file diff --git a/src/components/header/index.js b/src/components/header/index.js index c591f1e1..d84fd761 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -5,17 +5,26 @@ import Card from '../card'; import ProfileIcon from '../../assets/icons/profileIcon'; import CogIcon from '../../assets/icons/cogIcon'; import LogoutIcon from '../../assets/icons/logoutIcon'; -import { NavLink } from 'react-router-dom'; -import { useState } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { useEffect, useState } from 'react'; + +// eslint-disable-next-line camelcase +import jwt_decode from 'jwt-decode'; const Header = () => { const { token, onLogout } = useAuth(); const [isMenuVisible, setIsMenuVisible] = useState(false); + const location = useLocation(); + const { userId } = jwt_decode(token); const onClickProfileIcon = () => { setIsMenuVisible(!isMenuVisible); }; + useEffect(() => { + setIsMenuVisible(false); + }, [location]); + if (!token) { return null; } @@ -45,7 +54,7 @@ const Header = () => {
  • - +

    Profile

  • diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js index b31393a8..1960057d 100644 --- a/src/components/navigation/index.js +++ b/src/components/navigation/index.js @@ -5,8 +5,12 @@ import ProfileIcon from '../../assets/icons/profileIcon'; import useAuth from '../../hooks/useAuth'; import './style.css'; +// eslint-disable-next-line camelcase +import jwt_decode from 'jwt-decode'; + const Navigation = () => { const { token } = useAuth(); + const { userId } = jwt_decode(token); if (!token) { return null; @@ -22,13 +26,13 @@ const Navigation = () => {
  • - +

    Profile

  • - +

    Cohort

    diff --git a/src/components/post/index.js b/src/components/post/index.js index 337ca5a6..6558a49f 100644 --- a/src/components/post/index.js +++ b/src/components/post/index.js @@ -1,12 +1,17 @@ +import { useState } from 'react'; import useModal from '../../hooks/useModal'; import Card from '../card'; -import Comment from '../comment'; +// import Comment from '../comment'; import EditPostModal from '../editPostModal'; import ProfileCircle from '../profileCircle'; import './style.css'; -const Post = ({ name, date, content, comments = [], likes = 0 }) => { +const Post = ({ name, date, content, initialComments = [], initialLikes = 0 }) => { const { openModal, setModal } = useModal(); + const [comments, setComments] = useState(initialComments); + const [comment, setComment] = useState(''); + const [likes, setLikes] = useState(initialLikes); + const [isLiked, setIsLiked] = useState(false); const userInitials = name.match(/\b(\w)/g); @@ -15,6 +20,24 @@ const Post = ({ name, date, content, comments = [], likes = 0 }) => { openModal(); }; + const handleSubmit = (event) => { + const newCommentObj = { + commentName: name, + content: comment + }; + setComments([...comments, newCommentObj]); + setComment(''); + }; + + const toggleLike = () => { + if (isLiked) { + setLikes(likes - 1); + } else { + setLikes(likes + 1); + } + setIsLiked(!isLiked); + }; + return (
    @@ -39,17 +62,48 @@ const Post = ({ name, date, content, comments = [], likes = 0 }) => { className={`post-interactions-container border-top ${comments.length ? 'border-bottom' : null}`} >
    -
    Like
    -
    Comment
    +

    {!likes && 'Be the first to like this'}

-
+ {/*
{comments.map((comment) => ( - +
+ +
+ +
+
))} +
*/} +
+ {comments.map((comment, index) => ( +
+ +
+

{'Alex Jameson'}

+

{comment.content}

+
+
+ ))} +
+
+
+ +
+
+ setComment(e.target.value)} + /> + +
diff --git a/src/components/post/style.css b/src/components/post/style.css index 3eff5afc..a77f808d 100644 --- a/src/components/post/style.css +++ b/src/components/post/style.css @@ -29,6 +29,14 @@ background: #f0f5fa; } +.edit-icon:hover { + background-color: #3068a5; +} + +.edit-icon:active { + background-color: #052e5a; +} + .edit-icon p { text-align: center; font-size: 20px; @@ -40,11 +48,106 @@ padding: 20px 10px; } +.post-interactions-container p { + text-align: right; +} + +#comment-field { + background-color: rgb(219, 208, 192); + display: flex; + padding-left: 100px; + + + border-top: 1px solid #ccc; +} + +.comment-section { + display: flex; + align-items: center; + padding: 10px 0; + border-top: 1px solid #ccc; +} + +.comment-section .user-name { + margin-right: 10px; +} + +.comment-section .comment-input { + display: flex; + align-items: center; + flex: 1; +} + +.comment-section .comment-input input { + width: 100%; + padding: 8px; + margin-right: 10px; + background-color: #f0f0f0; + border-radius: 8px; + box-sizing: border-box; +} + +.comment-section .comment-input button { + padding: 8px 16px; +} + +.user-name .comment-section { + display: flex; + align-items: center; + padding: 10px 0; + border-top: 1px solid #ccc; +} + +#like-button { + background-color: transparent; + border: none; + color: #3068a5; + cursor: pointer; + font-size: 16px; + padding: 8px 16px; +} + +#like-button.liked { + color: #10c756; + font-weight: bold; +} + +#like-button:hover { + text-decoration: underline; +} + .post-interactions { - display: grid; - grid-template-columns: 1fr 1fr; + display: flex; + align-items: center; +} + +.post-interactions button { + margin-right: 10px; } -.post-interactions-container p { - text-align: right; +.comment-item { + display: flex; + align-items: flex-start; + margin-bottom: 15px; +} + +.comment-item .profile-circle { + margin-right: 10px; +} + +.comment-box { + background-color: #f0f0f0; + padding: 10px; + border-radius: 8px; + flex: 1; +} + +.comment-name { + font-weight: bold; + margin: 0 0 5px 0; +} + +.comment-text { + margin: 0; } + \ No newline at end of file diff --git a/src/components/posts/index.js b/src/components/posts/index.js index 79756c41..0f1ee8b1 100644 --- a/src/components/posts/index.js +++ b/src/components/posts/index.js @@ -7,6 +7,7 @@ const Posts = () => { useEffect(() => { getPosts().then(setPosts); + console.log(getPosts()); }, []); return ( diff --git a/src/components/stepper/index.js b/src/components/stepper/index.js index c9e5f259..553db15a 100644 --- a/src/components/stepper/index.js +++ b/src/components/stepper/index.js @@ -2,9 +2,9 @@ import Steps from './steps'; import Card from '../card'; import Button from '../button'; import './style.css'; -import { useState } from 'react'; +import React, { useState } from 'react'; -const Stepper = ({ header, children, onComplete }) => { +const Stepper = ({ header, children, onComplete, validate }) => { const [currentStep, setCurrentStep] = useState(0); const onBackClick = () => { @@ -16,10 +16,9 @@ const Stepper = ({ header, children, onComplete }) => { const onNextClick = () => { if (currentStep === children.length - 1) { onComplete(); - return; + } else if (validate(currentStep + 1)) { + setCurrentStep(currentStep + 1); } - - setCurrentStep(currentStep + 1); }; return ( diff --git a/src/components/stepper/style.css b/src/components/stepper/style.css index 8982959e..bb43047f 100644 --- a/src/components/stepper/style.css +++ b/src/components/stepper/style.css @@ -1,4 +1,5 @@ .stepper-buttons { + margin-top: 1rem; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..489fcedf 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -4,7 +4,7 @@ import Header from '../components/header'; import Modal from '../components/modal'; import Navigation from '../components/navigation'; import useAuth from '../hooks/useAuth'; -import { createProfile, login, register } from '../service/apiClient'; +import { updateProfile, getUserData, login, register } from '../service/apiClient'; // eslint-disable-next-line camelcase import jwt_decode from 'jwt-decode'; @@ -15,56 +15,131 @@ const AuthProvider = ({ children }) => { const navigate = useNavigate(); const location = useLocation(); const [token, setToken] = useState(null); + const [userCredentials, setUserCredentials] = useState({ email: '', password: '' }); + const [role, setRole] = useState(null); useEffect(() => { const storedToken = localStorage.getItem('token'); + const storedRole = localStorage.getItem('role'); - if (storedToken) { + const storedUserCredentials = JSON.parse(localStorage.getItem('userCredentials')); + + if (storedToken && storedRole && storedUserCredentials) { setToken(storedToken); - navigate(location.state?.from?.pathname || '/'); + setRole(storedRole); + setUserCredentials(storedUserCredentials); + navigate(location.pathname || '/'); + } else { + navigate('/login'); } - }, [location.state?.from?.pathname, navigate]); + }, []); const handleLogin = async (email, password) => { const res = await login(email, password); - if (!res.data.token) { + if (!res.data.token || !res.data.user.role) { return navigate('/login'); } localStorage.setItem('token', res.data.token); + localStorage.setItem('role', res.data.user.role); + localStorage.setItem('userCredentials', JSON.stringify({ email, password })); - setToken(res.token); - navigate(location.state?.from?.pathname || '/'); + setToken(res.data.token); + setRole(res.data.user.role); + setUserCredentials({ email, password }); + navigate('/'); }; const handleLogout = () => { localStorage.removeItem('token'); + localStorage.removeItem('role'); + localStorage.removeItem('userCredentials'); setToken(null); + setRole(null); + setUserCredentials({ email: '', password: '' }); }; - const handleRegister = async (email, password) => { + const handleRegister = async (email, password, setErrorMessage) => { const res = await register(email, password); + + if (res.status === 'fail') { + if (res.data.email) { + setErrorMessage(res.data.email); + } + return navigate('/register'); + } + + localStorage.setItem('token', res.data.token); + localStorage.setItem('role', res.data.user.role); + localStorage.setItem('userCredentials', JSON.stringify({ email, password })); + setToken(res.data.token); + setRole(res.data.user.role); + setUserCredentials({ email, password }); navigate('/verification'); }; - const handleCreateProfile = async (firstName, lastName, githubUrl, bio) => { - const { userId } = jwt_decode(token); - - await createProfile(userId, firstName, lastName, githubUrl, bio); + const handleUpdateProfile = async ( + firstName, + lastName, + bio, + username, + githubUsername, + profilePicture, + mobile, + id + ) => { + if (id == null) { + const { userId } = jwt_decode(token); + await updateProfile( + userId, + firstName, + lastName, + bio, + username, + githubUsername, + profilePicture, + mobile + ); + } else { + await updateProfile( + id, + firstName, + lastName, + bio, + username, + githubUsername, + profilePicture, + mobile + ); + } localStorage.setItem('token', token); + localStorage.setItem('role', role); navigate('/'); }; + const handleGetUserById = async (id) => { + if (id == null) { + const { userId } = jwt_decode(token); + + return await getUserData(userId); + } else { + return await getUserData(id); + } + }; + const value = { token, + userCredentials, + role, onLogin: handleLogin, onLogout: handleLogout, onRegister: handleRegister, - onCreateProfile: handleCreateProfile + onUpdateProfile: handleUpdateProfile, + onGetUser: handleGetUserById }; return {children}; @@ -73,7 +148,6 @@ const AuthProvider = ({ children }) => { const ProtectedRoute = ({ children }) => { const { token } = useAuth(); const location = useLocation(); - if (!token) { return ; } diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js new file mode 100644 index 00000000..724540ac --- /dev/null +++ b/src/pages/cohort/index.js @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +// import Card from '../../components/card'; +import './style.css'; +import { getCohorts } from '../../service/apiClient'; +import useAuth from '../../hooks/useAuth'; +// eslint-disable-next-line camelcase +import jwt_decode from 'jwt-decode'; +import { useLocation } from 'react-router-dom'; +import TeacherView from './teacher'; +import StudentView from './student'; + +const Cohort = () => { + const { token, role } = useAuth(); + const [cohort, setCohort] = useState([]); + const [students, setStudents] = useState([]); + const [teachers, setTeachers] = useState([]); + const [selectedCohort, setSelectedCohort] = useState({}); + const location = useLocation(); + const { userId } = jwt_decode(token); + + const fetchCohorts = async (cohortId) => { + const allCohorts = await getCohorts(); + const userCohorts = []; + + // Filter cohorts to include only those the user belongs to + allCohorts.forEach((cohort) => { + if (cohort.users.some((user) => user.id === userId)) { + userCohorts.push(cohort); + } + }); + + setCohort(userCohorts); + + // Find the cohort by ID if specified; otherwise, default to the first cohort + const initialCohort = userCohorts.find((cohort) => cohort.id === cohortId) || userCohorts[0]; + + if (initialCohort) { + setSelectedCohort(initialCohort); + setStudents(initialCohort.users.filter((user) => user.role === 'STUDENT')); + setTeachers(initialCohort.users.filter((user) => user.role === 'TEACHER')); + } + }; + + useEffect(() => { + const cohortId = location.state?.cohortId; + console.log(cohortId); + fetchCohorts(cohortId); + }, [location.state]); + + const handleCohortChange = (event) => { + const selectedCohortId = event.target.value; + const selectedCohort = cohort.find((c) => c.id === parseInt(selectedCohortId)); + setSelectedCohort(selectedCohort); + setStudents(selectedCohort.users.filter((user) => user.role === 'STUDENT')); + setTeachers(selectedCohort.users.filter((user) => user.role === 'TEACHER')); + }; + + return ( + <> + {role === 'TEACHER' ? ( + + ) : ( + + )} + + ); +}; + +export default Cohort; diff --git a/src/pages/cohort/student.js b/src/pages/cohort/student.js new file mode 100644 index 00000000..9462d4ff --- /dev/null +++ b/src/pages/cohort/student.js @@ -0,0 +1,163 @@ +import Card from '../../components/card'; + +const StudentView = ({ cohort, handleCohortChange, selectedCohort, students, teachers }) => { + if (cohort.length === 0) { + return ( +
+

Fetching Cohorts

+
+ +
+
+ ); + } + + return ( + <> +
+ +
+

My Cohort

+
+ +
+
+

< >

+
+
+ {/* This is a drop down menu so the user can change which cohort to view. */} +
+ +
+ + + {new Date(selectedCohort.startDate).toLocaleString('default', { + month: 'long', + year: 'numeric' + })}{' '} + -{' '} + {/* ADD space between the dash, ESLINT removs it, therefore I Added {' '} and {' '} */} + {new Date(selectedCohort.endDate).toLocaleString('default', { + month: 'long', + year: 'numeric' + })} + +
+
+ {/* This is where the list of students will go, User-list is the parent div for all the students */} +
+ {/* + This is a student card and it uses the profile-icon of the first letter. + Loop then through and fetch all the user data from the cohort. */} + {students.length > 0 ? ( + students.map((user) => { + return user.role === 'STUDENT' ? ( +
+
+
+

+ {user.profile.firstName.charAt(0) + '' + user.profile.lastName.charAt(0)} +

+
+
+

+ {user.profile.firstName} {user.profile.lastName} +

+
+ {/* This is the button to view the profile of the student and need to implement action of button. */} +
+ +
+
+
+ ) : null; + }, []) + ) : ( +

No students in this Cohort

+ )} +
+
+
+ + + + ); +}; + +export default StudentView; diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css new file mode 100644 index 00000000..3beb6a33 --- /dev/null +++ b/src/pages/cohort/style.css @@ -0,0 +1,58 @@ +.cohort-title { + height: 56px; + border-bottom: 1px solid #dce1f0; + margin-bottom: 1rem; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.cohort-div { + border-bottom: 1px solid #dce1f0; + height: 72px; +} + +.student-card { + height: 72px; + width: 365px; + padding-right: 1.5rem; +} + +.user-list { + margin-top: 1rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(365px, 1fr)); +} + +.cohort-student-name { + margin-top: 10px; +} + +.cohort-action-button { + padding: 0px 0px !important; + margin-top: 5px; +} + +.loading { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.cohort-option { + font-size: 1rem; +} + +.cohort-select { + font-size: 24px; +} + +.cohort-dropdown { + margin-right: 2rem; +} + +.start-and-end-date{ + margin-left: 5px !important; +} \ No newline at end of file diff --git a/src/pages/cohort/teacher.js b/src/pages/cohort/teacher.js new file mode 100644 index 00000000..e9a81725 --- /dev/null +++ b/src/pages/cohort/teacher.js @@ -0,0 +1,185 @@ +import Card from '../../components/card'; +import CreateCohortModal from '../../components/createCohortModal'; +import useModal from '../../hooks/useModal'; + +const TeacherView = ({ cohort, handleCohortChange, selectedCohort, students, teachers }) => { + // Use the useModal hook to get the openModal and setModal functions + 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('Create a Cohort', ); // CreatePostModal is just a standard React component, nothing special + + // Open the modal! + openModal(); + }; + + if (cohort.length === 0) { + return ( +
+
+

Cohorts cannot be found

+ +
+
+ ); + } + return ( + <> +
+ +
+

My Cohort

+
+
+ +
+
+
+ +
+
+

< >

+
+
+ {/* This is a drop down menu so the user can change which cohort to view. */} +
+ +
+ + + {new Date(selectedCohort.startDate).toLocaleString('default', { + month: 'long', + year: 'numeric' + })}{' '} + -{' '} + {/* ADD space between the dash, ESLINT removs it, therefore I Added {' '} and {' '} */} + {new Date(selectedCohort.endDate).toLocaleString('default', { + month: 'long', + year: 'numeric' + })} + +
+
+ {/* This is where the list of students will go, User-list is the parent div for all the students */} +
+ {/* + This is a student card and it uses the profile-icon of the first letter. + Loop then through and fetch all the user data from the cohort. */} + {students.length > 0 ? ( + students.map((user) => { + return user.role === 'STUDENT' ? ( +
+
+
+

+ {user.profile.firstName.charAt(0) + '' + user.profile.lastName.charAt(0)} +

+
+
+

+ {user.profile.firstName} {user.profile.lastName} +

+
+ {/* This is the button to view the profile of the student and need to implement action of button. */} +
+ +
+
+
+ ) : null; + }, []) + ) : ( +

No students in this Cohort

+ )} +
+
+
+ + + + ); +}; + +export default TeacherView; diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js index 54606849..721d358a 100644 --- a/src/pages/dashboard/index.js +++ b/src/pages/dashboard/index.js @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useNavigate, NavLink } from 'react-router-dom'; +import { useEffect, useState } from 'react'; import SearchIcon from '../../assets/icons/searchIcon'; import Button from '../../components/button'; import Card from '../../components/card'; @@ -6,24 +7,52 @@ import CreatePostModal from '../../components/createPostModal'; import TextInput from '../../components/form/textInput'; import Posts from '../../components/posts'; import useModal from '../../hooks/useModal'; +import StudentSearchResults from './search'; +import useAuth from '../../hooks/useAuth'; import './style.css'; +import { getCohorts } from '../../service/apiClient'; + +// eslint-disable-next-line camelcase +import jwt_decode from 'jwt-decode'; const Dashboard = () => { const [searchVal, setSearchVal] = useState(''); + const { role } = useAuth(); + const navigate = useNavigate(); // Added this line + const [cohorts, setCohorts] = useState([]); + const { token } = useAuth(); + + useEffect(() => { + const fetchCohorts = async () => { + const allCohorts = await getCohorts(); + const cohortToAdd = []; + const { userId } = jwt_decode(token); + // Map out the cohorts to check if the user is in the cohort + allCohorts.forEach((cohort) => { + if (cohort.users.some((user) => user.id === userId)) { + cohortToAdd.push(cohort); + } + }); + // SET STARTING POINT OF THE COHORT TO THE FIRST COHORT IN THE ARRAY + setCohorts(cohortToAdd); + }; + fetchCohorts(); + }, []); const onChange = (e) => { - setSearchVal(e.target.value); + const value = e.target.value; + setSearchVal(value); + }; + + const onSearch = (e) => { + e.preventDefault(); + navigate(`/dashboard/search/${searchVal}`); }; - // Use the useModal hook to get the openModal and setModal functions 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('Create a post', ); // CreatePostModal is just a standard React component, nothing special - - // Open the modal! + setModal('Create a post', ); openModal(); }; @@ -44,13 +73,48 @@ const Dashboard = () => { diff --git a/src/pages/dashboard/search/index.js b/src/pages/dashboard/search/index.js new file mode 100644 index 00000000..198575cc --- /dev/null +++ b/src/pages/dashboard/search/index.js @@ -0,0 +1,69 @@ +import ProfileCircle from '../../../components/profileCircle'; +import { useState, useEffect } from 'react'; +import './style.css'; +import { getUsers } from '../../../service/apiClient'; + +const StudentSearchResults = ({ searchVal }) => { + const [profiles, setProfiles] = useState([]); + const [searchUsers, setSearchUsers] = useState([]); + const hasResults = searchUsers.length > 0; + const displayUsers = searchUsers.slice(0, 10); // Show up to 10 users + + useEffect(() => { + if (searchVal.length > 0) { + const filteredUsers = profiles.filter((user) => + `${user.firstName} ${user.lastName}`.toLowerCase().includes(searchVal.toLowerCase()) + ); + setSearchUsers(filteredUsers); + } else { + setSearchUsers([]); // Clear search results if input is empty + } + }, [searchVal]); // Only runs when searchVal changes + + useEffect(() => { + const fetchUsers = async () => { + const allUsers = await getUsers(); + setProfiles(allUsers); + }; + fetchUsers(); + }, []); // Add an empty dependency array to run only once + + // Don't render anything if searchVal is empty + if (!searchVal.trim()) { + return null; + } + + return ( +
+

People

+
+ + {hasResults ? ( + <> + {displayUsers.map((user) => ( +
+
+ +
+
+

+ {user.firstName} {user.lastName} +

+

{user.specialism}

+
+
+ ))} + {searchUsers.length > 10 && } + + ) : ( +

+ Sorry, no results found +
+ Try changing your search term +

+ )} +
+ ); +}; + +export default StudentSearchResults; diff --git a/src/pages/dashboard/search/style.css b/src/pages/dashboard/search/style.css new file mode 100644 index 00000000..dd9193e2 --- /dev/null +++ b/src/pages/dashboard/search/style.css @@ -0,0 +1,61 @@ +.search-results { + padding: 0.5rem; + background-color: #ffffff; + border-radius: 5px; + /* Remove max-height to prevent auto-scroll for under 10 users */ + max-height: 100%; + overflow-y: auto; + } + + .search-user { + display: flex; + align-items: center; + padding: 0.5rem; + cursor: pointer; + } + + .search-user:hover { + background-color: #ffffff; + } + + .search-user p { + margin: 0; + } + + .profile-circle-container { + margin-right: 0.5rem; /* Space between profile and text */ + } + + .user-details { + display: flex; + flex-direction: column; + } + + .search-results-line { + border: none; + border-top: 2px solid rgb(210, 210, 210); + margin: 0.5rem 0; + opacity: 0.8; + } + + .show-all-button, + .edit-search-button { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem; + background-color: #d7dce3; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + } + + .show-all-button:hover, + .edit-search-button:hover { + background-color: #666; + } + + .no-results-message { + margin-bottom: 0.5rem; + } + \ No newline at end of file diff --git a/src/pages/dashboard/style.css b/src/pages/dashboard/style.css index f55ef0a7..3045e9e1 100644 --- a/src/pages/dashboard/style.css +++ b/src/pages/dashboard/style.css @@ -19,3 +19,22 @@ aside { max-width: 100% !important; background-color: var(--color-blue5); } + +.cohort-nav-link { + text-decoration: none; +} + +.cohort-div { + margin-top: 15px; +} + +.dashboard-cohort { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(365px, 1fr)); + max-height: 400px; + overflow: auto; +} + +.dashboard-cohort-title { + margin-bottom: 0rem !important; +} \ No newline at end of file diff --git a/src/pages/profilePage/components/BasicInfoForm.jsx b/src/pages/profilePage/components/BasicInfoForm.jsx new file mode 100644 index 00000000..1aa9a705 --- /dev/null +++ b/src/pages/profilePage/components/BasicInfoForm.jsx @@ -0,0 +1,61 @@ +import { useContext } from 'react'; +import { ProfileContext } from '..'; +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; +import ProfileCircle from '../../../components/profileCircle'; + +const BasicInfoForm = () => { + const { handleInputChange, profile, initials, isEditMode } = useContext(ProfileContext); + + return ( +
+
+
+

Basic Info

+
+
+ Photo +
+ + Add headshot +
+
+ + + + +
+
+
+ ); +}; + +export default BasicInfoForm; diff --git a/src/pages/profilePage/components/BioForm.jsx b/src/pages/profilePage/components/BioForm.jsx new file mode 100644 index 00000000..88339ec3 --- /dev/null +++ b/src/pages/profilePage/components/BioForm.jsx @@ -0,0 +1,37 @@ +import React, { useContext } from 'react'; +import Form from '../../../components/form'; +import { ProfileContext } from '..'; + +const BioForm = () => { + const { profile, handleBioChange, isEditMode } = useContext(ProfileContext); + const maxLength = 300; + const isMaxLengthReached = profile.bio.length >= maxLength; + + return ( +
+
+
+
+

Bio

+ +
+ +
+

{profile.bio.length}/300

+ {isMaxLengthReached &&

Max length is 300 characters

} +
+
+
+
+
+ ); +}; + +export default BioForm; diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx new file mode 100644 index 00000000..b195c457 --- /dev/null +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -0,0 +1,48 @@ +import React, { useContext } from 'react'; +import { ProfileContext } from '..'; +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const ContactInfoForm = () => { + const { profile, handleInputChange, isEditMode } = useContext(ProfileContext); + + return ( +
+
+
+

Contact Info

+
+ + + +
+
+
+ ); +}; + +export default ContactInfoForm; diff --git a/src/pages/profilePage/components/ProfileHeader.jsx b/src/pages/profilePage/components/ProfileHeader.jsx new file mode 100644 index 00000000..cfc757a3 --- /dev/null +++ b/src/pages/profilePage/components/ProfileHeader.jsx @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { ProfileContext } from '..'; +import ProfileCircle from '../../../components/profileCircle'; + +const ProfileHeader = () => { + const { initials, profile } = useContext(ProfileContext); + return ( +
+ +
+

+ {profile.firstName} {profile.lastName} +

+

{profile.role}

+
+
+ ); +}; + +export default ProfileHeader; diff --git a/src/pages/profilePage/components/TrainingInfoForm.jsx b/src/pages/profilePage/components/TrainingInfoForm.jsx new file mode 100644 index 00000000..2124ac74 --- /dev/null +++ b/src/pages/profilePage/components/TrainingInfoForm.jsx @@ -0,0 +1,74 @@ +import { useContext } from 'react'; +import { ProfileContext } from '..'; +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const TrainingInfoForm = () => { + const { profile, handleInputChange, formatRole, isEditMode, isCurrentUserTeacher } = + useContext(ProfileContext); + + return ( +
+
+
+

Training Info

+
+ + + + + +
+
+
+ ); +}; + +export default TrainingInfoForm; diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js new file mode 100644 index 00000000..b84c15eb --- /dev/null +++ b/src/pages/profilePage/index.js @@ -0,0 +1,151 @@ +import { createContext, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { user } from '../../service/mockData'; +import Card from '../../components/card'; +import './profilePage.css'; +import ProfileHeader from './components/ProfileHeader'; +import BasicInfoForm from './components/BasicInfoForm'; +import TrainingInfoForm from './components/TrainingInfoForm'; +import ContactInfoForm from './components/ContactInfoForm'; +import BioForm from './components/BioForm'; +import Button from '../../components/button'; + +// eslint-disable-next-line camelcase +import jwt_decode from 'jwt-decode'; +import useAuth from '../../hooks/useAuth'; + +export const ProfileContext = createContext(); + +const UserProfile = ({ isEditMode }) => { + const navigate = useNavigate(); + const { profileId } = useParams(); + const [profile, setProfile] = useState(null); + const [rollbackProfile, setRoolbackProfile] = useState(null); + const [initials, setInitials] = useState(''); + const [isCurrentUserProfile, setIsCurrentUserProfile] = useState(false); + const [isCurrentUserTeacher, setIsCurrentUserTeacher] = useState(false); + + const { token, role } = useAuth(); + const { userId } = jwt_decode(token); + + const fetchProfile = () => { + setProfile(user.user); + setRoolbackProfile(user.user); + setInitials(user.user.firstName[0] + user.user.lastName[0]); + /* try { + const data = await get(`profiles/${profileId}`); + setProfile(data); + const initials = data.user.firstName[0] + data.user.lastName[0]; + setInitials(initials); + } catch (error) { + console.error('Error fetching profile data:', error); + } */ + }; + + useEffect(() => { + fetchProfile(); + if (userId === Number(profileId)) { + setIsCurrentUserProfile(true); + } + if (role === 'TEACHER') { + setIsCurrentUserTeacher(true); + } + }, [profileId]); + + const handleSubmit = (e) => { + // Handle form submission + e.preventDefault(); + navigate(`/profile/${userId}`); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setProfile((prevProfile) => ({ + ...prevProfile, + [name]: value + })); + }; + + const handleBioChange = (event) => { + handleInputChange(event); + }; + + const handleCancel = () => { + setProfile(rollbackProfile); + navigate(`/profile/${userId}`); + }; + + const formatRole = (role) => { + if (!role) return ''; + return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase(); + }; + + if (!profile) { + return

Loading...

; + } + + const contextValues = { + profile, + setProfile, + initials, + setInitials, + handleSubmit, + handleInputChange, + formatRole, + handleBioChange, + isEditMode, + isCurrentUserProfile, + isCurrentUserTeacher + }; + + return ( +
+

Profile

+ + + +
+ + + + +

*Required

+ {isEditMode ? ( + <> +
+
+ + ) : ( + (isCurrentUserProfile || isCurrentUserTeacher) && ( +
+
+
+
+ ); +}; + +export default UserProfile; diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css new file mode 100644 index 00000000..67e5a73d --- /dev/null +++ b/src/pages/profilePage/profilePage.css @@ -0,0 +1,182 @@ +.profile-page-main { + grid-column: 2 / span 2; /* Occupy the last two columns */ + grid-row: 2; /* Occupy row 2 */ + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 20px; + box-sizing: border-box; +} + +.profile-header { + margin-top: 10px; + align-self: flex-start; + margin: 24px; + font-style: normal; + font-weight: 600; + font-size: 40px; + line-height: 48px; +} + +.profile-info-header { + display: flex; + align-items: center; +} + +.profile-info-header .profile-icon { + width: 80px; + height: 80px; +} + +.profile-info-header .profile-icon p { + line-height: 77px; + font-size: 32px; + font-weight: 400; + align-items: center; +} + +.profile-info { + margin-left: 16px; +} + +.profile-info h3 { + font-style: normal; + font-weight: 600; + font-size: 32px; + line-height: 40px; + margin: 0; +} + +.profile-info p { + font-style: normal; + font-weight: 400; + font-size: 18px; + line-height: 24px; + color: var(--color-blue1); + margin: 0; +} + +.profile-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; +} + +.profile-grid-section h3 { + margin-bottom: 32px; /* Add space below section headers */ +} + +.profile-grid-section { + display: flex; + flex-direction: column; + gap: 32px; +} + +.profile-grid-section.read-only input, +.profile-grid-section.read-only textarea { + pointer-events: none; /* Disable all mouse events */ +} + +.photo-section { + display: flex; + flex-direction: column; +} + +.photo-label { + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: var(--color-blue1); + margin-left: 2px; +} + +.profile-circle-wrapper { + display: flex; + align-items: center; +} + +.add-headshot { + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #888; + margin-left: 16px; /* Add space between profile circle and add headshot text */ +} + +.profile-grid-section-bio .profile-grid-section { + gap: 0; +} + +.section-divider { + border: none; + border-top: 1px solid var(--color-blue5); + margin: 32px 0; /* Add vertical space around the divider */ +} + +textarea { + height: 287px; /* Set the height of the textarea */ + resize: none; /* Förhindrar användaren från att ändra storlek på textarea */ + margin: 0; +} + +.info-container { + display: flex; + align-items: center; /* Justera avståndet mellan texten och felmeddelandet */ +} + +.info-text, +.error-text { + color: var(--color-blue1); + font-size: 16px; + line-height: 24px; + margin-left: 10px; +} + +.error-text { + margin-left: auto; + margin-right: 10px; +} + +.required-text { + margin-left: 0; +} + +.button-group { + display: flex; + flex-direction: row; + gap: 8px; + gap: 16px; +} + +.button { + display: flex; + flex-direction: row; + justify-content: center; + + padding: 14px 24px; + + border-radius: 8px; + + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 28px; + text-align: center; + + flex: none; + order: 0; + flex-grow: 1; +} + +.edit-button, +.submit-button { + background: var(--color-blue); + color: #ffffff; +} + +.cancel-button { + background: var(--color-offwhite); + color: var(--color-blue1); +} diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 5cc70e32..af6cbb04 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -3,11 +3,35 @@ import Button from '../../components/button'; import TextInput from '../../components/form/textInput'; import useAuth from '../../hooks/useAuth'; import CredentialsCard from '../../components/credentials'; +import ErrorMessage from '../../components/errorMessage'; import './register.css'; const Register = () => { const { onRegister } = useAuth(); const [formData, setFormData] = useState({ email: '', password: '' }); + const [errorMessage, setErrorMessage] = useState(''); + + // Email validation + const emailPatternDescription = 'Please enter a valid email address'; + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; + const emailRegexValidChars = 'A-Za-z0-9@._-'; + const isValidEmail = formData.email.match(emailRegex); + + // Password validation + // The password should not be less than 8 characters in length + // The password should contain at least one uppercase character: /(?=.*[A-Z])/ + // The password should contain at least one number: /(?=.*[0-9])/ + // The password should contain at least one special character: /(?=.*[!@#$%^&*])/ + const patternDescription = + 'Password must contain at least one uppercase letter, one number, and one special character'; + const passwordRegex = /^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/; + const passwordRegexValidChars = 'A-Za-z0-9@._\\-!@#\\$%\\^&\\*'; + const hasValidLength = formData.password.length >= 8; + const isValidPassword = passwordRegex.test(formData.password) && hasValidLength; + + // Form validation + const isRequiredFieldsProvided = formData.email && formData.password; + const isFormDataValid = isRequiredFieldsProvided && isValidEmail && isValidPassword; const onChange = (e) => { const { name, value } = e.target; @@ -31,6 +55,10 @@ const Register = () => { type="email" name="email" label={'Email *'} + isRequired={true} + validChars={emailRegexValidChars} + pattern={emailRegex} + patternDescription={emailPatternDescription} /> { name="password" label={'Password *'} type={'password'} + isRequired={true} + validChars={passwordRegexValidChars} + pattern={passwordRegex} + patternDescription={patternDescription} + minLength={8} /> + {errorMessage && } +
*Required
+

Search Results

+ + + +
+ } + value={searchVal} + onChange={onChange} + name="Search" + placeholder="Search for people" + /> + +
+ +

People

+
+ {searchUsers.length > 0 && searchQuery !== undefined ? ( +
+ {searchUsers.map((user) => ( +
+
+ +
+
+

+ {user.firstName} {user.lastName} +

+

{user.specialism}

+
+
+ ))} +
+ ) : ( +

+ Sorry, no results found +
+ Try changing your search term +

+ )} +
+ + + ); +}; + +export default Search; diff --git a/src/pages/search/style.css b/src/pages/search/style.css new file mode 100644 index 00000000..75c9316d --- /dev/null +++ b/src/pages/search/style.css @@ -0,0 +1,10 @@ +.search-results-header-container { + display: flex; + align-items: center; +} +.search-results-page { + margin : 20; + justify-content: center; + align-items: center; + height: 100vh; +} \ No newline at end of file diff --git a/src/pages/welcome/index.js b/src/pages/welcome/index.js index 85af11ab..3c8a6b58 100644 --- a/src/pages/welcome/index.js +++ b/src/pages/welcome/index.js @@ -1,20 +1,64 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Stepper from '../../components/stepper'; import useAuth from '../../hooks/useAuth'; import StepOne from './stepOne'; -import StepTwo from './stepTwo'; +import StepFour from './stepFour'; import './style.css'; +import StepTwo from './stepTwo'; +import StepThree from './stepThree'; const Welcome = () => { - const { onCreateProfile } = useAuth(); + const { onUpdateProfile, onGetUser, userCredentials } = useAuth(); const [profile, setProfile] = useState({ firstName: '', lastName: '', + username: '', githubUsername: '', - bio: '' + bio: '', + profilePicture: '', + email: userCredentials.email, + mobile: '', + password: userCredentials.password, + role: 'Student', + specialism: 'Software Developer', + cohort: 'Cohort 4', + startDate: 'January 2023', + endDate: 'June 2023' }); + useEffect(() => { + const setData = (data) => { + if (data !== '') { + return data; + } + return ''; + }; + const fetchUserData = async () => { + try { + const userData = await onGetUser(); + console.log(userData); + // Set the profile to the user data given. + setProfile((prevProfile) => ({ + ...prevProfile, + firstName: setData(userData.firstName), + lastName: setData(userData.lastName), + username: setData(userData.username), + githubUsername: setData(userData.githubUsername), + bio: setData(userData.biography), + profilePicture: setData(userData.profilePicture), + mobile: setData(userData.mobile), + role: setData(userData.role), + specialism: setData(userData.specialism) + })); + } catch (error) { + console.error('Failed to fetch user data:', error); + } + }; + + fetchUserData(); + }, []); + const onChange = (event) => { const { name, value } = event.target; @@ -24,8 +68,66 @@ const Welcome = () => { }); }; + const validate = (step) => { + switch (step) { + case 0: + if ( + !profile.firstName || + !profile.lastName || + !profile.username || + !profile.githubUsername || + !profile.mobile || + !profile.role || + !profile.specialism || + !profile.cohort || + !profile.startDate || + !profile.endDate + ) { + return false; + } + break; + case 1: + if ( + !profile.firstName || + !profile.lastName || + !profile.username || + !profile.githubUsername + ) { + return false; + } + break; + case 2: + if (!profile.mobile) { + return false; + } + break; + case 3: + if ( + !profile.role || + !profile.specialism || + !profile.cohort || + !profile.startDate || + !profile.endDate + ) { + return false; + } + break; + } + return true; + }; + const onComplete = () => { - onCreateProfile(profile.firstName, profile.lastName, profile.githubUsername, profile.bio); + if (validate(0)) { + onUpdateProfile( + profile.firstName, + profile.lastName, + profile.bio, + profile.username, + profile.githubUsername, + profile.profilePicture, + profile.mobile + ); + } }; return ( @@ -35,9 +137,11 @@ const Welcome = () => {

Create your profile to get started

- } onComplete={onComplete}> + } onComplete={onComplete} validate={validate}> + + ); diff --git a/src/pages/welcome/stepFour/index.js b/src/pages/welcome/stepFour/index.js new file mode 100644 index 00000000..7851e65a --- /dev/null +++ b/src/pages/welcome/stepFour/index.js @@ -0,0 +1,28 @@ +import Form from '../../../components/form'; + +const StepFour = ({ data, setData }) => { + return ( + <> +
+

Bio

+
+
+
+ +
+
+ + ); +}; + +export default StepFour; diff --git a/src/pages/welcome/stepOne/index.js b/src/pages/welcome/stepOne/index.js index 317940f8..bf0acca4 100644 --- a/src/pages/welcome/stepOne/index.js +++ b/src/pages/welcome/stepOne/index.js @@ -1,8 +1,40 @@ +import { useEffect, useState } from 'react'; import ProfileIcon from '../../../assets/icons/profileIcon'; import Form from '../../../components/form'; import TextInput from '../../../components/form/textInput'; -const StepOne = ({ data, setData }) => { +const StepOne = ({ data, setData, errors }) => { + const [uploadedImageHex, setUploadedImageHex] = useState(data.profilePicture); + const [isPopupVisible, setIsPopupVisible] = useState(false); + + const handleImageUpload = (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + const base64String = reader.result.replace('data:', '').replace(/^.+,/, ''); + setUploadedImageHex(base64String); + setData({ target: { name: 'profilePicture', value: base64String } }); + setIsPopupVisible(false); + }; + reader.readAsDataURL(file); + } + }; + + useEffect(() => { + if (data.profilePicture != null) { + setUploadedImageHex(data.profilePicture); + } + }, [data.profilePicture]); + + const handleUploadPressed = () => { + setIsPopupVisible(true); + }; + + const handleCancel = () => { + setIsPopupVisible(false); + }; + return ( <>
@@ -12,24 +44,76 @@ const StepOne = ({ data, setData }) => {

Photo

- -

Add headshot

+ {uploadedImageHex ? ( + Profile + ) : ( + + )} +

Please upload a valid image file

+ {isPopupVisible && ( +
+
+

Upload photo

+

Choose a file to upload as your profile picture.

+
+ + + +
+
+
+ )}
+ + -

*Required

diff --git a/src/pages/welcome/stepThree/index.js b/src/pages/welcome/stepThree/index.js new file mode 100644 index 00000000..8b0f84c4 --- /dev/null +++ b/src/pages/welcome/stepThree/index.js @@ -0,0 +1,34 @@ +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const StepThree = ({ data, setData }) => { + 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..50b06462 100644 --- a/src/pages/welcome/stepTwo/index.js +++ b/src/pages/welcome/stepTwo/index.js @@ -1,14 +1,31 @@ import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; const StepTwo = ({ data, setData }) => { return ( <>
-

Bio

+

Contact info

- + + +

*Required

diff --git a/src/pages/welcome/style.css b/src/pages/welcome/style.css index 7ff35605..7cf4dfc1 100644 --- a/src/pages/welcome/style.css +++ b/src/pages/welcome/style.css @@ -1,3 +1,10 @@ +.welcome-form { + height: 30vh; + width: 80vw; + overflow: auto; + box-sizing: border-box; +} + .welcome-titleblock { margin-bottom: 32px; } @@ -19,6 +26,11 @@ gap: 16px; align-items: center; } +.welcome-form-profileimg-icon { + width: 40px; + height: 40px; + border-radius: 20px; +} .welcome-form-profileimg-error { color: transparent; } @@ -42,3 +54,89 @@ grid-template-columns: 1fr 1fr; gap: 24px; } +.welcome-form-profileimg-addimg-button { + background-color: #f0f5fa; + color: #64648c; + border-radius: 4px; + padding: 2px 4px; + border-width: 0; + transition: + background-color 0.3s, + color 0.3s; + white-space: nowrap; +} +.welcome-form-profileimg-addimg-button:hover { + background-color: #000046; + color: #ffffff; + border-radius: 4px; +} + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + z-index: 100; +} + +.popup-content { + display: flex; + flex-direction: column; + gap: 2.5rem; + background: white; + padding: 20px; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); + height: 15rem; +} + +.popup-content h3 { + text-align: left; +} +.popup-content p { + text-align: left; +} + +.popup-options { + display: flex; + gap: 10px; +} + +.popup-options button { + flex: 1; + background-color: #f0f5fa; + color: #64648c; + border-radius: 4px; + padding: 2px 4px; + border-width: 0; + transition: + background-color 0.3s, + color 0.3s; +} +.popup-options button:hover { + background-color: #000046; + color: #ffffff; +} + +.custom-file-upload { + display: inline-block; + flex: 1; + font-size: larger; + padding: 14px 24px; + cursor: pointer; + background-color: #000046; + color: #ffffff; + border-radius: 4px; + border-width: 0; + transition: + background-color 0.3s, + color 0.3s; +} +.custom-file-upload:hover { + background-color: #f0f5fa; + color: #64648c; +} diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..9664c720 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -5,12 +5,44 @@ async function login(email, password) { } async function register(email, password) { - await post('users', { email, password }, false); + const res = await post('users', { email, password }, false); + + if (res.status === 'fail') { + return res; + } + return await login(email, password); } -async function createProfile(userId, firstName, lastName, githubUrl, bio) { - return await patch(`users/${userId}`, { firstName, lastName, githubUrl, bio }); +async function updateProfile( + userId, + firstName, + lastName, + bio, + username, + githubUsername, + profilePicture, + mobile +) { + return await patch(`users/${userId}`, { + firstName, + lastName, + bio, + username, + githubUsername, + profilePicture, + mobile + }); +} + +async function getUserData(userId) { + const res = await get(`users/${userId}`); + return res.data.user; +} + +async function getUsers() { + const res = await get(`users`); + return res.data.users; } async function getPosts() { @@ -18,6 +50,11 @@ async function getPosts() { return res.data.posts; } +async function getCohorts() { + const res = await get('cohorts'); + return res.data.cohorts; +} + async function post(endpoint, data, auth = true) { return await request('POST', endpoint, data, auth); } @@ -52,4 +89,4 @@ async function request(method, endpoint, data, auth = true) { return response.json(); } -export { login, getPosts, register, createProfile }; +export { login, getPosts, register, updateProfile, getCohorts, getUserData, getUsers }; diff --git a/src/service/mockData.js b/src/service/mockData.js index d49e98a4..0ef02b5c 100644 --- a/src/service/mockData.js +++ b/src/service/mockData.js @@ -8,7 +8,14 @@ const user = { firstName: 'Joe', lastName: 'Bloggs', bio: 'Lorem ipsum dolor sit amet.', - githubUrl: 'https://github.com/vherus' + githubUrl: 'https://github.com/vherus', + githubUsername: 'vherus', + specialism: 'Full Stack', + startDate: 'January 2020', + endDate: 'June 2020', + username: 'test-user', + mobile: '0123456789', + password: 'password' } }; diff --git a/src/styles/_form.css b/src/styles/_form.css index 419cfb63..a9639892 100644 --- a/src/styles/_form.css +++ b/src/styles/_form.css @@ -21,6 +21,20 @@ form label { position: relative; } +.input-error { + border: 1px solid red; +} +.input-error:focus { + border: 1px solid red; +} + +.passwordreveal-error { + border: 1px solid red; +} +.passwordreveal-error:focus { + border: 1px solid red; +} + .input-icon { position: absolute; left: 16px; @@ -31,7 +45,7 @@ form label { padding-right: 10px; } -.showpasswordbutton { +.lockbutton { position: absolute; bottom: 28px; right: 16px; @@ -40,6 +54,16 @@ form label { transform: translateY(19px); transition: all 0.2s ease; } + +.showpasswordbutton { + position: absolute; + top: 20px; + right: 16px; + z-index: 2; + background: none; + transform: translateY(19px); + transition: all 0.2s ease; +} .showpasswordbutton:hover { opacity: 0.7; } @@ -47,9 +71,25 @@ form label { opacity: 0.4; } +.showpasswordbutton-duo { + position: absolute; + bottom: 28px; + right: 50px; + z-index: 2; + background: none; + transform: translateY(19px); + transition: all 0.2s ease; +} +.showpasswordbutton-duo:hover { + opacity: 0.7; +} +.showpasswordbutton-duo.__faded { + opacity: 0.4; +} + .passwordreveal { position: absolute; - bottom: 0; + /* bottom: 0; */ right: 0; width: 100%; z-index: 1;