From 47b354db12ab6bc6899c78358e6f72eb8bc15551 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 28 Apr 2025 20:29:30 +0200 Subject: [PATCH 1/7] feat: course page changes --- .../(withmenu)/course/[courseuuid]/course.tsx | 360 +++++++++--------- 1 file changed, 185 insertions(+), 175 deletions(-) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx index 236415c3..41dfbbb1 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -60,6 +60,16 @@ const CourseClient = (props: any) => { useEffect(() => { getLearningTags() + + // Collapse chapters by default if more than 5 activities in total + if (course?.chapters) { + const totalActivities = course.chapters.reduce((sum: number, chapter: any) => sum + (chapter.activities?.length || 0), 0) + const defaultExpanded: {[key: string]: boolean} = {} + course.chapters.forEach((chapter: any) => { + defaultExpanded[chapter.chapter_uuid] = totalActivities <= 5 + }) + setExpandedChapters(defaultExpanded) + } }, [org, course]) const getActivityTypeLabel = (activityType: string) => { @@ -117,191 +127,52 @@ const CourseClient = (props: any) => { ) : ( <> -
+

Course

{course.name}

- {props.course?.thumbnail_image && org ? ( -
- ) : ( -
- )} - - - -
-
-

About

-
-

{course.about}

-
- - {learnings.length > 0 && learnings[0]?.text !== 'null' && ( -
-

- What you will learn -

-
- {learnings.map((learning: any) => { - // Handle both new format (object with text and emoji) and legacy format (string) - const learningText = typeof learning === 'string' ? learning : learning.text - const learningEmoji = typeof learning === 'string' ? null : learning.emoji - const learningId = typeof learning === 'string' ? learning : learning.id || learning.text - - if (!learningText) return null - - return ( -
-
- {learningEmoji ? ( - {learningEmoji} - ) : ( - - )} -
-

{learningText}

- {learning.link && ( - - Link to {learningText} - - - )} -
- ) - })} -
-
+
+
+ {props.course?.thumbnail_image && org ? ( +
+ ) : ( +
)} -

Course Lessons

-
- {course.chapters.map((chapter: any) => { - const isExpanded = expandedChapters[chapter.chapter_uuid] ?? true; // Default to expanded - return ( -
-
setExpandedChapters(prev => ({ - ...prev, - [chapter.chapter_uuid]: !isExpanded - }))} - > -

{chapter.name}

-
-

- {chapter.activities.length} Activities -

- - - -
-
-
-
- {chapter.activities.map((activity: any) => { - return ( -
-
-
-
- {isActivityDone(activity) ? ( -
- - -
- ) : ( -
- -
- )} -
- -
-

{activity.name}

- {isActivityCurrent(activity) && ( -
- Current -
- )} -
-
- {activity.activity_type === 'TYPE_DYNAMIC' && ( - - )} - {activity.activity_type === 'TYPE_VIDEO' && ( -
- -
- -
-
-
-
- ) - })} -
-
-
- ) - })} + {course?.trail?.runs?.find((run: any) => run.course_id == course.id) && ( + + )} + +
+
+

{course.about}

+
-
+ +
{/* Actions Box */} @@ -313,6 +184,145 @@ const CourseClient = (props: any) => {
+ + {learnings.length > 0 && learnings[0]?.text !== 'null' && ( +
+

What you will learn

+
+ {learnings.map((learning: any) => { + // Handle both new format (object with text and emoji) and legacy format (string) + const learningText = typeof learning === 'string' ? learning : learning.text + const learningEmoji = typeof learning === 'string' ? null : learning.emoji + const learningId = typeof learning === 'string' ? learning : learning.id || learning.text + + if (!learningText) return null + + return ( +
+
+ {learningEmoji ? ( + {learningEmoji} + ) : ( + + )} +
+

{learningText}

+ {learning.link && ( + + Link to {learningText} + + + )} +
+ ) + })} +
+
+ )} + +
+

Course Lessons

+
+ {course.chapters.map((chapter: any) => { + const isExpanded = expandedChapters[chapter.chapter_uuid] ?? true; // Default to expanded + return ( +
+
setExpandedChapters(prev => ({ + ...prev, + [chapter.chapter_uuid]: !isExpanded + }))} + > +

{chapter.name}

+
+

+ {chapter.activities.length} Activities +

+ + + +
+
+
+
+ {chapter.activities.map((activity: any) => { + return ( + +
+
+ {isActivityDone(activity) ? ( +
+ + +
+ ) : ( +
+ +
+ )} +
+
+
+

{activity.name}

+ {isActivityCurrent(activity) && ( +
+ Current +
+ )} +
+
+ {activity.activity_type === 'TYPE_DYNAMIC' && ( + + )} + {activity.activity_type === 'TYPE_VIDEO' && ( +
+
+
+ +
+
+ + ) + })} +
+
+
+ ) + })} +
+
{isMobile && ( From 0b2c4d3ad1b2286969d7ed9bcb6a703a3fa8dff2 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 28 Apr 2025 21:12:53 +0200 Subject: [PATCH 2/7] feat: activity page changes --- .../activity/[activityid]/activity.tsx | 169 ++++++++++++++---- .../Activity/FixedActivitySecondaryBar.tsx | 6 +- apps/web/hooks/useContributorStatus.ts | 12 +- 3 files changed, 144 insertions(+), 43 deletions(-) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 885d4ab4..6c3e6a3b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { getAPIUrl, getUriWithOrg } from '@services/config/config' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import VideoActivity from '@components/Objects/Activities/Video/Video' -import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree, X, Edit2 } from 'lucide-react' +import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree, X, Edit2, EllipsisVertical } from 'lucide-react' import { markActivityAsComplete, unmarkActivityAsComplete } from '@services/courses/activity' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' @@ -42,6 +42,43 @@ interface ActivityClientProps { course: any } +function ActivityActions({ activity, activityid, course, orgslug, assignment }: { activity: any, activityid: string, course: any, orgslug: string, assignment: any }) { + const session = useLHSession() as any; + const { contributorStatus } = useContributorStatus(course.course_uuid); + + return ( +
+ {activity && activity.published == true && activity.content.paid_access != false && ( + + {activity.activity_type != 'TYPE_ASSIGNMENT' && ( + <> + + + )} + {activity.activity_type == 'TYPE_ASSIGNMENT' && ( + <> + + + + + )} + + )} +
+ ); +} + function ActivityClient(props: ActivityClientProps) { const activityid = props.activityid const courseuuid = props.courseuuid @@ -131,6 +168,24 @@ function ActivityClient(props: ActivityClientProps) {
+ {activity && activity.published == true && activity.content.paid_access != false && ( + + { ( +
+ + +
+ )} +
+ )}
-
-
+
+
-
+
{activity && activity.published == true && activity.content.paid_access != false && ( {activity.activity_type != 'TYPE_ASSIGNMENT' && ( @@ -168,30 +223,9 @@ function ActivityClient(props: ActivityClientProps) { className="bg-emerald-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" > - Contribute to Activity + Contribute )} - - - - )} - {activity.activity_type == 'TYPE_ASSIGNMENT' && ( - <> - - - - )} @@ -249,7 +283,19 @@ function ActivityClient(props: ActivityClientProps) { )} )} - + + {/* Activity Actions below the content box */} + {activity && activity.published == true && activity.content.paid_access != false && ( +
+ +
+ )} {/* Fixed Activity Secondary Bar */} {activity && activity.published == true && activity.content.paid_access != false && ( @@ -483,15 +529,66 @@ function NextActivityButton({ course, currentActivityId, orgslug }: { course: an }; return ( - -
- {!isMobile && Next} - -
-
+
+ Next + + {nextActivity.name} + +
+ ); +} + +function PreviousActivityButton({ course, currentActivityId, orgslug }: { course: any, currentActivityId: string, orgslug: string }) { + const router = useRouter(); + const isMobile = useMediaQuery('(max-width: 768px)'); + + const findPreviousActivity = () => { + let allActivities: any[] = []; + let currentIndex = -1; + + // Flatten all activities from all chapters + course.chapters.forEach((chapter: any) => { + chapter.activities.forEach((activity: any) => { + const cleanActivityUuid = activity.activity_uuid?.replace('activity_', ''); + allActivities.push({ + ...activity, + cleanUuid: cleanActivityUuid, + chapterName: chapter.name + }); + + // Check if this is the current activity + if (activity.id === currentActivityId) { + currentIndex = allActivities.length - 1; + } + }); + }); + + // Get previous activity + return currentIndex > 0 ? allActivities[currentIndex - 1] : null; + }; + + const previousActivity = findPreviousActivity(); + + if (!previousActivity) return null; + + const navigateToActivity = () => { + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + router.push(getUriWithOrg(orgslug, '') + `/course/${cleanCourseUuid}/activity/${previousActivity.cleanUuid}`); + }; + + return ( +
+ + Previous + + {previousActivity.name} +
); } diff --git a/apps/web/components/Pages/Activity/FixedActivitySecondaryBar.tsx b/apps/web/components/Pages/Activity/FixedActivitySecondaryBar.tsx index 41679d4e..ae86377a 100644 --- a/apps/web/components/Pages/Activity/FixedActivitySecondaryBar.tsx +++ b/apps/web/components/Pages/Activity/FixedActivitySecondaryBar.tsx @@ -160,11 +160,7 @@ export default function FixedActivitySecondaryBar(props: FixedActivitySecondaryB + + +
+
)} {blockObject && !isEditable && ( -
+
Date: Mon, 28 Apr 2025 22:15:38 +0200 Subject: [PATCH 4/7] feat: introduce focus mode --- .../activity/[activityid]/activity.tsx | 614 +++++++++++++----- 1 file changed, 459 insertions(+), 155 deletions(-) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 6c3e6a3b..97c409c2 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { getAPIUrl, getUriWithOrg } from '@services/config/config' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import VideoActivity from '@components/Objects/Activities/Video/Video' -import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree, X, Edit2, EllipsisVertical } from 'lucide-react' +import { BookOpenCheck, Check, CheckCircle, ChevronDown, ChevronLeft, ChevronRight, FileText, Folder, List, Menu, MoreVertical, UserRoundPen, Video, Layers, ListFilter, ListTree, X, Edit2, EllipsisVertical, Maximize2, Minimize2 } from 'lucide-react' import { markActivityAsComplete, unmarkActivityAsComplete } from '@services/courses/activity' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' @@ -16,7 +16,7 @@ import { CourseProvider } from '@components/Contexts/CourseContext' import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk' import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext' import { useLHSession } from '@components/Contexts/LHSessionContext' -import React, { useEffect } from 'react' +import React, { useEffect, useRef } from 'react' import { getAssignmentFromActivityUUID, getFinalGrade, submitAssignmentForGrading } from '@services/courses/assignments' import AssignmentStudentActivity from '@components/Objects/Activities/Assignment/AssignmentStudentActivity' import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext' @@ -33,6 +33,7 @@ import ActivityNavigation from '@components/Pages/Activity/ActivityNavigation' import ActivityChapterDropdown from '@components/Pages/Activity/ActivityChapterDropdown' import FixedActivitySecondaryBar from '@components/Pages/Activity/FixedActivitySecondaryBar' import CourseEndView from '@components/Pages/Activity/CourseEndView' +import { motion, AnimatePresence } from 'framer-motion' interface ActivityClientProps { activityid: string @@ -42,7 +43,16 @@ interface ActivityClientProps { course: any } -function ActivityActions({ activity, activityid, course, orgslug, assignment }: { activity: any, activityid: string, course: any, orgslug: string, assignment: any }) { +interface ActivityActionsProps { + activity: any + activityid: string + course: any + orgslug: string + assignment: any + showNavigation?: boolean +} + +function ActivityActions({ activity, activityid, course, orgslug, assignment, showNavigation = true }: ActivityActionsProps) { const session = useLHSession() as any; const { contributorStatus } = useContributorStatus(course.course_uuid); @@ -73,6 +83,9 @@ function ActivityActions({ activity, activityid, course, orgslug, assignment }: )} + {showNavigation && ( + + )} )}
@@ -92,8 +105,65 @@ function ActivityClient(props: ActivityClientProps) { const [bgColor, setBgColor] = React.useState('bg-white') const [assignment, setAssignment] = React.useState(null) as any; const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false); + const [isFocusMode, setIsFocusMode] = React.useState(false); + const isInitialRender = useRef(true); const { contributorStatus } = useContributorStatus(courseuuid); - + const router = useRouter(); + + // Function to find the current activity's position in the course + const findActivityPosition = () => { + let allActivities: any[] = []; + let currentIndex = -1; + + // Flatten all activities from all chapters + course.chapters.forEach((chapter: any) => { + chapter.activities.forEach((activity: any) => { + const cleanActivityUuid = activity.activity_uuid?.replace('activity_', ''); + allActivities.push({ + ...activity, + cleanUuid: cleanActivityUuid, + chapterName: chapter.name + }); + + // Check if this is the current activity + if (cleanActivityUuid === activityid.replace('activity_', '')) { + currentIndex = allActivities.length - 1; + } + }); + }); + + return { allActivities, currentIndex }; + }; + + const { allActivities, currentIndex } = findActivityPosition(); + + // Get previous and next activities + const prevActivity = currentIndex > 0 ? allActivities[currentIndex - 1] : null; + const nextActivity = currentIndex < allActivities.length - 1 ? allActivities[currentIndex + 1] : null; + + // Navigate to an activity + const navigateToActivity = (activity: any) => { + if (!activity) return; + + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + router.push(getUriWithOrg(orgslug, '') + `/course/${cleanCourseUuid}/activity/${activity.cleanUuid}`); + }; + + // Initialize focus mode from localStorage + React.useEffect(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem('globalFocusMode'); + setIsFocusMode(saved === 'true'); + } + }, []); + + // Save focus mode to localStorage + React.useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('globalFocusMode', isFocusMode.toString()); + isInitialRender.current = false; + } + }, [isFocusMode]); function getChapterNameByActivityId(course: any, activity_id: any) { for (let i = 0; i < course.chapters.length; i++) { @@ -115,43 +185,72 @@ function ActivityClient(props: ActivityClientProps) { useEffect(() => { if (activity.activity_type == 'TYPE_DYNAMIC') { - setBgColor('bg-white nice-shadow'); + setBgColor(isFocusMode ? 'bg-white' : 'bg-white nice-shadow'); } else if (activity.activity_type == 'TYPE_ASSIGNMENT') { setMarkStatusButtonActive(false); - setBgColor('bg-white nice-shadow'); + setBgColor(isFocusMode ? 'bg-white' : 'bg-white nice-shadow'); getAssignmentUI(); } else { - setBgColor('bg-zinc-950'); + setBgColor(isFocusMode ? 'bg-zinc-950' : 'bg-zinc-950 nice-shadow'); } } - , [activity, pathname]) + , [activity, pathname, isFocusMode]) return ( <> - - {activityid === 'end' ? ( - - ) : ( -
-
-
-
-
+ {isFocusMode ? ( + + + {/* Focus Mode Top Bar */} + +
+
+
+ setIsFocusMode(false)} + className="bg-white nice-shadow p-2 rounded-full cursor-pointer hover:bg-gray-50" + title="Exit focus mode" + > + + + +
+ + {/* Center Course Info */} +
-

Course

-

+

Course

+

{course.name}

-
- {activity && activity.published == true && activity.content.paid_access != false && ( - - { ( -
- - -
- )} -
- )} + + + {/* Progress Indicator */} + +
+ + + run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1))} + /> + +
+ + {Math.round(((course.trail?.runs?.find((run: any) => run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0) / (course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 1)) * 100)}% + +
+
+
+ {course.trail?.runs?.find((run: any) => run.course_id === course.id)?.steps?.filter((step: any) => step.complete)?.length || 0} of {course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0} +
+
+
+ - + {/* Focus Mode Content */} +
+
+ {activity && activity.published == true && ( + <> + {activity.content.paid_access == false ? ( + + ) : ( + + {/* Activity Types */} +
+ {activity.activity_type == 'TYPE_DYNAMIC' && ( + + )} + {activity.activity_type == 'TYPE_VIDEO' && ( + + )} + {activity.activity_type == 'TYPE_DOCUMENT' && ( + + )} + {activity.activity_type == 'TYPE_ASSIGNMENT' && ( +
+ {assignment ? ( + + + + + + + + ) : ( +
+ )} +
+ )} +
+
+ )} + + )} +
+
-
-
- -
-

- Chapter : {getChapterNameByActivityId(course, activity.id)} -

-

- {activity.name} -

+ {/* Focus Mode Bottom Bar */} + {activity && activity.published == true && activity.content.paid_access != false && ( + +
+
+
+ +
+
+ +
-
+
+ + )} + + + ) : ( + + {/* Original non-focus mode UI */} + {activityid === 'end' ? ( + + ) : ( +
+
+
+
+
+
+ + + +
+
+

Course

+

+ {course.name} +

+
+
{activity && activity.published == true && activity.content.paid_access != false && ( - {activity.activity_type != 'TYPE_ASSIGNMENT' && ( - <> - - {contributorStatus === 'ACTIVE' && activity.activity_type == 'TYPE_DYNAMIC' && ( - - - Contribute - - )} - + { ( +
+ + +
)}
)}
-
-
- {activity && activity.published == false && ( -
-
-

- This activity is not published yet -

-
-
- )} - {activity && activity.published == true && ( - <> - {activity.content.paid_access == false ? ( - - ) : ( -
- {/* Activity Types */} -
- {activity.activity_type == 'TYPE_DYNAMIC' && ( - - )} - {activity.activity_type == 'TYPE_VIDEO' && ( - - )} - {activity.activity_type == 'TYPE_DOCUMENT' && ( - - )} - {activity.activity_type == 'TYPE_ASSIGNMENT' && ( -
- {assignment ? ( - - - - - - - - ) : ( -
- )} -
- )} + + +
+
+ + +
+

+ Chapter : {getChapterNameByActivityId(course, activity.id)} +

+

+ {activity.name} +

- )} - - )} - - {/* Activity Actions below the content box */} - {activity && activity.published == true && activity.content.paid_access != false && ( -
- +
+ {activity && activity.published == true && activity.content.paid_access != false && ( + + {activity.activity_type != 'TYPE_ASSIGNMENT' && ( + <> + + {contributorStatus === 'ACTIVE' && activity.activity_type == 'TYPE_DYNAMIC' && ( + + + Contribute + + )} + + )} + + )} +
+
- )} - {/* Fixed Activity Secondary Bar */} - {activity && activity.published == true && activity.content.paid_access != false && ( - - )} - -
+ {activity && activity.published == false && ( +
+
+

+ This activity is not published yet +

+
+
+ )} + + {activity && activity.published == true && ( + <> + {activity.content.paid_access == false ? ( + + ) : ( +
+ {/* Activity Types */} +
+ {activity.activity_type == 'TYPE_DYNAMIC' && ( + + )} + {activity.activity_type == 'TYPE_VIDEO' && ( + + )} + {activity.activity_type == 'TYPE_DOCUMENT' && ( + + )} + {activity.activity_type == 'TYPE_ASSIGNMENT' && ( +
+ {assignment ? ( + + + + + + + + ) : ( +
+ )} +
+ )} +
+
+ )} + + )} + + {/* Activity Actions below the content box */} + {activity && activity.published == true && activity.content.paid_access != false && ( +
+ +
+ )} + + {/* Fixed Activity Secondary Bar */} + {activity && activity.published == true && activity.content.paid_access != false && ( + + )} + +
+
-
- )} -
+ )} + + )} @@ -461,7 +767,6 @@ export function MarkStatus(props: { status="warning" /> -
) : (
@@ -483,7 +788,6 @@ export function MarkStatus(props: { )}{' '} {!isMobile && {isLoading ? 'Marking...' : 'Mark as complete'}}
-
)} From 59b5ac47937d9622097d5d20c280952ecff480ac Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 29 Apr 2025 14:44:34 +0200 Subject: [PATCH 5/7] feat: disable org menu when in focus mode on the activity page --- .../activity/[activityid]/activity.tsx | 4 ++ apps/web/components/Objects/Menus/OrgMenu.tsx | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 97c409c2..c629dbf3 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -161,6 +161,10 @@ function ActivityClient(props: ActivityClientProps) { React.useEffect(() => { if (typeof window !== 'undefined') { localStorage.setItem('globalFocusMode', isFocusMode.toString()); + // Dispatch custom event for focus mode change + window.dispatchEvent(new CustomEvent('focusModeChange', { + detail: { isFocusMode } + })); isInitialRender.current = false; } }, [isFocusMode]); diff --git a/apps/web/components/Objects/Menus/OrgMenu.tsx b/apps/web/components/Objects/Menus/OrgMenu.tsx index 6ecf1c18..265a85c4 100644 --- a/apps/web/components/Objects/Menus/OrgMenu.tsx +++ b/apps/web/components/Objects/Menus/OrgMenu.tsx @@ -1,5 +1,5 @@ 'use client' -import React from 'react' +import React, { useEffect, useState } from 'react' import Link from 'next/link' import { Search } from 'lucide-react' import { getUriWithOrg } from '@services/config/config' @@ -9,6 +9,7 @@ import { getOrgLogoMediaDirectory } from '@services/media/media' import { useLHSession } from '@components/Contexts/LHSessionContext' import { useOrg } from '@components/Contexts/OrgContext' import { SearchBar } from '@components/Objects/Search/SearchBar' +import { usePathname } from 'next/navigation' export const OrgMenu = (props: any) => { const orgslug = props.orgslug @@ -17,6 +18,41 @@ export const OrgMenu = (props: any) => { const [feedbackModal, setFeedbackModal] = React.useState(false) const org = useOrg() as any; const [isMenuOpen, setIsMenuOpen] = React.useState(false) + const [isFocusMode, setIsFocusMode] = useState(false) + const pathname = usePathname() + + useEffect(() => { + // Only check focus mode if we're in an activity page + if (typeof window !== 'undefined' && pathname?.includes('/activity/')) { + const saved = localStorage.getItem('globalFocusMode'); + setIsFocusMode(saved === 'true'); + } else { + setIsFocusMode(false); + } + + // Add storage event listener for cross-window changes + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'globalFocusMode' && pathname?.includes('/activity/')) { + setIsFocusMode(e.newValue === 'true'); + } + }; + + // Add custom event listener for same-window changes + const handleFocusModeChange = (e: CustomEvent) => { + if (pathname?.includes('/activity/')) { + setIsFocusMode(e.detail.isFocusMode); + } + }; + + window.addEventListener('storage', handleStorageChange); + window.addEventListener('focusModeChange', handleFocusModeChange as EventListener); + + // Cleanup + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener('focusModeChange', handleFocusModeChange as EventListener); + }; + }, [pathname]); function closeFeedbackModal() { setFeedbackModal(false) @@ -26,6 +62,11 @@ export const OrgMenu = (props: any) => { setIsMenuOpen(!isMenuOpen) } + // Only hide menu if we're in an activity page and focus mode is enabled + if (pathname?.includes('/activity/') && isFocusMode) { + return null; + } + return ( <>
From 8c2b1e5b99cb55cada188195a6d2b57fe49f6200 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 29 Apr 2025 18:00:38 +0200 Subject: [PATCH 6/7] chore: update Next.js to 15.3.1 --- apps/web/package.json | 2 +- apps/web/pnpm-lock.yaml | 316 ++++++++++++++++++++++++++++++++++------ 2 files changed, 269 insertions(+), 49 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 4f594216..5fd4bd82 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,7 +63,7 @@ "katex": "^0.16.21", "lowlight": "^3.3.0", "lucide-react": "^0.453.0", - "next": "15.2.4", + "next": "15.3.1", "next-auth": "^4.24.11", "nextjs-toploader": "^1.6.12", "prosemirror-state": "^1.4.3", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index a058da32..b7290c38 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -169,14 +169,14 @@ importers: specifier: ^0.453.0 version: 0.453.0(react@19.0.0) next: - specifier: 15.2.4 - version: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.3.1 + version: 15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-auth: specifier: ^4.24.11 - version: 4.24.11(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.24.11(next@15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nextjs-toploader: specifier: ^1.6.12 - version: 1.6.12(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.6.12(next@15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) prosemirror-state: specifier: ^1.4.3 version: 1.4.3 @@ -412,158 +412,268 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@0.2.8': resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} - '@next/env@15.2.4': - resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} + '@next/env@15.3.1': + resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} '@next/eslint-plugin-next@15.2.1': resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==} - '@next/swc-darwin-arm64@15.2.4': - resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==} + '@next/swc-darwin-arm64@15.3.1': + resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.4': - resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==} + '@next/swc-darwin-x64@15.3.1': + resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.4': - resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==} + '@next/swc-linux-arm64-gnu@15.3.1': + resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.2.4': - resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==} + '@next/swc-linux-arm64-musl@15.3.1': + resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.2.4': - resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==} + '@next/swc-linux-x64-gnu@15.3.1': + resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.2.4': - resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==} + '@next/swc-linux-x64-musl@15.3.1': + resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.2.4': - resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==} + '@next/swc-win32-arm64-msvc@15.3.1': + resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.4': - resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==} + '@next/swc-win32-x64-msvc@15.3.1': + resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2768,8 +2878,8 @@ packages: nodemailer: optional: true - next@15.2.4: - resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==} + next@15.3.1: + resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -3216,6 +3326,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3657,76 +3771,154 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.4.0 optional: true + '@img/sharp-wasm32@0.34.1': + dependencies: + '@emnapi/runtime': 1.4.0 + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.1': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.1': + optional: true + '@napi-rs/wasm-runtime@0.2.8': dependencies: '@emnapi/core': 1.4.0 @@ -3734,34 +3926,34 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.2.4': {} + '@next/env@15.3.1': {} '@next/eslint-plugin-next@15.2.1': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.4': + '@next/swc-darwin-arm64@15.3.1': optional: true - '@next/swc-darwin-x64@15.2.4': + '@next/swc-darwin-x64@15.3.1': optional: true - '@next/swc-linux-arm64-gnu@15.2.4': + '@next/swc-linux-arm64-gnu@15.3.1': optional: true - '@next/swc-linux-arm64-musl@15.2.4': + '@next/swc-linux-arm64-musl@15.3.1': optional: true - '@next/swc-linux-x64-gnu@15.2.4': + '@next/swc-linux-x64-gnu@15.3.1': optional: true - '@next/swc-linux-x64-musl@15.2.4': + '@next/swc-linux-x64-musl@15.3.1': optional: true - '@next/swc-win32-arm64-msvc@15.2.4': + '@next/swc-win32-arm64-msvc@15.3.1': optional: true - '@next/swc-win32-x64-msvc@15.2.4': + '@next/swc-win32-x64-msvc@15.3.1': optional: true '@nodelib/fs.scandir@2.1.5': @@ -6119,13 +6311,13 @@ snapshots: natural-compare@1.4.0: {} - next-auth@4.24.11(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next-auth@4.24.11(next@15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@babel/runtime': 7.27.0 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.26.5 @@ -6134,9 +6326,9 @@ snapshots: react-dom: 19.0.0(react@19.0.0) uuid: 8.3.2 - next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.2.4 + '@next/env': 15.3.1 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -6146,23 +6338,23 @@ snapshots: react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.4 - '@next/swc-darwin-x64': 15.2.4 - '@next/swc-linux-arm64-gnu': 15.2.4 - '@next/swc-linux-arm64-musl': 15.2.4 - '@next/swc-linux-x64-gnu': 15.2.4 - '@next/swc-linux-x64-musl': 15.2.4 - '@next/swc-win32-arm64-msvc': 15.2.4 - '@next/swc-win32-x64-msvc': 15.2.4 + '@next/swc-darwin-arm64': 15.3.1 + '@next/swc-darwin-x64': 15.3.1 + '@next/swc-linux-arm64-gnu': 15.3.1 + '@next/swc-linux-arm64-musl': 15.3.1 + '@next/swc-linux-x64-gnu': 15.3.1 + '@next/swc-linux-x64-musl': 15.3.1 + '@next/swc-win32-arm64-msvc': 15.3.1 + '@next/swc-win32-x64-msvc': 15.3.1 '@opentelemetry/api': 1.9.0 - sharp: 0.33.5 + sharp: 0.34.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextjs-toploader@1.6.12(next@15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + nextjs-toploader@1.6.12(next@15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - next: 15.2.4(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.3.1(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nprogress: 0.2.0 prop-types: 15.8.1 react: 19.0.0 @@ -6666,6 +6858,34 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.1: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 From dcfcb06b2e9a1943fa767e0fa7a6586d9b28eba0 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 5 May 2025 22:38:44 +0200 Subject: [PATCH 7/7] fix: improve authorization logic in RBAC by refining action checks and simplifying role permission validation --- apps/api/src/security/rbac/rbac.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 8a16afd3..1e56238c 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -60,7 +60,11 @@ async def authorization_verify_if_user_is_author( element_uuid: str, db_session: Session, ): - if action == "update" or "delete" or "read": + # For create action, we don't need to check existing resource + if action == "create": + return True # Allow creation if user is authenticated + + if action in ["update", "delete", "read"]: statement = select(ResourceAuthor).where( ResourceAuthor.resource_uuid == element_uuid ) @@ -79,6 +83,7 @@ async def authorization_verify_if_user_is_author( return False else: return False + return False # Tested and working @@ -101,17 +106,17 @@ async def authorization_verify_based_on_roles( user_roles_in_organization_and_standard_roles = db_session.exec(statement).all() - # Find in roles list if there is a role that matches users action for this type of element + # Check all roles until we find one that grants the permission for role in user_roles_in_organization_and_standard_roles: role = Role.model_validate(role) if role.rights: rights = role.rights - if rights[element_type][f"action_{action}"] is True: + element_rights = getattr(rights, element_type, None) + if element_rights and getattr(element_rights, f"action_{action}", False): return True - else: - return False - else: - return False + + # If we get here, no role granted the permission + return False async def authorization_verify_based_on_org_admin_status( @@ -133,13 +138,13 @@ async def authorization_verify_based_on_org_admin_status( user_roles_in_organization_and_standard_roles = db_session.exec(statement).all() - # Find in roles list if there is a role that matches users action for this type of element + # Check if user has admin role (role_id 1 or 2) in any organization for role in user_roles_in_organization_and_standard_roles: role = Role.model_validate(role) - if role.id == 1 or role.id == 2: + if role.id in [1, 2]: # Assuming 1 and 2 are admin role IDs return True - else: - return False + + return False # Tested and working