From 59bae82ee77a6e9268acee54ec65f11f15c10559 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 12 Jun 2025 16:09:41 +0200 Subject: [PATCH] feat: perf improvements and bug fixes --- apps/api/src/services/courses/courses.py | 46 ++----- .../activity/[activityuuid]/edit/page.tsx | 2 +- .../activity/[activityid]/activity.tsx | 127 +++++++++++++----- .../activity/[activityid]/page.tsx | 2 +- .../(withmenu)/course/[courseuuid]/course.tsx | 21 ++- .../(withmenu)/course/[courseuuid]/page.tsx | 11 +- .../CourseActions/CourseActionsMobile.tsx | 16 ++- .../Courses/CourseActions/CoursesActions.tsx | 40 ++++-- .../Activity/ActivityChapterDropdown.tsx | 23 +++- .../Pages/Courses/ActivityIndicators.tsx | 24 ++-- 10 files changed, 200 insertions(+), 112 deletions(-) diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 9922a57c..737cd8fe 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -17,7 +17,7 @@ from src.db.courses.courses import ( CourseCreate, CourseRead, CourseUpdate, - FullCourseReadWithTrail, + FullCourseRead, AuthorWithRole, ) from src.security.rbac.rbac import ( @@ -129,7 +129,7 @@ async def get_course_meta( with_unpublished_activities: bool, current_user: PublicUser | AnonymousUser, db_session: Session, -) -> FullCourseReadWithTrail: +) -> FullCourseRead: # Avoid circular import from src.services.courses.chapters import get_course_chapters @@ -156,30 +156,10 @@ async def get_course_meta( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - # Start async tasks concurrently - tasks = [] - - # Task 1: Get course chapters - async def get_chapters(): - # Ensure course.id is not None - if course.id is None: - return [] - return await get_course_chapters(request, course.id, db_session, current_user, with_unpublished_activities) - - # Task 2: Get user trail (only for authenticated users) - async def get_trail(): - if isinstance(current_user, AnonymousUser): - return None - return await get_user_trail_with_orgid( - request, current_user, course.org_id, db_session - ) - - # Add tasks to the list - tasks.append(get_chapters()) - tasks.append(get_trail()) - - # Run all tasks concurrently - chapters, trail = await asyncio.gather(*tasks) + # Get course chapters + chapters = [] + if course.id is not None: + chapters = await get_course_chapters(request, course.id, db_session, current_user, with_unpublished_activities) # Convert to AuthorWithRole objects authors = [ @@ -193,14 +173,14 @@ async def get_course_meta( for resource_author, user in author_results ] - # Create course read model - course_read = CourseRead(**course.model_dump(), authors=authors) - - return FullCourseReadWithTrail( - **course_read.model_dump(), - chapters=chapters, - trail=trail, + # Create course read model with chapters + course_read = FullCourseRead( + **course.model_dump(), + authors=authors, + chapters=chapters ) + + return course_read async def get_courses_orgslug( diff --git a/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx b/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx index 85030599..d6e99450 100644 --- a/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx +++ b/apps/web/app/editor/course/[courseid]/activity/[activityuuid]/edit/page.tsx @@ -24,7 +24,7 @@ export async function generateMetadata(props: MetadataProps): Promise // Get Org context information const course_meta = await getCourseMetadata( params.courseid, - { revalidate: 0, tags: ['courses'] }, + { revalidate: 60, tags: ['courses'] }, access_token ? access_token : null ) 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 7e59cd63..193ed281 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 @@ -16,6 +16,8 @@ import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/Assign import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext' import toast from 'react-hot-toast' import { mutate } from 'swr' +import useSWR from 'swr' +import { swrFetcher } from '@services/utils/ts/requests' import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal' import { useMediaQuery } from 'usehooks-ts' import PaidCourseActivityDisclaimer from '@components/Objects/Courses/CourseActions/PaidCourseActivityDisclaimer' @@ -32,6 +34,7 @@ import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/Ge import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import { revalidateTags } from '@services/utils/ts/requests' import UserAvatar from '@components/Objects/UserAvatar' +import CoursesActions from '@components/Objects/Courses/CourseActions/CoursesActions' // Lazy load heavy components const Canva = lazy(() => import('@components/Objects/Activities/DynamicCanva/DynamicCanva')) @@ -94,8 +97,18 @@ function useActivityPosition(course: any, activityId: string) { } function ActivityActions({ activity, activityid, course, orgslug, assignment, showNavigation = true }: ActivityActionsProps) { - const session = useLHSession() as any; + const { contributorStatus } = useContributorStatus(course.course_uuid); + const org = useOrg() as any; + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + // Add SWR for trail data + const { data: trailData } = useSWR( + `${getAPIUrl()}trail/org/${org?.id}/trail`, + (url) => swrFetcher(url, access_token) + ); + return (
@@ -108,6 +121,7 @@ function ActivityActions({ activity, activityid, course, orgslug, assignment, sh activityid={activityid} course={course} orgslug={orgslug} + trailData={trailData} /> )} @@ -171,6 +185,12 @@ function ActivityClient(props: ActivityClientProps) { const { contributorStatus } = useContributorStatus(courseuuid); const router = useRouter(); + // Add SWR for trail data + const { data: trailData, error: error } = useSWR( + `${getAPIUrl()}trail/org/${org?.id}/trail`, + (url) => swrFetcher(url, access_token) + ) + // Memoize activity position calculation const { allActivities, currentIndex } = useActivityPosition(course, activityid); @@ -331,17 +351,17 @@ function ActivityClient(props: ActivityClientProps) { fill="none" strokeLinecap="round" strokeDasharray={2 * Math.PI * 14} - strokeDashoffset={2 * Math.PI * 14 * (1 - (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))} + strokeDashoffset={2 * Math.PI * 14 * (1 - (trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.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)}% + {Math.round(((trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.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} + {trailData?.runs?.find((run: any) => run.course_uuid === course.course_uuid)?.steps?.filter((step: any) => step.complete)?.length || 0} of {course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0}
@@ -386,6 +406,7 @@ function ActivityClient(props: ActivityClientProps) { course={course} currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')} orgslug={orgslug} + trailData={trailData} />
@@ -639,6 +661,7 @@ function ActivityClient(props: ActivityClientProps) { course={course} currentActivityId={activity.activity_uuid ? activity.activity_uuid.replace('activity_', '') : activityid.replace('activity_', '')} orgslug={orgslug} + trailData={trailData} /> {contributorStatus === 'ACTIVE' && activity.activity_type == 'TYPE_DYNAMIC' && ( { if (typeof window !== 'undefined') { const markedTooltipCount = localStorage.getItem('activity_marked_tooltip_count'); @@ -799,8 +825,8 @@ export function MarkStatus(props: { ); const areAllActivitiesCompleted = () => { - const run = props.course.trail.runs.find( - (run: any) => run.course_id == props.course.id + const run = props.trailData?.runs?.find( + (run: any) => run.course_uuid === props.course.course_uuid ); if (!run) return false; @@ -811,7 +837,7 @@ export function MarkStatus(props: { chapter.activities.forEach((activity: any) => { totalActivities++; const isCompleted = run.steps.find( - (step: any) => step.activity_id === activity.id && step.complete === true + (step: any) => step.activity_uuid === activity.activity_uuid && step.complete === true ); if (isCompleted) { completedActivities++; @@ -826,23 +852,19 @@ export function MarkStatus(props: { try { const willCompleteAll = areAllActivitiesCompleted(); setIsLoading(true); - // refresh the page after marking the activity as complete - await revalidateTags(['courses'], props.orgslug); - router.refresh(); + await markActivityAsComplete( props.orgslug, props.course.course_uuid, props.activity.activity_uuid, session.data?.tokens?.access_token ); - - await mutate(`${getAPIUrl()}courses/${props.course.course_uuid}/meta`); + + await mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`); if (willCompleteAll) { const cleanCourseUuid = props.course.course_uuid.replace('course_', ''); router.push(getUriWithOrg(props.orgslug, '') + `/course/${cleanCourseUuid}/activity/end`); - } else { - router.refresh(); } } catch (error) { console.error('Error marking activity as complete:', error); @@ -855,15 +877,15 @@ export function MarkStatus(props: { async function unmarkActivityAsCompleteFront() { try { setIsLoading(true); + await unmarkActivityAsComplete( props.orgslug, props.course.course_uuid, props.activity.activity_uuid, session.data?.tokens?.access_token ); - await revalidateTags(['courses'], props.orgslug); - await mutate(`${getAPIUrl()}courses/${props.course.course_uuid}/meta`); - router.refresh(); + + await mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`); } catch (error) { toast.error('Failed to unmark activity as complete'); } finally { @@ -872,14 +894,28 @@ export function MarkStatus(props: { } const isActivityCompleted = () => { - let run = props.course.trail.runs.find( - (run: any) => run.course_id == props.course.id - ) + // Clean up course UUID by removing 'course_' prefix if it exists + const cleanCourseUuid = props.course.course_uuid?.replace('course_', ''); + + let run = props.trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ); + if (run) { + // Find the step that matches the current activity return run.steps.find( - (step: any) => (step.activity_id == props.activity.id) && (step.complete == true) - ) + (step: any) => step.activity_id === props.activity.id && step.complete === true + ); } + return false; + } + + // Don't render until we have trail data + if (!props.trailData) { + return null; } return ( @@ -931,24 +967,41 @@ export function MarkStatus(props: {
Status
- - - - {isLoading ? 'Marking...' : 'Mark as complete'} + {isLoading ? ( +
+ + + +
+ ) : ( + + + + )} + {isLoading ? 'Marking...' : 'Mark as complete'}
{showUnmarkedTooltip && ( diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx index 5e5b459c..f49295a6 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx @@ -21,7 +21,7 @@ type Session = { async function fetchCourseMetadata(courseuuid: string, access_token: string | null | undefined) { return await getCourseMetadata( courseuuid, - { revalidate: 0, tags: ['courses'] }, + { revalidate: 60, tags: ['courses'] }, access_token || null ) } 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 cfb59f4f..dce108e1 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -1,9 +1,9 @@ 'use client' import Link from 'next/link' import React, { useEffect, useState } from 'react' -import { getUriWithOrg } from '@services/config/config' +import { getUriWithOrg, getAPIUrl } from '@services/config/config' import PageLoading from '@components/Objects/Loaders/PageLoading' -import { revalidateTags } from '@services/utils/ts/requests' +import { revalidateTags, swrFetcher } from '@services/utils/ts/requests' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import { useRouter } from 'next/navigation' import GeneralWrapperStyled from '@components/Objects/StyledElements/Wrappers/GeneralWrapper' @@ -18,6 +18,8 @@ import CoursesActions from '@components/Objects/Courses/CourseActions/CoursesAct import CourseActionsMobile from '@components/Objects/Courses/CourseActions/CourseActionsMobile' import CourseAuthors from '@components/Objects/Courses/CourseAuthors/CourseAuthors' import CourseBreadcrumbs from '@components/Pages/Courses/CourseBreadcrumbs' +import { useLHSession } from '@components/Contexts/LHSessionContext' +import useSWR from 'swr' const CourseClient = (props: any) => { const [learnings, setLearnings] = useState([]) @@ -28,6 +30,14 @@ const CourseClient = (props: any) => { const org = useOrg() as any const router = useRouter() const isMobile = useMediaQuery('(max-width: 768px)') + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + // Add SWR for trail data + const { data: trailData } = useSWR( + `${getAPIUrl()}trail/org/${org?.id}/trail`, + (url) => swrFetcher(url, access_token) + ); console.log(course) @@ -178,7 +188,7 @@ const CourseClient = (props: any) => {
{/* Actions Box */} - + {/* Authors & Updates Box */}
@@ -329,10 +339,9 @@ const CourseClient = (props: any) => {
+ {/* Mobile Actions Box */} {isMobile && ( -
- -
+ )} )} diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx index df4fbccd..a6f749ea 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx @@ -24,7 +24,7 @@ export async function generateMetadata(props: MetadataProps): Promise }) const course_meta = await getCourseMetadata( params.courseuuid, - { revalidate: 0, tags: ['courses'] }, + { revalidate: 60, tags: ['courses'] }, access_token ? access_token : null ) @@ -69,17 +69,20 @@ const CoursePage = async (params: any) => { const session = await getServerSession(nextAuthOptions) const access_token = session?.tokens?.access_token + // Await params before using them + const { courseuuid, orgslug } = await params.params + // Fetch course metadata once const course_meta = await getCourseMetadata( - params.params.courseuuid, + courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null ) return ( diff --git a/apps/web/components/Objects/Courses/CourseActions/CourseActionsMobile.tsx b/apps/web/components/Objects/Courses/CourseActions/CourseActionsMobile.tsx index defb5045..5f0e2048 100644 --- a/apps/web/components/Objects/Courses/CourseActions/CourseActionsMobile.tsx +++ b/apps/web/components/Objects/Courses/CourseActions/CourseActionsMobile.tsx @@ -31,6 +31,7 @@ interface CourseRun { interface Course { id: string + course_uuid: string authors: Author[] trail?: { runs: CourseRun[] @@ -51,6 +52,7 @@ interface CourseActionsMobileProps { course: Course & { org_id: number } + trailData?: any } // Component for displaying multiple authors @@ -122,7 +124,7 @@ const MultipleAuthors = ({ authors }: { authors: Author[] }) => { ) } -const CourseActionsMobile = ({ courseuuid, orgslug, course }: CourseActionsMobileProps) => { +const CourseActionsMobile = ({ courseuuid, orgslug, course, trailData }: CourseActionsMobileProps) => { const router = useRouter() const session = useLHSession() as any const [linkedProducts, setLinkedProducts] = useState([]) @@ -131,9 +133,15 @@ const CourseActionsMobile = ({ courseuuid, orgslug, course }: CourseActionsMobil const [isModalOpen, setIsModalOpen] = useState(false) const [hasAccess, setHasAccess] = useState(null) - const isStarted = course.trail?.runs?.some( - (run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id - ) ?? false + // Clean up course UUID by removing 'course_' prefix if it exists + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + + const isStarted = trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ) ?? false; useEffect(() => { const fetchLinkedProducts = async () => { diff --git a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx index f2f45b3c..3839e756 100644 --- a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx +++ b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx @@ -3,7 +3,7 @@ import { removeCourse, startCourse } from '@services/courses/activity' import { revalidateTags } from '@services/utils/ts/requests' import { useRouter } from 'next/navigation' import { useLHSession } from '@components/Contexts/LHSessionContext' -import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' +import { getAPIUrl, getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getProductsByCourse } from '@services/payments/products' import { LogIn, LogOut, ShoppingCart, AlertCircle, UserPen, ClockIcon, ArrowRight, Sparkles, BookOpen } from 'lucide-react' import Modal from '@components/Objects/StyledElements/Modal/Modal' @@ -14,6 +14,8 @@ import toast from 'react-hot-toast' import { useContributorStatus } from '../../../../hooks/useContributorStatus' import CourseProgress from '../CourseProgress/CourseProgress' import UserAvatar from '@components/Objects/UserAvatar' +import { useOrg } from '@components/Contexts/OrgContext' +import { mutate } from 'swr' interface CourseRun { status: string @@ -26,6 +28,7 @@ interface CourseRun { interface Course { id: string + course_uuid: string trail?: { runs: CourseRun[] } @@ -46,9 +49,10 @@ interface CourseActionsProps { course: Course & { org_id: number } + trailData?: any } -function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) { +function CoursesActions({ courseuuid, orgslug, course, trailData }: CourseActionsProps) { const router = useRouter() const session = useLHSession() as any const [linkedProducts, setLinkedProducts] = useState([]) @@ -59,10 +63,17 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) { const [hasAccess, setHasAccess] = useState(null) const { contributorStatus, refetch } = useContributorStatus(courseuuid) const [isProgressOpen, setIsProgressOpen] = useState(false) + const org = useOrg() as any - const isStarted = course.trail?.runs?.some( - (run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id - ) ?? false + // Clean up course UUID by removing 'course_' prefix if it exists + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + + const isStarted = trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ) ?? false; useEffect(() => { const fetchLinkedProducts = async () => { @@ -120,12 +131,11 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) { try { if (isStarted) { await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token) - await revalidateTags(['courses'], orgslug) + mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`) toast.success('Successfully left the course', { id: loadingToast }) - router.refresh() } else { await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token) - await revalidateTags(['courses'], orgslug) + mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`) toast.success('Successfully started the course', { id: loadingToast }) // Get the first activity from the first chapter @@ -139,7 +149,7 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) { `/course/${courseuuid}/activity/${firstActivity.activity_uuid.replace('activity_', '')}` ) } else { - router.refresh() + mutate(`${getAPIUrl()}trail/org/${org?.id}/trail`) } } } catch (error) { @@ -262,10 +272,16 @@ function CoursesActions({ courseuuid, orgslug, course }: CourseActionsProps) { const renderProgressSection = () => { const totalActivities = course.chapters?.reduce((acc: number, chapter: any) => acc + chapter.activities.length, 0) || 0; - const completedActivities = course.trail?.runs?.find( - (run: CourseRun) => run.course_id === course.id - )?.steps?.filter((step) => step.complete)?.length || 0; + // Find the correct run using the cleaned UUID + const run = trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ); + + const completedActivities = run?.steps?.filter((step: any) => step.complete)?.length || 0; const progressPercentage = Math.round((completedActivities / totalActivities) * 100); if (!isStarted) { diff --git a/apps/web/components/Pages/Activity/ActivityChapterDropdown.tsx b/apps/web/components/Pages/Activity/ActivityChapterDropdown.tsx index 9e40af9c..a2bfff20 100644 --- a/apps/web/components/Pages/Activity/ActivityChapterDropdown.tsx +++ b/apps/web/components/Pages/Activity/ActivityChapterDropdown.tsx @@ -9,6 +9,7 @@ interface ActivityChapterDropdownProps { course: any currentActivityId: string orgslug: string + trailData?: any } export default function ActivityChapterDropdown(props: ActivityChapterDropdownProps): React.ReactNode { @@ -16,6 +17,9 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr const dropdownRef = React.useRef(null); const isMobile = useMediaQuery('(max-width: 768px)'); + // Clean up course UUID by removing 'course_' prefix if it exists + const cleanCourseUuid = props.course.course_uuid?.replace('course_', ''); + // Close dropdown when clicking outside React.useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -100,9 +104,20 @@ export default function ActivityChapterDropdown(props: ActivityChapterDropdownPr
{chapter.activities.map((activity: any) => { const cleanActivityUuid = activity.activity_uuid?.replace('activity_', ''); - const cleanCourseUuid = props.course.course_uuid?.replace('course_', ''); const isCurrent = cleanActivityUuid === props.currentActivityId.replace('activity_', ''); + // Find the correct run and check if activity is complete + const run = props.trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ); + + const isComplete = run?.steps?.find( + (step: any) => step.activity_id === activity.id && step.complete === true + ); + return (
- {props.course.trail?.runs?.find( - (run: any) => run.course_id === props.course.id - )?.steps?.find( - (step: any) => (step.activity_id === activity.id || step.activity_id === activity.activity_uuid) && step.complete === true - ) ? ( + {isComplete ? (
diff --git a/apps/web/components/Pages/Courses/ActivityIndicators.tsx b/apps/web/components/Pages/Courses/ActivityIndicators.tsx index 6d28b265..f5676681 100644 --- a/apps/web/components/Pages/Courses/ActivityIndicators.tsx +++ b/apps/web/components/Pages/Courses/ActivityIndicators.tsx @@ -12,6 +12,7 @@ interface Props { course_uuid: string current_activity?: string enableNavigation?: boolean + trailData?: any } // Helper functions @@ -109,8 +110,6 @@ function ActivityIndicators(props: Props) { const black_activity_style = 'bg-zinc-300 hover:bg-zinc-400' const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700' - const trail = props.course.trail - // Flatten all activities for navigation and rendering const allActivities = useMemo(() => { return course.chapters.flatMap((chapter: any) => @@ -131,14 +130,23 @@ function ActivityIndicators(props: Props) { // Memoize activity status checks const isActivityDone = useMemo(() => (activity: any) => { - let run = props.course.trail?.runs.find( - (run: any) => run.course_id == props.course.id - ) + // Clean up course UUID by removing 'course_' prefix if it exists + const cleanCourseUuid = course.course_uuid?.replace('course_', ''); + + let run = props.trailData?.runs?.find( + (run: any) => { + const cleanRunCourseUuid = run.course?.course_uuid?.replace('course_', ''); + return cleanRunCourseUuid === cleanCourseUuid; + } + ); + if (run) { - return run.steps.find((step: any) => step.activity_id == activity.id) + return run.steps.find( + (step: any) => step.activity_id === activity.id && step.complete === true + ); } - return false - }, [props.course]); + return false; + }, [props.trailData, course.course_uuid]); const isActivityCurrent = useMemo(() => (activity: any) => { let activity_uuid = activity.activity_uuid.replace('activity_', '')