setIsMenuVisible(!isMenuVisible)}>
- {isMenuVisible &&
}
+
+ <>
+ {clickable ? (
+
setIsMenuVisible(!isMenuVisible)}>
+ {isMenuVisible &&
}
+
+ ) : (
+
+ )}
+ >
+
);
};
-const CascadingMenu = () => {
+
+export const CascadingMenu = ({ id, setIsMenuVisible, setRefresh}) => {
+
+ const [clicked, setClicked] = useState(false);
+
return (
);
};
diff --git a/src/components/profileCircle/style.css b/src/components/profileCircle/style.css
index 47391fe7..066f134d 100644
--- a/src/components/profileCircle/style.css
+++ b/src/components/profileCircle/style.css
@@ -3,6 +3,10 @@
cursor: pointer;
}
+.profile-circle-noclick {
+ position: relative;
+}
+
.profile-circle-menu {
margin-left: 65px;
-}
+}
\ No newline at end of file
diff --git a/src/components/seeProfile/index.js b/src/components/seeProfile/index.js
new file mode 100644
index 00000000..a8b2996b
--- /dev/null
+++ b/src/components/seeProfile/index.js
@@ -0,0 +1,41 @@
+import Card from '../card';
+import './style.css';
+import { NavLink } from 'react-router-dom';
+import ProfileIcon from '../../assets/icons/profileIcon';
+import SimpleProfileCircle from '../simpleProfileCircle';
+
+
+const SeeProfile = ({ id, initials, firstname, lastname, role, photo=null }) => {
+ return (
+
+
+
+
+
+
+
{firstname} {lastname}
+
{role}
+
+
+
+
+
+
+ )
+
+}
+
+export default SeeProfile;
\ No newline at end of file
diff --git a/src/components/seeProfile/style.css b/src/components/seeProfile/style.css
new file mode 100644
index 00000000..9ba58d0d
--- /dev/null
+++ b/src/components/seeProfile/style.css
@@ -0,0 +1,9 @@
+.user-panel {
+ position: absolute;
+ z-index: 2000;
+}
+
+
+.card {
+ width: 450px;
+}
\ No newline at end of file
diff --git a/src/components/simpleProfileCircle/index.js b/src/components/simpleProfileCircle/index.js
new file mode 100644
index 00000000..f6b76b0f
--- /dev/null
+++ b/src/components/simpleProfileCircle/index.js
@@ -0,0 +1,65 @@
+import './style.css';
+
+const SimpleProfileCircle = ({ initials, size = 56, photo }) => {
+ const styleGuideColors = [
+ "#28C846",
+ "#A0E6AA",
+ "#46DCD2",
+ "#82E6E6",
+ "#5ABEDC",
+ "#46C8FA",
+ "#46A0FA",
+ "#666EDC"
+ ];
+
+ const getColorFromInitials = (initials) => {
+ if (!initials || typeof initials !== 'string') return styleGuideColors[0];
+
+ let hash = 0;
+ for (let i = 0; i < initials.length; i++) {
+ hash = initials.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const index = Math.abs(hash) % styleGuideColors.length;
+ return styleGuideColors[index];
+ };
+
+ const backgroundColor = getColorFromInitials(initials);
+
+ // If photo is provided, show image instead of initials
+
+ if (photo) {
+ return (
+
+

+
+ );
+ }
+
+ // Default behavior - show initials with colored background
+ return (
+
40 ? '14px' : '12px'
+ }}
+ >
+
{initials}
+
+ );
+};
+
+export default SimpleProfileCircle;
\ No newline at end of file
diff --git a/src/components/simpleProfileCircle/style.css b/src/components/simpleProfileCircle/style.css
new file mode 100644
index 00000000..84047821
--- /dev/null
+++ b/src/components/simpleProfileCircle/style.css
@@ -0,0 +1,30 @@
+.simple-profile-circle {
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: 600;
+ text-transform: uppercase;
+ /* border: 1px solid #000; */
+}
+
+.simple-profile-circle p {
+ margin: 0;
+ line-height: 1;
+ /* font-size: inherit; */
+}
+
+/* Image variant styling */
+.simple-profile-circle--image {
+ background-color: transparent;
+ overflow: hidden;
+ border: 1px solid #000;
+}
+
+.simple-profile-circle__image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+}
\ No newline at end of file
diff --git a/src/components/socialLinks/index.js b/src/components/socialLinks/index.js
index 9b112adb..103214c7 100644
--- a/src/components/socialLinks/index.js
+++ b/src/components/socialLinks/index.js
@@ -9,68 +9,34 @@ const SocialLinks = () => {
}}
className="socialbutton"
>
-
-
-
-
- >
+
+
+ >
+
);
};
export default SocialLinks;
+
+
+
+
diff --git a/src/components/stepper/index.js b/src/components/stepper/index.js
index c9e5f259..6013cdd4 100644
--- a/src/components/stepper/index.js
+++ b/src/components/stepper/index.js
@@ -4,7 +4,7 @@ import Button from '../button';
import './style.css';
import { useState } from 'react';
-const Stepper = ({ header, children, onComplete }) => {
+const Stepper = ({ header, children, onComplete, data }) => {
const [currentStep, setCurrentStep] = useState(0);
const onBackClick = () => {
@@ -22,6 +22,33 @@ const Stepper = ({ header, children, onComplete }) => {
setCurrentStep(currentStep + 1);
};
+ const validateName = (data) => {
+ if(!data) {
+ alert("OBSS!!! Please write first name and last name")
+ return false
+ } else {
+ return true
+ }
+ }
+
+ const validateUsername = (data) => {
+ if(data.username.length < 7) {
+ alert("Username is too short. Input must be at least 7 characters long")
+ return false
+ } else {
+ return true
+ }
+ }
+
+ const validateMobile = (data) => {
+ if(data.length < 8) {
+ alert("Mobile number is too short. Input must be at least 8 characters long")
+ return false
+ } else {
+ return true
+ }
+ }
+
return (
{header}
@@ -33,14 +60,34 @@ const Stepper = ({ header, children, onComplete }) => {
-
);
};
-export default Stepper;
+export default Stepper;
\ No newline at end of file
diff --git a/src/context/auth.js b/src/context/auth.js
index 47cd66c9..18d101fc 100644
--- a/src/context/auth.js
+++ b/src/context/auth.js
@@ -4,7 +4,7 @@ import Header from '../components/header';
import Modal from '../components/modal';
import Navigation from '../components/navigation';
import useAuth from '../hooks/useAuth';
-import { createProfile, login, register } from '../service/apiClient';
+import { createNewStudent, createProfile, login, refreshToken, register, getUserById } from '../service/apiClient';
// eslint-disable-next-line camelcase
import jwt_decode from 'jwt-decode';
@@ -15,11 +15,13 @@ const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const [token, setToken] = useState(null);
+ const [user, setUser] = useState(null);
+ const [userPhoto, setUserPhoto] = useState(localStorage.getItem('userPhoto'));
useEffect(() => {
const storedToken = localStorage.getItem('token');
- if (storedToken) {
+ if (storedToken && !token) {
setToken(storedToken);
navigate(location.state?.from?.pathname || '/');
}
@@ -34,26 +36,96 @@ const AuthProvider = ({ children }) => {
localStorage.setItem('token', res.data.token);
- setToken(res.token);
- navigate(location.state?.from?.pathname || '/');
+ setToken(res.data.token);
+
+ navigate(location.state?.from?.pathname || '/');
+
+ // After successful login, fetch and store user data
+ try {
+ const { userId } = jwt_decode(res.data.token);
+ const userData = await getUserById(userId);
+ const photo = userData.profile?.photo;
+
+ if (photo) {
+ localStorage.setItem('userPhoto', photo);
+ setUserPhoto(photo);
+ }
+ } catch (error) {
+ console.error('Error fetching user photo:', error);
+ }
};
const handleLogout = () => {
localStorage.removeItem('token');
+ localStorage.removeItem('userPhoto');
setToken(null);
+ setUserPhoto(null);
+ };
+
+ // Force a token refresh by setting the token again to trigger useEffect in other contexts
+ const forceTokenRefresh = () => {
+ const currentToken = token || localStorage.getItem('token');
+ if (currentToken) {
+ console.log("token is found and trying to refresh, but not refreshed?");
+ // Force re-render and context updates by setting token again
+ setToken(null);
+
+ setTimeout(() => {
+ setToken(currentToken);
+ }, 100);
+ }
};
const handleRegister = async (email, password) => {
const res = await register(email, password);
- setToken(res.data.token);
+
+ localStorage.setItem('token', res.data.token);
+ setToken(res.data.token);
navigate('/verification');
};
- const handleCreateProfile = async (firstName, lastName, githubUrl, bio) => {
+ /* eslint-disable camelcase */
+ const handleCreateProfile = async (first_name, last_name, username, github_username, mobile, bio, role, specialism, cohort, start_date, end_date, photo) => {
const { userId } = jwt_decode(token);
- await createProfile(userId, firstName, lastName, githubUrl, bio);
+ try {
+ const response = await createProfile(userId, first_name, last_name, username, github_username, mobile, bio, role, specialism, cohort, start_date, end_date, photo);
+
+ // Check if the backend returned a new token with updated user info
+ if (response.data?.token) {
+ // Use the new token from the response
+ localStorage.setItem('token', response.data.token);
+ setToken(response.data.token);
+ } else {
+ // Try to refresh the token to get updated user information
+ try {
+ const refreshResponse = await refreshToken();
+ if (refreshResponse.token) {
+ localStorage.setItem('token', refreshResponse.token);
+ setToken(refreshResponse.token);
+ console.log('Token refreshed successfully after profile creation');
+ } else {
+ // If token refresh is not available, force a refresh of contexts
+ forceTokenRefresh();
+ }
+ } catch (refreshError) {
+ console.log('Token refresh not available, forcing context refresh');
+ // Force a refresh of all contexts that depend on the token
+ forceTokenRefresh();
+ }
+ }
+
+ navigate('/');
+ } catch (error) {
+ console.error('Error creating profile:', error);
+ throw error;
+ }
+ };
+
+ const handleCreateNewStudent = async (first_name, last_name, username, github_username, email, mobile, password, bio, role, specialism, cohort, start_date, end_date, photo) => {
+
+ await createNewStudent(first_name, last_name, username, github_username, email, mobile, password, bio, role, specialism, cohort, start_date, end_date, photo);
localStorage.setItem('token', token);
navigate('/');
@@ -61,10 +133,14 @@ const AuthProvider = ({ children }) => {
const value = {
token,
+ user,
+ userPhoto,
+ setUserPhoto,
onLogin: handleLogin,
onLogout: handleLogout,
onRegister: handleRegister,
- onCreateProfile: handleCreateProfile
+ onCreateProfile: handleCreateProfile,
+ onCreateNewStudent: handleCreateNewStudent
};
return
{children};
diff --git a/src/context/comments.js b/src/context/comments.js
new file mode 100644
index 00000000..75898ff4
--- /dev/null
+++ b/src/context/comments.js
@@ -0,0 +1,74 @@
+import { createContext, useContext } from 'react';
+import { del, postTo } from '../service/apiClient';
+
+const CommentsContext = createContext();
+
+export const CommentsProvider = ({ children }) => {
+ // Add a comment to a specific post
+ const addComment = async (postId, commentData) => {
+ try {
+ const response = await postTo(`posts/${postId}/comments`, commentData);
+ return response;
+ } catch (error) {
+ console.error('Error adding comment:', error);
+ throw error;
+ }
+ };
+
+ // Update a comment
+ const updateComment = async (postId, commentId, commentData) => {
+ try {
+ // Assuming there's a PATCH endpoint for updating comments
+ const response = await fetch(`/api/posts/${postId}/comments/${commentId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(commentData)
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update comment');
+ }
+
+ return response.json();
+ } catch (error) {
+ console.error('Error updating comment:', error);
+ throw error;
+ }
+ };
+
+ // Delete a comment
+ const deleteComment = async (postId, commentId) => {
+ try {
+ await del(`posts/${postId}/comments/${commentId}`);
+ return true;
+ } catch (error) {
+ console.error('Error deleting comment:', error);
+ return false;
+ }
+ };
+
+ const value = {
+ addComment,
+ updateComment,
+ deleteComment
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useComments = () => {
+ const context = useContext(CommentsContext);
+ if (!context) {
+ throw new Error('useComments must be used within a CommentsProvider');
+ }
+ return context;
+};
+
+export default CommentsContext;
\ No newline at end of file
diff --git a/src/context/form.js b/src/context/form.js
new file mode 100644
index 00000000..c602786e
--- /dev/null
+++ b/src/context/form.js
@@ -0,0 +1,15 @@
+import React, { createContext, useContext, useState } from 'react';
+
+const FormContext = createContext();
+
+export const FormProvider = ({ children }) => {
+ const [formData, setFormData] = useState({ email: '', password: '' });
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useFormData = () => useContext(FormContext);
diff --git a/src/context/loading.js b/src/context/loading.js
new file mode 100644
index 00000000..37dbf410
--- /dev/null
+++ b/src/context/loading.js
@@ -0,0 +1,58 @@
+import { createContext, useContext, useState, useCallback, useRef } from 'react';
+
+const LoadingContext = createContext();
+
+export const LoadingProvider = ({ children }) => {
+ const [isGlobalLoading, setIsGlobalLoading] = useState(false);
+ const [loadingMessage, setLoadingMessage] = useState('Loading...');
+ const dashboardInitializedRef = useRef(false); // Track dashboard initialization globally
+
+ const showGlobalLoading = useCallback((message = 'Loading...') => {
+ console.log('Global loading started:', message);
+ setLoadingMessage(message);
+ setIsGlobalLoading(true);
+ }, []);
+
+ const hideGlobalLoading = useCallback(() => {
+ console.log('Global loading ended');
+ setIsGlobalLoading(false);
+ }, []);
+
+ const isDashboardInitialized = useCallback(() => {
+ return dashboardInitializedRef.current;
+ }, []);
+
+ const setDashboardInitialized = useCallback((value) => {
+ console.log('Dashboard initialization status:', value);
+ dashboardInitializedRef.current = value;
+ }, []);
+
+ const resetDashboardInitialization = useCallback(() => {
+ console.log('Resetting dashboard initialization');
+ dashboardInitializedRef.current = false;
+ }, []);
+
+ const value = {
+ isGlobalLoading,
+ loadingMessage,
+ showGlobalLoading,
+ hideGlobalLoading,
+ isDashboardInitialized,
+ setDashboardInitialized,
+ resetDashboardInitialization
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useLoading = () => {
+ const context = useContext(LoadingContext);
+ if (!context) {
+ throw new Error('useLoading must be used within LoadingProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/context/posts.js b/src/context/posts.js
new file mode 100644
index 00000000..2b9d4a4c
--- /dev/null
+++ b/src/context/posts.js
@@ -0,0 +1,173 @@
+import { createContext, useContext, useEffect, useState } from 'react';
+import { del, get, getPosts, patch, postTo } from '../service/apiClient';
+import useAuth from '../hooks/useAuth';
+import jwtDecode from 'jwt-decode';
+
+const PostsContext = createContext();
+
+export const PostsProvider = ({ children }) => {
+ const [posts, setPosts] = useState([]);
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const { token } = useAuth();
+
+ // Fetch posts and user data when token changes
+ useEffect(() => {
+ if (token) {
+ fetchPosts();
+ fetchUser();
+ } else {
+ // Clear data when no token (user logged out)
+ setPosts([]);
+ setUser(null);
+ setLoading(false);
+ }
+ }, [token]); // Re-run when token changes
+
+ const fetchPosts = async () => {
+ try {
+ setLoading(true);
+ const fetchedPosts = await getPosts();
+ setPosts(fetchedPosts.reverse()); // Reverse so newest are first
+ } catch (error) {
+ console.error('Error fetching posts:', error);
+ setPosts([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchUser = async () => {
+ // Re-decode token to get current user info
+ let currentDecodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ currentDecodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in fetchUser:', error);
+ setUser(null);
+ return;
+ }
+
+ const userId = currentDecodedToken.userId;
+ if (userId) {
+ try {
+ const userData = await get(`users/${userId}`);
+ setUser(userData);
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ setUser(null);
+ }
+ } else {
+ setUser(null);
+ }
+ };
+
+ // Add a new post
+ const addPost = (newPost) => {
+ setPosts(prevPosts => [newPost, ...prevPosts]);
+ };
+
+ // Update a post
+ const updatePost = (updatedPost) => {
+ setPosts(prevPosts =>
+ prevPosts.map(post =>
+ post.id === updatedPost.id ? { ...post, ...updatedPost } : post
+ )
+ );
+ };
+
+ // Delete a post
+ const deletePost = async (postId) => {
+ try {
+ await del(`posts/${postId}`);
+ setPosts(prevPosts => prevPosts.filter(post => post.id !== postId));
+ return true;
+ } catch (error) {
+ console.error('Error deleting post:', error);
+ return false;
+ }
+ };
+
+ // Like/unlike a post
+ const toggleLike = async (postId, currentlyLiked) => {
+ try {
+ // Re-decode token to get current user info
+ let currentDecodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ currentDecodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in toggleLike:', error);
+ return false;
+ }
+
+ if (currentlyLiked) {
+ await postTo(`posts/${postId}/like`);
+ } else {
+ await del(`posts/${postId}/like`);
+ }
+
+ // Update user's liked posts
+ await patch(`users/${currentDecodedToken.userId}/like`, { post_id: postId });
+
+ // Refresh user data to get updated liked posts
+ await fetchUser();
+
+ return true;
+ } catch (error) {
+ console.error('Error updating like state:', error);
+ return false;
+ }
+ };
+
+ // Get user's liked posts
+ const getUserLikedPosts = () => {
+ return user?.data?.user?.likedPosts || [];
+ };
+
+ // Reset all data (useful for logout)
+ const resetData = () => {
+ setPosts([]);
+ setUser(null);
+ setLoading(false);
+ };
+
+ // Force refresh user data (useful after profile updates)
+ const refreshUserData = async () => {
+ await fetchUser();
+ };
+
+ const value = {
+ posts,
+ user,
+ loading,
+ addPost,
+ updatePost,
+ deletePost,
+ toggleLike,
+ getUserLikedPosts,
+ fetchPosts,
+ fetchUser,
+ resetData,
+ refreshUserData
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const usePosts = () => {
+ const context = useContext(PostsContext);
+ if (!context) {
+ throw new Error('usePosts must be used within a PostsProvider');
+ }
+ return context;
+};
+
+export default PostsContext;
\ No newline at end of file
diff --git a/src/context/searchResults.js b/src/context/searchResults.js
new file mode 100644
index 00000000..e76dd5e9
--- /dev/null
+++ b/src/context/searchResults.js
@@ -0,0 +1,15 @@
+import { createContext, useContext, useState } from "react";
+
+const SearchResultsContext = createContext();
+
+export const SearchResultsProvider = ({ children }) => {
+ const [searchResults, setSearchResults] = useState([]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useSearchResults = () => useContext(SearchResultsContext);
\ No newline at end of file
diff --git a/src/context/selectedCohort.js b/src/context/selectedCohort.js
new file mode 100644
index 00000000..ab68c1db
--- /dev/null
+++ b/src/context/selectedCohort.js
@@ -0,0 +1,15 @@
+import React, { createContext, useContext, useState } from 'react';
+
+const CohortContext = createContext();
+
+export const CohortProvider = ({ children }) => {
+ const [cohortId, setCohortId] = useState({ cohort: ''});
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSelectedCohortId = () => useContext(CohortContext);
diff --git a/src/context/userRole..js b/src/context/userRole..js
new file mode 100644
index 00000000..acd5bab3
--- /dev/null
+++ b/src/context/userRole..js
@@ -0,0 +1,18 @@
+import { createContext, useContext, useState } from "react";
+
+const UserRoleContext = createContext({
+ userRole: null,
+ setUserRole: () => {},
+})
+
+export const UserRoleProvider = ({ children }) => {
+ const [userRole, setUserRole] = useState(null)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useUserRoleData = () => useContext(UserRoleContext)
\ No newline at end of file
diff --git a/src/pages/addCohort/index.js b/src/pages/addCohort/index.js
new file mode 100644
index 00000000..2c94c285
--- /dev/null
+++ b/src/pages/addCohort/index.js
@@ -0,0 +1,109 @@
+import { useNavigate } from "react-router-dom"
+import ExitIcon from "../../assets/icons/exitIcon"
+import "./style.css"
+import StepperCohort from "./steps"
+import StepOneCohort from "./stepOne"
+import { useEffect, useState } from "react"
+import { get } from "../../service/apiClient"
+import StepTwoCohort from "./stepTwo"
+import StepThreeCohort from "./stepThree"
+
+
+const AddCohort = () =>{
+ const [students, setStudents] = useState([])
+ const [courses, setCourses] = useState([])
+
+ const [cohortName, setCohortName] = useState("")
+ const[startDate, setStartDate] = useState("")
+ const[endDate, setEndDate] = useState("")
+ const [selectedCourse, setSelectedCourse] = useState("")
+ const [selectedStudents, setSelectedStudents] = useState([]);
+
+
+
+
+ useEffect(() => {
+ async function fetchStudents() {
+ try {
+ const response = await get("students");
+ setStudents(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ }
+
+ async function fetchCourses() {
+ try {
+ const response = await get("courses");
+ setCourses(response.data.courses);
+ } catch (error) {
+ console.error("Error fetching courses:", error);
+ }
+ }
+ fetchStudents();
+ fetchCourses()
+ }, []);
+
+ return (
+ <>
+
}
+ cohortName={cohortName}
+ setCohortName={setCohortName}
+ startDa={startDate}
+ setStartDate={setStartDate}
+ endDa={endDate}
+ setEndDate={setEndDate}
+ courses={courses}
+ selectedCourse={selectedCourse}
+ setSelectedCourse={setSelectedCourse}
+ selectedStudents={selectedStudents}>
+
+
+
+
+
+ >
+ )
+}
+
+
+const CohortHeader = () => {
+ const navigate = useNavigate()
+ return (
+ <>
+
+
Add cohort
+
+ navigate(-1)}>
+
+
+
+
+
Create a new cohort
+
+ >
+ )
+}
+export default AddCohort
\ No newline at end of file
diff --git a/src/pages/addCohort/stepOne/index.js b/src/pages/addCohort/stepOne/index.js
new file mode 100644
index 00000000..63b2b941
--- /dev/null
+++ b/src/pages/addCohort/stepOne/index.js
@@ -0,0 +1,85 @@
+
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon"
+import CoursesMenu from "../../addStudent/coursesMenu"
+import { useState } from "react"
+
+
+
+
+const StepOneCohort = ( {setCohortName, setStartDate, setEndDate, cohortName, startDate, endDate, courses, setSelectedCourse, selectedCourse}) => {
+ const [courseIsOpen, setCourseIsOpen] = useState(false)
+
+
+
+ const handleChangeCohortName = (event) => {
+ setCohortName(event.target.value)
+ }
+
+ const handleSelectCourse = (course) => {
+ console.log("selected course" + course)
+ setCourseIsOpen(false)
+ setSelectedCourse(course)
+ }
+
+ const handleStartDate = (event) => {
+ setStartDate(event.target.value)
+ }
+
+ const handleEndDate = (event) => {
+ setEndDate(event.target.value)
+ }
+
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
setCourseIsOpen(true)}>
+ {selectedCourse !== null ? ({selectedCourse.name}
+ ):( Select a course)}
+
+
+
+
+ {courseIsOpen && (
)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>
+ )
+}
+
+export default StepOneCohort
\ No newline at end of file
diff --git a/src/pages/addCohort/stepThree/index.js b/src/pages/addCohort/stepThree/index.js
new file mode 100644
index 00000000..0c4bc167
--- /dev/null
+++ b/src/pages/addCohort/stepThree/index.js
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon";
+import MultipleStudentsMenu from "../stepTwo/multipleStudentsMenu";
+import SearchBarMultiple from "../stepTwo/SearchBarMultiple";
+import CourseIcon from "../../../components/courseIcon";
+
+const StepThreeCohort = ({cohortName, selectedCourse, students, selectedStudents, setSelectedStudents, endDate, startDate}) => {
+ const [isOpenStudents, setIsOpenStudents] = useState(false);
+const [isOpenSearchBar, setIsOpenSearchBar] = useState(false);
+
+
+
+const handleSelectStudent = (student) => {
+
+
+ setSelectedStudents((prevSelected) => {
+ const alreadySelected = prevSelected.find((s) => s.id === student.id);
+ if (alreadySelected) {
+ // Fjern student hvis allerede valgt
+ return prevSelected.filter((s) => s.id !== student.id);
+ } else {
+ // Legg til student
+ return [...prevSelected, student];
+ }
+ })
+
+ setTimeout(()=> {
+ setIsOpenSearchBar(false)
+ }, 500)
+
+ console.log(selectedStudents);
+};
+
+ return (
+ <>
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+
+
Cohort details
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default StepThreeCohort
\ No newline at end of file
diff --git a/src/pages/addCohort/stepTwo/SearchBarMultiple/index.js b/src/pages/addCohort/stepTwo/SearchBarMultiple/index.js
new file mode 100644
index 00000000..d7049d25
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/SearchBarMultiple/index.js
@@ -0,0 +1,72 @@
+import { useRef, useState } from "react";
+import TextInput from "../../../../components/form/textInput";
+import SearchIcon from "../../../../assets/icons/searchIcon";
+import { get } from "../../../../service/apiClient";
+import '../../style.css';
+
+import MultipleStudentsSearch from "../multipleStudentsMenu/searchMultiple";
+
+
+
+const SearchBarMultiple = ({handleSelectStudent, isOpenSearchBar, setIsOpenSearchBar, selectedStudents}) => {
+ const [query, setQuery] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const popupRef = useRef();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ setSearchResults(response.data.profiles);
+ setIsOpenSearchBar(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ {isOpenSearchBar && (
+
+ {searchResults.length > 0 ? (
+
+ ) : (
+
No students with this name found
+ )}
+
+ )}
+
+ >
+ )
+}
+
+export default SearchBarMultiple
\ No newline at end of file
diff --git a/src/pages/addCohort/stepTwo/index.js b/src/pages/addCohort/stepTwo/index.js
new file mode 100644
index 00000000..1e13d62f
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/index.js
@@ -0,0 +1,63 @@
+import { useState } from "react";
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon";
+import MultipleStudentsMenu from "./multipleStudentsMenu";
+import SearchBarMultiple from "./SearchBarMultiple";
+
+const StepTwoCohort = ({students, selectedStudents, setSelectedStudents}) => {
+
+const [isOpenStudents, setIsOpenStudents] = useState(false);
+const [isOpenSearchBar, setIsOpenSearchBar] = useState(false);
+
+
+
+const handleSelectStudent = (student) => {
+
+ console.log("Klikket pΓ₯ student:", student);
+
+ setSelectedStudents((prevSelected) => {
+ const alreadySelected = prevSelected.find((s) => s.id === student.id);
+ if (alreadySelected) {
+ // Fjern student hvis allerede valgt
+ return prevSelected.filter((s) => s.id !== student.id);
+ } else {
+ // Legg til student
+ return [...prevSelected, student];
+ }
+ })
+
+ setTimeout(()=> {
+ setIsOpenSearchBar(false)
+ }, 500)
+
+};
+
+ return (
+ <>
+
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+ >
+ )
+}
+
+export default StepTwoCohort
\ No newline at end of file
diff --git a/src/pages/addCohort/stepTwo/multipleStudentsMenu/index.js b/src/pages/addCohort/stepTwo/multipleStudentsMenu/index.js
new file mode 100644
index 00000000..f7043e0b
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/multipleStudentsMenu/index.js
@@ -0,0 +1,29 @@
+
+
+import MultipleStudentsSearch from "./searchMultiple";
+
+const MultipleStudentsMenu = ({ students, handleSelectStudent, selectedStudents }) => {
+ return (
+ <>
+
+ {students.length > 0 ? (
+
+ ) : (
+
+ )}
+ >
+ );
+
+};
+
+export default MultipleStudentsMenu;
diff --git a/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js b/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
new file mode 100644
index 00000000..5b81f434
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
@@ -0,0 +1,63 @@
+import CheckIcon from "../../../../../assets/icons/checkIcon";
+import "./style.css"
+
+const MultipleStudentsSearch = ({ students, handleSelectStudent , selectedStudents }) => {
+
+ const styleGuideColors = [
+ "#28C846",
+ "#A0E6AA",
+ "#46DCD2",
+ "#82E6E6",
+ "#5ABEDC",
+ "#46C8FA",
+ "#46A0FA",
+ "#666EDC"
+ ];
+
+ const getColorFromInitials = (initials) => {
+ let hash = 0;
+ for (let i = 0; i < initials.length; i++) {
+ hash = initials.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const index = Math.abs(hash) % styleGuideColors.length;
+ return styleGuideColors[index];
+ };
+ return (
+
+<>
+
+ {students.map((student) => {
+ const isSelected = selectedStudents.some((s) => String(s.id) === String(student.id))
+
+
+ return (
+ - handleSelectStudent(student)}
+ >
+
+
+
+
{student.firstName.charAt(0) + student.lastName.charAt(0)}
+
+
+
+
{student.firstName} {student.lastName}
+
+
+ {isSelected && }
+
+ );
+ })}
+
+
+ >
+ );
+};
+
+export default MultipleStudentsSearch;
+
+
+
diff --git a/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css b/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css
new file mode 100644
index 00000000..a31c4790
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css
@@ -0,0 +1,49 @@
+.avatar-list-item {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 72px;
+ padding: 8px 16px;
+ gap: 16px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ transition: background-color 0.2s ease;
+}
+
+
+
+.avatar-list-item:hover {
+ background-color: #f9f9f9;
+}
+
+
+.avatar-list-item.selected {
+ background: #F5FAFF;
+
+}
+
+.avatar-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-color: #ccc; /* Dynamisk farge via JS */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ color: #fff;
+ font-size: 14px;
+}
+
+.avatar-name {
+ font-size: 15px;
+ font-weight: 500;
+ color: #333;
+}
+
+.avatar-checkmark {
+ margin-left: auto;
+ font-size: 16px;
+ color: #28C846;
+}
diff --git a/src/pages/addCohort/steps/index.js b/src/pages/addCohort/steps/index.js
new file mode 100644
index 00000000..9eafe77a
--- /dev/null
+++ b/src/pages/addCohort/steps/index.js
@@ -0,0 +1,132 @@
+import { Snackbar, SnackbarContent } from "@mui/material";
+import { useState } from "react";
+import CheckCircleIcon from "../../../assets/icons/checkCircleIcon";
+import { patch, post } from "../../../service/apiClient";
+import { useNavigate } from "react-router-dom";
+
+
+const StepperCohort = ({ header, children, cohortName, startDa, endDa, selectedCourse, selectedStudents, setSelectedCourse,setEndDate,setStartDate,setCohortName }) => {
+ const [currentStep, setCurrentStep] = useState(0);
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+ const navigate = useNavigate()
+
+
+
+ const onBackClick = () => {
+ if (currentStep > 0) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const onNextClick = () => {
+ setCurrentStep(currentStep + 1);
+
+ };
+
+ const onSkipClick = () => {
+ setCurrentStep(currentStep + 1);
+ };
+
+ const onCancel = () => {
+ setSelectedCourse("")
+ setEndDate("")
+ setStartDate("")
+ setCohortName("")
+ navigate(-1)
+
+
+ }
+
+ const onComplete = () =>{
+ async function addNewCohort() {
+ try {
+ const response = await post("cohorts",
+ {
+ name: cohortName,
+ courseId: selectedCourse.id,
+ startDate: startDa,
+ endDate: endDa
+ });
+ console.log(response)
+
+ const studentIds = selectedStudents.map(student => student.id);
+ const response2 = await patch(`cohorts/${response.id}`,
+ {
+ name: cohortName,
+ courseId: selectedCourse.id,
+ startDate: startDa,
+ endDate: endDa,
+ profileIds: studentIds
+ });
+ console.log(response2)
+ } catch (error) {
+ console.error("Error adding new cohort:", error);
+ }
+ } addNewCohort()
+
+ setSnackbarOpen(true)
+ setTimeout(()=> {
+ navigate("/cohorts")
+ }, 3000)
+ }
+
+ return (
+
+ {header}
+
+ {children[currentStep]}
+
+ {currentStep === 0 ?
+ (
+ Cancel
+ Next
+
+ ) :
+ currentStep === 1 ? (
+
+
Back
+
Skip
+
+ Add students
+
+
+ ) : (
+
+ Back
+ Add cohort
+
+
+
+
+ New cohort created
+
+ }
+ />
+
+
+
+ )
+ }
+
+
+ );
+};
+
+export default StepperCohort;
\ No newline at end of file
diff --git a/src/pages/addCohort/style.css b/src/pages/addCohort/style.css
new file mode 100644
index 00000000..1688010e
--- /dev/null
+++ b/src/pages/addCohort/style.css
@@ -0,0 +1,72 @@
+.add-cohort-card {
+ width: 700px !important;
+ height: auto;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
+ padding: 24px;
+ box-sizing: border-box;
+ margin-left: 50px;
+ margin-top: 50px;
+}
+
+.cohort-name-input,
+.cohort-start-date-input {
+width: 100%;
+ margin: 0;
+ height: 56px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ opacity: 1;
+ background-color: #E6EBF5;
+ border: 1px solid #E6EBF5;
+ font-size: 18px;
+ color: #000046;
+ flex: 1;
+ box-sizing: border-box;
+ font-family: 'Lexend', sans-serif;
+ font-weight: 400;
+}
+
+.s,
+.selected-students-view {
+ overflow-y: auto;
+ height: auto;
+ height: 350px;
+ width: 100%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px 0;
+}
+
+.three-buttons {
+ margin-top: 50px;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 20px;
+ justify-content: space-between;
+}
+
+.cohort-details-group{
+ margin-top:20px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.cohort-details-title {
+ font-size:32px;
+ margin-left:10px;
+}
+
+.selected-students {
+ margin-left:50px;
+ margin-top:10px;
+ font-size: 18px;
+}
\ No newline at end of file
diff --git a/src/pages/addStudent/cohortsMenu/index.js b/src/pages/addStudent/cohortsMenu/index.js
new file mode 100644
index 00000000..ef1f3591
--- /dev/null
+++ b/src/pages/addStudent/cohortsMenu/index.js
@@ -0,0 +1,38 @@
+const CohortsMenu = ({cohorts, onSelect}) => {
+ return (
+ <>
+
+
+ {cohorts.length > 0 ? (
+
+ {cohorts.map((cohort) => (
+ - onSelect(cohort)}>
+
+ Cohort {cohort.id}
+
+
+ ))}
+
+
+ ) : (
+
Please pick a course
+ )}
+
+ >
+ )
+}
+
+
+export default CohortsMenu
\ No newline at end of file
diff --git a/src/pages/addStudent/coursesMenu/index.js b/src/pages/addStudent/coursesMenu/index.js
new file mode 100644
index 00000000..0055bed9
--- /dev/null
+++ b/src/pages/addStudent/coursesMenu/index.js
@@ -0,0 +1,44 @@
+const CoursesMenu = ({courses, onSelect}) => {
+ return (
+ <>
+
+
+ {courses.length > 0 ? (
+
+ {courses.map((course) => (
+ - onSelect(course)}>
+
+ {course.name}
+
+
+ ))}
+
+
+ ) : (
+
+ )}
+
+ >
+ )
+}
+
+export default CoursesMenu
\ No newline at end of file
diff --git a/src/pages/addStudent/index.js b/src/pages/addStudent/index.js
new file mode 100644
index 00000000..2a806b82
--- /dev/null
+++ b/src/pages/addStudent/index.js
@@ -0,0 +1,197 @@
+import ExitIcon from "../../assets/icons/exitIcon";
+import "./style.css";
+import SearchBar from "./searchBar";
+import { useEffect, useState } from "react";
+import { get, patch } from "../../service/apiClient";
+import ArrowDownIcon from "../../assets/icons/arrowDownIcon";
+import StudentsMenu from "./studentsMenu";
+import CoursesMenu from "./coursesMenu";
+import { useNavigate } from "react-router-dom";
+import CohortsMenu from "./cohortsMenu";
+import { Snackbar, SnackbarContent } from '@mui/material';
+import CheckCircleIcon from "../../assets/icons/checkCircleIcon";
+
+
+
+const AddStudent = () => {
+ const [students, setStudents] = useState([])
+ const [courses, setCourses] = useState([])
+ const [cohorts, setCohorts] = useState([])
+
+ const [isOpenCourses, setIsOpenCourses] = useState(false);
+ const [isOpenStudents, setIsOpenStudents] = useState(false);
+ const [isOpenCohorts, setIsOpenCohorts] = useState(false)
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+
+ const [selectedStudent, setSelectedStudent] = useState(null)
+ const [selectedCourse, setSelectedCourse] = useState(null)
+ const [selectedCohort, setSelectedCohort] = useState(null)
+ const navigate = useNavigate()
+
+
+ useEffect(() => {
+ async function fetchStudents() {
+ try {
+ const response = await get("students");
+ setStudents(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ }
+
+ async function fetchCourses() {
+ try {
+ const response = await get("courses");
+ setCourses(response.data.courses);
+ } catch (error) {
+ console.error("Error fetching courses:", error);
+ }
+ }
+ fetchStudents();
+ fetchCourses()
+ }, []);
+
+
+
+
+ const handleSelectStudent = (student) => {
+ setIsOpenStudents(false);
+ setSelectedStudent(student)
+ };
+
+
+ const handleSelectCourse = (course) => {
+ setIsOpenCourses(false)
+ setSelectedCourse(course)
+ setCohorts(course.cohorts)
+ }
+
+ const handleSelectCohort = (cohort) => {
+ setIsOpenCohorts(false)
+ setSelectedCohort(cohort)
+ }
+
+ const handleAdd = () => {
+ async function addStudentToCohort() {
+ try {
+ const response = await patch(`cohorts/teacher/${selectedCohort.id}`, {profileId: parseInt(selectedStudent.id)});
+ console.log(response)
+ } catch (error) {
+ console.error("Error adding student to cohort:", error);
+ }
+ } addStudentToCohort()
+ setSnackbarOpen(true);
+
+ setTimeout(()=> {
+ navigate(-1)
+ }, 3000)
+ }
+ return (
+ <>
+
+
+
Add student to cohort
+
+ navigate(-1)} >
+
+
+
+
+
Add a student to an existing cohort
+
+
+
+
+
+
+
Or
+
+
+
setIsOpenStudents(true)}>
+ {selectedStudent !== null ? ({selectedStudent.firstName} {selectedStudent.lastName}) : (
+ Student*
+)}
+
+
+
+
+ {isOpenStudents && (
)}
+
+
Add to
+
+
+
setIsOpenCourses(true)}>
+ {selectedCourse !== null ? ({selectedCourse.name}
+ ):( Select a course)}
+
+
+
+
+
+ {isOpenCourses && (
)}
+
+
+
+
setIsOpenCohorts(true)}>
+ {selectedCohort !== null ? (Cohort {selectedCohort.id}
+ ):( Select a cohort)}
+
+
+
+
+
+ {isOpenCohorts && ()}
+
+
+
+
+
+
+
+ navigate(-1)}> Cancel
+ Add to cohort
+
+
+
+
+ Student added to cohort
+
+ }
+ />
+
+
+
+
+
+
+
+
Or
+
navigate("/cohorts/newStudent")}>Add new student
+
+
+
+ >
+ );
+};
+
+export default AddStudent;
diff --git a/src/pages/addStudent/searchBar/index.js b/src/pages/addStudent/searchBar/index.js
new file mode 100644
index 00000000..33666834
--- /dev/null
+++ b/src/pages/addStudent/searchBar/index.js
@@ -0,0 +1,97 @@
+import { useEffect, useRef, useState } from "react";
+import { get } from "../../../service/apiClient";
+import TextInput from "../../../components/form/textInput";
+import SearchIcon from "../../../assets/icons/searchIcon";
+import SearchResultsStudents from "../searchResults";
+
+const SearchBar = ({setSelectedStudent, selectedStudent}) => {
+ const [query, setQuery] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ const popupRef = useRef();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ console.log(response);
+ setSearchResults(response.data.profiles);
+ setIsOpen(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ };
+
+ const handleSelectStudent = (student) => {
+ console.log("Selected student:", student);
+ setSelectedStudent(student);
+ setQuery(` ${student.firstName} ${student.lastName}`);
+ setIsOpen(false);
+ };
+
+ useEffect(() => {
+ if (selectedStudent) {
+ setQuery(`${selectedStudent.firstName} ${selectedStudent.lastName}`);
+ }
+ }, [selectedStudent]);
+
+
+ return (
+ <>
+
+
+
+ {isOpen && (
+
+ {searchResults.length > 0 ? (
+
+ ) : (
+
No students with this name found
+ )}
+
+ )}
+
+ >
+ )
+}
+
+export default SearchBar
+
+/**
+ * placeholder="Search for people"
+ value={query}
+ name="Search"
+ onChange={(e) => setQuery(e.target.value)}
+ icon={
}
+ iconRight={true}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleSubmit(e);
+ }}
+ */
\ No newline at end of file
diff --git a/src/pages/addStudent/searchResults/index.js b/src/pages/addStudent/searchResults/index.js
new file mode 100644
index 00000000..ea67125c
--- /dev/null
+++ b/src/pages/addStudent/searchResults/index.js
@@ -0,0 +1,53 @@
+
+
+
+const SearchResultsStudents = ({ students, onSelect }) => {
+
+
+ const styleGuideColors = [
+ "#28C846",
+ "#A0E6AA",
+ "#46DCD2",
+ "#82E6E6",
+ "#5ABEDC",
+ "#46C8FA",
+ "#46A0FA",
+ "#666EDC"
+ ];
+
+ const getColorFromInitials = (initials) => {
+ let hash = 0;
+ for (let i = 0; i < initials.length; i++) {
+ hash = initials.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const index = Math.abs(hash) % styleGuideColors.length;
+ return styleGuideColors[index];
+ };
+
+
+ return (
+
+ {students.map((student) => (
+ - onSelect(student)}
+ >
+
+
+
+
{student.firstName.charAt(0) + student.lastName.charAt(0)}
+
+
+
+
{student.firstName} {student.lastName}
+
+
+
+ ))}
+
+ );
+};
+
+export default SearchResultsStudents;
diff --git a/src/pages/addStudent/studentsMenu/index.js b/src/pages/addStudent/studentsMenu/index.js
new file mode 100644
index 00000000..0ad797fb
--- /dev/null
+++ b/src/pages/addStudent/studentsMenu/index.js
@@ -0,0 +1,37 @@
+import SearchResultsStudents from "../searchResults"
+
+const StudentsMenu = ({students, handleSelectStudent}) => {
+ return (
+ <>
+
+ {students.length > 0 ? (
+
+ ) : (
+
+ )}
+
>
+ )
+
+
+}
+
+export default StudentsMenu
\ No newline at end of file
diff --git a/src/pages/addStudent/style.css b/src/pages/addStudent/style.css
new file mode 100644
index 00000000..d2c362dd
--- /dev/null
+++ b/src/pages/addStudent/style.css
@@ -0,0 +1,218 @@
+.add-student-card {
+ width: 700px !important;
+ height: 1108px !important;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
+ padding: 24px;
+ box-sizing: border-box;
+ margin-left: 50px;
+ margin-top: 50px;
+}
+
+.add-cohort-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.add-title {
+ font-size: 40px;
+ color: #000046;
+ margin: 0;
+}
+
+.add-under-title {
+ font-size: 18px;
+ color: #64648C;
+ margin-top: 8px;
+}
+
+.exit-button {
+ width: 48px;
+ height: 48px;
+ background-color: #F0F5FA;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: #64648C;
+
+}
+
+.exit-button svg {
+ width: 24px;
+ height: 24px;
+ fill: currentColor;
+}
+
+.line {
+ border-bottom: 1px solid var(--color-blue5);
+ margin-top: 10px
+
+}
+
+.add-search {
+ margin-top: 25px;
+ font-family: 'Lexend', sans-serif;
+
+}
+
+.dropdown-section {
+ width: 100%;
+}
+
+.inputwrapper {
+ position: relative;
+ width: 100%;
+ margin-top: 16px;
+ font-family: 'Lexend', sans-serif;
+}
+
+.dropbtn {
+ width: 100%;
+ padding: 14px 16px;
+ font-size: 16px;
+ font-weight: 500;
+ color: #000046;
+ background-color: #F0F5FA;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ cursor: pointer;
+ text-align: left;
+ transition: background-color 0.2s ease;
+}
+
+.dropbtn:hover {
+ background-color: #e0e6f0;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ max-height: 300px;
+ overflow-y: auto;
+ z-index: 10;
+ margin-top: -54px;
+ font-family: inherit;
+}
+
+.dropdown-menu li {
+ padding: 12px 16px;
+ font-size: 16px;
+ color: #64648C;
+ cursor: pointer;
+ transition: background-color 0.2s ease;;
+}
+
+.dropdown-menu li:hover {
+ background-color: #F0F5FA;
+}
+
+.dropdown-menu li.selected {
+ background-color: #E6EBF5;
+ font-weight: bold;
+}
+
+.add-student-loading {
+ font-size: 20px;
+}
+
+.add-student-students-button button,
+.select-course-button button,
+.select-cohort-button button {
+ width: 100%;
+ margin: 0;
+ height: 56px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ opacity: 1;
+ background-color: #E6EBF5;
+ border: 1px solid #E6EBF5;
+ font-size: 16px;
+ color: #000046;
+ flex: 1;
+ box-sizing: border-box;
+}
+
+
+.add-student-button-title,
+.select-course-title,
+.select-cohort-title {
+ font-size: 18px;
+ color: #64648C;
+}
+
+.dropdown-section {
+ margin-top: 70px;
+ display: flex;
+ flex-direction: column;
+ gap: 60px; /* gir jevn avstand mellom alle barn */
+}
+
+.the-label {
+ color: #64648C;
+ font-size: 16px;
+ margin-left: 15px;
+}
+
+.paragraph {
+ color: #64648C;
+ font-size: 16px;
+}
+
+.required-label {
+ color: #96A0BE;
+ font-size: 16px;
+
+}
+
+.buttons-at-bottom{
+ margin-top: 50px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 30px;
+ margin-bottom: 50px;
+}
+
+.bottom{
+ display: grid;
+ grid-template-columns: auto;
+
+}
+
+
+button.offwhite-button,
+.button.offwhite-button {
+ background-color: var(--color-offwhite);
+ color: var(--color-blue1);
+ width: 35% !important;
+ margin-left:60px;
+}
+button.offwhite-button:hover,
+.button.offwhite-button:hover,
+button.offwhite-button:focus,
+.button.offwhite-button:focus {
+ background-color: var(--color-blue);
+ color: white;
+}
+
+.no-course {
+ margin-bottom: 100px;
+}
+
+.select-course-title-selected{
+ font-size: 18px;
+ font-family: 'Lexend', sans-serif;
+ font-weight: 400;
+}
\ No newline at end of file
diff --git a/src/pages/cohort/exercises/exercises.css b/src/pages/cohort/exercises/exercises.css
new file mode 100644
index 00000000..e8764745
--- /dev/null
+++ b/src/pages/cohort/exercises/exercises.css
@@ -0,0 +1,27 @@
+.value {
+ color: var(--color-blue1);
+ margin-bottom: 15px;
+}
+
+.label {
+ color: var(--color-blue1);
+ margin-bottom: 15px;
+}
+
+.see-more-button {
+ background-color: var(--color-blue5);
+}
+
+.exercise-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.label {
+ font-weight: 500;
+}
+
+.value {
+ color: var(--color-blue1);
+}
diff --git a/src/pages/cohort/exercises/index.js b/src/pages/cohort/exercises/index.js
new file mode 100644
index 00000000..e1b6a573
--- /dev/null
+++ b/src/pages/cohort/exercises/index.js
@@ -0,0 +1,32 @@
+import Card from "../../../components/card";
+import './exercises.css'
+
+const Exercises = () => {
+ return (
+ <>
+
+ My Exercises
+
+
+
+ Modules:
+ 2/7 completed
+
+
+
+ Units:
+ 4/10 completed
+
+
+
+ Exercise:
+ 34/58 completed
+
+
+ See exercises
+
+ >
+ )
+}
+
+export default Exercises;
\ No newline at end of file
diff --git a/src/pages/cohort/index.js b/src/pages/cohort/index.js
new file mode 100644
index 00000000..947ed7b3
--- /dev/null
+++ b/src/pages/cohort/index.js
@@ -0,0 +1,159 @@
+import Students from "./students";
+
+import Teachers from './teachers';
+import Exercises from "./exercises";
+import { useUserRoleData } from "../../context/userRole.";
+import TeacherCohort from "./teacherCohort";
+import jwtDecode from "jwt-decode";
+import useAuth from "../../hooks/useAuth";
+import { get, getUserById } from "../../service/apiClient";
+import { useEffect, useState } from "react";
+
+
+
+const Cohort = () => {
+ const {userRole, setUserRole} = useUserRoleData()
+ const { token } = useAuth();
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ setUserRole(decodedToken.roleId);
+ } catch (error) {
+ console.error('Invalid token in Cohort component:', error);
+ }
+
+ const [studentsLoading, setStudentsLoading] = useState(true);
+ const [teachersLoading, setTeachersLoading] = useState(true);
+ const [cohortsLoading, setCohortsLoading] = useState(true);
+
+ const [teachers, setTeachers] = useState([]);
+
+ const [students, setStudents] = useState([]);
+ const [course, setcourse] = useState([]);
+ const [cohort, setCohort] = useState("");
+ const [cohorts, setCohorts] = useState([])
+ const [refresh, setRefresh] = useState(false)
+
+ useEffect(() => {
+ setCohortsLoading(true)
+ async function fetchCohorts() {
+ try {
+ const response = await get("cohorts");
+ setCohorts(response.data.cohorts);
+ } catch (error) {
+ console.error("Error fetching cohorts:", error);
+ } finally {
+ setCohortsLoading(false)
+ }
+ }
+
+ fetchCohorts();
+ }, [refresh]);
+
+
+ useEffect(() => {
+ setTeachersLoading(true);
+ setStudentsLoading(true);
+ async function fetchData() {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ console.error('No token found');
+ return;
+ }
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token);
+ userId = decodedToken.userId;
+ } catch (decodeError) {
+ console.error('Invalid token:', decodeError);
+ return;
+ }
+
+ const user = await getUserById(userId);
+ if (user.profile.cohort === null) {
+ return;
+ }
+ const data = await get(`cohorts/${user.profile.cohort.id}`);
+
+ // set cohort
+ const cohort = data.data.cohort;
+ setCohort(cohort);
+
+ // set teachers
+ const teachers = data.data.cohort.profiles.filter((userid) => userid?.role?.name === "ROLE_TEACHER");
+ setTeachers(teachers || []);
+
+ console.log(teachers, "teachers in cohort");
+
+ // students
+ const students = data.data.cohort.profiles.filter((profileid) => profileid?.role?.name === "ROLE_STUDENT");
+ setStudents(students || []);
+ console.log(students, "students in cohort");
+
+ // course
+ const course = data.data.cohort.course;
+ setcourse(course || "");
+
+ } catch (error) {
+ console.error('fetchData() in cohort/teachers/index.js:', error);
+ } finally {
+ setStudentsLoading(false);
+ setTeachersLoading(false);
+ }
+ }
+
+ fetchData();
+ }, []);
+
+ function getInitials(profile) {
+ if (!profile.firstName || !profile.lastName) return "NA";
+ const firstNameParts = profile.firstName.trim().split(/\s+/) || ''; // split by any number of spaces
+ const lastNameInitial = profile.lastName.trim().charAt(0);
+
+ const firstNameInitials = firstNameParts.map(name => name.charAt(0));
+
+ return (firstNameInitials.join('') + lastNameInitial).toUpperCase();
+ }
+
+ if (studentsLoading || teachersLoading || cohortsLoading) {
+ return (
+
+ )
+ }
+
+ return (
+ <>
+ {userRole === 2 ? (
+ <>
+
+
+
+
+
+ >):(
+
+ )
+ }
+
+ >
+ )
+
+}
+
+export default Cohort;
+
+
diff --git a/src/pages/cohort/newStudent/index.js b/src/pages/cohort/newStudent/index.js
new file mode 100644
index 00000000..9ebb6691
--- /dev/null
+++ b/src/pages/cohort/newStudent/index.js
@@ -0,0 +1,124 @@
+import { useState } from 'react';
+import NewStudentStepOne from './newStudentStepOne';
+import NewStudentStepTwo from './newStudentStepTwo';
+import NewStudentStepFour from './newStudentStepFour';
+import './style.css';
+import NewStudentStepThree from './newStudentStepThree';
+import Stepper from '../../../components/stepper';
+import useAuth from '../../../hooks/useAuth';
+import { useFormData } from '../../../context/form';
+import { validateEmail, validatePassword } from '../../register';
+
+const NewStudent = () => {
+ const { onRegister, onCreateNewStudent } = useAuth();
+ const { formData } = useFormData();
+
+ const [profile, setProfile] = useState({
+ first_name: '',
+ last_name: '',
+ username: '',
+ github_username: '',
+ email: '',
+ mobile: '',
+ password: '',
+ bio: '',
+ role: 'ROLE_STUDENT',
+ specialism: '',
+ cohort: '',
+ start_date: '',
+ end_date: '',
+ photo: ''
+ });
+
+ const onChange = (event) => {
+ const { name, value } = event.target;
+
+ setProfile({
+ ...profile,
+ [name]: value
+ });
+
+ };
+
+ const onComplete = async () => {
+ const ok = await validateEP(profile.email, profile.password);
+ if (!ok) return;
+
+ onCreateNewStudent(
+ profile.first_name,
+ profile.last_name,
+ profile.username,
+ profile.github_username,
+ profile.email,
+ profile.mobile,
+ profile.password,
+ profile.bio,
+ profile.role,
+ profile.specialism,
+ profile.cohort,
+ profile.start_date,
+ profile.end_date,
+ profile.photo
+ );
+ };
+
+
+ const handleFileChange = (event, close) => {
+
+ const file = event.target.files[0];
+ if (file) {
+ const url = URL.createObjectURL(file)
+ setProfile(prevProfile => ({
+ ...prevProfile,
+ photo: url
+ }));
+ close()
+ }
+ }
+
+ const validateE = (email) => {
+ if (!validateEmail(email)) {
+ return false;
+ }
+
+ }
+ const validateP = (password) => {
+ if (!validatePassword(password)) {
+ return false;
+ }
+ }
+
+ const validateEP = async (email, password) => {
+
+ try {
+ await onRegister(email, password);
+ return true;
+ } catch (err) {
+ if (err.status === 400) {
+ alert("Email is already in use");
+ }
+ }
+};
+
+ return (
+
+ } onComplete={onComplete}>
+
+
+
+
+
+
+ );
+};
+
+const WelcomeHeader = () => {
+ return (
+
+
Add new student
+
Create a new student profile
+
+ );
+};
+
+export default NewStudent;
diff --git a/src/pages/cohort/newStudent/newStudentStepFour/index.js b/src/pages/cohort/newStudent/newStudentStepFour/index.js
new file mode 100644
index 00000000..3d48d39c
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepFour/index.js
@@ -0,0 +1,28 @@
+import Form from '../../../../components/form';
+
+const NewStudentStepFour = ({ data, setData }) => {
+ return (
+
+ <>
+
+
Bio
+
+
+ >
+ );
+};
+
+export default NewStudentStepFour
diff --git a/src/pages/cohort/newStudent/newStudentStepOne/index.js b/src/pages/cohort/newStudent/newStudentStepOne/index.js
new file mode 100644
index 00000000..831a08f8
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepOne/index.js
@@ -0,0 +1,105 @@
+import Popup from 'reactjs-popup';
+import ProfileIcon from '../../../../assets/icons/profileIcon';
+
+import Form from '../../../../components/form';
+import TextInput from '../../../../components/form/textInput';
+import Card from '../../../../components/card';
+
+
+const StepOne = ({ data, setData, handleFileChange }) => {
+
+
+ return (
+ <>
+
+
Basic info
+
+
+ >
+ );
+};
+
+export default StepOne;
diff --git a/src/pages/cohort/newStudent/newStudentStepThree/index.js b/src/pages/cohort/newStudent/newStudentStepThree/index.js
new file mode 100644
index 00000000..0d0127a1
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepThree/index.js
@@ -0,0 +1,120 @@
+import { useEffect, useState } from 'react';
+import Form from '../../../../components/form';
+import TextInput from '../../../../components/form/textInput';
+import { get } from '../../../../service/apiClient';
+import ArrowDownIcon from '../../../../assets/icons/arrowDownIcon';
+import CoursesMenu from '../../../addStudent/coursesMenu';
+import CohortsMenu from '../../../addStudent/cohortsMenu';
+import LockIcon from '../../../../assets/icons/lockIcon';
+
+const NewStudentStepThree = ({ data, setData, setProfile }) => {
+
+ const [ courses, setCourses ] = useState([])
+ const [ cohorts, setCohorts ] = useState([])
+
+ const [ isOpenCourses, setIsOpenCourses ] = useState(false);
+ const [ isOpenCohorts, setIsOpenCohorts ] = useState(false);
+
+ const [ selectedCourse, setSelectedCourse ] = useState(null)
+ const [ selectedCohort, setSelectedCohort ] = useState(null)
+
+ useEffect(() => {
+
+ async function fetchCourses() {
+ try {
+ const response = await get("courses");
+ console.log(response)
+ setCourses(response.data.courses);
+ } catch (error) {
+ console.error("Error fetching courses:", error);
+ }
+ }
+ fetchCourses()
+ }, []);
+
+
+ const handleSelectCourse = (course) => {
+ setSelectedCourse(course)
+ setCohorts(course.cohorts)
+
+ setProfile(prev => ({
+ ...prev,
+ specialism: course?.name,
+ start_date: course?.startDate,
+ end_date: course?.endDate,
+ }));
+ }
+
+ const handleSelectCohort = (cohort) => {
+ setIsOpenCohorts(false)
+ setSelectedCohort(cohort)
+
+ setProfile(prev => ({
+ ...prev,
+ cohort: cohort?.id
+ }));
+ }
+
+ return (
+ <>
+
+
Training info
+
+
+ >
+ )
+}
+
+export default NewStudentStepThree;
\ No newline at end of file
diff --git a/src/pages/cohort/newStudent/newStudentStepTwo/index.js b/src/pages/cohort/newStudent/newStudentStepTwo/index.js
new file mode 100644
index 00000000..f29dbe9c
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepTwo/index.js
@@ -0,0 +1,48 @@
+import Form from '../../../../components/form';
+import NumberInput from '../../../../components/form/numberInput';
+import TextInput from '../../../../components/form/textInput';
+
+const NewStudentStepTwo = ({ data, setData, validateEmail, validatePassword }) => {
+
+ return (
+ <>
+
+
Basic info
+
+
+ >
+ );
+};
+
+export default NewStudentStepTwo;
\ No newline at end of file
diff --git a/src/pages/cohort/newStudent/style.css b/src/pages/cohort/newStudent/style.css
new file mode 100644
index 00000000..d40eaa65
--- /dev/null
+++ b/src/pages/cohort/newStudent/style.css
@@ -0,0 +1,244 @@
+.welcome-titleblock {
+ margin-bottom: 32px;
+}
+.welcome-titleblock h1 {
+ margin-bottom: 16px;
+}
+.welcome-cardheader h2 {
+ margin-bottom: 16px;
+}
+.welcome-cardheader p {
+ margin-bottom: 24px;
+}
+.welcome-formheader h3 {
+ margin-bottom: 32px;
+}
+.welcome-form-profileimg-input {
+ display: grid;
+ grid-template-columns: 40px auto;
+ gap: 16px;
+ align-items: center;
+}
+.welcome-form-profileimg-error {
+ color: transparent;
+}
+.welcome-form-inputs {
+ display: grid;
+ grid-template-rows: repeat(auto, auto);
+ gap: 5px;
+ margin-bottom: 48px;
+}
+
+.welcome-form-popup-wrapper {
+ width: 470px;
+ max-width: 100%;
+}
+.welcome-form-popup {
+ display: grid;
+ grid-template-rows: repeat(auto, auto);
+ gap: 24px;
+}
+.welcome-form-popup-buttons {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+}
+
+.welcome-counter {
+ margin-left: 10px;
+ font-size: 12px;
+ color:grey
+}
+
+.bio-label {
+ font-size: 10px;
+}
+
+.bio-heading {
+ font-size: 25px;
+}
+
+.addHeadshot {
+ height: 55px;
+ color:#64648c;
+ display: flex;
+}
+
+
+.upload-label {
+ background-color: var(--color-blue);
+ color: white;
+ padding: 14px 24px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-align: center;
+ font-size: 20px;
+}
+
+
+.line {
+ border-bottom: 1px solid var(--color-blue5);
+ margin-top: 10px
+
+}
+
+.dropdown-section {
+ width: 100%;
+}
+
+.inputwrapper {
+ position: relative;
+ width: 100%;
+ margin-top: 16px;
+}
+
+.dropbtn {
+ width: 100%;
+ padding: 14px 16px;
+ font-size: 16px;
+ font-weight: 500;
+ color: #000046;
+ background-color: #F0F5FA;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ cursor: pointer;
+ text-align: left;
+ transition: background-color 0.2s ease;
+}
+
+.dropbtn:hover {
+ background-color: #e0e6f0;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ max-height: 300px;
+ overflow-y: auto;
+ z-index: 10;
+ margin-top: -54px;
+}
+
+.dropdown-menu li {
+ padding: 12px 16px;
+ font-size: 16px;
+ color: #64648C;
+ cursor: pointer;
+ transition: background-color 0.2s ease;;
+}
+
+.dropdown-menu li:hover {
+ background-color: #F0F5FA;
+}
+
+
+.dropdown-menu li.selected {
+ background-color: #E6EBF5;
+ font-weight: bold;
+}
+
+.password-wrapper {
+ font-family:
+ 'Lexend' !important;
+}
+
+.select-course-button button,
+.select-cohort-button button {
+ width: 100% !important;
+ margin: 0;
+ height: 56px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ opacity: 1;
+ background-color: #E6EBF5;
+ border: 1px solid #E6EBF5;
+ font-size: 16px;
+ color: #000046;
+ flex: 1;
+ box-sizing: border-box;
+}
+
+
+.select-course-title,
+.select-cohort-title {
+ font-size: 16px;
+ color: #64648C;
+ font-family:
+ 'Lexend';
+
+}
+
+.select-course-title-selected-selected,
+.select-cohort-title-selected-selected {
+ font-size: 16px;
+ color: #000046;
+ font-family:
+ 'Lexend';
+
+}
+
+.dropdown-section {
+ margin-top: 70px;
+ display: flex;
+ flex-direction: column;
+ gap: 60px;
+}
+
+.the-label {
+ color: #64648C;
+ font-size: 16px;
+ width: 100% !important;
+}
+
+.paragraph {
+ color: #64648C;
+ font-size: 16px;
+}
+
+.required-label {
+ color: #96A0BE;
+ font-size: 16px;
+
+}
+
+.buttons-at-bottom{
+ margin-top: 50px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 30px;
+ margin-bottom: 50px;
+}
+
+.bottom{
+ display: grid;
+ grid-template-columns: auto;
+
+}
+
+
+button.offwhite-button,
+.button.offwhite-button {
+ background-color: var(--color-offwhite);
+ color: var(--color-blue1);
+ width: 35% ;
+}
+button.offwhite-button:hover,
+.button.offwhite-button:hover,
+button.offwhite-button:focus,
+.button.offwhite-button:focus {
+ background-color: var(--color-blue);
+ color: white;
+}
+
+
+.no-course {
+ margin-bottom: 100px;
+}
\ No newline at end of file
diff --git a/src/pages/cohort/students/index.js b/src/pages/cohort/students/index.js
new file mode 100644
index 00000000..b56805b9
--- /dev/null
+++ b/src/pages/cohort/students/index.js
@@ -0,0 +1,66 @@
+import Card from "../../../components/card";
+import Student from "./student";
+import './students.css';
+import SoftwareLogo from "../../../assets/icons/software-logo";
+import FrontEndLogo from "../../../assets/icons/frontEndLogo";
+import DataAnalyticsLogo from "../../../assets/icons/dataAnalyticsLogo";
+import '../../../components/profileCircle/style.css';
+import '../../../components/fullscreenCard/fullscreenCard.css';
+// import { useState } from "react";
+
+
+function Students({ students, getInitials, course, cohort }) {
+
+ return (
+
+
+
+
+ {course && (
+
+
+ {course.name === "Software Development" && }
+ {course.name === "Front-End Development" && }
+ {course.name === "Data Analytics" && }
+
+
+
+
{course.name}, Cohort {cohort.id}
+
+
+
+
{`${cohort.course.startDate} - ${cohort.course.endDate}`}
+
+
+ )}
+
+
+ {students.map((student) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default Students;
\ No newline at end of file
diff --git a/src/pages/cohort/students/student/index.js b/src/pages/cohort/students/student/index.js
new file mode 100644
index 00000000..e887f76e
--- /dev/null
+++ b/src/pages/cohort/students/student/index.js
@@ -0,0 +1,20 @@
+import UserIcon from "../../../../components/profile-icon";
+
+const Student = ({ id, initials, firstName, lastName, role, photo=null }) => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default Student;
diff --git a/src/pages/cohort/students/students.css b/src/pages/cohort/students/students.css
new file mode 100644
index 00000000..4a8a80ab
--- /dev/null
+++ b/src/pages/cohort/students/students.css
@@ -0,0 +1,115 @@
+.cohort {
+ display: grid;
+ row-gap: 20px;
+}
+
+
+/* FOR THE COURSE AND DATE SECTON */
+.cohort-course-date-wrapper {
+ display: grid;
+ grid-template-columns: 56px 1fr 144px;
+ align-items: center;
+ column-gap: 20px;
+ padding: 15px;
+ background: #fff;
+}
+
+.cohort-course-date {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+
+.cohort-title {
+ grid-column: 2;
+ grid-row: 1;
+}
+
+.cohort-title p {
+ font-weight: 600;
+ font-size: 1.1rem;
+}
+
+.cohort-dates {
+ grid-column: 2;
+ grid-row: 2;
+}
+
+/* FOR THE EDIT ICON!! DONT KNOW WHY BUT WE NEED IT */
+.edit-icon {
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ background: #f0f5fa;
+}
+
+.edit-icon p {
+ text-align: center;
+ font-size: 20px;
+}
+
+.edit-icon:hover {
+ background: #e1e8ef;
+ cursor: pointer;
+}
+
+/* FOR THE STUDENTS COLUMNS */
+.cohort-students-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+}
+
+.student-details {
+ display: grid;
+ grid-template-columns: 56px 1fr 48px;
+ align-items: center;
+ column-gap: 20px;
+ padding: 15px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.border-top {
+ border-top: 1px solid #e6ebf5;
+ padding-top: 20px;
+ padding-bottom: 10px;
+}
+
+/* FOR THE COURSE ICONS */
+.software-icon {
+ background: #28C846;
+}
+
+.front-icon {
+ background: #6E6EDC;
+}
+
+.data-icon {
+ background: #46A0FA;
+}
+
+.course-icon svg {
+ width: 24px;
+ height: 24px;
+}
+
+/* FOR THE COURSE NAV BUTTONS */
+.course-nav-buttons {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.course-nav-buttons button {
+ padding: 0.5rem 0.75rem;
+ font-size: 1rem;
+ cursor: pointer;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.course-nav-buttons button:hover {
+ background-color: #e1e8ef;
+}
\ No newline at end of file
diff --git a/src/pages/cohort/teacherCohort/cohortsList/index.js b/src/pages/cohort/teacherCohort/cohortsList/index.js
new file mode 100644
index 00000000..ff035cfd
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/cohortsList/index.js
@@ -0,0 +1,61 @@
+
+import SoftwareLogo from "../../../../assets/icons/software-logo";
+import FrontEndLogo from "../../../../assets/icons/frontEndLogo";
+import DataAnalyticsLogo from "../../../../assets/icons/dataAnalyticsLogo";
+import './style.css';
+import { useState } from "react";
+import { useSelectedCohortId } from "../../../../context/selectedCohort";
+
+
+const CohortsList= ({ onSelect, setSelectedCohort , cohorts}) => {
+ const [selectedCohortId, setSelectedCohortId] = useState(null);
+ const {setCohortId} = useSelectedCohortId();
+
+ const handleClick = (cohort) => {
+ setSelectedCohortId(cohort.id);
+ setSelectedCohort(cohort)
+ setCohortId(cohort.id);
+ if (onSelect) {
+ onSelect(cohort.profiles);
+ }
+ };
+
+
+ return (
+
+ );
+};
+
+export default CohortsList;
+
+
+
diff --git a/src/pages/cohort/teacherCohort/cohortsList/style.css b/src/pages/cohort/teacherCohort/cohortsList/style.css
new file mode 100644
index 00000000..8c2d27c4
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/cohortsList/style.css
@@ -0,0 +1,71 @@
+
+
+
+.course-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Farger per kurs */
+.software-icon {
+ background: #28C846;
+}
+
+.front-icon {
+ background: #6E6EDC;
+}
+
+.data-icon {
+ background: #46A0FA;
+}
+
+.course-icon svg {
+ width: 30px;
+ height: 30px;
+}
+
+.cohort-name-course {
+ font-size: 16px;
+ color: #64648C;
+ margin-top: 4px;
+ margin-right: 10px;
+}
+
+.cohort-info {
+ margin-top: 10px;
+}
+
+.course-name {
+ font-size: 20px;
+ font-weight: bold;
+
+}
+
+
+.cohort-course-row {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: background-color 0.2s ease;
+ width: 380px;
+ box-sizing: border-box;
+ gap: 12px;
+ margin-bottom: 8px;
+ font-size: 20px
+
+}
+
+.cohort-course-row:hover {
+ background-color: #f0f5fa; /* lys blΓ₯grΓ₯ ved hover */
+}
+
+.cohort-course-row.selected {
+ background-color: #E6EBF5;
+
+}
diff --git a/src/pages/cohort/teacherCohort/index.js b/src/pages/cohort/teacherCohort/index.js
new file mode 100644
index 00000000..8d722601
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/index.js
@@ -0,0 +1,103 @@
+import { useEffect, useState } from "react"
+// import SearchIcon from "../../../assets/icons/searchIcon"
+import EditIconCohortTeacher from "../../../components/editIconCohortTeacher"
+// import TextInput from "../../../components/form/textInput"
+import CohortsList from "./cohortsList"
+import './style.css';
+import StudentList from "./studentList"
+import EditIconCouse from "../../../components/editIconCourse"
+import CourseIcon from "../../../components/courseIcon"
+import { useNavigate } from "react-router-dom"
+import SearchTeacher from "./searchTeacher";
+
+
+const TeacherCohort = ({cohorts, setRefresh}) => {
+ // const [searchVal, setSearchVal] = useState('');
+ const [selectedProfiles, setSelectedProfiles] = useState([]);
+ const[selectedCohort, setSelectedCohort] = useState(null);
+ const navigate = useNavigate()
+
+ // const onChange = (e) => {
+ // setSearchVal(e.target.value);
+ // };
+
+ useEffect(() => {}, [selectedProfiles]);
+
+ return (
+ <>
+ {cohorts.length > 0 ? (
+
+
+
Cohorts
+ Students
+
+
+
+
+
+
+
+
+
+
+
+
+ navigate("/cohorts/new")}>Add cohort
+
+
+
+
+
+
+
+
+
+
+ setSelectedProfiles(profiles)} />
+
+
+
+
+
+
+ {selectedCohort !== null ? (
+ <>
+
+
+
+ >
+ ): (<>Select a course
>)}
+
+
+
+
+
+ navigate("/cohorts/add")}>Add student
+
+
+
+
+
+
+
+
+
+
+
+
+
):(
+
+ )}
+
+ >
+ )
+}
+
+export default TeacherCohort
diff --git a/src/pages/cohort/teacherCohort/searchTeacher/index.js b/src/pages/cohort/teacherCohort/searchTeacher/index.js
new file mode 100644
index 00000000..115a77e5
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/searchTeacher/index.js
@@ -0,0 +1,123 @@
+import { useEffect, useRef, useState } from "react";
+import { useSearchResults } from "../../../../context/searchResults";
+import { useNavigate } from "react-router-dom";
+import { get } from "../../../../service/apiClient";
+import TextInput from "../../../../components/form/textInput";
+import SearchIcon from "../../../../assets/icons/searchIcon";
+import ProfileIconTeacher from "../../../../components/profile-icon-teacherView";
+import Card from "../../../../components/card";
+
+const SearchTeacher = () => {
+ const [query, setQuery] = useState("");
+ const {searchResults, setSearchResults} = useSearchResults();
+ const [isOpen, setIsOpen] = useState(false);
+ const navigate = useNavigate();
+ const popupRef = useRef();
+
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ setSearchResults(response.data.profiles);
+ setIsOpen(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ }
+
+ useEffect(() => {
+ function handleClickOutside(e) {
+ if (popupRef.current && !popupRef.current.contains(e.target)) {
+ setIsOpen(false);
+ }
+ }
+
+ if (isOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("touchstart", handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("touchstart", handleClickOutside);
+ };
+ }, [isOpen]);
+
+
+ return (
+
+
+
+
+
+ {isOpen && (
+
+
+ People
+ {searchResults?.length > 0 ? (
+
+ {searchResults.slice(0, 10).map((student) => (
+ -
+
+
+ ))}
+
+ ) : (
+ Sorry, no results found
+ )}
+
+
+ {searchResults?.length > 10 && (
+
+ navigate("/search/profiles")}>All results
+
+ )}
+
+
+ )}
+
+ );
+
+}
+
+export default SearchTeacher;
diff --git a/src/pages/cohort/teacherCohort/studentList/index.js b/src/pages/cohort/teacherCohort/studentList/index.js
new file mode 100644
index 00000000..78ad0d08
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/studentList/index.js
@@ -0,0 +1,41 @@
+import { useEffect, useState } from "react";
+import ProfileIconTeacher from "../../../../components/profile-icon-teacherView";
+import { get } from "../../../../service/apiClient";
+
+const StudentList = ({ profiles, setSelectedProfiles }) => {
+ const [refresh, setRefresh] = useState(false);
+
+
+ useEffect(() => {
+ if (!profiles || profiles.length === 0) return;
+ async function fetchStudents() {
+ try {
+ const response = await get("profiles");
+ const studs = response.data.profiles;
+ const filteredStuds = studs.filter(stud => profiles.some(p => p.id === stud.id));
+ setSelectedProfiles(filteredStuds);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ } fetchStudents();
+ }, [refresh]);
+
+ if (!profiles || profiles.length === 0) {
+ return ;
+ }
+ return (
+
+ {profiles.map((student) => (
+ -
+
+
+ ))}
+
+ );
+};
+
+export default StudentList;
diff --git a/src/pages/cohort/teacherCohort/style.css b/src/pages/cohort/teacherCohort/style.css
new file mode 100644
index 00000000..1a48326b
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/style.css
@@ -0,0 +1,203 @@
+
+.cohort-card {
+ width: 88%;
+ height: 100%;
+ position: absolute;
+ top: 120px;
+ left: 175px;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ padding: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ display: flex;
+ flex-direction: column;
+
+
+}
+
+.cohort-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+ border-bottom: 1px solid var(--color-blue5);
+
+}
+
+.header-titles {
+ display: flex;
+ gap: 350px;
+}
+
+.header-titles h3 {
+ font-size: 32px;
+ color: #000046;
+
+}
+
+.search-bar {
+ margin-bottom: 10px;
+}
+
+
+.sections-wrapper {
+ display: flex;
+ flex-direction: row;
+ height: calc(100vh - 80px); /* justerer for header-hΓΈyden */
+ width: 100%;
+}
+
+.cohorts-section {
+ position: relative;
+ width: 500px;
+ padding: 24px;
+ border-right: 1px solid var(--color-blue5);
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ height: 100%; /* viktig for at linjen skal dekke hele hΓΈyden */
+}
+
+.cohort-list {
+ overflow-y: auto;
+ height: 100%;
+ width: 106%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ scrollbar-width: thin;
+}
+
+
+
+.student-list {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ width: 101%;
+ min-height: auto;
+ overflow-y: auto;
+ scrollbar-width: thin;
+
+}
+
+.students-section {
+ position: relative;
+ width: 100%;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.students {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+}
+
+
+
+.selected-course {
+ flex: 1; /* tar opp all tilgjengelig plass til venstre */
+}
+
+
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 16px; /* mellomrom mellom knapp og ikon */
+}
+
+
+.add-student-button button {
+ height: 56px;
+ width: 166px;
+ padding: 0 24px;
+ background-color: #F0F5FA;
+ border: none;
+ color: #64648C;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ box-sizing: border-box;
+ margin-right: 40px;
+}
+
+
+.edit-icon-course {
+ font-size: 24px;
+ color: #64648C;
+}
+
+
+.add-cohort {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px; /* gir luft mellom knapp og ikon */
+
+
+}
+
+.add-cohort-button {
+ width: auto;
+ flex-grow: 2;
+}
+
+
+.add-cohort-button button{
+ width: 199px;
+ height: 56px;
+ padding: 14px 24px;
+ gap: 8px; /* hvis du har ikon og tekst inni */
+ border-radius: 8px;
+ background-color: #F0F5FA;
+ border: none;
+ cursor: pointer;
+ font-size: 20px;
+ color: #64648C;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transform: rotate(0deg); /* angle: 0 deg */
+ position: relative; /* ikke absolute med top/left med mindre nΓΈdvendig */
+}
+
+
+.edit-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.divider {
+ border-bottom: 1px solid var(--color-blue5);
+
+}
+
+.cohort-teacher-loading {
+ margin-top: 20px;
+ margin-left: 20px;
+}
+
+.search-bar-in-cohorts {
+ margin-bottom:10px;
+ overflow: visible;
+}
+
+.profile-icon-cohorts {
+ position: absolute !important;
+ top: 100%;
+ left: 0;
+ width: "100%";
+}
\ No newline at end of file
diff --git a/src/pages/cohort/teachers/index.js b/src/pages/cohort/teachers/index.js
new file mode 100644
index 00000000..a8147025
--- /dev/null
+++ b/src/pages/cohort/teachers/index.js
@@ -0,0 +1,33 @@
+import Card from "../../../components/card";
+import './style.css';
+import Teacher from "./teacher";
+
+
+const Teachers = ({ teachers, getInitials }) => {
+ console.log(teachers, "teachers in teachers component");
+ return (
+
+
+
+
+
+ {teachers.map((teacher) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default Teachers;
diff --git a/src/pages/cohort/teachers/style.css b/src/pages/cohort/teachers/style.css
new file mode 100644
index 00000000..107be272
--- /dev/null
+++ b/src/pages/cohort/teachers/style.css
@@ -0,0 +1,35 @@
+.card {
+ background: white;
+ padding: 24px;
+ border-radius: 8px;
+ width: 50%;
+ margin-bottom: 25px;
+ border: 1px #e6ebf5 solid;
+}
+
+.cohort {
+ display: grid;
+ row-gap: 20px;
+}
+
+.cohort-teachers-container {
+ display: grid;
+ gap: 20px;
+}
+
+.teacher-details {
+ display: grid;
+ grid-template-columns: 56px 1fr 48px;
+ align-items: center;
+ column-gap: 20px;
+ padding: 15px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.border-top {
+ border-top: 1px solid var(--color-blue5);
+ padding-top: 20px;
+ padding-bottom: 10px;
+}
\ No newline at end of file
diff --git a/src/pages/cohort/teachers/teacher/index.js b/src/pages/cohort/teachers/teacher/index.js
new file mode 100644
index 00000000..17f06fd3
--- /dev/null
+++ b/src/pages/cohort/teachers/teacher/index.js
@@ -0,0 +1,21 @@
+import UserIcon from "../../../../components/profile-icon";
+
+const Teacher = ({ id, initials, firstName, lastName, role, photo=null }) => {
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default Teacher;
diff --git a/src/pages/dashboard/cohorts/index.js b/src/pages/dashboard/cohorts/index.js
new file mode 100644
index 00000000..40fd5aae
--- /dev/null
+++ b/src/pages/dashboard/cohorts/index.js
@@ -0,0 +1,58 @@
+
+import Card from "../../../components/card"
+import SoftwareLogo from "../../../assets/icons/software-logo"
+import FrontEndLogo from "../../../assets/icons/frontEndLogo"
+import DataAnalyticsLogo from "../../../assets/icons/dataAnalyticsLogo"
+import './style.css';
+
+const Cohorts = ({cohorts}) => {
+
+ return (
+ <>
+
+ Cohorts
+
+ {cohorts !== null ? (
+
+ {cohorts.map((cohort, index) => {
+ return (
+ -
+ {cohort.course === null ? <>> :
+
+
+
+ {cohort.course.name === "Software Development" && }
+ {cohort.course.name === "Front-End Development" && }
+ {cohort.course.name === "Data Analytics" && }
+
+
+
{cohort.course.name}
+
Cohort {cohort.id}
+
+
+ }
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+ }
+
+export default Cohorts
\ No newline at end of file
diff --git a/src/pages/dashboard/cohorts/style.css b/src/pages/dashboard/cohorts/style.css
new file mode 100644
index 00000000..f9c27bcc
--- /dev/null
+++ b/src/pages/dashboard/cohorts/style.css
@@ -0,0 +1,105 @@
+main {
+ padding: 30px;
+}
+
+aside {
+ padding: 30px 60px 30px 0;
+}
+
+.create-post-input {
+ display: grid;
+ grid-template-columns: 70px auto;
+}
+
+.create-post-input button {
+ color: var(--color-blue1);
+ font-size: 1rem !important;
+ padding-left: 15px !important;
+ text-align: left !important;
+ max-width: 100% !important;
+ background-color: var(--color-blue5);
+}
+
+
+
+.dashboard-cohort-item {
+ margin-top: 10px;
+
+}
+
+.dashboard-cohort-info {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.course-text {
+ margin-left: -35px
+}
+
+
+.course-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+/* Farger per kurs */
+.software-icon {
+ background: #28C846;
+}
+
+.front-icon {
+ background: #6E6EDC;
+}
+
+.data-icon {
+ background: #46A0FA;
+}
+
+.course-icon svg {
+ width: 30px;
+ height: 30px;
+
+}
+
+.dashboard-cohort-name {
+ margin-left: 50px;
+ font-size: 16px;
+ color: #64648C;
+ margin-top: 4px;
+
+}
+
+
+.dashboard-course-name {
+ font-size: 20px;
+ font-weight: bold;
+ margin-left: 50px;
+
+}
+
+.student-button {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 199px;
+ height: 56px;
+ padding: 14px 24px;
+ gap: 8px;
+ border-radius: 8px;
+ background: #F0F5FA;
+ color: #64648C;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+
+}
+
+.loading-cohorts {
+ font-size: 20px;
+}
\ No newline at end of file
diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js
index 54606849..8a12aeb7 100644
--- a/src/pages/dashboard/index.js
+++ b/src/pages/dashboard/index.js
@@ -1,19 +1,85 @@
-import { useState } from 'react';
-import SearchIcon from '../../assets/icons/searchIcon';
+
+
+import { useState, useEffect } from 'react';
+
+
+
import Button from '../../components/button';
import Card from '../../components/card';
import CreatePostModal from '../../components/createPostModal';
-import TextInput from '../../components/form/textInput';
import Posts from '../../components/posts';
import useModal from '../../hooks/useModal';
import './style.css';
+import Cohorts from './cohorts';
+import { useUserRoleData } from '../../context/userRole.';
+import Students from './students';
+import TeachersDashboard from './teachers';
+import useAuth from '../../hooks/useAuth';
+import jwtDecode from 'jwt-decode';
+import Search from './search';
+
+import { getUserById, get } from '../../service/apiClient';
+import UserIcon from '../../components/profile-icon';
+import SimpleProfileCircle from '../../components/simpleProfileCircle';
const Dashboard = () => {
- const [searchVal, setSearchVal] = useState('');
+ const { token } = useAuth();
+ const [students, setStudents] = useState([]);
+ const [cohort, setCohort] = useState([]);
+ const [course, setCourse] = useState([]);
+ const [cohorts, setCohorts] = useState(null)
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in Dashboard:', error);
+ }
+
+ // to view people My Cohort
+ useEffect(() => {
+ async function fetchCohortData() {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ console.error('No token found.');
+ return;
+ }
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token);
+ userId = decodedToken.userId;
+ } catch (decodeError) {
+ console.error('Invalid token:', decodeError);
+ return;
+ }
+
+ const user = await getUserById(userId);
+ if (user.profile.cohort === null) {
+ return;
+ }
+ const data = await get(`cohorts/${user.profile.cohort.id}`);
+ setCohort(data.data.cohort)
+ setCourse(data.data.cohort.course);
+ setStudents(data.data.cohort.profiles)
+
+ } catch (error) {
+ console.error('fetchCohortData() in dashboard/index.js:', error);
+ }
+ }
+ fetchCohortData();
+ }, []);
+
+ const fullName = `${decodedToken.firstName || decodedToken.first_name || 'Current'} ${decodedToken.lastName || decodedToken.last_name || 'User'}`;
+ const initials = fullName?.match(/\b(\w)/g)?.join('') || 'NO';
+ const { userRole, setUserRole } = useUserRoleData();
+
+
- const onChange = (e) => {
- setSearchVal(e.target.value);
- };
// Use the useModal hook to get the openModal and setModal functions
const { openModal, setModal } = useModal();
@@ -27,31 +93,135 @@ const Dashboard = () => {
openModal();
};
+ const [refresh, setRefresh] = useState(false);
+
+ useEffect(() => {
+ async function fetchAndSetUserRole() {
+ const storedToken = token || localStorage.getItem('token');
+ if (!storedToken) return;
+ try {
+ const decoded = jwtDecode(storedToken);
+ const user = await getUserById(decoded.userId);
+ // check the role from backend
+ const roleName = user.profile.role.name;
+ if (roleName === 'ROLE_TEACHER') setUserRole(1);
+ else if (roleName === 'ROLE_STUDENT') setUserRole(2);
+ else setUserRole(null);
+ } catch (error) {
+ console.error('Error fetching user role from backend:', error);
+ }
+ }
+ fetchAndSetUserRole();
+ }, [token, setUserRole]);
+
+ useEffect(() => {
+ async function fetchCohorts() {
+ try {
+ const response = await get("cohorts");
+ setCohorts(response.data.cohorts);
+ } catch (error) {
+ console.error("Error fetching cohorts:", error);
+ }
+ }
+ fetchCohorts();
+ }, []);
+
+ function getInitials(profile) {
+ if (!profile.firstName || !profile.lastName) return "NA";
+ const firstNameParts = profile.firstName.trim().split(/\s+/) || ''; // split by any number of spaces
+ const lastNameInitial = profile.lastName.trim().charAt(0);
+
+ const firstNameInitials = firstNameParts.map(name => name.charAt(0));
+
+ return (firstNameInitials.join('') + lastNameInitial).toUpperCase();
+ }
+
return (
<>
-
+ {/*
*/}
+
+
+{/*
*/}
+ {/*
{initials}
*/}
+ {/*
*/}
+
-
+
>
);
diff --git a/src/pages/dashboard/search/index.js b/src/pages/dashboard/search/index.js
new file mode 100644
index 00000000..bb61c5c7
--- /dev/null
+++ b/src/pages/dashboard/search/index.js
@@ -0,0 +1,111 @@
+import { useNavigate } from "react-router-dom"
+import { useState, useRef, useEffect } from "react"
+import Card from "../../../components/card"
+import TextInput from "../../../components/form/textInput"
+import SearchIcon from "../../../assets/icons/searchIcon"
+import { get } from "../../../service/apiClient"
+import UserIcon from "../../../components/profile-icon"
+import { useSearchResults } from "../../../context/searchResults"
+
+const Search = () => {
+ const [query, setQuery] = useState("");
+ const {searchResults, setSearchResults} = useSearchResults();
+ const [isOpen, setIsOpen] = useState(false);
+ const navigate = useNavigate();
+ const popupRef = useRef();
+
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ setSearchResults(response.data.profiles);
+ setIsOpen(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ }
+
+ useEffect(() => {
+ function handleClickOutside(e) {
+ if (popupRef.current && !popupRef.current.contains(e.target)) {
+ setIsOpen(false);
+ }
+ }
+
+ if (isOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("touchstart", handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("touchstart", handleClickOutside);
+ };
+ }, [isOpen]);
+
+
+ return (
+
+
+
+ {isOpen && (
+
+
+ People
+ {searchResults?.length > 0 ? (
+
+ {searchResults.slice(0, 10).map((student) => (
+ -
+
+
+ ))}
+
+ ) : (
+ Sorry, no results found
+ )}
+
+
+ {searchResults?.length > 10 && (
+
+ navigate("/search/profiles")}>All results
+
+ )}
+
+
+ )}
+
+ );
+
+}
+
+export default Search;
diff --git a/src/pages/dashboard/students/index.js b/src/pages/dashboard/students/index.js
new file mode 100644
index 00000000..31f417f8
--- /dev/null
+++ b/src/pages/dashboard/students/index.js
@@ -0,0 +1,70 @@
+import { useEffect, useState } from "react";
+import { get } from "../../../service/apiClient";
+
+import Card from "../../../components/card"
+
+// import UserIcon from "../../../components/profile-icon";
+
+import ProfileIconTeacher from "../../../components/profile-icon-teacherView";
+
+const Students = ({refresh, setRefresh }) => {
+ const [students, setStudents] = useState(null)
+
+
+ useEffect(() => {
+ async function fetchStudents() {
+ try {
+ const response = await get("students");
+ setStudents(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ }
+ fetchStudents();
+ }, [refresh]);
+
+ return(
+ <>
+
+ Students
+
+ {students !== null ? (
+
+
+ {students.map((student, index) => (
+ -
+
+
word[0].toUpperCase())
+ .join('')}
+ firstname={student.firstName}
+ lastname={student.lastName}
+ role={"Student"}
+ setRefresh={setRefresh}
+ />
+
+
+ ))}
+
+
+
+
+ ):(
+
+ )}
+
+
+ >
+ )
+}
+export default Students
\ No newline at end of file
diff --git a/src/pages/dashboard/style.css b/src/pages/dashboard/style.css
index f55ef0a7..8ee9a57a 100644
--- a/src/pages/dashboard/style.css
+++ b/src/pages/dashboard/style.css
@@ -8,7 +8,14 @@ aside {
.create-post-input {
display: grid;
- grid-template-columns: 70px auto;
+ grid-template-columns: 48px auto;
+ gap: 8px;
+}
+
+/* Override UserIcon padding in create post area */
+.create-post-input .user {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
}
.create-post-input button {
@@ -19,3 +26,133 @@ aside {
max-width: 100% !important;
background-color: var(--color-blue5);
}
+
+
+.dashboard-cohort-item {
+ margin-bottom: 20px;
+}
+
+.cohort-header {
+ display: flex;
+ align-items: center;
+}
+
+.course-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Farger per kurs */
+.software-icon {
+ background: #28C846;
+}
+
+.front-icon {
+ background: #6E6EDC;
+}
+
+.data-icon {
+ background: #46A0FA;
+}
+
+.course-icon svg {
+ width: 30px;
+ height: 30px;
+}
+
+
+.cohort-name {
+ font-size: 16px;
+ color: #64648C;
+ margin-top: 4px;
+}
+
+.course-text {
+ display: flex;
+ flex-direction: column;
+}
+
+
+.dashboard-course-name {
+ font-size: 20px;
+ font-weight: bold;
+ margin: 0;
+}
+
+.student-button {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 199px;
+ height: 56px;
+ padding: 14px 24px;
+ gap: 8px;
+ border-radius: 8px;
+ background: #F0F5FA;
+ color: #64648C;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+
+}
+
+.people {
+ font-size: 16px;
+ color: #64648C;
+ border-bottom: 1px solid var(--color-blue5);
+ padding: 10px 10px;
+
+}
+
+.cohort-teachers-container {
+ display: grid;
+ gap: 20px;
+}
+
+.border-top {
+ border-top: 1px solid var(--color-blue5);
+ padding-top: 20px;
+ padding-bottom: 10px;
+}
+
+.padding-top {
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.cohort-name-student {
+ margin-left: 50px;
+ font-size: 16px;
+ color: #64648C;
+ margin-top: 4px;
+}
+
+.snackbar {
+ position: fixed;
+ bottom: 32px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #333;
+ color: #fff;
+ padding: 16px 32px;
+ border-radius: 8px;
+ z-index: 9999;
+ font-size: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+ opacity: 1;
+ transition: opacity 0.3s;
+}
+
+.students-list-teacher-view {
+ max-height: 300px;
+ overflow-y: auto;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
\ No newline at end of file
diff --git a/src/pages/dashboard/teachers/index.js b/src/pages/dashboard/teachers/index.js
new file mode 100644
index 00000000..c8c3000c
--- /dev/null
+++ b/src/pages/dashboard/teachers/index.js
@@ -0,0 +1,61 @@
+import { useEffect, useState } from "react"
+import { get } from "../../../service/apiClient"
+import Card from "../../../components/card"
+import UserIcon from "../../../components/profile-icon"
+
+const TeachersDashboard = () => {
+ const [teachers, setTeachers] = useState(null)
+
+ useEffect(() => {
+ async function fetchTeachers() {
+ try {
+ const response = await get("teachers")
+ setTeachers(Array.isArray(response.data.profiles) ? response.data.profiles : [])
+ } catch (error) {
+ console.error("Error fetching teachers: ", error)
+ }
+ }
+ fetchTeachers()
+ }, [])
+
+ return (
+ <>
+
+ Teachers
+
+ {teachers !== null ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+}
+
+export default TeachersDashboard
\ No newline at end of file
diff --git a/src/pages/edit/edit.css b/src/pages/edit/edit.css
new file mode 100644
index 00000000..483aa929
--- /dev/null
+++ b/src/pages/edit/edit.css
@@ -0,0 +1,164 @@
+.edit-profile-form {
+ width: 120%;
+ margin: 2rem auto;
+ padding: 2rem;
+ background-color: #fff;
+ border: 1px solid #e6ebf5;
+ border-radius: 12px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+ font-family: 'Inter', sans-serif;
+ display: flex;
+ flex-direction: column;
+}
+
+.edit-profile-form h2 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: #333;
+}
+
+.section h3 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ color: #444;
+}
+
+.row {
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+}
+
+.section {
+ flex: 1;
+ min-width: 300px;
+}
+
+.half {
+ width: 100%;
+}
+
+@media (min-width: 768px) {
+ .half {
+ width: 48%;
+ }
+}
+
+.section > *:not(h3):not(.photo-placeholder):not(.char-count) {
+ margin-bottom: 1.5rem;
+}
+
+.photo-placeholder {
+ width: 80px;
+ height: 80px;
+ background-color: #ddd;
+ border-radius: 50%;
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #555;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+}
+
+input,
+select,
+textarea {
+ width: 100%;
+ padding: 01.6rem;
+ font-size: 1rem;
+ font-family: 'Inter', sans-serif;
+ background-color: #fff;
+}
+
+.char-count {
+ text-align: right;
+ font-size: 0.85rem;
+ color: #666;
+ margin-top: -1rem;
+ margin-bottom: 1.5rem;
+}
+
+.save-button {
+ align-self: flex-end;
+ background-color: #0077cc;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.save-button:hover {
+ background-color: #005fa3;
+}
+
+.bio-area {
+ background-color: #e6ebf5
+}
+
+.save {
+ background-color: var(--color-blue);
+ color: white;
+}
+
+.cancel {
+ background-color: var(--color-blue5);
+}
+
+.bottom-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 20px;
+ padding: 20px;
+}
+
+.change-password-button {
+ width: 100%;
+ padding: 0.6rem;
+ font-size: 1rem;
+ font-family: 'Inter', sans-serif;
+ background-color: var(--color-blue);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.photo-row {
+ display: flex !important;
+ flex-direction: row !important;
+ align-items: center !important;
+ gap: 20px !important;
+ flex-wrap: nowrap !important;
+}
+
+.photo-wrapper .profile-photo {
+ width: 60px !important;
+ height: 60px !important;
+ object-fit: cover !important;
+ display: block !important;
+}
+
+.profile-photo {
+ width: 100px;
+ height: 100px;
+}
+
+.profile-container .info-section .info-row .label {
+ color: #333333;
+}
+
+.profile-container .info-section .info-row .value {
+ color: #111111;
+}
+
+.profile-container .info-section .info-row .value a {
+ color: #0077cc;
+ text-decoration: underline;
+}
diff --git a/src/pages/edit/index.js b/src/pages/edit/index.js
new file mode 100644
index 00000000..ee014b68
--- /dev/null
+++ b/src/pages/edit/index.js
@@ -0,0 +1,402 @@
+import { useEffect, useState } from "react";
+import "./edit.css";
+import Popup from "reactjs-popup";
+import imageCompression from "browser-image-compression";
+import { getUserById, updateUserProfile, refreshToken } from "../../service/apiClient";
+import useAuth from "../../hooks/useAuth";
+import jwtDecode from "jwt-decode";
+import TextInput from "../../components/form/textInput";
+import Card from "../../components/card";
+import { validatePassword, validateEmail } from '../register';
+import LockIcon from '../../assets/icons/lockIcon'
+import SimpleProfileCircle from "../../components/simpleProfileCircle";
+
+const EditPage = () => {
+ const [formData, setFormData] = useState(null);
+ const { token } = useAuth();
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token || localStorage.getItem('token'));
+ userId = decodedToken?.userId;
+ } catch (error) {
+ console.error('Invalid token:', error);
+ userId = null;
+ }
+
+ const [formValues, setFormValues] = useState({
+ photo: "",
+ firstName: "",
+ lastName: "",
+ username: "",
+ githubUsername: "",
+ email: "",
+ mobile: "",
+ password: "",
+ bio: "",
+ });
+ const [showPasswordFields, setShowPasswordFields] = useState(false);
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+
+ useEffect(() => {
+ async function fetchUser() {
+ try {
+ const data = await getUserById(userId);
+ setFormData(data);
+
+ const profile = data.profile || {};
+ setFormValues({
+ photo: profile.photo || "",
+ firstName: profile.firstName || "",
+ lastName: profile.lastName || "",
+ username: profile.username || "",
+ githubUsername: profile.githubUrl || "",
+ email: data.email || "",
+ mobile: profile.mobile || "",
+ password: data.password || "",
+ bio: profile.bio || "",
+ });
+ } catch (error) {
+ console.error("Error in EditPage", error);
+ }
+ }
+ if (userId) fetchUser();
+ }, [userId]);
+
+ if (!formData || !formData.profile) {
+ return (
+
+ );
+ }
+
+ const firstName = formData.profile.firstName;
+ const lastName = formData.profile.lastName;
+ const name = `${firstName} ${lastName}`;
+
+ const getReadableRole = (role) => {
+ switch (role) {
+ case 'ROLE_STUDENT': return 'Student';
+ case 'ROLE_TEACHER': return 'Teacher';
+ case 'ROLE_ADMIN': return 'Administrator';
+ default: return role;
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormValues((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const togglePasswordFields = () => setShowPasswordFields(prev => !prev);
+
+ const handleFileCompressionAndSet = async (file, closePopup) => {
+ if (!file) return;
+ if (!file.type.startsWith('image/')) { alert('Not an image'); return; }
+
+ const options = { maxSizeMB: 0.5, maxWidthOrHeight: 1024, useWebWorker: true, initialQuality: 0.8 };
+
+ try {
+ const compressedFile = await imageCompression(file, options);
+ if (compressedFile.size > 2 * 1024 * 1024) {
+ alert('Bildet er fortsatt for stort etter komprimering. Velg et mindre bilde.');
+ return;
+ }
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const dataUrl = reader.result;
+ setFormValues(prev => ({ ...prev, photo: dataUrl }));
+ if (typeof closePopup === 'function') closePopup();
+ };
+ reader.readAsDataURL(compressedFile);
+ } catch (err) {
+ console.error('Compression error', err);
+ alert('Kunne ikke komprimere bildet');
+ }
+ };
+
+ const resetFormToSaved = () => {
+ if (!formData) return;
+ const profile = formData.profile || {};
+ setFormValues({
+ photo: profile.photo || "",
+ firstName: profile.firstName || "",
+ lastName: profile.lastName || "",
+ username: profile.username || "",
+ githubUsername: profile.githubUrl || "",
+ email: formData.email || "",
+ mobile: profile.mobile || "",
+ password: "",
+ bio: profile.bio || "",
+ });
+ alert("The changes are discarded")
+ setNewPassword("");
+ setConfirmPassword("");
+ setShowPasswordFields(false);
+ };
+
+ const handleSave = async (e) => {
+ e.preventDefault();
+
+ if (!validateEmail(formValues.email)) return;
+
+ if (showPasswordFields) {
+ const isValidFormat = validatePassword(newPassword);
+ if (!isValidFormat) return;
+ if (newPassword !== confirmPassword) {
+ alert("The passwords do not match.");
+ return;
+ }
+ };
+
+ const updatedValues = { ...formValues, password: showPasswordFields ? newPassword : "" };
+
+ try {
+ const refreshed = await updateUserProfile(userId, updatedValues);
+ alert("Profile is updated!");
+ setFormData(refreshed);
+ const refreshedProfile = refreshed.profile || {};
+
+ // Update localStorage with new photo
+ if (refreshedProfile.photo) {
+ localStorage.setItem('userPhoto', refreshedProfile.photo);
+ }
+
+ // Refresh the token to get updated user information
+ try {
+ const refreshResponse = await refreshToken();
+ if (refreshResponse.token) {
+ localStorage.setItem('token', refreshResponse.token);
+ }
+ } catch (tokenError) {
+ console.error('Token refresh failed:', tokenError);
+ }
+
+ setFormValues({
+ photo: refreshedProfile.photo || "",
+ firstName: refreshedProfile.firstName || "",
+ lastName: refreshedProfile.lastName || "",
+ username: refreshedProfile.username || "",
+ githubUsername: refreshedProfile.githubUrl || "",
+ email: refreshed.email || "",
+ mobile: refreshedProfile.mobile || "",
+ bio: refreshedProfile.bio || "",
+ });
+ } catch (error) {
+ console.error("Error by update:", error);
+ alert("Something went wrong by the update.");
+ }
+ };
+
+ return (
+ <>
+
+ Profile
+
+
+ >
+ );
+};
+
+export default EditPage;
diff --git a/src/pages/editCohort/index.js b/src/pages/editCohort/index.js
new file mode 100644
index 00000000..08fb1a84
--- /dev/null
+++ b/src/pages/editCohort/index.js
@@ -0,0 +1,149 @@
+import { useNavigate, useParams } from "react-router-dom"
+import ExitIcon from "../../assets/icons/exitIcon"
+import "./style.css"
+import StepperCohort from "./steps"
+import StepOneCohort from "./stepOne"
+import { useEffect, useState} from "react"
+import { get } from "../../service/apiClient"
+import StepTwoCohort from "./stepTwo"
+import StepThreeCohort from "./stepThree"
+
+
+const EditCohort = () =>{
+ const [students, setStudents] = useState([])
+ const [courses, setCourses] = useState([])
+
+ const [cohortName, setCohortName] = useState("")
+ const[startDate, setStartDate] = useState("")
+ const[endDate, setEndDate] = useState("")
+ const [selectedCourse, setSelectedCourse] = useState("")
+ const [cohort, setCohort] = useState(null)
+
+
+ const [selectedStudents, setSelectedStudents] = useState([]);
+
+ const {id} = useParams()
+
+
+
+ useEffect(() => {
+ async function fetchCohortById() {
+
+ try {
+ const response = await get(`cohorts/${id}`);
+ setCohort(response.data.cohort);
+ console.log("Cohort: " , response)
+ } catch (error) {
+ console.error("Error fetching cohort by ID:", error);
+ }
+
+ }
+ async function fetchStudents() {
+ try {
+ const response = await get("students");
+ setStudents(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ }
+
+ async function fetchCourses() {
+ try {
+ const response = await get("courses");
+ setCourses(response.data.courses);
+ } catch (error) {
+ console.error("Error fetching courses:", error);
+ }
+ }
+ fetchStudents();
+ fetchCourses();
+ fetchCohortById();
+ }, []);
+
+
+ // TODO
+ // Prelaod informasjon fra cohorten
+
+ console.log(cohort)
+
+ useEffect(()=>{
+
+ if(cohort){
+ setCohortName(cohort.name)
+ setSelectedStudents(cohort.profiles)
+ setSelectedCourse(cohort.course)
+ setStartDate(cohort.startDate)
+ setEndDate(cohort.endDate)
+
+ console.log(selectedCourse)
+ }
+
+
+
+ },[cohort])
+
+
+ return (
+ <>
+ }
+ cohortName={cohortName}
+ setCohortName={setCohortName}
+ startDate={startDate}
+ setStartDate={setStartDate}
+ endDate={endDate}
+ setEndDate={setEndDate}
+ courses={courses}
+ selectedCourse={selectedCourse}
+ selectedStudents={selectedStudents}
+ setSelectedCourse={setSelectedCourse}
+ setSelectedStudents={setSelectedStudents}
+ cohortId = {id}>
+
+
+
+
+ >
+ )
+}
+
+
+const CohortHeader = () => {
+ const navigate = useNavigate()
+ return (
+ <>
+
+
Edit cohort
+
+ navigate(-1)}>
+
+
+
+
+ Update the info for this cohort
+
+ >
+ )
+}
+export default EditCohort
diff --git a/src/pages/editCohort/stepOne/index.js b/src/pages/editCohort/stepOne/index.js
new file mode 100644
index 00000000..f0308cfe
--- /dev/null
+++ b/src/pages/editCohort/stepOne/index.js
@@ -0,0 +1,86 @@
+
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon"
+import CoursesMenu from "../../addStudent/coursesMenu"
+import { useState } from "react"
+
+
+
+
+const StepOneCohort = ( {setCohortName, setStartDate, setEndDate, cohortName, startDate, endDate, courses, setSelectedCourse, selectedCourse}) => {
+ const [courseIsOpen, setCourseIsOpen] = useState(false)
+
+
+
+ const handleChangeCohortName = (event) => {
+ setCohortName(event.target.value)
+ }
+
+ const handleSelectCourse = (course) => {
+ console.log("selected course", course)
+ setCourseIsOpen(false)
+ setSelectedCourse(course)
+ }
+
+ const handleStartDate = (event) => {
+ setStartDate(event.target.value)
+ }
+
+ const handleEndDate = (event) => {
+ setEndDate(event.target.value)
+ }
+
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
setCourseIsOpen(true)}>
+ {selectedCourse !== null ? ({selectedCourse.name}
+ ):( Select a course)}
+
+
+
+
+ {courseIsOpen && (
)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+>
+ )
+}
+
+export default StepOneCohort
diff --git a/src/pages/editCohort/stepThree/index.js b/src/pages/editCohort/stepThree/index.js
new file mode 100644
index 00000000..7dd516c1
--- /dev/null
+++ b/src/pages/editCohort/stepThree/index.js
@@ -0,0 +1,79 @@
+import { useState } from "react";
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon";
+import MultipleStudentsMenu from "../stepTwo/multipleStudentsMenu";
+import SearchBarMultiple from "../stepTwo/SearchBarMultiple";
+import CourseIcon from "../../../components/courseIcon";
+
+const StepThreeCohort = ({cohortName, selectedCourse, students, selectedStudents, setSelectedStudents, endDate, startDate}) => {
+ const [isOpenStudents, setIsOpenStudents] = useState(false);
+const [isOpenSearchBar, setIsOpenSearchBar] = useState(false);
+
+
+
+const handleSelectStudent = (student) => {
+
+ console.log("Klikket pΓ₯ student:", student);
+
+ setSelectedStudents((prevSelected) => {
+ const alreadySelected = prevSelected.find((s) => s.id === student.id);
+ if (alreadySelected) {
+ // Fjern student hvis allerede valgt
+ return prevSelected.filter((s) => s.id !== student.id);
+ } else {
+ // Legg til student
+ return [...prevSelected, student];
+ }
+ })
+
+ setTimeout(()=> {
+ setIsOpenSearchBar(false)
+ }, 500)
+
+ console.log(selectedStudents);
+};
+
+ return (
+ <>
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+
+
Cohort details
+
+ {console.log(selectedCourse)}
+ {console.log(cohortName)}
+ {console.log(startDate)}
+ {console.log(endDate)}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default StepThreeCohort
\ No newline at end of file
diff --git a/src/pages/editCohort/stepTwo/SearchBarMultiple/index.js b/src/pages/editCohort/stepTwo/SearchBarMultiple/index.js
new file mode 100644
index 00000000..f8082c79
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/SearchBarMultiple/index.js
@@ -0,0 +1,73 @@
+import { useRef, useState } from "react";
+import TextInput from "../../../../components/form/textInput";
+import SearchIcon from "../../../../assets/icons/searchIcon";
+import { get } from "../../../../service/apiClient";
+import '../../style.css';
+
+import MultipleStudentsSearch from "../multipleStudentsMenu/searchMultiple";
+
+
+
+const SearchBarMultiple = ({handleSelectStudent, isOpenSearchBar, setIsOpenSearchBar, selectedStudents}) => {
+ const [query, setQuery] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const popupRef = useRef();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ console.log(response);
+ setSearchResults(response.data.profiles);
+ setIsOpenSearchBar(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ {isOpenSearchBar && (
+
+ {searchResults.length > 0 ? (
+
+ ) : (
+
No students with this name found
+ )}
+
+ )}
+
+ >
+ )
+}
+
+export default SearchBarMultiple
\ No newline at end of file
diff --git a/src/pages/editCohort/stepTwo/index.js b/src/pages/editCohort/stepTwo/index.js
new file mode 100644
index 00000000..9b889ee6
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/index.js
@@ -0,0 +1,64 @@
+import { useState } from "react";
+import ArrowDownIcon from "../../../assets/icons/arrowDownIcon";
+import MultipleStudentsMenu from "./multipleStudentsMenu";
+import SearchBarMultiple from "./SearchBarMultiple";
+
+const StepTwoCohort = ({students, selectedStudents, setSelectedStudents}) => {
+
+const [isOpenStudents, setIsOpenStudents] = useState(false);
+const [isOpenSearchBar, setIsOpenSearchBar] = useState(false);
+
+
+
+const handleSelectStudent = (student) => {
+
+ console.log("Klikket pΓ₯ student:", student);
+
+ setSelectedStudents((prevSelected) => {
+ const alreadySelected = prevSelected.find((s) => s.id === student.id);
+ if (alreadySelected) {
+ // Fjern student hvis allerede valgt
+ return prevSelected.filter((s) => s.id !== student.id);
+ } else {
+ // Legg til student
+ return [...prevSelected, student];
+ }
+ })
+
+ setTimeout(()=> {
+ setIsOpenSearchBar(false)
+ }, 500)
+
+ console.log(selectedStudents);
+};
+
+ return (
+ <>
+
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+ >
+ )
+}
+
+export default StepTwoCohort
\ No newline at end of file
diff --git a/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js b/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js
new file mode 100644
index 00000000..de0ce84f
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js
@@ -0,0 +1,32 @@
+
+
+import MultipleStudentsSearch from "./searchMultiple";
+
+const MultipleStudentsMenu = ({ students, handleSelectStudent, selectedStudents }) => {
+ return (
+ <>
+
+ {students.length > 0 ? (
+
+ ) : (
+
+ )}
+ >
+ );
+
+};
+
+export default MultipleStudentsMenu;
diff --git a/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js b/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
new file mode 100644
index 00000000..5f1d2da6
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
@@ -0,0 +1,63 @@
+import "./style.css"
+
+const MultipleStudentsSearch = ({ students, handleSelectStudent , selectedStudents }) => {
+
+ const styleGuideColors = [
+ "#28C846",
+ "#A0E6AA",
+ "#46DCD2",
+ "#82E6E6",
+ "#5ABEDC",
+ "#46C8FA",
+ "#46A0FA",
+ "#666EDC"
+ ];
+
+ const getColorFromInitials = (initials) => {
+ let hash = 0;
+ for (let i = 0; i < initials.length; i++) {
+ hash = initials.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const index = Math.abs(hash) % styleGuideColors.length;
+ return styleGuideColors[index];
+ };
+ return (
+
+<>
+
+ {students.map((student) => {
+ const isSelected = selectedStudents.some((s) => String(s.id) === String(student.id))
+ console.log("Valgte studenter:", selectedStudents);
+
+
+ return (
+ - handleSelectStudent(student)}
+ >
+
+
+
+
{student.firstName.charAt(0) + student.lastName.charAt(0)}
+
+
+
+
{student.firstName} {student.lastName}
+
+
+ {isSelected && }
+
+ );
+ })}
+
+
+ >
+ );
+};
+
+export default MultipleStudentsSearch;
+
+
+
diff --git a/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css b/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css
new file mode 100644
index 00000000..a31c4790
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/style.css
@@ -0,0 +1,49 @@
+.avatar-list-item {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 72px;
+ padding: 8px 16px;
+ gap: 16px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ transition: background-color 0.2s ease;
+}
+
+
+
+.avatar-list-item:hover {
+ background-color: #f9f9f9;
+}
+
+
+.avatar-list-item.selected {
+ background: #F5FAFF;
+
+}
+
+.avatar-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-color: #ccc; /* Dynamisk farge via JS */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ color: #fff;
+ font-size: 14px;
+}
+
+.avatar-name {
+ font-size: 15px;
+ font-weight: 500;
+ color: #333;
+}
+
+.avatar-checkmark {
+ margin-left: auto;
+ font-size: 16px;
+ color: #28C846;
+}
diff --git a/src/pages/editCohort/steps/index.js b/src/pages/editCohort/steps/index.js
new file mode 100644
index 00000000..0f8c98b3
--- /dev/null
+++ b/src/pages/editCohort/steps/index.js
@@ -0,0 +1,123 @@
+/* eslint-disable object-shorthand */
+import { Snackbar, SnackbarContent } from "@mui/material";
+import { useState } from "react";
+import CheckCircleIcon from "../../../assets/icons/checkCircleIcon";
+import { patch} from "../../../service/apiClient";
+import { useNavigate } from "react-router-dom";
+
+
+const StepperCohort = ({ header, children, cohortName, startDate, endDate, selectedCourse, selectedStudents, setSelectedCourse,setEndDate,setStartDate,setCohortName, cohortId }) => {
+ const [currentStep, setCurrentStep] = useState(0);
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+ const navigate = useNavigate()
+
+
+
+ const onBackClick = () => {
+ if (currentStep > 0) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const onNextClick = () => {
+ setCurrentStep(currentStep + 1);
+ };
+
+ const onSkipClick = () => {
+ setCurrentStep(currentStep + 1);
+ };
+
+ const onCancel = () => {
+ setSelectedCourse("")
+ setEndDate("")
+ setStartDate("")
+ setCohortName("")
+ navigate(-1)
+ }
+
+ const onComplete = () =>{
+ async function updateCohort() {
+ try {
+ console.log(selectedStudents)
+ const studentIds = selectedStudents.map(student => student.id);
+ const response2 = await patch(`cohorts/${cohortId}`,
+ {
+ name: cohortName,
+ courseId: selectedCourse.id,
+ startDate: startDate,
+ endDate: endDate,
+ profileIds: studentIds
+ });
+ console.log(response2)
+ } catch (error) {
+ console.error("Error adding new cohort:", error);
+ }
+ } updateCohort()
+
+ setSnackbarOpen(true)
+ setTimeout(()=> {
+ navigate("/cohorts")
+ }, 3000)
+ }
+
+ return (
+
+ {header}
+
+ {children[currentStep]}
+
+ {currentStep === 0 ?
+ (
+ Cancel
+ Next
+
+ ) :
+ currentStep === 1 ? (
+
+
Back
+
Skip
+
+ Add students
+
+
+ ) : (
+
+ Back
+ Update cohort
+
+
+
+
+ Cohort updated
+
+
+ }
+ />
+
+
+
+ )
+ }
+
+
+ );
+};
+
+export default StepperCohort;
diff --git a/src/pages/editCohort/style.css b/src/pages/editCohort/style.css
new file mode 100644
index 00000000..0c84fd98
--- /dev/null
+++ b/src/pages/editCohort/style.css
@@ -0,0 +1,66 @@
+.add-cohort-card {
+ width: 700px !important;
+ height: auto;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
+ padding: 24px;
+ box-sizing: border-box;
+ margin-left: 50px;
+ margin-top: 50px;
+}
+
+.cohort-name-input,
+.cohort-start-date-input {
+width: 100%;
+ margin: 0;
+ height: 56px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 8px;
+ opacity: 1;
+ background-color: #E6EBF5;
+ border: 1px solid #E6EBF5;
+ font-size: 18px;
+ color: #000046;
+ flex: 1;
+ box-sizing: border-box;
+ font-family: 'Lexend', sans-serif;
+ font-weight: 400;
+}
+
+.s,
+.selected-students-view {
+ overflow-y: auto;
+ height: auto;
+ height: 350px;
+ width: 100%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px 0;
+}
+
+.three-buttons {
+ margin-top: 50px;
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 20px;
+ justify-content: space-between;
+}
+
+.cohort-details-group{
+ margin-top:20px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.cohort-details-title {
+ font-size:32px;
+ margin-left:10px;
+}
\ No newline at end of file
diff --git a/src/pages/login/index.js b/src/pages/login/index.js
index 08df7d5a..53e02453 100644
--- a/src/pages/login/index.js
+++ b/src/pages/login/index.js
@@ -4,10 +4,17 @@ import TextInput from '../../components/form/textInput';
import useAuth from '../../hooks/useAuth';
import CredentialsCard from '../../components/credentials';
import './login.css';
+import { useUserRoleData } from '../../context/userRole.';
+import { get } from '../../service/apiClient';
+// eslint-disable-next-line camelcase
+import jwt_decode from 'jwt-decode';
+import { useNavigate } from 'react-router-dom';
const Login = () => {
- const { onLogin } = useAuth();
+ const { onLogin} = useAuth();
const [formData, setFormData] = useState({ email: '', password: '' });
+ const {setUserRole} = useUserRoleData()
+ const navigate = useNavigate()
const onChange = (e) => {
const { name, value } = e.target;
@@ -36,7 +43,20 @@ const Login = () => {
onLogin(formData.email, formData.password)}
+ onClick={async () => {
+ try {
+ await onLogin(formData.email, formData.password);
+ const { userId } = jwt_decode(localStorage.getItem('token'));
+ const role = await get(`users/${userId}`)
+ setUserRole(role.data.user.profile.role.id)
+ navigate("/")
+ }
+ catch (err) {
+ if (err.status === 401) {
+ alert("Email or password is wrong");
+ }
+ }
+ }}
classes="green width-full"
/>
diff --git a/src/pages/profile/index.js b/src/pages/profile/index.js
new file mode 100644
index 00000000..b8dda618
--- /dev/null
+++ b/src/pages/profile/index.js
@@ -0,0 +1,17 @@
+import FullScreenCard from '../../components/fullscreenCard';
+import './profile.css';
+
+const ProfilePage = () => {
+ return (
+ <>
+