- needed to position the dropdown content */
+.dropdown {
+ position: relative;
+ left: 0;
+ display: inline-block;
+}
+
+/* Dropdown Content (Hidden by Default) */
+.dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: #f1f1f1;
+ min-width: 160px;
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+ z-index: 1;
+}
+
+/* Links inside the dropdown */
+.dropdown-content a {
+ color: black;
+ padding: 12px 16px;
+ text-decoration: none;
+ display: block;
+}
+
+/* Change color of dropdown links on hover */
+.dropdown-content a:hover {background-color: #ddd;}
+
+/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
+.show {display:block;}
\ No newline at end of file
diff --git a/src/components/editCommentModal/index.js b/src/components/editCommentModal/index.js
new file mode 100644
index 00000000..b77618e3
--- /dev/null
+++ b/src/components/editCommentModal/index.js
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+import useModal from '../../hooks/useModal';
+import './style.css';
+import Button from '../button';
+import jwt_decode from 'jwt-decode';
+import useAuth from '../../hooks/useAuth';
+import { put } from '../../service/apiClient';
+
+
+
+const EditCommentModal = ({ postText, postId, name, commentId}) => {
+ const { closeModal } = useModal();
+ const { token } = useAuth();
+ const [message, setMessage] = useState(null);
+ const [text, setText] = useState(postText || '');
+ const initials = name?.match(/\b(\w)/g);
+
+
+ const onChange = (e) => {
+ setText(e.target.value);
+ };
+
+ const onSubmit = async () => {
+ try {
+ const { userId } = jwt_decode(token || localStorage.getItem('token')) || {};
+ if (!userId) {
+ setMessage('Could not determine user. Please log in again.');
+ return;
+ }
+
+ const postResponse = await put(`posts/${String(postId)}/comments/${String(commentId)}`, { body: text, userId });
+ console.log('Post updated successfully:', postResponse);
+ setMessage('Posted! Closing modal in 1.5 seconds...');
+ setTimeout(() => {
+ setMessage(null);
+ closeModal();
+ }, 1500);
+ } catch (error) {
+ console.error('Error creating post:', error);
+ setMessage('Failed to create post. Please try again.');
+ }
+
+
+ window.location.reload();
+
+ console.log('Submitting comment:', text);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {message &&
{message}
}
+ >
+ );
+};
+
+export default EditCommentModal;
diff --git a/src/components/editCommentModal/style.css b/src/components/editCommentModal/style.css
new file mode 100644
index 00000000..5b7faa1d
--- /dev/null
+++ b/src/components/editCommentModal/style.css
@@ -0,0 +1,28 @@
+.create-post-user-details {
+ display: grid;
+ grid-template-columns: 56px auto;
+ column-gap: 20px;
+}
+
+textarea {
+ width: 100%;
+ height: 256px;
+ resize: none;
+ background-color: #e6ebf5;
+ border: none;
+ border-radius: 9px;
+ padding: 16px;
+ font-size: 14px;
+ color: #1c1e21;
+ outline: none;
+ transition: background-color 0.2s ease;
+ font-family: inherit;
+}
+
+textarea:focus {
+ background-color: #e6ebf5;
+}
+
+textarea::placeholder {
+ color: #64648c;
+}
diff --git a/src/components/editIconCohortTeacher/cascadingMenuCohort/index.js b/src/components/editIconCohortTeacher/cascadingMenuCohort/index.js
new file mode 100644
index 00000000..3d190737
--- /dev/null
+++ b/src/components/editIconCohortTeacher/cascadingMenuCohort/index.js
@@ -0,0 +1,20 @@
+
+import Menu from "../../menu"
+import MenuItem from "../../menu/menuItem"
+import AddCohortIcon from "../../../assets/icons/addCohortIcon"
+import EditCohortIcon from "../../../assets/icons/editCohortIcon"
+import DeleteIcon from "../../../assets/icons/deleteIcon"
+
+const CascadingMenuCohort = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default CascadingMenuCohort
\ No newline at end of file
diff --git a/src/components/editIconCohortTeacher/index.js b/src/components/editIconCohortTeacher/index.js
new file mode 100644
index 00000000..1383dd44
--- /dev/null
+++ b/src/components/editIconCohortTeacher/index.js
@@ -0,0 +1,42 @@
+import { useState, useRef, useEffect } from 'react';
+import CascadingMenuCohort from './cascadingMenuCohort';
+import './style.css';
+
+
+const EditIconCohortTeacher = ({ initials, menuVisible }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+ const menuRef = useRef(null);
+
+ // Lukk meny ved klikk utenfor
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsMenuVisible(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
setIsMenuVisible(!isMenuVisible)}>
+
+ β’
+ β’
+ β’
+
+
+
+ {isMenuVisible && }
+
+
+
+
+ );
+};
+
+export default EditIconCohortTeacher;
diff --git a/src/components/editIconCohortTeacher/style.css b/src/components/editIconCohortTeacher/style.css
new file mode 100644
index 00000000..7bd82ce4
--- /dev/null
+++ b/src/components/editIconCohortTeacher/style.css
@@ -0,0 +1,7 @@
+
+.menu-left {
+ position: absolute;
+ left: -100px; /* juster avstanden etter behov */
+ top: 0;
+ z-index: 1000;
+}
diff --git a/src/components/editIconCourse/cascadingMenuCourse/index.js b/src/components/editIconCourse/cascadingMenuCourse/index.js
new file mode 100644
index 00000000..2cbc635b
--- /dev/null
+++ b/src/components/editIconCourse/cascadingMenuCourse/index.js
@@ -0,0 +1,22 @@
+
+import Menu from "../../menu"
+import MenuItem from "../../menu/menuItem"
+import AddCohortIcon from "../../../assets/icons/addCohortIcon"
+import EditCohortIcon from "../../../assets/icons/editCohortIcon"
+import DeleteIcon from "../../../assets/icons/deleteIcon"
+import AddStudentIcon from "../../../assets/icons/addStudentIcon"
+
+const CascadingMenuCourse = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default CascadingMenuCourse
\ No newline at end of file
diff --git a/src/components/editIconCourse/index.js b/src/components/editIconCourse/index.js
new file mode 100644
index 00000000..f34f9cd2
--- /dev/null
+++ b/src/components/editIconCourse/index.js
@@ -0,0 +1,42 @@
+import { useState, useRef, useEffect } from 'react';
+import './style.css';
+import CascadingMenuCourse from './cascadingMenuCourse';
+
+
+const EditIconCouse = ({ initials, menuVisible }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+ const menuRef = useRef(null);
+
+ // Lukk meny ved klikk utenfor
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsMenuVisible(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
setIsMenuVisible(!isMenuVisible)}>
+
+ β’
+ β’
+ β’
+
+
+
+ {isMenuVisible && }
+
+
+
+
+ );
+};
+
+export default EditIconCouse;
diff --git a/src/components/editIconCourse/style.css b/src/components/editIconCourse/style.css
new file mode 100644
index 00000000..bb96e1e0
--- /dev/null
+++ b/src/components/editIconCourse/style.css
@@ -0,0 +1,7 @@
+
+.menu-left {
+ position: absolute;
+ left: -300px; /* juster avstanden etter behov */
+ top: 0;
+ z-index: 1000;
+}
diff --git a/src/components/editPostModal/index.js b/src/components/editPostModal/index.js
index 1292ce17..bbc74c3d 100644
--- a/src/components/editPostModal/index.js
+++ b/src/components/editPostModal/index.js
@@ -2,33 +2,57 @@ import { useState } from 'react';
import useModal from '../../hooks/useModal';
import './style.css';
import Button from '../button';
+import jwt_decode from 'jwt-decode';
+import useAuth from '../../hooks/useAuth';
+import { put } from '../../service/apiClient';
-const EditPostModal = () => {
+
+
+const EditPostModal = ({ postText, postId, name }) => {
const { closeModal } = useModal();
+ const { token } = useAuth();
const [message, setMessage] = useState(null);
- const [text, setText] = useState('');
+ const [text, setText] = useState(postText || '');
+ const initials = name?.match(/\b(\w)/g);
const onChange = (e) => {
setText(e.target.value);
};
- const onSubmit = () => {
- setMessage('Submit button was clicked! Closing modal in 2 seconds...');
+ const onSubmit = async () => {
+ try {
+ const { userId } = jwt_decode(token || localStorage.getItem('token')) || {};
+ if (!userId) {
+ setMessage('Could not determine user. Please log in again.');
+ return;
+ }
+
+ const postResponse = await put(`posts/${String(postId)}`, { content: text, userId });
+ console.log('Post updated successfully:', postResponse);
+ setMessage('Posted! Closing modal in 1.5 seconds...');
+ setTimeout(() => {
+ setMessage(null);
+ closeModal();
+ }, 1500);
+ } catch (error) {
+ console.error('Error creating post:', error);
+ setMessage('Failed to create post. Please try again.');
+ }
+
+
+ window.location.reload();
- setTimeout(() => {
- setMessage(null);
- closeModal();
- }, 2000);
+ console.log('Submitting comment:', text);
};
return (
<>
diff --git a/src/components/editPostModal/style.css b/src/components/editPostModal/style.css
index 989fc0d8..5b7faa1d 100644
--- a/src/components/editPostModal/style.css
+++ b/src/components/editPostModal/style.css
@@ -8,7 +8,21 @@ textarea {
width: 100%;
height: 256px;
resize: none;
- background-color: var(--color-blue5);
- border-radius: 8px;
+ background-color: #e6ebf5;
+ border: none;
+ border-radius: 9px;
padding: 16px;
+ font-size: 14px;
+ color: #1c1e21;
+ outline: none;
+ transition: background-color 0.2s ease;
+ font-family: inherit;
+}
+
+textarea:focus {
+ background-color: #e6ebf5;
+}
+
+textarea::placeholder {
+ color: #64648c;
}
diff --git a/src/components/form/numberInput/index.js b/src/components/form/numberInput/index.js
new file mode 100644
index 00000000..00205226
--- /dev/null
+++ b/src/components/form/numberInput/index.js
@@ -0,0 +1,25 @@
+
+const NumberInput = ({ value, onChange, name, label, icon, type = 'number',placeholder}) => {
+
+ return (
+
+ {label && }
+ {
+ if (e.target.value.length > 11) {
+ e.target.value = e.target.value.slice(0, 11);
+ }}}
+ />
+ {icon && {icon}}
+
+ );
+ }
+
+export default NumberInput;
\ No newline at end of file
diff --git a/src/components/form/textInput/index.js b/src/components/form/textInput/index.js
index 39da3cae..55de4189 100644
--- a/src/components/form/textInput/index.js
+++ b/src/components/form/textInput/index.js
@@ -1,8 +1,8 @@
import { useState } from 'react';
-const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => {
- const [input, setInput] = useState('');
+const TextInput = ({ value, onChange, name, label, icon,iconRight, type = 'text', placeholder, readOnly = false }) => {
const [showpassword, setShowpassword] = useState(false);
+ const [input, setInput] = useState(value);
if (type === 'password') {
return (
@@ -11,10 +11,12 @@ const TextInput = ({ value, onChange, name, label, icon, type = 'text' }) => {
type={type}
name={name}
value={value}
+ placeholder = {placeholder}
onChange={(e) => {
onChange(e);
- setInput(e.target.value);
+ setInput(e.target.value)
}}
+ readOnly={readOnly}
/>
{showpassword && }
);
}
-};
+}
const EyeLogo = () => {
return (
diff --git a/src/components/fullscreenCard/fullscreenCard.css b/src/components/fullscreenCard/fullscreenCard.css
new file mode 100644
index 00000000..276aebf0
--- /dev/null
+++ b/src/components/fullscreenCard/fullscreenCard.css
@@ -0,0 +1,96 @@
+.fullscreen-card {
+ width: 150%;
+ height: auto;
+ min-height: 80vh;
+ background: white;
+ padding: 2rem;
+ box-sizing: border-box;
+
+ display: flex;
+ flex-direction: column;
+
+ border: 1px solid #e6ebf5;
+ border-radius: 12px;
+ margin: 0 auto;
+}
+
+.top-bar {
+ height: auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 2rem;
+ box-sizing: border-box;
+}
+
+.profile-container {
+ display: flex;
+ flex-direction: row;
+ gap: 3rem;
+ padding: 3rem;
+ font-family: 'Inter', sans-serif;
+ font-size: 1.4rem;
+ flex-wrap: wrap;
+}
+
+.photo-section {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.profile-photo {
+ width: 150px;
+ height: 150px;
+ object-fit: cover;
+ border-radius: 50%;
+}
+
+.info-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.edit {
+ background-color: var(--color-blue5);
+ border: none;
+
+ cursor: pointer;
+ font-size: 16px;
+}
+
+.photo-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.name-text {
+ margin-top: 0.5rem;
+ font-weight: 600;
+ font-size: 1.6rem;
+ text-align: center;
+}
+
+.bio-text {
+ margin-top: 0.5rem;
+ text-align: center;
+ font-style: italic;
+ color: #555;
+ font-size: 1.4rem;
+ max-width: 200px;
+}
+
+.name-text {
+ font-size: 2rem;
+ font-weight: 600;
+ margin: 0;
+ color: #222;
+}
+
+.edit {
+ width: 15% !important;
+}
\ No newline at end of file
diff --git a/src/components/fullscreenCard/index.js b/src/components/fullscreenCard/index.js
new file mode 100644
index 00000000..384877a8
--- /dev/null
+++ b/src/components/fullscreenCard/index.js
@@ -0,0 +1,76 @@
+import { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import './fullscreenCard.css';
+import ProfileData from '../../pages/profile/profile-data';
+import useAuth from '../../hooks/useAuth';
+import jwtDecode from 'jwt-decode';
+import { getUserById } from '../../service/apiClient';
+import ProfileCircle from '../../components/profileCircle';
+import '../../pages/loading';
+
+const FullScreenCard = () => {
+ const [user, setUser] = useState(null);
+ const { token } = useAuth();
+
+ // Safely decode token with fallback
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token || localStorage.getItem('token'));
+ userId = decodedToken?.userId;
+ } catch (error) {
+ console.error('Invalid token:', error);
+ userId = null;
+ }
+
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const targetId = id ?? userId;
+
+ useEffect(() => {
+ async function fetchUser() {
+ try {
+ const data = await getUserById(targetId);
+ setUser(data);
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ }
+ }
+ fetchUser();
+ }, [targetId]);
+
+ const goToEdit = () => {
+ navigate(`/profile/${userId}/edit`);
+ };
+
+ if (!user || !user.profile) {
+ return
+ }
+
+ const firstname = user.profile.firstName;
+ const lastname = user.profile.lastName;
+ const name = firstname + " " + lastname;
+
+
+ return (
+
+
n[0]).join("").toUpperCase()}/>
+
+
+
+
+
+
+
+ );
+};
+
+export default FullScreenCard;
diff --git a/src/components/header/index.js b/src/components/header/index.js
index c591f1e1..d4a9fe0f 100644
--- a/src/components/header/index.js
+++ b/src/components/header/index.js
@@ -3,14 +3,30 @@ import useAuth from '../../hooks/useAuth';
import './style.css';
import Card from '../card';
import ProfileIcon from '../../assets/icons/profileIcon';
-import CogIcon from '../../assets/icons/cogIcon';
+import CogIcon from '../../assets/icons/EditIcon';
import LogoutIcon from '../../assets/icons/logoutIcon';
import { NavLink } from 'react-router-dom';
import { useState } from 'react';
+import jwtDecode from 'jwt-decode';
+
const Header = () => {
const { token, onLogout } = useAuth();
const [isMenuVisible, setIsMenuVisible] = useState(false);
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in Header:', error);
+ }
+
+ const fullName = `${decodedToken.firstName || decodedToken.first_name || 'Current'} ${decodedToken.lastName || decodedToken.last_name || 'User'}`;
+ const initials = fullName?.match(/\b(\w)/g)?.join('') || 'NO';
+
const onClickProfileIcon = () => {
setIsMenuVisible(!isMenuVisible);
@@ -20,12 +36,17 @@ const Header = () => {
return null;
}
+ let userIdFromToken = null;
+
+ const decoded = jwtDecode(token || localStorage.getItem('token'));
+ userIdFromToken = decoded.userId;
+
return (
-
+
{isMenuVisible && (
@@ -33,19 +54,19 @@ const Header = () => {
-
Alex Jameson
-
Software Developer, Cohort 3
+
{fullName}
+
{decoded.specialism}, Cohort {decoded.cohortId || "3"}
-
-
+
Profile
diff --git a/src/components/header/style.css b/src/components/header/style.css
index cca1ac7a..361faa33 100644
--- a/src/components/header/style.css
+++ b/src/components/header/style.css
@@ -1,16 +1,14 @@
-header {
+.app-header {
grid-column: span 3;
display: grid;
grid-template-columns: 1fr 50px;
background-color: #000046;
padding: 20px 64px;
}
-
-header .profile-icon {
+.app-header .profile-icon {
cursor: pointer;
}
-
-header .user-panel {
+.app-header .user-panel {
position: absolute;
right: 60px;
top: 85px;
diff --git a/src/components/menu/menu.css b/src/components/menu/menu.css
index 619ac144..5b4c18fe 100644
--- a/src/components/menu/menu.css
+++ b/src/components/menu/menu.css
@@ -21,7 +21,8 @@
.menu li {
position: relative;
}
-.menu li a {
+.menu li a,
+.menu li button {
display: grid;
grid-template-columns: 40px auto;
gap: 20px;
@@ -29,18 +30,22 @@
padding: 16px 28px;
text-decoration: none;
}
-.menu li a svg {
+.menu li a svg,
+.menu li button svg {
justify-self: center;
}
-.menu li a p {
+.menu li a p,
+.menu li button p {
color: var(--color-blue1);
font-size: 18px;
line-height: 24px;
}
-.menu li a svg path {
+.menu li a svg path,
+.menu li button svg path {
fill: var(--color-blue1);
}
-.menu li a svg:nth-of-type(2) {
+.menu li a svg:nth-of-type(2),
+.menu li button svg:nth-of-type(2) {
position: absolute;
top: 50%;
right: 26px;
@@ -53,13 +58,16 @@
.menu li:hover {
background: var(--color-blue7);
}
-.menu li:hover > a > p {
+.menu li:hover > a > p,
+.menu li:hover > button > p {
color: var(--color-blue);
}
-.menu li:hover > a > svg {
+.menu li:hover > a > svg,
+.menu li:hover > button > svg {
fill: var(--color-blue);
}
-.menu li:hover > a > svg path {
+.menu li:hover > a > svg path,
+.menu li:hover > button > svg path {
fill: var(--color-blue);
}
.menu li:hover > ul {
diff --git a/src/components/menu/menuItem/index.js b/src/components/menu/menuItem/index.js
index 7b045e88..aadefacc 100644
--- a/src/components/menu/menuItem/index.js
+++ b/src/components/menu/menuItem/index.js
@@ -1,7 +1,133 @@
import { NavLink } from 'react-router-dom';
import ArrowRightIcon from '../../../assets/icons/arrowRightIcon';
+import useModal from '../../../hooks/useModal';
+import EditPostModal from '../../editPostModal';
+import EditCommentModal from '../../editCommentModal';
+import { usePosts } from '../../../context/posts';
+import { useComments } from '../../../context/comments';
+
+const MenuItem = ({ icon, text, children, linkTo = '#nogo', clickable, postText, postId, name, isMenuVisible, setIsMenuVisible, commentText, commentId, onCommentDeleted, onPostDeleted }) => {
+ const { openModal, setModal, closeModal } = useModal();
+ const { deletePost } = usePosts();
+ const { deleteComment } = useComments();
+
+ const showModal = () => {
+ setModal('Edit post', );
+ setIsMenuVisible(false);
+ openModal();
+ };
+
+ const showCommentModal = () => {
+ setModal('Edit comment', );
+ setIsMenuVisible(false);
+ openModal();
+ };
+
+ const handleDeletePost = async () => {
+ setIsMenuVisible(false);
+ console.log('deletePost function called');
+ setModal(`The post is being deleted!`,
+ <>
+ The post is being deleted!
+ >
+ );
+ openModal();
+
+ setTimeout(() => {
+ closeModal();
+ }, 2500);
+ try {
+ const success = await deletePost(postId);
+
+
+ if (success) {
+ console.log('Post deleted successfully');
+
+ } else {
+ console.error('Failed to delete post');
+ }
+ } catch (error) {
+ console.error('Error deleting post:', error);
+ }
+ };
+
+ const handleDeleteComment = async () => {
+ setIsMenuVisible(false);
+ console.log('deleteComment function called');
+
+ // If there's a callback provided, use it instead of calling the API directly
+ if (onCommentDeleted) {
+ onCommentDeleted(commentId);
+ return;
+ }
+
+ // Only call the API directly if no callback is provided
+ try {
+ const success = await deleteComment(postId, commentId);
+ if (success) {
+ console.log('Comment deleted successfully');
+ } else {
+ console.error('Failed to delete comment');
+ }
+ } catch (error) {
+ console.error('Error deleting comment:', error);
+ }
+ };
+
+ const handleReport = () => {
+ setIsMenuVisible(false);
+ console.log('reportComment function called, and reported');
+ setModal(`Reported`,
+ <>
+
+ Thank you for reporting this. Our team will review it shortly.
+ >
+ );
+ setIsMenuVisible(false);
+ openModal();
+
+ setTimeout(() => {
+ closeModal();
+ }, 1500);
+
+ };
+
+ const getClickHandler = () => {
+ switch (clickable) {
+ case "Modal":
+ return showModal;
+ case "CommentModal":
+ return showCommentModal;
+ case "Delete":
+ return handleDeletePost;
+ case "DeleteComment":
+ return handleDeleteComment;
+ case "Report":
+ return handleReport;
+ case "ReportComment":
+ return handleReport;
+ default:
+ return undefined;
+ }
+ };
+
+
+
+
+
+ if (clickable) {
+ return (
+ -
+
+ {children && }
+
+ );
+ }
-const MenuItem = ({ icon, text, children, linkTo = '#nogo' }) => {
return (
-
diff --git a/src/components/navigation/index.js b/src/components/navigation/index.js
index b31393a8..c6c91d0f 100644
--- a/src/components/navigation/index.js
+++ b/src/components/navigation/index.js
@@ -1,38 +1,96 @@
import { NavLink } from 'react-router-dom';
import CohortIcon from '../../assets/icons/cohortIcon';
-import HomeIcon from '../../assets/icons/homeIcon';
import ProfileIcon from '../../assets/icons/profileIcon';
import useAuth from '../../hooks/useAuth';
import './style.css';
+import { useState } from 'react';
+import ProfileIconFilled from '../../assets/icons/profileIconFilled';
+import HomeIconFilled from '../../assets/icons/homeIconFilled';
+import HomeIcon from '../../assets/icons/homeIcon';
+import CohortIconFill from '../../assets/icons/cohortIcon-fill';
+import ExcersicesIconFilled from '../../assets/icons/excersicesIconFilled';
+import ExcersicesIcon from '../../assets/icons/excersicesIcon';
+import NotesIconFilled from '../../assets/icons/notesIconFilled';
+import NotesIcon from '../../assets/icons/notesIcon';
+import LogsIconFilled from '../../assets/icons/logsIconFilled';
+import LogsIcon from '../../assets/icons/logsIcon';
+import jwtDecode from 'jwt-decode';
+import { useUserRoleData } from '../../context/userRole.';
const Navigation = () => {
const { token } = useAuth();
-
+ const [active, setActive] = useState(1)
+ const{userRole} = useUserRoleData()
+
if (!token) {
return null;
}
+ let userIdFromToken = null;
+ try {
+ const decoded = jwtDecode(token);
+ userIdFromToken = decoded.userId;
+ } catch (err) {
+ console.error("Error when decoding by navigation", err);
+ }
+
return (
);
diff --git a/src/components/navigation/style.css b/src/components/navigation/style.css
index 91849135..2b443e74 100644
--- a/src/components/navigation/style.css
+++ b/src/components/navigation/style.css
@@ -27,3 +27,33 @@ nav svg {
nav p {
line-height: 24px;
}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 10px;
+ background-color: white;
+ border-radius: 8px;
+ text-decoration: none;
+ transition: background-color 0.3s ease;
+}
+
+.nav-item.active {
+ background: #E6EBF5;
+}
+
+
+.no-line {
+ border: none;
+}
+
+.border-line {
+ border-bottom: 1px solid var(--color-blue5);
+}
+
+nav li:hover a {
+background: #F5FAFF;
+border-radius: 5px;
+
+}
\ No newline at end of file
diff --git a/src/components/post/dropdown/index.js b/src/components/post/dropdown/index.js
new file mode 100644
index 00000000..408ae42b
--- /dev/null
+++ b/src/components/post/dropdown/index.js
@@ -0,0 +1,42 @@
+import { useState, useRef, useEffect } from 'react';
+import { CascadingMenuPost } from './menu/index';
+
+const MenuPost = ({ edit=false, report=false, del=false, menuVisible, postText, postId, name, onPostDeleted, commentText, commentId, post }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+ const menuRef = useRef(null);
+
+ // Close menu when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsMenuVisible(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
{
+ setIsMenuVisible(!isMenuVisible);
+ }}>
+
+ β’
+ β’
+ β’
+
+
+
+ {isMenuVisible && }
+
+
+
+
+ );
+};
+
+export default MenuPost;
diff --git a/src/components/post/dropdown/menu/index.js b/src/components/post/dropdown/menu/index.js
new file mode 100644
index 00000000..d18299bd
--- /dev/null
+++ b/src/components/post/dropdown/menu/index.js
@@ -0,0 +1,40 @@
+import { useState } from 'react';
+import EditIcon from '../../../../assets/icons/EditIcon';
+import DeleteIcon from '../../../../assets/icons/deleteIcon';
+import Menu from '../../../menu';
+import MenuItem from '../../../menu/menuItem';
+import './style.css';
+import ReportIcon from '../../../../assets/icons/reporticon';
+
+const ProfileCirclePost = ({ initials, menuVisible, postText, postId, name }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+
+
+
+ return (
+ setIsMenuVisible(!isMenuVisible)}>
+ {isMenuVisible &&
}
+
+
+
+ );
+};
+
+export const CascadingMenuPost = ({ editPost, deletePost, reportPost, postText, postId, name, isMenuVisible, setIsMenuVisible }) => {
+ return (
+
+ );
+};
+
+export default ProfileCirclePost;
diff --git a/src/components/post/dropdown/menu/style.css b/src/components/post/dropdown/menu/style.css
new file mode 100644
index 00000000..467e0d43
--- /dev/null
+++ b/src/components/post/dropdown/menu/style.css
@@ -0,0 +1,98 @@
+.user {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ gap: 12px;
+}
+
+
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.profile-circle{
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.user-name {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color:#000046;
+
+}
+
+.user-role {
+ font-size: 16px;
+ color:#64648C
+}
+
+.edit-icon-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.icon-button {
+ width: 40px;
+ height: 40px;
+ margin-top: 4px;
+ margin-left: 4px;
+ background-color: #F0F5FA;
+ border: none;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.dots {
+ display: flex;
+ gap: 4px;
+ font-size: 18px;
+ color:#64648C;
+
+}
+
+.dot {
+ line-height: 1;
+}
+
+/* Reset and override all menu positioning */
+.edit-icon-wrapper .menu-left {
+ position: absolute !important;
+ left: auto !important;
+ right: 100% !important; /* Position to the left of trigger */
+ top: 0 !important; /* Align with trigger top */
+ transform: translateX(-10px) !important; /* Add small gap */
+ z-index: 1000 !important;
+}
+
+/* Complete override of menu positioning with maximum specificity */
+.edit-icon-wrapper .menu-left .menu {
+ position: relative !important;
+ left: 0 !important;
+ top: 0 !important;
+ right: auto !important;
+}
+
+.edit-icon-wrapper .menu-left .menu ul {
+ position: relative !important;
+ left: 0 !important;
+ top: 0 !important;
+ right: auto !important;
+ transform: none !important;
+}
diff --git a/src/components/post/index.js b/src/components/post/index.js
index 337ca5a6..2caf22d0 100644
--- a/src/components/post/index.js
+++ b/src/components/post/index.js
@@ -1,55 +1,203 @@
-import useModal from '../../hooks/useModal';
+import { useEffect, useRef, useState } from 'react';
import Card from '../card';
import Comment from '../comment';
-import EditPostModal from '../editPostModal';
import ProfileCircle from '../profileCircle';
+
+import CreateComment from '../createComment';
+import HeartIcon from '../../assets/icons/heartIcon';
+import HeartIconFilled from '../../assets/icons/heartIconFilled';
+import CommentBubbleIcon from '../../assets/icons/commentBubbleIcon';
+import CommentBubbleIconFilled from '../../assets/icons/commentBubbleIconFilled';
import './style.css';
+import { usePosts } from '../../context/posts';
+
+import MenuPost from './dropdown';
+import jwtDecode from 'jwt-decode';
+import useAuth from '../../hooks/useAuth';
+
+const Post = ({ post }) => {
+ const { getUserLikedPosts, toggleLike } = usePosts();
+ const commentInputRef = useRef(null);
+ const [localComments, setLocalComments] = useState((post.comments || []).reverse());
+ const [isLiked, setIsLiked] = useState(false);
+ const [likeCount, setLikeCount] = useState(post.likesCount || post.likes || 0);
+ const [isAnimating, setIsAnimating] = useState(false);
+ const [isCommentHovered, setIsCommentHovered] = useState(false);
+ const { token } = useAuth();
+ const { userId } = jwtDecode(token || localStorage.getItem('token')) || {};
+
+ const authorName = post.user.profile
+ ? `${post.user.profile.firstName || 'Unknown'} ${post.user.profile.lastName || 'User'}`
+ : 'Unknown User';
+ const userInitials = authorName.match(/\b(\w)/g);
+
+ const isLikedInitial = () => {
+ const likedPosts = getUserLikedPosts();
+ if (!Array.isArray(likedPosts)) {
+ setIsLiked(false);
+ return;
+ }
+
+ const liked = likedPosts.some((likedPost) => likedPost.id === post.id);
+ setIsLiked(liked);
+ };
+
+ useEffect(() => {
+ isLikedInitial();
+ }, [post.id]); // Remove user dependency since we get it from context
+
+ const formatDate = (dateString) => {
+ if (!dateString) return 'Unknown date';
+ const d = new Date(dateString);
+ if (isNaN(d.getTime())) return 'Unknown date';
+ const months = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+ const day = String(d.getDate()).padStart(2, '0');
+ const month = months[d.getMonth()];
+ const hours = String(d.getHours()).padStart(2, '0');
+ const minutes = String(d.getMinutes()).padStart(2, '0');
+ return `${day} ${month} at ${hours}.${minutes}`;
+ };
-const Post = ({ name, date, content, comments = [], likes = 0 }) => {
- const { openModal, setModal } = useModal();
+ const comments = Array.isArray(localComments) ? localComments : [];
+ const handleCommentClick = () => {
+ if (commentInputRef.current) {
+ commentInputRef.current.focus();
+ }
+ };
+
+ const handleCommentAdded = (newComment) => {
+ // Add the new comment to the local state
+ setLocalComments(prevComments => [...prevComments, newComment]);
+ };
+
+ const handleCommentDeleted = (deletedCommentId) => {
+ // Remove the deleted comment from the local state
+ setLocalComments(prevComments => prevComments.filter(comment => comment.id !== deletedCommentId));
+ };
- const userInitials = name.match(/\b(\w)/g);
+ const handleLikeClick = async () => {
+ // Trigger animation
+ setIsAnimating(true);
+
+ // Store current state in case we need to revert
+ const previousLikedState = isLiked;
+ const previousLikeCount = likeCount;
+
+ // Optimistically update UI
+ setIsLiked(prev => !prev);
+ setLikeCount(prev => previousLikedState ? prev - 1 : prev + 1);
+
+ // Reset animation after a short delay
+ setTimeout(() => {
+ setIsAnimating(false);
+ }, 300);
- const showModal = () => {
- setModal('Edit post', );
- openModal();
+ try {
+ const success = await toggleLike(post.id, !previousLikedState);
+ if (!success) {
+ // Revert optimistic updates on error
+ setIsLiked(previousLikedState);
+ setLikeCount(previousLikeCount);
+ }
+ } catch (error) {
+ // Revert optimistic updates on error
+ setIsLiked(previousLikedState);
+ setLikeCount(previousLikeCount);
+ console.error('Failed to update like state:', error);
+ }
};
return (
-
-
+
+
-
-
{name}
-
{date}
+
+
{authorName}
+
{formatDate(post.timeCreated)}
+ {(post.timeCreated === post.timeUpdated) ? null : (
Edited
)}
+
+
+
-
-
-
-
- {content}
+
-
-
-
Like
-
Comment
+
+
+
+
- {!likes && 'Be the first to like this'}
+
+ {likeCount === 0 ? 'Be the first to like this' : `${likeCount} ${likeCount === 1 ? 'like' : 'likes'}`}
+
-
- {comments.map((comment) => (
-
- ))}
+ {comments.length > 2 && (
+ See previous comments
+ )}
+
+
+ {comments.map((comment, idx) => {
+ const commentAuthorName = comment.user?.profile
+ ? `${comment.user.profile.firstName || 'Unknown'} ${comment.user.profile.lastName || 'User'}`
+ : 'Unknown User';
+
+ return (
+
+ );
+ })}
+
diff --git a/src/components/post/style.css b/src/components/post/style.css
index 3eff5afc..827bf876 100644
--- a/src/components/post/style.css
+++ b/src/components/post/style.css
@@ -1,50 +1,159 @@
.post {
display: grid;
- row-gap: 20px;
+ row-gap: 16px;
}
-.post-details {
+/* Header */
+.post__header {
display: grid;
- grid-template-columns: 56px auto 48px;
- column-gap: 20px;
+ grid-template-columns: 56px auto 40px;
+ column-gap: 16px;
+ align-items: center
}
-.post-user-name {
- padding-top: 4px;
+.post__meta {
+ display: grid;
+ row-gap: 2px;
}
-.post-user-name p {
+.post__author {
font-weight: 600;
font-size: 1.1rem;
}
-.post-user-name small {
- color: #64648c;
+.post__date {
+ color: var(--color-blue1);
}
-.edit-icon {
- border-radius: 50%;
+.post__menu {
width: 40px;
height: 40px;
- background: #f0f5fa;
+ border-radius: 8px;
+ border: 0;
+ background: var(--color-blue5);
+ color: var(--color-blue1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer
}
-.edit-icon p {
- text-align: center;
- font-size: 20px;
+/* Content */
+.post__content p {
+ line-height: 1.6;
}
-.post-interactions-container {
- display: grid;
- grid-template-columns: 1fr 3fr;
- padding: 20px 10px;
+/* Actions row */
+.post__actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0
+}
+
+.post__actions-left {
+ display: flex;
+ gap: 12px;
+}
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ border: none;
+ background: #eef3f9;
+ /* muted */
+ color: var(--color-blue1);
+ padding: 8px 12px;
+ border-radius: 999px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.pill:hover {
+ background: #d9e7f4;
+}
+
+.pill svg {
+ transition: all 0.2s ease;
+}
+
+.pill--animating svg {
+ animation: heartBounce 0.4s ease-out;
+}
+
+/* New action button styles to match the photo design */
+.action-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ border: none;
+ background: transparent;
+ color: #65676B;
+ padding: 8px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.action-button:hover {
+ background: #f2f3f5;
}
-.post-interactions {
+.action-button svg {
+ width: 48px;
+ height: 48px;
+ transition: all 0.2s ease;
+}
+
+.action-button--animating svg {
+ animation: heartBounce 0.4s ease-out;
+}
+
+.action-button--liked {
+ color: #c00000;
+}
+
+.action-button--liked svg {
+ fill: #c00000;
+}
+
+@keyframes heartBounce {
+ 0% {
+ transform: translateY(0) scale(1);
+ }
+ 30% {
+ transform: translateY(-3px) scale(1.1);
+ }
+ 60% {
+ transform: translateY(1px) scale(1.05);
+ }
+ 100% {
+ transform: translateY(0) scale(1);
+ }
+}
+
+.pill svg {
+ width: 18px;
+ height: 18px;
+}
+
+.post__likes-hint {
+ color: var(--color-blue1);
+}
+
+/* Comments section spacing */
+.post__comments {
display: grid;
- grid-template-columns: 1fr 1fr;
+ row-gap: 12px;
}
-.post-interactions-container p {
+.post__see-previous {
+ color: var(--color-blue1);
+ font-size: 0.95rem;
text-align: right;
-}
+}
\ No newline at end of file
diff --git a/src/components/posts/index.js b/src/components/posts/index.js
index 79756c41..a1523f54 100644
--- a/src/components/posts/index.js
+++ b/src/components/posts/index.js
@@ -1,27 +1,18 @@
-import { useEffect, useState } from 'react';
import Post from '../post';
-import { getPosts } from '../../service/apiClient';
+import { usePosts } from '../../context/posts';
const Posts = () => {
- const [posts, setPosts] = useState([]);
+ const { posts, loading } = usePosts();
- useEffect(() => {
- getPosts().then(setPosts);
- }, []);
+ if (loading) {
+ return Loading posts...
;
+ }
return (
<>
- {posts.map((post) => {
- return (
-
- );
- })}
+ {posts.map((post) => (
+
+ ))}
>
);
};
diff --git a/src/components/posts/style.css b/src/components/posts/style.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/profile-icon-searchStudentView/index.js b/src/components/profile-icon-searchStudentView/index.js
new file mode 100644
index 00000000..c08abfa2
--- /dev/null
+++ b/src/components/profile-icon-searchStudentView/index.js
@@ -0,0 +1,82 @@
+
+import Popup from 'reactjs-popup';
+import './style.css';
+import SeeProfile from '../seeProfile';
+import { useNavigate } from 'react-router-dom';
+const UserIconStudentView = ({ id, initials, firstname, lastname, role}) => {
+ const navigate = useNavigate();
+
+ 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];
+ };
+
+ const backgroundColor = getColorFromInitials(initials);
+
+ const viewProfile = () => {
+ navigate(`/profile`); // MΓ₯ legge til ID - senere
+ }
+
+ return (
+
+
+
+
+
{firstname} {lastname}
+
{role}
+
+
+
+
+
+
+
+
+ β’
+ β’
+ β’
+
+
+
+
+ } position="left center"
+ closeOnDocumentClick
+ arrow={false}>
+
+
+
+ )
+}
+
+
+export default UserIconStudentView;
+
+
+
\ No newline at end of file
diff --git a/src/components/profile-icon-searchStudentView/style.css b/src/components/profile-icon-searchStudentView/style.css
new file mode 100644
index 00000000..cbdf3e6b
--- /dev/null
+++ b/src/components/profile-icon-searchStudentView/style.css
@@ -0,0 +1,71 @@
+.user {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.profile-circle{
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.user-name {
+ margin: 0;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.pop-up {
+ position: absolute;
+ top: 100%; /* under .edit-icon */
+ right: 0; /* hΓΈyrejustert med ikonet */
+ margin-top: 8px;
+ z-index: 100;
+}
+
+
+.edit-icon-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.icon-button {
+ width: 40px;
+ height: 40px;
+ margin-top: 4px;
+ margin-left: 4px;
+ background-color: #F0F5FA;
+ border: none;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.dots {
+ display: flex;
+ gap: 4px;
+ font-size: 18px;
+ color:#64648C;
+
+}
+
+.dot {
+ line-height: 1;
+}
\ No newline at end of file
diff --git a/src/components/profile-icon-searchTeacherView/cascadinuMenuSearch/index.js b/src/components/profile-icon-searchTeacherView/cascadinuMenuSearch/index.js
new file mode 100644
index 00000000..06c672ec
--- /dev/null
+++ b/src/components/profile-icon-searchTeacherView/cascadinuMenuSearch/index.js
@@ -0,0 +1,22 @@
+
+import Menu from "../../menu"
+import MenuItem from "../../menu/menuItem"
+import DeleteIcon from "../../../assets/icons/deleteIcon"
+import NotesIcon from "../../../assets/icons/notesIcon"
+import CohortIcon from "../../../assets/icons/cohortIcon"
+import ProfileIcon from "../../../assets/icons/profileIcon"
+
+const CascadingMenuSearch = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default CascadingMenuSearch
\ No newline at end of file
diff --git a/src/components/profile-icon-searchTeacherView/index.js b/src/components/profile-icon-searchTeacherView/index.js
new file mode 100644
index 00000000..f1fa0044
--- /dev/null
+++ b/src/components/profile-icon-searchTeacherView/index.js
@@ -0,0 +1,89 @@
+
+import './style.css';
+import { useNavigate } from 'react-router-dom';
+import CascadingMenuSearch from './cascadinuMenuSearch';
+import { useEffect, useRef, useState } from 'react';
+const UserIconTeacherView = ({ id, initials, firstname, lastname, role, menuVisible}) => {
+
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+ const menuRef = useRef(null);
+ const navigate = useNavigate();
+ 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];
+ };
+
+ const backgroundColor = getColorFromInitials(initials);
+
+ const viewProfile = () => {
+ navigate(`/profile`); // MΓ₯ legge til ID - senere
+ }
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsMenuVisible(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+
+ return (
+
+
+
+
+
{firstname} {lastname}
+
{role}
+
+
+
+
+
+
+
+
+
+
setIsMenuVisible(!isMenuVisible)}>
+
+ β’
+ β’
+ β’
+
+
+
+
+
+ {isMenuVisible && }
+
+
+
+
+ )
+}
+
+export default UserIconTeacherView;
diff --git a/src/components/profile-icon-searchTeacherView/style.css b/src/components/profile-icon-searchTeacherView/style.css
new file mode 100644
index 00000000..edb77975
--- /dev/null
+++ b/src/components/profile-icon-searchTeacherView/style.css
@@ -0,0 +1,89 @@
+.user {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.user-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between; /* navnet til venstre, knappene til hΓΈyre */
+}
+
+.user-name {
+ margin: 0;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.pop-up {
+ position: absolute;
+ top: 100%; /* under .edit-icon */
+ right: 0; /* hΓΈyrejustert med ikonet */
+ margin-top: 8px;
+ z-index: 100;
+}
+
+.profile-circle{
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.edit-icon-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.icon-button {
+ width: 40px;
+ height: 40px;
+ margin-top: 4px;
+ margin-left: 4px;
+ background-color: #F0F5FA;
+ border: none;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.dots {
+ display: flex;
+ gap: 4px;
+ font-size: 18px;
+ color:#64648C;
+
+}
+
+.dot {
+ line-height: 1;
+}
+
+.buttons {
+ width: 120px !important;
+ text-align: center !important;
+ padding: 2px 0 !important;
+ font-size: 16px !important;
+ color: #64648C ;
+}
+
+.cascading-menu-container {
+ position: absolute;
+ z-index: 1000;
+}
\ No newline at end of file
diff --git a/src/components/profile-icon-teacherView/editIconTeacher/index.js b/src/components/profile-icon-teacherView/editIconTeacher/index.js
new file mode 100644
index 00000000..50535c8d
--- /dev/null
+++ b/src/components/profile-icon-teacherView/editIconTeacher/index.js
@@ -0,0 +1,40 @@
+import { useState, useRef, useEffect } from 'react';
+import { CascadingMenu } from '../../profileCircle';
+
+const EditIconTeacher = ({ id, initials, menuVisible }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
+ const menuRef = useRef(null);
+
+ // Lukk meny ved klikk utenfor
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setIsMenuVisible(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
setIsMenuVisible(!isMenuVisible)}>
+
+ β’
+ β’
+ β’
+
+
+
+ {isMenuVisible && }
+
+
+
+
+ );
+};
+
+export default EditIconTeacher;
diff --git a/src/components/profile-icon-teacherView/index.js b/src/components/profile-icon-teacherView/index.js
new file mode 100644
index 00000000..68dffe59
--- /dev/null
+++ b/src/components/profile-icon-teacherView/index.js
@@ -0,0 +1,52 @@
+import EditIconTeacher from './editIconTeacher';
+import './style.css';
+
+
+const ProfileIconTeacher = ({id, initials, firstname, lastname, role}) => {
+
+ 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];
+ };
+
+
+
+ const backgroundColor = getColorFromInitials(initials);
+
+
+
+ return (
+
+
+
+
+
{firstname} {lastname}
+
{role}
+
+
+
+ )
+}
+
+export default ProfileIconTeacher;
\ No newline at end of file
diff --git a/src/components/profile-icon-teacherView/style.css b/src/components/profile-icon-teacherView/style.css
new file mode 100644
index 00000000..61947779
--- /dev/null
+++ b/src/components/profile-icon-teacherView/style.css
@@ -0,0 +1,77 @@
+.user {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.profile-circle{
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.user-name {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color:#000046;
+
+}
+
+.user-role {
+ font-size: 16px;
+ color:#64648C
+}
+
+.edit-icon-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.icon-button {
+ width: 48px;
+ height: 48px;
+ margin-top: 4px;
+ margin-left: 4px;
+ background-color: #F0F5FA;
+ border: none;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.dots {
+ display: flex;
+ gap: 4px;
+ font-size: 18px;
+ color:#64648C;
+
+}
+
+.dot {
+ line-height: 1;
+}
+
+.menu-left {
+ position: absolute;
+ left: -500px; /* juster avstanden etter behov */
+ top: 0;
+ z-index: 1000;
+}
diff --git a/src/components/profile-icon/index.js b/src/components/profile-icon/index.js
new file mode 100644
index 00000000..e44613d1
--- /dev/null
+++ b/src/components/profile-icon/index.js
@@ -0,0 +1,73 @@
+
+import Popup from 'reactjs-popup';
+import './style.css';
+import SeeProfile from '../seeProfile';
+const UserIcon = ({ id, initials, firstname, lastname, role}) => {
+
+ 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];
+ };
+
+ const backgroundColor = getColorFromInitials(initials);
+
+ return (
+
+
+
+
+
{firstname} {lastname}
+
{role}
+
+
+
+
+ β’
+ β’
+ β’
+
+
+
+
+ } position="left center"
+ closeOnDocumentClick
+ arrow={false}>
+
+
+
+
+ )
+}
+
+
+export default UserIcon;
+
+
+
\ No newline at end of file
diff --git a/src/components/profile-icon/style.css b/src/components/profile-icon/style.css
new file mode 100644
index 00000000..c6a730e6
--- /dev/null
+++ b/src/components/profile-icon/style.css
@@ -0,0 +1,72 @@
+.user {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ gap: 12px;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+
+.profile-circle{
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.user-name {
+ margin: 0;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.pop-up {
+ position: absolute;
+ top: 100%; /* under .edit-icon */
+ right: 0; /* hΓΈyrejustert med ikonet */
+ margin-top: 8px;
+ z-index: 100;
+}
+
+
+.edit-icon-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.icon-button {
+ width: 40px;
+ height: 40px;
+ margin-top: 4px;
+ margin-left: 4px;
+ background-color: #F0F5FA;
+ border: none;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.dots {
+ display: flex;
+ gap: 4px;
+ font-size: 18px;
+ color:#64648C;
+
+}
+
+.dot {
+ line-height: 1;
+}
\ No newline at end of file
diff --git a/src/components/profileCircle/index.js b/src/components/profileCircle/index.js
index 7dc5a614..2d7ef425 100644
--- a/src/components/profileCircle/index.js
+++ b/src/components/profileCircle/index.js
@@ -10,12 +10,12 @@ import Menu from '../menu';
import MenuItem from '../menu/menuItem';
import './style.css';
-const ProfileCircle = ({ initials }) => {
- const [isMenuVisible, setIsMenuVisible] = useState(false);
+const ProfileCircle = ({ id, initials, menuVisible }) => {
+ const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false);
return (
setIsMenuVisible(!isMenuVisible)}>
- {isMenuVisible &&
}
+ {isMenuVisible &&
}
{initials}
@@ -24,10 +24,11 @@ const ProfileCircle = ({ initials }) => {
);
};
-const CascadingMenu = () => {
+export const CascadingMenu = ({ id }) => {
+
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..ddfbc6cf
--- /dev/null
+++ b/src/pages/addStudent/style.css
@@ -0,0 +1,212 @@
+.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;
+}
+
+.dropdown-section {
+ width: 100%;
+}
+
+.inputwrapper {
+ position: relative;
+ width: 100%;
+ margin-top: 16px;
+ font-family: inherit;
+}
+
+.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;
+}
\ 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
+
+
+
+
+ >
+ )
+}
+
+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..ad4f9917
--- /dev/null
+++ b/src/pages/cohort/index.js
@@ -0,0 +1,152 @@
+import Students from "./students";
+
+import Teachers from './teachers';
+import Exercises from "./exercises";
+import { useUserRoleData } from "../../context/userRole.";
+import TeacherCohort from "./teacherCohort";
+import jwtDecode from "jwt-decode";
+import useAuth from "../../hooks/useAuth";
+import { get, getUserById } from "../../service/apiClient";
+import { useEffect, useState } from "react";
+
+
+
+const Cohort = () => {
+ const {userRole, setUserRole} = useUserRoleData()
+ const { token } = useAuth();
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ setUserRole(decodedToken.roleId);
+ } catch (error) {
+ console.error('Invalid token in Cohort component:', error);
+ }
+
+ const [studentsLoading, setStudentsLoading] = useState(true);
+ const [teachersLoading, setTeachersLoading] = useState(true);
+ const [cohortsLoading, setCohortsLoading] = useState(true);
+
+ const [teachers, setTeachers] = useState([]);
+
+ const [students, setStudents] = useState([]);
+ const [course, setcourse] = useState([]);
+ const [cohort, setCohort] = useState("");
+ const [cohorts, setCohorts] = useState([])
+
+ useEffect(() => {
+ setCohortsLoading(true)
+ async function fetchCohorts() {
+ try {
+ const response = await get("cohorts");
+ setCohorts(response.data.cohorts);
+ } catch (error) {
+ console.error("Error fetching cohorts:", error);
+ } finally {
+ setCohortsLoading(false)
+ }
+ }
+
+ fetchCohorts();
+ }, []);
+
+
+ useEffect(() => {
+ setTeachersLoading(true);
+ setStudentsLoading(true);
+ async function fetchData() {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ console.error('No token found');
+ return;
+ }
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token);
+ userId = decodedToken.userId;
+ } catch (decodeError) {
+ console.error('Invalid token:', decodeError);
+ return;
+ }
+
+ const user = await getUserById(userId);
+ const data = await get(`cohorts/${user.profile.cohort.id}`);
+
+ // set cohort
+ const cohort = data.data.cohort;
+ setCohort(cohort);
+
+ // set teachers
+ const teachers = data.data.cohort.profiles.filter((userid) => userid?.role?.name === "ROLE_TEACHER");
+ setTeachers(teachers || []);
+
+ // students
+ const students = data.data.cohort.profiles.filter((profileid) => profileid?.role?.name === "ROLE_STUDENT");
+ setStudents(students || []);
+
+ // course
+ const course = data.data.cohort.course;
+ setcourse(course || "");
+
+ } catch (error) {
+ console.error('fetchData() in cohort/teachers/index.js:', error);
+ } finally {
+ setStudentsLoading(false);
+ setTeachersLoading(false);
+ }
+ }
+
+ fetchData();
+ }, []);
+
+ function getInitials(profile) {
+ if (!profile.firstName || !profile.lastName) return "NA";
+ const firstNameParts = profile.firstName.trim().split(/\s+/) || ''; // split by any number of spaces
+ const lastNameInitial = profile.lastName.trim().charAt(0);
+
+ const firstNameInitials = firstNameParts.map(name => name.charAt(0));
+
+ return (firstNameInitials.join('') + lastNameInitial).toUpperCase();
+ }
+
+ if (studentsLoading || teachersLoading || cohortsLoading) {
+ return (
+
+ )
+ }
+
+ return (
+ <>
+ {userRole === 2 ? (
+ <>
+
+
+
+
+
+ >):(
+
+ )
+ }
+
+ >
+ )
+
+}
+
+export default Cohort;
+
+
diff --git a/src/pages/cohort/students/index.js b/src/pages/cohort/students/index.js
new file mode 100644
index 00000000..69d02185
--- /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';
+// import { useState } from "react";
+
+
+function Students({ students, getInitials, course, cohort }) {
+
+ return (
+
+
+
+
+ {course && (
+
+
+ {course.name === "Software Development" && }
+ {course.name === "Front-End Development" && }
+ {course.name === "Data Analytics" && }
+
+
+
+
{course.name}, Cohort {cohort.id}
+
+
+
+ {`${cohort.startDate} - ${cohort.endDate}`}
+
+
+ )}
+
+
+ {students.map((student) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default Students;
\ No newline at end of file
diff --git a/src/pages/cohort/students/student/index.js b/src/pages/cohort/students/student/index.js
new file mode 100644
index 00000000..51277c6b
--- /dev/null
+++ b/src/pages/cohort/students/student/index.js
@@ -0,0 +1,19 @@
+import UserIcon from "../../../../components/profile-icon";
+
+const Student = ({ id, initials, firstName, lastName, role }) => {
+ 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..2250c10c
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/cohortsList/index.js
@@ -0,0 +1,59 @@
+
+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";
+
+
+const CohortsList= ({ onSelect, setSelectedCohort , cohorts}) => {
+ const [selectedCohortId, setSelectedCohortId] = useState(null);
+
+
+ const handleClick = (cohort) => {
+ setSelectedCohortId(cohort.id);
+ setSelectedCohort(cohort)
+ 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..25b2f0f6
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/index.js
@@ -0,0 +1,102 @@
+import { useState } from "react"
+import SearchIcon from "../../../assets/icons/searchIcon"
+import EditIconCohortTeacher from "../../../components/editIconCohortTeacher"
+import TextInput from "../../../components/form/textInput"
+import CohortsList from "./cohortsList"
+import './style.css';
+import StudentList from "./studentList"
+import EditIconCouse from "../../../components/editIconCourse"
+import CourseIcon from "../../../components/courseIcon"
+import { useNavigate } from "react-router-dom"
+
+
+const TeacherCohort = ({cohorts}) => {
+ const [searchVal, setSearchVal] = useState('');
+ const [selectedProfiles, setSelectedProfiles] = useState([]);
+ const[selectedCohort, setSelectedCohort] = useState(null);
+ const navigate = useNavigate()
+
+
+
+ const onChange = (e) => {
+ setSearchVal(e.target.value);
+ };
+
+
+ return (
+ <>
+ {cohorts.length > 0 ? (
+
+
+
Cohorts
+ Students
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setSelectedProfiles(profiles)} />
+
+
+
+
+
+
+ {selectedCohort !== null ? (
+ <>
+
+ >
+ ): (<>Select a course
>)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
):(
+
+ )}
+
+ >
+ )
+}
+
+export default TeacherCohort
\ No newline at end of file
diff --git a/src/pages/cohort/teacherCohort/studentList/index.js b/src/pages/cohort/teacherCohort/studentList/index.js
new file mode 100644
index 00000000..efa362a4
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/studentList/index.js
@@ -0,0 +1,21 @@
+import ProfileIconTeacher from "../../../../components/profile-icon-teacherView";
+
+const StudentList = ({ profiles }) => {
+ 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..7efe1ed6
--- /dev/null
+++ b/src/pages/cohort/teacherCohort/style.css
@@ -0,0 +1,191 @@
+
+.cohort-card {
+ width: 88%;
+ height: 100%;
+ position: absolute;
+ top: 120px;
+ left: 175px;
+ background-color: #FFFFFF;
+ border: 1px solid #E6EBF5;
+ border-radius: 8px;
+ padding: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ display: flex;
+ flex-direction: column;
+
+
+}
+
+.cohort-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+ border-bottom: 1px solid var(--color-blue5);
+
+}
+
+.header-titles {
+ display: flex;
+ gap: 350px;
+}
+
+.header-titles h3 {
+ font-size: 32px;
+ color: #000046;
+
+}
+
+.search-bar {
+ margin-bottom: 10px;
+}
+
+
+.sections-wrapper {
+ display: flex;
+ flex-direction: row;
+ height: calc(100vh - 80px); /* justerer for header-hΓΈyden */
+ width: 100%;
+}
+
+.cohorts-section {
+ position: relative;
+ width: 500px;
+ padding: 24px;
+ border-right: 1px solid var(--color-blue5);
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ height: 100%; /* viktig for at linjen skal dekke hele hΓΈyden */
+}
+
+.cohort-list {
+ overflow-y: auto;
+ height: 100%;
+ width: 106%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ scrollbar-width: thin;
+}
+
+
+
+.student-list {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ width: 101%;
+ min-height: auto;
+ overflow-y: auto;
+ scrollbar-width: thin;
+
+}
+
+.students-section {
+ position: relative;
+ width: 100%;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.students {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+}
+
+
+
+.selected-course {
+ flex: 1; /* tar opp all tilgjengelig plass til venstre */
+}
+
+
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 16px; /* mellomrom mellom knapp og ikon */
+}
+
+
+.add-student-button button {
+ height: 56px;
+ width: 166px;
+ padding: 0 24px;
+ background-color: #F0F5FA;
+ border: none;
+ color: #64648C;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ box-sizing: border-box;
+ margin-right: 40px;
+}
+
+
+.edit-icon-course {
+ font-size: 24px;
+ color: #64648C;
+}
+
+
+.add-cohort {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px; /* gir luft mellom knapp og ikon */
+
+
+}
+
+.add-cohort-button {
+ width: auto;
+ flex-grow: 2;
+}
+
+
+.add-cohort-button button{
+ width: 199px;
+ height: 56px;
+ padding: 14px 24px;
+ gap: 8px; /* hvis du har ikon og tekst inni */
+ border-radius: 8px;
+ background-color: #F0F5FA;
+ border: none;
+ cursor: pointer;
+ font-size: 20px;
+ color: #64648C;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transform: rotate(0deg); /* angle: 0 deg */
+ position: relative; /* ikke absolute med top/left med mindre nΓΈdvendig */
+}
+
+
+.edit-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.divider {
+ border-bottom: 1px solid var(--color-blue5);
+
+}
+
+.cohort-teacher-loading {
+ margin-top: 20px;
+ margin-left: 20px;
+}
\ 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..8be4ebd1
--- /dev/null
+++ b/src/pages/cohort/teachers/index.js
@@ -0,0 +1,31 @@
+import Card from "../../../components/card";
+import './style.css';
+import Teacher from "./teacher";
+
+
+const Teachers = ({ teachers, getInitials }) => {
+
+ return (
+
+
+
+
+
+ {teachers.map((teacher, index) => (
+
+ ))}
+
+
+
+ );
+}
+
+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..1311a5ad
--- /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 #ccc;
+ 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..00ca77fa
--- /dev/null
+++ b/src/pages/cohort/teachers/teacher/index.js
@@ -0,0 +1,19 @@
+import UserIcon from "../../../../components/profile-icon";
+
+const Teacher = ({ initials, firstName, lastName, role }) => {
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default Teacher;
diff --git a/src/pages/dashboard/cohorts/index.js b/src/pages/dashboard/cohorts/index.js
new file mode 100644
index 00000000..40fd5aae
--- /dev/null
+++ b/src/pages/dashboard/cohorts/index.js
@@ -0,0 +1,58 @@
+
+import Card from "../../../components/card"
+import SoftwareLogo from "../../../assets/icons/software-logo"
+import FrontEndLogo from "../../../assets/icons/frontEndLogo"
+import DataAnalyticsLogo from "../../../assets/icons/dataAnalyticsLogo"
+import './style.css';
+
+const Cohorts = ({cohorts}) => {
+
+ return (
+ <>
+
+ Cohorts
+
+ {cohorts !== null ? (
+
+ {cohorts.map((cohort, index) => {
+ return (
+ -
+ {cohort.course === null ? <>> :
+
+
+
+ {cohort.course.name === "Software Development" && }
+ {cohort.course.name === "Front-End Development" && }
+ {cohort.course.name === "Data Analytics" && }
+
+
+
{cohort.course.name}
+
Cohort {cohort.id}
+
+
+ }
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+ }
+
+export default Cohorts
\ No newline at end of file
diff --git a/src/pages/dashboard/cohorts/style.css b/src/pages/dashboard/cohorts/style.css
new file mode 100644
index 00000000..f9c27bcc
--- /dev/null
+++ b/src/pages/dashboard/cohorts/style.css
@@ -0,0 +1,105 @@
+main {
+ padding: 30px;
+}
+
+aside {
+ padding: 30px 60px 30px 0;
+}
+
+.create-post-input {
+ display: grid;
+ grid-template-columns: 70px auto;
+}
+
+.create-post-input button {
+ color: var(--color-blue1);
+ font-size: 1rem !important;
+ padding-left: 15px !important;
+ text-align: left !important;
+ max-width: 100% !important;
+ background-color: var(--color-blue5);
+}
+
+
+
+.dashboard-cohort-item {
+ margin-top: 10px;
+
+}
+
+.dashboard-cohort-info {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.course-text {
+ margin-left: -35px
+}
+
+
+.course-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+/* Farger per kurs */
+.software-icon {
+ background: #28C846;
+}
+
+.front-icon {
+ background: #6E6EDC;
+}
+
+.data-icon {
+ background: #46A0FA;
+}
+
+.course-icon svg {
+ width: 30px;
+ height: 30px;
+
+}
+
+.dashboard-cohort-name {
+ margin-left: 50px;
+ font-size: 16px;
+ color: #64648C;
+ margin-top: 4px;
+
+}
+
+
+.dashboard-course-name {
+ font-size: 20px;
+ font-weight: bold;
+ margin-left: 50px;
+
+}
+
+.student-button {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 199px;
+ height: 56px;
+ padding: 14px 24px;
+ gap: 8px;
+ border-radius: 8px;
+ background: #F0F5FA;
+ color: #64648C;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+
+}
+
+.loading-cohorts {
+ font-size: 20px;
+}
\ No newline at end of file
diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js
index 54606849..e741f805 100644
--- a/src/pages/dashboard/index.js
+++ b/src/pages/dashboard/index.js
@@ -1,19 +1,84 @@
-import { useState } from 'react';
-import SearchIcon from '../../assets/icons/searchIcon';
+
+
+import { useState, useEffect } from 'react';
+
+
+
import Button from '../../components/button';
import Card from '../../components/card';
import CreatePostModal from '../../components/createPostModal';
-import TextInput from '../../components/form/textInput';
import Posts from '../../components/posts';
import useModal from '../../hooks/useModal';
import './style.css';
+import Cohorts from './cohorts';
+import { useUserRoleData } from '../../context/userRole.';
+import Students from './students';
+import TeachersDashboard from './teachers';
+import useAuth from '../../hooks/useAuth';
+import jwtDecode from 'jwt-decode';
+import Search from './search';
+
+import { getUserById, get } from '../../service/apiClient';
+import UserIcon from '../../components/profile-icon';
+
const Dashboard = () => {
- const [searchVal, setSearchVal] = useState('');
+ const { token } = useAuth();
+ const [students, setStudents] = useState([]);
+ const [cohort, setCohort] = useState([]);
+ const [course, setCourse] = useState([]);
+
+ const [cohorts, setCohorts] = useState(null)
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in Dashboard:', error);
+ }
+
+ // to view people My Cohort
+ useEffect(() => {
+ async function fetchCohortData() {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ console.error('No token found.');
+ return;
+ }
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token);
+ userId = decodedToken.userId;
+ } catch (decodeError) {
+ console.error('Invalid token:', decodeError);
+ return;
+ }
+
+ const user = await getUserById(userId);
+ const data = await get(`cohorts/${user.profile.cohort.id}`);
+
+ setCohort(data.data.cohort)
+ setCourse(data.data.cohort);
+ setStudents(data.data.cohort.profiles)
+
+ } catch (error) {
+ console.error('fetchCohortData() in dashboard/index.js:', error);
+ }
+ }
+ fetchCohortData();
+ }, []);
+
+ const fullName = `${decodedToken.firstName || decodedToken.first_name || 'Current'} ${decodedToken.lastName || decodedToken.last_name || 'User'}`;
+ const initials = fullName?.match(/\b(\w)/g)?.join('') || 'NO';
+ const { userRole, setUserRole } = useUserRoleData();
+
+
- const onChange = (e) => {
- setSearchVal(e.target.value);
- };
// Use the useModal hook to get the openModal and setModal functions
const { openModal, setModal } = useModal();
@@ -27,14 +92,47 @@ const Dashboard = () => {
openModal();
};
+ useEffect(() => {
+ async function fetchAndSetUserRole() {
+ const storedToken = token || localStorage.getItem('token');
+ if (!storedToken) return;
+ try {
+ const decoded = jwtDecode(storedToken);
+ const user = await getUserById(decoded.userId);
+ // check the role from backend
+ const roleName = user.profile.role.name;
+ if (roleName === 'ROLE_TEACHER') setUserRole(1);
+ else if (roleName === 'ROLE_STUDENT') setUserRole(2);
+ else setUserRole(null);
+ } catch (error) {
+ console.error('Error fetching user role from backend:', error);
+ }
+ }
+ fetchAndSetUserRole();
+ }, [token, setUserRole]);
+
+ useEffect(() => {
+ async function fetchCohorts() {
+ try {
+ const response = await get("cohorts");
+ setCohorts(response.data.cohorts);
+ } catch (error) {
+ console.error("Error fetching cohorts:", error);
+ }
+ }
+
+ fetchCohorts();
+ }, []);
+
return (
<>
@@ -43,15 +141,37 @@ const Dashboard = () => {
>
);
diff --git a/src/pages/dashboard/search/index.js b/src/pages/dashboard/search/index.js
new file mode 100644
index 00000000..01200646
--- /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, index) => (
+ -
+
+
+ ))}
+
+ ) : (
+ Sorry, no results found
+ )}
+
+
+ {searchResults?.length > 10 && (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+
+}
+
+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..2df3b589
--- /dev/null
+++ b/src/pages/dashboard/students/index.js
@@ -0,0 +1,76 @@
+import { useEffect, useState } from "react";
+import { get } from "../../../service/apiClient";
+
+import { useNavigate } from "react-router-dom";
+import Card from "../../../components/card"
+import UserIcon from "../../../components/profile-icon";
+import ProfileIconTeacher from "../../../components/profile-icon-teacherView";
+
+const Students = () => {
+ const [students, setStudents] = useState(null)
+
+
+ useEffect(() => {
+ async function fetchStudents() {
+ try {
+ const response = await get("students");
+ setStudents(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching students:", error);
+ }
+ }
+
+ fetchStudents();
+ }, []);
+
+ const navigate = useNavigate()
+
+ const handleClick = () => {
+ navigate("/")
+ // navigate("/students")
+ }
+
+ return(
+ <>
+
+ Students
+
+ {students !== null ? (
+
+
+ {students.slice(0,10).map((student, index) => (
+ -
+
+
word[0].toUpperCase())
+ .join('')}
+ firstname={student.firstName}
+ lastname={student.lastName}
+ />
+
+
+ ))}
+
+
+
+
+
+ ):(
+
+ )}
+
+
+ >
+ )
+}
+export default Students
\ No newline at end of file
diff --git a/src/pages/dashboard/style.css b/src/pages/dashboard/style.css
index f55ef0a7..2e3bef39 100644
--- a/src/pages/dashboard/style.css
+++ b/src/pages/dashboard/style.css
@@ -19,3 +19,109 @@ 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 #ccc;
+ 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;
+}
\ 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..a9dc5a39
--- /dev/null
+++ b/src/pages/dashboard/teachers/index.js
@@ -0,0 +1,59 @@
+import { useEffect, useState } from "react"
+import { get } from "../../../service/apiClient"
+import Card from "../../../components/card"
+import UserIcon from "../../../components/profile-icon"
+
+const TeachersDashboard = () => {
+ const [teachers, setTeachers] = useState(null)
+
+ useEffect(() => {
+ async function fetchTeachers() {
+ try {
+ const response = await get("teachers")
+ setTeachers(Array.isArray(response.data.profiles) ? response.data.profiles : [])
+ } catch (error) {
+ console.error("Error fetching teachers: ", error)
+ }
+ }
+ fetchTeachers()
+ }, [])
+
+ return (
+ <>
+
+ Teachers
+
+ {teachers !== null ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+}
+
+export default TeachersDashboard
\ No newline at end of file
diff --git a/src/pages/edit/edit.css b/src/pages/edit/edit.css
new file mode 100644
index 00000000..6d9701c5
--- /dev/null
+++ b/src/pages/edit/edit.css
@@ -0,0 +1,164 @@
+.edit-profile-form {
+ width: 120%;
+ margin: 2rem auto;
+ padding: 2rem;
+ background-color: #fff;
+ border: 1px solid #e6ebf5;
+ border-radius: 12px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+ font-family: 'Inter', sans-serif;
+ display: flex;
+ flex-direction: column;
+}
+
+.edit-profile-form h2 {
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ color: #333;
+}
+
+.section h3 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ color: #444;
+}
+
+.row {
+ display: flex;
+ gap: 2rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+}
+
+.section {
+ flex: 1;
+ min-width: 300px;
+}
+
+.half {
+ width: 100%;
+}
+
+@media (min-width: 768px) {
+ .half {
+ width: 48%;
+ }
+}
+
+.section > *:not(h3):not(.photo-placeholder):not(.char-count) {
+ margin-bottom: 1.5rem;
+}
+
+.photo-placeholder {
+ width: 80px;
+ height: 80px;
+ background-color: #ddd;
+ border-radius: 50%;
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #555;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+}
+
+input,
+select,
+textarea {
+ width: 100%;
+ padding: 01.6rem;
+ font-size: 1rem;
+ font-family: 'Inter', sans-serif;
+ background-color: #fff;
+}
+
+.char-count {
+ text-align: right;
+ font-size: 0.85rem;
+ color: #666;
+ margin-top: -1rem;
+ margin-bottom: 1.5rem;
+}
+
+.save-button {
+ align-self: flex-end;
+ background-color: #0077cc;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.save-button:hover {
+ background-color: #005fa3;
+}
+
+.bio-area {
+ background-color: #e6ebf5
+}
+
+.save {
+ background-color: var(--color-blue);
+ color: white;
+}
+
+.cancel {
+ background-color: var(--color-blue5);
+}
+
+.bottom-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ 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..e29e142a
--- /dev/null
+++ b/src/pages/edit/index.js
@@ -0,0 +1,356 @@
+import { useEffect, useState } from "react";
+import "./edit.css";
+import Popup from "reactjs-popup";
+import imageCompression from "browser-image-compression";
+import { getUserById, updateUserProfile } from "../../service/apiClient";
+import useAuth from "../../hooks/useAuth";
+import jwtDecode from "jwt-decode";
+import TextInput from "../../components/form/textInput";
+import ProfileCircle from "../../components/profileCircle";
+import Card from "../../components/card";
+import { validatePassword, validateEmail } from '../register';
+import LockIcon from '../../assets/icons/lockIcon';
+
+const EditPage = () => {
+ const [formData, setFormData] = useState(null);
+ const { token } = useAuth();
+
+ let userId;
+ try {
+ const decodedToken = jwtDecode(token || localStorage.getItem('token'));
+ userId = decodedToken?.userId;
+ } catch (error) {
+ console.error('Invalid token:', error);
+ userId = null;
+ }
+
+ const [formValues, setFormValues] = useState({
+ photo: "",
+ firstName: "",
+ lastName: "",
+ username: "",
+ githubUsername: "",
+ email: "",
+ mobile: "",
+ password: "",
+ bio: "",
+ });
+
+ const [showPasswordFields, setShowPasswordFields] = useState(false);
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+
+ useEffect(() => {
+ async function fetchUser() {
+ try {
+ const data = await getUserById(userId);
+ setFormData(data);
+
+ const profile = data.profile || {};
+ setFormValues({
+ photo: profile.photo || "",
+ firstName: profile.firstName || "",
+ lastName: profile.lastName || "",
+ username: profile.username || "",
+ githubUsername: profile.githubUrl || "",
+ email: data.email || "",
+ mobile: profile.mobile || "",
+ password: "",
+ bio: profile.bio || "",
+ });
+ } catch (error) {
+ console.error("Error in EditPage", error);
+ }
+ }
+ if (userId) fetchUser();
+ }, [userId]);
+
+ if (!formData || !formData.profile) {
+ return (
+
+ );
+ }
+
+ const firstName = formData.profile.firstName;
+ const lastName = formData.profile.lastName;
+ const name = `${firstName} ${lastName}`;
+
+ const getReadableRole = (role) => {
+ switch (role) {
+ case 'ROLE_STUDENT': return 'Student';
+ case 'ROLE_TEACHER': return 'Teacher';
+ case 'ROLE_ADMIN': return 'Administrator';
+ default: return role;
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormValues((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const togglePasswordFields = () => setShowPasswordFields(prev => !prev);
+
+ const handleFileCompressionAndSet = async (file, closePopup) => {
+ if (!file) return;
+ if (!file.type.startsWith('image/')) { alert('Not an image'); return; }
+
+ const options = { maxSizeMB: 0.5, maxWidthOrHeight: 1024, useWebWorker: true, initialQuality: 0.8 };
+
+ try {
+ const compressedFile = await imageCompression(file, options);
+ if (compressedFile.size > 2 * 1024 * 1024) {
+ alert('Bildet er fortsatt for stort etter komprimering. Velg et mindre bilde.');
+ return;
+ }
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const dataUrl = reader.result;
+ setFormValues(prev => ({ ...prev, photo: dataUrl }));
+ if (typeof closePopup === 'function') closePopup();
+ };
+ reader.readAsDataURL(compressedFile);
+ } catch (err) {
+ console.error('Compression error', err);
+ alert('Kunne ikke komprimere bildet');
+ }
+ };
+
+ const resetFormToSaved = () => {
+ if (!formData) return;
+ const profile = formData.profile || {};
+ setFormValues({
+ photo: profile.photo || "",
+ firstName: profile.firstName || "",
+ lastName: profile.lastName || "",
+ username: profile.username || "",
+ githubUsername: profile.githubUrl || "",
+ email: formData.email || "",
+ mobile: profile.mobile || "",
+ password: "",
+ bio: profile.bio || "",
+ });
+ alert("The changes are discarded")
+ setNewPassword("");
+ setConfirmPassword("");
+ setShowPasswordFields(false);
+ };
+
+ const handleSave = async (e) => {
+ e.preventDefault();
+
+ if (!validateEmail(formValues.email)) return;
+
+ if (showPasswordFields) {
+ const isValidFormat = validatePassword(newPassword);
+ if (!isValidFormat) return;
+ if (newPassword !== confirmPassword) {
+ alert("The passwords do not match.");
+ return;
+ }
+ };
+
+ const updatedValues = { ...formValues, password: showPasswordFields ? newPassword : "" };
+
+ try {
+ await updateUserProfile(userId, updatedValues);
+ alert("Profile is updated!");
+ const refreshed = await getUserById(userId);
+ setFormData(refreshed);
+ setFormValues(prev => ({ ...prev, photo: refreshed.profile.photo || prev.photo }));
+ } 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/login/index.js b/src/pages/login/index.js
index 08df7d5a..53e02453 100644
--- a/src/pages/login/index.js
+++ b/src/pages/login/index.js
@@ -4,10 +4,17 @@ import TextInput from '../../components/form/textInput';
import useAuth from '../../hooks/useAuth';
import CredentialsCard from '../../components/credentials';
import './login.css';
+import { useUserRoleData } from '../../context/userRole.';
+import { get } from '../../service/apiClient';
+// eslint-disable-next-line camelcase
+import jwt_decode from 'jwt-decode';
+import { useNavigate } from 'react-router-dom';
const Login = () => {
- const { onLogin } = useAuth();
+ const { onLogin} = useAuth();
const [formData, setFormData] = useState({ email: '', password: '' });
+ const {setUserRole} = useUserRoleData()
+ const navigate = useNavigate()
const onChange = (e) => {
const { name, value } = e.target;
@@ -36,7 +43,20 @@ const Login = () => {
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..c52e8af2
--- /dev/null
+++ b/src/pages/profile/profile-data/index.js
@@ -0,0 +1,91 @@
+import './profile-data.css'
+
+const ProfileData = ({ user }) => {
+ const {email} = user;
+ const roleName = user.profile.role.name;
+ const { firstName, lastName, githubUrl, mobile, specialism, bio, photo } = user.profile;
+
+ const getReadableRole = (role) => {
+ switch (role) {
+ case 'ROLE_STUDENT':
+ return 'Student';
+ case 'ROLE_TEACHER':
+ return 'Teacher';
+ case 'ROLE_ADMIN':
+ return 'Administrator'
+ default:
+ return role;
+ }
+ };
+
+ return (
+
+
+

+ {(firstName || lastName) && (
+
{firstName} {lastName}
+ )}
+ {bio &&
{bio}
}
+
+
+
+ {(firstName || lastName) && (
+
+ Full Name:
+ {firstName} {lastName}
+
+ )}
+
+ {email && (
+
+ Email:
+ {email}
+
+ )}
+
+ {mobile && (
+
+ Mobile:
+ {mobile}
+
+ )}
+
+ {githubUrl && githubUrl.trim() !== '' && (
+
+ )}
+
+
+ {specialism && (
+
+ Specialism:
+ {specialism}
+
+ )}
+
+ {roleName && (
+
+ Role:
+ {getReadableRole(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..a04d18f1
--- /dev/null
+++ b/src/pages/profile/profile-data/profile-data.css
@@ -0,0 +1,66 @@
+.profile-container {
+ display: flex;
+ flex-direction: row;
+ gap: 3rem;
+ padding: 3rem;
+ font-family: 'Inter', sans-serif;
+ font-size: 1.4rem;
+}
+
+.info-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.info-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ font-size: 1.6rem;
+}
+
+.label {
+ font-weight: 600;
+ margin-right: 1rem;
+ min-width: 150px;
+}
+
+.value {
+ font-weight: 400;
+ flex: 1;
+}
+
+.value a {
+ color: #0077cc;
+ text-decoration: underline;
+ font-size: 1.6rem;
+}
+
+.bio-text {
+ margin-top: 1rem;
+ text-align: center;
+ font-style: italic;
+ color: #555;
+ font-size: 1.4rem;
+ max-width: 450px;
+}
+
+.photo-section-edit {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1rem;
+}
+
+.profile-photo-edit {
+ width: 300px;
+ height: 300px;
+ object-fit: cover;
+ border-radius: 50%;
+ display: block;
+ border: 2px solid black;
+}
+
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
/>
+
diff --git a/src/pages/search/index.js b/src/pages/search/index.js
new file mode 100644
index 00000000..6763edd1
--- /dev/null
+++ b/src/pages/search/index.js
@@ -0,0 +1,146 @@
+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 { useUserRoleData } from "../../context/userRole.";
+import useAuth from "../../hooks/useAuth";
+import jwtDecode from "jwt-decode";
+
+const SearchPage = () => {
+ const [query, setQuery] = useState("");
+ const [newresults, setNewResults] = useState(null);
+ const {searchResults} = useSearchResults();
+ const navigate = useNavigate();
+ const { token } = useAuth();
+
+
+ // Safely decode token with fallback
+ let decodedToken = {};
+ try {
+ if (token || localStorage.getItem('token')) {
+ decodedToken = jwtDecode(token || localStorage.getItem('token')) || {};
+ }
+ } catch (error) {
+ console.error('Invalid token in Dashboard:', error);
+ }
+
+ const { userRole, setUserRole } = useUserRoleData();
+ setUserRole(decodedToken.roleId)
+
+ const handleGoBack = () => {
+ navigate("/");
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!query.trim()) return;
+ try {
+ const response = await get(`search/profiles/${query}`);
+ setNewResults(response.data.profiles);
+ } catch (error) {
+ console.error("Error fetching search results:", error);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {searchResults && (
+
+
+ People
+
+ {searchResults.length > 0 && newresults === null ? (
+
+ {searchResults.slice(0, 10).map((student, index) => (
+ -
+ {userRole === 1 ? (
+
+ ) : userRole === 2 ? (
+
+ ) : null}
+
+ ))}
+
+ ) : newresults !== null ? (
+
+ {newresults.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..fb7ee354 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: 'Software Development',
+ cohort: 1,
+ start_date: '2025-09-14',
+ end_date: '2025-10-15',
+ 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
-