From e5404885b195989e70bc3446a4059d617dedf712 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Mon, 28 Oct 2024 15:39:41 +0100 Subject: [PATCH 01/84] Created the Cohort page and added a design with mockup data of 'My Cohort' Component with students --- src/App.js | 11 ++++++ src/pages/cohort/index.js | 78 ++++++++++++++++++++++++++++++++++++++ src/pages/cohort/style.css | 22 +++++++++++ 3 files changed, 111 insertions(+) create mode 100644 src/pages/cohort/index.js create mode 100644 src/pages/cohort/style.css diff --git a/src/App.js b/src/App.js index 136c3a15..ed7dc5c6 100644 --- a/src/App.js +++ b/src/App.js @@ -8,6 +8,7 @@ import Verification from './pages/verification'; import { AuthProvider, ProtectedRoute } from './context/auth'; import { ModalProvider } from './context/modal'; import Welcome from './pages/welcome'; +import Cohort from './pages/cohort'; const App = () => { return ( @@ -28,6 +29,16 @@ const App = () => { } /> + + + + + } + /> + { + /* Waiting for the backend implementation of Cohort */ + return ( + <> +
+ +
+

My Cohort

+
+
+
+

< >

+
+
+ {/* This is where the cohort name will go */} +

Software Development, Cohort 4

+ January 2023 - June 2023 +
+
+ {/* 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. + Fill with proper data from the database. + This is just mockup data and therefore the data is static. + What needs to be done is to .map the data of all the ids from the cohort. + Loop then through and fetch all the user data from the cohort. */} +
+
+
+

AB

+
+
+

Alexander B

+
+
+
+
+
+
+

AJ

+
+
+

Alexander Jansson

+
+
+
+
+
+
+

AJ

+
+
+

Alexander Jansson

+
+
+
+
+
+
+

AJ

+
+
+

Alexander Jansson

+
+
+
+
+
+
+ + ); +}; + +export default Cohort; diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css new file mode 100644 index 00000000..0ae7aa41 --- /dev/null +++ b/src/pages/cohort/style.css @@ -0,0 +1,22 @@ +.cohort-title { + height: 56px; + border-bottom: 1px solid #DCE1F0; + margin-bottom: 1rem; +} + +.cohort-div { + border-bottom: 1px solid #DCE1F0; + height: 72px; +} + +.student-card { + height: 72px; + width: 365px; +} + + +.user-list { + margin-top: 1rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(365px, 1fr)); +} \ No newline at end of file From 7dfd8c7d9c3214095aecafcbd151b0de8298e09a Mon Sep 17 00:00:00 2001 From: Valen98 Date: Tue, 29 Oct 2024 08:04:46 +0100 Subject: [PATCH 02/84] Added a 3 dot button on each student --- src/components/navigation/index.js | 2 +- src/context/auth.js | 2 +- src/pages/cohort/index.js | 77 ++++++++++++++++++++++++++++-- src/pages/cohort/style.css | 8 +++- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js index b31393a8..73c5821d 100644 --- a/src/components/navigation/index.js +++ b/src/components/navigation/index.js @@ -28,7 +28,7 @@ const Navigation = () => {
  • - +

    Cohort

    diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..418d9401 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -23,7 +23,7 @@ const AuthProvider = ({ children }) => { setToken(storedToken); navigate(location.state?.from?.pathname || '/'); } - }, [location.state?.from?.pathname, navigate]); + }, []); const handleLogin = async (email, password) => { const res = await login(email, password); diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js index fee05218..66cce723 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -33,9 +33,27 @@ const Cohort = () => {

    AB

    -
    +

    Alexander B

    + {/* This is the button to view the profile of the student and need to implement action of button. */} +
    + +
    @@ -43,9 +61,26 @@ const Cohort = () => {

    AJ

    -
    +

    Alexander Jansson

    +
    + +
    @@ -53,9 +88,26 @@ const Cohort = () => {

    AJ

    -
    +

    Alexander Jansson

    +
    + +
    @@ -63,9 +115,26 @@ const Cohort = () => {

    AJ

    -
    +

    Alexander Jansson

    +
    + +
    diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css index 0ae7aa41..5be8714e 100644 --- a/src/pages/cohort/style.css +++ b/src/pages/cohort/style.css @@ -12,6 +12,7 @@ .student-card { height: 72px; width: 365px; + padding-right: 1.5rem; } @@ -19,4 +20,9 @@ margin-top: 1rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(365px, 1fr)); -} \ No newline at end of file +} + +.cohort-student-name { + padding: 4px; + margin-top: 15px; +} From 1a05f0fb4e6ca99678e10080f36fa69eebe30c19 Mon Sep 17 00:00:00 2001 From: shyye Date: Tue, 29 Oct 2024 08:34:07 +0100 Subject: [PATCH 03/84] Check if post request return resonse error when email already exists, redirect user to /register if this is the case --- src/context/auth.js | 6 +++++- src/service/apiClient.js | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..0fb395d1 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -45,8 +45,12 @@ const AuthProvider = ({ children }) => { const handleRegister = async (email, password) => { const res = await register(email, password); - setToken(res.data.token); + if (res.status === 'fail') { + return navigate('/register'); + } + + setToken(res.data.token); navigate('/verification'); }; diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..e774b419 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -5,7 +5,12 @@ 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); } From d470ad38b003bf5a48fc414ea5edf3a933639aa8 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:00:16 +0100 Subject: [PATCH 04/84] Refactor header component to close menu when navigating to a new page --- src/components/header/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/header/index.js b/src/components/header/index.js index c591f1e1..ecadfcc4 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -5,17 +5,22 @@ 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'; const Header = () => { const { token, onLogout } = useAuth(); const [isMenuVisible, setIsMenuVisible] = useState(false); + const location = useLocation(); const onClickProfileIcon = () => { setIsMenuVisible(!isMenuVisible); }; + useEffect(() => { + setIsMenuVisible(false); + }, [location]); + if (!token) { return null; } @@ -45,7 +50,7 @@ const Header = () => {
    • - +

      Profile

    • From 5f5cf2509d89b8803475e02487688e488573f0b8 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:00:32 +0100 Subject: [PATCH 05/84] Add user profile page route --- src/App.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/App.js b/src/App.js index 136c3a15..159d60dd 100644 --- a/src/App.js +++ b/src/App.js @@ -8,6 +8,7 @@ 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'; const App = () => { return ( @@ -36,6 +37,14 @@ const App = () => { } /> + + + + } + /> From b47dcfe7913483de13ff967f05dedcf4c8abd011 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Tue, 29 Oct 2024 09:52:46 +0100 Subject: [PATCH 06/84] Reverted auth.js file and added 'replace state' for pathname --- src/components/navigation/index.js | 2 +- src/context/auth.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js index 73c5821d..0a6fec63 100644 --- a/src/components/navigation/index.js +++ b/src/components/navigation/index.js @@ -28,7 +28,7 @@ const Navigation = () => {
    • - +

      Cohort

      diff --git a/src/context/auth.js b/src/context/auth.js index 418d9401..47cd66c9 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -23,7 +23,7 @@ const AuthProvider = ({ children }) => { setToken(storedToken); navigate(location.state?.from?.pathname || '/'); } - }, []); + }, [location.state?.from?.pathname, navigate]); const handleLogin = async (email, password) => { const res = await login(email, password); From 46ff866497ae23bb00487a9eb9aa933568e7f5ec Mon Sep 17 00:00:00 2001 From: shyye Date: Tue, 29 Oct 2024 09:53:39 +0100 Subject: [PATCH 07/84] Add ErrorMessage component --- src/components/errorMessage/index.js | 7 +++++++ src/components/errorMessage/style.css | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/components/errorMessage/index.js create mode 100644 src/components/errorMessage/style.css diff --git a/src/components/errorMessage/index.js b/src/components/errorMessage/index.js new file mode 100644 index 00000000..3bc37ad1 --- /dev/null +++ b/src/components/errorMessage/index.js @@ -0,0 +1,7 @@ +import './style.css'; + +const ErrorMessage = ({ message }) => { + return
      {message}
      ; +}; + +export default ErrorMessage; diff --git a/src/components/errorMessage/style.css b/src/components/errorMessage/style.css new file mode 100644 index 00000000..4f44b4e8 --- /dev/null +++ b/src/components/errorMessage/style.css @@ -0,0 +1,12 @@ +.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; +} From 1003a6d59fac8db19cea983a239aa6ab2c3732af Mon Sep 17 00:00:00 2001 From: shyye Date: Tue, 29 Oct 2024 09:56:58 +0100 Subject: [PATCH 08/84] Get and add response error message to the UI --- src/context/auth.js | 5 ++++- src/pages/register/index.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index 0fb395d1..4edb6a0f 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -43,10 +43,13 @@ const AuthProvider = ({ children }) => { setToken(null); }; - 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'); } diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 5cc70e32..7cb7df8b 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -3,11 +3,13 @@ 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(''); const onChange = (e) => { const { name, value } = e.target; @@ -40,9 +42,10 @@ const Register = () => { type={'password'} /> + {errorMessage && } + {error && {error}} ); } else { @@ -36,10 +79,11 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => { type={type} name={name} value={value} - onChange={onChange} + onChange={handleChange} className={icon && 'input-has-icon'} /> {icon && {icon}} + {error && {error}} ); } diff --git a/src/components/stepper/index.js b/src/components/stepper/index.js index c9e5f259..7651f9ed 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()) { + setCurrentStep(currentStep + 1); } - - setCurrentStep(currentStep + 1); }; return ( diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..249056e1 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -50,10 +50,17 @@ const AuthProvider = ({ children }) => { navigate('/verification'); }; - const handleCreateProfile = async (firstName, lastName, githubUrl, bio) => { + const handleCreateProfile = async ( + firstName, + lastName, + username, + githubUrl, + bio, + profilePicture + ) => { const { userId } = jwt_decode(token); - await createProfile(userId, firstName, lastName, githubUrl, bio); + await createProfile(userId, firstName, lastName, username, githubUrl, bio, profilePicture); localStorage.setItem('token', token); navigate('/'); diff --git a/src/pages/welcome/index.js b/src/pages/welcome/index.js index 85af11ab..109cc232 100644 --- a/src/pages/welcome/index.js +++ b/src/pages/welcome/index.js @@ -11,8 +11,10 @@ const Welcome = () => { const [profile, setProfile] = useState({ firstName: '', lastName: '', + username: '', githubUsername: '', - bio: '' + bio: '', + profilePicture: '' }); const onChange = (event) => { @@ -24,8 +26,24 @@ const Welcome = () => { }); }; + const validate = () => { + if (!profile.firstName && !profile.lastName && !profile.username && !profile.githubUsername) { + return false; + } + return true; + }; + const onComplete = () => { - onCreateProfile(profile.firstName, profile.lastName, profile.githubUsername, profile.bio); + if (validate()) { + onCreateProfile( + profile.firstName, + profile.lastName, + profile.username, + profile.githubUsername, + profile.bio, + profile.profilePicture + ); + } }; return ( @@ -35,7 +53,7 @@ const Welcome = () => {

      Create your profile to get started

      - } onComplete={onComplete}> + } onComplete={onComplete} validate={validate}> diff --git a/src/pages/welcome/stepOne/index.js b/src/pages/welcome/stepOne/index.js index 317940f8..cf0e2334 100644 --- a/src/pages/welcome/stepOne/index.js +++ b/src/pages/welcome/stepOne/index.js @@ -1,8 +1,24 @@ +import { 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 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 } }); + }; + reader.readAsDataURL(file); + } + }; + return ( <>
      @@ -12,8 +28,21 @@ const StepOne = ({ data, setData }) => {

      Photo

      - -

      Add headshot

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

      Please upload a valid image file

      @@ -22,14 +51,29 @@ const StepOne = ({ data, setData }) => { onChange={setData} value={data.firstName} name="firstName" - label={'First name'} + label={'First name*'} + isRequired={true} + /> + + -

      *Required

      diff --git a/src/pages/welcome/style.css b/src/pages/welcome/style.css index 7ff35605..f03b7114 100644 --- a/src/pages/welcome/style.css +++ b/src/pages/welcome/style.css @@ -19,6 +19,26 @@ gap: 16px; align-items: center; } +.welcome-form-profileimg-icon { + width: 40px; + height: 40px; + border-radius: 20px; +} +.welcome-form-profileimg-upload-button[type='file']::file-selector-button { + background-color: #000046; + color: #ffffff; + border-radius: 4px; + padding: 2px 4px; + border-width: 0; + transition: + background-color 0.3s, + color 0.3s; +} +.welcome-form-profileimg-upload-button[type='file']::file-selector-button:hover { + background-color: #f0f5fa; + color: #64648c; + border-radius: 4px; +} .welcome-form-profileimg-error { color: transparent; } diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..707e1284 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -9,8 +9,23 @@ async function register(email, password) { return await login(email, password); } -async function createProfile(userId, firstName, lastName, githubUrl, bio) { - return await patch(`users/${userId}`, { firstName, lastName, githubUrl, bio }); +async function createProfile( + userId, + firstName, + lastName, + username, + githubUrl, + bio, + profilePicture +) { + return await patch(`users/${userId}`, { + firstName, + lastName, + username, + githubUrl, + bio, + profilePicture + }); } async function getPosts() { From 8803ae5b82ec0ef1682da6f6fb2a61c01c3800e4 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:08:04 +0100 Subject: [PATCH 10/84] Add additional user details to mockData.js --- src/service/mockData.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/service/mockData.js b/src/service/mockData.js index d49e98a4..290d0573 100644 --- a/src/service/mockData.js +++ b/src/service/mockData.js @@ -8,7 +8,12 @@ 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' } }; From 9323d8a76dec03edfe9df4ecbc166b039a7cff80 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:08:22 +0100 Subject: [PATCH 11/84] Add ProfileHeader component to display user profile information --- .../profilePage/components/ProfileHeader.jsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/pages/profilePage/components/ProfileHeader.jsx 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; From 2259e7bce3ab4be8a496623b64b948a38173d598 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:08:50 +0100 Subject: [PATCH 12/84] Add BasicInfoForm component to display basic user information --- .../profilePage/components/BasicInfoForm.jsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/pages/profilePage/components/BasicInfoForm.jsx diff --git a/src/pages/profilePage/components/BasicInfoForm.jsx b/src/pages/profilePage/components/BasicInfoForm.jsx new file mode 100644 index 00000000..f2ffd054 --- /dev/null +++ b/src/pages/profilePage/components/BasicInfoForm.jsx @@ -0,0 +1,58 @@ +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 } = useContext(ProfileContext); + + return ( +
      +
      +
      +

      Basic Info

      +
      +
      + Photo +
      + + Add headshot +
      +
      + + + + +
      +
      +
      +
      + ); +}; + +export default BasicInfoForm; From 8a5de92bceccf89d0f8a73464342e0c06b39f6f6 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:09:12 +0100 Subject: [PATCH 13/84] Add TrainingInfoForm component to display training information in the user profile page --- .../components/TrainingInfoForm.jsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/pages/profilePage/components/TrainingInfoForm.jsx diff --git a/src/pages/profilePage/components/TrainingInfoForm.jsx b/src/pages/profilePage/components/TrainingInfoForm.jsx new file mode 100644 index 00000000..d602f2a4 --- /dev/null +++ b/src/pages/profilePage/components/TrainingInfoForm.jsx @@ -0,0 +1,57 @@ +import { useContext } from 'react'; +import { ProfileContext } from '..'; +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const TrainingInfoForm = () => { + const { profile, handleInputChange, formatRole } = useContext(ProfileContext); + + return ( +
      +
      +
      +

      Training Info

      +
      + + + + + +
      +
      +
      +
      + ); +}; + +export default TrainingInfoForm; From d5d1ece0045b76bb723664e3bd2155d21fd48b7e Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:09:22 +0100 Subject: [PATCH 14/84] Refactor profile page CSS styles --- src/pages/profilePage/profilePage.css | 97 +++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/pages/profilePage/profilePage.css diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css new file mode 100644 index 00000000..39b6e3e2 --- /dev/null +++ b/src/pages/profilePage/profilePage.css @@ -0,0 +1,97 @@ +.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; + color: #000046; +} + +.profile-info-header { + display: flex; + align-items: center; + margin-bottom: 24px; +} + +.profile-info { + margin-left: 16px; +} + +.profile-info h3 { + font-style: normal; + font-weight: 600; + font-size: 32px; + line-height: 40px; + color: #000046; + margin: 0; +} + +.profile-info p { + font-style: normal; + font-weight: 400; + font-size: 18px; + line-height: 24px; + color: #64648c; + margin: 0; +} + +.profile-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; +} + +.profile-grid-section input { + margin-bottom: 36px; /* Add vertical space between inputs */ +} + +.profile-grid-section.read-only input { + pointer-events: none; /* Disable all mouse events */ +} + +.photo-section { + display: flex; + flex-direction: column; + margin-bottom: 30px; /* Add space below the photo section */ +} + +.photo-label { + font-weight: 600; + font-size: 16px; + line-height: 24px; + color: #000046; + margin-bottom: 8px; /* Add space between label and profile circle */ +} + +.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 */ +} + +.section-divider { + border: none; + border-top: 1px solid #e6ebf5; + margin: 32px 0; /* Add vertical space around the divider */ +} From 6174f8603196c3804152892c8973feeb408f73ce Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:09:49 +0100 Subject: [PATCH 15/84] Add UserProfile component with Profile context to display user profile information --- src/pages/profilePage/index.js | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/pages/profilePage/index.js diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js new file mode 100644 index 00000000..f1850cc6 --- /dev/null +++ b/src/pages/profilePage/index.js @@ -0,0 +1,109 @@ +import { createContext, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { user } from '../../service/mockData'; +import Card from '../../components/card'; +import './profilePage.css'; + +// eslint-disable-next-line camelcase +// import jwt_decode from 'jwt-decode'; +import ProfileHeader from './components/ProfileHeader'; +import BasicInfoForm from './components/BasicInfoForm'; +import TrainingInfoForm from './components/TrainingInfoForm'; + +export const ProfileContext = createContext(); + +const UserProfile = () => { + const { profileId } = useParams(); + const [profile, setProfile] = useState(null); + const [initials, setInitials] = useState(''); + // const storedToken = localStorage.getItem('token'); + // const { userId } = jwt_decode(storedToken); + + const fetchProfile = () => { + setProfile(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(); + }, [profileId]); + + const handleSubmit = (e) => { + // Handle form submission + e.preventDefault(); + console.log('Form submitted'); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setProfile((prevProfile) => ({ + ...prevProfile, + [name]: value + })); + }; + + const formatRole = (role) => { + if (role === 'STUDENT') { + return 'Student'; + } else { + return 'Teacher'; + } + }; + + if (!profile) { + return

      Loading...

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

      Profile

      + + + + +
      + + + +
      +

      Contact Info

      +

      Email: {profile.email}

      +

      + GitHub: {profile.githubUrl} +

      +
      +
      +

      Bio

      +

      {profile.bio}

      +
      +
      +
      +
      +
      + ); +}; + +export default UserProfile; From e240ddcb45f627394e380f1d91f8d32714d134d9 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Tue, 29 Oct 2024 12:41:48 +0100 Subject: [PATCH 16/84] Solved the navigation issue by removing data in useEffect, setToken to the correct data and then use navigate('/') to automatcly go to the dashboard when signed in --- src/context/auth.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..d2ef571e 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -21,9 +21,11 @@ const AuthProvider = ({ children }) => { if (storedToken) { setToken(storedToken); - navigate(location.state?.from?.pathname || '/'); + navigate(location.pathname || '/'); + } else { + navigate('/login'); } - }, [location.state?.from?.pathname, navigate]); + }, []); const handleLogin = async (email, password) => { const res = await login(email, password); @@ -34,8 +36,8 @@ const AuthProvider = ({ children }) => { localStorage.setItem('token', res.data.token); - setToken(res.token); - navigate(location.state?.from?.pathname || '/'); + setToken(res.data.token); + navigate('/'); }; const handleLogout = () => { @@ -73,7 +75,6 @@ const AuthProvider = ({ children }) => { const ProtectedRoute = ({ children }) => { const { token } = useAuth(); const location = useLocation(); - if (!token) { return ; } From 903466f4c548f22b4678d5556512497fd3c84a14 Mon Sep 17 00:00:00 2001 From: shyye Date: Tue, 29 Oct 2024 14:09:31 +0100 Subject: [PATCH 17/84] CHange name to avoid name conflicts --- src/components/errorMessage/{style.css => _errorMessage.css} | 2 +- src/components/errorMessage/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/components/errorMessage/{style.css => _errorMessage.css} (93%) diff --git a/src/components/errorMessage/style.css b/src/components/errorMessage/_errorMessage.css similarity index 93% rename from src/components/errorMessage/style.css rename to src/components/errorMessage/_errorMessage.css index 4f44b4e8..0f1bc2c4 100644 --- a/src/components/errorMessage/style.css +++ b/src/components/errorMessage/_errorMessage.css @@ -1,4 +1,4 @@ -.error-message { +.simple-error-message { background: var(--color-offwhite); padding: 24px; border-radius: 8px; diff --git a/src/components/errorMessage/index.js b/src/components/errorMessage/index.js index 3bc37ad1..0ff02ec1 100644 --- a/src/components/errorMessage/index.js +++ b/src/components/errorMessage/index.js @@ -1,7 +1,7 @@ -import './style.css'; +import './_errorMessage.css'; const ErrorMessage = ({ message }) => { - return
      {message}
      ; + return
      {message}
      ; }; export default ErrorMessage; From 695a19017754677850eb98ad061789a66f87b731 Mon Sep 17 00:00:00 2001 From: shyye Date: Tue, 29 Oct 2024 14:35:37 +0100 Subject: [PATCH 18/84] Add validation on Register Page for email and password --- src/pages/register/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 7cb7df8b..5d08daa1 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -11,6 +11,12 @@ const Register = () => { const [formData, setFormData] = useState({ email: '', password: '' }); const [errorMessage, setErrorMessage] = useState(''); + const isRequiredFieldsProvided = formData.email && formData.password; + const isValidEmail = formData.email.match(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/); + const isValidPassword = formData.password.length >= 8; + + const isFormDataValid = isRequiredFieldsProvided && isValidEmail && isValidPassword; + const onChange = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); @@ -33,6 +39,7 @@ const Register = () => { type="email" name="email" label={'Email *'} + isRequired={true} /> { name="password" label={'Password *'} type={'password'} + isRequired={true} /> {errorMessage && } ); + } else if (type === 'email') { + return ( +
      + {label && } + + {icon && {icon}} +
      + ); } else { return (
      From a2cc12eb4a998a5f92b126d5cda5093d503d2849 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Tue, 29 Oct 2024 14:40:00 +0100 Subject: [PATCH 20/84] Finished with the teacher view --- src/pages/cohort/index.js | 74 +++++++++++++++++++++++++++++++++++--- src/pages/cohort/style.css | 9 +++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js index 66cce723..018b6afe 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -38,7 +38,7 @@ const Cohort = () => {
      {/* This is the button to view the profile of the student and need to implement action of button. */}
      -
      -
      -
      -
      + + ); }; diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css index 5be8714e..4138f4e5 100644 --- a/src/pages/cohort/style.css +++ b/src/pages/cohort/style.css @@ -23,6 +23,11 @@ } .cohort-student-name { - padding: 4px; - margin-top: 15px; + + margin-top: 10px; } + +.cohort-action-button { + padding: 0px 0px !important; + margin-top: 5px; +} \ No newline at end of file From 65035bcf4dca497c0241d0f93330fda35d65b9e7 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:03:15 +0100 Subject: [PATCH 21/84] Refactor BasicInfoForm component to remove section divider at the bottom --- src/pages/profilePage/components/BasicInfoForm.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/profilePage/components/BasicInfoForm.jsx b/src/pages/profilePage/components/BasicInfoForm.jsx index f2ffd054..091bace0 100644 --- a/src/pages/profilePage/components/BasicInfoForm.jsx +++ b/src/pages/profilePage/components/BasicInfoForm.jsx @@ -49,7 +49,6 @@ const BasicInfoForm = () => { type="text" /> -
      ); From 27e315aae192f5c0db5ddb8b74b430c024626a52 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:03:30 +0100 Subject: [PATCH 22/84] Refactor TrainingInfoForm component to remove section divider at the bottom --- src/pages/profilePage/components/TrainingInfoForm.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/profilePage/components/TrainingInfoForm.jsx b/src/pages/profilePage/components/TrainingInfoForm.jsx index d602f2a4..9a2ee116 100644 --- a/src/pages/profilePage/components/TrainingInfoForm.jsx +++ b/src/pages/profilePage/components/TrainingInfoForm.jsx @@ -48,7 +48,6 @@ const TrainingInfoForm = () => { type="text" /> -
      ); From 1d8b62159bd2f390168cc6d349d76fdfbd46d4e6 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:04:00 +0100 Subject: [PATCH 23/84] Created ContactInfoForm component to add contact information fields in the user profile page --- .../components/ContactInfoForm.jsx | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/pages/profilePage/components/ContactInfoForm.jsx diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx new file mode 100644 index 00000000..405bc8f9 --- /dev/null +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -0,0 +1,42 @@ +import React, { useContext } from 'react'; +import { ProfileContext } from '..'; +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const ContactInfoForm = () => { + const { profile, handleInputChange } = useContext(ProfileContext); + + return ( +
      +
      +
      +

      Contact Info

      +
      + + + +
      +
      +
      + ); +}; + +export default ContactInfoForm; From f705350e1099ef96dc00aac0ebba3ecc7887a5d9 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:04:18 +0100 Subject: [PATCH 24/84] Add BioForm component to display user's bio in the profile page --- src/pages/profilePage/components/BioForm.jsx | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/pages/profilePage/components/BioForm.jsx diff --git a/src/pages/profilePage/components/BioForm.jsx b/src/pages/profilePage/components/BioForm.jsx new file mode 100644 index 00000000..02aa4955 --- /dev/null +++ b/src/pages/profilePage/components/BioForm.jsx @@ -0,0 +1,30 @@ +import React, { useContext } from 'react'; +import Form from '../../../components/form'; +import { ProfileContext } from '..'; + +const BioForm = () => { + const { profile, handleBioChange, bioLength } = useContext(ProfileContext); + return ( +
      +
      +
      +
      +

      Bio

      + +
      + +

      {bioLength}/300

      +
      +
      +
      +
      + ); +}; + +export default BioForm; From 40a58b3ab98ec411c1279e4cd76dc0cff7da00ab Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:04:26 +0100 Subject: [PATCH 25/84] Refactor profile page CSS to improve layout and styling --- src/pages/profilePage/profilePage.css | 49 +++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css index 39b6e3e2..b3dec1a8 100644 --- a/src/pages/profilePage/profilePage.css +++ b/src/pages/profilePage/profilePage.css @@ -23,7 +23,18 @@ .profile-info-header { display: flex; align-items: center; - margin-bottom: 24px; +} + +.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 { @@ -55,8 +66,14 @@ width: 100%; } -.profile-grid-section input { - margin-bottom: 36px; /* Add vertical space between inputs */ +.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 { @@ -66,15 +83,14 @@ .photo-section { display: flex; flex-direction: column; - margin-bottom: 30px; /* Add space below the photo section */ } .photo-label { font-weight: 600; font-size: 16px; line-height: 24px; - color: #000046; - margin-bottom: 8px; /* Add space between label and profile circle */ + color: #64648c; + margin-left: 2px; } .profile-circle-wrapper { @@ -90,8 +106,29 @@ 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 #e6ebf5; 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-text { + color: #64648c; + font-size: 16px; + line-height: 24px; + margin-left: 10px; +} + +.required-text { + margin-left: 0; +} From c1c0d0c9e194d7035e597a74f81a9eb24a455455 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:04:35 +0100 Subject: [PATCH 26/84] Add mobile and password fields to user object in mockData.js --- src/service/mockData.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/service/mockData.js b/src/service/mockData.js index 290d0573..0ef02b5c 100644 --- a/src/service/mockData.js +++ b/src/service/mockData.js @@ -13,7 +13,9 @@ const user = { specialism: 'Full Stack', startDate: 'January 2020', endDate: 'June 2020', - username: 'test-user' + username: 'test-user', + mobile: '0123456789', + password: 'password' } }; From 9dde0fae715abc5d34486c2f934d7b4642161d20 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:05:57 +0100 Subject: [PATCH 27/84] Refactor profile page to add BioForm and ContactInfoForm components --- src/pages/profilePage/index.js | 43 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index f1850cc6..945c5e9e 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -3,12 +3,14 @@ import { useParams } from 'react-router-dom'; import { user } from '../../service/mockData'; import Card from '../../components/card'; import './profilePage.css'; - -// eslint-disable-next-line camelcase -// import jwt_decode from 'jwt-decode'; 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'; + +// eslint-disable-next-line camelcase +// import jwt_decode from 'jwt-decode'; export const ProfileContext = createContext(); @@ -16,12 +18,14 @@ const UserProfile = () => { const { profileId } = useParams(); const [profile, setProfile] = useState(null); const [initials, setInitials] = useState(''); + const [bioLength, setBioLength] = useState(0); // const storedToken = localStorage.getItem('token'); // const { userId } = jwt_decode(storedToken); const fetchProfile = () => { setProfile(user.user); setInitials(user.user.firstName[0] + user.user.lastName[0]); + setBioLength(user.user.bio.length); /* try { const data = await get(`profiles/${profileId}`); setProfile(data); @@ -50,6 +54,12 @@ const UserProfile = () => { })); }; + const handleBioChange = (event) => { + handleInputChange(event); + console.log(event.target.value.length); + setBioLength(event.target.value.length); + }; + const formatRole = (role) => { if (role === 'STUDENT') { return 'Student'; @@ -69,7 +79,9 @@ const UserProfile = () => { setInitials, handleSubmit, handleInputChange, - formatRole + formatRole, + handleBioChange, + bioLength }; return ( @@ -78,27 +90,12 @@ const UserProfile = () => { -
      - - -
      -

      Contact Info

      -

      Email: {profile.email}

      -

      - GitHub: {profile.githubUrl} -

      -
      -
      -

      Bio

      -

      {profile.bio}

      -
      + + + +

      *Required

      From f8cfd2d3cac68735397f1bb43f4c50034d9053ea Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:20:36 +0100 Subject: [PATCH 28/84] Refactor profile page to dynamically generate profile link based on user ID --- src/components/header/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/header/index.js b/src/components/header/index.js index ecadfcc4..d84fd761 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -8,10 +8,14 @@ import LogoutIcon from '../../assets/icons/logoutIcon'; 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); @@ -50,7 +54,7 @@ const Header = () => {
      • - +

        Profile

      • From 9bff10e4d5d3ddb7028db495f2b63ea6732ee474 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:20:44 +0100 Subject: [PATCH 29/84] Refactor profile link in navigation to dynamically generate based on user ID --- src/components/navigation/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js index b31393a8..fa7b17c9 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,7 +26,7 @@ const Navigation = () => {
      • - +

        Profile

        From f14c44e2fd13c9f3d0facda18d49fe56942bcec3 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:20:55 +0100 Subject: [PATCH 30/84] Refactor profile page to remove commented out code and unused variables --- src/pages/profilePage/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index 945c5e9e..d9a110de 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -19,7 +19,7 @@ const UserProfile = () => { const [profile, setProfile] = useState(null); const [initials, setInitials] = useState(''); const [bioLength, setBioLength] = useState(0); - // const storedToken = localStorage.getItem('token'); + // const { token } = useAuth(); // const { userId } = jwt_decode(storedToken); const fetchProfile = () => { From 5b2b815f0758fa704b3846249db967fd932cbbd6 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:21:12 +0100 Subject: [PATCH 31/84] Refactor auth.js to remove unnecessary dependencies in useEffect --- src/context/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..418d9401 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -23,7 +23,7 @@ const AuthProvider = ({ children }) => { setToken(storedToken); navigate(location.state?.from?.pathname || '/'); } - }, [location.state?.from?.pathname, navigate]); + }, []); const handleLogin = async (email, password) => { const res = await login(email, password); From f744e85b299b66b051f9f8b05560ea8771e0ad9b Mon Sep 17 00:00:00 2001 From: Joaquin Lindkvist Date: Tue, 29 Oct 2024 15:36:49 +0100 Subject: [PATCH 32/84] Updated the TextInput component to be able to display locked text inputs as well and made it more robust. Also added the rest of the welcome pages (step 2 and step 3) and made sure that they are working as intended. Added red markings to TextInputs that require input and are not fulfilled. Finally made the auth hold the userCredentials so that they can be accessed in the welcome page, this method can be altered later on based for cyber security reasons --- src/components/form/textInput/index.js | 58 +++++++++++++++++++--- src/components/stepper/index.js | 2 +- src/context/auth.js | 5 ++ src/pages/welcome/index.js | 67 +++++++++++++++++++++++--- src/pages/welcome/stepFour/index.js | 28 +++++++++++ src/pages/welcome/stepThree/index.js | 34 +++++++++++++ src/pages/welcome/stepTwo/index.js | 21 +++++++- src/styles/_form.css | 40 +++++++++++++++ 8 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 src/pages/welcome/stepFour/index.js create mode 100644 src/pages/welcome/stepThree/index.js diff --git a/src/components/form/textInput/index.js b/src/components/form/textInput/index.js index 396e0842..463c229c 100644 --- a/src/components/form/textInput/index.js +++ b/src/components/form/textInput/index.js @@ -9,7 +9,8 @@ const TextInput = ({ type = 'text', isRequired = false, validChars = 'A-Za-z0-9@_-', - maxLength = 50 + maxLength = 50, + isLocked = false }) => { const [input, setInput] = useState(''); const [error, setError] = useState(''); @@ -24,7 +25,9 @@ const TextInput = ({ const validateInput = (value, event) => { const regex = new RegExp(`^[${validChars}]+$`); const isValid = regex.test(value) && value.length <= maxLength; - if (value.length === 0 && isRequired) { + if (!isRequired) { + onChange(event); + } else if (value.length === 0 && isRequired) { setError(`${label.slice(0, -1)} is required`); onChange(event); } else if (!isValid) { @@ -45,7 +48,37 @@ const TextInput = ({ setInput(value); }; - if (type === 'password') { + if (isLocked && type === 'password') { + return ( +
        + + + {showpassword && } + + +
        + ); + } else if (isLocked) { + return ( +
        + + + +
        + ); + } else if (type === 'password') { return (
        @@ -53,10 +86,8 @@ const TextInput = ({ type={type} name={name} value={value} - onChange={(e) => { - handleChange(e); - setInput(e.target.value); - }} + onChange={handleChange} + className={error && 'input-error'} /> {showpassword && }

        Please upload a valid image file

        + {isPopupVisible && ( +
        +
        +

        Upload photo

        +

        Choose a file to upload as your profile picture.

        +
        + + + +
        +
        +
        + )}
        Date: Tue, 29 Oct 2024 16:36:42 +0100 Subject: [PATCH 34/84] Refactor profilePage.css to update color variables and remove unused styles --- src/pages/profilePage/profilePage.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css index b3dec1a8..ac5d4942 100644 --- a/src/pages/profilePage/profilePage.css +++ b/src/pages/profilePage/profilePage.css @@ -17,7 +17,6 @@ font-weight: 600; font-size: 40px; line-height: 48px; - color: #000046; } .profile-info-header { @@ -46,7 +45,6 @@ font-weight: 600; font-size: 32px; line-height: 40px; - color: #000046; margin: 0; } @@ -55,7 +53,7 @@ font-weight: 400; font-size: 18px; line-height: 24px; - color: #64648c; + color: var(--color-blue1); margin: 0; } @@ -89,7 +87,7 @@ font-weight: 600; font-size: 16px; line-height: 24px; - color: #64648c; + color: var(--color-blue1); margin-left: 2px; } @@ -112,7 +110,7 @@ .section-divider { border: none; - border-top: 1px solid #e6ebf5; + border-top: 1px solid var(--color-blue5); margin: 32px 0; /* Add vertical space around the divider */ } @@ -123,7 +121,7 @@ textarea { } .info-text { - color: #64648c; + color: var(--color-blue1); font-size: 16px; line-height: 24px; margin-left: 10px; From 89d9909c6b5b545484930e9f15d5fded960077d1 Mon Sep 17 00:00:00 2001 From: Joaquin Lindkvist Date: Tue, 29 Oct 2024 16:50:09 +0100 Subject: [PATCH 35/84] Updated CSS so that the changes made by others didn't break the website because of my welcome page. --- src/components/stepper/style.css | 1 + src/pages/welcome/style.css | 7 +++++++ 2 files changed, 8 insertions(+) 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/pages/welcome/style.css b/src/pages/welcome/style.css index 986f79d4..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; } From 8036fa72f08e6214d334ef18824cc93ac57a4f03 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Tue, 29 Oct 2024 17:16:37 +0100 Subject: [PATCH 36/84] Refactor profilePage to add an edit button in the user profile section. It only write a message to the console for now. --- src/pages/profilePage/index.js | 9 +++++++++ src/pages/profilePage/profilePage.css | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index d9a110de..e894b8ec 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -8,6 +8,7 @@ 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'; @@ -96,6 +97,14 @@ const UserProfile = () => {

        *Required

        +
      diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css index ac5d4942..4ff8231c 100644 --- a/src/pages/profilePage/profilePage.css +++ b/src/pages/profilePage/profilePage.css @@ -130,3 +130,26 @@ textarea { .required-text { margin-left: 0; } + +.edit-button { + display: flex; + flex-direction: row; + justify-content: center; + margin-left: auto; + padding: 14px 24px; + gap: 8px; + + background: var(--color-blue); + border-radius: 8px; + + font-style: normal; + font-weight: 400; + font-size: 20px; + line-height: 28px; + text-align: center; + color: #ffffff; + + flex: none; + order: 0; + flex-grow: 1; +} From 6ec1eb8c1b8ae1b22422b98eed7e073ee3bfc462 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:45:33 +0100 Subject: [PATCH 37/84] Refactor AuthContext to store user data - Store the entire user object returned at login in state and localStorage - Update useEffect to retrieve and parse user data from localStorage - Ensure user data is properly set and cleared during login and logout --- src/context/auth.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index d2ef571e..0e6a3d3f 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -15,12 +15,15 @@ const AuthProvider = ({ children }) => { const navigate = useNavigate(); const location = useLocation(); const [token, setToken] = useState(null); + const [user, setUser] = useState(null); useEffect(() => { const storedToken = localStorage.getItem('token'); + const storedUser = localStorage.getItem('user'); - if (storedToken) { + if (storedToken && storedUser) { setToken(storedToken); + setUser(JSON.parse(storedUser)); navigate(location.pathname || '/'); } else { navigate('/login'); @@ -30,25 +33,37 @@ const AuthProvider = ({ children }) => { const handleLogin = async (email, password) => { const res = await login(email, password); - if (!res.data.token) { + if (!res.data.token || !res.data.user) { return navigate('/login'); } localStorage.setItem('token', res.data.token); + localStorage.setItem('user', JSON.stringify(res.data.user)); setToken(res.data.token); + setUser(res.data.user); navigate('/'); }; const handleLogout = () => { localStorage.removeItem('token'); + localStorage.removeItem('user'); setToken(null); + setUser(null); }; const handleRegister = async (email, password) => { const res = await register(email, password); - setToken(res.data.token); + if (!res.data.token || !res.data.user) { + return navigate('/login'); + } + + localStorage.setItem('token', res.data.token); + localStorage.setItem('user', JSON.stringify(res.data.user)); + + setToken(res.data.token); + setUser(res.data.user); navigate('/verification'); }; @@ -58,11 +73,13 @@ const AuthProvider = ({ children }) => { await createProfile(userId, firstName, lastName, githubUrl, bio); localStorage.setItem('token', token); + localStorage.setItem('user', JSON.stringify(user)); navigate('/'); }; const value = { token, + user, onLogin: handleLogin, onLogout: handleLogout, onRegister: handleRegister, From 7b95409d5ebb1b7ea05e8f56ae3fd58df563e4c6 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:00:56 +0100 Subject: [PATCH 38/84] Refactor AuthContext to store user role instead of user - Simplify state management by storing only the user role and match the issue - Update useEffect to retrieve and set the role from localStorage - Adjust handleLogin and handleRegister to store the role in localStorage - Ensure role is properly set and cleared during login and logout --- src/context/auth.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index 0e6a3d3f..74d35200 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -15,15 +15,15 @@ const AuthProvider = ({ children }) => { const navigate = useNavigate(); const location = useLocation(); const [token, setToken] = useState(null); - const [user, setUser] = useState(null); + const [role, setRole] = useState(null); useEffect(() => { const storedToken = localStorage.getItem('token'); - const storedUser = localStorage.getItem('user'); + const storedRole = localStorage.getItem('role'); - if (storedToken && storedUser) { + if (storedToken && storedRole) { setToken(storedToken); - setUser(JSON.parse(storedUser)); + setRole(storedRole); navigate(location.pathname || '/'); } else { navigate('/login'); @@ -33,37 +33,37 @@ const AuthProvider = ({ children }) => { const handleLogin = async (email, password) => { const res = await login(email, password); - if (!res.data.token || !res.data.user) { + if (!res.data.token || !res.data.user || !res.data.user.role) { return navigate('/login'); } localStorage.setItem('token', res.data.token); - localStorage.setItem('user', JSON.stringify(res.data.user)); + localStorage.setItem('role', res.data.user.role); setToken(res.data.token); - setUser(res.data.user); + setRole(res.data.user.role); navigate('/'); }; const handleLogout = () => { localStorage.removeItem('token'); - localStorage.removeItem('user'); + localStorage.removeItem('role'); setToken(null); - setUser(null); + setRole(null); }; const handleRegister = async (email, password) => { const res = await register(email, password); - if (!res.data.token || !res.data.user) { + if (!res.data.token || !res.data.user || !res.data.user.role) { return navigate('/login'); } localStorage.setItem('token', res.data.token); - localStorage.setItem('user', JSON.stringify(res.data.user)); + localStorage.setItem('role', res.data.user.role); setToken(res.data.token); - setUser(res.data.user); + setRole(res.data.user.role); navigate('/verification'); }; @@ -73,13 +73,13 @@ const AuthProvider = ({ children }) => { await createProfile(userId, firstName, lastName, githubUrl, bio); localStorage.setItem('token', token); - localStorage.setItem('user', JSON.stringify(user)); + localStorage.setItem('role', role); navigate('/'); }; const value = { token, - user, + role, onLogin: handleLogin, onLogout: handleLogout, onRegister: handleRegister, From cac4c0ac902173de01ca66ea2935f805fd426a2b Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:47:08 +0100 Subject: [PATCH 39/84] Refactor AuthContext to remove unnecessary user data check --- src/context/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context/auth.js b/src/context/auth.js index 74d35200..913f0807 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -33,7 +33,7 @@ const AuthProvider = ({ children }) => { const handleLogin = async (email, password) => { const res = await login(email, password); - if (!res.data.token || !res.data.user || !res.data.user.role) { + if (!res.data.token || !res.data.user.role) { return navigate('/login'); } @@ -55,7 +55,7 @@ const AuthProvider = ({ children }) => { const handleRegister = async (email, password) => { const res = await register(email, password); - if (!res.data.token || !res.data.user || !res.data.user.role) { + if (!res.data.token || !res.data.user.role) { return navigate('/login'); } From 57f5c19e3e1fce477afe0918617c3e5a49d6b50c Mon Sep 17 00:00:00 2001 From: Joaquin Lindkvist Date: Wed, 30 Oct 2024 11:19:49 +0100 Subject: [PATCH 40/84] Changed back .prettierrc but now it uses deprecated code so npx prettier --write will complain and might not work in some cases --- .prettierrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierrc b/.prettierrc index 18e58de9..639c6972 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,6 +4,6 @@ "printWidth": 100, "singleQuote": true, "trailingComma": "none", - "bracketSameLine": false, + "jsxBracketSameLine": false, "endOfLine": "lf" } From 881667e12a85cc7cd3378316db2d6d5947310f04 Mon Sep 17 00:00:00 2001 From: shyye Date: Wed, 30 Oct 2024 13:41:21 +0100 Subject: [PATCH 41/84] Add validChars to the email component that includes '.' --- src/pages/register/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 5d08daa1..e817d33c 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -40,6 +40,7 @@ const Register = () => { name="email" label={'Email *'} isRequired={true} + validChars={'A-Za-z0-9@._-'} /> Date: Tue, 29 Oct 2024 15:53:25 +0100 Subject: [PATCH 42/84] Display posts looks pretty alright --- .eslintrc.json | 7 ++- src/components/post/index.js | 69 +++++++++++++++++++-- src/components/post/style.css | 111 ++++++++++++++++++++++++++++++++-- 3 files changed, 176 insertions(+), 11 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 2988e2ba..deef124a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,12 @@ "plugins": ["react", "prettier"], "rules": { "react/react-in-jsx-scope": "off", - "react/prop-types": 0 + "react/prop-types": 0, + "prettier/prettier": [ + "error", { + "endOfLine": "auto" + } + ] }, "settings": { "react": { diff --git a/src/components/post/index.js b/src/components/post/index.js index 337ca5a6..c37d7f55 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,27 @@ const Post = ({ name, date, content, comments = [], likes = 0 }) => { openModal(); }; + const handleSubmit = (event) => { + console.log('ooooo'); + const newCommentObj = { + commentName: name, + content: comment + }; + console.log(newCommentObj); + + setComments([...comments, newCommentObj]); + console.log(comments); + }; + + const toggleLike = () => { + if (isLiked) { + setLikes(likes - 1); + } else { + setLikes(likes + 1); + } + setIsLiked(!isLiked); + }; + return (
      @@ -39,17 +65,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) => ( +
    + +
    +

    {comment.commentName}

    +

    {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 From 2d2527c2762e9c130e8d0598038ea9acfc2f598b Mon Sep 17 00:00:00 2001 From: Malte Date: Wed, 30 Oct 2024 12:18:36 +0100 Subject: [PATCH 43/84] Updated my branch with new main, looks to be working --- src/components/post/index.js | 11 ++++------- src/components/posts/index.js | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/post/index.js b/src/components/post/index.js index c37d7f55..6558a49f 100644 --- a/src/components/post/index.js +++ b/src/components/post/index.js @@ -21,15 +21,12 @@ const Post = ({ name, date, content, initialComments = [], initialLikes = 0 }) = }; const handleSubmit = (event) => { - console.log('ooooo'); const newCommentObj = { commentName: name, content: comment }; - console.log(newCommentObj); - setComments([...comments, newCommentObj]); - console.log(comments); + setComment(''); }; const toggleLike = () => { @@ -86,9 +83,9 @@ const Post = ({ name, date, content, initialComments = [], initialLikes = 0 }) =
    {comments.map((comment, index) => (
    - +
    -

    {comment.commentName}

    +

    {'Alex Jameson'}

    {comment.content}

    @@ -96,7 +93,7 @@ const Post = ({ name, date, content, initialComments = [], initialLikes = 0 }) =
    - +
    { useEffect(() => { getPosts().then(setPosts); + console.log(getPosts()); }, []); return ( From b89dc2360b0d441481088871de3ba13a25e06986 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:34:03 +0100 Subject: [PATCH 44/84] Refactor user profile routes to use profileId instead of userId and added route /profile/:profileId/edit --- src/App.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index 83a269c6..c9ebcabd 100644 --- a/src/App.js +++ b/src/App.js @@ -49,10 +49,18 @@ const App = () => { } /> - + + + } + /> + + } /> From 648a1f5253036cd56195a0b9a7dd9c1b98767342 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:37:40 +0100 Subject: [PATCH 45/84] Refactor profile page CSS --- src/pages/profilePage/profilePage.css | 41 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/pages/profilePage/profilePage.css b/src/pages/profilePage/profilePage.css index 4ff8231c..67e5a73d 100644 --- a/src/pages/profilePage/profilePage.css +++ b/src/pages/profilePage/profilePage.css @@ -74,7 +74,8 @@ gap: 32px; } -.profile-grid-section.read-only input { +.profile-grid-section.read-only input, +.profile-grid-section.read-only textarea { pointer-events: none; /* Disable all mouse events */ } @@ -120,26 +121,42 @@ textarea { margin: 0; } -.info-text { +.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; } -.edit-button { +.button-group { + display: flex; + flex-direction: row; + gap: 8px; + gap: 16px; +} + +.button { display: flex; flex-direction: row; justify-content: center; - margin-left: auto; + padding: 14px 24px; - gap: 8px; - background: var(--color-blue); border-radius: 8px; font-style: normal; @@ -147,9 +164,19 @@ textarea { font-size: 20px; line-height: 28px; text-align: center; - color: #ffffff; 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); +} From a6c60ac9477b56be50baf9cedf86d8dd8003e020 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:38:28 +0100 Subject: [PATCH 46/84] Add isEditMode to enable profile editing functionality - Introduced isEditMode state to toggle between view and edit modes - Added functionality for users/teachers to edit profile content - Implemented Cancel button to rollback changes - Implemented Submit button to save changes and scroll to top --- .../profilePage/components/BasicInfoForm.jsx | 8 +- src/pages/profilePage/components/BioForm.jsx | 15 +++- .../components/ContactInfoForm.jsx | 7 +- .../components/TrainingInfoForm.jsx | 19 ++++- src/pages/profilePage/index.js | 84 ++++++++++++++----- 5 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/pages/profilePage/components/BasicInfoForm.jsx b/src/pages/profilePage/components/BasicInfoForm.jsx index 091bace0..1aa9a705 100644 --- a/src/pages/profilePage/components/BasicInfoForm.jsx +++ b/src/pages/profilePage/components/BasicInfoForm.jsx @@ -5,14 +5,14 @@ import TextInput from '../../../components/form/textInput'; import ProfileCircle from '../../../components/profileCircle'; const BasicInfoForm = () => { - const { handleInputChange, profile, initials } = useContext(ProfileContext); + const { handleInputChange, profile, initials, isEditMode } = useContext(ProfileContext); return (

    Basic Info

    -
    +
    Photo
    @@ -26,6 +26,7 @@ const BasicInfoForm = () => { value={profile.firstName} onChange={handleInputChange} type="text" + isRequired={true} /> { value={profile.lastName} onChange={handleInputChange} type="text" + isRequired={true} /> { value={profile.username} onChange={handleInputChange} type="text" + isRequired={true} /> { value={profile.githubUsername} onChange={handleInputChange} type="text" + isRequired={true} />
    diff --git a/src/pages/profilePage/components/BioForm.jsx b/src/pages/profilePage/components/BioForm.jsx index 02aa4955..88339ec3 100644 --- a/src/pages/profilePage/components/BioForm.jsx +++ b/src/pages/profilePage/components/BioForm.jsx @@ -3,7 +3,10 @@ import Form from '../../../components/form'; import { ProfileContext } from '..'; const BioForm = () => { - const { profile, handleBioChange, bioLength } = useContext(ProfileContext); + const { profile, handleBioChange, isEditMode } = useContext(ProfileContext); + const maxLength = 300; + const isMaxLengthReached = profile.bio.length >= maxLength; + return (
    @@ -11,15 +14,19 @@ const BioForm = () => {

    Bio

    -
    +
    -

    {bioLength}/300

    +
    +

    {profile.bio.length}/300

    + {isMaxLengthReached &&

    Max length is 300 characters

    } +
    diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx index 405bc8f9..875f575e 100644 --- a/src/pages/profilePage/components/ContactInfoForm.jsx +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -4,20 +4,21 @@ import Form from '../../../components/form'; import TextInput from '../../../components/form/textInput'; const ContactInfoForm = () => { - const { profile, handleInputChange } = useContext(ProfileContext); + const { profile, handleInputChange, isEditMode } = useContext(ProfileContext); return (

    Contact Info

    -
    +
    { value={profile.mobile} onChange={handleInputChange} type="number" + isRequired={true} /> { value={profile.password} onChange={handleInputChange} type="password" + isRequired={true} />
    diff --git a/src/pages/profilePage/components/TrainingInfoForm.jsx b/src/pages/profilePage/components/TrainingInfoForm.jsx index 9a2ee116..f67fcfaa 100644 --- a/src/pages/profilePage/components/TrainingInfoForm.jsx +++ b/src/pages/profilePage/components/TrainingInfoForm.jsx @@ -4,20 +4,25 @@ import Form from '../../../components/form'; import TextInput from '../../../components/form/textInput'; const TrainingInfoForm = () => { - const { profile, handleInputChange, formatRole } = useContext(ProfileContext); + const { profile, handleInputChange, formatRole, isEditMode, isCurrentUserTeacher } = + useContext(ProfileContext); return (

    Training Info

    -
    +
    { value={profile.specialism} onChange={handleInputChange} type="text" + isLocked={isEditMode && !isCurrentUserTeacher} + isRequired={true} /> { value={profile.startDate} onChange={handleInputChange} type="text" + isLocked={isEditMode && !isCurrentUserTeacher} + isRequired={true} /> { value={profile.endDate} onChange={handleInputChange} type="text" + isLocked={isEditMode && !isCurrentUserTeacher} + isRequired={true} />
    diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index e894b8ec..a491872f 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -1,5 +1,5 @@ import { createContext, useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { NavLink, useParams } from 'react-router-dom'; import { user } from '../../service/mockData'; import Card from '../../components/card'; import './profilePage.css'; @@ -11,22 +11,27 @@ import BioForm from './components/BioForm'; import Button from '../../components/button'; // eslint-disable-next-line camelcase -// import jwt_decode from 'jwt-decode'; +import jwt_decode from 'jwt-decode'; +import useAuth from '../../hooks/useAuth'; export const ProfileContext = createContext(); -const UserProfile = () => { +const UserProfile = ({ isEditMode }) => { const { profileId } = useParams(); const [profile, setProfile] = useState(null); + const [rollbackProfile, setRoolbackProfile] = useState(null); const [initials, setInitials] = useState(''); - const [bioLength, setBioLength] = useState(0); - // const { token } = useAuth(); - // const { userId } = jwt_decode(storedToken); + const [isCurrentUserProfile, setIsCurrentUserProfile] = useState(false); + const [isCurrentUserTeacher, setIsCurrentUserTeacher] = useState(false); + + const { token, role } = useAuth(); + const { userId } = jwt_decode(token); + console.log('role:', role); const fetchProfile = () => { setProfile(user.user); + setRoolbackProfile(user.user); setInitials(user.user.firstName[0] + user.user.lastName[0]); - setBioLength(user.user.bio.length); /* try { const data = await get(`profiles/${profileId}`); setProfile(data); @@ -37,14 +42,26 @@ const UserProfile = () => { } */ }; + console.log('profileId:', profileId); + useEffect(() => { fetchProfile(); + if (Number(userId) === profileId) { + setIsCurrentUserProfile(true); + } + if (role === 'STUDENT') { + setIsCurrentUserTeacher(true); + } + + console.log('isCurrentUserProfile:', isCurrentUserProfile); + console.log('isCurrentUserTeacher:', isCurrentUserTeacher); }, [profileId]); const handleSubmit = (e) => { // Handle form submission e.preventDefault(); - console.log('Form submitted'); + console.log(profile); + window.scrollTo({ top: 0, behavior: 'smooth' }); }; const handleInputChange = (e) => { @@ -57,8 +74,10 @@ const UserProfile = () => { const handleBioChange = (event) => { handleInputChange(event); - console.log(event.target.value.length); - setBioLength(event.target.value.length); + }; + + const handleCancel = () => { + setProfile(rollbackProfile); }; const formatRole = (role) => { @@ -82,7 +101,9 @@ const UserProfile = () => { handleInputChange, formatRole, handleBioChange, - bioLength + isEditMode, + isCurrentUserProfile, + isCurrentUserTeacher }; return ( @@ -97,14 +118,39 @@ const UserProfile = () => {

    *Required

    -
    + + ) : ( + +
    From 7141e9fe09968065bc4a242427bce23d931ebeaa Mon Sep 17 00:00:00 2001 From: sebgro98 Date: Wed, 30 Oct 2024 14:47:12 +0100 Subject: [PATCH 47/84] revert eslint.json file --- .eslintrc.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index deef124a..6b70ab54 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,16 +16,11 @@ "plugins": ["react", "prettier"], "rules": { "react/react-in-jsx-scope": "off", - "react/prop-types": 0, - "prettier/prettier": [ - "error", { - "endOfLine": "auto" - } - ] + "react/prop-types": 0 }, "settings": { "react": { "version": "detect" } } -} +} \ No newline at end of file From cbc53a9aa95f80f21db12b6dc6e7b0d36e3ab648 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:57:31 +0100 Subject: [PATCH 48/84] Refactor profile page useEffect to set isCurrentUserTeacher correct regarding if it is a teatcher --- src/pages/profilePage/index.js | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index a491872f..c14c7c57 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -26,7 +26,6 @@ const UserProfile = ({ isEditMode }) => { const { token, role } = useAuth(); const { userId } = jwt_decode(token); - console.log('role:', role); const fetchProfile = () => { setProfile(user.user); @@ -42,25 +41,19 @@ const UserProfile = ({ isEditMode }) => { } */ }; - console.log('profileId:', profileId); - useEffect(() => { fetchProfile(); - if (Number(userId) === profileId) { + if (userId === Number(profileId)) { setIsCurrentUserProfile(true); } - if (role === 'STUDENT') { + if (role === 'TEACHER') { setIsCurrentUserTeacher(true); } - - console.log('isCurrentUserProfile:', isCurrentUserProfile); - console.log('isCurrentUserTeacher:', isCurrentUserTeacher); }, [profileId]); const handleSubmit = (e) => { // Handle form submission e.preventDefault(); - console.log(profile); window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -140,16 +133,18 @@ const UserProfile = ({ isEditMode }) => { ) : ( - -
    From 002f26978f3962d4790ade93f539d2d0d1150790 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:17:52 +0100 Subject: [PATCH 49/84] Refactor profile page to use useNavigate instead of NavLink for routing --- src/pages/profilePage/index.js | 40 ++++++++++++++++------------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index c14c7c57..8c99cfa2 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -1,5 +1,5 @@ import { createContext, useEffect, useState } from 'react'; -import { NavLink, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { user } from '../../service/mockData'; import Card from '../../components/card'; import './profilePage.css'; @@ -17,6 +17,7 @@ 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); @@ -54,6 +55,7 @@ const UserProfile = ({ isEditMode }) => { const handleSubmit = (e) => { // Handle form submission e.preventDefault(); + navigate(`/profile/${userId}`); window.scrollTo({ top: 0, behavior: 'smooth' }); }; @@ -71,6 +73,7 @@ const UserProfile = ({ isEditMode }) => { const handleCancel = () => { setProfile(rollbackProfile); + navigate(`/profile/${userId}`); }; const formatRole = (role) => { @@ -114,36 +117,31 @@ const UserProfile = ({ isEditMode }) => { {isEditMode ? ( <>
    - -
    ) : ( (isCurrentUserProfile || isCurrentUserTeacher) && ( - - - - - -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    + {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

    + )} @@ -148,61 +120,48 @@ const Cohort = () => {

    Teachers

    {/* This is where the list of teachers will go. Each section is a teacher. */} -
    -
    -

    DA

    -
    + {teachers.length >= 1 ? ( + teachers.map((teacher) => { + return ( +
    +
    +

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

    +
    -
    -

    Dave Ames

    - Software Developer -
    -
    - -
    -
    -
    -
    -

    NS

    -
    -
    -

    Nigel Smith

    - Software Developer -
    -
    - -
    -
    +
    +

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

    + Software Developer +
    +
    + +
    +
    + ); + }) + ) : ( +

    No teachers in this Cohort

    + )} diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css index 4138f4e5..c2b74738 100644 --- a/src/pages/cohort/style.css +++ b/src/pages/cohort/style.css @@ -30,4 +30,12 @@ .cohort-action-button { padding: 0px 0px !important; margin-top: 5px; +} + +.loading { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + flex-direction: column; } \ No newline at end of file diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..ece88ebf 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -18,6 +18,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 +57,4 @@ async function request(method, endpoint, data, auth = true) { return response.json(); } -export { login, getPosts, register, createProfile }; +export { login, getPosts, register, createProfile, getCohorts }; From 0a3ace133d44cd016084227c7d836ac170e1ffbd Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:33:49 +0100 Subject: [PATCH 52/84] Refactor formatRole function to handle role capitalization --- src/pages/profilePage/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/profilePage/index.js b/src/pages/profilePage/index.js index 8c99cfa2..b84c15eb 100644 --- a/src/pages/profilePage/index.js +++ b/src/pages/profilePage/index.js @@ -77,11 +77,8 @@ const UserProfile = ({ isEditMode }) => { }; const formatRole = (role) => { - if (role === 'STUDENT') { - return 'Student'; - } else { - return 'Teacher'; - } + if (!role) return ''; + return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase(); }; if (!profile) { From 0d85659dfc7340f8d807192a3de207a21788f894 Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:34:01 +0100 Subject: [PATCH 53/84] Refactor TrainingInfoForm to validate input fields with specific characters --- src/pages/profilePage/components/TrainingInfoForm.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/profilePage/components/TrainingInfoForm.jsx b/src/pages/profilePage/components/TrainingInfoForm.jsx index f67fcfaa..2124ac74 100644 --- a/src/pages/profilePage/components/TrainingInfoForm.jsx +++ b/src/pages/profilePage/components/TrainingInfoForm.jsx @@ -23,6 +23,7 @@ const TrainingInfoForm = () => { type="text" isLocked={isEditMode && !isCurrentUserTeacher} isRequired={true} + validChars="A-Za-z0-9 -" /> { type="text" isLocked={isEditMode && !isCurrentUserTeacher} isRequired={true} + validChars="A-Za-z0-9 -" /> { type="text" isLocked={isEditMode && !isCurrentUserTeacher} isRequired={true} + validChars="A-Za-z0-9 -" /> From 1e7d9e6653580699187050f73a1bddc959e64a16 Mon Sep 17 00:00:00 2001 From: shyye Date: Wed, 30 Oct 2024 16:53:32 +0100 Subject: [PATCH 54/84] Add minLength to textInput component --- src/components/form/textInput/index.js | 4 ++++ src/pages/register/index.js | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/form/textInput/index.js b/src/components/form/textInput/index.js index 4fa84375..3310b8fd 100644 --- a/src/components/form/textInput/index.js +++ b/src/components/form/textInput/index.js @@ -9,6 +9,7 @@ const TextInput = ({ type = 'text', isRequired = false, validChars = 'A-Za-z0-9@_-', + minLength = 0, maxLength = 50, isLocked = false }) => { @@ -35,6 +36,9 @@ const TextInput = ({ setError( `Input must be up to ${maxLength} characters long and contain only: ${validChars.split('').join(', ')}` ); + } else if (value.length < minLength) { + setError(`Input must be at least ${minLength} characters long`); + onChange(event); } else { setError(''); } diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 5d08daa1..87be4aa9 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -11,10 +11,20 @@ const Register = () => { const [formData, setFormData] = useState({ email: '', password: '' }); const [errorMessage, setErrorMessage] = useState(''); - const isRequiredFieldsProvided = formData.email && formData.password; + // Email validation const isValidEmail = formData.email.match(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/); - const isValidPassword = formData.password.length >= 8; + // 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 passwordRegex = /^(?=.*[A-Z])(?=.*[0-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) => { @@ -40,6 +50,7 @@ const Register = () => { name="email" label={'Email *'} isRequired={true} + validChars={'A-Za-z0-9@._-'} /> { label={'Password *'} type={'password'} isRequired={true} + validChars={'A-Za-z0-9@._-'} + minLength={8} /> {errorMessage && } From 3286d10099d36b6064430c66b754e4c04f15c44a Mon Sep 17 00:00:00 2001 From: Simon Fredriksson <128689713+SimpFred@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:53:36 +0100 Subject: [PATCH 55/84] Refactor ContactInfoForm to validate input fields with specific characters --- src/pages/profilePage/components/ContactInfoForm.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx index 2f8bbd7e..244f4156 100644 --- a/src/pages/profilePage/components/ContactInfoForm.jsx +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -18,6 +18,7 @@ const ContactInfoForm = () => { value={profile.email} onChange={handleInputChange} type="email" + validChars="A-Za-z0-9@.-_" isRequired={true} /> Date: Wed, 30 Oct 2024 16:56:14 +0100 Subject: [PATCH 56/84] Refactor ContactInfoForm to remove validChars prop from email input --- src/pages/profilePage/components/ContactInfoForm.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx index 244f4156..2f8bbd7e 100644 --- a/src/pages/profilePage/components/ContactInfoForm.jsx +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -18,7 +18,6 @@ const ContactInfoForm = () => { value={profile.email} onChange={handleInputChange} type="email" - validChars="A-Za-z0-9@.-_" isRequired={true} /> Date: Wed, 30 Oct 2024 17:05:40 +0100 Subject: [PATCH 57/84] merge with main --- .eslintrc.json | 2 +- src/components/post/index.js | 66 +++- src/components/post/style.css | 111 ++++++- src/components/posts/index.js | 1 + src/pages/cohort/index.js | 301 ++++++++---------- src/pages/cohort/style.css | 8 + .../components/ContactInfoForm.jsx | 2 + src/pages/register/index.js | 1 + src/service/apiClient.js | 7 +- 9 files changed, 316 insertions(+), 183 deletions(-) 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/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/pages/cohort/index.js b/src/pages/cohort/index.js index 018b6afe..43201c2e 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -1,8 +1,39 @@ +import { useEffect, useState } from 'react'; import Card from '../../components/card'; import './style.css'; +import { getCohorts } from '../../service/apiClient'; const Cohort = () => { - /* Waiting for the backend implementation of Cohort */ + const [cohort, setCohort] = useState([]); + const [students, setStudents] = useState([]); + const [teachers, setTeachers] = useState([]); + + useEffect(() => { + const fetchCohorts = async () => { + const cohorts = await getCohorts(); + // Divert the teacher and students into separate arrays for easy access. Later, this would be not using cohort[0] but depending on which cohort is selected. + setStudents(cohorts[0].users.filter((user) => user.role === 'STUDENT')); + setTeachers(cohorts[0].users.filter((user) => user.role === 'TEACHER')); + setCohort(cohorts); + }; + fetchCohorts(); + }, []); + + if (cohort.length === 0) { + return ( +
    +

    Fetching Cohorts

    +
    + +
    +
    + ); + } + + /* This is the main component for the cohort page. + For future use, there will be a dropdown to select the cohort depending on the current user that is logged in. + As of now, the data is showing the first cohort in the database. Does not matter who is logged in. + */ return ( <>
    @@ -15,128 +46,69 @@ const Cohort = () => {

    < >

    - {/* This is where the cohort name will go */} -

    Software Development, Cohort 4

    - January 2023 - June 2023 + {/* Need to only fetch the cohort from current user that is logged in. Then maybe create a drop down to see a specific cohort. */} +

    + {cohort[0].name}, Cohort {cohort[0].id} +

    + + {new Date(cohort[0].startDate).toLocaleString('default', { + month: 'long', + year: 'numeric' + })}{' '} + -{' '} + {/* ADD space between the dash, ESLINT removs it, therefore I Added {' '} and {' '} */} + {new Date(cohort[0].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. - Fill with proper data from the database. - This is just mockup data and therefore the data is static. - What needs to be done is to .map the data of all the ids from the cohort. + 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. */} -
    -
    -
    -

    AB

    -
    -
    -

    Alexander B

    -
    - {/* This is the button to view the profile of the student and need to implement action of button. */} -
    - -
    -
    -
    -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    -
    -
    -
    -

    AJ

    -
    -
    -

    Alexander Jansson

    -
    -
    - -
    -
    -
    + {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

    + )}
    @@ -148,61 +120,48 @@ const Cohort = () => {

    Teachers

    {/* This is where the list of teachers will go. Each section is a teacher. */} -
    -
    -

    DA

    -
    + {teachers.length >= 1 ? ( + teachers.map((teacher) => { + return ( +
    +
    +

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

    +
    -
    -

    Dave Ames

    - Software Developer -
    -
    - -
    -
    -
    -
    -

    NS

    -
    -
    -

    Nigel Smith

    - Software Developer -
    -
    - -
    -
    +
    +

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

    + Software Developer +
    +
    + +
    +
    + ); + }) + ) : ( +

    No teachers in this Cohort

    + )} diff --git a/src/pages/cohort/style.css b/src/pages/cohort/style.css index 4138f4e5..c2b74738 100644 --- a/src/pages/cohort/style.css +++ b/src/pages/cohort/style.css @@ -30,4 +30,12 @@ .cohort-action-button { padding: 0px 0px !important; margin-top: 5px; +} + +.loading { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + flex-direction: column; } \ No newline at end of file diff --git a/src/pages/profilePage/components/ContactInfoForm.jsx b/src/pages/profilePage/components/ContactInfoForm.jsx index 2f8bbd7e..b195c457 100644 --- a/src/pages/profilePage/components/ContactInfoForm.jsx +++ b/src/pages/profilePage/components/ContactInfoForm.jsx @@ -19,6 +19,7 @@ const ContactInfoForm = () => { onChange={handleInputChange} type="email" isRequired={true} + validChars="A-Za-z0-9@." /> { onChange={handleInputChange} type="password" isRequired={true} + validChars="A-Za-z0-9 -" /> diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 5d08daa1..e817d33c 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -40,6 +40,7 @@ const Register = () => { name="email" label={'Email *'} isRequired={true} + validChars={'A-Za-z0-9@._-'} /> Date: Wed, 30 Oct 2024 18:26:38 +0100 Subject: [PATCH 58/84] Add optional choice for pattern on textInput --- src/components/form/textInput/index.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/form/textInput/index.js b/src/components/form/textInput/index.js index 3310b8fd..dd351f30 100644 --- a/src/components/form/textInput/index.js +++ b/src/components/form/textInput/index.js @@ -1,5 +1,12 @@ +import './textInput.css'; import { useEffect, useState } from 'react'; +/** + * 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 e.g. email and 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, @@ -9,6 +16,8 @@ const TextInput = ({ type = 'text', isRequired = false, validChars = 'A-Za-z0-9@_-', + pattern = null, + patternDescription = null, minLength = 0, maxLength = 50, isLocked = false @@ -36,6 +45,13 @@ const TextInput = ({ 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); From e0f4a1606e7f5aee5ba8709326d85d03c6facf6e Mon Sep 17 00:00:00 2001 From: shyye Date: Wed, 30 Oct 2024 18:27:16 +0100 Subject: [PATCH 59/84] Change font size for error messages --- src/components/form/textInput/textInput.css | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/components/form/textInput/textInput.css 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 From f6aef717250e3dc72c331fffb26302d8856c45e5 Mon Sep 17 00:00:00 2001 From: shyye Date: Wed, 30 Oct 2024 18:28:08 +0100 Subject: [PATCH 60/84] Add pattern to password input --- src/pages/register/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 87be4aa9..0a4a400a 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -19,7 +19,10 @@ const Register = () => { // 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; @@ -59,7 +62,9 @@ const Register = () => { label={'Password *'} type={'password'} isRequired={true} - validChars={'A-Za-z0-9@._-'} + validChars={passwordRegexValidChars} + pattern={passwordRegex} + patternDescription={patternDescription} minLength={8} /> From cc99d0e33903f81c061eb77b8dd2d6c64f1d704d Mon Sep 17 00:00:00 2001 From: shyye Date: Wed, 30 Oct 2024 18:29:25 +0100 Subject: [PATCH 61/84] Change _form.css to fix position for the password eye symbol and the show password box --- src/styles/_form.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/_form.css b/src/styles/_form.css index 57ac8e31..a9639892 100644 --- a/src/styles/_form.css +++ b/src/styles/_form.css @@ -57,7 +57,7 @@ form label { .showpasswordbutton { position: absolute; - bottom: 28px; + top: 20px; right: 16px; z-index: 2; background: none; @@ -89,7 +89,7 @@ form label { .passwordreveal { position: absolute; - bottom: 0; + /* bottom: 0; */ right: 0; width: 100%; z-index: 1; From 03d1679fdc238bbf5f83dfd7599bd06534419497 Mon Sep 17 00:00:00 2001 From: shyye Date: Thu, 31 Oct 2024 09:06:45 +0100 Subject: [PATCH 62/84] Add '*Required' text --- src/pages/register/index.js | 1 + src/pages/register/register.css | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/pages/register/index.js b/src/pages/register/index.js index 0a4a400a..11c97ad1 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -69,6 +69,7 @@ const Register = () => { /> {errorMessage && } +
    *Required
    } + + ) : ( + <> +

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

    + + + )} + + ); +}; + +export default SearchResults; 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 From c935988b652e4e7bf5089df6ef79ef1603fe1447 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Thu, 31 Oct 2024 09:34:46 +0100 Subject: [PATCH 65/84] Added and removed documentation --- src/pages/cohort/index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js index dd2a49b6..d161e27d 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -18,7 +18,7 @@ const Cohort = () => { const allCohorts = await getCohorts(); const cohortToAdd = []; const { userId } = jwt_decode(token); - // Divert the teacher and students into separate arrays for easy access. Later, this would be not using cohort[0] but depending on which cohort is selected. + // 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); @@ -52,10 +52,6 @@ const Cohort = () => { setTeachers(selectedCohort.users.filter((user) => user.role === 'TEACHER')); }; - /* This is the main component for the cohort page. - For future use, there will be a dropdown to select the cohort depending on the current user that is logged in. - As of now, the data is showing the first cohort in the database. Does not matter who is logged in. - */ return ( <>
    @@ -68,7 +64,7 @@ const Cohort = () => {

    < >

    - {/* Need to only fetch the cohort from current user that is logged in. Then maybe create a drop down to see a specific cohort. */} + {/* This is a drop down menu so the user can change which cohort to view. */}
    + + +
    + +
    +
    + +
    + +
    +
    +
    + ); +}; + +export default CreateCohortModal; diff --git a/src/components/createCohortModal/style.cs b/src/components/createCohortModal/style.cs new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js index 01235285..2a448b21 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -1,24 +1,26 @@ import { useEffect, useState } from 'react'; -import Card from '../../components/card'; +// 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 } = useAuth(); + 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 = []; - const { userId } = jwt_decode(token); // Filter cohorts to include only those the user belongs to allCohorts.forEach((cohort) => { @@ -66,147 +68,23 @@ const Cohort = () => { 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

    - )} -
    -
    -
    - - + {role === 'TEACHER' ? ( + + ) : ( + + )} ); }; diff --git a/src/pages/cohort/student.js b/src/pages/cohort/student.js new file mode 100644 index 00000000..6baac963 --- /dev/null +++ b/src/pages/cohort/student.js @@ -0,0 +1,152 @@ +import Card from '../../components/card'; + +const StudentView = ({ cohort, handleCohortChange, selectedCohort, students, teachers }) => { + 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 index 1388cb35..3beb6a33 100644 --- a/src/pages/cohort/style.css +++ b/src/pages/cohort/style.css @@ -2,6 +2,9 @@ height: 56px; border-bottom: 1px solid #dce1f0; margin-bottom: 1rem; + display: flex; + flex-direction: row; + justify-content: space-between; } .cohort-div { diff --git a/src/pages/cohort/teacher.js b/src/pages/cohort/teacher.js new file mode 100644 index 00000000..58cc96e7 --- /dev/null +++ b/src/pages/cohort/teacher.js @@ -0,0 +1,172 @@ +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 post', ); // CreatePostModal is just a standard React component, nothing special + + // Open the modal! + openModal(); + }; + 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; From 5bd9df2be8d231761366c210ecb33db68d852214 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Thu, 31 Oct 2024 14:03:26 +0100 Subject: [PATCH 80/84] Added better titles for modal --- src/components/createCohortModal/index.js | 2 +- src/pages/cohort/teacher.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/createCohortModal/index.js b/src/components/createCohortModal/index.js index 78644254..bd68e5fd 100644 --- a/src/components/createCohortModal/index.js +++ b/src/components/createCohortModal/index.js @@ -58,7 +58,7 @@ const CreateCohortModal = () => {
    +
    + + ); + } return ( <>
    From dfb720e953e0cc5c96f546a615cd498ddb4b2fb8 Mon Sep 17 00:00:00 2001 From: Valen98 Date: Thu, 31 Oct 2024 14:27:56 +0100 Subject: [PATCH 83/84] Removed a useless return :) --- src/pages/cohort/index.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js index 2a448b21..724540ac 100644 --- a/src/pages/cohort/index.js +++ b/src/pages/cohort/index.js @@ -47,17 +47,6 @@ const Cohort = () => { fetchCohorts(cohortId); }, [location.state]); - if (cohort.length === 0) { - return ( -
    -

    Fetching Cohorts

    -
    - -
    -
    - ); - } - const handleCohortChange = (event) => { const selectedCohortId = event.target.value; const selectedCohort = cohort.find((c) => c.id === parseInt(selectedCohortId)); From 8d6d7bb1843b5cce3af6cce75064ac160a4b4b10 Mon Sep 17 00:00:00 2001 From: Fredrik Haupt Date: Thu, 31 Oct 2024 14:41:47 +0100 Subject: [PATCH 84/84] Merge for testing