diff --git a/.gitignore b/.gitignore index 2cd0c01c..c094933b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.vscode \ No newline at end of file +.vscode +.idea + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..877839d2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ + +src/* +src/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6891fbef..d9caa348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,15 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-modal": "^3.16.1", + "react-password-checklist": "^1.8.1", "react-router-dom": "^6.8.0", "react-scripts": "5.0.1", + "reactjs-popup": "^2.0.6", "web-vitals": "^3.1.1" }, "devDependencies": { "eslint": "^8.57.1", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^9.1.2", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", @@ -7360,9 +7362,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", "bin": { @@ -14974,6 +14976,15 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, + "node_modules/react-password-checklist": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/react-password-checklist/-/react-password-checklist-1.8.1.tgz", + "integrity": "sha512-QHIU/OejxoH4/cIfYLHaHLb+yYc8mtL0Vr4HTmULxQg3ZNdI9Ni/yYf7pwLBgsUh4sseKCV/GzzYHWpHqejTGw==", + "license": "MIT", + "peerDependencies": { + "react": ">16.0.0-alpha || >17.0.0-alpha || >18.0.0-alpha" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15084,6 +15095,19 @@ } } }, + "node_modules/reactjs-popup": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.6.tgz", + "integrity": "sha512-A+tt+x9wdgZiZjv0e2WzYLD3IfFwJALaRaqwrCSXGjo0iQdsry/EtBEbQXRSmQs7cHmOi5eytCiSlOm8k4C+dg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 249b6b05..1825e548 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-modal": "^3.16.1", + "react-password-checklist": "^1.8.1", "react-router-dom": "^6.8.0", "react-scripts": "5.0.1", + "reactjs-popup": "^2.0.6", "web-vitals": "^3.1.1" }, "scripts": { @@ -43,7 +45,7 @@ }, "devDependencies": { "eslint": "^8.57.1", - "eslint-config-prettier": "^9.1.0", + "eslint-config-prettier": "^9.1.2", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", diff --git a/public/faviconsvg.svg b/public/faviconsvg.svg new file mode 100644 index 00000000..03d77140 --- /dev/null +++ b/public/faviconsvg.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/favicontransparent.ico b/public/favicontransparent.ico new file mode 100644 index 00000000..d73f75c4 Binary files /dev/null and b/public/favicontransparent.ico differ diff --git a/public/faviconwhite.ico b/public/faviconwhite.ico new file mode 100644 index 00000000..b7340a08 Binary files /dev/null and b/public/faviconwhite.ico differ diff --git a/public/index.html b/public/index.html index aa069f27..dc1bfc18 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + - React App + Cohort Manager diff --git a/public/microsoft-azure-logo.svg b/public/microsoft-azure-logo.svg new file mode 100644 index 00000000..d321d23a --- /dev/null +++ b/public/microsoft-azure-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.css b/src/App.css index ec62bd1a..5262a6ba 100644 --- a/src/App.css +++ b/src/App.css @@ -7,10 +7,17 @@ grid-template-columns: 151px 2fr 1fr; grid-template-rows: 96px auto; background-color: #f0f5fa; - height: 100vh; + + height: auto; + min-height: 100vh; } .ReactModal__Body--open, .ReactModal__Html--open { overflow: hidden; } + +.border-line { + border-bottom: 1px solid var(--color-blue5); + padding: 20px 10px; +} diff --git a/src/App.js b/src/App.js index 136c3a15..b4a9dc9f 100644 --- a/src/App.js +++ b/src/App.js @@ -8,18 +8,26 @@ import Verification from './pages/verification'; import { AuthProvider, ProtectedRoute } from './context/auth'; import { ModalProvider } from './context/modal'; import Welcome from './pages/welcome'; +import { FormProvider } from './context/form'; +import Cohort from './pages/cohort'; +import ProfilePage from './pages/profile'; +import { UserRoleProvider } from './context/userRole.'; +import ProfileData from './pages/profile/profile-data'; +import FullScreenCard from './components/fullscreenCard'; +import EditPage from './pages/edit'; const App = () => { return ( <> + + } /> } /> } /> } /> - { } /> + + + + } + /> + + + + } + /> + + + + } + /> + + ); diff --git a/src/assets/icons/EditIcon.js b/src/assets/icons/EditIcon.js new file mode 100644 index 00000000..a3784603 --- /dev/null +++ b/src/assets/icons/EditIcon.js @@ -0,0 +1,9 @@ +const EditIcon = () => { + return ( + + + + ); +}; + +export default EditIcon; diff --git a/src/assets/icons/cogIcon.js b/src/assets/icons/cogIcon.js deleted file mode 100644 index 9253d6be..00000000 --- a/src/assets/icons/cogIcon.js +++ /dev/null @@ -1,12 +0,0 @@ -const CogIcon = () => { - return ( - - - - ); -}; - -export default CogIcon; diff --git a/src/assets/icons/cohortIcon-fill.js b/src/assets/icons/cohortIcon-fill.js index 817b44b7..3cc54e18 100644 --- a/src/assets/icons/cohortIcon-fill.js +++ b/src/assets/icons/cohortIcon-fill.js @@ -1,11 +1,6 @@ const CohortIconFill = () => { return ( - - - + ); }; diff --git a/src/assets/icons/cohortIcon.js b/src/assets/icons/cohortIcon.js index 4b7ee14f..8a8aa551 100644 --- a/src/assets/icons/cohortIcon.js +++ b/src/assets/icons/cohortIcon.js @@ -1,10 +1,6 @@ -const CohortIcon = ({ colour = '#64648C' }) => { +const CohortIcon = () => { return ( - - + ); }; diff --git a/src/assets/icons/commentBubbleIcon.js b/src/assets/icons/commentBubbleIcon.js new file mode 100644 index 00000000..3931b175 --- /dev/null +++ b/src/assets/icons/commentBubbleIcon.js @@ -0,0 +1,9 @@ +const CommentBubbleIcon = () => { + return ( + + + + ); +}; + +export default CommentBubbleIcon; diff --git a/src/assets/icons/dataAnalyticsLogo.js b/src/assets/icons/dataAnalyticsLogo.js new file mode 100644 index 00000000..f39dfb47 --- /dev/null +++ b/src/assets/icons/dataAnalyticsLogo.js @@ -0,0 +1,8 @@ +const DataAnalyticsLogo = () => { + return ( + <> + + ) +} + +export default DataAnalyticsLogo \ No newline at end of file diff --git a/src/assets/icons/excersicesIcon.js b/src/assets/icons/excersicesIcon.js new file mode 100644 index 00000000..613b9473 --- /dev/null +++ b/src/assets/icons/excersicesIcon.js @@ -0,0 +1,10 @@ +const ExcersicesIcon = () => { + return ( + + + + + ) +} + +export default ExcersicesIcon \ No newline at end of file diff --git a/src/assets/icons/excersicesIconFilled.js b/src/assets/icons/excersicesIconFilled.js new file mode 100644 index 00000000..3613a36b --- /dev/null +++ b/src/assets/icons/excersicesIconFilled.js @@ -0,0 +1,7 @@ +const ExcersicesIconFilled = () => { + return ( + + ) +} + +export default ExcersicesIconFilled \ No newline at end of file diff --git a/src/assets/icons/frontEndLogo.js b/src/assets/icons/frontEndLogo.js new file mode 100644 index 00000000..ac6ad0d3 --- /dev/null +++ b/src/assets/icons/frontEndLogo.js @@ -0,0 +1,8 @@ +const FrontEndLogo = () => { + return ( + <> + + ) +} + +export default FrontEndLogo \ No newline at end of file diff --git a/src/assets/icons/heartIcon.js b/src/assets/icons/heartIcon.js new file mode 100644 index 00000000..556e7699 --- /dev/null +++ b/src/assets/icons/heartIcon.js @@ -0,0 +1,9 @@ +const HeartIcon = () => { + return ( + + + + ); +}; + +export default HeartIcon; diff --git a/src/assets/icons/heartIconFilled.js b/src/assets/icons/heartIconFilled.js new file mode 100644 index 00000000..acfc5478 --- /dev/null +++ b/src/assets/icons/heartIconFilled.js @@ -0,0 +1,9 @@ +const HeartIconFilled = () => { + return ( + + + + ); +}; + +export default HeartIconFilled; \ No newline at end of file diff --git a/src/assets/icons/homeIcon.js b/src/assets/icons/homeIcon.js index 235fcfdf..71c3b87d 100644 --- a/src/assets/icons/homeIcon.js +++ b/src/assets/icons/homeIcon.js @@ -1,9 +1,7 @@ -const HomeIcon = ({ colour = '#64648C' }) => { +const HomeIcon = () => { return ( - - - - ); + + ); }; export default HomeIcon; diff --git a/src/assets/icons/homeIconFilled.js b/src/assets/icons/homeIconFilled.js new file mode 100644 index 00000000..78c4998a --- /dev/null +++ b/src/assets/icons/homeIconFilled.js @@ -0,0 +1,7 @@ +const HomeIconFilled = ({colour}) => { + return ( + + ); +}; + +export default HomeIconFilled; diff --git a/src/assets/icons/logsIcon.js b/src/assets/icons/logsIcon.js new file mode 100644 index 00000000..f0e086c8 --- /dev/null +++ b/src/assets/icons/logsIcon.js @@ -0,0 +1,9 @@ +const LogsIcon = () => { + return ( + + + + ) +} + +export default LogsIcon \ No newline at end of file diff --git a/src/assets/icons/logsIconFilled.js b/src/assets/icons/logsIconFilled.js new file mode 100644 index 00000000..464f3cae --- /dev/null +++ b/src/assets/icons/logsIconFilled.js @@ -0,0 +1,9 @@ +const LogsIconFilled = () => { + return ( + + + + ) +} + +export default LogsIconFilled \ No newline at end of file diff --git a/src/assets/icons/notesIcon.js b/src/assets/icons/notesIcon.js new file mode 100644 index 00000000..6bd41927 --- /dev/null +++ b/src/assets/icons/notesIcon.js @@ -0,0 +1,9 @@ +const NotesIcon = () => { + return ( + + + + ) +} + +export default NotesIcon \ No newline at end of file diff --git a/src/assets/icons/notesIconFilled.js b/src/assets/icons/notesIconFilled.js new file mode 100644 index 00000000..b53035a0 --- /dev/null +++ b/src/assets/icons/notesIconFilled.js @@ -0,0 +1,9 @@ +const NotesIconFilled = () => { + return ( + + + + ) +} + +export default NotesIconFilled \ No newline at end of file diff --git a/src/assets/icons/profileIcon.js b/src/assets/icons/profileIcon.js index 9f57f958..462e7410 100644 --- a/src/assets/icons/profileIcon.js +++ b/src/assets/icons/profileIcon.js @@ -1,21 +1,7 @@ -const ProfileIcon = ({ colour = '#64648C', background = 'transparent' }) => { +const ProfileIcon = () => { return ( - - + + ); }; diff --git a/src/assets/icons/profileIconFilled.js b/src/assets/icons/profileIconFilled.js new file mode 100644 index 00000000..3f9fb82f --- /dev/null +++ b/src/assets/icons/profileIconFilled.js @@ -0,0 +1,17 @@ +const ProfileIconFilled = () => { + return ( + + + ); +}; + +export default ProfileIconFilled; + + + + + diff --git a/src/assets/icons/sendIcon.js b/src/assets/icons/sendIcon.js new file mode 100644 index 00000000..d3ff5b41 --- /dev/null +++ b/src/assets/icons/sendIcon.js @@ -0,0 +1,9 @@ +const SendIcon = () => { + return ( + + + + ); +}; + +export default SendIcon; \ No newline at end of file diff --git a/src/assets/icons/software-logo.js b/src/assets/icons/software-logo.js new file mode 100644 index 00000000..0c0a161d --- /dev/null +++ b/src/assets/icons/software-logo.js @@ -0,0 +1,7 @@ +const SoftwareLogo = () => { + return ( + + ) +} + +export default SoftwareLogo \ No newline at end of file diff --git a/src/assets/icons/specialismIcon.js b/src/assets/icons/specialismIcon.js new file mode 100644 index 00000000..f4c17a85 --- /dev/null +++ b/src/assets/icons/specialismIcon.js @@ -0,0 +1,59 @@ +// contains all specialism icons used in the app, +// add yours if you add more courses, just copy&paste and change the function name, color prop and path data, and add it to the export statement below. + +// maybe add the same style as for ProfileCircle where these are used? + +const SoftwareIcon = ({ color = '#28C84F', background = 'transparent'}) => { + return ( + + + + ) +}; + +const FrontendIcon = ({ color = '#FFFFFF', background = '#6E6EDC'}) => { + return ( + + + + ) +}; + +const DataAnalyticsIcon = ({ color = '#FFFFFF', background = '#46A0FA'}) => { + return ( + + + + ) +}; + + +export { SoftwareIcon, FrontendIcon, DataAnalyticsIcon }; \ No newline at end of file diff --git a/src/components/card/style.css b/src/components/card/style.css index c1f0e8b2..e7e867a5 100644 --- a/src/components/card/style.css +++ b/src/components/card/style.css @@ -2,11 +2,11 @@ background: white; padding: 24px; border-radius: 8px; - width: 100%; + width: 100% !important; margin-bottom: 25px; border: 1px #e6ebf5 solid; } .card-shadow { box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); -} +} \ No newline at end of file diff --git a/src/components/comment/index.js b/src/components/comment/index.js index 16ffb366..a380fdf7 100644 --- a/src/components/comment/index.js +++ b/src/components/comment/index.js @@ -1,10 +1,37 @@ -const Comment = ({ name, content }) => { +import useModal from '../../hooks/useModal'; +import EditCommentModal from '../editCommentModal'; +import MenuPost from '../post/dropdown'; +import './style.css'; + +const Comment = ({ name, content, postId, commentId}) => { + + + const initials = name?.match(/\b(\w)/g); + + const { openModal, setModal } = useModal(); + const showModal = () => { + setModal('Edit comment', + ); + openModal(); + }; + + return ( - <> -
{name}
-

{content}

- +
+
+
+

{initials}

+
+
+
+
{name}
+

{content}

+
+{/* + */} + +
); }; -export default Comment; +export default Comment; \ No newline at end of file diff --git a/src/components/comment/style.css b/src/components/comment/style.css new file mode 100644 index 00000000..7d9af6c6 --- /dev/null +++ b/src/components/comment/style.css @@ -0,0 +1,35 @@ +.comment { + display: grid; + grid-template-columns: 56px 1fr auto; + gap: 12px; + align-items: flex-start +} + +.comment__bubble { + background: var(--color-blue5); + border-radius: 16px; + padding: 12px 16px; + color: var(--color-blue) +} + +.comment__author { + font-size: .9rem; + font-weight: 600; + margin: 0 0 4px 0; + color: var(--color-blue) +} + +.comment__content { + margin: 0; + line-height: 1.4 +} + +.comment__menu { + border: 0; + background: transparent; + color: var(--color-blue1); + padding: 4px 8px; + border-radius: 4px; + font-size: 1.2rem; + line-height: 1 +} \ No newline at end of file diff --git a/src/components/createComment/index.js b/src/components/createComment/index.js new file mode 100644 index 00000000..35711bf4 --- /dev/null +++ b/src/components/createComment/index.js @@ -0,0 +1,116 @@ +import { forwardRef, useState } from 'react'; +import useAuth from '../../hooks/useAuth'; +import { post } from '../../service/apiClient'; +import jwtDecode from 'jwt-decode'; +import SendIcon from '../../assets/icons/sendIcon'; +import './style.css'; + +const CreateComment = forwardRef(({ postId, onCommentAdded }, ref) => { + const { token } = useAuth(); + const [message, setMessage] = useState(null); + const [text, setText] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; + const fullName = `${decodedToken.firstName || decodedToken.first_name || 'Current'} ${decodedToken.lastName || decodedToken.last_name || 'User'}`; + const initials = fullName?.match(/\b(\w)/g)?.join('') || 'NO'; + const onChange = (e) => { + setText(e.target.value); + }; + + const onSubmit = async (e) => { + e.preventDefault(); + if (!text.trim() || isSubmitting) return; + + setIsSubmitting(true); + try { + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; + const { userId } = decodedToken; + + if (!userId) { + setMessage('Could not determine user. Please log in again.'); + setIsSubmitting(false); + return; + } + + const response = await post(`posts/${String(postId)}/comments`, { body: text, userId }); + console.log('Comment created successfully:', response); + + // Store the comment text before clearing + const commentText = text; + + // Clear the input and show success message + setText(''); + setMessage('Comment posted successfully!'); + + // Create a properly structured comment object for immediate display + // Try to get user info from token, fallback to defaults + const firstName = decodedToken.firstName || decodedToken.first_name || 'Current'; + const lastName = decodedToken.lastName || decodedToken.last_name || 'User'; + + const newComment = { + id: response.data?.id || Date.now(), + body: commentText, + user: { + id: userId, + profile: { + firstName, + lastName + } + }, + timeCreated: new Date().toISOString() + }; + + // Call the callback to update the parent component + if (onCommentAdded) { + onCommentAdded(newComment); + } + + // Clear success message after 2 seconds + setTimeout(() => { + setMessage(null); + }, 2000); + + } catch (error) { + console.error('Error creating comment:', error); + setMessage('Failed to post comment. Please try again.'); + setTimeout(() => { + setMessage(null); + }, 3000); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+

{initials}

+
+ +
+ + +
+ + {message &&

{message}

} +
+ ); +}); + +CreateComment.displayName = 'CreateComment'; + +export default CreateComment; \ No newline at end of file diff --git a/src/components/createComment/style.css b/src/components/createComment/style.css new file mode 100644 index 00000000..1647882d --- /dev/null +++ b/src/components/createComment/style.css @@ -0,0 +1,88 @@ +.create-comment { + display: grid; + grid-template-columns: 40px 1fr; + gap: 12px; + align-items: center; + padding: 12px 0 +} + +.profile-icon--sm { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: white; + font-size: 14px +} + +.profile-icon--sm p { + line-height: 40px; + font-size: 1rem +} + +.create-comment__input-wrapper { + position: relative; + display: flex; + align-items: center +} + +.create-comment__input { + width: 100%; + padding: 12px 50px 12px 16px; + border: none; + border-radius: 24px; + background-color: var(--color-blue5); + font-size: 14px; + outline: none; + transition: background-color 0.2s ease +} + +.create-comment__input:focus { + background-color: var(--color-blue4) +} + +.create-comment__input::placeholder { + color: var(--color-blue2) +} + +.create-comment__submit { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + background-color: transparent; + color: var(--color-blue2); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease +} + +.create-comment__submit:enabled { + background-color: var(--color-blue5); + color: white +} + +.create-comment__submit:enabled:hover { + background-color: var(--color-blue5) +} + +.create-comment__submit svg { + width: 18px; + height: 18px +} + +.create-comment__message { + grid-column: 1 / -1; + margin-top: 8px; + font-size: 12px; + color: var(--color-blue2) +} \ No newline at end of file diff --git a/src/components/createPostModal/index.js b/src/components/createPostModal/index.js index f19e5f1d..4239e504 100644 --- a/src/components/createPostModal/index.js +++ b/src/components/createPostModal/index.js @@ -1,37 +1,107 @@ import { useState } from 'react'; import useModal from '../../hooks/useModal'; +import useAuth from '../../hooks/useAuth'; import './style.css'; import Button from '../button'; +import { post } from '../../service/apiClient'; +import jwtDecode from 'jwt-decode'; -const CreatePostModal = () => { + +const CreatePostModal = ({ authorName, onPostAdded }) => { // Use the useModal hook to get the closeModal function so we can close the modal on user interaction const { closeModal } = useModal(); + const { token } = useAuth(); const [message, setMessage] = useState(null); const [text, setText] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; + const fullName = `${decodedToken.firstName || decodedToken.first_name || 'Current'} ${decodedToken.lastName || decodedToken.last_name || 'User'}`; + const initials = fullName?.match(/\b(\w)/g)?.join('') || 'NO'; const onChange = (e) => { setText(e.target.value); }; - const onSubmit = () => { - setMessage('Submit button was clicked! Closing modal in 2 seconds...'); + const onSubmit = async () => { + if (!text.trim() || isSubmitting) return; + + setIsSubmitting(false); + setMessage('Post created successfully!'); + + try { + + + const { userId } = decodedToken; + + if (!userId) { + setMessage('Could not determine user. Please log in again.'); + setIsSubmitting(false); + return; + } + + const response = await post('posts', { content: text, userId }); + console.log('Post created successfully:', response); - setTimeout(() => { - setMessage(null); - closeModal(); - }, 2000); + // Get user info from token for immediate display + const firstName = decodedToken.firstName || decodedToken.first_name || 'Current'; + const lastName = decodedToken.lastName || decodedToken.last_name || 'User'; + + // Create a properly structured post object for immediate display + const newPost = { + id: response.data?.id || Date.now(), + content: text, + user: { + id: userId, + profile: { + firstName, + lastName + } + }, + timeCreated: new Date().toISOString(), + timeUpdated: new Date().toISOString(), + comments: [], + likes: 0 + }; + + // Clear the input and show success message + setText(''); + + // Call the callback to update the parent component + if (onPostAdded) { + onPostAdded(newPost); + } + + // Close modal after short delay + setTimeout(() => { + setMessage('Post is processing!'); + closeModal(); + }, 100); + + } catch (error) { + console.error('Error creating post:', error); + setMessage('Failed to create post. Please try again.'); + setTimeout(() => { + setMessage(null); + }, 3000); + } finally { + setIsSubmitting(false); + } }; return ( <>
-

AJ

+ + {/* TODO: TO THIS SO THAT IT WORKS WIHT CORRECT NAMES */} +

{initials}

-

Alex J

+

{fullName}

+{/* + */}
@@ -43,7 +113,7 @@ const CreatePostModal = () => { onClick={onSubmit} text="Post" classes={`${text.length ? 'blue' : 'offwhite'} width-full`} - disabled={!text.length} + disabled={!text.length || isSubmitting} />
diff --git a/src/components/dropdown/index.js b/src/components/dropdown/index.js new file mode 100644 index 00000000..9149a65e --- /dev/null +++ b/src/components/dropdown/index.js @@ -0,0 +1,75 @@ +import React, { useState, useRef, useEffect } from "react"; + +function DropdownMenu({ + label, + options = [], + value, + onChange, + placeholder = "Select an option", +}) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const toggleDropdown = () => setIsOpen((prev) => !prev); + + const handleOptionClick = (optionValue) => { + onChange(optionValue); + setIsOpen(false); + }; + + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const normalizedOptions = Array.isArray(options) + ? options.map((opt) => + typeof opt === "object" + ? { label: opt.label, value: opt.value } + : { label: opt, value: opt } + ) + : []; + + const selectedOption = normalizedOptions.find((opt) => opt.value === value); + + return ( +
+ {label && } + + + {isOpen && ( + + )} +
+ ); +} + +export default DropdownMenu; diff --git a/src/components/dropdown/style.css b/src/components/dropdown/style.css new file mode 100644 index 00000000..9619e527 --- /dev/null +++ b/src/components/dropdown/style.css @@ -0,0 +1,45 @@ +/* Dropdown Button */ +.dropbtn { + background-color: #3498DB; + color: white; + padding: 16px; + font-size: 16px; + border: none; + cursor: pointer; +} + +/* Dropdown button on hover & focus */ +.dropbtn:hover, .dropbtn:focus { + background-color: #2980B9; +} + +/* The container
- 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 ( + <> +
+
+

{initials}

+
+
+

{name}

+
+
+ +
+ +
+ +
+
+ + {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..989fc0d8 --- /dev/null +++ b/src/components/editCommentModal/style.css @@ -0,0 +1,14 @@ +.create-post-user-details { + display: grid; + grid-template-columns: 56px auto; + column-gap: 20px; +} + +textarea { + width: 100%; + height: 256px; + resize: none; + background-color: var(--color-blue5); + border-radius: 8px; + padding: 16px; +} 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 ( <>
-

AJ

+

{initials}

-

Alex J

+

{name}

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..5a918c03 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, 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..930f5f28 --- /dev/null +++ b/src/components/fullscreenCard/fullscreenCard.css @@ -0,0 +1,92 @@ +.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; +} diff --git a/src/components/fullscreenCard/index.js b/src/components/fullscreenCard/index.js new file mode 100644 index 00000000..6a1605fd --- /dev/null +++ b/src/components/fullscreenCard/index.js @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +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 '../profileCircle'; +import '../../pages/loading'; +import { useNavigate } from 'react-router-dom'; + +const FullScreenCard = () => { + const [user, setUser] = useState(null); + const { token } = useAuth(); + const { userId } = jwtDecode(token); + const navigate = useNavigate(); + + useEffect(() => { + async function fetchUser() { + try { + const data = await getUserById(userId); + setUser(data); + } catch (error) { + console.error('Error fetching user:', error); + } + } + fetchUser(); + }, [userId]); + + const goToEdit = () => { + navigate(`/profile/${userId}/edit`); + }; + + if (!user || !user.profile) { + return
+
+

Loading...

+
+ +
+
+
+ } + + const firstname = user.profile.firstName; + const lastname = user.profile.lastName; + const name = firstname + " " + lastname; + + return ( +
+
n[0]).join("").toUpperCase()}/> +
+

{name}

+
+
+
+ + +
+ ); +}; + +export default FullScreenCard; diff --git a/src/components/header/index.js b/src/components/header/index.js index c591f1e1..ff3504ab 100644 --- a/src/components/header/index.js +++ b/src/components/header/index.js @@ -3,14 +3,21 @@ 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); + + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; + 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 +27,20 @@ const Header = () => { return null; } + let userIdFromToken = null; + try { + const decoded = jwtDecode(token); + userIdFromToken = decoded.userId; + } catch (err) { + console.error("Error when decoding by header", err); + } + return ( -
+
-

AJ

+

{initials}

{isMenuVisible && ( @@ -33,11 +48,11 @@ const Header = () => {
-

AJ

+

{initials}

-

Alex Jameson

+

{fullName}

Software Developer, Cohort 3
@@ -45,7 +60,7 @@ const Header = () => {
  • - +

    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..be50640c 100644 --- a/src/components/menu/menuItem/index.js +++ b/src/components/menu/menuItem/index.js @@ -1,7 +1,51 @@ import { NavLink } from 'react-router-dom'; import ArrowRightIcon from '../../../assets/icons/arrowRightIcon'; +import useModal from '../../../hooks/useModal'; +import EditPostModal from '../../editPostModal'; +import { del } from '../../../service/apiClient'; + +const MenuItem = ({ icon, text, children, linkTo = '#nogo', clickable, postText, postId, name, isMenuVisible, setIsMenuVisible }) => { + const { openModal, setModal } = useModal(); + const showModal = () => { + setModal('Edit post', ); + setIsMenuVisible(false); + openModal(); + }; + + const deletePost = async () => { + setIsMenuVisible(false); + console.log('deletePost function called'); + try { + + const results = await del(`posts/${postId}`); + console.log('Post deleted successfully', results); + + + } catch (error) { + console.error('Error deleting post:', error); + } + console.log('Deleting post with ID:', postId); + + /* window.location.reload(); */ + }; + + + + + + if (clickable) { + return ( +
  • + + {children &&
      {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..e60ca6ef --- /dev/null +++ b/src/components/post/dropdown/index.js @@ -0,0 +1,50 @@ +import { useState, useRef, useEffect } from 'react'; +import { CascadingMenuPost, PostMenu } from './menu'; +import { CascadingMenu } from '../../profileCircle'; +import EditCommentModal from '../../editCommentModal'; +import useModal from '../../../hooks/useModal'; +import EditPostModal from '../../editPostModal'; + +const MenuPost = ({ menuVisible, postText, postId, name }) => { + 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 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..964bee25 --- /dev/null +++ b/src/components/post/dropdown/menu/index.js @@ -0,0 +1,38 @@ +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'; + +const ProfileCirclePost = ({ initials, menuVisible, postText, postId, name }) => { + const [isMenuVisible, setIsMenuVisible] = useState(menuVisible || false); + + + + return ( +
    setIsMenuVisible(!isMenuVisible)}> + {isMenuVisible && } + +
    +

    {initials}

    +
    +
    + ); +}; + +export const CascadingMenuPost = ({ postText, postId, name, isMenuVisible, setIsMenuVisible }) => { + return ( + +{/*
  • + + +

    Edit post

    +
  • */} + } linkTo="" text="Edit post" clickable={"Modal"} postText={postText} postId={postId} name={name} isMenuVisible={isMenuVisible} setIsMenuVisible={setIsMenuVisible} /> + } text="Delete post" clickable={"Delete"} postId={postId} name={name} isMenuVisible={isMenuVisible} setIsMenuVisible={setIsMenuVisible}/> +
    + ); +}; + +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..26fedd57 --- /dev/null +++ b/src/components/post/dropdown/menu/style.css @@ -0,0 +1,79 @@ +.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; +} + +.menu-left { + position: absolute; + left: -500px; /* juster avstanden etter behov */ + top: 0; + z-index: 1000; +} diff --git a/src/components/post/index.js b/src/components/post/index.js index 337ca5a6..54eac0a3 100644 --- a/src/components/post/index.js +++ b/src/components/post/index.js @@ -1,55 +1,206 @@ +import { useEffect, useRef, useState } from 'react'; import useModal from '../../hooks/useModal'; 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 './style.css'; +import { del, get, patch, postTo } from '../../service/apiClient'; +import useAuth from '../../hooks/useAuth'; +import jwtDecode from 'jwt-decode'; +import MenuPost from './dropdown'; -const Post = ({ name, date, content, comments = [], likes = 0 }) => { +const Post = ({ post, user }) => { const { openModal, setModal } = useModal(); + 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 { token } = useAuth(); + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; - const userInitials = name.match(/\b(\w)/g); + 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 showModal = () => { - setModal('Edit post', ); + setModal('Edit post', ); openModal(); }; + + const isLikedInitial = () => { + if (!user || !Array.isArray(user)) { + setIsLiked(false); + return; + } + + const liked = user.some((likedPost) => likedPost.id === post.id); + setIsLiked(liked); + }; + + useEffect(() => { + isLikedInitial(); + }, [user, post.id]); // Add dependencies to re-run when user data changes + + 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 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 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); + + try { + await setBackendLikeState(!previousLikedState); + } catch (error) { + // Revert optimistic updates on error + setIsLiked(previousLikedState); + setLikeCount(previousLikeCount); + console.error('Failed to update like state:', error); + } + }; + + const setBackendLikeState = async (currentlyLiked) => { + try { + let results = null; + + if (currentlyLiked) { + results = await postTo(`posts/${post.id}/like`); + } else { + results = await del(`posts/${post.id}/like`); + } + + // Update user's liked posts + const person = await patch(`users/${decodedToken.userId}/like`, {post_id: post.id}); + + console.log('Like state update successful:', results); + console.log('User like list updated:', person.data.user.posts); + + } catch (error) { + console.error('Error updating like state:', error); + throw error; // Re-throw to allow error handling in handleLikeClick + } + }; + return (
    -
    +
    -
    -

    {name}

    - {date} +
    +

    {authorName}

    + {formatDate(post.timeCreated)} + {(post.timeCreated === post.timeUpdated) ? null : (

    Edited

    )} +
    + + {/* */} +
    -
    -

    ...

    -
    +
    +

    {post.content}

    -
    -

    {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..9f3517c2 100644 --- a/src/components/post/style.css +++ b/src/components/post/style.css @@ -1,50 +1,121 @@ .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; } -.post-interactions { +.pill--animating svg { + animation: heartBounce 0.4s ease-out; +} + +@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..8fb177f8 100644 --- a/src/components/posts/index.js +++ b/src/components/posts/index.js @@ -1,27 +1,56 @@ import { useEffect, useState } from 'react'; import Post from '../post'; -import { getPosts } from '../../service/apiClient'; +import { get, getPosts } from '../../service/apiClient'; +import useAuth from '../../hooks/useAuth'; +import jwtDecode from 'jwt-decode'; -const Posts = () => { +const Posts = ({ onPostAdded }) => { const [posts, setPosts] = useState([]); - + const [user, setUser] = useState(null); + const { token } = useAuth(); + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; useEffect(() => { - getPosts().then(setPosts); + async function fetchPosts() { + try { + const fetchedPosts = await getPosts(); + setPosts(fetchedPosts.reverse()); // Reverse fetched posts so newest are first + } catch (error) { + console.error('Error fetching posts:', error); + setPosts([]); + } + } + async function fetchUser() { + const userId = decodedToken.userId; + if (userId) { + try { + const user = await get(`users/${userId}`); + console.log('Fetched user:', user); + setUser(user); + } catch (error) { + console.error('Error fetching user:', error); + setUser(null); + } + } + } + fetchUser(); + fetchPosts(); }, []); + // Expose the function to add a new post + useEffect(() => { + if (onPostAdded) { + onPostAdded.current = (newPost) => { + setPosts(prevPosts => [newPost, ...prevPosts]); + }; + } + }, [onPostAdded]); + + 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-teacherView/editIconTeacher/index.js b/src/components/profile-icon-teacherView/editIconTeacher/index.js new file mode 100644 index 00000000..0c5568fa --- /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 = ({ 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..5286960d --- /dev/null +++ b/src/components/profile-icon-teacherView/index.js @@ -0,0 +1,52 @@ +import EditIconTeacher from './editIconTeacher'; +import './style.css'; + + +const ProfileIconTeacher = ({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 ( +
    + +
    +
    +

    {initials}

    +
    +
    +
    +

    {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..dfb5f889 --- /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: 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; +} + +.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..deac174f --- /dev/null +++ b/src/components/profile-icon/index.js @@ -0,0 +1,77 @@ +import Popup from 'reactjs-popup'; +import './style.css'; +import SeeProfile from '../seeProfile'; +import Popup from 'reactjs-popup'; +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 ( +
    + +
    +
    +

    {initials}

    +
    +
    +
    +

    {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..cbdf3e6b --- /dev/null +++ b/src/components/profile-icon/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/profileCircle/index.js b/src/components/profileCircle/index.js index 7dc5a614..8e4f9b67 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 = () => { +const CascadingMenu = ({ id }) => { + return ( - } text="Profile" /> + } text="Profile" linkTo={`profile/${id}`} /> } text="Add note" /> } text="Move to cohort"> diff --git a/src/components/profileCircle/style.css b/src/components/profileCircle/style.css index 47391fe7..9d9da818 100644 --- a/src/components/profileCircle/style.css +++ b/src/components/profileCircle/style.css @@ -5,4 +5,4 @@ .profile-circle-menu { margin-left: 65px; -} +} \ No newline at end of file diff --git a/src/components/seeProfile/index.js b/src/components/seeProfile/index.js new file mode 100644 index 00000000..9b7edb5c --- /dev/null +++ b/src/components/seeProfile/index.js @@ -0,0 +1,37 @@ +import Card from '../card'; +import './style.css'; +import { NavLink } from 'react-router-dom'; +import ProfileIcon from '../../assets/icons/profileIcon'; + +const SeeProfile = ({initials, firstname, lastname, role, userId}) => { + + return ( +
+ +
+
+

{initials}

+
+ +
+

{firstname} {lastname}

+ {role}, Cohort 3 +
+
+ +
+
    +
  • + +

    Profile

    +
    +
  • +
+
+
+
+ ) + +} + +export default SeeProfile; \ No newline at end of file diff --git a/src/components/seeProfile/style.css b/src/components/seeProfile/style.css new file mode 100644 index 00000000..0b4797a9 --- /dev/null +++ b/src/components/seeProfile/style.css @@ -0,0 +1,5 @@ + + +.card { + width: 450px; +} \ No newline at end of file diff --git a/src/components/socialLinks/index.js b/src/components/socialLinks/index.js index 9b112adb..103214c7 100644 --- a/src/components/socialLinks/index.js +++ b/src/components/socialLinks/index.js @@ -9,68 +9,34 @@ const SocialLinks = () => { }} className="socialbutton" > - - - - - - - - - - + + + + ); }; export default SocialLinks; + + + + diff --git a/src/components/stepper/index.js b/src/components/stepper/index.js index c9e5f259..3f745c15 100644 --- a/src/components/stepper/index.js +++ b/src/components/stepper/index.js @@ -4,7 +4,7 @@ import Button from '../button'; import './style.css'; import { useState } from 'react'; -const Stepper = ({ header, children, onComplete }) => { +const Stepper = ({ header, children, onComplete, data }) => { const [currentStep, setCurrentStep] = useState(0); const onBackClick = () => { @@ -22,6 +22,33 @@ const Stepper = ({ header, children, onComplete }) => { setCurrentStep(currentStep + 1); }; + const validateName = (data) => { + if(!data) { + alert("OBSS!!! Please write first_name and last_name") + return false + } else { + return true + } + } + + const validateUsername = (data) => { + if(data.username.length < 7) { + alert("Username is too short. Input must be at least 7 characters long") + return false + } else { + return true + } + } + + const validateMobile = (data) => { + if(data.length < 8) { + alert("Mobile number is too short. Input must be at least 8 characters long") + return false + } else { + return true + } + } + return ( {header} @@ -33,14 +60,34 @@ const Stepper = ({ header, children, onComplete }) => {
); }; -export default Stepper; +export default Stepper; \ No newline at end of file diff --git a/src/context/auth.js b/src/context/auth.js index 47cd66c9..cc5508ef 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -19,7 +19,7 @@ const AuthProvider = ({ children }) => { useEffect(() => { const storedToken = localStorage.getItem('token'); - if (storedToken) { + if (storedToken && !token) { setToken(storedToken); navigate(location.state?.from?.pathname || '/'); } @@ -34,8 +34,8 @@ const AuthProvider = ({ children }) => { localStorage.setItem('token', res.data.token); - setToken(res.token); - navigate(location.state?.from?.pathname || '/'); + setToken(res.data.token); + navigate(location.state?.from?.pathname || '/'); }; const handleLogout = () => { @@ -45,15 +45,18 @@ const AuthProvider = ({ children }) => { const handleRegister = async (email, password) => { const res = await register(email, password); - setToken(res.data.token); + + localStorage.setItem('token', res.data.token); + setToken(res.data.token); navigate('/verification'); }; - const handleCreateProfile = async (firstName, lastName, githubUrl, bio) => { + /* eslint-disable camelcase */ + const handleCreateProfile = async (first_name, last_name, username, github_username, mobile, bio, role, specialism, cohort, start_date, end_date, photo) => { const { userId } = jwt_decode(token); - await createProfile(userId, firstName, lastName, githubUrl, bio); + await createProfile(userId, first_name, last_name, username, github_username, mobile, bio, role, specialism, cohort, start_date, end_date, photo); localStorage.setItem('token', token); navigate('/'); diff --git a/src/context/form.js b/src/context/form.js new file mode 100644 index 00000000..c602786e --- /dev/null +++ b/src/context/form.js @@ -0,0 +1,15 @@ +import React, { createContext, useContext, useState } from 'react'; + +const FormContext = createContext(); + +export const FormProvider = ({ children }) => { + const [formData, setFormData] = useState({ email: '', password: '' }); + + return ( + + {children} + + ); +}; + +export const useFormData = () => useContext(FormContext); diff --git a/src/context/userRole..js b/src/context/userRole..js new file mode 100644 index 00000000..d3fad599 --- /dev/null +++ b/src/context/userRole..js @@ -0,0 +1,15 @@ +import { createContext, useContext, useState } from "react"; + +const UserRoleContext = createContext() + +export const UserRoleProvider = ({ children }) => { + const [userRole, setUserRole] = useState(2) + + return ( + + {children} + + ) +} + +export const useUserRoleData = () => useContext(UserRoleContext) \ 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..d476caa0 --- /dev/null +++ b/src/pages/cohort/index.js @@ -0,0 +1,22 @@ +import Students from "./students"; + +import Teachers from './teachers'; +import Exercises from "./exercises"; + +const Cohort = () => { + return ( + <> +
+ +
+ + + + ) + +} + +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..c8e4fc1d --- /dev/null +++ b/src/pages/cohort/students/index.js @@ -0,0 +1,144 @@ +import { useEffect, useState } from "react"; +import Card from "../../../components/card"; +import Student from "./student"; +import './students.css'; +import { get, getUserById } from "../../../service/apiClient"; +import jwtDecode from "jwt-decode"; +import SoftwareLogo from "../../../assets/icons/software-logo"; +import FrontEndLogo from "../../../assets/icons/frontEndLogo"; +import DataAnalyticsLogo from "../../../assets/icons/dataAnalyticsLogo"; +import '../../../components/profileCircle/style.css'; +import '../../../components/fullscreenCard/fullscreenCard.css'; + + +function Students() { + const [students, setStudents] = useState([]); + const [courses, setCourses] = useState([]); + const [cohortId, setCohortId] = useState(""); + const [selectedCourseIndex, setSelectedCourseIndex] = useState(0); // to click through courses + const course = courses[selectedCourseIndex]; + + + useEffect(() => { + async function fetchData() { + try { + const token = localStorage.getItem('token'); + const { userId } = jwtDecode(token); + const user = await getUserById(userId); + + setCohortId(user.cohort.id || ""); + + const data = await get(`cohorts/${user.cohort.id}`); + const students = data.data.cohort.profiles.filter((profileid) => profileid?.role?.name === "ROLE_STUDENT"); + setStudents(students || []); + + const courses = data.data.cohort.cohort_courses; + setCourses(courses || []); + + setSelectedCourseIndex(0); // reset to first course when data changes + + + } catch (error) { + console.error('fetchStudents() in cohort/students/index.js:', error); + } + } + fetchData(); + }, []); + + function getInitials(student) { + if (!student.firstName || !student.lastName) return "NA"; + const firstNameParts = student.firstName.trim().split(/\s+/); // split by any number of spaces + const lastNameInitial = student.lastName.trim().charAt(0); + + const firstNameInitials = firstNameParts.map(name => name.charAt(0)); + + return (firstNameInitials.join('') + lastNameInitial).toUpperCase(); + } + + if (!course) { + return
+
+

Loading...

+
+ +
+
+
+ } + + // in cohort-course-date: getCohortsForStudent() or something, make it a dropdown to select which subject to render!!!! we do NOT want to scroll through lots of students to view the next subject's students + return ( + +
+
+

My cohort

+
+ + {course && ( +
+
+ {course.name === "Software Development" && } + {course.name === "Front-End Development" && } + {course.name === "Data Analytics" && } +
+ +
+

{course.name}, Cohort {cohortId}

+
+ +
+ January 2023 - June 2023 +
+ +
+ + +
+
+ )} + + + +
+ {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/teachers/index.js b/src/pages/cohort/teachers/index.js new file mode 100644 index 00000000..bec11141 --- /dev/null +++ b/src/pages/cohort/teachers/index.js @@ -0,0 +1,63 @@ +import Card from "../../../components/card"; +import './style.css'; +import Teacher from "./teacher"; +import { get, getUserById } from "../../../service/apiClient"; +import { useEffect, useState } from "react"; +import jwtDecode from "jwt-decode"; + +const Teachers = () => { + + const [teachers, setTeachers] = useState([]); + + useEffect(() => { + async function fetchData() { + try { + const token = localStorage.getItem('token'); + const { userId } = jwtDecode(token); + const user = await getUserById(userId); + const data = await get(`cohorts/${user.cohort.id}`); + + const teachers = data.data.cohort.profiles.filter((userid) => userid?.role?.name === "ROLE_TEACHER"); + setTeachers(teachers || []); + } catch (error) { + console.error('fetchTeachers() in cohort/teachers/index.js:', error); + } + } + + fetchData(); + }, []); + + function getInitials(teacher) { + if (!teacher.firstName || !teacher.lastName) return "NA"; + const firstNameParts = teacher.firstName.trim().split(/\s+/); // split by any number of spaces + const lastNameInitial = teacher.lastName.trim().charAt(0); + + const firstNameInitials = firstNameParts.map(name => name.charAt(0)); + + return (firstNameInitials.join('') + lastNameInitial).toUpperCase(); + } + + return ( + +
+
+

Teachers

+
+ +
+ {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..27b7c9eb --- /dev/null +++ b/src/pages/dashboard/cohorts/index.js @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react" +import Card from "../../../components/card" +import { get } from "../../../service/apiClient" +import SoftwareLogo from "../../../assets/icons/software-logo" +import FrontEndLogo from "../../../assets/icons/frontEndLogo" +import DataAnalyticsLogo from "../../../assets/icons/dataAnalyticsLogo" + +const Cohorts = () => { + const [cohorts, setCohorts] = useState(null) + + + useEffect(() => { + async function fetchCohorts() { + try { + const response = await get("cohorts"); + setCohorts(response.data.cohorts); + } catch (error) { + console.error("Error fetching cohorts:", error); + } + } + + fetchCohorts(); + }, []); + + + + + return ( + <> + +

Cohorts

+
+ {cohorts !== null ? ( +
    + {cohorts.map((cohort, index) => ( +
  • +
    +
    +
      + {cohort.cohort_courses.map((course, j) => ( +
    • +
      + {course.name === "Software Development" && } + {course.name === "Front-End Development" && } + {course.name === "Data Analytics" && } +
      + {course.name} +
    • + ))} +
    +

    Cohort {cohort.id}

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

No cohorts found.

+ )} +
+ + + + + + +
+ + ) +} + +export default Cohorts \ No newline at end of file diff --git a/src/pages/dashboard/index.js b/src/pages/dashboard/index.js index 54606849..2b8852e3 100644 --- a/src/pages/dashboard/index.js +++ b/src/pages/dashboard/index.js @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import SearchIcon from '../../assets/icons/searchIcon'; import Button from '../../components/button'; import Card from '../../components/card'; @@ -7,9 +7,21 @@ 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'; const Dashboard = () => { const [searchVal, setSearchVal] = useState(''); + const onPostAddedRef = useRef(null); + const { token } = useAuth(); + const decodedToken = jwtDecode(token || localStorage.getItem('token')) || {}; + 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 } = useUserRoleData(); const onChange = (e) => { setSearchVal(e.target.value); @@ -21,25 +33,56 @@ const Dashboard = () => { // Create a function to run on user interaction const showModal = () => { // Use setModal to set the header of the modal and the component the modal should render - setModal('Create a post', ); // CreatePostModal is just a standard React component, nothing special + setModal('Create a post', ); // CreatePostModal is just a standard React component, nothing special // Open the modal! openModal(); }; + const handlePostAdded = (newPost) => { + // Call the Posts component's add function + if (onPostAddedRef.current) { + onPostAddedRef.current(newPost); + } + }; + +/* TODO TRIED ADDING CORRECT INITALS TO PROFILE CIRCLE, DIDN'T WORK +useEffect(() => { + async function fetchUser() { + try { + const { userId } = jwt_decode(token || localStorage.getItem('token')) || {}; + if (!userId) { + console.log('Could not determine user. Please log in again.'); + return; + } + const fetchedUser = await get(`users/${userId}`); + setUser(fetchedUser); + } catch (error) { + console.error('Error fetching user:', error); + setUser([]); + } + const authorName = post.user.profile + ? `${post.user.profile.firstName || 'Unknown'} ${post.user.profile.lastName || 'User'}` + : 'Unknown User'; + setUserInitials(authorName.match(/\b(\w)/g)); + } + fetchUser(); + }, []); */ + return ( <>
-

AJ

+

{initials}

+
- +
); diff --git a/src/pages/dashboard/students/index.js b/src/pages/dashboard/students/index.js new file mode 100644 index 00000000..4e1c6b4c --- /dev/null +++ b/src/pages/dashboard/students/index.js @@ -0,0 +1,75 @@ +// import { useEffect, useState } from "react"; +// import { get } from "../../../service/apiClient"; + +import { useNavigate } from "react-router-dom"; +import Card from "../../../components/card" +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.students); + } catch (error) { + console.error("Error fetching students:", error); + } + } + + fetchStudents(); + }, []); + +*/ + const navigate = useNavigate() + + const handleClick = () => { + navigate("/") + // navigate("/students") + } + + const students = [ + { first_name: "Ola", last_name: "Nordmann", specialism: "Software Development" }, + { first_name: "Kari", last_name: "Nordmann", specialism: "Data Analytics" }, + { first_name: "Per", last_name: "Hansen", specialism: "Software Development" }, + { first_name: "Anne", last_name: "Larsen", specialism: "Data Analyticsr" }, + { first_name: "Jonas", last_name: "Berg", specialism: "Software Development" }, + { first_name: "Maria", last_name: "Solberg", specialism: "Software Development" }, + { first_name: "Erik", last_name: "Johansen", specialism: "Front-End Development" }, + { first_name: "Nora", last_name: "Lie", specialism: "Front-End Development" }, + { first_name: "Henrik", last_name: "Strøm", specialism: "Front-End Development" }, + { first_name: "Silje", last_name: "Moe", specialism: "Data Analytics" } + ]; + + return( + <> + +

Students

+
+ {students !== null ? ( +
+
    + {students.slice(0,10).map((student, index) => ( +
  • +
    + +
    +
  • + ))} +
+ +
+ +
+ ):( +

No students found.

+ )} +
+
+ + ) +} +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..fe263c5c 100644 --- a/src/pages/dashboard/style.css +++ b/src/pages/dashboard/style.css @@ -19,3 +19,75 @@ aside { max-width: 100% !important; background-color: var(--color-blue5); } + + + +.course-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + font-size: 20px +} + +.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 { + margin-left: 70px; + font-size: 16px; + color: #64648C; + margin-top: 4px; +} + +.cohort-info { + margin-top: 10px; +} + +.course-name { + font-size: 20px; + font-weight: bold +} + +.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; + +} \ 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..83edc27a --- /dev/null +++ b/src/pages/dashboard/teachers/index.js @@ -0,0 +1,54 @@ +// import { useEffect, useState } from "react" +// import { get } from "../../../service/apiClient" +import Card from "../../../components/card" +import ProfileIconTeacher from "../../../components/profile-icon-teacherView" + +const TeachersDashboard = () => { + // const [teachers, setTeachers] = useState(null) + + /* useEffect(() => { + async function fetchTeachers() { + try { + const response = await get("teachers") + setTeachers(response.data.teachers) + } catch (error) { + console.error("Error fetching teachers: ", error) + } + } + fetchTeachers() + }, []) +*/ + const teachers = [ + { first_name: "Lina", last_name: "Andersen", specialism: "Software Development" }, + { first_name: "Marius", last_name: "Bakke", specialism: "Data Analytics" }, + { first_name: "Sofie", last_name: "Holt", specialism: "Software Development" }, + { first_name: "Tobias", last_name: "Nilsen", specialism: "Data Analytics" }, + { first_name: "Emilie", last_name: "Foss", specialism: "Software Development" }, + { first_name: "Daniel", last_name: "Rød", specialism: "Software Development" }, + { first_name: "Ida", last_name: "Lund", specialism: "Front-End Development" }, + { first_name: "Sebastian", last_name: "Vik", specialism: "Front-End Development" }, + { first_name: "Julie", last_name: "Aas", specialism: "Front-End Development" }, + { first_name: "Kristoffer", last_name: "Hagen", specialism: "Data Analytics" } + ] + + return ( + <> + +

Teachers

+
+
    + {teachers.slice(0,10).map((teacher, index) => ( +
  • +
    + +
    +
  • + ))} +
+
+
+ + ) +} + +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..8a95e30d --- /dev/null +++ b/src/pages/edit/edit.css @@ -0,0 +1,118 @@ +.edit-profile-form { + width: 120%; + margin: 2rem auto; + padding: 2rem; + background-color: #fff; + border: 1px solid #e6ebf5; + border-radius: 12px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); + font-family: 'Inter', sans-serif; + display: flex; + flex-direction: column; +} + +.edit-profile-form h2 { + font-size: 2rem; + margin-bottom: 2rem; + color: #333; +} + +.section h3 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: #444; +} + +.row { + display: flex; + gap: 2rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.section { + flex: 1; + min-width: 300px; +} + +.half { + width: 100%; +} + +@media (min-width: 768px) { + .half { + width: 48%; + } +} + +.section > *:not(h3):not(.photo-placeholder):not(.char-count) { + margin-bottom: 1.5rem; +} + +.photo-placeholder { + width: 80px; + height: 80px; + background-color: #ddd; + border-radius: 50%; + font-size: 1.5rem; + font-weight: bold; + color: #555; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; +} + +input, +select, +textarea { + width: 100%; + padding: 0.6rem; + 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; +} \ No newline at end of file diff --git a/src/pages/edit/index.js b/src/pages/edit/index.js new file mode 100644 index 00000000..016c758b --- /dev/null +++ b/src/pages/edit/index.js @@ -0,0 +1,227 @@ +import { useEffect, useState } from "react"; +import "./edit.css"; +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 NumberInput from "../../components/form/numberInput"; + +const EditPage = () => { + const [formData, setFormData] = useState([]); + const { token } = useAuth(); + const { userId } = jwtDecode(token) + const [formValues, setFormValues] = useState({ + photo: "", + firstName: "", + lastName: "", + username: "", + githubUsername: "", + email: "", + mobile: "", + password: "", + bio: "", + }); + + useEffect(() => { + async function fetchUser() { + try { + const data = await getUserById(userId); + setFormData(data); + + const profile = data.profile || {}; + setFormValues({ + 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); + } + } + fetchUser(); + }, [userId]); + + + if (!formData || !formData.profile) { + return
+
+

Loading...

+
+ +
+
+
+ } + + 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 handleSave = async (e) => { + e.preventDefault(); + try { + await updateUserProfile(userId, formValues); + alert("Profile updated successfully!"); + } + catch (error) { + console.error("Failed to update profile:", error); + alert("Something went wrong while saving."); + } + } + + console.log() + + return ( + <> +
+

Profile

+
+
+ n[0]).join("").toUpperCase()} /> +
+

{name}

+
+
+
+ +
+
+

Basic Info

+
AW
+ + + + +
+ +
+

Training Info

+ + + + + +
+
+ +
+
+

Contact Info

+ + + +
+ +
+

Bio

+ +
{formValues.bio.length}/300
+
+
+ +
+ + +
+
+
+ + ); +}; + +export default EditPage; diff --git a/src/pages/login/index.js b/src/pages/login/index.js index 08df7d5a..0b9341ee 100644 --- a/src/pages/login/index.js +++ b/src/pages/login/index.js @@ -4,10 +4,15 @@ 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'; const Login = () => { - const { onLogin } = useAuth(); + const { onLogin} = useAuth(); const [formData, setFormData] = useState({ email: '', password: '' }); + const {setUserRole} = useUserRoleData() const onChange = (e) => { const { name, value } = e.target; @@ -36,7 +41,19 @@ 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..bb77d316 --- /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() !== '' && ( +
+ Github URL: + + + {githubUrl} + + +
+ )} + + + {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..00307e2e --- /dev/null +++ b/src/pages/profile/profile-data/profile-data.css @@ -0,0 +1,48 @@ +.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; +} 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..56404695 100644 --- a/src/pages/register/index.js +++ b/src/pages/register/index.js @@ -1,19 +1,44 @@ -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'; const Register = () => { const { onRegister } = useAuth(); - const [formData, setFormData] = useState({ email: '', password: '' }); + const {formData, setFormData} = useFormData() const onChange = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; + 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; + } + + } + + 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; + } + } + + return (
{ type="email" name="email" label={'Email *'} + required /> { name="password" label={'Password *'} type={'password'} + required /> +
diff --git a/src/pages/welcome/index.js b/src/pages/welcome/index.js index 85af11ab..5aeca849 100644 --- a/src/pages/welcome/index.js +++ b/src/pages/welcome/index.js @@ -3,16 +3,28 @@ 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'; 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,9 +37,39 @@ 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 = (event, close) => { + + const file = event.target.files[0]; + if (file) { + const url = URL.createObjectURL(file) + setProfile(prevProfile => ({ + ...prevProfile, + photo: url + })); + close() + } + } + + + + return (
@@ -35,9 +77,11 @@ const Welcome = () => {

Create your profile to get started

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

Bio

+
+
+
+ +
+ {data.bio.length}/{300} +
+
+
+ + ); +}; + +export default StepFour diff --git a/src/pages/welcome/stepOne/index.js b/src/pages/welcome/stepOne/index.js index 317940f8..7c50155f 100644 --- a/src/pages/welcome/stepOne/index.js +++ b/src/pages/welcome/stepOne/index.js @@ -1,8 +1,12 @@ +import Popup from 'reactjs-popup'; import ProfileIcon from '../../../assets/icons/profileIcon'; + import Form from '../../../components/form'; import TextInput from '../../../components/form/textInput'; +import Card from '../../../components/card'; + -const StepOne = ({ data, setData }) => { +const StepOne = ({ data, setData, handleFileChange }) => { return ( <>
@@ -11,25 +15,82 @@ const StepOne = ({ data, setData }) => {

Photo

+
- -

Add headshot

+ {data.photo ? ( + profile photo + ) : ( )} + + {data.photo ? "Replace headshot" : "Add headshot"} + } modal> + {close => ( + +
+

+ {data.photo ? "Replace Photo" : "Upload Photo"} +

+

Choose a file to upload your headshot

+ +
+ + + handleFileChange(e, close)} + /> + +
+
+
+ )} +
+
+

Please upload a valid image file

+ + -

*Required

diff --git a/src/pages/welcome/stepThree/index.js b/src/pages/welcome/stepThree/index.js new file mode 100644 index 00000000..b226f410 --- /dev/null +++ b/src/pages/welcome/stepThree/index.js @@ -0,0 +1,50 @@ +import Form from '../../../components/form'; +import TextInput from '../../../components/form/textInput'; + +const StepThree = ({ data, setData }) => { + + + return ( + <> +
+

Training info

+
+ +
+ + + + + +
+
+ + ) +} + +export default StepThree; \ No newline at end of file diff --git a/src/pages/welcome/stepTwo/index.js b/src/pages/welcome/stepTwo/index.js index f40dad3e..82ea8610 100644 --- a/src/pages/welcome/stepTwo/index.js +++ b/src/pages/welcome/stepTwo/index.js @@ -1,14 +1,41 @@ + import Form from '../../../components/form'; +import NumberInput from '../../../components/form/numberInput'; +import TextInput from '../../../components/form/textInput'; -const StepTwo = ({ data, setData }) => { +const StepTwo = ({ data, setData, formData }) => { return ( <>
-

Bio

+

Basic info

- + + +

*Required

@@ -16,4 +43,4 @@ const StepTwo = ({ data, setData }) => { ); }; -export default StepTwo; +export default StepTwo; \ No newline at end of file diff --git a/src/pages/welcome/style.css b/src/pages/welcome/style.css index 7ff35605..3e37fce8 100644 --- a/src/pages/welcome/style.css +++ b/src/pages/welcome/style.css @@ -25,7 +25,7 @@ .welcome-form-inputs { display: grid; grid-template-rows: repeat(auto, auto); - gap: 58px; + gap: 5px; margin-bottom: 48px; } .welcome-form-popup-wrapper { @@ -42,3 +42,35 @@ grid-template-columns: 1fr 1fr; gap: 24px; } + +.welcome-counter { + margin-left: 10px; + font-size: 12px; + color:grey +} + +.bio-label { + font-size: 10px; +} + +.bio-heading { + font-size: 25px; +} + +.addHeadshot { + height: 55px; + color:#64648c; + display: flex; +} + + + +.upload-label { + background-color: var(--color-blue); + color: white; + padding: 14px 24px; + border-radius: 4px; + cursor: pointer; + text-align: center; + font-size: 20px; +} diff --git a/src/service/apiClient.js b/src/service/apiClient.js index 5f3cdbcf..757ed184 100644 --- a/src/service/apiClient.js +++ b/src/service/apiClient.js @@ -5,22 +5,108 @@ async function login(email, password) { } async function register(email, password) { - await post('users', { email, password }, false); + await post('signup', { email, password }, false); return await login(email, password); } +/* eslint-disable camelcase */ -async function createProfile(userId, firstName, lastName, githubUrl, bio) { - return await patch(`users/${userId}`, { firstName, lastName, githubUrl, bio }); +async function createProfile(userId, + first_name, + last_name, + username, + github_username, + mobile, + bio, + role, + specialism, + cohort, + start_date, + end_date, + photo) { + + cohort = parseInt(cohort) + photo = JSON.stringify(photo) + + return await post(`profiles`, { userId, + first_name, + last_name, + username, + github_username, + mobile, + bio, + role, + specialism, + cohort, + start_date, + end_date, + photo } + ); } async function getPosts() { const res = await get('posts'); return res.data.posts; } +async function getComments(postId) { + const res = await get(`posts/${String(postId)}/comments`); + return res.data.comments; +} + +async function getUserById(id) { + const res = await get(`users/${id}`); + return res.data.user; +} + +async function getMyCohortProfiles(role) { + const token = localStorage.getItem('token'); + + if (!token) { + console.error('No token found'); + } + + const { userId } = jwt_decode(token); + const user = await getUserById(userId); + const res = await get(`cohorts/${user.profile.cohort.id}`); + + if (role === "teacher") { + const teachers = res.data.cohort.profiles.filter((userid) => userid?.role?.name === "ROLE_TEACHER"); + return teachers; + } + else if (role === "student") { + const students = res.data.cohort.profiles.filter((userid) => userid?.role?.name === "ROLE_STUDENT"); + return students; + } +} + +async function updateUserProfile(userId, formValues) { + const payload = { + photo: formValues.photo || "", + first_name: formValues.firstName || "", + last_name: formValues.lastName || "", + username: formValues.username || "", + github_username: formValues.githubUsername || "", + email: formValues.email || "", + mobile: formValues.mobile || "", + password: formValues.password || "", + bio: formValues.bio || "" + }; + + return await patch(`students/${userId}`, payload); +} + async function post(endpoint, data, auth = true) { return await request('POST', endpoint, data, auth); } +async function postTo(endpoint, data, auth = true) { + return await request('POST', endpoint, data, auth); +} +async function del(endpoint, data, auth = true) { + return await request('DELETE', endpoint, data, auth); +} +async function put(endpoint, data, auth = true) { + return await request('PUT', endpoint, data, auth); +} async function patch(endpoint, data, auth = true) { return await request('PATCH', endpoint, data, auth); @@ -49,7 +135,18 @@ async function request(method, endpoint, data, auth = true) { const response = await fetch(`${API_URL}/${endpoint}`, opts); + if (!response.ok) { + const error = new Error(response.message || `Request failed with status ${response.status}`); + error.status = response.status; + throw error; + } + return response.json(); } -export { login, getPosts, register, createProfile }; + + + +export { login, getPosts, register, createProfile, get, getUserById, getComments, post, patch, put, getMyCohortProfiles, updateUserProfile, postTo, del }; + + diff --git a/src/service/mockData.js b/src/service/mockData.js index d49e98a4..6b93d5f8 100644 --- a/src/service/mockData.js +++ b/src/service/mockData.js @@ -5,8 +5,8 @@ const user = { email: 'test@email.com', cohortId: 1, role: 'STUDENT', - firstName: 'Joe', - lastName: 'Bloggs', + first_name: 'Joe', + last_name: 'Bloggs', bio: 'Lorem ipsum dolor sit amet.', githubUrl: 'https://github.com/vherus' } @@ -23,8 +23,8 @@ const posts = [ id: 1, cohortId: 1, role: 'STUDENT', - firstName: 'Sam', - lastName: 'Fletcher', + first_name: 'Sam', + last_name: 'Fletcher', bio: 'Lorem ipsum dolor sit amet.', githubUrl: 'https://github.com/vherus' } @@ -39,8 +39,8 @@ const posts = [ id: 2, cohortId: 1, role: 'STUDENT', - firstName: 'Dolor', - lastName: 'Lobortis', + first_name: 'Dolor', + last_name: 'Lobortis', bio: 'Lorem ipsum dolor sit amet.', githubUrl: 'https://github.com/vherus' }, diff --git a/src/styles/_globals.css b/src/styles/_globals.css index eb9772c6..9539bdef 100644 --- a/src/styles/_globals.css +++ b/src/styles/_globals.css @@ -35,6 +35,7 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: var(--color-offwhite); } code {