setIsMenuVisible(!isMenuVisible)}>
- {isMenuVisible &&
}
+
+ <>
+ {clickable ? (
+
setIsMenuVisible(!isMenuVisible)}>
+ {isMenuVisible &&
}
+
+ ) : (
+
+ )}
+ >
+
);
};
-const CascadingMenu = () => {
+
+
+export const CascadingMenu = ({ id, setIsMenuVisible, cohorts = []}) => {
+
+
+ const [clicked, setClicked] = useState(false);
+
return (
);
};
diff --git a/src/components/profileCircle/style.css b/src/components/profileCircle/style.css
index 47391fe7..8d6e63bc 100644
--- a/src/components/profileCircle/style.css
+++ b/src/components/profileCircle/style.css
@@ -3,6 +3,15 @@
cursor: pointer;
}
+.profile-circle-noclick {
+ position: relative;
+}
+
.profile-circle-menu {
margin-left: 65px;
}
+
+.profile-circle-menu menu{
+ margin-left: 65px;
+ position:absolute !important;
+}
\ 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..007c1e27
--- /dev/null
+++ b/src/components/seeProfile/style.css
@@ -0,0 +1,12 @@
+.user-panel {
+ position: absolute;
+ top: 100%; /* plasseres rett under .edit-icon-wrapper */
+ right: 0; /* høyrekant justert med prikkene */
+ margin-top: 8px; /* liten avstand ned fra knappene */
+ 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..b7d8d564 100644
--- a/src/components/socialLinks/index.js
+++ b/src/components/socialLinks/index.js
@@ -1,76 +1,123 @@
+import React, { useState } from 'react';
import './socialinks.css';
const SocialLinks = () => {
+ const [showGallery, setShowGallery] = useState(false);
+ const [currentImageIndex, setCurrentImageIndex] = useState(0);
+
+ const images = [
+ 'https://i.imgur.com/m03ohUW.png',
+ 'https://i.imgur.com/CiWtEZw.png',
+ 'https://i.imgur.com/wYqvu2p.png',
+ ];
+
+ const handleButtonClick = (e) => {
+ e.preventDefault();
+ setShowGallery(true);
+ setCurrentImageIndex(0);
+ };
+
+ const handleImageClick = () => {
+ if (currentImageIndex < images.length - 1) {
+ setCurrentImageIndex(currentImageIndex + 1);
+ } else {
+ setShowGallery(false);
+ setCurrentImageIndex(0);
+ }
+ };
+
+ const handleOverlayClick = (e) => {
+ if (e.target === e.currentTarget) {
+ setShowGallery(false);
+ setCurrentImageIndex(0);
+ }
+ };
return (
<>
-
-
+
+ {showGallery && (
+
+
+

+
+
+
+
+ )}
>
);
};
export default SocialLinks;
+
+
+
+
diff --git a/src/components/stepper/index.js b/src/components/stepper/index.js
index c9e5f259..67bbd6cf 100644
--- a/src/components/stepper/index.js
+++ b/src/components/stepper/index.js
@@ -3,9 +3,14 @@ import Card from '../card';
import Button from '../button';
import './style.css';
import { useState } from 'react';
+import CheckCircleIcon from '../../assets/icons/checkCircleIcon';
+import { Snackbar, SnackbarContent } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
-const Stepper = ({ header, children, onComplete }) => {
+const Stepper = ({ header, children, onComplete, data,message }) => {
const [currentStep, setCurrentStep] = useState(0);
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+ const navigate = useNavigate()
const onBackClick = () => {
if (currentStep > 0) {
@@ -16,12 +21,46 @@ const Stepper = ({ header, children, onComplete }) => {
const onNextClick = () => {
if (currentStep === children.length - 1) {
onComplete();
+
+ setSnackbarOpen(true)
+ if(snackbarOpen) {
+ setTimeout(() => {
+ navigate('/cohorts');
+ }, 2000);
+ }
return;
}
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 +72,61 @@ 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..cadf3a14 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,15 +15,24 @@ const AuthProvider = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const [token, setToken] = useState(null);
+ const [user] = useState(null);
+ const [userPhoto, setUserPhoto] = useState(localStorage.getItem('userPhoto'));
+ const [refresh, setRefresh] = useState(false)
useEffect(() => {
const storedToken = localStorage.getItem('token');
+ const storedPhoto = localStorage.getItem('userPhoto');
- if (storedToken) {
+ if (storedToken && !token) {
setToken(storedToken);
navigate(location.state?.from?.pathname || '/');
}
- }, [location.state?.from?.pathname, navigate]);
+
+ // Set the user photo if it exists in localStorage
+ if (storedPhoto && !userPhoto) {
+ setUserPhoto(storedPhoto);
+ }
+ }, [location.state?.from?.pathname, navigate, token, userPhoto]);
const handleLogin = async (email, password) => {
const res = await login(email, password);
@@ -34,37 +43,143 @@ const AuthProvider = ({ children }) => {
localStorage.setItem('token', res.data.token);
- setToken(res.token);
- navigate(location.state?.from?.pathname || '/');
+ setToken(res.data.token);
+
+ navigate('/');
+
+ // After successful login, fetch and store user data
+ try {
+ const { userId } = jwt_decode(res.data.token);
+ const userData = await getUserById(userId);
+ const photo = userData.data.user.profile?.photo;
+
+ // Only update localStorage and state if we got a photo from the server
+ // This preserves any photo that might already be stored from registration
+ if (photo) {
+ localStorage.setItem('userPhoto', photo);
+ setUserPhoto(photo);
+ } else {
+ // If no photo from server, check if we already have one in localStorage
+ const existingPhoto = localStorage.getItem('userPhoto');
+ if (existingPhoto) {
+ setUserPhoto(existingPhoto);
+ }
+ }
+ } catch (error) {
+ console.error('Error fetching user photo:', error);
+ // If there's an error fetching from server, try to use existing localStorage photo
+ const existingPhoto = localStorage.getItem('userPhoto');
+ if (existingPhoto) {
+ setUserPhoto(existingPhoto);
+ }
+ }
};
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) {
+ // 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);
+
+ // Always store the photo in localStorage if provided, regardless of token response
+ if (photo) {
+ localStorage.setItem('userPhoto', photo);
+ setUserPhoto(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);
+ } else {
+ // If token refresh is not available, force a refresh of contexts
+ forceTokenRefresh();
+ }
+
+ } catch (refreshError) {
+ console.error('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, photo) => {
+
+ await createNewStudent(first_name, last_name, username, github_username, email, mobile, password, bio, role, specialism, cohort, photo);
+
+ // Store the photo in localStorage if it was provided during student creation
+ if (photo) {
+ localStorage.setItem('userPhoto', photo);
+ setUserPhoto(photo);
+ }
localStorage.setItem('token', token);
- navigate('/');
+ setRefresh(prev => !prev)
+
+ setTimeout(() => {
+ navigate('/cohorts');
+ }, 2000);
+
};
const value = {
token,
+ user,
+ userPhoto,
+ refresh,
+ setRefresh,
+ 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/data.js b/src/context/data.js
new file mode 100644
index 00000000..61b5ce07
--- /dev/null
+++ b/src/context/data.js
@@ -0,0 +1,167 @@
+import { createContext, useContext, useEffect, useState } from "react";
+import useAuth from "../hooks/useAuth";
+import { get } from "../service/apiClient";
+import jwtDecode from "jwt-decode";
+
+// Roller
+const ROLES = {
+ TEACHER: "ROLE_TEACHER",
+ STUDENT: "ROLE_STUDENT"
+};
+
+const DataContext = createContext();
+
+export const DataProvider = ({ children }) => {
+ const [cohorts, setCohorts] = useState([]);
+ const [students, setStudents] = useState([]);
+ const [teachers, setTeachers] = useState([]);
+ const [courses, setCourses] = useState([]);
+
+ const [myCohort, setMyCohort] = useState(null);
+ const [myProfile, setMyProfile] = useState(null);
+ const [teachersInMyCohort, setTeachersInMyCohort] = useState([]);
+ const [studentsInMyCohort, setStudentsInMyCohort] = useState([]);
+
+ const { token, refresh } = useAuth();
+ const [userId, setUserId] = useState("");
+ const [userRole, setUserRole] = useState(2);
+
+
+ // 1. Hent bruker-ID når token er tilgjengelig
+ useEffect(() => {
+ if (token) {
+ try {
+ const decodedToken = jwtDecode(token);
+ setUserId(decodedToken.userId);
+ setUserRole(decodedToken.roleId);
+ console.log("User role ID:", decodedToken.roleId);
+ } catch (error) {
+ console.error("Invalid token:", error);
+ }
+ } else {
+ // Logout: nullstill state
+ setUserId("");
+ setUserRole(2);
+ setCohorts([]);
+ setStudents([]);
+ setTeachers([]);
+ setCourses([]);
+ setMyCohort(null);
+ setMyProfile(null);
+ setTeachersInMyCohort([]);
+ setStudentsInMyCohort([]);
+ }
+ }, [token]);
+
+ // 2. Hent data ved aktiv token
+ useEffect(() => {
+ if (!token) return;
+
+ const fetchCohorts = async () => {
+ try {
+ const response = await get("cohorts");
+ setCohorts(response.data.cohorts);
+ } catch (error) {
+ console.error("Error fetching cohorts:", error);
+ }
+ };
+
+ const fetchStudents = async () => {
+ try {
+ const response = await get("students");
+ const studentData = response.data.profiles;
+ setStudents(studentData);
+ console.log("Fetched students:", studentData);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ };
+
+ const fetchTeachers = async () => {
+ try {
+ const response = await get("teachers");
+ const teacherData = Array.isArray(response.data.profiles)
+ ? response.data.profiles
+ : [];
+ setTeachers(teacherData);
+ } catch (error) {
+ console.error("Error fetching teachers: ", error);
+ }
+ };
+
+ const fetchCourses = async () => {
+ try {
+ const response = await get("courses");
+ setCourses(response.data.courses);
+ } catch (error) {
+ console.error("Error fetching courses:", error);
+ }
+ };
+
+ fetchCohorts();
+ fetchStudents();
+ fetchTeachers();
+ fetchCourses();
+ }, [token, refresh]);
+
+ // 3. Finn min cohort og profil
+ useEffect(() => {
+ if (userId && cohorts.length > 0) {
+ const cohort = cohorts.find(
+ c =>
+ Array.isArray(c.profiles) &&
+ c.profiles.some(p => p.user.id === userId)
+ );
+
+ if (cohort) {
+ const profile = cohort.profiles.find(p => p.user.id === userId);
+ setMyCohort(cohort);
+ setMyProfile(profile);
+ console.log("Cohort:", cohort);
+ console.log("Profile:", profile);
+ } else {
+ console.warn("Ingen cohort funnet for userId:", userId);
+ setMyCohort(null);
+ setMyProfile(null);
+ }
+ }
+ }, [userId, cohorts]);
+
+ // 4. Filtrer lærere og studenter i min cohort
+ useEffect(() => {
+ if (myCohort?.profiles) {
+ const teachers = myCohort.profiles.filter(
+ profile => profile.role.name === ROLES.TEACHER
+ );
+ const students = myCohort.profiles.filter(
+ profile => profile.role.name === ROLES.STUDENT
+ );
+ setTeachersInMyCohort(teachers);
+ setStudentsInMyCohort(students);
+ console.log("Teachers in my cohort:", teachers);
+ console.log("Students in my cohort:", students);
+ }
+ }, [myCohort]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useData = () => useContext(DataContext);
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..b59ed892
--- /dev/null
+++ b/src/context/loading.js
@@ -0,0 +1,54 @@
+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...') => {
+ setLoadingMessage(message);
+ setIsGlobalLoading(true);
+ }, []);
+
+ const hideGlobalLoading = useCallback(() => {
+ setIsGlobalLoading(false);
+ }, []);
+
+ const isDashboardInitialized = useCallback(() => {
+ return dashboardInitializedRef.current;
+ }, []);
+
+ const setDashboardInitialized = useCallback((value) => {
+ dashboardInitializedRef.current = value;
+ }, []);
+
+ const resetDashboardInitialization = useCallback(() => {
+ 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;
+};
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/index.js b/src/index.js
index edcbecda..11f59edd 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './styles/index.css';
import App from './App';
-import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
@@ -12,9 +11,4 @@ root.render(
-);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
+);
\ 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..234017cf
--- /dev/null
+++ b/src/pages/addCohort/index.js
@@ -0,0 +1,85 @@
+import { useNavigate } from "react-router-dom"
+import ExitIcon from "../../assets/icons/exitIcon"
+import "./style.css"
+import StepperCohort from "./steps"
+import StepOneCohort from "./stepOne"
+import { useState } from "react"
+import StepTwoCohort from "./stepTwo"
+import StepThreeCohort from "./stepThree"
+import { useData } from "../../context/data"
+
+
+const AddCohort = () =>{
+
+ const {students, courses} = useData()
+
+ const [cohortName, setCohortName] = useState("")
+ const[startDate, setStartDate] = useState("")
+ const[endDate, setEndDate] = useState("")
+ const [selectedCourse, setSelectedCourse] = useState("")
+ const [selectedStudents, setSelectedStudents] = useState([]);
+
+
+ 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..3a2bf61a
--- /dev/null
+++ b/src/pages/addCohort/stepOne/index.js
@@ -0,0 +1,84 @@
+
+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) => {
+ 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/addCohort/stepThree/index.js b/src/pages/addCohort/stepThree/index.js
new file mode 100644
index 00000000..c0b207ba
--- /dev/null
+++ b/src/pages/addCohort/stepThree/index.js
@@ -0,0 +1,72 @@
+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)
+
+};
+
+ return (
+ <>
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+
+
Cohort details
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default StepThreeCohort
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..68001697
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/index.js
@@ -0,0 +1,62 @@
+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) => {
+
+
+ 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
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..6bd11f3b
--- /dev/null
+++ b/src/pages/addCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
@@ -0,0 +1,65 @@
+import CheckIcon from "../../../../../assets/icons/checkIcon";
+import SimpleProfileCircle from "../../../../../components/simpleProfileCircle";
+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..a7c2ea60
--- /dev/null
+++ b/src/pages/addCohort/steps/index.js
@@ -0,0 +1,134 @@
+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";
+import useAuth from "../../../hooks/useAuth";
+
+
+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 {setRefresh} = useAuth()
+
+
+ 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
+ });
+
+ 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)
+ setRefresh(prev => !prev)
+
+ } catch (error) {
+ console.error("Error adding new cohort:", error);
+ }
+ } addNewCohort()
+ setRefresh(prev => !prev)
+ 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;
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..3dcf0dff
--- /dev/null
+++ b/src/pages/addStudent/index.js
@@ -0,0 +1,181 @@
+import ExitIcon from "../../assets/icons/exitIcon";
+import "./style.css";
+import SearchBar from "./searchBar";
+import {useState } from "react";
+import { 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";
+import { useData } from "../../context/data";
+import useAuth from "../../hooks/useAuth";
+
+
+
+const AddStudent = () => {
+
+
+
+ const {students, courses} = useData()
+ const{setRefresh} = useAuth()
+ 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()
+
+
+
+
+
+ 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)});
+ setRefresh(prev => !prev)
+ console.log(response)
+ } catch (error) {
+ console.error("Error adding student to cohort:", error);
+ }
+ } setRefresh(prev => !prev)
+ addStudentToCohort()
+ setSnackbarOpen(true);
+
+ setTimeout(()=> {
+ navigate(-1)
+ }, 4000)
+ }
+ 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..6f0047f1
--- /dev/null
+++ b/src/pages/addStudent/searchBar/index.js
@@ -0,0 +1,94 @@
+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}`);
+ setSearchResults(response.data.profiles);
+ setIsOpen(true);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ };
+
+ const handleSelectStudent = (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);
+ }}
+ */
diff --git a/src/pages/addStudent/searchResults/index.js b/src/pages/addStudent/searchResults/index.js
new file mode 100644
index 00000000..158d626a
--- /dev/null
+++ b/src/pages/addStudent/searchResults/index.js
@@ -0,0 +1,55 @@
+import SimpleProfileCircle from "../../../components/simpleProfileCircle";
+
+
+
+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..5afdf4f2
--- /dev/null
+++ b/src/pages/cohort/index.js
@@ -0,0 +1,41 @@
+import Students from "./students";
+
+import Teachers from './teachers';
+import Exercises from "./exercises";
+import TeacherCohort from "./teacherCohort";
+import { useData } from "../../context/data";
+
+
+
+const Cohort = () => {
+
+
+ const{cohorts, myProfile, myCohort, teachersInMyCohort, studentsInMyCohort, userRole} = useData()
+
+
+
+ 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..51775eb4
--- /dev/null
+++ b/src/pages/cohort/newStudent/index.js
@@ -0,0 +1,106 @@
+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 { 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: '',
+ photo: ''
+ });
+
+ const onChange = (event) => {
+ const { name, value } = event.target;
+
+ setProfile({
+ ...profile,
+ [name]: value
+ });
+
+ };
+
+ const onComplete = () => {
+
+ 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.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;
+ }
+ }
+
+ return (
+
+ } onComplete={onComplete} message={"Added new student"}>
+
+
+
+
+
+
+ );
+};
+
+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..0111edaa
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepOne/index.js
@@ -0,0 +1,55 @@
+
+import Form from '../../../../components/form';
+import TextInput from '../../../../components/form/textInput';
+
+
+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..f253096f
--- /dev/null
+++ b/src/pages/cohort/newStudent/newStudentStepThree/index.js
@@ -0,0 +1,114 @@
+import { useState } from 'react';
+import Form from '../../../../components/form';
+import TextInput from '../../../../components/form/textInput';
+import ArrowDownIcon from '../../../../assets/icons/arrowDownIcon';
+import CoursesMenu from '../../../addStudent/coursesMenu';
+import CohortsMenu from '../../../addStudent/cohortsMenu';
+import LockIcon from '../../../../assets/icons/lockIcon';
+import { useData } from '../../../../context/data';
+
+const NewStudentStepThree = ({ setData, setProfile }) => {
+ const {courses, cohorts} = useData()
+
+ const [ cohortsInCourse, setCohorts ] = useState([])
+
+ const [ isOpenCourses, setIsOpenCourses ] = useState(false);
+ const [ isOpenCohorts, setIsOpenCohorts ] = useState(false);
+
+ const [ selectedCourse, setSelectedCourse ] = useState(null)
+ const [ selectedCohort, setSelectedCohort ] = useState(null)
+
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+
+
+
+
+ const handleSelectCourse = (course) => {
+ setSelectedCourse(course)
+ setIsOpenCourses(false)
+ setCohorts(course.cohorts)
+
+ setProfile(prev => ({
+ ...prev,
+ specialism: course?.name,
+ }));
+ }
+
+ const handleSelectCohort = (cohort) => {
+ setIsOpenCohorts(false)
+ setSelectedCohort(cohort)
+ const selected = cohorts.find(c => c.id === cohort.id);
+ if (!selected) return;
+ setStartDate(selected.startDate)
+ setEndDate(selected.endDate)
+
+ setProfile(prev => ({
+ ...prev,
+ cohort: cohort.id
+ }));
+ }
+
+ return (
+ <>
+
+
Training info
+
+
+ >
+ )
+}
+
+export default NewStudentStepThree;
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..54282bdd
--- /dev/null
+++ b/src/pages/cohort/students/index.js
@@ -0,0 +1,65 @@
+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';
+
+
+function Students({ students, 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;
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..88bd90b9
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/index.js
@@ -0,0 +1,100 @@
+import { useEffect, useState } from "react"
+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]);
+
+ // const filteredProfiles = selectedProfiles.filter(profile => profile.role.id === 2)
+
+ 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..4dd79f7f
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/searchTeacher/index.js
@@ -0,0 +1,124 @@
+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.filter(profile => profile.role.id === 2));
+ 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..8f0f03b2
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/studentList/index.js
@@ -0,0 +1,25 @@
+import ProfileIconTeacher from "../../../../components/profile-icon-teacherView";
+
+const StudentList = ({ profiles, cohorts}) => {
+
+ 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..449af1b4
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/style.css
@@ -0,0 +1,205 @@
+
+.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;
+ max-height: 740px;
+ overflow: visible !important;
+}
+
+.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..ebd05026
--- /dev/null
+++ b/src/pages/cohort/teachers/index.js
@@ -0,0 +1,34 @@
+import Card from "../../../components/card";
+import './style.css';
+import Teacher from "./teacher";
+
+
+const Teachers = ({ teachers }) => {
+ 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..ff053044
--- /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.length > 0 ? (
+
+ {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..bad54461 100644
--- a/src/pages/dashboard/index.js
+++ b/src/pages/dashboard/index.js
@@ -1,57 +1,142 @@
-import { useState } from 'react';
-import SearchIcon from '../../assets/icons/searchIcon';
+
+
+import { 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 Students from './students';
+import TeachersDashboard from './teachers';
+import Search from './search';
-const Dashboard = () => {
- const [searchVal, setSearchVal] = useState('');
+import UserIcon from '../../components/profile-icon';
+import SimpleProfileCircle from '../../components/simpleProfileCircle';
+import { useLoading } from '../../context/loading';
+import { usePosts } from '../../context/posts';
+import { useData } from '../../context/data';
+import useAuth from '../../hooks/useAuth';
- const onChange = (e) => {
- setSearchVal(e.target.value);
- };
+const Dashboard = () => {
+
+ const {cohorts,students, teachers, myCohort, studentsInMyCohort, myProfile, userRole} = useData()
+ const{refresh} = useAuth()
+
+
- // Use the useModal hook to get the openModal and setModal functions
const { openModal, setModal } = useModal();
+ const { showGlobalLoading, hideGlobalLoading } = useLoading();
+ const { loading: postsLoading, posts } = usePosts();
// Create a function to run on user interaction
const showModal = () => {
- // Use setModal to set the header of the modal and the component the modal should render
- setModal('Create a post',
); // CreatePostModal is just a standard React component, nothing special
-
- // Open the modal!
+ setModal('Create a post',
);
openModal();
};
+
+ // Handle global loading based on posts loading state
+ useEffect(() => {
+ if (postsLoading && posts.length === 0) {
+ showGlobalLoading('Loading posts...');
+ } else {
+ hideGlobalLoading();
+ }
+ }, [postsLoading, posts.length, showGlobalLoading, hideGlobalLoading]);
+
+
+
return (
<>
-
+ {/*
*/}
+
+
+{/*
*/}
+ {/*
{initials}
*/}
+ {/*
*/}
+
-
+
>
);
diff --git a/src/pages/dashboard/search/index.js b/src/pages/dashboard/search/index.js
new file mode 100644
index 00000000..e35d500e
--- /dev/null
+++ b/src/pages/dashboard/search/index.js
@@ -0,0 +1,112 @@
+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..4b1ed0d9
--- /dev/null
+++ b/src/pages/dashboard/students/index.js
@@ -0,0 +1,62 @@
+
+
+import Card from "../../../components/card"
+
+
+import ProfileIconTeacher from "../../../components/profile-icon-teacherView";
+
+const Students = ({students}) => {
+
+
+
+
+ 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(
+ <>
+
+ Students
+
+ {students.length > 0 ? (
+
+
+ {students.map((student, index) => (
+ -
+
+
+ ))}
+
+
+
+
+ ):(
+
+ )}
+
+
+ >
+ )
+}
+export default Students
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..53f5eef3
--- /dev/null
+++ b/src/pages/dashboard/teachers/index.js
@@ -0,0 +1,47 @@
+
+import Card from "../../../components/card"
+import UserIcon from "../../../components/profile-icon"
+
+const TeachersDashboard = ({teachers}) => {
+
+ return (
+ <>
+
+ Teachers
+
+ {teachers.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+}
+
+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..86d1f12d
--- /dev/null
+++ b/src/pages/edit/edit.css
@@ -0,0 +1,165 @@
+.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: 0.6rem; /* rettet fra 01.6rem */
+ font-size: 1rem;
+ font-family: 'Inter', sans-serif;
+ background-color: #fff;
+ box-sizing: border-box; /* viktig så padding ikke øker total bredde */
+}
+
+.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..cb8e359f
--- /dev/null
+++ b/src/pages/edit/index.js
@@ -0,0 +1,486 @@
+import { useEffect, useState } from "react";
+import { Snackbar, SnackbarContent } from "@mui/material";
+import CheckCircleIcon from "../../assets/icons/checkCircleIcon";
+
+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();
+
+ const [snackbar, setSnackbar] = useState({
+ open: false,
+ message: "",
+ actionLabel: null,
+ onAction: null,
+ type: "success",
+ autoHideDuration: 3000,
+ });
+ // const lastValuesBeforeDiscardRef = useRef(null);
+ // const navigate = useNavigate();
+
+ function showSnackbar({ message, actionLabel = null, onAction = null, type = "success", autoHideDuration = 3000 }) {
+ setSnackbar({
+ open: true,
+ message,
+ actionLabel,
+ onAction,
+ type,
+ autoHideDuration,
+ });
+ }
+
+ 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 initials = name.split(" ").map(n => n[0]).join("").toUpperCase();
+
+ 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 || "",
+ });
+ setNewPassword("");
+ setConfirmPassword("");
+ setShowPasswordFields(false);
+
+ showSnackbar({
+
+ message: "Changes discarded",
+ actionLabel: "Edit",
+ onAction: () => { const el = document.querySelector("input, textarea, select"); if (el) el.focus(); },
+ type: "success",
+ autoHideDuration: 3000,
+ });
+
+
+ };
+
+ 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);
+ setFormData(refreshed);
+ showSnackbar({
+
+ message: "Profile saved",
+ actionLabel: "Edit",
+ onAction: () => { const el = document.querySelector("input, textarea, select"); if (el) el.focus(); },
+ type: "success",
+ });
+
+ 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..4ed2e233
--- /dev/null
+++ b/src/pages/editCohort/index.js
@@ -0,0 +1,125 @@
+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"
+import { useData } from "../../context/data"
+
+
+const EditCohort = () =>{
+ const{students, courses} = useData()
+
+ 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);
+ } catch (error) {
+ console.error("Error fetching cohort by ID:", error);
+ }
+
+ }
+
+ fetchCohortById();
+ }, []);
+
+
+ // TODO
+ // Prelaod informasjon fra cohorten
+
+
+ useEffect(()=>{
+
+ if(cohort){
+ setCohortName(cohort.name)
+ setSelectedStudents(cohort.profiles)
+ setSelectedCourse(cohort.course)
+ setStartDate(cohort.startDate)
+ setEndDate(cohort.endDate)
+
+ }
+ },[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..5024b094
--- /dev/null
+++ b/src/pages/editCohort/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) => {
+ 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..a5d46a9c
--- /dev/null
+++ b/src/pages/editCohort/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)
+
+};
+
+ return (
+ <>
+
+
+
+ Or select students:
+
+
+
setIsOpenStudents(prev => !prev)}>
+ Students
+
+
+
+
+ {isOpenStudents && ()}
+
+
+
+
+
+
Cohort details
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default StepThreeCohort
diff --git a/src/pages/editCohort/stepTwo/SearchBarMultiple/index.js b/src/pages/editCohort/stepTwo/SearchBarMultiple/index.js
new file mode 100644
index 00000000..69131e99
--- /dev/null
+++ b/src/pages/editCohort/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
diff --git a/src/pages/editCohort/stepTwo/index.js b/src/pages/editCohort/stepTwo/index.js
new file mode 100644
index 00000000..68001697
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/index.js
@@ -0,0 +1,62 @@
+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) => {
+
+
+ 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
diff --git a/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js b/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js
new file mode 100644
index 00000000..a1ebd32e
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/multipleStudentsMenu/index.js
@@ -0,0 +1,33 @@
+
+
+import MultipleStudentsSearch from "./searchMultiple";
+
+const MultipleStudentsMenu = ({ students, handleSelectStudent, selectedStudents }) => {
+ console.log("students in menu", students)
+ 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..cb9903d3
--- /dev/null
+++ b/src/pages/editCohort/stepTwo/multipleStudentsMenu/searchMultiple/index.js
@@ -0,0 +1,63 @@
+import SimpleProfileCircle from "../../../../../components/simpleProfileCircle";
+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("isSelected", student)
+
+ return (
+ - handleSelectStudent(student)}
+ >
+
+
+
+
{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..6c427bd7
--- /dev/null
+++ b/src/pages/editCohort/steps/index.js
@@ -0,0 +1,126 @@
+/* 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";
+import useAuth from "../../../hooks/useAuth";
+
+
+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 {setRefresh} = useAuth()
+
+
+
+ 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 {
+ 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)
+ setRefresh(prev => !prev)
+
+ } catch (error) {
+ console.error("Error adding new cohort:", error);
+ }
+ } updateCohort()
+ setRefresh(prev => !prev)
+ 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/loading/index.js b/src/pages/loading/index.js
index 30f16c46..d3b7f856 100644
--- a/src/pages/loading/index.js
+++ b/src/pages/loading/index.js
@@ -1,7 +1,7 @@
import FullLogo from '../../assets/fullLogo-whiteLines';
import './loading.css';
-const Dashboard = () => {
+const DashboardLoading = () => {
return (
@@ -14,4 +14,4 @@ const Dashboard = () => {
);
};
-export default Dashboard;
+export default DashboardLoading;
diff --git a/src/pages/login/index.js b/src/pages/login/index.js
index 08df7d5a..b73b71a1 100644
--- a/src/pages/login/index.js
+++ b/src/pages/login/index.js
@@ -5,10 +5,12 @@ import useAuth from '../../hooks/useAuth';
import CredentialsCard from '../../components/credentials';
import './login.css';
+
const Login = () => {
- const { onLogin } = useAuth();
+ const { onLogin} = useAuth();
const [formData, setFormData] = useState({ email: '', password: '' });
+
const onChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
@@ -36,7 +38,16 @@ const Login = () => {
onLogin(formData.email, formData.password)}
+ onClick={async () => {
+ try {
+ await onLogin(formData.email, formData.password);
+ }
+ 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 (
+ <>
+
+ Profile
+
+
+
+
+ >
+ )
+}
+
+export default ProfilePage;
diff --git a/src/pages/profile/profile-data/index.js b/src/pages/profile/profile-data/index.js
new file mode 100644
index 00000000..e73dfdeb
--- /dev/null
+++ b/src/pages/profile/profile-data/index.js
@@ -0,0 +1,85 @@
+import SimpleProfileCircle from '../../../components/simpleProfileCircle';
+import './profile-data.css'
+
+const ProfileData = ({ user, initials, roleValue}) => {
+ const {email} = user;
+ const roleName = roleValue === 2 ? "Student" : "Teacher";
+
+
+
+ return (
+
+
+ {user.photo ?
+

+
+ :
+
+}
+ {(user.firstName || user.lastName) && (
+
{user.firstName} {user.lastName}
+ )}
+ {user.bio &&
{user.bio}
}
+
+
+
+ {(user.firstName || user.lastName) && (
+
+ Full Name:
+ {user.firstName} {user.lastName}
+
+ )}
+
+ {email && (
+
+ Email:
+ {email}
+
+ )}
+
+ {user.mobile && (
+
+ Mobile:
+ {user.mobile}
+
+ )}
+
+ {user.githubUrl && user.githubUrl.trim() !== '' && (
+
+ )}
+
+
+ {user.specialism && (
+
+ Specialism:
+ {user.specialism}
+
+ )}
+
+ {roleName && (
+
+ Role:
+ {roleName}
+
+ )}
+
+
+ );
+};
+
+export default ProfileData;
diff --git a/src/pages/profile/profile-data/profile-data.css b/src/pages/profile/profile-data/profile-data.css
new file mode 100644
index 00000000..246f14a2
--- /dev/null
+++ b/src/pages/profile/profile-data/profile-data.css
@@ -0,0 +1,103 @@
+/* Container: to kolonner */
+.profile-container {
+ display: grid;
+ grid-template-columns: 320px 1fr; /* venstre kolonne fast bredde for bilde, høyre tar resten */
+ gap: 2.5rem;
+ align-items: start;
+ padding: 2.5rem;
+ font-family: 'Inter', sans-serif;
+ font-size: 1.4rem;
+}
+
+/* Venstre kolonne: bilde, navn, bio (sentrert vertikalt/horizontalt) */
+.photo-section-edit {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem;
+}
+
+/* Store profilbilde */
+.profile-photo-edit {
+ width: 220px; /* tilpass ønsket størrelse */
+ height: 220px;
+ object-fit: cover;
+ border-radius: 50%;
+ border: 2px solid #111;
+ display: block;
+}
+
+/* Navn under bildet */
+.name-text {
+ margin: 0;
+ font-size: 1.8rem;
+ font-weight: 700;
+ color: #222;
+ text-align: center;
+}
+
+/* Bio under navnet — tillat linjebryting og riktig bredde */
+.bio-text {
+ margin: 0.5rem 0 0;
+ font-style: italic;
+ color: #555;
+ font-size: 1.05rem;
+ text-align: center;
+ max-width: 280px; /* begrenset kolonne under bildet */
+ white-space: normal;
+ word-wrap: break-word;
+ overflow-wrap: anywhere;
+}
+
+/* Høyre kolonne: info */
+.info-section {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+/* Hver rad i info-seksjonen */
+.info-row {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ font-size: 1.4rem;
+ flex-wrap: wrap;
+}
+
+/* Label og value */
+.label {
+ min-width: 140px;
+ font-weight: 600;
+ color: #333;
+}
+
+.value {
+ flex: 1;
+ font-weight: 400;
+ color: #111;
+ word-break: break-word;
+}
+
+/* Responsive: stack under hverandre på små skjermer */
+@media (max-width: 768px) {
+ .profile-container {
+ grid-template-columns: 1fr; /* stack vertikalt */
+ padding: 1.25rem;
+ }
+ .photo-section-edit {
+ order: 0;
+ margin-bottom: 1rem;
+ }
+ .info-section {
+ order: 1;
+ }
+ .bio-text {
+ max-width: 100%;
+ }
+ .profile-photo-edit {
+ width: 160px;
+ height: 160px;
+ }
+}
diff --git a/src/pages/profile/profile.css b/src/pages/profile/profile.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/register/index.js b/src/pages/register/index.js
index 5cc70e32..d518be30 100644
--- a/src/pages/register/index.js
+++ b/src/pages/register/index.js
@@ -1,13 +1,36 @@
-import { useState } from 'react';
import Button from '../../components/button';
import TextInput from '../../components/form/textInput';
import useAuth from '../../hooks/useAuth';
import CredentialsCard from '../../components/credentials';
import './register.css';
+import ReactPasswordChecklist from 'react-password-checklist';
+import { useFormData } from '../../context/form';
+
+export const validateEmail = (email) => {
+ const mailFormat = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/;
+ if (email.match(mailFormat)) {
+ return true;
+ }
+ else {
+ alert("You have entered an invalid email address");
+ return false;
+ }
+ }
+
+ export const validatePassword = (password) => {
+ const passwordFormat = /^(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/;
+ if (password.match(passwordFormat)) {
+ return true;
+ }
+ else {
+ alert("Your password is not in the right format");
+ return false;
+ }
+ }
const Register = () => {
const { onRegister } = useAuth();
- const [formData, setFormData] = useState({ email: '', password: '' });
+ const {formData, setFormData} = useFormData()
const onChange = (e) => {
const { name, value } = e.target;
@@ -31,6 +54,7 @@ const Register = () => {
type="email"
name="email"
label={'Email *'}
+ required
/>
{
name="password"
label={'Password *'}
type={'password'}
+ required
/>
+
onRegister(formData.email, formData.password)}
+ onClick={async () => {
+ if (validateEmail(formData.email) && validatePassword(formData.password)) {
+ try {
+ await onRegister(formData.email, formData.password);
+ }
+ catch (err) {
+ if (err.status === 400) {
+ alert("Email is already in use");
+ }
+ }
+ }
+ }}
classes="green width-full"
/>
diff --git a/src/pages/search/index.js b/src/pages/search/index.js
new file mode 100644
index 00000000..9746a608
--- /dev/null
+++ b/src/pages/search/index.js
@@ -0,0 +1,119 @@
+import { useState } from "react";
+import { get } from "../../service/apiClient";
+import Card from "../../components/card";
+import TextInput from "../../components/form/textInput";
+import SearchIcon from "../../assets/icons/searchIcon";
+import './style.css';
+import ArrowBack from "../../assets/icons/arrowBack";
+import { useNavigate } from "react-router-dom";
+import { useSearchResults } from "../../context/searchResults";
+import UserIconTeacherView from "../../components/profile-icon-searchTeacherView";
+import UserIconStudentView from "../../components/profile-icon-searchStudentView";
+
+import { useData } from "../../context/data";
+
+const SearchPage = () => {
+ const [query, setQuery] = useState("");
+ const [newresults, setNewResults] = useState(null);
+ const {searchResults} = useSearchResults();
+ const navigate = useNavigate();
+ const {userRole} = useData()
+
+
+
+
+ const handleGoBack = () => {
+ navigate(-1);
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ setNewResults(Array.isArray(response.data.profiles) ? response.data.profiles : []);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ setNewResults([]); // fallback til tom array hvis feil
+ }
+ };
+
+
+ return (
+
+
+
+
+
+
+
+
+
+ {searchResults && (
+
+
+ People
+
+ {(() => {
+ const resultsToShow = newresults ?? searchResults;
+
+ return resultsToShow.length > 0 ? (
+
+ {resultsToShow.slice(0, 10).map((student, index) => (
+ -
+ {userRole === 1 ? (
+
+ ) : userRole === 2 ? (
+
+ ) : null}
+
+ ))}
+
+ ) : (
+ Sorry, no results found
+ );
+ })()}
+
+
+)}
+
+
+
+
+ );
+};
+
+export default SearchPage;
diff --git a/src/pages/search/style.css b/src/pages/search/style.css
new file mode 100644
index 00000000..89d52a7d
--- /dev/null
+++ b/src/pages/search/style.css
@@ -0,0 +1,17 @@
+.search-page {
+ display: "flex";
+ flex-direction: "column";
+
+ gap: "10px";
+ padding: "30px";
+}
+
+.results-section {
+ width: 100%;
+ margin-top: 20px;
+}
+.inputwrapper {
+ position: relative;
+ width: 100%;
+ margin: 0 auto;
+}
diff --git a/src/pages/welcome/index.js b/src/pages/welcome/index.js
index 85af11ab..e2b9ab86 100644
--- a/src/pages/welcome/index.js
+++ b/src/pages/welcome/index.js
@@ -3,16 +3,29 @@ import Stepper from '../../components/stepper';
import useAuth from '../../hooks/useAuth';
import StepOne from './stepOne';
import StepTwo from './stepTwo';
+import StepFour from './stepFour';
import './style.css';
+import { useFormData } from '../../context/form';
+import StepThree from './stepThree';
+import imageCompression from 'browser-image-compression';
const Welcome = () => {
const { onCreateProfile } = useAuth();
+ const { formData } = useFormData();
const [profile, setProfile] = useState({
- firstName: '',
- lastName: '',
- githubUsername: '',
- bio: ''
+ first_name: '',
+ last_name: '',
+ username: '',
+ github_username: '',
+ mobile: '',
+ bio: '',
+ role: 'ROLE_STUDENT',
+ specialism: '',
+ cohort: '',
+ start_date: '',
+ end_date: '',
+ photo: ''
});
const onChange = (event) => {
@@ -25,7 +38,57 @@ const Welcome = () => {
};
const onComplete = () => {
- onCreateProfile(profile.firstName, profile.lastName, profile.githubUsername, profile.bio);
+ onCreateProfile(
+ profile.first_name,
+ profile.last_name,
+ profile.username,
+ profile.github_username,
+ profile.mobile,
+ profile.bio,
+ profile.role,
+ profile.specialism,
+ profile.cohort,
+ profile.start_date,
+ profile.end_date,
+ profile.photo
+ );
+ };
+
+ const handleFileChange = async (event, close) => {
+ const file = event.target.files[0];
+ 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 = () => {
+ setProfile(prev => ({ ...prev, photo: reader.result }));
+ if (typeof close === 'function') close();
+ };
+ reader.readAsDataURL(compressedFile);
+
+ } catch (err) {
+ console.error('Compression error', err);
+ alert('Kunne ikke komprimere bildet. Prøv et annet bilde.');
+ }
};
return (
@@ -35,9 +98,11 @@ const Welcome = () => {
Create your profile to get started
-