From 46bf3f9c8274ccaa0fa4407c853344315203c956 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 28 Apr 2025 22:15:38 +0200 Subject: [PATCH] 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'}}
-
)}